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 { LaravelPlugin } from '../../src/indexer/plugins/integration/framework/laravel/index.js';
import { VueFrameworkPlugin } from '../../src/indexer/plugins/integration/view/vue/index.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 {
  classifyIntent,
  getTaskContext,
  type TaskIntent,
} from '../../src/tools/navigation/task-context.js';
import { createTestStore } from '../test-utils.js';

const FIXTURE_DIR = path.resolve(__dirname, '../fixtures/laravel-10');

function makeConfig(): TraceMcpConfig {
  return {
    root: FIXTURE_DIR,
    include: ['app/**/*.php', 'routes/**/*.php', 'database/migrations/**/*.php'],
    exclude: ['vendor/**', 'node_modules/**'],
    db: { path: ':memory:' },
    plugins: [],
  };
}

// ═══════════════════════════════════════════════════════════════════
// Intent Classification
// ═══════════════════════════════════════════════════════════════════

describe('classifyIntent', () => {
  it.each<[string, TaskIntent]>([
    // bugfix variants
    ['fix the login bug', 'bugfix'],
    ['the login page crashes on submit', 'bugfix'],
    ['debug the failing test', 'bugfix'],
    ['error in authentication flow', 'bugfix'],
    ['wrong output from parser', 'bugfix'],
    ['regression in v2.1', 'bugfix'],
    ['patch the XSS issue', 'bugfix'],
    ['hotfix for production', 'bugfix'],
    // new_feature variants
    ['add dark mode support', 'new_feature'],
    ['create a new user profile page', 'new_feature'],
    ['implement caching for API responses', 'new_feature'],
    ['build a notification system', 'new_feature'],
    ['introduce rate limiting', 'new_feature'],
    ['integrate Stripe payments', 'new_feature'],
    ['setup CI pipeline', 'new_feature'],
    // refactor variants
    ['refactor the auth module', 'refactor'],
    ['extract the validation logic', 'refactor'],
    ['rename UserService to AccountService', 'refactor'],
    ['split the monolith controller', 'refactor'],
    ['simplify the query builder', 'refactor'],
    ['consolidate duplicate helpers', 'refactor'],
    ['inline the wrapper function', 'refactor'],
    // understand (default)
    ['how does authentication work', 'understand'],
    ['explain the payment flow', 'understand'],
    ['what is this codebase about', 'understand'],
    ['show me the database schema', 'understand'],
  ])('classifies "%s" as %s', (task, expected) => {
    expect(classifyIntent(task)).toBe(expected);
  });

  it('returns understand for empty string', () => {
    expect(classifyIntent('')).toBe('understand');
  });

  it('returns understand for pure stopwords', () => {
    expect(classifyIntent('the and or a')).toBe('understand');
  });

  it('prioritizes first matching intent (bugfix over new_feature)', () => {
    // "fix" matches bugfix, "add" matches new_feature — bugfix patterns come first
    expect(classifyIntent('fix and add something')).toBe('bugfix');
  });
});

