// posture.test.mjs — Tests for the deterministic posture scanner
// Tests against fixtures in tests/fixtures/posture-scan/ with:
//   - grade-a-project: full security config (hooks, settings, CLAUDE.md) → Grade A
//   - grade-f-project: dangerous flags, poisoned memory, no hooks → Grade F

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 } from '../../scanners/posture-scanner.mjs';

const __dirname = fileURLToPath(new URL('.', import.meta.url));
const GRADE_A_FIXTURE = resolve(__dirname, '../fixtures/posture-scan/grade-a-project');
const GRADE_F_FIXTURE = resolve(__dirname, '../fixtures/posture-scan/grade-f-project');

// ---------------------------------------------------------------------------
// Grade A project — well-configured, should get Grade A
// ---------------------------------------------------------------------------

describe('posture-scanner: grade-a-project', () => {
  let result;

  beforeEach(async () => {
    resetCounter();
    result = await scan(GRADE_A_FIXTURE);
  });

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

  it('has scanner name', () => {
    assert.equal(result.scanner, 'posture-scanner');
  });

  it('has version', () => {
    assert.ok(result.version, 'Expected version string');
  });

  it('produces Grade A', () => {
    assert.equal(result.scoring.grade, 'A');
  });

  it('has 16 categories assessed', () => {
    assert.equal(result.categories.length, 16);
  });

  it('has low risk score', () => {
    assert.ok(result.risk.score <= 25, `Expected risk score <= 25, got ${result.risk.score}`);
  });

  it('verdict is ALLOW or WARNING', () => {
    assert.ok(
      result.risk.verdict === 'ALLOW' || result.risk.verdict === 'WARNING',
      `Expected ALLOW or WARNING, got ${result.risk.verdict}`,
    );
  });

  it('deny-first configuration is PASS', () => {
    const cat = result.categories.find(c => c.id === 1);
    assert.equal(cat.status, 'PASS');
  });

  it('secrets protection is PASS', () => {
    const cat = result.categories.find(c => c.id === 2);
    assert.equal(cat.status, 'PASS');
  });

  it('path guarding is PASS', () => {
    const cat = result.categories.find(c => c.id === 3);
    assert.equal(cat.status, 'PASS');
  });

  it('MCP trust is N/A (no MCP servers)', () => {
    const cat = result.categories.find(c => c.id === 4);
    assert.equal(cat.status, 'N_A');
  });

  it('destructive blocking is PASS', () => {
    const cat = result.categories.find(c => c.id === 5);
    assert.equal(cat.status, 'PASS');
  });

  it('sandbox configuration is PASS', () => {
    const cat = result.categories.find(c => c.id === 6);
    assert.equal(cat.status, 'PASS');
  });

  it('cognitive state security is PASS', () => {
    const cat = result.categories.find(c => c.id === 10);
    assert.equal(cat.status, 'PASS');
  });

  it('zero critical findings', () => {
    assert.equal(result.counts.critical, 0);
  });

  it('pass rate >= 0.89', () => {
    assert.ok(result.scoring.pass_rate >= 0.89, `Expected pass_rate >= 0.89, got ${result.scoring.pass_rate}`);
  });

  it('duration_ms is a non-negative number', () => {
    assert.ok(typeof result.duration_ms === 'number');
    assert.ok(result.duration_ms >= 0);
  });

  it('timestamp is ISO format', () => {
    assert.ok(result.timestamp.match(/^\d{4}-\d{2}-\d{2}T/), 'Expected ISO timestamp');
  });

  it('each category has required fields', () => {
    for (const cat of result.categories) {
      assert.ok(cat.id, 'Category missing id');
      assert.ok(cat.name, 'Category missing name');
      assert.ok(cat.owasp, 'Category missing owasp');
      assert.ok(cat.status, 'Category missing status');
      assert.ok(typeof cat.findings_count === 'number', 'Category missing findings_count');
      assert.ok(Array.isArray(cat.evidence), 'Category missing evidence array');
    }
  });

  // v5.0 new categories
  it('prompt injection hardening is PASS', () => {
    const cat = result.categories.find(c => c.id === 11);
    assert.equal(cat.status, 'PASS');
  });

  it('Rule of Two is PASS', () => {
    const cat = result.categories.find(c => c.id === 12);
    assert.equal(cat.status, 'PASS');
  });

  it('long-horizon monitoring is PASS', () => {
    const cat = result.categories.find(c => c.id === 13);
    assert.equal(cat.status, 'PASS');
  });

  it('new categories have correct names', () => {
    const cat11 = result.categories.find(c => c.id === 11);
    const cat12 = result.categories.find(c => c.id === 12);
    const cat13 = result.categories.find(c => c.id === 13);
    assert.equal(cat11.name, 'Prompt Injection Hardening');
    assert.equal(cat12.name, 'Rule of Two');
    assert.equal(cat13.name, 'Long-Horizon Monitoring');
  });

  it('new categories have OWASP mappings', () => {
    const cat11 = result.categories.find(c => c.id === 11);
    const cat12 = result.categories.find(c => c.id === 12);
    const cat13 = result.categories.find(c => c.id === 13);
    assert.ok(cat11.owasp.includes('LLM01'), 'Cat 11 should map to LLM01');
    assert.ok(cat12.owasp.includes('ASI02'), 'Cat 12 should map to ASI02');
    assert.ok(cat13.owasp.includes('ASI06'), 'Cat 13 should map to ASI06');
  });

  // v6.0 compliance categories
  it('EU AI Act Compliance category exists', () => {
    const cat = result.categories.find(c => c.id === 14);
    assert.ok(cat, 'Category 14 should exist');
    assert.equal(cat.name, 'EU AI Act Compliance');
  });

  it('NIST AI RMF Alignment category exists', () => {
    const cat = result.categories.find(c => c.id === 15);
    assert.ok(cat, 'Category 15 should exist');
    assert.equal(cat.name, 'NIST AI RMF Alignment');
  });

  it('ISO 42001 Readiness category exists', () => {
    const cat = result.categories.find(c => c.id === 16);
    assert.ok(cat, 'Category 16 should exist');
    assert.equal(cat.name, 'ISO 42001 Readiness');
  });

  it('compliance categories are PARTIAL for grade-a (has hooks+config but no reports/tests)', () => {
    for (const id of [14, 15, 16]) {
      const cat = result.categories.find(c => c.id === id);
      assert.ok(
        cat.status === 'PASS' || cat.status === 'PARTIAL',
        `Category ${id} (${cat.name}) should be PASS or PARTIAL, got ${cat.status}`,
      );
    }
  });

  it('compliance categories have Governance OWASP mapping', () => {
    for (const id of [14, 15, 16]) {
      const cat = result.categories.find(c => c.id === id);
      assert.ok(cat.owasp, `Category ${id} should have owasp mapping`);
    }
  });
});

