import { OAuthCredentialGrantTypeEnum, type OAuthTokens } from '@api/graphql';
import { OAuthCreateTokenHandler } from '@app/o-auth/flows';
import { GqlThrottlerGuard } from '@aurora/modules';
import { Test, TestingModule } from '@nestjs/testing';
import type { Request } from 'express';
import { OAuthCreateTokenResolver } from './o-auth-create-token.resolver';

/**
 * Encodes a `clientId:clientSecret` pair as HTTP Basic auth header value (RFC 6749 §2.3.1).
 */
const basic = (clientId: string, clientSecret: string): string =>
  `Basic ${Buffer.from(`${clientId}:${clientSecret}`).toString('base64')}`;

const buildContext = (
  overrides: {
    ip?: string | null;
    userAgent?: string | null;
    authorization?: string;
  } = {},
): { req: Request } => ({
  req: {
    ip: overrides.ip ?? '127.0.0.1',
    headers: {
      'user-agent': overrides.userAgent ?? 'jest/test',
      ...(overrides.authorization !== undefined ? { authorization: overrides.authorization } : {}),
    },
  } as unknown as Request,
});

const buildHandlerResponse = (overrides: Partial<Record<string, unknown>> = {}) => ({
  access_token: 'header.payload.signature',
  refresh_token: 'opaque-refresh-token',
  token_type: 'Bearer' as const,
  expires_in: 3600,
  ...overrides,
});

