/**
 * tests/unit/harness-audit-categories.test.mjs
 *
 * Unit tests for the 7 exported scoring functions in
 * scripts/lib/harness-audit/categories.mjs (issue #210).
 *
 * Each category is tested with:
 *   - PASS state  → all required files in correct shape
 *   - PARTIAL state → some files present but with wrong/missing content
 *   - FAIL state  → required files absent entirely
 *
 * Tests use tmpdir-based isolation. Never touches the host repo or network.
 */

import { describe, it, expect, afterEach } from 'vitest';
import { mkdtempSync, writeFileSync, mkdirSync, rmSync } from 'node:fs';
import { join } from 'node:path';
import { tmpdir } from 'node:os';
import { fileURLToPath } from 'node:url';

import {
  runCategory1,
  runCategory2,
  runCategory3,
  runCategory4,
  runCategory5,
  runCategory6,
  runCategory7,
  runCategory8,
} from '@lib/harness-audit/categories.mjs';

// ---------------------------------------------------------------------------
// Constants
// ---------------------------------------------------------------------------

const FIXTURE_ROOT = fileURLToPath(
  new URL('../fixtures/harness-audit/clean-repo', import.meta.url),
);

// ---------------------------------------------------------------------------
// Helpers
// ---------------------------------------------------------------------------

/** Create a fresh isolated tmpdir for one test. */
function makeTmp() {
  return mkdtempSync(join(tmpdir(), 'harness-audit-test-'));
}

/** Recursively create directories and write a file. */
function write(root, relPath, content) {
  const abs = join(root, relPath);
  const parts = relPath.split('/');
  if (parts.length > 1) {
    mkdirSync(join(root, ...parts.slice(0, -1)), { recursive: true });
  }
  writeFileSync(abs, content, 'utf8');
}

/** Sum points_earned across a checks array (returned by runCategoryN). */
function totalPoints(checks) {
  return checks.reduce((sum, c) => sum + (c.points ?? 0), 0);
}

/** Sum max_points across a checks array. */
function maxPoints(checks) {
  return checks.reduce((sum, c) => sum + (c.max_points ?? 0), 0);
}

// ---------------------------------------------------------------------------
// Shared tmpdir registry — cleaned up in afterEach
// ---------------------------------------------------------------------------

let _tmpdirs = [];

afterEach(() => {
  for (const d of _tmpdirs) {
    try { rmSync(d, { recursive: true, force: true }); } catch { /* best-effort */ }
  }
  _tmpdirs = [];
});

function tmp() {
  const d = makeTmp();
  _tmpdirs.push(d);
  return d;
}

// ---------------------------------------------------------------------------
// Category 1: Session Discipline
// ---------------------------------------------------------------------------

describe('category 1: Session Discipline', () => {
  it('passes when all required files are present and well-formed', () => {
    const root = tmp();

    // STATE.md with full frontmatter
    write(root, '.claude/STATE.md', [
      '---',
      'schema-version: 1',
      'session-type: feature',
      'branch: main',
      'status: completed',
      'current-wave: 3',
      'total-waves: 3',
      '---',
      '',
      '## Current Wave',
    ].join('\n'));

    // sessions.jsonl — 2 valid lines with required keys
    const s1 = JSON.stringify({ session_id: 's1', session_type: 'feature', started_at: '2026-04-20T08:00:00Z' });
    const s2 = JSON.stringify({ session_id: 's2', session_type: 'housekeeping', started_at: '2026-04-21T08:00:00Z' });
    write(root, '.orchestrator/metrics/sessions.jsonl', s1 + '\n' + s2 + '\n');

    // learnings.jsonl — 1 valid line with required keys
    const l1 = JSON.stringify({ type: 'architectural', subject: 'x', confidence: 0.8 });
    write(root, '.orchestrator/metrics/learnings.jsonl', l1 + '\n');

    // .orchestrator layout
    write(root, '.orchestrator/bootstrap.lock', 'version: 1\ntier: standard\narchetype: node\n');
    mkdirSync(join(root, '.orchestrator/policy'), { recursive: true });

    const checks = runCategory1(root);
    const earned = totalPoints(checks);
    const possible = maxPoints(checks);

    expect(possible).toBe(10);
    expect(earned).toBeGreaterThanOrEqual(8);
  });

  it('returns partial score when STATE.md is present but missing frontmatter keys', () => {
    const root = tmp();

    // STATE.md with incomplete frontmatter — schema-version missing
    write(root, '.claude/STATE.md', [
      '---',
      'session-type: feature',
      'status: active',
      '---',
      '',
      '## Current Wave',
    ].join('\n'));

    // sessions.jsonl — 2 valid lines
    const s1 = JSON.stringify({ session_id: 's1', session_type: 'feature', started_at: '2026-04-20T08:00:00Z' });
    const s2 = JSON.stringify({ session_id: 's2', session_type: 'housekeeping', started_at: '2026-04-21T08:00:00Z' });
    write(root, '.orchestrator/metrics/sessions.jsonl', s1 + '\n' + s2 + '\n');

    const l1 = JSON.stringify({ type: 'architectural', subject: 'x', confidence: 0.8 });
    write(root, '.orchestrator/metrics/learnings.jsonl', l1 + '\n');

    write(root, '.orchestrator/bootstrap.lock', 'version: 1\ntier: standard\narchetype: node\n');
    mkdirSync(join(root, '.orchestrator/policy'), { recursive: true });

    const checks = runCategory1(root);
    // state-md-present fails (0/3), but sessions+learnings+layout pass (3+2+2=7)
    const earned = totalPoints(checks);
    expect(earned).toBeGreaterThanOrEqual(5);
    expect(earned).toBeLessThan(10);
  });

  it('scores 0 when all required files are absent', () => {
    const root = tmp();
    // Empty directory — no files at all
    const checks = runCategory1(root);
    const earned = totalPoints(checks);
    expect(earned).toBe(0);
  });

  it('fails state-md-present when STATE.md has no frontmatter delimiters', () => {
    const root = tmp();
    write(root, '.claude/STATE.md', 'No YAML frontmatter here\n');

    const checks = runCategory1(root);
    const stateMdCheck = checks.find((c) => c.check_id === 'state-md-present');
    expect(stateMdCheck).toBeDefined();
    expect(stateMdCheck.status).toBe('fail');
    expect(stateMdCheck.points).toBe(0);
  });

  it('fails sessions-jsonl-growth when only 1 line exists', () => {
    const root = tmp();
    const s1 = JSON.stringify({ session_id: 's1', session_type: 'feature', started_at: '2026-04-20T08:00:00Z' });
    write(root, '.orchestrator/metrics/sessions.jsonl', s1 + '\n');

    const checks = runCategory1(root);
    const sessionsCheck = checks.find((c) => c.check_id === 'sessions-jsonl-growth');
    expect(sessionsCheck).toBeDefined();
    expect(sessionsCheck.status).toBe('fail');
    expect(sessionsCheck.points).toBe(0);
  });

  it('fails orchestrator-layout when bootstrap.lock is missing', () => {
    const root = tmp();
    // Create only the .orchestrator dir and metrics/policy — but NOT bootstrap.lock
    mkdirSync(join(root, '.orchestrator/policy'), { recursive: true });
    mkdirSync(join(root, '.orchestrator/metrics'), { recursive: true });

    const checks = runCategory1(root);
    const layoutCheck = checks.find((c) => c.check_id === 'orchestrator-layout');
    expect(layoutCheck).toBeDefined();
    expect(layoutCheck.status).toBe('fail');
    expect(layoutCheck.points).toBe(0);
  });
});

