/**
 * Integration test: hybrid AI search (FTS + vector).
 * Verifies search() returns search_mode='hybrid_ai' when vector store is populated.
 */

import path from 'node:path';
import { beforeAll, describe, expect, it } from 'vitest';
import { EmbeddingPipeline } from '../../src/ai/embedding-pipeline.js';
import type { EmbeddingService } from '../../src/ai/interfaces.js';
import { BlobVectorStore } from '../../src/ai/vector-store.js';
import type { TraceMcpConfig } from '../../src/config.js';
import { IndexingPipeline } from '../../src/indexer/pipeline.js';
import { TypeScriptLanguagePlugin } from '../../src/indexer/plugins/language/typescript/index.js';
import { PluginRegistry } from '../../src/plugin-api/registry.js';
import { search } from '../../src/tools/navigation/navigation.js';
import { createTestStore } from '../test-utils.js';

const FIXTURE_DIR = path.resolve(__dirname, '../fixtures/no-framework');

function makeConfig(): TraceMcpConfig {
  return {
    root: FIXTURE_DIR,
    include: ['src/**/*.ts'],
    exclude: [],
    db: { path: ':memory:' },
    plugins: [],
  };
}

/** Deterministic mock — returns consistent vectors so similarity is meaningful */
function makeMockEmbedding(): EmbeddingService {
  const cache = new Map<string, number[]>();
  return {
    async embed(text: string) {
      if (cache.has(text)) return cache.get(text)!;
      // Simple hash-based vector for determinism
      const vec = Array.from({ length: 8 }, (_, i) => {
        let h = 0;
        for (const c of text) h = ((h << 5) - h + c.charCodeAt(0) + i) | 0;
        return (h % 1000) / 1000;
      });
      cache.set(text, vec);
      return vec;
    },
    async embedBatch(texts: string[]) {
      return Promise.all(texts.map((t) => this.embed(t)));
    },
    dimensions() {
      return 8;
    },
    modelName() {
      return 'mock-model';
    },
  };
}

describe('Hybrid AI search', () => {
  let store: ReturnType<typeof createTestStore>;
  let vectorStore: BlobVectorStore;
  let embeddingService: EmbeddingService;

  beforeAll(async () => {
    store = createTestStore();
    const db = store.db;
    const registry = new PluginRegistry();
    registry.registerLanguagePlugin(new TypeScriptLanguagePlugin());

    const config = makeConfig();
    const pipeline = new IndexingPipeline(store, registry, config, FIXTURE_DIR);
    await pipeline.indexAll();

    // Populate vector store
    vectorStore = new BlobVectorStore(db);
    embeddingService = makeMockEmbedding();
    const embeddingPipeline = new EmbeddingPipeline(store, embeddingService, vectorStore);
    await embeddingPipeline.indexUnembedded();

    expect(vectorStore.count()).toBeGreaterThan(0);
  });

  it('returns search_mode=fts when no AI options provided', async () => {
    const result = await search(store, 'add', {}, 10, 0);
    expect(result.search_mode).toBe('fts');
    expect(result.items.length).toBeGreaterThan(0);
  });

  it('returns search_mode=hybrid_ai when vector store + embedding service provided', async () => {
    const result = await search(store, 'add', {}, 10, 0, {
      vectorStore,
      embeddingService,
    });
    expect(result.search_mode).toBe('hybrid_ai');
    expect(result.items.length).toBeGreaterThan(0);
  });

  it('hybrid results include score and symbol data', async () => {
    const result = await search(store, 'User', {}, 10, 0, {
      vectorStore,
      embeddingService,
    });
    expect(result.items.length).toBeGreaterThan(0);
    for (const item of result.items) {
      expect(item.score).toBeGreaterThanOrEqual(0);
      expect(item.symbol).toBeDefined();
      expect(item.file).toBeDefined();
    }
  });

  it('hybrid results are sorted by score descending', async () => {
    const result = await search(store, 'User', {}, 20, 0, {
      vectorStore,
      embeddingService,
    });
    for (let i = 1; i < result.items.length; i++) {
      expect(result.items[i - 1].score).toBeGreaterThanOrEqual(result.items[i].score);
    }
  });

  it('FTS returns empty for nonsense query', async () => {
    const fts = await search(store, 'zzzznonexistent999', {}, 10, 0);
    expect(fts.items).toHaveLength(0);
    expect(fts.search_mode).toBe('fts');
  });

  it('hybrid may return results for nonsense (vector similarity is fuzzy)', async () => {
    const hybrid = await search(store, 'zzzznonexistent999', {}, 10, 0, {
      vectorStore,
      embeddingService,
    });
    // Vector search always finds nearest neighbors — we just verify it doesn't crash
    expect(hybrid.search_mode).toBe('hybrid_ai');
  });

  it('semantic="off" forces FTS even when AI is configured', async () => {
    const result = await search(
      store,
      'add',
      {},
      10,
      0,
      { vectorStore, embeddingService },
      undefined,
      { semantic: 'off' },
    );
    expect(result.search_mode).toBe('fts');
  });

  it('semantic="only" returns hybrid_ai mode and works on nonsense queries', async () => {
    const result = await search(
      store,
      'zzzznonexistent999',
      {},
      10,
      0,
      { vectorStore, embeddingService },
      undefined,
      { semantic: 'only' },
    );
    // Vector-only mode finds nearest neighbors regardless of FTS hits
    expect(result.search_mode).toBe('hybrid_ai');
  });

  it('semantic_weight=0 reproduces FTS-only ordering through the hybrid path', async () => {
    const ftsOnly = await search(store, 'User', {}, 10, 0);
    const weightedZero = await search(
      store,
      'User',
      {},
      10,
      0,
      { vectorStore, embeddingService },
      undefined,
      { semantic: 'on', semanticWeight: 0 },
    );
    // Same top result — the lexical signal dominates entirely
    expect(weightedZero.items.length).toBeGreaterThan(0);
    if (ftsOnly.items.length > 0 && weightedZero.items.length > 0) {
      expect(weightedZero.items[0].symbol.symbol_id).toBe(ftsOnly.items[0].symbol.symbol_id);
    }
  });

  it('semantic="auto" with no AI configured falls back to FTS', async () => {
    const result = await search(store, 'User', {}, 10, 0, undefined, undefined, {
      semantic: 'auto',
    });
    expect(result.search_mode).toBe('fts');
  });
});
