/**
 * Unit tests for OAuthSessionController and its exported parsing helpers.
 *
 * `resolveAccessToken` is the most attack-exposed parsing in the session flow: it
 * determines which token (if any) the caller presented, and its output is passed
 * directly to JWT verification. Every parsing path is exercised below.
 *
 * Controller-level tests use a mock `res` object (spies for setHeader / status / send)
 * to assert that Cache-Control: no-store AND Pragma: no-cache headers are always emitted
 * explicitly — because @Res() without passthrough: true causes NestJS to skip route-level
 * @Header() metadata. The controller must call res.setHeader() for each header.
 *
 * @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 type { OAuthHubSessionService } from '@app/o-auth/flows/application/session/o-auth-hub-session.service';
import type { Response } from 'express';
import { OAuthSessionController, resolveAccessToken } from './o-auth-session.controller';

// ---------------------------------------------------------------------------
// Controller-level tests: Cache-Control + Pragma headers and 204 response
// ---------------------------------------------------------------------------

describe('OAuthSessionController', () => {
  let controller: OAuthSessionController;
  let mockSessionService: jest.Mocked<
    Pick<OAuthHubSessionService, 'verifyAccessTokenAndGetAccountId' | 'createSessionCookie'>
  >;

  const makeRes = (): jest.Mocked<Pick<Response, 'setHeader' | 'status' | 'send'>> => {
    const res = {
      setHeader: jest.fn(),
      status: jest.fn(),
      send: jest.fn(),
    };
    // res.status(204) must return an object with .send() — chain mock
    res.status.mockReturnValue(res);
    return res;
  };

  beforeEach(() => {
    mockSessionService = {
      verifyAccessTokenAndGetAccountId: jest.fn(),
      createSessionCookie: jest.fn(),
    };

    controller = new OAuthSessionController(
      mockSessionService as unknown as OAuthHubSessionService,
    );
  });

  it('sets Cache-Control: no-store and Pragma: no-cache before the 204 response', () => {
    mockSessionService.verifyAccessTokenAndGetAccountId.mockReturnValue('account-uuid');

    const res = makeRes();

    controller.establish(
      { access_token: 'valid.jwt.token' },
      undefined,
      res as unknown as Response,
    );

    expect(res.setHeader).toHaveBeenCalledWith('Cache-Control', 'no-store');
    expect(res.setHeader).toHaveBeenCalledWith('Pragma', 'no-cache');
    expect(res.status).toHaveBeenCalledWith(204);
    expect(res.send).toHaveBeenCalled();
  });

  it('sets Cache-Control: no-store and Pragma: no-cache when token is from the Authorization header', () => {
    mockSessionService.verifyAccessTokenAndGetAccountId.mockReturnValue('account-uuid');

    const res = makeRes();

    controller.establish({}, 'Bearer valid.jwt.token', res as unknown as Response);

    expect(res.setHeader).toHaveBeenCalledWith('Cache-Control', 'no-store');
    expect(res.setHeader).toHaveBeenCalledWith('Pragma', 'no-cache');
    expect(res.status).toHaveBeenCalledWith(204);
  });
});

// ---------------------------------------------------------------------------
// resolveAccessToken pure-function tests
// ---------------------------------------------------------------------------

describe('resolveAccessToken', () => {
  describe('body access_token (preferred source)', () => {
    it('returns the body token when access_token is a non-empty string', () => {
      expect(resolveAccessToken({ access_token: 'my-token' }, undefined)).toBe('my-token');
    });

    it('body takes precedence over the Authorization header', () => {
      // When both sources are present, body wins (D2: body is preferred).
      expect(resolveAccessToken({ access_token: 'body-token' }, 'Bearer header-token')).toBe(
        'body-token',
      );
    });

    it('falls through to the Authorization header when access_token is an empty string', () => {
      // An empty body token is treated as absent; the header is the fallback.
      expect(resolveAccessToken({ access_token: '' }, 'Bearer header-token')).toBe('header-token');
    });

    it('falls through to the Authorization header when access_token is not a string', () => {
      expect(resolveAccessToken({ access_token: 42 }, 'Bearer header-token')).toBe('header-token');
    });

    it('falls through to the Authorization header when access_token is absent from body', () => {
      expect(resolveAccessToken({}, 'Bearer fallback-token')).toBe('fallback-token');
    });
  });

  describe('Authorization Bearer header (fallback source)', () => {
    it('extracts the token from a well-formed Bearer header', () => {
      expect(resolveAccessToken({}, 'Bearer my-bearer-token')).toBe('my-bearer-token');
    });

    it('trims leading/trailing whitespace from the Bearer token', () => {
      // Some clients may add extra whitespace after "Bearer ".
      expect(resolveAccessToken({}, 'Bearer   spaced-token   ')).toBe('spaced-token');
    });

    it('returns null when Authorization header is missing entirely', () => {
      expect(resolveAccessToken({}, undefined)).toBeNull();
    });

    it('returns null when Authorization header does not start with "Bearer "', () => {
      expect(resolveAccessToken({}, 'Basic dXNlcjpwYXNz')).toBeNull();
    });

    it('returns null when the Bearer token part is empty (only "Bearer " with no value)', () => {
      // "Bearer " followed by only whitespace → empty token after trim → null.
      expect(resolveAccessToken({}, 'Bearer   ')).toBeNull();
    });

    it('returns null for a malformed header with no space after Bearer', () => {
      // Must start with exactly "Bearer " (with space) — "Bearer" alone does not match.
      expect(resolveAccessToken({}, 'Bearer')).toBeNull();
    });
  });

  describe('both sources absent', () => {
    it('returns null when body is empty and authorization is undefined', () => {
      expect(resolveAccessToken({}, undefined)).toBeNull();
    });

    it('returns null when body has unrelated keys and authorization is undefined', () => {
      expect(resolveAccessToken({ grant_type: 'authorization_code' }, undefined)).toBeNull();
    });
  });
});