// ---------------------------------------------------------------------------
// Category 2: Quality Gate Coverage
// ---------------------------------------------------------------------------

describe('category 2: Quality Gate Coverage', () => {
  it('passes when package.json has all three scripts and bootstrap.lock is valid', () => {
    const root = tmp();

    write(root, 'package.json', JSON.stringify({
      scripts: { test: 'vitest --run', typecheck: 'tsc --noEmit', lint: 'eslint .' },
    }));

    write(root, '.orchestrator/bootstrap.lock',
      'version: 1\ntier: standard\narchetype: node-minimal\n');

    // schema-drift-ci check — provide a GitHub workflow
    mkdirSync(join(root, '.github/workflows'), { recursive: true });
    write(root, '.github/workflows/ci.yml', 'name: CI\njobs:\n  schema-drift-check:\n    runs-on: ubuntu-latest\n');

    const checks = runCategory2(root);
    const earned = totalPoints(checks);
    const possible = maxPoints(checks);
    expect(possible).toBe(10);
    expect(earned).toBeGreaterThanOrEqual(8);
  });

  it('returns partial score when package.json is missing lint script', () => {
    const root = tmp();

    write(root, 'package.json', JSON.stringify({
      scripts: { test: 'vitest --run', typecheck: 'tsc --noEmit' },
    }));

    write(root, '.orchestrator/bootstrap.lock',
      'version: 1\ntier: standard\narchetype: node-minimal\n');

    const checks = runCategory2(root);
    const pkgCheck = checks.find((c) => c.check_id === 'package-json-scripts');
    expect(pkgCheck).toBeDefined();
    expect(pkgCheck.status).toBe('fail');

    const earned = totalPoints(checks);
    // bootstrap-lock passes (3) + quality-gates-policy absent = pass (2) = 5
    expect(earned).toBeGreaterThanOrEqual(2);
    expect(earned).toBeLessThan(10);
  });

  it('scores 0 for package-json-scripts when package.json is absent', () => {
    const root = tmp();
    // No files at all
    const checks = runCategory2(root);
    const pkgCheck = checks.find((c) => c.check_id === 'package-json-scripts');
    expect(pkgCheck).toBeDefined();
    expect(pkgCheck.status).toBe('fail');
    expect(pkgCheck.points).toBe(0);
  });

  it('fails bootstrap-lock-schema when tier is invalid', () => {
    const root = tmp();
    write(root, 'package.json', JSON.stringify({
      scripts: { test: 'vitest --run', typecheck: 'tsc --noEmit', lint: 'eslint .' },
    }));
    // Invalid tier value
    write(root, '.orchestrator/bootstrap.lock',
      'version: 1\ntier: turbo\narchetype: node-minimal\n');

    const checks = runCategory2(root);
    const lockCheck = checks.find((c) => c.check_id === 'bootstrap-lock-schema');
    expect(lockCheck).toBeDefined();
    expect(lockCheck.status).toBe('fail');
    expect(lockCheck.points).toBe(0);
  });

  it('passes quality-gates-policy when quality-gates.json is absent (optional check)', () => {
    const root = tmp();
    // Absent file counts as skip/pass per #183 fallback chain
    const checks = runCategory2(root);
    const qgCheck = checks.find((c) => c.check_id === 'quality-gates-policy');
    expect(qgCheck).toBeDefined();
    expect(qgCheck.status).toBe('pass');
    expect(qgCheck.points).toBe(2);
  });

  it('fails quality-gates-policy when quality-gates.json is present but invalid JSON', () => {
    const root = tmp();
    mkdirSync(join(root, '.orchestrator/policy'), { recursive: true });
    write(root, '.orchestrator/policy/quality-gates.json', '{ invalid json }');

    const checks = runCategory2(root);
    const qgCheck = checks.find((c) => c.check_id === 'quality-gates-policy');
    expect(qgCheck).toBeDefined();
    expect(qgCheck.status).toBe('fail');
    expect(qgCheck.points).toBe(0);
  });

  it('passes schema-drift-ci when gitlab-ci.yml contains schema-drift-check', () => {
    const root = tmp();
    write(root, '.gitlab-ci.yml', 'schema-drift-check:\n  stage: lint\n');

    const checks = runCategory2(root);
    const driftCheck = checks.find((c) => c.check_id === 'schema-drift-ci');
    expect(driftCheck).toBeDefined();
    expect(driftCheck.status).toBe('pass');
    expect(driftCheck.points).toBe(2);
  });

  it('fails schema-drift-ci when no CI config references schema-drift', () => {
    const root = tmp();
    write(root, '.gitlab-ci.yml', 'stages:\n  - test\n\nbuild:\n  script: npm run build\n');

    const checks = runCategory2(root);
    const driftCheck = checks.find((c) => c.check_id === 'schema-drift-ci');
    expect(driftCheck).toBeDefined();
    expect(driftCheck.status).toBe('fail');
    expect(driftCheck.points).toBe(0);
  });
});