// ---------------------------------------------------------------------------
// Grade F project — dangerous config, poisoned memory → Grade F
// ---------------------------------------------------------------------------

describe('posture-scanner: grade-f-project', () => {
  let result;

  beforeEach(async () => {
    resetCounter();
    result = await scan(GRADE_F_FIXTURE);
  });

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

  it('produces Grade F', () => {
    assert.equal(result.scoring.grade, 'F');
  });

  it('has high risk score (>= 40)', () => {
    assert.ok(result.risk.score >= 40, `Expected risk >= 40, got ${result.risk.score}`);
  });

  it('verdict is BLOCK or WARNING', () => {
    assert.ok(
      result.risk.verdict === 'BLOCK' || result.risk.verdict === 'WARNING',
      `Expected BLOCK or WARNING, got ${result.risk.verdict}`,
    );
  });

  it('deny-first configuration is FAIL', () => {
    const cat = result.categories.find(c => c.id === 1);
    assert.equal(cat.status, 'FAIL');
  });

  it('secrets protection is FAIL', () => {
    const cat = result.categories.find(c => c.id === 2);
    assert.equal(cat.status, 'FAIL');
  });

  it('destructive blocking is FAIL', () => {
    const cat = result.categories.find(c => c.id === 5);
    assert.equal(cat.status, 'FAIL');
  });

  it('sandbox configuration is FAIL', () => {
    const cat = result.categories.find(c => c.id === 6);
    assert.equal(cat.status, 'FAIL');
  });

  it('cognitive state security is FAIL', () => {
    const cat = result.categories.find(c => c.id === 10);
    assert.equal(cat.status, 'FAIL');
  });

  it('has multiple FAIL categories', () => {
    const fails = result.categories.filter(c => c.status === 'FAIL');
    assert.ok(fails.length >= 7, `Expected >= 7 FAIL categories, got ${fails.length}`);
  });

  it('has critical findings', () => {
    assert.ok(result.counts.critical >= 1, `Expected >= 1 critical findings, got ${result.counts.critical}`);
  });

  it('has high findings', () => {
    assert.ok(result.counts.high >= 1, `Expected >= 1 high findings, got ${result.counts.high}`);
  });

  it('findings have PST scanner prefix', () => {
    const wrongPrefix = result.findings.filter(f => !f.id.startsWith('DS-PST-'));
    assert.equal(
      wrongPrefix.length, 0,
      `All findings should have DS-PST- prefix. Wrong: ${wrongPrefix.map(f => f.id).join(', ')}`,
    );
  });

  it('findings have required fields', () => {
    for (const f of result.findings) {
      assert.ok(f.id, `Finding missing id`);
      assert.ok(f.scanner === 'PST', `Finding ${f.id} scanner should be PST, got ${f.scanner}`);
      assert.ok(f.severity, `Finding ${f.id} missing severity`);
      assert.ok(f.title, `Finding ${f.id} missing title`);
      assert.ok(f.owasp, `Finding ${f.id} missing owasp`);
    }
  });

  it('finding IDs are sequential starting from DS-PST-001', () => {
    if (result.findings.length === 0) return;
    assert.equal(result.findings[0].id, 'DS-PST-001');
  });

  it('severity counts sum to total findings', () => {
    const { counts } = result;
    const total = counts.critical + counts.high + counts.medium + counts.low + counts.info;
    assert.equal(total, result.findings.length, 'Severity counts should sum to total findings');
  });

  it('pass_rate < 0.33', () => {
    assert.ok(result.scoring.pass_rate < 0.33, `Expected pass_rate < 0.33, got ${result.scoring.pass_rate}`);
  });

  // v5.0 new categories
  it('prompt injection hardening is FAIL', () => {
    const cat = result.categories.find(c => c.id === 11);
    assert.equal(cat.status, 'FAIL');
  });

  it('Rule of Two is FAIL', () => {
    const cat = result.categories.find(c => c.id === 12);
    assert.equal(cat.status, 'FAIL');
  });

  it('long-horizon monitoring is FAIL', () => {
    const cat = result.categories.find(c => c.id === 13);
    assert.equal(cat.status, 'FAIL');
  });

  // v6.0 compliance categories — grade-f has no security config → FAIL
  it('EU AI Act Compliance is FAIL', () => {
    const cat = result.categories.find(c => c.id === 14);
    assert.ok(cat, 'Category 14 should exist');
    assert.equal(cat.status, 'FAIL');
  });

  it('NIST AI RMF Alignment is FAIL', () => {
    const cat = result.categories.find(c => c.id === 15);
    assert.ok(cat, 'Category 15 should exist');
    assert.equal(cat.status, 'FAIL');
  });

  it('ISO 42001 Readiness is FAIL', () => {
    const cat = result.categories.find(c => c.id === 16);
    assert.ok(cat, 'Category 16 should exist');
    assert.equal(cat.status, 'FAIL');
  });
});

