/**
 * Tests for the post-login routing decision in LoginForm.
 *
 * Decision logic (in handlePostLoginNavigation):
 *   - With `?continue=<url>` query param: POST /session with access token
 *     (withCredentials: true) → navigate full-page via window.location.href.
 *   - Without `?continue=`: Angular Router.navigateByUrl (current behavior).
 *
 * We test `handlePostLoginNavigation` directly to decouple from template
 * rendering, TranslocoDirective, and SignalForms dependencies.
 */
import { HttpClient } from '@angular/common/http';
import '@angular/compiler';
import { TestBed } from '@angular/core/testing';
import { ActivatedRoute, Router } from '@angular/router';
import { AuthenticationService } from '@aurora';
import { environment } from 'environments/environment';
import { of, throwError, type Observable } from 'rxjs';
import { vi } from 'vitest';

import { LoginForm, isSafeContinueUrl, resolveContinueNavigationTarget } from './login-form';
import { OAuthSessionService } from './oauth-session.service';

const makeActivatedRoute = (queryParams: Record<string, string> = {}) => ({
  snapshot: {
    queryParamMap: {
      get: (key: string) => queryParams[key] ?? null,
    },
  },
});

// Build continue URL from the configured API origin so validation passes in tests.
const API_ORIGIN = new URL(environment.api.rest).origin;
const CONTINUE_URL = `${API_ORIGIN}/api/o-auth/authorize?response_type=code&client_id=myapp&state=xyz`;

const EVIL_URL = 'https://evil.example/x';
const EVIL_URL_LOOK_ALIKE = 'https://evil.example/api/o-auth/authorize?foo=bar';

// ── isSafeContinueUrl unit tests ────────────────────────────────────────────

describe('isSafeContinueUrl', () => {
  const apiBase = 'http://localhost:8080/api';

  it('accepts a same-origin path starting with /api/o-auth/authorize', () => {
    expect(isSafeContinueUrl('/api/o-auth/authorize?foo=bar', apiBase)).toBe(true);
  });

  it('accepts an absolute URL whose origin matches the API and path starts with /api/o-auth/authorize', () => {
    expect(isSafeContinueUrl('http://localhost:8080/api/o-auth/authorize?foo=bar', apiBase)).toBe(
      true,
    );
  });

  it('rejects an off-origin absolute URL (evil.example)', () => {
    expect(isSafeContinueUrl('https://evil.example/x', apiBase)).toBe(false);
  });

  it('rejects an evil URL that has /api/o-auth/authorize path but wrong origin', () => {
    expect(isSafeContinueUrl('https://evil.example/api/o-auth/authorize?foo=bar', apiBase)).toBe(
      false,
    );
  });

  it('rejects a null value', () => {
    expect(isSafeContinueUrl(null, apiBase)).toBe(false);
  });

  it('rejects an empty string', () => {
    expect(isSafeContinueUrl('', apiBase)).toBe(false);
  });

  it('rejects a same-origin path not starting with /api/o-auth/authorize', () => {
    expect(isSafeContinueUrl('/dashboard', apiBase)).toBe(false);
  });
});

// ── resolveContinueNavigationTarget unit tests ───────────────────────────────

describe('resolveContinueNavigationTarget', () => {
  const backendBase = 'http://localhost:8080/api';
  const backendOrigin = 'http://localhost:8080';

  it('prepends the backend origin to a bare /api/o-auth/authorize path', () => {
    const result = resolveContinueNavigationTarget('/api/o-auth/authorize?foo=bar', backendBase);
    expect(result).toBe(`${backendOrigin}/api/o-auth/authorize?foo=bar`);
  });

  it('returns an already-absolute backend URL unchanged', () => {
    const absolute = 'http://localhost:8080/api/o-auth/authorize?response_type=code';
    expect(resolveContinueNavigationTarget(absolute, backendBase)).toBe(absolute);
  });

  it('returns the path as-is when apiRestBaseUrl is relative/same-origin (non-parseable as absolute)', () => {
    // When the app is same-origin (e.g. deployed behind a proxy), environment.api.rest
    // may be a relative path. In that case the frontend and backend share the origin
    // and no prefix is needed.
    const result = resolveContinueNavigationTarget('/api/o-auth/authorize?x=1', '/api');
    expect(result).toBe('/api/o-auth/authorize?x=1');
  });

  it('does NOT produce a :4200-origin URL when the backend is on a different port', () => {
    const path = '/api/o-auth/authorize?state=abc';
    const result = resolveContinueNavigationTarget(path, backendBase);
    expect(result.startsWith('http://localhost:4200')).toBe(false);
    expect(result.startsWith(backendOrigin)).toBe(true);
  });
});