// ---------------------------------------------------------------------------
// Category 3: Hook Integrity
// ---------------------------------------------------------------------------

describe('category 3: Hook Integrity', () => {
  it('passes when hooks.json is valid with matchers and hook files exist', () => {
    const root = tmp();

    mkdirSync(join(root, 'hooks'), { recursive: true });

    // Valid hooks.json pointing to real .mjs files that will be created
    write(root, 'hooks/hooks.json', JSON.stringify({
      hooks: {
        SessionStart: [
          { matcher: 'startup', hooks: [{ type: 'command', command: 'node "$CLAUDE_PLUGIN_ROOT/hooks/on-session-start.mjs"' }] },
        ],
        PreToolUse: [
          { matcher: 'Bash', hooks: [{ type: 'command', command: 'node "$CLAUDE_PLUGIN_ROOT/hooks/pre-bash-destructive-guard.mjs"' }] },
        ],
      },
    }));

    // Hook files referenced by hooks.json
    write(root, 'hooks/on-session-start.mjs', '#!/usr/bin/env node\nprocess.exit(0);\n');
    write(root, 'hooks/pre-bash-destructive-guard.mjs',
      '#!/usr/bin/env node\n// References blocked-commands.json\nprocess.exit(0);\n');

    const checks = runCategory3(root);
    const earned = totalPoints(checks);
    const possible = maxPoints(checks);
    expect(possible).toBe(10);
    expect(earned).toBeGreaterThanOrEqual(8);
  });

  it('returns partial score when hooks.json is valid but a referenced hook file is missing', () => {
    const root = tmp();

    mkdirSync(join(root, 'hooks'), { recursive: true });

    write(root, 'hooks/hooks.json', JSON.stringify({
      hooks: {
        SessionStart: [
          { matcher: 'startup', hooks: [{ type: 'command', command: 'node "$CLAUDE_PLUGIN_ROOT/hooks/missing-hook.mjs"' }] },
        ],
      },
    }));

    // Do NOT create hooks/missing-hook.mjs

    const checks = runCategory3(root);
    const hookFilesCheck = checks.find((c) => c.check_id === 'hook-files-exist');
    expect(hookFilesCheck).toBeDefined();
    expect(hookFilesCheck.status).toBe('fail');

    const earned = totalPoints(checks);
    expect(earned).toBeGreaterThanOrEqual(3); // hooks-json-valid (3) should still pass
    expect(earned).toBeLessThan(10);
  });

  it('scores 0 for hooks-json-valid when hooks.json is absent', () => {
    const root = tmp();
    const checks = runCategory3(root);
    const hooksCheck = checks.find((c) => c.check_id === 'hooks-json-valid');
    expect(hooksCheck).toBeDefined();
    expect(hooksCheck.status).toBe('fail');
    expect(hooksCheck.points).toBe(0);
  });

  it('fails hooks-json-valid when hooks.json has no matcher blocks', () => {
    const root = tmp();
    write(root, 'hooks/hooks.json', JSON.stringify({ hooks: {} }));

    const checks = runCategory3(root);
    const hooksCheck = checks.find((c) => c.check_id === 'hooks-json-valid');
    expect(hooksCheck).toBeDefined();
    expect(hooksCheck.status).toBe('fail');
    expect(hooksCheck.points).toBe(0);
  });

  it('fails destructive-guard-loads-policy when pre-bash-destructive-guard.mjs is absent', () => {
    const root = tmp();
    const checks = runCategory3(root);
    const guardCheck = checks.find((c) => c.check_id === 'destructive-guard-loads-policy');
    expect(guardCheck).toBeDefined();
    expect(guardCheck.status).toBe('fail');
    expect(guardCheck.points).toBe(0);
  });

  it('fails destructive-guard-loads-policy when guard file does not reference policy', () => {
    const root = tmp();
    mkdirSync(join(root, 'hooks'), { recursive: true });
    // Guard file with no reference to blocked-commands.json or .orchestrator/policy
    write(root, 'hooks/pre-bash-destructive-guard.mjs',
      '#!/usr/bin/env node\nprocess.exit(0);\n');

    const checks = runCategory3(root);
    const guardCheck = checks.find((c) => c.check_id === 'destructive-guard-loads-policy');
    expect(guardCheck).toBeDefined();
    expect(guardCheck.status).toBe('fail');
    expect(guardCheck.points).toBe(0);
  });

  it('passes destructive-guard-loads-policy when guard file references blocked-commands.json', () => {
    const root = tmp();
    mkdirSync(join(root, 'hooks'), { recursive: true });
    write(root, 'hooks/pre-bash-destructive-guard.mjs',
      '#!/usr/bin/env node\nimport { readFileSync } from "node:fs";\nconst p = readFileSync("blocked-commands.json", "utf8");\nprocess.exit(0);\n');

    const checks = runCategory3(root);
    const guardCheck = checks.find((c) => c.check_id === 'destructive-guard-loads-policy');
    expect(guardCheck).toBeDefined();
    expect(guardCheck.status).toBe('pass');
    expect(guardCheck.points).toBe(2);
  });

  it('fails hook-mjs-syntax when a hook file has a syntax error', () => {
    const root = tmp();
    mkdirSync(join(root, 'hooks'), { recursive: true });

    // Invalid JavaScript syntax
    write(root, 'hooks/bad-hook.mjs', 'this is not valid javascript @@@ <<<\n');

    // hooks.json is not strictly needed for syntax check — it scans hooks/*.mjs
    write(root, 'hooks/hooks.json', JSON.stringify({
      hooks: {
        SessionStart: [{ matcher: 'startup', hooks: [{ type: 'command', command: 'node "$CLAUDE_PLUGIN_ROOT/hooks/bad-hook.mjs"' }] }],
      },
    }));

    const checks = runCategory3(root);
    const syntaxCheck = checks.find((c) => c.check_id === 'hook-mjs-syntax');
    expect(syntaxCheck).toBeDefined();
    expect(syntaxCheck.status).toBe('fail');
    expect(syntaxCheck.points).toBe(0);
  });
});

