import { describe, it, expect } from 'vitest';
import { TagMatcher, extractKeywordsFromToolCall, tokenizeDescription, type PromptIndexEntry } from '../src/gate/tag-matcher.js';

function makePrompt(overrides: Partial<PromptIndexEntry> = {}): PromptIndexEntry {
  return {
    name: 'test-prompt',
    priority: 5,
    summary: 'A test prompt for testing',
    chapters: ['Chapter One', 'Chapter Two'],
    content: 'Full content of the test prompt.',
    ...overrides,
  };
}

describe('TagMatcher', () => {
  it('returns priority 10 prompts first, then others by priority', () => {
    const matcher = new TagMatcher();
    const critical = makePrompt({ name: 'common-mistakes', priority: 10, summary: 'Unrelated stuff' });
    const normal = makePrompt({ name: 'normal', priority: 5, summary: 'Something else' });

    const result = matcher.match([], [critical, normal]);
    // Both included — priority 10 first (Infinity), then priority 5 (baseline 5)
    expect(result.fullContent.map((p) => p.name)).toEqual(['common-mistakes', 'normal']);
    expect(result.remaining).toEqual([]);
  });

  it('scores by priority baseline + matching_tags * priority', () => {
    const matcher = new TagMatcher();
    const high = makePrompt({ name: 'important', priority: 8, summary: 'zigbee mqtt pairing' });
    const low = makePrompt({ name: 'basic', priority: 3, summary: 'zigbee basics' });

    // high: 8 + 1*8 = 16, low: 3 + 1*3 = 6
    const result = matcher.match(['zigbee'], [low, high]);
    expect(result.fullContent[0]!.name).toBe('important');
    expect(result.fullContent[1]!.name).toBe('basic');
  });

  it('matches more tags = higher score', () => {
    const matcher = new TagMatcher();
    const twoMatch = makePrompt({ name: 'two-match', priority: 5, summary: 'zigbee mqtt' });
    const oneMatch = makePrompt({ name: 'one-match', priority: 5, summary: 'zigbee only' });

    // two-match: 5 + 2*5 = 15, one-match: 5 + 1*5 = 10
    const result = matcher.match(['zigbee', 'mqtt'], [oneMatch, twoMatch]);
    expect(result.fullContent[0]!.name).toBe('two-match');
  });

  it('performs case-insensitive matching', () => {
    const matcher = new TagMatcher();
    const prompt = makePrompt({ name: 'test', summary: 'ZIGBEE Protocol Setup' });

    const result = matcher.match(['zigbee'], [prompt]);
    expect(result.fullContent).toHaveLength(1);
  });

  it('matches against name, summary, and chapters', () => {
    const matcher = new TagMatcher();
    const byName = makePrompt({ name: 'zigbee-config', summary: 'unrelated', chapters: [] });
    const bySummary = makePrompt({ name: 'setup', summary: 'zigbee setup guide', chapters: [] });
    const byChapter = makePrompt({ name: 'guide', summary: 'unrelated', chapters: ['Zigbee Pairing'] });

    const result = matcher.match(['zigbee'], [byName, bySummary, byChapter]);
    expect(result.fullContent).toHaveLength(3);
  });

  it('respects byte budget', () => {
    const matcher = new TagMatcher(100); // Very small budget
    const small = makePrompt({ name: 'small', summary: 'zigbee', content: 'Short.' }); // ~6 bytes
    const big = makePrompt({ name: 'big', summary: 'zigbee', content: 'x'.repeat(200) }); // 200 bytes

    const result = matcher.match(['zigbee'], [small, big]);
    expect(result.fullContent.map((p) => p.name)).toEqual(['small']);
    expect(result.indexOnly.map((p) => p.name)).toEqual(['big']);
  });

  it('includes all prompts — tag-matched ranked higher', () => {
    const matcher = new TagMatcher();
    const matched = makePrompt({ name: 'matched', summary: 'zigbee stuff' });
    const unmatched = makePrompt({ name: 'unmatched', summary: 'completely different topic' });

    const result = matcher.match(['zigbee'], [matched, unmatched]);
    // matched: 5 + 1*5 = 10, unmatched: 5 + 0 = 5 — both included, matched first
    expect(result.fullContent.map((p) => p.name)).toEqual(['matched', 'unmatched']);
    expect(result.remaining).toEqual([]);
  });

  it('handles empty tags — all prompts included by priority', () => {
    const matcher = new TagMatcher();
    const critical = makePrompt({ name: 'critical', priority: 10 });
    const normal = makePrompt({ name: 'normal', priority: 5 });

    const result = matcher.match([], [critical, normal]);
    // priority 10 → Infinity, priority 5 → baseline 5
    expect(result.fullContent.map((p) => p.name)).toEqual(['critical', 'normal']);
    expect(result.remaining).toEqual([]);
  });

  it('includes unrelated prompts within byte budget (priority baseline)', () => {
    const matcher = new TagMatcher(500);
    const related = makePrompt({ name: 'node-red-flows', priority: 5, summary: 'node-red flow management' });
    const unrelated = makePrompt({ name: 'stack', priority: 5, summary: 'project stack overview', content: 'Tech stack info...' });

    // Tags match "node-red-flows" but not "stack" — both should be included
    const result = matcher.match(['node-red', 'flows'], [related, unrelated]);
    expect(result.fullContent.map((p) => p.name)).toContain('stack');
    expect(result.fullContent.map((p) => p.name)).toContain('node-red-flows');
    // Related prompt should be ranked higher
    expect(result.fullContent[0]!.name).toBe('node-red-flows');
  });

  it('pushes low-priority unrelated prompts to indexOnly when budget is tight', () => {
    const matcher = new TagMatcher(100);
    const related = makePrompt({ name: 'related', priority: 5, summary: 'zigbee', content: 'x'.repeat(80) });
    const unrelated = makePrompt({ name: 'unrelated', priority: 3, summary: 'other', content: 'y'.repeat(80) });

    const result = matcher.match(['zigbee'], [related, unrelated]);
    // related: 5 + 1*5 = 10 (higher score, fits budget), unrelated: 3 + 0 = 3 (overflow)
    expect(result.fullContent.map((p) => p.name)).toEqual(['related']);
    expect(result.indexOnly.map((p) => p.name)).toEqual(['unrelated']);
  });

  it('handles empty prompts array', () => {
    const matcher = new TagMatcher();
    const result = matcher.match(['zigbee'], []);
    expect(result.fullContent).toEqual([]);
    expect(result.indexOnly).toEqual([]);
    expect(result.remaining).toEqual([]);
  });

  it('all priority 10 prompts are included even beyond budget', () => {
    const matcher = new TagMatcher(50); // Tiny budget
    const c1 = makePrompt({ name: 'c1', priority: 10, content: 'x'.repeat(40) });
    const c2 = makePrompt({ name: 'c2', priority: 10, content: 'y'.repeat(40) });

    const result = matcher.match([], [c1, c2]);
    // Both should be in fullContent — priority 10 has Infinity score
    // First one fits budget, second overflows but still priority 10
    expect(result.fullContent.length + result.indexOnly.length).toBe(2);
    // At minimum the first one is in fullContent
    expect(result.fullContent[0]!.name).toBe('c1');
  });

  it('sorts matched by score descending', () => {
    const matcher = new TagMatcher();
    const p1 = makePrompt({ name: 'p1', priority: 3, summary: 'mqtt zigbee lights' }); // 3 + 3*3 = 12
    const p2 = makePrompt({ name: 'p2', priority: 8, summary: 'mqtt' }); // 8 + 1*8 = 16
    const p3 = makePrompt({ name: 'p3', priority: 2, summary: 'mqtt zigbee lights pairing automation' }); // 2 + 5*2 = 12

    const result = matcher.match(['mqtt', 'zigbee', 'lights', 'pairing', 'automation'], [p1, p2, p3]);
    // p2 (16) > p1 (12) = p3 (12), tie-break by input order
    expect(result.fullContent[0]!.name).toBe('p2');
  });
});

