import { loadSkills } from '@gossip/orchestrator';
import { listAvailableSkills } from '../../packages/orchestrator/src/skill-loader';
import { SkillIndex } from '../../packages/orchestrator/src/skill-index';
import { mkdirSync, writeFileSync, rmSync, symlinkSync, realpathSync } from 'fs';
import { join } from 'path';
import { tmpdir } from 'os';

describe('SkillLoader', () => {
  it('loads default skills by name', () => {
    const result = loadSkills('test-agent', ['typescript'], process.cwd());
    expect(result.content).toContain('TypeScript');
    expect(result.content).toContain('SKILLS');
    expect(result.loaded).toContain('typescript');
  });

  it('returns empty for no skills', () => {
    const result = loadSkills('test-agent', [], process.cwd());
    expect(result.content).toBe('');
    expect(result.loaded).toEqual([]);
  });

  it('returns empty for unknown skill', () => {
    const result = loadSkills('test-agent', ['nonexistent-skill-xyz'], process.cwd());
    expect(result.content).toBe('');
  });

  it('lists available default skills', () => {
    const skills = listAvailableSkills('test-agent', process.cwd());
    expect(skills).toContain('typescript');
    expect(skills).toContain('code-review');
    expect(skills).toContain('debugging');
  });

  it('resolves verify-the-premise from bundled default-skills/', () => {
    // Component C ships this skill with the npm bundle; resolution must work
    // regardless of the caller's projectRoot (no per-project override needed).
    const skills = listAvailableSkills('test-agent', process.cwd());
    expect(skills).toContain('verify-the-premise');
    const result = loadSkills('opus-implementer', ['verify-the-premise'], process.cwd());
    expect(result.loaded).toContain('verify-the-premise');
    expect(result.content).toContain('Iron law');
    expect(result.content).toContain('grep');
  });

  it('wraps multiple skills with delimiters', () => {
    const result = loadSkills('test-agent', ['typescript'], process.cwd());
    expect(result.content).toMatch(/^[\s\S]*--- SKILLS ---[\s\S]*--- END SKILLS ---[\s\S]*$/);
  });

  it('resolves underscore skill names to hyphenated filenames', () => {
    const tmpDir = join(tmpdir(), `gossip-test-${Date.now()}`);
    const skillDir = join(tmpDir, '.gossip', 'skills');
    mkdirSync(skillDir, { recursive: true });
    writeFileSync(join(skillDir, 'code-review.md'), '# Code Review Skill');

    try {
      const result = loadSkills('test-agent', ['code_review'], tmpDir);
      expect(result.content).toContain('Code Review Skill');
    } finally {
      rmSync(tmpDir, { recursive: true, force: true });
    }
  });
});