// ---------------------------------------------------------------------------
// Category 4: Persistence Health
// ---------------------------------------------------------------------------

describe('category 4: Persistence Health', () => {
  it('passes when STATE.md has all required keys and sessions.jsonl has a recent entry', () => {
    const root = tmp();

    write(root, '.claude/STATE.md', [
      '---',
      'schema-version: 1',
      'session-type: feature',
      'branch: main',
      'status: completed',
      'current-wave: 3',
      'total-waves: 3',
      '---',
      '',
    ].join('\n'));

    // sessions.jsonl — last entry completed today
    const today = new Date().toISOString();
    const s1 = JSON.stringify({ session_id: 's1', session_type: 'feature', started_at: '2026-04-20T08:00:00Z', completed_at: today });
    write(root, '.orchestrator/metrics/sessions.jsonl', s1 + '\n');

    // learnings.jsonl — valid with expires_at and confidence
    const l1 = JSON.stringify({ type: 'architectural', subject: 'x', confidence: 0.8, expires_at: '2027-01-01T00:00:00Z' });
    write(root, '.orchestrator/metrics/learnings.jsonl', l1 + '\n');

    const checks = runCategory4(root);
    const earned = totalPoints(checks);
    const possible = maxPoints(checks);
    expect(possible).toBe(10);
    expect(earned).toBeGreaterThanOrEqual(8);
  });

  it('returns partial score when STATE.md is missing required keys', () => {
    const root = tmp();

    // STATE.md missing current-wave and total-waves
    write(root, '.claude/STATE.md', [
      '---',
      'schema-version: 1',
      'session-type: feature',
      'branch: main',
      'status: completed',
      '---',
      '',
    ].join('\n'));

    const today = new Date().toISOString();
    const s1 = JSON.stringify({ session_id: 's1', session_type: 'feature', started_at: '2026-04-20T08:00:00Z', completed_at: today });
    write(root, '.orchestrator/metrics/sessions.jsonl', s1 + '\n');

    const l1 = JSON.stringify({ type: 'architectural', subject: 'x', confidence: 0.8, expires_at: '2027-01-01T00:00:00Z' });
    write(root, '.orchestrator/metrics/learnings.jsonl', l1 + '\n');

    const checks = runCategory4(root);
    const stateMdCheck = checks.find((c) => c.check_id === 'state-md-schema');
    expect(stateMdCheck).toBeDefined();
    expect(stateMdCheck.status).toBe('fail');

    const earned = totalPoints(checks);
    expect(earned).toBeGreaterThanOrEqual(3);
    expect(earned).toBeLessThan(10);
  });

  it('scores 0 when all required persistence files are absent', () => {
    const root = tmp();
    const checks = runCategory4(root);
    const earned = totalPoints(checks);
    // Only vault-sync-validator can still pass (when vault not enabled)
    expect(earned).toBeLessThanOrEqual(2);
  });

  it('fails sessions-jsonl-recent when last entry is older than 30 days', () => {
    const root = tmp();
    // Date well in the past
    const oldDate = '2020-01-01T00:00:00Z';
    const s1 = JSON.stringify({ session_id: 's1', session_type: 'feature', started_at: '2020-01-01T08:00:00Z', completed_at: oldDate });
    write(root, '.orchestrator/metrics/sessions.jsonl', s1 + '\n');

    const checks = runCategory4(root);
    const recentCheck = checks.find((c) => c.check_id === 'sessions-jsonl-recent');
    expect(recentCheck).toBeDefined();
    expect(recentCheck.status).toBe('fail');
    expect(recentCheck.points).toBe(0);
  });

  it('fails learnings-prunable when learnings lack expires_at', () => {
    const root = tmp();
    // Learning without expires_at field
    const l1 = JSON.stringify({ type: 'architectural', subject: 'x', confidence: 0.8 });
    write(root, '.orchestrator/metrics/learnings.jsonl', l1 + '\n');

    const checks = runCategory4(root);
    const prunableCheck = checks.find((c) => c.check_id === 'learnings-prunable');
    expect(prunableCheck).toBeDefined();
    expect(prunableCheck.status).toBe('fail');
    expect(prunableCheck.points).toBe(0);
  });

  it('fails learnings-prunable when confidence is out of range', () => {
    const root = tmp();
    // confidence > 1 is invalid
    const l1 = JSON.stringify({ type: 'architectural', subject: 'x', confidence: 1.5, expires_at: '2027-01-01T00:00:00Z' });
    write(root, '.orchestrator/metrics/learnings.jsonl', l1 + '\n');

    const checks = runCategory4(root);
    const prunableCheck = checks.find((c) => c.check_id === 'learnings-prunable');
    expect(prunableCheck).toBeDefined();
    expect(prunableCheck.status).toBe('fail');
    expect(prunableCheck.points).toBe(0);
  });

  it('passes vault-sync-validator when vault-integration is not enabled', () => {
    const root = tmp();
    // No CLAUDE.md — vault not enabled
    const checks = runCategory4(root);
    const vaultCheck = checks.find((c) => c.check_id === 'vault-sync-validator');
    expect(vaultCheck).toBeDefined();
    expect(vaultCheck.status).toBe('pass');
    expect(vaultCheck.points).toBe(2);
  });
});

