import { BadRequestException } from '@nestjs/common';
import { TokenExchangeService } from './token-exchange.service';
import type { AppEnvironment } from '../../configuration/src';

describe('TokenExchangeService.exchangeAuthorizationCode', () => {
  const environment: AppEnvironment = {
    databaseUrl: 'postgresql://u:p@postgres:5432/db',
    redisUrl: 'redis://redis:6379',
    oidcIssuer: 'https://idp.example.test',
    oidcAudience: 'client-1',
    oidcClientId: 'client-1',
    oidcClientSecret: 'super-secret',
    appVersion: 'dev',
    teamCityBuildNumber: 'local',
    sourceRevision: 'local',
  };

  function fakeDiscovery(
    tokenEndpoint = 'https://idp.example.test/oidc/token',
  ) {
    return { getTokenEndpoint: jest.fn().mockReturnValue(tokenEndpoint) };
  }

  it('posts a client-secret-authenticated request and returns only the safe fields', async () => {
    const fetchMock = jest.fn().mockResolvedValue({
      ok: true,
      json: () =>
        Promise.resolve({
          access_token: 'at-1',
          expires_in: 3600,
          refresh_token: 'rt-1',
          id_token: 'idt-1',
        }),
    });
    (globalThis as { fetch: typeof fetch }).fetch = fetchMock as never;

    const service = new TokenExchangeService(
      environment,
      fakeDiscovery() as never,
    );
    const result = await service.exchangeAuthorizationCode({
      code: 'auth-code-1',
      codeVerifier: 'verifier-1',
      redirectUri: 'http://localhost:4200/auth/callback',
    });

    expect(result).toEqual({ accessToken: 'at-1', expiresIn: 3600 });

    const [url, init] = fetchMock.mock.calls[0] as [string, RequestInit];
    expect(url).toBe('https://idp.example.test/oidc/token');
    const body = new URLSearchParams(init.body as string);
    expect(body.get('grant_type')).toBe('authorization_code');
    expect(body.get('client_id')).toBe('client-1');
    expect(body.get('client_secret')).toBe('super-secret');
    expect(body.get('code')).toBe('auth-code-1');
    expect(body.get('code_verifier')).toBe('verifier-1');
    expect(body.get('redirect_uri')).toBe(
      'http://localhost:4200/auth/callback',
    );
  });

  it('never leaks the refresh_token or id_token to the caller', async () => {
    const fetchMock = jest.fn().mockResolvedValue({
      ok: true,
      json: () =>
        Promise.resolve({
          access_token: 'at-1',
          expires_in: 3600,
          refresh_token: 'rt-1',
          id_token: 'idt-1',
        }),
    });
    (globalThis as { fetch: typeof fetch }).fetch = fetchMock as never;

    const service = new TokenExchangeService(
      environment,
      fakeDiscovery() as never,
    );
    const result = await service.exchangeAuthorizationCode({
      code: 'auth-code-1',
      codeVerifier: 'verifier-1',
      redirectUri: 'http://localhost:4200/auth/callback',
    });

    expect(result).not.toHaveProperty('refreshToken');
    expect(result).not.toHaveProperty('idToken');
  });

  it('rejects with BadRequestException when the identity provider rejects the code', async () => {
    const fetchMock = jest.fn().mockResolvedValue({
      ok: false,
      status: 400,
      json: () => Promise.resolve({ error: 'invalid_grant' }),
    });
    (globalThis as { fetch: typeof fetch }).fetch = fetchMock as never;

    const service = new TokenExchangeService(
      environment,
      fakeDiscovery() as never,
    );

    await expect(
      service.exchangeAuthorizationCode({
        code: 'bad-code',
        codeVerifier: 'verifier-1',
        redirectUri: 'http://localhost:4200/auth/callback',
      }),
    ).rejects.toThrow(BadRequestException);
  });
});
