/**
 * Tests for authorizationCodeCallbackGuard.
 *
 * Focus: the catchError branch must NOT set window.location.href to
 * `/auth/login` (the infinite-loop footgun). On exchange failure the guard
 * must navigate to a safe terminal route via Angular Router.
 */
import { HttpClient } from '@angular/common/http';
import '@angular/compiler';
import { Injector, runInInjectionContext } from '@angular/core';
import { ActivatedRouteSnapshot, ParamMap, Router, UrlTree } from '@angular/router';
import { firstValueFrom, isObservable, of, throwError } from 'rxjs';
import { vi } from 'vitest';

vi.mock('environments/environment', () => ({
  environment: {
    api: { rest: 'http://localhost:8080/api', graphql: 'http://localhost:8080/graphql' },
    authStrategy: 'authorization-code' as const,
  },
}));

import { TokenStore } from '../shared/token-store.service';
import { authorizationCodeCallbackGuard } from './authorization-code-callback.guard';

function makeRoute(params: Record<string, string>): ActivatedRouteSnapshot {
  return {
    queryParamMap: {
      get: (key: string): string | null => params[key] ?? null,
      has: (key: string): boolean => key in params,
    } as unknown as ParamMap,
  } as unknown as ActivatedRouteSnapshot;
}

/**
 * The guard returns MaybeAsync<GuardResult>. In practice it always returns an
 * Observable, but we unwrap defensively so test code does not have to cast.
 */
async function runGuard(
  injector: Injector,
  route: ActivatedRouteSnapshot,
): Promise<boolean | UrlTree> {
  const result = runInInjectionContext(injector, () =>
    authorizationCodeCallbackGuard(route, null!),
  );
  if (isObservable(result)) {
    return firstValueFrom(result) as Promise<boolean | UrlTree>;
  }
  return result as boolean | UrlTree;
}

// ── Missing code param ────────────────────────────────────────────────────────

describe('authorizationCodeCallbackGuard — missing code param', () => {
  it('redirects to "/" when no code query param is present', async () => {
    const createUrlTree = vi.fn((commands: string[]) => commands as unknown as UrlTree);
    const injector = Injector.create({
      providers: [
        { provide: HttpClient, useValue: { post: vi.fn() } },
        { provide: Router, useValue: { createUrlTree, navigate: vi.fn() } },
        { provide: TokenStore, useValue: { setCredentials: vi.fn() } },
      ],
    });

    const route = makeRoute({}); // no code param
    const result = await runGuard(injector, route);

    expect(createUrlTree).toHaveBeenCalledWith(['/']);
    expect(result).toBeTruthy(); // UrlTree
  });
});

// ── Successful token exchange ─────────────────────────────────────────────────

describe('authorizationCodeCallbackGuard — successful token exchange', () => {
  it('stores credentials and redirects to redirectURL param on success', async () => {
    const tokens = { accessToken: 'access', refreshToken: 'refresh' };
    const setCredentials = vi.fn();
    const createUrlTree = vi.fn((commands: string[]) => commands as unknown as UrlTree);
    const httpPost = vi.fn().mockReturnValue(of(tokens));

    const injector = Injector.create({
      providers: [
        { provide: HttpClient, useValue: { post: httpPost } },
        { provide: Router, useValue: { createUrlTree, navigate: vi.fn() } },
        { provide: TokenStore, useValue: { setCredentials } },
      ],
    });

    const route = makeRoute({ code: 'code123', state: 'state1', redirectURL: '/dashboard' });
    await runGuard(injector, route);

    expect(setCredentials).toHaveBeenCalledWith(tokens);
    expect(createUrlTree).toHaveBeenCalledWith(['/dashboard']);
  });
});

// ── Failed token exchange — the critical invariant ────────────────────────────

describe('authorizationCodeCallbackGuard — failed token exchange', () => {
  it('does NOT set window.location.href on exchange failure (no infinite-loop relaunch)', async () => {
    const httpPost = vi.fn().mockReturnValue(throwError(() => new Error('exchange failed')));
    const createUrlTree = vi.fn((commands: string[]) => commands as unknown as UrlTree);

    let hrefWasSet = false;
    vi.stubGlobal('location', {
      ...window.location,
      set href(_v: string) {
        hrefWasSet = true;
      },
    });

    const injector = Injector.create({
      providers: [
        { provide: HttpClient, useValue: { post: httpPost } },
        { provide: Router, useValue: { createUrlTree, navigate: vi.fn() } },
        { provide: TokenStore, useValue: { setCredentials: vi.fn() } },
      ],
    });

    const route = makeRoute({ code: 'bad-code', state: 'state1' });
    await runGuard(injector, route);

    // The guard must NOT auto-relaunch the hub; only the user can initiate a new auth round-trip
    expect(hrefWasSet).toBe(false);
    vi.unstubAllGlobals();
  });

  it('does NOT set window.location.href to /auth/login on exchange failure', async () => {
    const httpPost = vi.fn().mockReturnValue(throwError(() => new Error('500')));
    const createUrlTree = vi.fn((commands: string[]) => commands as unknown as UrlTree);

    let assignedHref = '';
    vi.stubGlobal('location', {
      ...window.location,
      get href() {
        return assignedHref;
      },
      set href(v: string) {
        assignedHref = v;
      },
    });

    const injector = Injector.create({
      providers: [
        { provide: HttpClient, useValue: { post: httpPost } },
        { provide: Router, useValue: { createUrlTree, navigate: vi.fn() } },
        { provide: TokenStore, useValue: { setCredentials: vi.fn() } },
      ],
    });

    const route = makeRoute({ code: 'bad-code', state: 'state1' });
    await runGuard(injector, route);

    expect(assignedHref).not.toContain('/auth/login');
    vi.unstubAllGlobals();
  });

  it('returns the /callback-error UrlTree on exchange failure', async () => {
    const httpPost = vi.fn().mockReturnValue(throwError(() => new Error('500')));
    const errorUrlTree = Symbol('error-url-tree') as unknown as UrlTree;
    const createUrlTree = vi.fn((commands: string[]) => {
      if (Array.isArray(commands) && commands[0] === '/callback-error') {
        return errorUrlTree;
      }
      return commands as unknown as UrlTree;
    });

    const injector = Injector.create({
      providers: [
        { provide: HttpClient, useValue: { post: httpPost } },
        { provide: Router, useValue: { createUrlTree, navigate: vi.fn() } },
        { provide: TokenStore, useValue: { setCredentials: vi.fn() } },
      ],
    });

    const route = makeRoute({ code: 'bad-code', state: 'state1' });
    const result = await runGuard(injector, route);

    // Guard must navigate to /callback-error — not loop back to the hub
    expect(createUrlTree).toHaveBeenCalledWith(['/callback-error']);
    expect(result).toBe(errorUrlTree);
  });
});