// ---------------------------------------------------------------------------
// Category 5: Plugin-Root Resolution
// ---------------------------------------------------------------------------

describe('category 5: Plugin-Root Resolution', () => {
  it('passes when all 3 env vars are wired and doc files are present', () => {
    const root = tmp();

    // Wire all 3 env vars in platform.mjs
    write(root, 'scripts/lib/platform.mjs', [
      'export function detect() {',
      '  if (process.env.CLAUDE_PLUGIN_ROOT) return "claude";',
      '  if (process.env.CODEX_PLUGIN_ROOT) return "codex";',
      '  if (process.env.CURSOR_RULES_DIR) return "cursor";',
      '}',
    ].join('\n'));

    // hooks.json using env var prefix — no absolute paths
    mkdirSync(join(root, 'hooks'), { recursive: true });
    write(root, 'hooks/hooks.json', JSON.stringify({
      hooks: {
        SessionStart: [
          { matcher: 'startup', hooks: [{ type: 'command', command: 'node "$CLAUDE_PLUGIN_ROOT/hooks/on-session-start.mjs"' }] },
        ],
      },
    }));

    // Doc files
    write(root, 'skills/_shared/config-reading.md',
      '# Config Reading\n\nResolve `$PLUGIN_ROOT` via platform env vars.\n');
    write(root, 'skills/_shared/bootstrap-gate.md',
      '# Bootstrap Gate\n\nChecks CLAUDE.md, Session Config, and bootstrap.lock.\n');

    const checks = runCategory5(root);
    const earned = totalPoints(checks);
    const possible = maxPoints(checks);
    expect(possible).toBe(9);
    expect(earned).toBeGreaterThanOrEqual(7);
  });

  it('returns partial score when only 1 of 3 env vars is referenced', () => {
    const root = tmp();

    // Only CLAUDE_PLUGIN_ROOT referenced — missing CODEX and CURSOR
    write(root, 'scripts/lib/platform.mjs',
      'export const root = process.env.CLAUDE_PLUGIN_ROOT;\n');

    mkdirSync(join(root, 'hooks'), { recursive: true });
    write(root, 'hooks/hooks.json', JSON.stringify({
      hooks: {
        SessionStart: [
          { matcher: 'startup', hooks: [{ type: 'command', command: 'node "$CLAUDE_PLUGIN_ROOT/hooks/on-session-start.mjs"' }] },
        ],
      },
    }));

    const checks = runCategory5(root);
    const fallbackCheck = checks.find((c) => c.check_id === 'parse-config-fallback-chain');
    expect(fallbackCheck).toBeDefined();
    expect(fallbackCheck.status).toBe('fail');

    const earned = totalPoints(checks);
    expect(earned).toBeGreaterThanOrEqual(2);
    expect(earned).toBeLessThan(9);
  });

  it('scores 0 for parse-config-fallback-chain when no script files exist', () => {
    const root = tmp();
    const checks = runCategory5(root);
    const fallbackCheck = checks.find((c) => c.check_id === 'parse-config-fallback-chain');
    expect(fallbackCheck).toBeDefined();
    expect(fallbackCheck.status).toBe('fail');
    expect(fallbackCheck.points).toBe(0);
  });

  it('fails hooks-use-plugin-root-var when hook command uses absolute path', () => {
    const root = tmp();
    mkdirSync(join(root, 'hooks'), { recursive: true });
    // Absolute path without env var prefix
    write(root, 'hooks/hooks.json', JSON.stringify({
      hooks: {
        SessionStart: [
          { matcher: 'startup', hooks: [{ type: 'command', command: 'node /absolute/path/hooks/on-session-start.mjs' }] },
        ],
      },
    }));

    const checks = runCategory5(root);
    const hookRootCheck = checks.find((c) => c.check_id === 'hooks-use-plugin-root-var');
    expect(hookRootCheck).toBeDefined();
    expect(hookRootCheck.status).toBe('fail');
    expect(hookRootCheck.points).toBe(0);
  });

  it('fails config-reading-doc when skills/_shared/config-reading.md is absent', () => {
    const root = tmp();
    const checks = runCategory5(root);
    const docCheck = checks.find((c) => c.check_id === 'config-reading-doc');
    expect(docCheck).toBeDefined();
    expect(docCheck.status).toBe('fail');
    expect(docCheck.points).toBe(0);
  });

  it('fails config-reading-doc when config-reading.md does not contain PLUGIN_ROOT', () => {
    const root = tmp();
    write(root, 'skills/_shared/config-reading.md',
      '# Config Reading\n\nNo mention of the env var here.\n');

    const checks = runCategory5(root);
    const docCheck = checks.find((c) => c.check_id === 'config-reading-doc');
    expect(docCheck).toBeDefined();
    expect(docCheck.status).toBe('fail');
    expect(docCheck.points).toBe(0);
  });

  it('fails bootstrap-gate-doc when bootstrap-gate.md is missing required strings', () => {
    const root = tmp();
    // Present but missing "bootstrap.lock"
    write(root, 'skills/_shared/bootstrap-gate.md',
      '# Bootstrap Gate\n\nChecks CLAUDE.md and Session Config only.\n');

    const checks = runCategory5(root);
    const gateCheck = checks.find((c) => c.check_id === 'bootstrap-gate-doc');
    expect(gateCheck).toBeDefined();
    expect(gateCheck.status).toBe('fail');
    expect(gateCheck.points).toBe(0);
  });
});

