/**
 * Tests for SignOutComponent.
 *
 * The component must navigate differently depending on the configured auth strategy:
 *   - AUTH_STRATEGY.PASSWORD            → navigate to '/auth/sign-in' (local form).
 *   - AUTH_STRATEGY.AUTHORIZATION_CODE  → top-level navigation to the satellite's own BFF
 *                                         logout endpoint (`${api.rest}/auth/logout`) via
 *                                         window.location.href. The BFF (not the frontend)
 *                                         owns the hub URL (D2/D8).
 *
 * We test the exported pure functions (`signOutDestinationForStrategy`,
 * `buildBffLogoutUrl`) and the component behaviour so that changing the production
 * logic immediately breaks these tests.
 *
 * TDD: tests for the BFF-redirect navigation were written BEFORE the navigation target
 * was fixed from the (buggy) direct hub URL to the correct BFF endpoint.
 *
 * Task 6.2 (TDD): hub revocation tests written BEFORE the HttpClient.post wiring was added
 * to the component. They FAIL until the component reads the refresh token from TokenStore and
 * calls POST /auth/logout before delegating to authenticationService.signOut().
 */
import '@angular/compiler';
import { HttpClient } from '@angular/common/http';
import { TestBed } from '@angular/core/testing';
import { Router } from '@angular/router';
import { AuthenticationService, TokenStore } from '@aurora';
import { of, tap, throwError } from 'rxjs';
import { vi } from 'vitest';

vi.mock('environments/environment', () => ({
  environment: {
    api: { rest: 'http://localhost:8080/api', graphql: 'http://localhost:8080/graphql' },
    authStrategy: 'password' as 'password' | 'authorization-code',
    lang: { base: 'es', fallback: 'es', langs: ['es', 'en'] },
    oAuth: { applicationCode: 'aurora', applicationSecret: 'secret' },
    production: false,
    debug: false,
  },
}));

import { environment } from 'environments/environment';
import { AUTH_STRATEGY } from '../../../../environments/environment.type';
import SignOutComponent, {
  buildBffLogoutUrl,
  signOutDestinationForStrategy,
} from './sign-out.component';

// ── signOutDestinationForStrategy — password strategy ────────────────────────

describe('signOutDestinationForStrategy — password strategy', () => {
  beforeEach(() => {
    (environment as { authStrategy: string }).authStrategy = AUTH_STRATEGY.PASSWORD;
  });

  it('returns /auth/sign-in so the user lands on the local login form', () => {
    const destination = signOutDestinationForStrategy(AUTH_STRATEGY.PASSWORD);
    // This test FAILS if the condition is inverted in sign-out.component.ts.
    expect(destination).toBe('/auth/sign-in');
  });

  it('does NOT return null for password strategy (must use Angular Router)', () => {
    const destination = signOutDestinationForStrategy(AUTH_STRATEGY.PASSWORD);
    expect(destination).not.toBeNull();
  });
});

// ── signOutDestinationForStrategy — authorization-code strategy ───────────────

describe('signOutDestinationForStrategy — authorization-code strategy', () => {
  beforeEach(() => {
    (environment as { authStrategy: string }).authStrategy = AUTH_STRATEGY.AUTHORIZATION_CODE;
  });

  it('returns null so the component performs a top-level navigation to the hub instead of Angular Router', () => {
    const destination = signOutDestinationForStrategy(AUTH_STRATEGY.AUTHORIZATION_CODE);
    // Under AUTHORIZATION_CODE, sign-out navigates via window.location.href (hub bounce),
    // NOT via the Angular Router. Returning null signals that router navigation is skipped.
    expect(destination).toBeNull();
  });

  it('does NOT return /auth/sign-in in authorization-code mode (would trigger noLocalAuthGuard re-auth)', () => {
    const destination = signOutDestinationForStrategy(AUTH_STRATEGY.AUTHORIZATION_CODE);
    expect(destination).not.toBe('/auth/sign-in');
  });
});