describe('Contextual Skill Loading', () => {
  let tmpDir: string;
  let index: SkillIndex;

  beforeEach(() => {
    tmpDir = join(tmpdir(), `gossip-ctx-${Date.now()}`);
    mkdirSync(join(tmpDir, '.gossip', 'agents', 'test-agent', 'skills'), { recursive: true });
    mkdirSync(join(tmpDir, '.gossip', 'skills'), { recursive: true });

    // Create a contextual skill with keywords
    writeFileSync(join(tmpDir, '.gossip', 'agents', 'test-agent', 'skills', 'trust-boundaries.md'),
`---
name: trust-boundary-validation
description: Trust boundary review
keywords: [auth, authentication, session, cookie, injection]
category: trust_boundaries
mode: contextual
status: active
---

## Iron Law
Never trust user input.
`);

    // Create a permanent skill
    writeFileSync(join(tmpDir, '.gossip', 'skills', 'typescript.md'),
`---
name: typescript
description: TypeScript patterns
keywords: []
mode: permanent
status: active
---

## TypeScript Guide
Use strict types.
`);

    index = new SkillIndex(tmpDir);
    index.bind('test-agent', 'typescript', { source: 'config', mode: 'permanent' });
    index.bind('test-agent', 'trust-boundaries', { source: 'auto', mode: 'contextual' });
  });

  afterEach(() => {
    rmSync(tmpDir, { recursive: true, force: true });
  });

  it('always loads permanent skills regardless of task', () => {
    const result = loadSkills('test-agent', [], tmpDir, index, 'fix a CSS bug');
    expect(result.loaded).toContain('typescript');
    expect(result.content).toContain('TypeScript');
  });

  it('activates contextual skill when task matches 1+ keywords', () => {
    const result = loadSkills('test-agent', [], tmpDir, index, 'Review the auth handler for session management');
    expect(result.loaded).toContain('trust-boundaries');
    expect(result.activatedContextual).toContain('trust-boundaries');
    expect(result.content).toContain('trust user input');
  });

  it('skips contextual skill when task matches zero keywords', () => {
    const result = loadSkills('test-agent', [], tmpDir, index, 'Review the CSS layout');
    expect(result.loaded).not.toContain('trust-boundaries');
    expect(result.activatedContextual).toEqual([]);
    // Below-threshold (0 hits) now records a structured drop
    const drop = result.dropped.find(d => d.skill === 'trust-boundaries');
    expect(drop?.reason).toBe('below-keyword-threshold');
    expect(drop?.hits).toBe(0);
  });

  it('activates contextual skill with single keyword hit (MIN_KEYWORD_HITS=1)', () => {
    // Only "auth" matches — under MIN_KEYWORD_HITS=1, this now activates.
    // Budget cap + hit ordering remain the safety nets.
    const result = loadSkills('test-agent', [], tmpDir, index, 'Check the auth flow');
    expect(result.activatedContextual).toContain('trust-boundaries');
  });

  it('uses word-boundary matching to prevent false positives', () => {
    // "auth" should NOT match "author"; only "authentication" matches → 1 hit
    const result = loadSkills('test-agent', [], tmpDir, index, 'Check the author name and authentication');
    // Under MIN_KEYWORD_HITS=1, 1 hit now activates the skill.
    // Word-boundary correctness is still asserted by the fact we get exactly 1
    // match (via countKeywordHits internally) — "author" does not contribute.
    expect(result.activatedContextual).toContain('trust-boundaries');
  });

  it('respects MAX_CONTEXTUAL_SKILLS budget', () => {
    // Add 4 more contextual skills
    for (const cat of ['injection-vectors', 'concurrency', 'error-handling', 'data-integrity']) {
      writeFileSync(join(tmpDir, '.gossip', 'agents', 'test-agent', 'skills', `${cat}.md`),
`---
name: ${cat}
description: ${cat} skill
keywords: [review, code, security, bug]
category: ${cat.replace(/-/g, '_')}
mode: contextual
status: active
---

## ${cat} guide
Check everything.
`);
      index.bind('test-agent', cat, { source: 'auto', mode: 'contextual' });
    }

    const result = loadSkills('test-agent', [], tmpDir, index, 'Review the code for security bugs');
    // All 4 new skills match (review + code + security + bug = 4 hits each)
    // Plus trust-boundaries doesn't match (no 2+ keyword hits for this task)
    // Budget: max 3 contextual
    expect(result.activatedContextual.length).toBeLessThanOrEqual(3);
    expect(result.dropped.length).toBeGreaterThan(0);
    // Per cross-review 5ad115dd-fbc14d01:f9 — assert the structured reason
    // so budget-exceeded path has regression protection parity with the
    // other drop reasons.
    expect(result.dropped.some((d) => d.reason === 'budget-exceeded')).toBe(true);
  });

  it('skips contextual skills when no task provided', () => {
    const result = loadSkills('test-agent', [], tmpDir, index);
    expect(result.loaded).toContain('typescript');
    expect(result.activatedContextual).toEqual([]);
  });

  it('records contextual skill as dropped with reason=no-task-provided', () => {
    // Closes the silent-drop observability gap: when `task` is falsy,
    // contextual skills must appear in `dropped` so operators can see why.
    const result = loadSkills('test-agent', [], tmpDir, index);
    const trustDrop = result.dropped.find(d => d.skill === 'trust-boundaries');
    expect(trustDrop).toBeDefined();
    expect(trustDrop?.reason).toBe('no-task-provided');
    expect(trustDrop?.hits).toBe(0);
  });
});