describe('OAuthCreateTokenResolver (transport — pure mapping)', () => {
  let resolver: OAuthCreateTokenResolver;
  let handler: { main: jest.Mock };

  const buildModule = async (): Promise<void> => {
    handler = { main: jest.fn().mockResolvedValue(buildHandlerResponse()) };

    const module: TestingModule = await Test.createTestingModule({
      providers: [
        OAuthCreateTokenResolver,
        { provide: OAuthCreateTokenHandler, useValue: handler },
      ],
    })
      // GqlThrottlerGuard is wired on the mutation. Bypass it here — rate-limit behavior is an
      // integration concern (verified by the guard's own spec), not a transport-mapping concern.
      .overrideGuard(GqlThrottlerGuard)
      .useValue({ canActivate: () => true })
      .compile();

    resolver = module.get(OAuthCreateTokenResolver);
  };

  describe('Basic auth parsing', () => {
    it('decodes the Basic header into clientId + clientSecret', async () => {
      await buildModule();

      await resolver.oAuthCreateToken(
        {
          grantType: OAuthCredentialGrantTypeEnum.PASSWORD,
          username: 'carlos',
          password: 'hunter2',
        },
        buildContext({ authorization: basic('aurora-cli', 'top-secret-12345') }),
      );

      expect(handler.main).toHaveBeenCalledWith(
        expect.objectContaining({
          clientId: 'aurora-cli',
          clientSecret: 'top-secret-12345',
        }),
      );
    });

    it('forwards EMPTY client credentials when Authorization header is missing', async () => {
      await buildModule();

      await resolver.oAuthCreateToken(
        {
          grantType: OAuthCredentialGrantTypeEnum.PASSWORD,
          username: 'carlos',
          password: 'hunter2',
        },
        buildContext(),
      );

      expect(handler.main).toHaveBeenCalledWith(
        expect.objectContaining({ clientId: '', clientSecret: '' }),
      );
    });

    it('forwards EMPTY client credentials when Authorization scheme is not Basic', async () => {
      await buildModule();

      await resolver.oAuthCreateToken(
        {
          grantType: OAuthCredentialGrantTypeEnum.PASSWORD,
          username: 'carlos',
          password: 'hunter2',
        },
        buildContext({ authorization: 'Bearer some-token' }),
      );

      expect(handler.main).toHaveBeenCalledWith(
        expect.objectContaining({ clientId: '', clientSecret: '' }),
      );
    });
  });

  describe('payload mapping', () => {
    it('maps PASSWORD payload to canonical OAuthCreateTokenInput', async () => {
      await buildModule();

      await resolver.oAuthCreateToken(
        {
          grantType: OAuthCredentialGrantTypeEnum.PASSWORD,
          username: 'carlos',
          password: 'hunter2',
        },
        buildContext({ authorization: basic('cli', 'sec') }),
      );

      expect(handler.main).toHaveBeenCalledTimes(1);
      expect(handler.main).toHaveBeenCalledWith({
        grantType: 'PASSWORD',
        rawGrantType: 'PASSWORD',
        clientId: 'cli',
        clientSecret: 'sec',
        username: 'carlos',
        password: 'hunter2',
        refreshToken: undefined,
        ipAddress: '127.0.0.1',
        userAgent: 'jest/test',
      });
    });

    it('maps REFRESH_TOKEN payload to canonical OAuthCreateTokenInput', async () => {
      await buildModule();

      await resolver.oAuthCreateToken(
        {
          grantType: OAuthCredentialGrantTypeEnum.REFRESH_TOKEN,
          refreshToken: 'opaque-current-refresh',
        },
        buildContext({ authorization: basic('cli', 'sec') }),
      );

      expect(handler.main).toHaveBeenCalledWith(
        expect.objectContaining({
          grantType: 'REFRESH_TOKEN',
          refreshToken: 'opaque-current-refresh',
        }),
      );
    });

    it('maps AUTHORIZATION_CODE payload to canonical AUTHORIZATION_CODE', async () => {
      await buildModule();

      await resolver.oAuthCreateToken(
        {
          grantType: OAuthCredentialGrantTypeEnum.AUTHORIZATION_CODE,
          code: 'auth-code-abc',
          redirectUri: 'https://client.example.com/callback',
          codeVerifier: 'dBjftJeZ4CVP-mB92K27uhbUJU1p1r_wW1gFWFOEjXk',
        },
        buildContext({ authorization: basic('cli', 'sec') }),
      );

      expect(handler.main).toHaveBeenCalledWith(
        expect.objectContaining({
          grantType: 'AUTHORIZATION_CODE',
          rawGrantType: 'AUTHORIZATION_CODE',
          code: 'auth-code-abc',
          redirectUri: 'https://client.example.com/callback',
          codeVerifier: 'dBjftJeZ4CVP-mB92K27uhbUJU1p1r_wW1gFWFOEjXk',
        }),
      );
    });
  });

  describe('response mapping', () => {
    it('maps snake_case OAuthCreateTokenResponse into camelCase OAuthTokens', async () => {
      await buildModule();
      handler.main.mockResolvedValueOnce(
        buildHandlerResponse({
          access_token: 'aaa.bbb.ccc',
          refresh_token: 'refresh-xyz',
          token_type: 'Bearer',
          expires_in: 7200,
        }),
      );

      const result: OAuthTokens = await resolver.oAuthCreateToken(
        {
          grantType: OAuthCredentialGrantTypeEnum.PASSWORD,
          username: 'carlos',
          password: 'hunter2',
        },
        buildContext({ authorization: basic('cli', 'sec') }),
      );

      expect(result).toEqual({
        accessToken: 'aaa.bbb.ccc',
        refreshToken: 'refresh-xyz',
        tokenType: 'Bearer',
        expiresIn: 7200,
      });
    });
  });

  describe('request context extraction', () => {
    it('passes null ipAddress and userAgent when req has neither', async () => {
      await buildModule();

      await resolver.oAuthCreateToken(
        {
          grantType: OAuthCredentialGrantTypeEnum.PASSWORD,
          username: 'carlos',
          password: 'hunter2',
        },
        { req: { headers: { authorization: basic('cli', 'sec') } } as unknown as Request },
      );

      expect(handler.main).toHaveBeenCalledWith(
        expect.objectContaining({ ipAddress: null, userAgent: null }),
      );
    });

    it('passes through ipAddress and userAgent when present', async () => {
      await buildModule();

      await resolver.oAuthCreateToken(
        {
          grantType: OAuthCredentialGrantTypeEnum.PASSWORD,
          username: 'carlos',
          password: 'hunter2',
        },
        buildContext({
          authorization: basic('cli', 'sec'),
          ip: '203.0.113.42',
          userAgent: 'Mozilla/5.0',
        }),
      );

      expect(handler.main).toHaveBeenCalledWith(
        expect.objectContaining({ ipAddress: '203.0.113.42', userAgent: 'Mozilla/5.0' }),
      );
    });
  });

  describe('rate limiting', () => {
    it('guards the oAuthCreateToken mutation with GqlThrottlerGuard', () => {
      const guards =
        (Reflect.getMetadata(
          '__guards__',
          OAuthCreateTokenResolver.prototype.oAuthCreateToken,
        ) as unknown[]) ?? [];

      expect(guards).toContain(GqlThrottlerGuard);
    });
  });
});