// ── Without continue param ─────────────────────────────────────────────────

describe('LoginForm — handlePostLoginNavigation — without continue param', () => {
  let component: LoginForm;
  let navigateByUrlSpy: ReturnType<typeof vi.fn>;
  let establishSessionSpy: ReturnType<typeof vi.fn>;

  beforeEach(() => {
    navigateByUrlSpy = vi.fn().mockResolvedValue(true);
    establishSessionSpy = vi.fn();

    TestBed.configureTestingModule({
      providers: [
        LoginForm,
        { provide: Router, useValue: { navigateByUrl: navigateByUrlSpy } },
        { provide: OAuthSessionService, useValue: { establishSession: establishSessionSpy } },
        {
          provide: AuthenticationService,
          useValue: {
            signIn: vi.fn(),
            signInUsingRefreshToken: vi.fn(),
            signOut: vi.fn(),
            signUp: vi.fn(),
            forgotPassword: vi.fn(),
            resetPassword: vi.fn(),
            check: vi.fn(),
          },
        },
        // No 'continue' param — redirectURL present
        { provide: ActivatedRoute, useValue: makeActivatedRoute({ redirectURL: '/dashboard' }) },
        { provide: HttpClient, useValue: { post: vi.fn() } },
      ],
    });

    component = TestBed.inject(LoginForm);
  });

  it('should navigate via Router.navigateByUrl to redirectURL when continue is absent', () =>
    new Promise<void>((resolve) => {
      component.handlePostLoginNavigation('access-token-abc').subscribe({
        complete: () => {
          expect(establishSessionSpy).not.toHaveBeenCalled();
          expect(navigateByUrlSpy).toHaveBeenCalledWith('/dashboard');
          resolve();
        },
      });
    }));
});

// "no params at all" variant gets its own describe so beforeEach can configure cleanly
describe('LoginForm — handlePostLoginNavigation — no redirectURL and no continue', () => {
  let navigateByUrlSpy: ReturnType<typeof vi.fn>;

  beforeEach(() => {
    navigateByUrlSpy = vi.fn().mockResolvedValue(true);

    TestBed.configureTestingModule({
      providers: [
        LoginForm,
        { provide: Router, useValue: { navigateByUrl: navigateByUrlSpy } },
        { provide: OAuthSessionService, useValue: { establishSession: vi.fn() } },
        {
          provide: AuthenticationService,
          useValue: {
            signIn: vi.fn(),
            signInUsingRefreshToken: vi.fn(),
            signOut: vi.fn(),
            signUp: vi.fn(),
            forgotPassword: vi.fn(),
            resetPassword: vi.fn(),
            check: vi.fn(),
          },
        },
        { provide: ActivatedRoute, useValue: makeActivatedRoute({}) }, // no params at all
        { provide: HttpClient, useValue: { post: vi.fn() } },
      ],
    });
  });

  it('should navigate to "/" when no redirectURL and no continue are present', () =>
    new Promise<void>((resolve) => {
      const comp = TestBed.inject(LoginForm);

      comp.handlePostLoginNavigation('any-token').subscribe({
        complete: () => {
          expect(navigateByUrlSpy).toHaveBeenCalledWith('/');
          resolve();
        },
      });
    }));
});

// ── Open-redirect validation ────────────────────────────────────────────────