// ── buildBffLogoutUrl — unit tests ───────────────────────────────────────────
//
// The frontend now navigates to the satellite's OWN BFF endpoint (D2/D8).
// The BFF owns the hub URL — the frontend just builds a simple same-origin URL.

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

  it('builds a URL targeting {bffRestBase}/auth/logout (the BFF endpoint, not the hub)', () => {
    const url = buildBffLogoutUrl(bffRestBase);
    expect(url).toBe('http://localhost:8080/api/auth/logout');
  });

  it('does NOT include /o-auth/logout (that is the hub endpoint, not the BFF)', () => {
    const url = buildBffLogoutUrl(bffRestBase);
    expect(url).not.toContain('/o-auth/logout');
  });

  it('does NOT include client_id (the BFF adds it, not the frontend)', () => {
    const url = buildBffLogoutUrl(bffRestBase);
    expect(url).not.toContain('client_id');
  });

  it('does NOT include post_logout_redirect_uri (the BFF adds it from its config)', () => {
    const url = buildBffLogoutUrl(bffRestBase);
    expect(url).not.toContain('post_logout_redirect_uri');
  });

  it('does NOT include state (the BFF generates it server-side)', () => {
    const url = buildBffLogoutUrl(bffRestBase);
    expect(url).not.toContain('state=');
  });
});

// ── SignOutComponent — authorization-code — navigates to BFF /auth/logout ─────
//
// Per D2/D8: the component navigates to the satellite's OWN BFF endpoint, NOT
// directly to the hub. The BFF (GET /auth/logout) issues the redirect to the hub.

describe('SignOutComponent — authorization-code strategy — navigates to BFF /auth/logout', () => {
  let assignedHref: string;

  beforeEach(() => {
    assignedHref = '';
    vi.stubGlobal('location', {
      ...window.location,
      get href() {
        return assignedHref;
      },
      set href(v: string) {
        assignedHref = v;
      },
      origin: 'http://localhost:4200',
    });
    (environment as { authStrategy: string }).authStrategy = AUTH_STRATEGY.AUTHORIZATION_CODE;
  });

  afterEach(() => {
    vi.unstubAllGlobals();
    (environment as { authStrategy: string }).authStrategy = AUTH_STRATEGY.PASSWORD;
  });

  it('navigates to the BFF /auth/logout URL via window.location.href (not Angular Router)', () =>
    new Promise<void>((resolve) => {
      const signOutSpy = vi.fn().mockReturnValue(of(true));
      const navigateSpy = vi.fn().mockResolvedValue(true);

      TestBed.configureTestingModule({
        providers: [
          SignOutComponent,
          { provide: AuthenticationService, useValue: { signOut: signOutSpy } },
          { provide: Router, useValue: { navigate: navigateSpy } },
          { provide: HttpClient, useValue: { post: vi.fn().mockReturnValue(of(null)) } },
          { provide: TokenStore, useValue: { refreshToken: vi.fn().mockReturnValue('token') } },
        ],
      });

      const component = TestBed.inject(SignOutComponent);
      component.ngOnInit();

      // Allow the Observable to flush (synchronous in this test because of(true))
      setTimeout(() => {
        expect(signOutSpy).toHaveBeenCalled();
        // Must navigate via window.location.href to the BFF, NOT Angular Router.
        // The BFF endpoint is /auth/logout (NOT /o-auth/logout — that is the hub).
        expect(assignedHref).toContain('/auth/logout');
        expect(assignedHref).not.toContain('/o-auth/logout');
        // Frontend must NOT add client_id, post_logout_redirect_uri, or state —
        // the BFF constructs those from its own backend config.
        expect(assignedHref).not.toContain('client_id=');
        expect(assignedHref).not.toContain('post_logout_redirect_uri=');
        expect(assignedHref).not.toContain('state=');
        expect(navigateSpy).not.toHaveBeenCalled();
        resolve();
      }, 0);
    }));

  it('clears local credentials (signOut completes) BEFORE performing the BFF navigation', () =>
    new Promise<void>((resolve) => {
      const callOrder: string[] = [];

      const signOutSpy = vi.fn().mockReturnValue(
        of(true).pipe(tap(() => callOrder.push('signOut'))),
      );

      const navigateSpy = vi.fn();

      // Re-stub with call-order tracking
      vi.stubGlobal('location', {
        ...window.location,
        get href() {
          return assignedHref;
        },
        set href(v: string) {
          callOrder.push('bffNavigation');
          assignedHref = v;
        },
        origin: 'http://localhost:4200',
      });

      TestBed.configureTestingModule({
        providers: [
          SignOutComponent,
          { provide: AuthenticationService, useValue: { signOut: signOutSpy } },
          { provide: Router, useValue: { navigate: navigateSpy } },
          { provide: HttpClient, useValue: { post: vi.fn().mockReturnValue(of(null)) } },
          { provide: TokenStore, useValue: { refreshToken: vi.fn().mockReturnValue('token') } },
        ],
      });

      const component = TestBed.inject(SignOutComponent);
      component.ngOnInit();

      setTimeout(() => {
        // signOut must appear before bffNavigation in the call order
        const signOutIndex = callOrder.indexOf('signOut');
        const navIndex = callOrder.indexOf('bffNavigation');
        expect(signOutIndex).toBeGreaterThanOrEqual(0);
        expect(navIndex).toBeGreaterThan(signOutIndex);
        resolve();
      }, 0);
    }));
});