// ---------------------------------------------------------------------------
// Category 6: Config Hygiene
// ---------------------------------------------------------------------------

describe('category 6: Config Hygiene', () => {
  it('passes when CLAUDE.md is under 250 lines and has no dead branch refs', () => {
    const root = tmp();

    // 5-line CLAUDE.md with v2.0 Features heading (needed for plugin-repo check to pass)
    // but we also create the plugin-repo marker so c6.3 checks apply
    write(root, 'skills/session-start/SKILL.md', '# Session Start Skill\n');
    write(root, 'CLAUDE.md', [
      '# My Project',
      '',
      '## v2.0 Features',
      '',
      '- Session persistence via STATE.md',
    ].join('\n'));

    const checks = runCategory6(root);
    const earned = totalPoints(checks);
    const possible = maxPoints(checks);
    expect(possible).toBe(8);
    expect(earned).toBeGreaterThanOrEqual(6);
  });

  it('returns partial score when CLAUDE.md exceeds 250 lines', () => {
    const root = tmp();
    // Create 300-line CLAUDE.md with no dead branch refs
    const lines = ['# Project'];
    for (let i = 0; i < 299; i++) {
      lines.push(`Line ${i}`);
    }
    write(root, 'CLAUDE.md', lines.join('\n'));

    const checks = runCategory6(root);
    const lineCountCheck = checks.find((c) => c.check_id === 'claude-md-line-count');
    expect(lineCountCheck).toBeDefined();
    expect(lineCountCheck.status).toBe('fail');
    expect(lineCountCheck.points).toBe(0);

    const earned = totalPoints(checks);
    expect(earned).toBeGreaterThanOrEqual(3); // no-dead-branch-refs should still pass
    expect(earned).toBeLessThan(8);
  });

  it('scores 0 for claude-md-line-count and no-dead-branch-refs when CLAUDE.md is absent', () => {
    const root = tmp();
    const checks = runCategory6(root);

    // CLAUDE.md absent → line-count fails (0/3) and dead-branch-refs fails (0/3)
    const lineCountCheck = checks.find((c) => c.check_id === 'claude-md-line-count');
    expect(lineCountCheck).toBeDefined();
    expect(lineCountCheck.status).toBe('fail');
    expect(lineCountCheck.points).toBe(0);

    const deadCheck = checks.find((c) => c.check_id === 'no-dead-branch-refs');
    expect(deadCheck).toBeDefined();
    expect(deadCheck.status).toBe('fail');
    expect(deadCheck.points).toBe(0);

    // plugin-narrative-section passes as "consumer repo skip" (no session-start/SKILL.md either)
    const earned = totalPoints(checks);
    expect(earned).toBe(2);
  });

  it('fails no-dead-branch-refs when CLAUDE.md contains a dead branch pattern', () => {
    const root = tmp();
    write(root, 'CLAUDE.md', [
      '# Project',
      '',
      'Legacy note: see windows-native-v3 branch for old implementation.',
    ].join('\n'));

    const checks = runCategory6(root);
    const deadBranchCheck = checks.find((c) => c.check_id === 'no-dead-branch-refs');
    expect(deadBranchCheck).toBeDefined();
    expect(deadBranchCheck.status).toBe('fail');
    expect(deadBranchCheck.points).toBe(0);
  });

  it('passes no-dead-branch-refs when CLAUDE.md contains none of the dead patterns', () => {
    const root = tmp();
    write(root, 'CLAUDE.md', '# Clean Project\n\nNo dead branch references here.\n');

    const checks = runCategory6(root);
    const deadBranchCheck = checks.find((c) => c.check_id === 'no-dead-branch-refs');
    expect(deadBranchCheck).toBeDefined();
    expect(deadBranchCheck.status).toBe('pass');
    expect(deadBranchCheck.points).toBe(3);
  });

  it('passes plugin-narrative-section for consumer repos (no session-start/SKILL.md)', () => {
    const root = tmp();
    // No skills/session-start/SKILL.md — treated as consumer repo
    write(root, 'CLAUDE.md', '# Consumer Project\n\nNo plugin narrative needed.\n');

    const checks = runCategory6(root);
    const narrativeCheck = checks.find((c) => c.check_id === 'plugin-narrative-section');
    expect(narrativeCheck).toBeDefined();
    expect(narrativeCheck.status).toBe('pass');
    expect(narrativeCheck.points).toBe(2);
    expect(narrativeCheck.evidence.skipped).toBe(true);
  });

  it('fails plugin-narrative-section for plugin repos missing any narrative anchor', () => {
    const root = tmp();
    // Plugin repo marker present
    write(root, 'skills/session-start/SKILL.md', '# Session Start\n');
    // CLAUDE.md with neither ## Current State nor ## v<n>.<n> Features
    write(root, 'CLAUDE.md', '# Plugin Project\n\nNo narrative anchor heading.\n');

    const checks = runCategory6(root);
    const narrativeCheck = checks.find((c) => c.check_id === 'plugin-narrative-section');
    expect(narrativeCheck).toBeDefined();
    expect(narrativeCheck.status).toBe('fail');
    expect(narrativeCheck.points).toBe(0);
  });

  it('passes plugin-narrative-section for plugin repos with ## Current State (post-v3 canonical)', () => {
    const root = tmp();
    write(root, 'skills/session-start/SKILL.md', '# Session Start\n');
    write(root, 'CLAUDE.md', [
      '# Plugin Project',
      '',
      '## Current State',
      '',
      '- Latest session note',
    ].join('\n'));

    const checks = runCategory6(root);
    const narrativeCheck = checks.find((c) => c.check_id === 'plugin-narrative-section');
    expect(narrativeCheck).toBeDefined();
    expect(narrativeCheck.status).toBe('pass');
    expect(narrativeCheck.points).toBe(2);
    expect(narrativeCheck.evidence.present).toBe(true);
  });

  it('passes plugin-narrative-section for plugin repos with legacy ## v2.0 Features', () => {
    const root = tmp();
    write(root, 'skills/session-start/SKILL.md', '# Session Start\n');
    write(root, 'CLAUDE.md', [
      '# Plugin Project',
      '',
      '## v2.0 Features',
      '',
      '- Legacy plugin pre-v3',
    ].join('\n'));

    const checks = runCategory6(root);
    const narrativeCheck = checks.find((c) => c.check_id === 'plugin-narrative-section');
    expect(narrativeCheck).toBeDefined();
    expect(narrativeCheck.status).toBe('pass');
    expect(narrativeCheck.points).toBe(2);
  });
});

