import { describe, it, beforeEach } from 'node:test';
import assert from 'node:assert/strict';
import { resolve } from 'node:path';
import { fileURLToPath } from 'node:url';
import { resetCounter } from '../../scanners/lib/output.mjs';
import { scan, opportunitySummary } from '../../scanners/feature-gap-scanner.mjs';
import { discoverConfigFiles } from '../../scanners/lib/file-discovery.mjs';

const __dirname = fileURLToPath(new URL('.', import.meta.url));
const FIXTURES = resolve(__dirname, '../fixtures');

// Pre-discover fixture files WITHOUT includeGlobal so tests are environment-independent.
// The GAP scanner uses shared discovery when it has files, avoiding its own includeGlobal scan.
async function fixtureDiscovery(name) {
  return discoverConfigFiles(resolve(FIXTURES, name));
}

describe('GAP scanner — healthy project', () => {
  let result;
  beforeEach(async () => {
    resetCounter();
    const discovery = await fixtureDiscovery('healthy-project');
    result = await scan(resolve(FIXTURES, 'healthy-project'), discovery);
  });

  it('returns status ok', () => {
    assert.equal(result.status, 'ok');
  });

  it('reports scanner prefix GAP', () => {
    assert.equal(result.scanner, 'GAP');
  });

  it('scans multiple files', () => {
    assert.ok(result.files_scanned >= 1);
  });

  it('finding IDs match CA-GAP-NNN pattern', () => {
    for (const f of result.findings) {
      assert.match(f.id, /^CA-GAP-\d{3}$/);
    }
  });

  it('does NOT report missing CLAUDE.md', () => {
    assert.ok(!result.findings.some(f =>
      f.scanner === 'GAP' && f.category === 't1' && /CLAUDE\.md/.test(f.recommendation || '')
    ));
  });

  it('does NOT report missing MCP', () => {
    assert.ok(!result.findings.some(f =>
      f.scanner === 'GAP' && f.category === 't1' && /\.mcp\.json/.test(f.recommendation || '')
    ));
  });

  it('does NOT report missing hooks', () => {
    assert.ok(!result.findings.some(f =>
      f.scanner === 'GAP' && f.category === 't1' && /hook/i.test(f.recommendation || '')
    ));
  });

  it('has counts object with all severity levels', () => {
    assert.ok('critical' in result.counts);
    assert.ok('high' in result.counts);
    assert.ok('medium' in result.counts);
    assert.ok('low' in result.counts);
    assert.ok('info' in result.counts);
  });

  it('has no critical or high findings', () => {
    assert.equal(result.counts.critical, 0);
    assert.equal(result.counts.high, 0);
  });

  it('all findings have recommendations', () => {
    for (const f of result.findings) {
      assert.ok(f.recommendation, `Finding ${f.id} missing recommendation`);
    }
  });

  it('T3/T4 findings are info severity', () => {
    const infoFindings = result.findings.filter(f => f.category === 't3' || f.category === 't4');
    for (const f of infoFindings) {
      assert.equal(f.severity, 'info', `${f.id} (${f.category}) should be info, got ${f.severity}`);
    }
  });
});

describe('GAP scanner — minimal project', () => {
  let result;
  beforeEach(async () => {
    resetCounter();
    const discovery = await fixtureDiscovery('minimal-project');
    result = await scan(resolve(FIXTURES, 'minimal-project'), discovery);
  });

  it('returns status ok', () => {
    assert.equal(result.status, 'ok');
  });

  it('reports missing hooks', () => {
    // CA-GAP-002 in minimal-project = t1_3 (No hooks configured); see docs/v5.1.0-test-audit.md.
    assert.ok(result.findings.some(f => f.scanner === 'GAP' && f.id === 'CA-GAP-002'));
  });

  it('reports missing MCP', () => {
    // CA-GAP-004 in minimal-project = t1_5 (No MCP servers configured).
    assert.ok(result.findings.some(f => f.scanner === 'GAP' && f.id === 'CA-GAP-004'));
  });

  it('T1 gaps are medium severity', () => {
    const t1 = result.findings.filter(f => f.category === 't1');
    for (const f of t1) {
      assert.equal(f.severity, 'medium', `${f.id} should be medium, got ${f.severity}`);
    }
  });

  it('T2 gaps are low severity', () => {
    const t2 = result.findings.filter(f => f.category === 't2');
    for (const f of t2) {
      assert.equal(f.severity, 'low', `${f.id} should be low, got ${f.severity}`);
    }
  });

  it('has more findings than healthy project', async () => {
    resetCounter();
    const discovery = await fixtureDiscovery('healthy-project');
    const healthyResult = await scan(resolve(FIXTURES, 'healthy-project'), discovery);
    assert.ok(result.findings.length > healthyResult.findings.length);
  });
});

describe('GAP scanner — empty project', () => {
  let result;
  beforeEach(async () => {
    resetCounter();
    const discovery = await fixtureDiscovery('empty-project');
    result = await scan(resolve(FIXTURES, 'empty-project'), discovery);
  });

  it('returns status ok (never skips)', () => {
    assert.equal(result.status, 'ok');
  });

  it('has multiple medium findings (T1 gaps)', () => {
    const mediums = result.findings.filter(f => f.severity === 'medium');
    assert.ok(mediums.length >= 1);
  });

  it('all findings have category field', () => {
    for (const f of result.findings) {
      assert.ok(f.category, `Finding ${f.id} missing category`);
      assert.match(f.category, /^t[1-4]$/);
    }
  });

  it('reports T1 gaps including missing CLAUDE.md', () => {
    // CA-GAP-001 in empty-project = t1_1 (No CLAUDE.md file).
    assert.ok(result.findings.some(f => f.scanner === 'GAP' && f.id === 'CA-GAP-001'));
  });
});

describe('opportunitySummary', () => {
  it('returns empty arrays for no findings', () => {
    const result = opportunitySummary([]);
    assert.deepEqual(result.highImpact, []);
    assert.deepEqual(result.mediumImpact, []);
    assert.deepEqual(result.explore, []);
  });

  it('routes T1 to highImpact', () => {
    const findings = [{ category: 't1', title: 'No CLAUDE.md' }];
    const result = opportunitySummary(findings);
    assert.equal(result.highImpact.length, 1);
    assert.equal(result.mediumImpact.length, 0);
    assert.equal(result.explore.length, 0);
  });

  it('routes T2 to mediumImpact', () => {
    const findings = [{ category: 't2', title: 'Low hook diversity' }];
    const result = opportunitySummary(findings);
    assert.equal(result.highImpact.length, 0);
    assert.equal(result.mediumImpact.length, 1);
  });

  it('routes T3 and T4 to explore', () => {
    const findings = [
      { category: 't3', title: 'No status line' },
      { category: 't4', title: 'No custom plugin' },
    ];
    const result = opportunitySummary(findings);
    assert.equal(result.explore.length, 2);
  });

  it('handles mixed tiers', () => {
    const findings = [
      { category: 't1', title: 'A' },
      { category: 't2', title: 'B' },
      { category: 't2', title: 'C' },
      { category: 't3', title: 'D' },
      { category: 't4', title: 'E' },
    ];
    const result = opportunitySummary(findings);
    assert.equal(result.highImpact.length, 1);
    assert.equal(result.mediumImpact.length, 2);
    assert.equal(result.explore.length, 2);
  });
});
