import { describe, it, expect, mock, beforeEach } from 'bun:test';
import { HybridSearchStrategy } from '../../../../src/services/worker/search/strategies/HybridSearchStrategy.js';
import type { StrategySearchOptions, ObservationSearchResult, SessionSummarySearchResult } from '../../../../src/services/worker/search/types.js';

const mockObservation1: ObservationSearchResult = {
  id: 1,
  memory_session_id: 'session-123',
  project: 'test-project',
  text: 'Test observation 1',
  type: 'decision',
  title: 'First Decision',
  subtitle: 'Subtitle 1',
  facts: '["fact1"]',
  narrative: 'Narrative 1',
  concepts: '["concept1"]',
  files_read: '["file1.ts"]',
  files_modified: '["file2.ts"]',
  prompt_number: 1,
  discovery_tokens: 100,
  created_at: '2025-01-01T12:00:00.000Z',
  created_at_epoch: Date.now() - 1000 * 60 * 60 * 24
};

const mockObservation2: ObservationSearchResult = {
  id: 2,
  memory_session_id: 'session-123',
  project: 'test-project',
  text: 'Test observation 2',
  type: 'bugfix',
  title: 'Second Bugfix',
  subtitle: 'Subtitle 2',
  facts: '["fact2"]',
  narrative: 'Narrative 2',
  concepts: '["concept2"]',
  files_read: '["file3.ts"]',
  files_modified: '["file4.ts"]',
  prompt_number: 2,
  discovery_tokens: 150,
  created_at: '2025-01-02T12:00:00.000Z',
  created_at_epoch: Date.now() - 1000 * 60 * 60 * 24 * 2
};

const mockObservation3: ObservationSearchResult = {
  id: 3,
  memory_session_id: 'session-456',
  project: 'test-project',
  text: 'Test observation 3',
  type: 'feature',
  title: 'Third Feature',
  subtitle: 'Subtitle 3',
  facts: '["fact3"]',
  narrative: 'Narrative 3',
  concepts: '["concept3"]',
  files_read: '["file5.ts"]',
  files_modified: '["file6.ts"]',
  prompt_number: 3,
  discovery_tokens: 200,
  created_at: '2025-01-03T12:00:00.000Z',
  created_at_epoch: Date.now() - 1000 * 60 * 60 * 24 * 3
};

const mockSession: SessionSummarySearchResult = {
  id: 1,
  memory_session_id: 'session-123',
  project: 'test-project',
  request: 'Test request',
  investigated: 'Test investigated',
  learned: 'Test learned',
  completed: 'Test completed',
  next_steps: 'Test next steps',
  files_read: '["file1.ts"]',
  files_edited: '["file2.ts"]',
  notes: 'Test notes',
  prompt_number: 1,
  discovery_tokens: 500,
  created_at: '2025-01-01T12:00:00.000Z',
  created_at_epoch: Date.now() - 1000 * 60 * 60 * 24
};

