import { OAuthCreateAccessTokenService } from '@app/o-auth/access-token/application/create/o-auth-create-access-token.service';
import { OAuthFindApplicationService } from '@app/o-auth/application';
import { OAuthCreateCredentialService } from '@app/o-auth/credential/application/create/o-auth-create-credential.service';
import { OAuthCreateRefreshTokenService } from '@app/o-auth/refresh-token/application/create/o-auth-create-refresh-token.service';
import {
  ACCOUNT_LOADER,
  CREDENTIAL_VERIFIER,
  JWT_SIGNER,
} from '@aurora/modules/authentication/domain/ports';
import { InternalServerErrorException } from '@nestjs/common';
import { Test, TestingModule } from '@nestjs/testing';
import {
  InvalidClientError,
  InvalidGrantError,
  UnauthorizedClientError,
} from '../../../domain/errors';
import type { OAuthCreateTokenPasswordInput } from '../../../domain/flows.types';
import { OAuthCreateTokenPasswordService } from './o-auth-create-token-password.service';

const ACCOUNT_ID = 'acc-1111-1111-1111-111111111111';
const CLIENT_ID = 'cli-1111-1111-1111-111111111111';
const APPLICATION_CODE = 'aurora';
const APPLICATION_SECRET = 'top-secret-12345';

const buildApplication = (overrides: Record<string, unknown> = {}) => ({
  id: 'app-1111-1111-1111-111111111111',
  code: APPLICATION_CODE,
  secret: APPLICATION_SECRET,
  clients: [{ id: CLIENT_ID, grantType: 'PASSWORD' }],
  ...overrides,
});

const buildInput = (
  overrides: Partial<OAuthCreateTokenPasswordInput> = {},
): OAuthCreateTokenPasswordInput => ({
  clientId: APPLICATION_CODE,
  clientSecret: APPLICATION_SECRET,
  username: 'carlos',
  password: 'hunter2',
  ipAddress: '127.0.0.1',
  userAgent: 'curl/8',
  ...overrides,
});