describe('LoginForm — handlePostLoginNavigation — open-redirect guard', () => {
  let component: LoginForm;
  let navigateByUrlSpy: ReturnType<typeof vi.fn>;
  let establishSessionSpy: ReturnType<typeof vi.fn>;

  const setupWith = (continueParam: string) => {
    navigateByUrlSpy = vi.fn().mockResolvedValue(true);
    establishSessionSpy = vi.fn().mockReturnValue(of(null));

    TestBed.configureTestingModule({
      providers: [
        LoginForm,
        { provide: Router, useValue: { navigateByUrl: navigateByUrlSpy } },
        { provide: OAuthSessionService, useValue: { establishSession: establishSessionSpy } },
        {
          provide: AuthenticationService,
          useValue: {
            signIn: vi.fn(),
            signInUsingRefreshToken: vi.fn(),
            signOut: vi.fn(),
            signUp: vi.fn(),
            forgotPassword: vi.fn(),
            resetPassword: vi.fn(),
            check: vi.fn(),
          },
        },
        { provide: ActivatedRoute, useValue: makeActivatedRoute({ continue: continueParam }) },
        { provide: HttpClient, useValue: { post: vi.fn() } },
      ],
    });

    component = TestBed.inject(LoginForm);
  };

  it('should NOT navigate to an off-allowlist continue URL (evil.example)', () =>
    new Promise<void>((resolve) => {
      setupWith(EVIL_URL);

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

      component.handlePostLoginNavigation('token').subscribe({
        complete: () => {
          // Must NOT navigate to the evil URL, must fall back to default route
          expect(assignedHref).not.toBe(EVIL_URL);
          expect(navigateByUrlSpy).toHaveBeenCalledWith('/');
          expect(establishSessionSpy).not.toHaveBeenCalled();
          vi.unstubAllGlobals();
          resolve();
        },
      });
    }));

  it('should NOT navigate to an evil.example URL that contains /api/o-auth/authorize path', () =>
    new Promise<void>((resolve) => {
      setupWith(EVIL_URL_LOOK_ALIKE);

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

      component.handlePostLoginNavigation('token').subscribe({
        complete: () => {
          expect(assignedHref).not.toBe(EVIL_URL_LOOK_ALIKE);
          expect(navigateByUrlSpy).toHaveBeenCalledWith('/');
          vi.unstubAllGlobals();
          resolve();
        },
      });
    }));
});

// ── With continue param ────────────────────────────────────────────────────

describe('LoginForm — handlePostLoginNavigation — with continue param', () => {
  let component: LoginForm;
  let navigateByUrlSpy: ReturnType<typeof vi.fn>;
  let establishSessionSpy: ReturnType<typeof vi.fn>;

  beforeEach(() => {
    navigateByUrlSpy = vi.fn().mockResolvedValue(true);
    establishSessionSpy = vi.fn();

    TestBed.configureTestingModule({
      providers: [
        LoginForm,
        { provide: Router, useValue: { navigateByUrl: navigateByUrlSpy } },
        { provide: OAuthSessionService, useValue: { establishSession: establishSessionSpy } },
        {
          provide: AuthenticationService,
          useValue: {
            signIn: vi.fn(),
            signInUsingRefreshToken: vi.fn(),
            signOut: vi.fn(),
            signUp: vi.fn(),
            forgotPassword: vi.fn(),
            resetPassword: vi.fn(),
            check: vi.fn(),
          },
        },
        { provide: ActivatedRoute, useValue: makeActivatedRoute({ continue: CONTINUE_URL }) },
        { provide: HttpClient, useValue: { post: vi.fn() } },
      ],
    });

    component = TestBed.inject(LoginForm);
  });

  it('should call OAuthSessionService.establishSession with the access token', () =>
    new Promise<void>((resolve) => {
      establishSessionSpy.mockReturnValue(of(null));
      vi.stubGlobal('location', { ...window.location, href: '' });

      component.handlePostLoginNavigation('my-access-token').subscribe({
        complete: () => {
          expect(establishSessionSpy).toHaveBeenCalledWith('my-access-token');
          vi.unstubAllGlobals();
          resolve();
        },
      });
    }));

  it('should navigate to the continue URL via window.location.href after session is established', () =>
    new Promise<void>((resolve) => {
      establishSessionSpy.mockReturnValue(of(null));

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

      component.handlePostLoginNavigation('my-access-token').subscribe({
        complete: () => {
          expect(assignedHref).toBe(CONTINUE_URL);
          vi.unstubAllGlobals();
          resolve();
        },
      });
    }));

  it('should NOT use Angular Router when continue is present', () =>
    new Promise<void>((resolve) => {
      establishSessionSpy.mockReturnValue(of(null));
      vi.stubGlobal('location', { ...window.location, href: '' });

      component.handlePostLoginNavigation('my-access-token').subscribe({
        complete: () => {
          expect(navigateByUrlSpy).not.toHaveBeenCalled();
          vi.unstubAllGlobals();
          resolve();
        },
      });
    }));

  it('should propagate errors from establishSession without calling Router', () =>
    new Promise<void>((resolve) => {
      const mockError = new Error('Session establishment failed');
      establishSessionSpy.mockReturnValue(throwError(() => mockError));

      component.handlePostLoginNavigation('bad-token').subscribe({
        next: () => {},
        error: (err: unknown) => {
          expect(err).toBe(mockError);
          expect(navigateByUrlSpy).not.toHaveBeenCalled();
          resolve();
        },
      });
    }));
});