// ═══════════════════════════════════════════════════════════════════
// Main Pipeline — Indexed Fixture
// ═══════════════════════════════════════════════════════════════════

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

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

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

  // ─── Intent detection in pipeline ───

  it('returns bugfix intent and primary context', async () => {
    const result = await getTaskContext(store, FIXTURE_DIR, {
      task: 'fix the user authentication bug',
    });
    expect(result.intent).toBe('bugfix');
    expect(result.sections.primary.length).toBeGreaterThan(0);
    expect(result.seedCount).toBeGreaterThan(0);
    expect(result.graphNodesExplored).toBeGreaterThan(0);
  });

  it('returns new_feature intent', async () => {
    const result = await getTaskContext(store, FIXTURE_DIR, {
      task: 'add user profile management',
    });
    expect(result.intent).toBe('new_feature');
    expect(result.sections.primary.length).toBeGreaterThan(0);
  });

  it('returns refactor intent', async () => {
    const result = await getTaskContext(store, FIXTURE_DIR, {
      task: 'refactor user controller',
    });
    expect(result.intent).toBe('refactor');
    expect(result.sections.primary.length).toBeGreaterThan(0);
  });

  it('returns understand intent', async () => {
    const result = await getTaskContext(store, FIXTURE_DIR, {
      task: 'how does the user model work',
    });
    expect(result.intent).toBe('understand');
    expect(result.sections.primary.length).toBeGreaterThan(0);
  });

  // ─── Token budget ───

  it('respects token budget — small budget produces fewer tokens', async () => {
    const small = await getTaskContext(store, FIXTURE_DIR, {
      task: 'user management',
      tokenBudget: 200,
    });
    expect(small.totalTokens).toBeLessThanOrEqual(200);
  });

  it('larger budget produces more context', async () => {
    const small = await getTaskContext(store, FIXTURE_DIR, {
      task: 'user management',
      tokenBudget: 200,
    });
    const large = await getTaskContext(store, FIXTURE_DIR, {
      task: 'user management',
      tokenBudget: 10000,
    });
    expect(large.totalTokens).toBeGreaterThanOrEqual(small.totalTokens);
  });

  it('zero-budget produces zero tokens', async () => {
    const result = await getTaskContext(store, FIXTURE_DIR, {
      task: 'user',
      tokenBudget: 100,
    });
    // With 100 tokens, we may get one signature or nothing
    expect(result.totalTokens).toBeLessThanOrEqual(100);
  });

  // ─── Empty / edge cases ───

  it('returns empty for gibberish query', async () => {
    const result = await getTaskContext(store, FIXTURE_DIR, {
      task: 'xyzzyplugh99nonexistent',
    });
    expect(result.sections.primary).toHaveLength(0);
    expect(result.sections.dependencies).toHaveLength(0);
    expect(result.sections.callers).toHaveLength(0);
    expect(result.sections.tests).toHaveLength(0);
    expect(result.sections.types).toHaveLength(0);
    expect(result.totalTokens).toBe(0);
    expect(result.seedCount).toBe(0);
    expect(result.graphNodesExplored).toBe(0);
  });

  it('returns empty for stopwords-only query', async () => {
    const result = await getTaskContext(store, FIXTURE_DIR, {
      task: 'the and or a',
    });
    expect(result.sections.primary).toHaveLength(0);
    expect(result.totalTokens).toBe(0);
  });

  // ─── Detail levels ───

  it('includes valid detail level for every item in all sections', async () => {
    const result = await getTaskContext(store, FIXTURE_DIR, {
      task: 'user controller',
    });
    for (const [_sectionName, items] of Object.entries(result.sections)) {
      for (const item of items) {
        expect(['full', 'no_source', 'signature_only']).toContain(item.detail);
        expect(item.tokens).toBeGreaterThan(0);
        expect(item.symbolId).toBeTruthy();
        expect(item.name).toBeTruthy();
        expect(item.kind).toBeTruthy();
        expect(item.filePath).toBeTruthy();
        expect(item.content).toBeTruthy();
      }
    }
  });

  // ─── Focus modes ───

  it('focus=minimal returns fewer or equal results than focus=broad', async () => {
    const minimal = await getTaskContext(store, FIXTURE_DIR, {
      task: 'user',
      focus: 'minimal',
    });
    const broad = await getTaskContext(store, FIXTURE_DIR, {
      task: 'user',
      focus: 'broad',
    });
    const minTotal = Object.values(minimal.sections).flat().length;
    const broadTotal = Object.values(broad.sections).flat().length;
    expect(broadTotal).toBeGreaterThanOrEqual(minTotal);
  });

  it('focus=deep explores more graph nodes than focus=minimal', async () => {
    const minimal = await getTaskContext(store, FIXTURE_DIR, {
      task: 'user',
      focus: 'minimal',
    });
    const deep = await getTaskContext(store, FIXTURE_DIR, {
      task: 'user',
      focus: 'deep',
    });
    expect(deep.graphNodesExplored).toBeGreaterThanOrEqual(minimal.graphNodesExplored);
  });

  // ─── AI fallback ───

  it('works without AI (explicit null)', async () => {
    const result = await getTaskContext(
      store,
      FIXTURE_DIR,
      {
        task: 'user authentication',
      },
      null,
    );
    expect(result.sections.primary.length).toBeGreaterThan(0);
  });

  it('works without AI (undefined)', async () => {
    const result = await getTaskContext(
      store,
      FIXTURE_DIR,
      {
        task: 'user authentication',
      },
      undefined,
    );
    expect(result.sections.primary.length).toBeGreaterThan(0);
  });

  it('works with empty vectorStore/embeddingService', async () => {
    const result = await getTaskContext(
      store,
      FIXTURE_DIR,
      {
        task: 'user authentication',
      },
      { vectorStore: null, embeddingService: null },
    );
    expect(result.sections.primary.length).toBeGreaterThan(0);
  });

  // ─── Result metadata ───

  it('includes task and intent in result', async () => {
    const result = await getTaskContext(store, FIXTURE_DIR, {
      task: 'fix login',
    });
    expect(result.task).toBe('fix login');
    expect(result.intent).toBe('bugfix');
  });

  it('reports seedCount and graphNodesExplored', async () => {
    const result = await getTaskContext(store, FIXTURE_DIR, {
      task: 'user',
      focus: 'deep',
    });
    expect(result.seedCount).toBeGreaterThan(0);
    expect(result.graphNodesExplored).toBeGreaterThanOrEqual(result.seedCount);
  });

  it('reports truncated flag correctly', async () => {
    // Very small budget should truncate
    const small = await getTaskContext(store, FIXTURE_DIR, {
      task: 'user controller model',
      tokenBudget: 50,
    });
    // Large budget should not truncate (or at least be less likely)
    const large = await getTaskContext(store, FIXTURE_DIR, {
      task: 'user controller model',
      tokenBudget: 50000,
    });
    // We can't guarantee truncation, but we can verify the flag is boolean
    expect(typeof small.truncated).toBe('boolean');
    expect(typeof large.truncated).toBe('boolean');
  });

  // ─── Graph walk behavior ───

  it('graph walk discovers dependencies beyond seeds', async () => {
    const result = await getTaskContext(store, FIXTURE_DIR, {
      task: 'user controller',
      focus: 'deep',
    });
    // With depth > 0, we should explore nodes beyond the initial seeds
    const totalItems = Object.values(result.sections).flat().length;
    // The graph should discover at least some dependencies or callers
    expect(result.graphNodesExplored).toBeGreaterThan(0);
    // Primary should have seeds, and at least one other section should have items
    // (the fixture has controllers → models → migrations chain)
    if (result.sections.primary.length > 0) {
      const nonPrimaryItems = totalItems - result.sections.primary.length;
      // We may or may not get non-primary items depending on edges in fixture
      expect(nonPrimaryItems).toBeGreaterThanOrEqual(0);
    }
  });

  it('bugfix intent explores deeper than understand', async () => {
    const bugfix = await getTaskContext(store, FIXTURE_DIR, {
      task: 'fix user issue',
      focus: 'broad',
    });
    const understand = await getTaskContext(store, FIXTURE_DIR, {
      task: 'user issue',
      focus: 'broad',
    });
    // Bugfix has graphDepth=3, understand has graphDepth=1
    // So bugfix should explore at least as many nodes
    expect(bugfix.graphNodesExplored).toBeGreaterThanOrEqual(understand.graphNodesExplored);
  });

  // ─── Section classification ───

  it('primary section contains items with highest scores', async () => {
    const result = await getTaskContext(store, FIXTURE_DIR, {
      task: 'user model',
      tokenBudget: 10000,
    });

    if (result.sections.primary.length > 0 && result.sections.dependencies.length > 0) {
      const maxPrimaryScore = Math.max(...result.sections.primary.map((i) => i.score));
      const maxDepScore = Math.max(...result.sections.dependencies.map((i) => i.score));
      // Primary items (seeds) should generally score higher than dependencies
      // This may not always hold due to PageRank, but primary seeds start with search relevance
      expect(maxPrimaryScore).toBeGreaterThanOrEqual(0);
      expect(maxDepScore).toBeGreaterThanOrEqual(0);
    }
  });

  it('all sections have non-overlapping symbol IDs', async () => {
    const result = await getTaskContext(store, FIXTURE_DIR, {
      task: 'user',
      focus: 'deep',
      tokenBudget: 20000,
    });

    const allIds = Object.values(result.sections)
      .flat()
      .map((i) => i.symbolId);
    const uniqueIds = new Set(allIds);
    expect(allIds.length).toBe(uniqueIds.size);
  });

  // ─── includeTests flag ───

  it('includeTests=false excludes test section', async () => {
    const withTests = await getTaskContext(store, FIXTURE_DIR, {
      task: 'user',
      includeTests: true,
    });
    const withoutTests = await getTaskContext(store, FIXTURE_DIR, {
      task: 'user',
      includeTests: false,
    });
    // Without tests, the tests section should be empty
    expect(withoutTests.sections.tests).toHaveLength(0);
    // With tests, it may or may not have items (depends on fixture having test_covers edges)
    expect(withTests.sections.tests.length).toBeGreaterThanOrEqual(0);
  });

  // ─── Different queries find different symbols ───

  it('different queries produce different primary symbols', async () => {
    const userResult = await getTaskContext(store, FIXTURE_DIR, {
      task: 'user model',
    });
    const postResult = await getTaskContext(store, FIXTURE_DIR, {
      task: 'post model',
    });

    if (userResult.sections.primary.length > 0 && postResult.sections.primary.length > 0) {
      const userNames = userResult.sections.primary.map((i) => i.name);
      const postNames = postResult.sections.primary.map((i) => i.name);
      // At least some names should differ
      const overlap = userNames.filter((n) => postNames.includes(n));
      expect(overlap.length).toBeLessThan(Math.max(userNames.length, postNames.length));
    }
  });
});

// ═══════════════════════════════════════════════════════════════════
// Empty Index
// ═══════════════════════════════════════════════════════════════════

describe('getTaskContext with empty index', () => {
  let emptyStore: Store;

  beforeAll(() => {
    emptyStore = createTestStore();
  });

  it('returns empty result on empty index', async () => {
    const result = await getTaskContext(emptyStore, '/tmp', {
      task: 'find something',
    });
    expect(result.sections.primary).toHaveLength(0);
    expect(result.totalTokens).toBe(0);
    expect(result.seedCount).toBe(0);
    expect(result.graphNodesExplored).toBe(0);
    expect(result.intent).toBe('understand');
  });

  it('bugfix on empty index returns empty gracefully', async () => {
    const result = await getTaskContext(emptyStore, '/tmp', {
      task: 'fix the crash',
    });
    expect(result.intent).toBe('bugfix');
    expect(result.sections.primary).toHaveLength(0);
  });
});