describe('Pattern cache LRU eviction', () => {
  // The cache is module-scoped; these tests use a fresh skill to exercise
  // getPattern via the public loadSkills path, verifying LRU promotion works
  // through the observable API without reaching into internals.
  let tmpDir: string;
  let index: SkillIndex;

  beforeEach(() => {
    tmpDir = join(tmpdir(), `gossip-lru-${Date.now()}`);
    mkdirSync(join(tmpDir, '.gossip', 'agents', 'test-agent', 'skills'), { recursive: true });
    index = new SkillIndex(tmpDir);
  });

  afterEach(() => {
    rmSync(tmpDir, { recursive: true, force: true });
  });

  it('promotes cache hits to most-recently-used on repeated access', () => {
    // Write a skill with one unique keyword. Call loadSkills many times so the
    // keyword's pattern is compiled once and subsequently hit — each hit must
    // be a cache promotion, not a recompile. Correctness is observable as:
    // (a) activation still works across repeated calls, and (b) no exception
    // is thrown from the delete-then-set promotion path.
    const skillsDir = join(tmpDir, '.gossip', 'agents', 'test-agent', 'skills');
    writeFileSync(join(skillsDir, 'lru-probe.md'), `---
name: lru-probe
description: LRU probe
keywords: [xyzzylrumarker]
category: trust_boundaries
mode: contextual
status: active
---

LRU probe body.
`);
    index.bind('test-agent', 'lru-probe', { source: 'auto', mode: 'contextual' });

    for (let i = 0; i < 5; i++) {
      const result = loadSkills('test-agent', [], tmpDir, index, 'task xyzzylrumarker present');
      expect(result.activatedContextual).toContain('lru-probe');
    }
  });

  it('evicts least-recently-used keyword when cache is full', () => {
    // Reach into the module-scoped cache to test eviction order deterministically.
    // Clearing at the start isolates from any warming done by prior tests or
    // the other cases in this block.
    const internals = (require('../../packages/orchestrator/src/skill-loader') as {
      __lruInternals: {
        patternCache: Map<string, RegExp>;
        getPattern: (k: string) => RegExp;
        MAX_PATTERN_CACHE: number;
      };
    }).__lruInternals;
    expect(internals).toBeDefined();
    const { patternCache, getPattern, MAX_PATTERN_CACHE } = internals;

    patternCache.clear();

    // Warm two sentinels
    getPattern('keep-hot');
    getPattern('will-evict');

    // Fill cache with MAX_PATTERN_CACHE-2 fresh keys, touching keep-hot between
    // each insert to keep it fresh while will-evict ages.
    for (let i = 0; i < MAX_PATTERN_CACHE - 2; i++) {
      getPattern(`filler-${i}`);
      getPattern('keep-hot'); // LRU promotion
    }

    expect(patternCache.size).toBe(MAX_PATTERN_CACHE);
    expect(patternCache.has('keep-hot')).toBe(true);
    expect(patternCache.has('will-evict')).toBe(true);

    // Overflow by one — should evict the oldest, which is will-evict.
    getPattern('final-key');

    expect(patternCache.has('keep-hot')).toBe(true);
    expect(patternCache.has('will-evict')).toBe(false);
    expect(patternCache.has('final-key')).toBe(true);
  });
});

