import { Test, TestingModule } from '@nestjs/testing';
import { getRepositoryToken } from '@nestjs/typeorm';
import { IdempotencyRepository } from './idempotency.repository';
import { IdempotencyKey } from 'src/common/entities';
import { AppLoggerService } from 'src/common/services/logger.service';
import { IdempotencyStatusEnum } from 'src/common/enum/idempotency-status.enum';

describe('IdempotencyRepository', () => {
  let repository: IdempotencyRepository;
  const mockTypeOrmRepo = { query: jest.fn(), findOne: jest.fn(), update: jest.fn(), increment: jest.fn() };
  const mockLogger = { log: jest.fn(), error: jest.fn() };

  beforeEach(async () => {
    jest.clearAllMocks();

    const module: TestingModule = await Test.createTestingModule({
      providers: [
        IdempotencyRepository,
        { provide: getRepositoryToken(IdempotencyKey), useValue: mockTypeOrmRepo },
        { provide: AppLoggerService, useValue: mockLogger },
      ],
    }).compile();

    repository = module.get<IdempotencyRepository>(IdempotencyRepository);
  });

  describe('tryInsertPending', () => {
    it('returns the new row id when the insert wins the race', async () => {
      mockTypeOrmRepo.query.mockResolvedValue([{ id: 1 }]);

      const result = await repository.tryInsertPending('key-1', 'POST /registration', 'hash', 5);

      expect(result).toEqual({ id: 1 });
      expect(mockTypeOrmRepo.query).toHaveBeenCalledWith(
        expect.stringContaining('ON CONFLICT (idempotency_key, endpoint, user_id) DO NOTHING'),
        ['key-1', 'POST /registration', 'hash', IdempotencyStatusEnum.PENDING, 5],
      );
    });

    it('returns null when the row already exists (ON CONFLICT DO NOTHING found no rows)', async () => {
      mockTypeOrmRepo.query.mockResolvedValue([]);

      const result = await repository.tryInsertPending('key-1', 'POST /registration', 'hash', 5);

      expect(result).toBeNull();
    });
  });

  describe('findByKeyAndEndpoint', () => {
    it('scopes the lookup to the calling user, so two users can never see each other\'s claim', async () => {
      mockTypeOrmRepo.findOne.mockResolvedValue({ id: 1, status: IdempotencyStatusEnum.COMPLETED });

      const result = await repository.findByKeyAndEndpoint('key-1', 'POST /registration', 5);

      expect(result).toEqual({ id: 1, status: IdempotencyStatusEnum.COMPLETED });
      expect(mockTypeOrmRepo.findOne).toHaveBeenCalledWith({
        where: { idempotencyKey: 'key-1', endpoint: 'POST /registration', userId: 5 },
      });
    });
  });

  describe('incrementHitCount', () => {
    it('increments hit_count for the row', async () => {
      await repository.incrementHitCount(1);

      expect(mockTypeOrmRepo.increment).toHaveBeenCalledWith({ id: 1 }, 'hitCount', 1);
    });

    it('swallows increment errors so a logging/DB blip never blocks the claim decision', async () => {
      mockTypeOrmRepo.increment.mockRejectedValue(new Error('db down'));

      await expect(repository.incrementHitCount(1)).resolves.toBeUndefined();
      expect(mockLogger.error).toHaveBeenCalled();
    });
  });

  describe('tryReclaimFailed', () => {
    it('returns the row id when the CAS from failed back to pending succeeds', async () => {
      mockTypeOrmRepo.query.mockResolvedValue([{ id: 1 }]);

      const result = await repository.tryReclaimFailed('key-1', 'POST /registration', 'hash', 5);

      expect(result).toEqual({ id: 1 });
      expect(mockTypeOrmRepo.query).toHaveBeenCalledWith(expect.stringContaining('WHERE idempotency_key = $1'), [
        'key-1',
        'POST /registration',
        'hash',
        IdempotencyStatusEnum.PENDING,
        IdempotencyStatusEnum.FAILED,
        5,
      ]);
    });

    it('returns null when another concurrent retry already reclaimed the row', async () => {
      mockTypeOrmRepo.query.mockResolvedValue([]);

      const result = await repository.tryReclaimFailed('key-1', 'POST /registration', 'hash', 5);

      expect(result).toBeNull();
    });
  });

  describe('markCompleted / markFailed', () => {
    it('updates the row status and cached response on completion', async () => {
      await repository.markCompleted(1, { registrationId: 42 });

      expect(mockTypeOrmRepo.update).toHaveBeenCalledWith(
        { id: 1 },
        { status: IdempotencyStatusEnum.COMPLETED, responseData: { registrationId: 42 } },
      );
    });

    it('swallows update errors so a logging/DB blip never masks the real executor error', async () => {
      mockTypeOrmRepo.update.mockRejectedValue(new Error('db down'));

      await expect(repository.markFailed(1)).resolves.toBeUndefined();
      expect(mockLogger.error).toHaveBeenCalled();
    });
  });
});