// ---------------------------------------------------------------------------
// Category 7: Policy Freshness
// ---------------------------------------------------------------------------

describe('category 7: Policy Freshness', () => {
  function makeValidBlockedCommands(ruleCount = 11) {
    const rules = [];
    for (let i = 0; i < ruleCount; i++) {
      rules.push({ id: `rule-${i}`, pattern: `dangerous-cmd-${i}`, severity: 'block' });
    }
    return JSON.stringify({ version: 1, rationale: 'Safety rules', rules });
  }

  it('passes when blocked-commands.json has all required fields and >= 10 well-formed rules', () => {
    const root = tmp();

    write(root, '.orchestrator/policy/blocked-commands.json', makeValidBlockedCommands(11));

    write(root, '.claude/rules/parallel-sessions.md', [
      '# PSA Rules',
      '',
      '## PSA-001: Detect Before Acting',
      '## PSA-002: Ask, Don\'t Assume',
      '## PSA-003: Never Destroy',
      '## PSA-004: Isolate Your Changes',
    ].join('\n'));

    const checks = runCategory7(root);
    const earned = totalPoints(checks);
    const possible = maxPoints(checks);
    expect(possible).toBe(10);
    expect(earned).toBeGreaterThanOrEqual(8);
  });

  it('returns partial score when blocked-commands.json has < 10 rules', () => {
    const root = tmp();

    // Only 5 rules — fails blocked-commands-min-rules
    write(root, '.orchestrator/policy/blocked-commands.json', makeValidBlockedCommands(5));

    write(root, '.claude/rules/parallel-sessions.md', [
      '# PSA Rules',
      'PSA-001, PSA-002, PSA-003, PSA-004',
    ].join('\n'));

    const checks = runCategory7(root);
    const minRulesCheck = checks.find((c) => c.check_id === 'blocked-commands-min-rules');
    expect(minRulesCheck).toBeDefined();
    expect(minRulesCheck.status).toBe('fail');

    // blocked-commands-schema should still pass (3), parallel-sessions (2) passes
    const earned = totalPoints(checks);
    expect(earned).toBeGreaterThanOrEqual(3);
    expect(earned).toBeLessThan(10);
  });

  it('scores 0 for blocked-commands-schema when policy file is absent', () => {
    const root = tmp();
    const checks = runCategory7(root);
    const schemaCheck = checks.find((c) => c.check_id === 'blocked-commands-schema');
    expect(schemaCheck).toBeDefined();
    expect(schemaCheck.status).toBe('fail');
    expect(schemaCheck.points).toBe(0);
  });

  it('fails blocked-commands-schema when rationale field is missing', () => {
    const root = tmp();
    const policy = { version: 1, rules: [{ id: 'r1', pattern: 'x', severity: 'block' }] };
    write(root, '.orchestrator/policy/blocked-commands.json', JSON.stringify(policy));

    const checks = runCategory7(root);
    const schemaCheck = checks.find((c) => c.check_id === 'blocked-commands-schema');
    expect(schemaCheck).toBeDefined();
    expect(schemaCheck.status).toBe('fail');
    expect(schemaCheck.points).toBe(0);
  });

  it('fails blocked-commands-min-rules when a rule has an invalid severity', () => {
    const root = tmp();
    // One rule has severity "deny" (not in valid set block|warn)
    const rules = [];
    for (let i = 0; i < 10; i++) {
      rules.push({ id: `r${i}`, pattern: `cmd-${i}`, severity: i === 0 ? 'deny' : 'block' });
    }
    write(root, '.orchestrator/policy/blocked-commands.json',
      JSON.stringify({ version: 1, rationale: 'test', rules }));

    const checks = runCategory7(root);
    const minCheck = checks.find((c) => c.check_id === 'blocked-commands-min-rules');
    expect(minCheck).toBeDefined();
    expect(minCheck.status).toBe('fail');
    expect(minCheck.points).toBe(0);
  });

  it('fails parallel-sessions-rules when all 4 PSA codes are missing', () => {
    const root = tmp();
    write(root, '.claude/rules/parallel-sessions.md', '# Some Rules\n\nNo PSA codes here.\n');

    const checks = runCategory7(root);
    const psaCheck = checks.find((c) => c.check_id === 'parallel-sessions-rules');
    expect(psaCheck).toBeDefined();
    expect(psaCheck.status).toBe('fail');
    expect(psaCheck.points).toBe(0);
  });

  it('fails parallel-sessions-rules when the file is absent', () => {
    const root = tmp();
    const checks = runCategory7(root);
    const psaCheck = checks.find((c) => c.check_id === 'parallel-sessions-rules');
    expect(psaCheck).toBeDefined();
    expect(psaCheck.status).toBe('fail');
    expect(psaCheck.points).toBe(0);
  });

  it('passes ecosystem-schema-optional when ecosystem.schema.json is absent', () => {
    const root = tmp();
    const checks = runCategory7(root);
    const ecoCheck = checks.find((c) => c.check_id === 'ecosystem-schema-optional');
    expect(ecoCheck).toBeDefined();
    expect(ecoCheck.status).toBe('pass');
    expect(ecoCheck.points).toBe(2);
  });

  it('fails ecosystem-schema-optional when ecosystem.schema.json is invalid JSON', () => {
    const root = tmp();
    mkdirSync(join(root, '.orchestrator/policy'), { recursive: true });
    write(root, '.orchestrator/policy/ecosystem.schema.json', '{ invalid }');

    const checks = runCategory7(root);
    const ecoCheck = checks.find((c) => c.check_id === 'ecosystem-schema-optional');
    expect(ecoCheck).toBeDefined();
    expect(ecoCheck.status).toBe('fail');
    expect(ecoCheck.points).toBe(0);
  });
});