describe('LoadSkillsResult.paths (warm-cache fingerprint prerequisite)', () => {
  let tmpDir: string;
  let index: SkillIndex;

  beforeEach(() => {
    tmpDir = join(tmpdir(), `gossip-paths-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`);
    mkdirSync(join(tmpDir, '.gossip', 'agents', 'test-agent', 'skills'), { recursive: true });
    mkdirSync(join(tmpDir, '.gossip', 'skills'), { recursive: true });
    index = new SkillIndex(tmpDir);
  });

  afterEach(() => {
    rmSync(tmpDir, { recursive: true, force: true });
  });

  it('returns paths: [] when no skills loaded', () => {
    const result = loadSkills('test-agent', [], tmpDir, index);
    expect(result.loaded).toEqual([]);
    expect(result.paths).toEqual([]);
  });

  it('returns paths: [] when requested skill does not resolve', () => {
    const result = loadSkills('test-agent', ['definitely-not-a-real-skill-xyz'], tmpDir);
    expect(result.loaded).toEqual([]);
    expect(result.paths).toEqual([]);
  });

  it('paths is index-aligned with loaded for permanent skills', () => {
    const skillPathA = join(tmpDir, '.gossip', 'skills', 'perm-a.md');
    const skillPathB = join(tmpDir, '.gossip', 'skills', 'perm-b.md');
    writeFileSync(skillPathA, `---
name: perm-a
description: A
keywords: []
mode: permanent
status: active
---

permanent A
`);
    writeFileSync(skillPathB, `---
name: perm-b
description: B
keywords: []
mode: permanent
status: active
---

permanent B
`);
    index.bind('test-agent', 'perm-a', { source: 'config', mode: 'permanent' });
    index.bind('test-agent', 'perm-b', { source: 'config', mode: 'permanent' });

    const result = loadSkills('test-agent', [], tmpDir, index);
    expect(result.loaded.length).toBe(result.paths.length);
    expect(result.loaded.length).toBeGreaterThanOrEqual(2);

    const aIdx = result.loaded.indexOf('perm-a');
    const bIdx = result.loaded.indexOf('perm-b');
    expect(aIdx).toBeGreaterThanOrEqual(0);
    expect(bIdx).toBeGreaterThanOrEqual(0);
    expect(result.paths[aIdx]).toBe(realpathSync(skillPathA));
    expect(result.paths[bIdx]).toBe(realpathSync(skillPathB));
  });

  it('paths is index-aligned with loaded for contextual skills activated by keyword match', () => {
    const ctxPath = join(tmpDir, '.gossip', 'agents', 'test-agent', 'skills', 'ctx-trust.md');
    writeFileSync(ctxPath, `---
name: ctx-trust
description: Trust ctx
keywords: [auth, session]
category: trust_boundaries
mode: contextual
status: active
---

ctx trust body
`);
    index.bind('test-agent', 'ctx-trust', { source: 'auto', mode: 'contextual' });

    const result = loadSkills('test-agent', [], tmpDir, index, 'review the auth session handler');
    expect(result.activatedContextual).toContain('ctx-trust');
    expect(result.loaded.length).toBe(result.paths.length);

    const idx = result.loaded.indexOf('ctx-trust');
    expect(idx).toBeGreaterThanOrEqual(0);
    expect(result.paths[idx]).toBe(realpathSync(ctxPath));
  });

  it('paths contains realpath-resolved values for symlinked skills', () => {
    const targetDir = join(tmpDir, 'real-skills');
    mkdirSync(targetDir, { recursive: true });
    const targetFile = join(targetDir, 'real-skill.md');
    writeFileSync(targetFile, `---
name: linked-skill
description: linked
keywords: []
mode: permanent
status: active
---

linked body
`);

    const linkPath = join(tmpDir, '.gossip', 'agents', 'test-agent', 'skills', 'linked-skill.md');
    symlinkSync(targetFile, linkPath);
    index.bind('test-agent', 'linked-skill', { source: 'config', mode: 'permanent' });

    const result = loadSkills('test-agent', [], tmpDir, index);
    expect(result.loaded).toContain('linked-skill');
    const idx = result.loaded.indexOf('linked-skill');
    const expected = realpathSync(targetFile);
    expect(result.paths[idx]).toBe(expected);
    expect(result.paths[idx]).not.toBe(linkPath);
  });
});
