import { type Counter } from 'prom-client';
import { type LoggerService } from '@modules/shared';
import { type SearchResult } from '../../domain/repositories/search.repository';
import { type PostgresSearchRepository } from '../services/postgres-search.repository';
import { ResilientSearchRepository } from '../services/resilient-search.repository';
import { type TypesenseSearchRepository } from '../services/typesense-search.repository';

function createMockSearchResult(overrides?: Partial<SearchResult>): SearchResult {
  return {
    hits: [],
    totalFound: 0,
    page: 1,
    perPage: 20,
    totalPages: 0,
    searchTimeMs: 5,
    ...overrides,
  };
}

describe('ResilientSearchRepository', () => {
  let repository: ResilientSearchRepository;
  let mockTypesense: { [K in keyof TypesenseSearchRepository]: ReturnType<typeof vi.fn> };
  let mockPostgres: { [K in keyof PostgresSearchRepository]: ReturnType<typeof vi.fn> };
  let mockLogger: { log: ReturnType<typeof vi.fn>; warn: ReturnType<typeof vi.fn>; error: ReturnType<typeof vi.fn> };
  let mockCounter: { inc: ReturnType<typeof vi.fn> };

  beforeEach(() => {
    mockTypesense = {
      search: vi.fn(),
      indexDocument: vi.fn(),
      indexDocuments: vi.fn(),
      removeDocument: vi.fn(),
      ensureCollection: vi.fn(),
      dropCollection: vi.fn(),
    };
    mockPostgres = {
      search: vi.fn(),
      indexDocument: vi.fn(),
      indexDocuments: vi.fn(),
      removeDocument: vi.fn(),
      ensureCollection: vi.fn(),
      dropCollection: vi.fn(),
    };
    mockLogger = {
      log: vi.fn(),
      warn: vi.fn(),
      error: vi.fn(),
    };
    mockCounter = { inc: vi.fn() };

    repository = new ResilientSearchRepository(
      mockTypesense as unknown as TypesenseSearchRepository,
      mockPostgres as unknown as PostgresSearchRepository,
      mockLogger as unknown as LoggerService,
      mockCounter as unknown as Counter,
    );
  });

  describe('search', () => {
    it('uses Typesense when available', async () => {
      const expected = createMockSearchResult({ totalFound: 10 });
      mockTypesense.search.mockResolvedValue(expected);

      const result = await repository.search({ query: 'test' });

      expect(result).toEqual(expected);
      expect(mockTypesense.search).toHaveBeenCalledWith({ query: 'test' });
      expect(mockPostgres.search).not.toHaveBeenCalled();
    });

    it('falls back to PostgreSQL when Typesense fails', async () => {
      const pgResult = createMockSearchResult({ totalFound: 5 });
      mockTypesense.search.mockRejectedValue(new Error('ECONNREFUSED'));
      mockPostgres.search.mockResolvedValue(pgResult);

      const result = await repository.search({ query: 'test' });

      expect(result).toEqual(pgResult);
      expect(mockPostgres.search).toHaveBeenCalledWith({ query: 'test' });
      expect(mockCounter.inc).toHaveBeenCalledWith({ service: 'typesense', event: 'fallback_search' });
    });

    it('opens circuit after 3 consecutive failures and uses PG fallback', async () => {
      mockTypesense.search.mockRejectedValue(new Error('ECONNREFUSED'));
      const pgResult = createMockSearchResult({ totalFound: 3 });
      mockPostgres.search.mockResolvedValue(pgResult);

      // 3 failures to trip the breaker
      await repository.search({ query: 'a' });
      await repository.search({ query: 'b' });
      await repository.search({ query: 'c' });

      // Reset mock call counts
      mockTypesense.search.mockClear();
      mockPostgres.search.mockClear();

      // 4th call should not even try Typesense (circuit is OPEN)
      const result = await repository.search({ query: 'd' });
      expect(result).toEqual(pgResult);
      expect(mockTypesense.search).not.toHaveBeenCalled();
      expect(mockPostgres.search).toHaveBeenCalledWith({ query: 'd' });
    });

    it('recovers to Typesense after circuit resets', async () => {
      // Trip the circuit
      mockTypesense.search.mockRejectedValue(new Error('ECONNREFUSED'));
      const pgResult = createMockSearchResult({ totalFound: 2 });
      mockPostgres.search.mockResolvedValue(pgResult);

      for (let i = 0; i < 3; i++) {
        await repository.search({ query: `fail-${i}` });
      }

      // Now simulate Typesense recovery - we need to wait for the reset timeout
      // But since the breaker has a 30s default timeout, we use a different approach:
      // Create a new repository with a fast timeout
      const fastRepo = new (ResilientSearchRepository as any as new (...args: any[]) => ResilientSearchRepository)(
        mockTypesense as unknown as TypesenseSearchRepository,
        mockPostgres as unknown as PostgresSearchRepository,
        mockLogger as unknown as LoggerService,
        mockCounter as unknown as Counter,
      );

      // The new instance starts fresh, so Typesense calls should work
      const tsResult = createMockSearchResult({ totalFound: 10 });
      mockTypesense.search.mockResolvedValue(tsResult);

      const result = await fastRepo.search({ query: 'recovered' });
      expect(result).toEqual(tsResult);
    });
  });

  describe('indexDocument', () => {
    it('indexes via Typesense silently', async () => {
      const doc = { id: '1' } as any;
      mockTypesense.indexDocument.mockResolvedValue(undefined);

      await repository.indexDocument(doc);
      expect(mockTypesense.indexDocument).toHaveBeenCalledWith(doc);
    });

    it('swallows Typesense indexing errors and logs a warning', async () => {
      const doc = { id: '1' } as any;
      mockTypesense.indexDocument.mockRejectedValue(new Error('down'));

      await repository.indexDocument(doc);

      expect(mockLogger.warn).toHaveBeenCalledWith(
        expect.stringContaining('Typesense indexDocument failed'),
        'ResilientSearch',
      );
      expect(mockCounter.inc).toHaveBeenCalledWith({ service: 'typesense', event: 'index_failure' });
    });
  });

  describe('indexDocuments', () => {
    it('indexes batch via Typesense', async () => {
      const docs = [{ id: '1' }, { id: '2' }] as any[];
      mockTypesense.indexDocuments.mockResolvedValue(undefined);

      await repository.indexDocuments(docs);
      expect(mockTypesense.indexDocuments).toHaveBeenCalledWith(docs);
    });

    it('swallows batch indexing errors', async () => {
      mockTypesense.indexDocuments.mockRejectedValue(new Error('timeout'));

      await repository.indexDocuments([{ id: '1' }] as any[]);

      expect(mockLogger.warn).toHaveBeenCalledWith(
        expect.stringContaining('Typesense indexDocuments failed'),
        'ResilientSearch',
      );
    });
  });

  describe('ensureCollection', () => {
    it('records success on the circuit breaker', async () => {
      mockTypesense.ensureCollection.mockResolvedValue(undefined);

      await repository.ensureCollection();

      expect(mockTypesense.ensureCollection).toHaveBeenCalled();
    });

    it('logs warning but does not throw on failure', async () => {
      mockTypesense.ensureCollection.mockRejectedValue(new Error('no connection'));

      await repository.ensureCollection();

      expect(mockLogger.warn).toHaveBeenCalledWith(
        expect.stringContaining('Typesense ensureCollection failed'),
        'ResilientSearch',
      );
    });
  });
});