// ── SignOutComponent — password strategy — uses Angular Router ────────────────

describe('SignOutComponent — password strategy — uses Angular Router', () => {
  beforeEach(() => {
    (environment as { authStrategy: string }).authStrategy = AUTH_STRATEGY.PASSWORD;
  });

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

  it('navigates via Angular Router to /auth/sign-in (not window.location.href)', () =>
    new Promise<void>((resolve) => {
      const signOutSpy = vi.fn().mockReturnValue(of(true));
      const navigateSpy = vi.fn().mockResolvedValue(true);

      let hrefWasSet = false;
      vi.stubGlobal('location', {
        ...window.location,
        get href() {
          return '';
        },
        set href(_v: string) {
          hrefWasSet = true;
        },
        origin: 'http://localhost:4200',
      });

      TestBed.configureTestingModule({
        providers: [
          SignOutComponent,
          { provide: AuthenticationService, useValue: { signOut: signOutSpy } },
          { provide: Router, useValue: { navigate: navigateSpy } },
          // HttpClient and TokenStore provided even for PASSWORD to satisfy DI,
          // but httpPost must NOT be called (asserted in Task 6.2 tests).
          { provide: HttpClient, useValue: { post: vi.fn() } },
          { provide: TokenStore, useValue: { refreshToken: vi.fn().mockReturnValue('') } },
        ],
      });

      const component = TestBed.inject(SignOutComponent);
      component.ngOnInit();

      setTimeout(() => {
        expect(navigateSpy).toHaveBeenCalledWith(['/auth/sign-in']);
        expect(hrefWasSet).toBe(false);
        resolve();
      }, 0);
    }));
});

// ── Task 5.3 — AUTHORIZATION_CODE navigates to BFF /auth/logout (NOT hub directly) ──
//
// These tests assert the NEW behavior: the sign-out component navigates to the
// satellite's own BFF endpoint (${api.rest}/auth/logout), NOT directly to the hub
// `o-auth/logout` endpoint. The BFF owns the hub URL (D2).
//
// TDD: these tests were written BEFORE the navigation target was fixed from
// `${api.rest}/o-auth/logout` to `${api.rest}/auth/logout`.

