import { OAuthSequelizeAccessTokenRepository } from '@app/o-auth/access-token';
import { OAuthFindApplicationService } from '@app/o-auth/application';
import { OAuthSequelizeRefreshTokenRepository } from '@app/o-auth/refresh-token';
import { Test, TestingModule } from '@nestjs/testing';
import { InvalidClientError } from '../../../domain/errors';
import type { OAuthRevokeTokenInput } from '../../../domain/flows.types';
import { OAuthRevokeTokenService } from './o-auth-revoke-token.service';

const APPLICATION_CODE = 'aurora';
const APPLICATION_SECRET = 'top-secret-12345';
const ACCESS_TOKEN_ID = 'tok-1111-1111-1111-111111111111';
const REFRESH_TOKEN_ID = 'ref-1111-1111-1111-111111111111';

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

const buildInput = (overrides: Partial<OAuthRevokeTokenInput> = {}): OAuthRevokeTokenInput => ({
  clientId: APPLICATION_CODE,
  clientSecret: APPLICATION_SECRET,
  token: 'opaque-refresh-token-value',
  ...overrides,
});

describe('OAuthRevokeTokenService', () => {
  let service: OAuthRevokeTokenService;
  let findApplication: { main: jest.Mock };
  let refreshTokenRepo: { repository: { findOne: jest.Mock; update: jest.Mock } };
  let accessTokenRepo: { repository: { update: jest.Mock } };

  beforeEach(async () => {
    findApplication = { main: jest.fn().mockResolvedValue(buildApplication()) };
    refreshTokenRepo = {
      repository: {
        findOne: jest.fn().mockResolvedValue({
          id: REFRESH_TOKEN_ID,
          accessTokenId: ACCESS_TOKEN_ID,
          isRevoked: false,
        }),
        update: jest.fn().mockResolvedValue(undefined),
      },
    };
    accessTokenRepo = { repository: { update: jest.fn().mockResolvedValue(undefined) } };

    const module: TestingModule = await Test.createTestingModule({
      providers: [
        OAuthRevokeTokenService,
        { provide: OAuthFindApplicationService, useValue: findApplication },
        { provide: OAuthSequelizeRefreshTokenRepository, useValue: refreshTokenRepo },
        { provide: OAuthSequelizeAccessTokenRepository, useValue: accessTokenRepo },
      ],
    }).compile();

    service = module.get(OAuthRevokeTokenService);
  });

  describe('golden path', () => {
    it('revokes the refresh token and its paired access token, returns revoked=true', async () => {
      const result = await service.main(buildInput());

      expect(refreshTokenRepo.repository.update).toHaveBeenCalledWith(
        { isRevoked: true },
        { where: { id: REFRESH_TOKEN_ID } },
      );
      expect(accessTokenRepo.repository.update).toHaveBeenCalledWith(
        { isRevoked: true },
        { where: { id: ACCESS_TOKEN_ID } },
      );
      expect(result).toEqual({ revoked: true });
    });
  });

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

      await expect(service.main(buildInput())).rejects.toBeInstanceOf(InvalidClientError);
      expect(refreshTokenRepo.repository.findOne).not.toHaveBeenCalled();
    });

    it('throws InvalidClientError when the application secret mismatches', async () => {
      findApplication.main.mockResolvedValue(buildApplication({ secret: 'wrong' }));

      await expect(service.main(buildInput())).rejects.toBeInstanceOf(InvalidClientError);
      expect(refreshTokenRepo.repository.findOne).not.toHaveBeenCalled();
    });
  });

  describe('idempotent / non-enumerable', () => {
    it('returns revoked=true and performs no update when the token is unknown', async () => {
      refreshTokenRepo.repository.findOne.mockResolvedValue(null);

      const result = await service.main(buildInput());

      expect(result).toEqual({ revoked: true });
      expect(refreshTokenRepo.repository.update).not.toHaveBeenCalled();
      expect(accessTokenRepo.repository.update).not.toHaveBeenCalled();
    });

    it('returns revoked=true and performs no update when the token is already revoked', async () => {
      refreshTokenRepo.repository.findOne.mockResolvedValue({
        id: REFRESH_TOKEN_ID,
        accessTokenId: ACCESS_TOKEN_ID,
        isRevoked: true,
      });

      const result = await service.main(buildInput());

      expect(result).toEqual({ revoked: true });
      expect(refreshTokenRepo.repository.update).not.toHaveBeenCalled();
      expect(accessTokenRepo.repository.update).not.toHaveBeenCalled();
    });
  });
});