describe('OAuthCreateTokenPasswordService', () => {
  let handler: OAuthCreateTokenPasswordService;
  let credentialVerifier: { verify: jest.Mock };
  let accountLoader: { loadById: jest.Mock };
  let jwtSigner: { signAccess: jest.Mock; signRefresh: jest.Mock };
  let findApplication: { main: jest.Mock };
  let createAccessToken: { main: jest.Mock };
  let createRefreshToken: { main: jest.Mock };
  let createCredential: { main: jest.Mock };

  beforeEach(async () => {
    credentialVerifier = { verify: jest.fn().mockResolvedValue({ accountId: ACCOUNT_ID }) };
    accountLoader = { loadById: jest.fn().mockResolvedValue({ id: ACCOUNT_ID }) };
    jwtSigner = {
      signAccess: jest.fn().mockResolvedValue({
        token: 'header.payload.signature',
        expiresAt: new Date(Date.now() + 3600_000),
      }),
      signRefresh: jest.fn().mockResolvedValue({
        token: 'opaque-refresh-token-64-chars-XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX',
        expiresAt: new Date(Date.now() + 2_592_000_000),
      }),
    };
    findApplication = { main: jest.fn().mockResolvedValue(buildApplication()) };
    createAccessToken = { main: jest.fn().mockResolvedValue(undefined) };
    createRefreshToken = { main: jest.fn().mockResolvedValue(undefined) };
    createCredential = { main: jest.fn().mockResolvedValue(undefined) };

    const module: TestingModule = await Test.createTestingModule({
      providers: [
        OAuthCreateTokenPasswordService,
        { provide: CREDENTIAL_VERIFIER, useValue: credentialVerifier },
        { provide: ACCOUNT_LOADER, useValue: accountLoader },
        { provide: JWT_SIGNER, useValue: jwtSigner },
        { provide: OAuthFindApplicationService, useValue: findApplication },
        { provide: OAuthCreateAccessTokenService, useValue: createAccessToken },
        { provide: OAuthCreateRefreshTokenService, useValue: createRefreshToken },
        { provide: OAuthCreateCredentialService, useValue: createCredential },
      ],
    }).compile();

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

  describe('golden path', () => {
    it('returns the RFC 6749 §5.1 response shape on success', async () => {
      const result = await handler.main(buildInput());

      expect(result).toEqual({
        access_token: 'header.payload.signature',
        token_type: 'Bearer',
        expires_in: expect.any(Number),
        refresh_token: expect.stringMatching(/^opaque-refresh/),
      });
      expect(result.expires_in).toBeGreaterThan(3500);
      expect(result.expires_in).toBeLessThanOrEqual(3600);
    });

    it('persists rows with the RESOLVED technical client id', async () => {
      await handler.main(buildInput());

      expect(createAccessToken.main).toHaveBeenCalledTimes(1);
      expect(createRefreshToken.main).toHaveBeenCalledTimes(1);
      expect(createCredential.main).toHaveBeenCalledTimes(1);

      expect(createAccessToken.main).toHaveBeenCalledWith(
        expect.objectContaining({ clientId: CLIENT_ID, accountId: ACCOUNT_ID }),
      );
      expect(createCredential.main).toHaveBeenCalledWith(
        expect.objectContaining({
          grantType: 'PASSWORD',
          accountId: ACCOUNT_ID,
          ipAddress: '127.0.0.1',
          userAgent: 'curl/8',
        }),
      );
    });

    it('truncates userAgent longer than 255 chars', async () => {
      const longUA = 'a'.repeat(300);
      await handler.main(buildInput({ userAgent: longUA }));

      const credentialPayload = createCredential.main.mock.calls[0][0] as { userAgent: string };
      expect(credentialPayload.userAgent).toHaveLength(255);
    });
  });

  describe('invalid_client', () => {
    it('throws InvalidClientError when the application code is not found', async () => {
      findApplication.main.mockRejectedValue(new Error('NotFound'));

      await expect(handler.main(buildInput())).rejects.toBeInstanceOf(InvalidClientError);
      expect(credentialVerifier.verify).not.toHaveBeenCalled();
    });

    it('throws InvalidClientError when the application secret does not match', async () => {
      findApplication.main.mockResolvedValue(buildApplication({ secret: 'different-secret' }));

      await expect(handler.main(buildInput())).rejects.toBeInstanceOf(InvalidClientError);
      expect(credentialVerifier.verify).not.toHaveBeenCalled();
    });
  });

  describe('unauthorized_client', () => {
    it('throws UnauthorizedClientError when the application has no PASSWORD client', async () => {
      findApplication.main.mockResolvedValue(
        buildApplication({ clients: [{ id: 'other', grantType: 'REFRESH_TOKEN' }] }),
      );

      await expect(handler.main(buildInput())).rejects.toBeInstanceOf(UnauthorizedClientError);
      expect(credentialVerifier.verify).not.toHaveBeenCalled();
    });
  });

  describe('ambiguous configuration', () => {
    it('throws InternalServerErrorException when more than one PASSWORD client is linked', async () => {
      findApplication.main.mockResolvedValue(
        buildApplication({
          clients: [
            { id: 'cli-a', grantType: 'PASSWORD' },
            { id: 'cli-b', grantType: 'PASSWORD' },
          ],
        }),
      );

      await expect(handler.main(buildInput())).rejects.toBeInstanceOf(InternalServerErrorException);
      expect(credentialVerifier.verify).not.toHaveBeenCalled();
    });
  });

  describe('invalid_grant', () => {
    it('throws InvalidGrantError when credentialVerifier returns null (user not found / wrong password / inactive)', async () => {
      credentialVerifier.verify.mockResolvedValue(null);

      await expect(handler.main(buildInput())).rejects.toBeInstanceOf(InvalidGrantError);
      expect(createAccessToken.main).not.toHaveBeenCalled();
    });

    it('throws InvalidGrantError when accountLoader returns null after verify (race condition)', async () => {
      accountLoader.loadById.mockResolvedValue(null);

      await expect(handler.main(buildInput())).rejects.toBeInstanceOf(InvalidGrantError);
      expect(createAccessToken.main).not.toHaveBeenCalled();
    });
  });
});