// ---------------------------------------------------------------------------
// Fixture-based smoke tests (clean-repo scores high on all 8 categories)
// ---------------------------------------------------------------------------

describe('fixture clean-repo: all 8 categories score >= 8/10 or max', () => {
  it('category 1 scores at least 8/10 against fixture', () => {
    const checks = runCategory1(FIXTURE_ROOT);
    expect(totalPoints(checks)).toBeGreaterThanOrEqual(8);
  });

  it('category 2 scores at least 8/10 against fixture', () => {
    const checks = runCategory2(FIXTURE_ROOT);
    expect(totalPoints(checks)).toBeGreaterThanOrEqual(8);
  });

  it('category 3 scores at least 8/10 against fixture', () => {
    const checks = runCategory3(FIXTURE_ROOT);
    expect(totalPoints(checks)).toBeGreaterThanOrEqual(8);
  });

  it('category 4 scores at least 7/10 against fixture', () => {
    // c4.2 (sessions-jsonl-recent) drops from 3→0 points 30 days after the
    // fixture's last sessions.jsonl entry — bump the fixture date or accept the
    // -3 drift. Threshold is 7 (not 8) so the test stays green as the fixture ages.
    const checks = runCategory4(FIXTURE_ROOT);
    expect(totalPoints(checks)).toBeGreaterThanOrEqual(7);
  });

  it('category 5 scores at least 7/9 against fixture', () => {
    const checks = runCategory5(FIXTURE_ROOT);
    expect(totalPoints(checks)).toBeGreaterThanOrEqual(7);
  });

  it('category 6 scores at least 6/8 against fixture', () => {
    const checks = runCategory6(FIXTURE_ROOT);
    expect(totalPoints(checks)).toBeGreaterThanOrEqual(6);
  });

  it('category 7 scores at least 8/10 against fixture', () => {
    const checks = runCategory7(FIXTURE_ROOT);
    expect(totalPoints(checks)).toBeGreaterThanOrEqual(8);
  });

  // Floor of 9 (not 7) per W4-Q4 HIGH-4: fixture scores 10/10, a 3-pt slack
  // allows ~half the checks to silently regress. 9 = 1-pt safety margin, matching
  // siblings cat1/cat2/cat3/cat7.
  it('category 8 scores at least 9/10 against fixture', () => {
    const checks = runCategory8(FIXTURE_ROOT);
    expect(totalPoints(checks)).toBeGreaterThanOrEqual(9);
  });
});