describe('Task 5.3 — AUTHORIZATION_CODE navigates to BFF /auth/logout (not hub directly)', () => {
  let assignedHref: string;

  beforeEach(() => {
    assignedHref = '';
    vi.stubGlobal('location', {
      ...window.location,
      get href() {
        return assignedHref;
      },
      set href(v: string) {
        assignedHref = v;
      },
      origin: 'http://localhost:4200',
    });
    (environment as { authStrategy: string }).authStrategy = AUTH_STRATEGY.AUTHORIZATION_CODE;
  });

  afterEach(() => {
    vi.unstubAllGlobals();
    (environment as { authStrategy: string }).authStrategy = AUTH_STRATEGY.PASSWORD;
  });

  it('navigates to ${api.rest}/auth/logout — the satellite BFF, not the hub directly', () =>
    new Promise<void>((resolve) => {
      const signOutSpy = vi.fn().mockReturnValue(of(true));
      const navigateSpy = vi.fn().mockResolvedValue(true);

      TestBed.configureTestingModule({
        providers: [
          SignOutComponent,
          { provide: AuthenticationService, useValue: { signOut: signOutSpy } },
          { provide: Router, useValue: { navigate: navigateSpy } },
          { provide: HttpClient, useValue: { post: vi.fn().mockReturnValue(of(null)) } },
          { provide: TokenStore, useValue: { refreshToken: vi.fn().mockReturnValue('token') } },
        ],
      });

      const component = TestBed.inject(SignOutComponent);
      component.ngOnInit();

      setTimeout(() => {
        // Must navigate to BFF /auth/logout (NOT /o-auth/logout).
        expect(assignedHref).toContain('/auth/logout');
        expect(assignedHref).not.toContain('/o-auth/logout');
        resolve();
      }, 0);
    }));

  it('navigation target does NOT contain the hub URL — the frontend must not know it', () =>
    new Promise<void>((resolve) => {
      const signOutSpy = vi.fn().mockReturnValue(of(true));
      const navigateSpy = vi.fn().mockResolvedValue(true);

      TestBed.configureTestingModule({
        providers: [
          SignOutComponent,
          { provide: AuthenticationService, useValue: { signOut: signOutSpy } },
          { provide: Router, useValue: { navigate: navigateSpy } },
          { provide: HttpClient, useValue: { post: vi.fn().mockReturnValue(of(null)) } },
          { provide: TokenStore, useValue: { refreshToken: vi.fn().mockReturnValue('token') } },
        ],
      });

      const component = TestBed.inject(SignOutComponent);
      component.ngOnInit();

      setTimeout(() => {
        // The frontend navigates to its OWN origin BFF — no hub URL should appear.
        // hub URL in this test env would be OAUTH_HUB_SERVER_URL (not in environment).
        // The env.api.rest is `http://localhost:8080/api` — the BFF URL is same origin.
        // Ensure no query params for client_id / post_logout_redirect_uri are added by
        // the frontend (the BFF constructs those itself).
        expect(assignedHref).toBe(`${environment.api.rest}/auth/logout`);
        resolve();
      }, 0);
    }));

  it('navigates AFTER signOut() completes (local credentials cleared first)', () =>
    new Promise<void>((resolve) => {
      const callOrder: string[] = [];

      const signOutSpy = vi.fn().mockReturnValue(
        of(true).pipe(tap(() => callOrder.push('signOut'))),
      );

      vi.stubGlobal('location', {
        ...window.location,
        get href() {
          return assignedHref;
        },
        set href(v: string) {
          callOrder.push('bffNavigation');
          assignedHref = v;
        },
        origin: 'http://localhost:4200',
      });

      TestBed.configureTestingModule({
        providers: [
          SignOutComponent,
          { provide: AuthenticationService, useValue: { signOut: signOutSpy } },
          { provide: Router, useValue: { navigate: vi.fn() } },
          { provide: HttpClient, useValue: { post: vi.fn().mockReturnValue(of(null)) } },
          { provide: TokenStore, useValue: { refreshToken: vi.fn().mockReturnValue('token') } },
        ],
      });

      const component = TestBed.inject(SignOutComponent);
      component.ngOnInit();

      setTimeout(() => {
        const signOutIndex = callOrder.indexOf('signOut');
        const navIndex = callOrder.indexOf('bffNavigation');
        expect(signOutIndex).toBeGreaterThanOrEqual(0);
        expect(navIndex).toBeGreaterThan(signOutIndex);
        resolve();
      }, 0);
    }));
});

// ── Task 6.2 — Hub revocation caller (TDD, Round 2) ───────────────────────────
//
// For AUTHORIZATION_CODE strategy, the component must:
//   1. Read the refresh token from TokenStore BEFORE local cleanup clears it.
//   2. Call POST {api.rest}/auth/logout with { refreshToken } (best-effort).
//   3. Proceed with signOut() (local cleanup) + BFF-redirect navigation.
//
// These tests were written BEFORE the implementation — they FAIL until the
// component injects HttpClient + TokenStore and performs the revocation call.
//
// HR-PORTS-001 compliance: the call lives in @app (this component), NOT in the
// @aurora adapter. The adapter stays framework-agnostic (revoke via GraphQL + clear).