describe('HybridSearchStrategy', () => {
  let strategy: HybridSearchStrategy;
  let mockChromaSync: any;
  let mockSessionStore: any;
  let mockSessionSearch: any;

  beforeEach(() => {
    mockChromaSync = {
      queryChroma: mock(() => Promise.resolve({
        ids: [2, 1, 3], // Chroma returns in semantic relevance order
        distances: [0.1, 0.2, 0.3],
        metadatas: []
      }))
    };

    mockSessionStore = {
      getObservationsByIds: mock((ids: number[]) => {
        const allObs = [mockObservation1, mockObservation2, mockObservation3];
        return allObs.filter(obs => ids.includes(obs.id));
      }),
      getSessionSummariesByIds: mock(() => [mockSession]),
      getUserPromptsByIds: mock(() => [])
    };

    mockSessionSearch = {
      findByConcept: mock(() => [mockObservation1, mockObservation2, mockObservation3]),
      findByType: mock(() => [mockObservation1, mockObservation2]),
      findByFile: mock(() => ({
        observations: [mockObservation1, mockObservation2],
        sessions: [mockSession]
      }))
    };

    strategy = new HybridSearchStrategy(mockChromaSync, mockSessionStore, mockSessionSearch);
  });

  describe('canHandle', () => {
    it('should return true when concepts filter is present', () => {
      const options: StrategySearchOptions = {
        concepts: ['test-concept']
      };
      expect(strategy.canHandle(options)).toBe(true);
    });

    it('should return true when files filter is present', () => {
      const options: StrategySearchOptions = {
        files: ['/path/to/file.ts']
      };
      expect(strategy.canHandle(options)).toBe(true);
    });

    it('should return true when type and query are present', () => {
      const options: StrategySearchOptions = {
        type: 'decision',
        query: 'semantic query'
      };
      expect(strategy.canHandle(options)).toBe(true);
    });

    it('should return true when strategyHint is hybrid', () => {
      const options: StrategySearchOptions = {
        strategyHint: 'hybrid'
      };
      expect(strategy.canHandle(options)).toBe(true);
    });

    it('should return false for query-only (no filters)', () => {
      const options: StrategySearchOptions = {
        query: 'semantic query'
      };
      expect(strategy.canHandle(options)).toBe(false);
    });

    it('should return false for filter-only without Chroma', () => {
      const strategyNoChroma = new HybridSearchStrategy(null as any, mockSessionStore, mockSessionSearch);

      const options: StrategySearchOptions = {
        concepts: ['test-concept']
      };
      expect(strategyNoChroma.canHandle(options)).toBe(false);
    });
  });

  describe('search', () => {
    it('should return empty result for generic hybrid search without query', async () => {
      const options: StrategySearchOptions = {
        concepts: ['test-concept']
      };

      const result = await strategy.search(options);

      expect(result.results.observations).toHaveLength(0);
      expect(result.strategy).toBe('hybrid');
    });

    it('should return empty result for generic hybrid search (use specific methods)', async () => {
      const options: StrategySearchOptions = {
        query: 'test query'
      };

      const result = await strategy.search(options);

      expect(result.results.observations).toHaveLength(0);
    });
  });

  describe('findByConcept', () => {
    it('should combine metadata + semantic results', async () => {
      const options: StrategySearchOptions = {
        limit: 10
      };

      const result = await strategy.findByConcept('test-concept', options);

      expect(mockSessionSearch.findByConcept).toHaveBeenCalledWith('test-concept', expect.any(Object));
      expect(mockChromaSync.queryChroma).toHaveBeenCalledWith('test-concept', expect.any(Number));
      expect(result.usedChroma).toBe(true);
      expect(result.strategy).toBe('hybrid');
    });

    it('should preserve semantic ranking order from Chroma', async () => {

      const options: StrategySearchOptions = {
        limit: 10
      };

      const result = await strategy.findByConcept('test-concept', options);

      expect(result.results.observations.length).toBeGreaterThan(0);
      expect(result.results.observations[0].id).toBe(2);
    });

    it('should only include observations that match both metadata and Chroma', async () => {
      mockChromaSync.queryChroma = mock(() => Promise.resolve({
        ids: [2, 4, 5],
        distances: [0.1, 0.2, 0.3],
        metadatas: []
      }));

      const options: StrategySearchOptions = {
        limit: 10
      };

      const result = await strategy.findByConcept('test-concept', options);

      expect(result.results.observations).toHaveLength(1);
      expect(result.results.observations[0].id).toBe(2);
    });

    it('should return empty when no metadata matches', async () => {
      mockSessionSearch.findByConcept = mock(() => []);

      const options: StrategySearchOptions = {
        limit: 10
      };

      const result = await strategy.findByConcept('nonexistent-concept', options);

      expect(result.results.observations).toHaveLength(0);
      expect(mockChromaSync.queryChroma).not.toHaveBeenCalled(); 
    });

    it('should propagate Chroma error (fail-fast, no silent fallback)', async () => {
      mockChromaSync.queryChroma = mock(() => Promise.reject(new Error('Chroma failed')));

      const options: StrategySearchOptions = {
        limit: 10
      };

      await expect(
        strategy.findByConcept('test-concept', options)
      ).rejects.toThrow('Chroma failed');
    });
  });

  describe('findByType', () => {
    it('should find observations by type with semantic ranking', async () => {
      const options: StrategySearchOptions = {
        limit: 10
      };

      const result = await strategy.findByType('decision', options);

      expect(mockSessionSearch.findByType).toHaveBeenCalledWith('decision', expect.any(Object));
      expect(mockChromaSync.queryChroma).toHaveBeenCalled();
      expect(result.usedChroma).toBe(true);
    });

    it('should handle array of types', async () => {
      const options: StrategySearchOptions = {
        limit: 10
      };

      await strategy.findByType(['decision', 'bugfix'], options);

      expect(mockSessionSearch.findByType).toHaveBeenCalledWith(['decision', 'bugfix'], expect.any(Object));
      expect(mockChromaSync.queryChroma).toHaveBeenCalledWith('decision, bugfix', expect.any(Number));
    });

    it('should preserve Chroma ranking order for types', async () => {
      mockChromaSync.queryChroma = mock(() => Promise.resolve({
        ids: [2, 1], // Chroma order
        distances: [0.1, 0.2],
        metadatas: []
      }));

      const options: StrategySearchOptions = {
        limit: 10
      };

      const result = await strategy.findByType('decision', options);

      expect(result.results.observations[0].id).toBe(2);
    });

    it('should propagate Chroma error (fail-fast, no silent fallback)', async () => {
      mockChromaSync.queryChroma = mock(() => Promise.reject(new Error('Chroma unavailable')));

      const options: StrategySearchOptions = {
        limit: 10
      };

      await expect(
        strategy.findByType('bugfix', options)
      ).rejects.toThrow('Chroma unavailable');
    });

    it('should return empty when no metadata matches', async () => {
      mockSessionSearch.findByType = mock(() => []);

      const options: StrategySearchOptions = {
        limit: 10
      };

      const result = await strategy.findByType('nonexistent', options);

      expect(result.results.observations).toHaveLength(0);
    });
  });

  describe('findByFile', () => {
    it('should find observations and sessions by file path', async () => {
      const options: StrategySearchOptions = {
        limit: 10
      };

      const result = await strategy.findByFile('/path/to/file.ts', options);

      expect(mockSessionSearch.findByFile).toHaveBeenCalledWith('/path/to/file.ts', expect.any(Object));
      expect(result.observations.length).toBeGreaterThanOrEqual(0);
      expect(result.sessions).toHaveLength(1);
    });

    it('should return sessions without semantic ranking', async () => {
      const options: StrategySearchOptions = {
        limit: 10
      };

      const result = await strategy.findByFile('/path/to/file.ts', options);

      expect(result.sessions).toHaveLength(1);
      expect(result.sessions[0].id).toBe(1);
    });

    it('should apply semantic ranking only to observations', async () => {
      mockChromaSync.queryChroma = mock(() => Promise.resolve({
        ids: [2, 1], // Chroma ranking for observations
        distances: [0.1, 0.2],
        metadatas: []
      }));

      const options: StrategySearchOptions = {
        limit: 10
      };

      const result = await strategy.findByFile('/path/to/file.ts', options);

      expect(result.observations[0].id).toBe(2);
      expect(result.usedChroma).toBe(true);
    });

    it('should return usedChroma: false when no observations to rank', async () => {
      mockSessionSearch.findByFile = mock(() => ({
        observations: [],
        sessions: [mockSession]
      }));

      const options: StrategySearchOptions = {
        limit: 10
      };

      const result = await strategy.findByFile('/path/to/file.ts', options);

      expect(result.usedChroma).toBe(false);
      expect(result.sessions).toHaveLength(1);
    });

    it('should propagate Chroma error (fail-fast, no silent fallback)', async () => {
      mockChromaSync.queryChroma = mock(() => Promise.reject(new Error('Chroma down')));

      const options: StrategySearchOptions = {
        limit: 10
      };

      await expect(
        strategy.findByFile('/path/to/file.ts', options)
      ).rejects.toThrow('Chroma down');
    });
  });

  describe('strategy name', () => {
    it('should have name "hybrid"', () => {
      expect(strategy.name).toBe('hybrid');
    });
  });
});