describe('extractKeywordsFromToolCall', () => {
  it('extracts from tool name', () => {
    const keywords = extractKeywordsFromToolCall('home-assistant/get_entities', {});
    expect(keywords).toContain('home');
    expect(keywords).toContain('assistant');
    expect(keywords).toContain('get_entities');
  });

  it('extracts from string arguments', () => {
    const keywords = extractKeywordsFromToolCall('tool', { domain: 'light', area: 'kitchen' });
    expect(keywords).toContain('light');
    expect(keywords).toContain('kitchen');
  });

  it('ignores short words (<=2 chars)', () => {
    const keywords = extractKeywordsFromToolCall('ab', { x: 'hi' });
    expect(keywords).not.toContain('ab');
    expect(keywords).not.toContain('hi');
  });

  it('ignores long string values (>200 chars)', () => {
    const keywords = extractKeywordsFromToolCall('tool', { data: 'x'.repeat(201) });
    // Only 'tool' from the name
    expect(keywords).toEqual(['tool']);
  });

  it('caps at 10 keywords', () => {
    const args: Record<string, string> = {};
    for (let i = 0; i < 20; i++) args[`key${i}`] = `keyword${i}value`;
    const keywords = extractKeywordsFromToolCall('tool', args);
    expect(keywords.length).toBeLessThanOrEqual(10);
  });

  it('lowercases all keywords', () => {
    const keywords = extractKeywordsFromToolCall('MyTool', { name: 'MQTT' });
    expect(keywords).toContain('mytool');
    expect(keywords).toContain('mqtt');
  });
});