// ── Bare-path continue navigates to backend origin ────────────────────────────

describe('LoginForm — handlePostLoginNavigation — bare-path continue targets backend origin', () => {
  it('navigates to the backend origin (not :4200) when continue is a bare /api/o-auth/authorize path', () =>
    new Promise<void>((resolve) => {
      const navigateByUrlSpy = vi.fn().mockResolvedValue(true);
      const establishSessionSpy = vi.fn().mockReturnValue(of(null));
      const barePath = '/api/o-auth/authorize?response_type=code&state=s1';
      const expectedTarget = `${new URL(environment.api.rest).origin}${barePath}`;

      TestBed.configureTestingModule({
        providers: [
          LoginForm,
          { provide: Router, useValue: { navigateByUrl: navigateByUrlSpy } },
          { provide: OAuthSessionService, useValue: { establishSession: establishSessionSpy } },
          {
            provide: AuthenticationService,
            useValue: {
              signIn: vi.fn(),
              signInUsingRefreshToken: vi.fn(),
              signOut: vi.fn(),
              signUp: vi.fn(),
              forgotPassword: vi.fn(),
              resetPassword: vi.fn(),
              check: vi.fn(),
            },
          },
          { provide: ActivatedRoute, useValue: makeActivatedRoute({ continue: barePath }) },
          { provide: HttpClient, useValue: { post: vi.fn() } },
        ],
      });

      const component = TestBed.inject(LoginForm);

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

      component.handlePostLoginNavigation('token').subscribe({
        complete: () => {
          // Must target the BACKEND origin, not the frontend origin (:4200)
          expect(assignedHref).toBe(expectedTarget);
          expect(assignedHref.startsWith('http://localhost:4200')).toBe(false);
          expect(navigateByUrlSpy).not.toHaveBeenCalled();
          vi.unstubAllGlobals();
          resolve();
        },
      });
    }));
});

// ── attemptAuthenticatedContinue — SSO fast-path ─────────────────────────────

/**
 * Helper: builds a TestBed config with configurable AuthenticationService.check
 * and OAuthSessionService.establishSession, plus a given ActivatedRoute.
 */
const makeAttemptAuthTestBed = (
  checkReturnValue: Observable<boolean>,
  establishSessionSpy: ReturnType<typeof vi.fn>,
  queryParams: Record<string, string>,
) => {
  const checkSpy = vi.fn().mockReturnValue(checkReturnValue);

  TestBed.configureTestingModule({
    providers: [
      LoginForm,
      { provide: Router, useValue: { navigateByUrl: vi.fn().mockResolvedValue(true) } },
      { provide: OAuthSessionService, useValue: { establishSession: establishSessionSpy } },
      {
        provide: AuthenticationService,
        useValue: {
          signIn: vi.fn(),
          signInUsingRefreshToken: vi.fn(),
          signOut: vi.fn(),
          signUp: vi.fn(),
          forgotPassword: vi.fn(),
          resetPassword: vi.fn(),
          check: checkSpy,
        },
      },
      { provide: ActivatedRoute, useValue: makeActivatedRoute(queryParams) },
      { provide: HttpClient, useValue: { post: vi.fn() } },
    ],
  });

  return TestBed.inject(LoginForm);
};

