import path from 'node:path';
import { beforeAll, describe, expect, it } from 'vitest';
import type { TraceMcpConfig } from '../../src/config.js';
import type { Store } from '../../src/db/store.js';
import { IndexingPipeline } from '../../src/indexer/pipeline.js';
import { PhpLanguagePlugin } from '../../src/indexer/plugins/language/php/index.js';
import { TypeScriptLanguagePlugin } from '../../src/indexer/plugins/language/typescript/index.js';
import { VueLanguagePlugin } from '../../src/indexer/plugins/language/vue/index.js';
import { PluginRegistry } from '../../src/plugin-api/registry.js';
import { getFileOutline, getSymbol, 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: ['app/**/*.php', 'src/**/*.ts', 'components/**/*.vue'],
    exclude: ['vendor/**', 'node_modules/**'],
    db: { path: ':memory:' },
    plugins: [],
  };
}

describe('Navigation tools', () => {
  let store: Store;

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

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

  describe('search()', () => {
    it('finds symbols by name', async () => {
      const result = await search(store, 'User');
      expect(result.items.length).toBeGreaterThan(0);
      expect(result.items.some((r) => r.symbol.name === 'User')).toBe(true);
    });

    it('finds functions by name', async () => {
      const result = await search(store, 'add');
      expect(result.items.length).toBeGreaterThan(0);
      expect(result.items.some((r) => r.symbol.name === 'add')).toBe(true);
    });

    it('filters by kind', async () => {
      const result = await search(store, 'User', { kind: 'class' });
      for (const item of result.items) {
        expect(item.symbol.kind).toBe('class');
      }
    });

    it('filters by language', async () => {
      const result = await search(store, 'User', { language: 'php' });
      for (const item of result.items) {
        expect(item.file.language).toBe('php');
      }
    });

    it('returns scored results sorted by score', async () => {
      const result = await search(store, 'User');
      for (let i = 1; i < result.items.length; i++) {
        expect(result.items[i - 1].score).toBeGreaterThanOrEqual(result.items[i].score);
      }
    });

    it('returns empty for nonsense query', async () => {
      const result = await search(store, 'zzzxyznonexistent999');
      expect(result.items).toHaveLength(0);
    });

    it('supports pagination with limit/offset', async () => {
      const all = await search(store, 'User', undefined, 100, 0);
      if (all.items.length >= 2) {
        const page1 = await search(store, 'User', undefined, 1, 0);
        const page2 = await search(store, 'User', undefined, 1, 1);
        expect(page1.items[0].symbol.symbol_id).not.toBe(page2.items[0].symbol.symbol_id);
      }
    });

    describe('semantic-mode degradation when AI is unavailable', () => {
      // Reproduces the silent-degradation surprise: callers used to ask for
      // semantic="on" / "only" against an instance with no AI provider and get
      // unmarked FTS results back. The fix:
      //   - "only" must hard-fail with a structured _error
      //   - "on"   must succeed via FTS but stamp _warning so the caller sees
      //            the degradation
      //   - "auto" / "off" stay silent (their contract is "use what's available")

      const noAiOptions = {
        vectorStore: null,
        embeddingService: null,
        reranker: null,
      } as const;

      it('semantic="only" returns _error when no AI provider is configured', async () => {
        const result = await search(store, 'User', undefined, 20, 0, noAiOptions, undefined, {
          semantic: 'only',
        });
        expect(result._error).toBeDefined();
        expect(result._error?.code).toBe('no_ai_provider');
        expect(result._error?.message).toMatch(/semantic="only"/);
        expect(result._error?.hint).toMatch(/embed_repo/);
        expect(result.items).toEqual([]);
      });

      it('semantic="on" stamps _warning and falls through to FTS', async () => {
        const result = await search(store, 'User', undefined, 20, 0, noAiOptions, undefined, {
          semantic: 'on',
        });
        expect(result._warning).toMatch(/semantic="on"/);
        expect(result._warning).toMatch(/lexical/);
        expect(result.search_mode).toBe('fts');
        expect(result.items.length).toBeGreaterThan(0);
        expect(result._error).toBeUndefined();
      });

      it('semantic="auto" stays silent on FTS fallback', async () => {
        const result = await search(store, 'User', undefined, 20, 0, noAiOptions, undefined, {
          semantic: 'auto',
        });
        expect(result._warning).toBeUndefined();
        expect(result._error).toBeUndefined();
        expect(result.search_mode).toBe('fts');
      });

      it('semantic="off" stays silent on FTS', async () => {
        const result = await search(store, 'User', undefined, 20, 0, noAiOptions, undefined, {
          semantic: 'off',
        });
        expect(result._warning).toBeUndefined();
        expect(result._error).toBeUndefined();
      });
    });
  });

  describe('getSymbol()', () => {
    it('returns symbol source by symbol_id', async () => {
      // First find a symbol
      const searchResult = await search(store, 'add');
      expect(searchResult.items.length).toBeGreaterThan(0);

      const symbolId = searchResult.items[0].symbol.symbol_id;
      const result = getSymbol(store, FIXTURE_DIR, { symbolId });

      expect(result.isOk()).toBe(true);
      if (result.isOk()) {
        expect(result.value.source).toBeTruthy();
        expect(result.value.symbol.symbol_id).toBe(symbolId);
      }
    });

    it('returns symbol source by FQN', () => {
      const result = getSymbol(store, FIXTURE_DIR, { fqn: 'App\\Models\\User' });
      expect(result.isOk()).toBe(true);
      if (result.isOk()) {
        expect(result.value.symbol.name).toBe('User');
        expect(result.value.source).toContain('class User');
      }
    });

    it('returns NOT_FOUND for unknown symbol_id', () => {
      const result = getSymbol(store, FIXTURE_DIR, { symbolId: 'nonexistent' });
      expect(result.isErr()).toBe(true);
      if (result.isErr()) {
        expect(result.error.code).toBe('NOT_FOUND');
      }
    });
  });

  describe('getFileOutline()', () => {
    it('returns symbols for a known file', async () => {
      const result = await getFileOutline(store, 'app/Models/User.php');
      expect(result.isOk()).toBe(true);
      if (result.isOk()) {
        expect(result.value.symbols.length).toBeGreaterThan(0);
        expect(result.value.language).toBe('php');
      }
    });

    it('returns signatures without source bodies', async () => {
      const result = await getFileOutline(store, 'src/utils.ts');
      expect(result.isOk()).toBe(true);
      if (result.isOk()) {
        for (const sym of result.value.symbols) {
          // Outline should have signature (if available) but the object
          // does not contain a `source` field
          expect(sym).not.toHaveProperty('source');
          expect(sym).toHaveProperty('signature');
        }
      }
    });

    it('returns NOT_FOUND for unknown file', async () => {
      const result = await getFileOutline(store, 'nonexistent.php');
      expect(result.isErr()).toBe(true);
    });
  });
});