// ---------------------------------------------------------------------------
// Scanner interface compliance
// ---------------------------------------------------------------------------

describe('posture-scanner: interface', () => {
  it('scan() accepts a path string and returns a result', async () => {
    resetCounter();
    const result = await scan(GRADE_A_FIXTURE);
    assert.ok(result);
    assert.equal(typeof result, 'object');
  });

  it('result has scoring block', async () => {
    resetCounter();
    const result = await scan(GRADE_A_FIXTURE);
    assert.ok(result.scoring);
    assert.ok(typeof result.scoring.pass === 'number');
    assert.ok(typeof result.scoring.partial === 'number');
    assert.ok(typeof result.scoring.fail === 'number');
    assert.ok(typeof result.scoring.na === 'number');
    assert.ok(typeof result.scoring.applicable === 'number');
    assert.ok(typeof result.scoring.score === 'number');
    assert.ok(typeof result.scoring.pass_rate === 'number');
    assert.ok(typeof result.scoring.grade === 'string');
  });

  it('result has risk block', async () => {
    resetCounter();
    const result = await scan(GRADE_A_FIXTURE);
    assert.ok(result.risk);
    assert.ok(typeof result.risk.score === 'number');
    assert.ok(typeof result.risk.band === 'string');
    assert.ok(typeof result.risk.verdict === 'string');
  });

  it('result has counts block', async () => {
    resetCounter();
    const result = await scan(GRADE_A_FIXTURE);
    assert.ok(result.counts);
    assert.ok(typeof result.counts.critical === 'number');
    assert.ok(typeof result.counts.high === 'number');
    assert.ok(typeof result.counts.medium === 'number');
    assert.ok(typeof result.counts.low === 'number');
    assert.ok(typeof result.counts.info === 'number');
  });

  it('completes in under 2 seconds', async () => {
    resetCounter();
    const start = Date.now();
    await scan(GRADE_A_FIXTURE);
    const elapsed = Date.now() - start;
    assert.ok(elapsed < 2000, `Expected < 2000ms, took ${elapsed}ms`);
  });
});