describe('LoginForm — attemptAuthenticatedContinue — SSO fast-path', () => {
  // Stub window.location.href as a settable property so we can observe navigations.
  let assignedHref: string;

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

  afterEach(() => {
    vi.unstubAllGlobals();
  });

  it('(1) authenticated + valid continue → calls establishSession with the access token AND navigates; emits true', () =>
    new Promise<void>((resolve) => {
      const establishSessionSpy = vi.fn().mockReturnValue(of(null));
      const component = makeAttemptAuthTestBed(of(true), establishSessionSpy, {
        continue: CONTINUE_URL,
      });

      // Prime localStorage so getAccessToken() returns a known token.
      const fakeToken = 'sso-access-token';
      localStorage.setItem('credentials', btoa(JSON.stringify({ accessToken: fakeToken })));

      component.attemptAuthenticatedContinue().subscribe({
        next: (result) => {
          expect(result).toBe(true);
          expect(establishSessionSpy).toHaveBeenCalledWith(fakeToken);
          expect(assignedHref).toBe(CONTINUE_URL);
        },
        complete: () => {
          localStorage.removeItem('credentials');
          resolve();
        },
      });
    }));

  it('(2) NOT authenticated + valid continue → does NOT call establishSession, does NOT navigate; emits false', () =>
    new Promise<void>((resolve) => {
      const establishSessionSpy = vi.fn();
      const component = makeAttemptAuthTestBed(of(false), establishSessionSpy, {
        continue: CONTINUE_URL,
      });

      component.attemptAuthenticatedContinue().subscribe({
        next: (result) => {
          expect(result).toBe(false);
          expect(establishSessionSpy).not.toHaveBeenCalled();
          expect(assignedHref).toBe('');
        },
        complete: resolve,
      });
    }));

  it('(3) no continue param → emits false without calling check or establishSession', () =>
    new Promise<void>((resolve) => {
      const establishSessionSpy = vi.fn();
      const component = makeAttemptAuthTestBed(of(true), establishSessionSpy, {});

      component.attemptAuthenticatedContinue().subscribe({
        next: (result) => {
          expect(result).toBe(false);
          expect(establishSessionSpy).not.toHaveBeenCalled();
          expect(assignedHref).toBe('');
        },
        complete: resolve,
      });
    }));

  it('(4) authenticated + UNSAFE continue → does NOT navigate; emits false', () =>
    new Promise<void>((resolve) => {
      const establishSessionSpy = vi.fn();
      const component = makeAttemptAuthTestBed(of(true), establishSessionSpy, {
        continue: EVIL_URL,
      });

      component.attemptAuthenticatedContinue().subscribe({
        next: (result) => {
          expect(result).toBe(false);
          expect(establishSessionSpy).not.toHaveBeenCalled();
          expect(assignedHref).toBe('');
        },
        complete: resolve,
      });
    }));

  it('(5) authenticated + valid continue but establishSession errors → does NOT navigate; emits false', () =>
    new Promise<void>((resolve) => {
      const sessionError = new Error('Session POST failed');
      const establishSessionSpy = vi.fn().mockReturnValue(throwError(() => sessionError));
      const component = makeAttemptAuthTestBed(of(true), establishSessionSpy, {
        continue: CONTINUE_URL,
      });

      // Prime localStorage with a token.
      localStorage.setItem('credentials', btoa(JSON.stringify({ accessToken: 'any-token' })));

      component.attemptAuthenticatedContinue().subscribe({
        next: (result) => {
          expect(result).toBe(false);
          expect(assignedHref).toBe('');
        },
        complete: () => {
          localStorage.removeItem('credentials');
          resolve();
        },
        error: () => {
          // Should NOT reach here — catchError swallows the error and emits false.
          localStorage.removeItem('credentials');
          throw new Error('attemptAuthenticatedContinue should not propagate errors');
        },
      });
    }));
});
