import { Test, TestingModule } from '@nestjs/testing';
import {
  InvalidClientError,
  InvalidRequestError,
  UnsupportedGrantTypeError,
} from '../../domain/errors';
import { OAuthCreateTokenAuthorizationCodeService } from './services/o-auth-create-token-authorization-code.service';
import { OAuthCreateTokenHandler } from './o-auth-create-token.handler';
import { OAuthCreateTokenPasswordService } from './services/o-auth-create-token-password.service';
import { OAuthCreateTokenRefreshService } from './services/o-auth-create-token-refresh.service';

const buildResponse = () => ({
  access_token: 'header.payload.signature',
  refresh_token: 'opaque-refresh',
  token_type: 'Bearer' as const,
  expires_in: 3600,
});

const CTX = {
  clientId: 'cli-1',
  clientSecret: 'sec-1',
  ipAddress: '127.0.0.1' as string | null,
  userAgent: 'jest/test' as string | null,
};

describe('OAuthCreateTokenHandler (composite)', () => {
  let handler: OAuthCreateTokenHandler;
  let passwordService: { main: jest.Mock };
  let refreshService: { main: jest.Mock };
  let authCodeService: { main: jest.Mock };

  beforeEach(async () => {
    passwordService = { main: jest.fn().mockResolvedValue(buildResponse()) };
    refreshService = { main: jest.fn().mockResolvedValue(buildResponse()) };
    authCodeService = { main: jest.fn().mockResolvedValue(buildResponse()) };

    const module: TestingModule = await Test.createTestingModule({
      providers: [
        OAuthCreateTokenHandler,
        { provide: OAuthCreateTokenPasswordService, useValue: passwordService },
        { provide: OAuthCreateTokenRefreshService, useValue: refreshService },
        { provide: OAuthCreateTokenAuthorizationCodeService, useValue: authCodeService },
      ],
    }).compile();

    handler = module.get(OAuthCreateTokenHandler);
  });

  describe('client-credential validation', () => {
    it('throws InvalidClientError when clientId is empty', async () => {
      await expect(
        handler.main({
          grantType: 'PASSWORD',
          clientId: '',
          clientSecret: 'sec',
          username: 'u',
          password: 'p',
          ipAddress: null,
          userAgent: null,
        }),
      ).rejects.toBeInstanceOf(InvalidClientError);
      expect(passwordService.main).not.toHaveBeenCalled();
      expect(refreshService.main).not.toHaveBeenCalled();
    });

    it('throws InvalidClientError when clientSecret is empty', async () => {
      await expect(
        handler.main({
          grantType: 'PASSWORD',
          clientId: 'cli',
          clientSecret: '',
          username: 'u',
          password: 'p',
          ipAddress: null,
          userAgent: null,
        }),
      ).rejects.toBeInstanceOf(InvalidClientError);
    });
  });

  describe('PASSWORD dispatch', () => {
    it('forwards the canonical input to the password sub-handler', async () => {
      await handler.main({
        grantType: 'PASSWORD',
        ...CTX,
        username: 'carlos',
        password: 'hunter2',
      });

      expect(passwordService.main).toHaveBeenCalledTimes(1);
      expect(passwordService.main).toHaveBeenCalledWith({
        clientId: 'cli-1',
        clientSecret: 'sec-1',
        username: 'carlos',
        password: 'hunter2',
        ipAddress: '127.0.0.1',
        userAgent: 'jest/test',
      });
      expect(refreshService.main).not.toHaveBeenCalled();
    });

    it('returns whatever the sub-handler returns', async () => {
      passwordService.main.mockResolvedValueOnce({
        access_token: 'X',
        refresh_token: 'Y',
        token_type: 'Bearer',
        expires_in: 100,
      });

      const result = await handler.main({
        grantType: 'PASSWORD',
        ...CTX,
        username: 'u',
        password: 'p',
      });

      expect(result).toEqual({
        access_token: 'X',
        refresh_token: 'Y',
        token_type: 'Bearer',
        expires_in: 100,
      });
    });

    it('throws InvalidRequestError when username is missing', async () => {
      await expect(
        handler.main({ grantType: 'PASSWORD', ...CTX, password: 'p' }),
      ).rejects.toBeInstanceOf(InvalidRequestError);
      expect(passwordService.main).not.toHaveBeenCalled();
    });

    it('throws InvalidRequestError when password is missing', async () => {
      await expect(
        handler.main({ grantType: 'PASSWORD', ...CTX, username: 'u' }),
      ).rejects.toBeInstanceOf(InvalidRequestError);
      expect(passwordService.main).not.toHaveBeenCalled();
    });

    it('throws InvalidRequestError when username is whitespace-only', async () => {
      await expect(
        handler.main({ grantType: 'PASSWORD', ...CTX, username: '   ', password: 'p' }),
      ).rejects.toBeInstanceOf(InvalidRequestError);
    });
  });

  describe('REFRESH_TOKEN dispatch', () => {
    it('forwards the canonical input to the refresh sub-handler', async () => {
      await handler.main({
        grantType: 'REFRESH_TOKEN',
        ...CTX,
        refreshToken: 'opaque-current',
      });

      expect(refreshService.main).toHaveBeenCalledTimes(1);
      expect(refreshService.main).toHaveBeenCalledWith({
        clientId: 'cli-1',
        clientSecret: 'sec-1',
        refreshToken: 'opaque-current',
        ipAddress: '127.0.0.1',
        userAgent: 'jest/test',
      });
      expect(passwordService.main).not.toHaveBeenCalled();
    });

    it('throws InvalidRequestError when refreshToken is missing', async () => {
      await expect(handler.main({ grantType: 'REFRESH_TOKEN', ...CTX })).rejects.toBeInstanceOf(
        InvalidRequestError,
      );
      expect(refreshService.main).not.toHaveBeenCalled();
    });

    it('throws InvalidRequestError when refreshToken is whitespace-only', async () => {
      await expect(
        handler.main({ grantType: 'REFRESH_TOKEN', ...CTX, refreshToken: '   ' }),
      ).rejects.toBeInstanceOf(InvalidRequestError);
    });
  });

  describe('AUTHORIZATION_CODE dispatch', () => {
    const AUTH_CODE_INPUT = {
      code: 'auth-code-abc',
      redirectUri: 'https://client.example.com/callback',
      codeVerifier: 'dBjftJeZ4CVP-mB92K27uhbUJU1p1r_wW1gFWFOEjXk',
    };

    it('forwards the canonical input to the authorization-code sub-service', async () => {
      await handler.main({
        grantType: 'AUTHORIZATION_CODE',
        ...CTX,
        ...AUTH_CODE_INPUT,
      });

      expect(authCodeService.main).toHaveBeenCalledTimes(1);
      expect(authCodeService.main).toHaveBeenCalledWith({
        clientId: 'cli-1',
        clientSecret: 'sec-1',
        code: 'auth-code-abc',
        redirectUri: 'https://client.example.com/callback',
        codeVerifier: 'dBjftJeZ4CVP-mB92K27uhbUJU1p1r_wW1gFWFOEjXk',
        ipAddress: '127.0.0.1',
        userAgent: 'jest/test',
      });
      expect(passwordService.main).not.toHaveBeenCalled();
      expect(refreshService.main).not.toHaveBeenCalled();
    });

    it('returns whatever the sub-service returns', async () => {
      authCodeService.main.mockResolvedValueOnce({
        access_token: 'AC-token',
        refresh_token: 'AC-refresh',
        token_type: 'Bearer',
        expires_in: 600,
      });

      const result = await handler.main({
        grantType: 'AUTHORIZATION_CODE',
        ...CTX,
        ...AUTH_CODE_INPUT,
      });

      expect(result).toEqual({
        access_token: 'AC-token',
        refresh_token: 'AC-refresh',
        token_type: 'Bearer',
        expires_in: 600,
      });
    });

    it('throws InvalidRequestError when code is missing', async () => {
      await expect(
        handler.main({
          grantType: 'AUTHORIZATION_CODE',
          ...CTX,
          redirectUri: 'https://client.example.com/callback',
          codeVerifier: 'verifier',
        }),
      ).rejects.toBeInstanceOf(InvalidRequestError);
      expect(authCodeService.main).not.toHaveBeenCalled();
    });

    it('throws InvalidRequestError when redirectUri is missing', async () => {
      await expect(
        handler.main({ grantType: 'AUTHORIZATION_CODE', ...CTX, code: 'abc', codeVerifier: 'v' }),
      ).rejects.toBeInstanceOf(InvalidRequestError);
      expect(authCodeService.main).not.toHaveBeenCalled();
    });

    it('throws InvalidRequestError when codeVerifier is missing', async () => {
      await expect(
        handler.main({
          grantType: 'AUTHORIZATION_CODE',
          ...CTX,
          code: 'abc',
          redirectUri: 'https://client.example.com/callback',
        }),
      ).rejects.toBeInstanceOf(InvalidRequestError);
      expect(authCodeService.main).not.toHaveBeenCalled();
    });
  });

  describe('UNSUPPORTED dispatch', () => {
    it('throws UnsupportedGrantTypeError and invokes no sub-handler', async () => {
      await expect(
        handler.main({
          grantType: 'UNSUPPORTED',
          rawGrantType: 'client_credentials',
          ...CTX,
        }),
      ).rejects.toBeInstanceOf(UnsupportedGrantTypeError);
      expect(passwordService.main).not.toHaveBeenCalled();
      expect(refreshService.main).not.toHaveBeenCalled();
      expect(authCodeService.main).not.toHaveBeenCalled();
    });
  });
});