describe('Task 6.2 — AUTHORIZATION_CODE strategy revokes hub token via POST /auth/logout', () => {
  const REFRESH_TOKEN = 'test-refresh-token-value';
  const EXPECTED_REVOKE_URL = `${environment.api.rest}/auth/logout`;

  let assignedHref: string;
  let httpPostSpy: ReturnType<typeof vi.fn>;

  beforeEach(() => {
    assignedHref = '';
    httpPostSpy = vi.fn().mockReturnValue(of(null));

    vi.stubGlobal('location', {
      ...window.location,
      get href() {
        return assignedHref;
      },
      set href(v: string) {
        assignedHref = v;
      },
      origin: 'http://localhost:4200',
    });
    (environment as { authStrategy: string }).authStrategy = AUTH_STRATEGY.AUTHORIZATION_CODE;
  });

  afterEach(() => {
    vi.unstubAllGlobals();
    (environment as { authStrategy: string }).authStrategy = AUTH_STRATEGY.PASSWORD;
  });

  it('calls POST {api.rest}/auth/logout with the refresh token for AUTHORIZATION_CODE strategy', () =>
    new Promise<void>((resolve) => {
      // TokenStore must return the refresh token BEFORE signOut clears it.
      const tokenStoreMock = { refreshToken: vi.fn().mockReturnValue(REFRESH_TOKEN) };
      const signOutSpy = vi.fn().mockReturnValue(of(true));
      const navigateSpy = vi.fn().mockResolvedValue(true);

      TestBed.configureTestingModule({
        providers: [
          SignOutComponent,
          { provide: AuthenticationService, useValue: { signOut: signOutSpy } },
          { provide: Router, useValue: { navigate: navigateSpy } },
          { provide: HttpClient, useValue: { post: httpPostSpy } },
          { provide: TokenStore, useValue: tokenStoreMock },
        ],
      });

      const component = TestBed.inject(SignOutComponent);
      component.ngOnInit();

      setTimeout(() => {
        // Must have called POST /auth/logout with the refresh token.
        expect(httpPostSpy).toHaveBeenCalledWith(
          EXPECTED_REVOKE_URL,
          { refreshToken: REFRESH_TOKEN },
          expect.anything(),
        );
        resolve();
      }, 0);
    }));

  it('reads the refresh token from TokenStore BEFORE signOut() clears it', () =>
    new Promise<void>((resolve) => {
      const callOrder: string[] = [];

      // TokenStore tracks when its refreshToken is read.
      const tokenStoreMock = {
        refreshToken: vi.fn().mockImplementation(() => {
          callOrder.push('readRefreshToken');
          return REFRESH_TOKEN;
        }),
      };

      // signOut tracks when it runs.
      const signOutSpy = vi.fn().mockReturnValue(
        of(true).pipe(tap(() => callOrder.push('signOut'))),
      );

      httpPostSpy = vi.fn().mockReturnValue(of(null));

      TestBed.configureTestingModule({
        providers: [
          SignOutComponent,
          { provide: AuthenticationService, useValue: { signOut: signOutSpy } },
          { provide: Router, useValue: { navigate: vi.fn() } },
          { provide: HttpClient, useValue: { post: httpPostSpy } },
          { provide: TokenStore, useValue: tokenStoreMock },
        ],
      });

      const component = TestBed.inject(SignOutComponent);
      component.ngOnInit();

      setTimeout(() => {
        // Token must be read before signOut() is called.
        const readIndex = callOrder.indexOf('readRefreshToken');
        const signOutIndex = callOrder.indexOf('signOut');
        expect(readIndex).toBeGreaterThanOrEqual(0);
        expect(signOutIndex).toBeGreaterThan(readIndex);
        resolve();
      }, 0);
    }));

  it('does NOT block navigation when the hub revocation call fails (best-effort)', () =>
    new Promise<void>((resolve) => {
      // Simulate a network failure on the revocation POST.
      const failingHttpPostSpy = vi.fn().mockReturnValue(throwError(() => new Error('Network error')));

      const tokenStoreMock = { refreshToken: vi.fn().mockReturnValue(REFRESH_TOKEN) };
      const signOutSpy = vi.fn().mockReturnValue(of(true));

      TestBed.configureTestingModule({
        providers: [
          SignOutComponent,
          { provide: AuthenticationService, useValue: { signOut: signOutSpy } },
          { provide: Router, useValue: { navigate: vi.fn() } },
          { provide: HttpClient, useValue: { post: failingHttpPostSpy } },
          { provide: TokenStore, useValue: tokenStoreMock },
        ],
      });

      const component = TestBed.inject(SignOutComponent);
      component.ngOnInit();

      setTimeout(() => {
        // Navigation must still proceed even when revocation fails.
        // signOut must have been called.
        expect(signOutSpy).toHaveBeenCalled();
        // window.location.href must have been assigned (BFF redirect).
        expect(assignedHref).toContain('/auth/logout');
        resolve();
      }, 0);
    }));

  it('does NOT call POST /auth/logout for PASSWORD strategy (only AUTHORIZATION_CODE)', () =>
    new Promise<void>((resolve) => {
      // Switch to password strategy.
      (environment as { authStrategy: string }).authStrategy = AUTH_STRATEGY.PASSWORD;

      const tokenStoreMock = { refreshToken: vi.fn().mockReturnValue(REFRESH_TOKEN) };
      const signOutSpy = vi.fn().mockReturnValue(of(true));
      const navigateSpy = vi.fn().mockResolvedValue(true);

      TestBed.configureTestingModule({
        providers: [
          SignOutComponent,
          { provide: AuthenticationService, useValue: { signOut: signOutSpy } },
          { provide: Router, useValue: { navigate: navigateSpy } },
          { provide: HttpClient, useValue: { post: httpPostSpy } },
          { provide: TokenStore, useValue: tokenStoreMock },
        ],
      });

      const component = TestBed.inject(SignOutComponent);
      component.ngOnInit();

      setTimeout(() => {
        // PASSWORD strategy must NOT call the hub revocation endpoint.
        expect(httpPostSpy).not.toHaveBeenCalled();
        resolve();
      }, 0);
    }));
});
