/**
 * Unit tests for OAuthAuthorizeController and its exported parsing helpers.
 *
 * `parseHubSessionCookie` is the most attack-exposed parsing in the change: it reads
 * from an untrusted Cookie header and its output directly controls whether the user is
 * treated as authenticated. Every parsing path is exercised below.
 *
 * Controller-level tests use a mock `res` object (spies for setHeader / redirect) to
 * assert that the no-store cache header is always emitted — even though @Res() disables
 * the NestJS route-level @Header() metadata (passthrough: true is not set). The
 * controller must call res.setHeader() explicitly.
 *
 * @Auth/ESM limitation: decorating the controller class with NestJS + passport guards
 * breaks ts-jest (ESM-unresolved dependencies). The controller is therefore instantiated
 * directly (not via TestingModule) with mocked dependencies.
 */
import { OAuthAuthorizeController, parseHubSessionCookie } from './o-auth-authorize.controller';
import type { OAuthAuthorizeHandler } from '@app/o-auth/flows/application/authorize/o-auth-authorize.handler';
import type { OAuthHubSessionService } from '@app/o-auth/flows/application/session/o-auth-hub-session.service';
import type { Request, Response } from 'express';

// ---------------------------------------------------------------------------
// Controller-level tests: Cache-Control header and redirect behaviour
// ---------------------------------------------------------------------------

describe('OAuthAuthorizeController', () => {
  let controller: OAuthAuthorizeController;
  let mockHandler: jest.Mocked<Pick<OAuthAuthorizeHandler, 'main'>>;
  let mockSessionService: jest.Mocked<Pick<OAuthHubSessionService, 'verifyHubSession'>>;

  const makeRes = (): jest.Mocked<Pick<Response, 'setHeader' | 'redirect'>> => ({
    setHeader: jest.fn(),
    redirect: jest.fn(),
  });

  const makeReq = (cookie?: string): Pick<Request, 'headers' | 'originalUrl'> => ({
    headers: cookie ? { cookie } : {},
    originalUrl: '/api/o-auth/authorize?client_id=app',
  });

  beforeEach(() => {
    mockHandler = { main: jest.fn() };
    mockSessionService = { verifyHubSession: jest.fn() };

    controller = new OAuthAuthorizeController(
      mockHandler as unknown as OAuthAuthorizeHandler,
      mockSessionService as unknown as OAuthHubSessionService,
    );
  });

  it('sets Cache-Control: no-store before redirecting to sign-in (no session)', async () => {
    mockSessionService.verifyHubSession.mockReturnValue(null);
    mockHandler.main.mockResolvedValue({
      kind: 'redirect',
      signInUrl: 'http://localhost:4200/sign-in?continue=…',
    });

    const res = makeRes();
    const req = makeReq();

    await controller.main(
      'app', 'http://localhost:4200/callback', 'code', 'state123',
      undefined, 'challenge', 'S256',
      req as Request, res as unknown as Response,
    );

    expect(res.setHeader).toHaveBeenCalledWith('Cache-Control', 'no-store');
    expect(res.redirect).toHaveBeenCalledWith('http://localhost:4200/sign-in?continue=…');
  });

  it('sets Cache-Control: no-store before redirecting with code (valid session)', async () => {
    mockSessionService.verifyHubSession.mockReturnValue('account-uuid');
    mockHandler.main.mockResolvedValue({
      kind: 'code',
      redirectUrl: 'http://localhost:4200/callback?code=abc&state=state123',
    });

    const res = makeRes();
    const req = makeReq('hub_session=valid.jwt.token');

    await controller.main(
      'app', 'http://localhost:4200/callback', 'code', 'state123',
      undefined, 'challenge', 'S256',
      req as Request, res as unknown as Response,
    );

    expect(res.setHeader).toHaveBeenCalledWith('Cache-Control', 'no-store');
    expect(res.redirect).toHaveBeenCalledWith(
      'http://localhost:4200/callback?code=abc&state=state123',
    );
  });
});

// ---------------------------------------------------------------------------
// parseHubSessionCookie pure-function tests
// ---------------------------------------------------------------------------

describe('parseHubSessionCookie', () => {
  describe('when the cookie header is absent or falsy', () => {
    it('returns null for undefined', () => {
      expect(parseHubSessionCookie(undefined)).toBeNull();
    });

    it('returns null for an empty string', () => {
      expect(parseHubSessionCookie('')).toBeNull();
    });
  });

  describe('when hub_session is the only cookie', () => {
    it('extracts the value', () => {
      expect(parseHubSessionCookie('hub_session=abc123')).toBe('abc123');
    });

    it('decodes percent-encoded characters in the value', () => {
      // Cookies set via Set-Cookie are percent-encoded when they contain special chars.
      expect(parseHubSessionCookie('hub_session=hello%20world')).toBe('hello world');
    });
  });

  describe('when hub_session is not the first cookie', () => {
    it('extracts hub_session when preceded by another cookie (no space after semicolon)', () => {
      expect(parseHubSessionCookie('other=val;hub_session=token-xyz')).toBe('token-xyz');
    });

    it('extracts hub_session when preceded by another cookie (space after semicolon)', () => {
      expect(parseHubSessionCookie('other=val; hub_session=token-xyz')).toBe('token-xyz');
    });

    it('extracts hub_session when it is the middle cookie', () => {
      expect(
        parseHubSessionCookie('a=1; hub_session=middle-token; b=2'),
      ).toBe('middle-token');
    });

    it('extracts hub_session when it is the last cookie', () => {
      expect(
        parseHubSessionCookie('foo=bar; baz=qux; hub_session=last-token'),
      ).toBe('last-token');
    });

    it('extracts hub_session when multiple spaces follow the semicolon', () => {
      // The regex uses \s* so multiple spaces should still match.
      expect(parseHubSessionCookie('other=x;   hub_session=spaced')).toBe('spaced');
    });
  });

  describe('when hub_session is absent from the cookie header', () => {
    it('returns null when cookie header has unrelated cookies only', () => {
      expect(parseHubSessionCookie('session_id=abc; csrf=xyz')).toBeNull();
    });
  });

  describe('edge cases', () => {
    it('returns null when hub_session value is empty (hub_session=)', () => {
      // An empty cookie value is semantically absent; returning null is correct
      // because decodeURIComponent('') === '' and the regex requires [^;]+ (one or more chars).
      expect(parseHubSessionCookie('hub_session=')).toBeNull();
    });

    it('handles a cookie value that contains an equals sign (e.g. a JWT)', () => {
      // JWT values end with base64 padding '='; the regex [^;]+ captures everything
      // up to the next semicolon, so '=' inside the value is preserved.
      const jwt = 'eyJhbGciOiJSUzI1NiJ9.eyJzdWIiOiJ1c2VyIn0.sig==';
      expect(parseHubSessionCookie(`hub_session=${jwt}`)).toBe(jwt);
    });

    it('returns null for a malformed percent-encoded value (decodeURIComponent would throw)', () => {
      // 'abc%' is an incomplete percent-escape sequence: decodeURIComponent('abc%') throws
      // URIError. The function MUST return null (no session) instead of propagating the error.
      expect(parseHubSessionCookie('hub_session=abc%')).toBeNull();
    });

    it('returns null for a truncated percent-encoded sequence in a multi-cookie header', () => {
      // Same failure mode with surrounding cookies: must not throw.
      expect(parseHubSessionCookie('csrf=tok; hub_session=%GG; other=x')).toBeNull();
    });
  });
});