describe('tokenizeDescription', () => {
  it('extracts meaningful words from a sentence', () => {
    const result = tokenizeDescription('I want to get node-red flows');
    expect(result).toContain('node-red');
    expect(result).toContain('flows');
  });

  it('filters stop words', () => {
    const result = tokenizeDescription('I want to get the flows for my project');
    expect(result).not.toContain('want');
    expect(result).not.toContain('the');
    expect(result).not.toContain('for');
    expect(result).toContain('flows');
    expect(result).toContain('project');
  });

  it('filters words shorter than 3 characters', () => {
    const result = tokenizeDescription('go to my HA setup');
    expect(result).not.toContain('go');
    expect(result).not.toContain('to');
    expect(result).not.toContain('my');
    expect(result).not.toContain('ha');
    expect(result).toContain('setup');
  });

  it('lowercases all tokens', () => {
    const result = tokenizeDescription('Configure MQTT Broker Settings');
    expect(result).toContain('configure');
    expect(result).toContain('mqtt');
    expect(result).toContain('broker');
    expect(result).toContain('settings');
  });

  it('caps at 10 keywords', () => {
    const result = tokenizeDescription(
      'alpha bravo charlie delta echo foxtrot golf hotel india juliet kilo lima mike november oscar papa',
    );
    expect(result.length).toBeLessThanOrEqual(10);
  });

  it('deduplicates words', () => {
    const result = tokenizeDescription('zigbee zigbee zigbee pairing');
    expect(result.filter((w) => w === 'zigbee')).toHaveLength(1);
    expect(result).toContain('pairing');
  });

  it('handles punctuation and special characters', () => {
    const result = tokenizeDescription('home-assistant; mqtt/broker (setup)');
    // Hyphens are preserved within words (compound names)
    expect(result).toContain('home-assistant');
    expect(result).toContain('mqtt');
    expect(result).toContain('broker');
    expect(result).toContain('setup');
  });

  it('returns empty array for empty string', () => {
    expect(tokenizeDescription('')).toEqual([]);
  });

  it('returns empty array for only stop words', () => {
    expect(tokenizeDescription('I want to get the')).toEqual([]);
  });
});
