// @tier: fast
import { test } from 'node:test';
import assert from 'node:assert/strict';
import * as fs from 'node:fs';
import * as os from 'node:os';
import * as path from 'node:path';
import { fileURLToPath } from 'node:url';
import { spawnSync } from 'node:child_process';
import { setupAnatomyPark, writePipelineStatus } from '../bin/pipeline-runner.js';
import { finalizeGateMain } from '../bin/finalize-gate.js';
import { filterBySubsystem } from '../services/scope-resolver.js';

const __dirname = path.dirname(fileURLToPath(import.meta.url));
const EXTENSION_ROOT = path.join(os.homedir(), '.claude/pickle-rick');
const ANATOMY_PARK_MD = path.resolve(__dirname, '../../.claude/commands/anatomy-park.md');
const CHECK_SCOPE_DIFF = path.resolve(__dirname, '..', 'bin', 'check-scope-diff.js');

function makeTarget() {
  return fs.mkdtempSync(path.join(os.tmpdir(), 'ap-scope-target-'));
}

function makeSession() {
  return fs.mkdtempSync(path.join(os.tmpdir(), 'ap-scope-session-'));
}

function writeState(sessionDir, workingDir) {
  fs.writeFileSync(
    path.join(sessionDir, 'state.json'),
    JSON.stringify({
      active: false,
      working_dir: workingDir,
      step: 'review',
      iteration: 0,
      max_iterations: 10,
      max_time_minutes: 60,
      worker_timeout_seconds: 1200,
      start_time_epoch: Math.floor(Date.now() / 1000),
      completion_promise: null,
      original_prompt: 'test',
      current_ticket: null,
      history: [],
      started_at: new Date().toISOString(),
      session_dir: sessionDir,
    }, null, 2),
  );
}

function makeSubsystem(root, name, fileCount = 3) {
  const dir = path.join(root, name);
  fs.mkdirSync(dir, { recursive: true });
  for (let i = 0; i < fileCount; i++) {
    fs.writeFileSync(path.join(dir, `f${i}.ts`), `export const x${i} = ${i};\n`);
  }
}

function readAnatomyPark(sessionDir) {
  return JSON.parse(fs.readFileSync(path.join(sessionDir, 'anatomy-park.json'), 'utf-8'));
}

function readMicroverse(sessionDir) {
  return JSON.parse(fs.readFileSync(path.join(sessionDir, 'microverse.json'), 'utf-8'));
}

// ---------------------------------------------------------------------------
// Pipeline mode: scope filters subsystems in anatomy-park.json
// ---------------------------------------------------------------------------

test('pipeline filter: 4 subsystems, scope covering 2 → anatomy-park.json has exactly those 2', () => {
  const session = makeSession();
  const target = makeTarget();
  try {
    makeSubsystem(target, 'alpha');
    makeSubsystem(target, 'beta');
    makeSubsystem(target, 'gamma');
    makeSubsystem(target, 'delta');

    // scope touches alpha and gamma only
    const allowedPaths = ['alpha/f0.ts', 'gamma/f1.ts'];
    const repoRoot = target; // target IS repoRoot in this fixture

    setupAnatomyPark(session, target, 3, EXTENSION_ROOT, () => {}, { allowedPaths, repoRoot });

    const ap = readAnatomyPark(session);
    assert.deepStrictEqual(ap.subsystems, ['alpha', 'gamma']);
    assert.deepStrictEqual(ap.pass_counts, { alpha: 0, gamma: 0 });
    assert.deepStrictEqual(ap.consecutive_clean, { alpha: 0, gamma: 0 });
    assert.deepStrictEqual(ap.stall_counts, { alpha: 0, gamma: 0 });
    assert.deepStrictEqual(ap.findings_history, { alpha: [], gamma: [] });
  } finally {
    fs.rmSync(session, { recursive: true, force: true });
    fs.rmSync(target, { recursive: true, force: true });
  }
});

test('R-PSSS-1: anatomy-park scope excluding all subsystems skips with a structured WARN', () => {
  const session = makeSession();
  const target = makeTarget();
  try {
    makeSubsystem(target, 'alpha');
    makeSubsystem(target, 'beta');

    const logs = [];
    // allowed_paths touch only docs — they match no discovered subsystem.
    const ok = setupAnatomyPark(session, target, 3, EXTENSION_ROOT, (m) => logs.push(m), {
      allowedPaths: ['docs/guide.md', 'README.md'],
      repoRoot: target,
    });

    assert.deepStrictEqual(ok, { skipReason: 'empty_scope' }, 'setup must skip with an empty_scope disposition');
    const warn = logs.join('\n');
    assert.match(warn, /⚠ anatomy-park did not run/);
    assert.match(warn, /scope filter excluded all subsystems/);
    assert.match(warn, /docs\/guide\.md/, 'WARN must name the in-scope diff paths');
    assert.equal(
      fs.existsSync(path.join(session, 'anatomy-park.json')), false,
      'no anatomy-park.json must be written on an empty-scope skip',
    );
  } finally {
    fs.rmSync(session, { recursive: true, force: true });
    fs.rmSync(target, { recursive: true, force: true });
  }
});

test('R-PSSS-3: anatomy-park with no subsystems returns a no_subsystems skip disposition', () => {
  const session = makeSession();
  const target = makeTarget(); // empty target — no subsystem directories
  try {
    const ok = setupAnatomyPark(session, target, 3, EXTENSION_ROOT, () => {});
    assert.deepStrictEqual(ok, { skipReason: 'no_subsystems' });
  } finally {
    fs.rmSync(session, { recursive: true, force: true });
    fs.rmSync(target, { recursive: true, force: true });
  }
});

test('R-PSSS-3: writePipelineStatus persists non-empty phase_skips and omits an empty map', () => {
  const dir = makeSession();
  try {
    writePipelineStatus(dir, 'running', { phase_skips: { 'anatomy-park': 'empty_scope' } });
    const withSkips = JSON.parse(fs.readFileSync(path.join(dir, 'pipeline-status.json'), 'utf-8'));
    assert.deepStrictEqual(withSkips.phase_skips, { 'anatomy-park': 'empty_scope' });

    writePipelineStatus(dir, 'running', { phase_skips: {} });
    const noSkips = JSON.parse(fs.readFileSync(path.join(dir, 'pipeline-status.json'), 'utf-8'));
    assert.equal('phase_skips' in noSkips, false, 'an empty phase_skips map must be omitted from the status file');
  } finally {
    fs.rmSync(dir, { recursive: true, force: true });
  }
});

test('pipeline setup writes anatomy-park.json through shared atomic writer before init-microverse consumes it', () => {
  const session = makeSession();
  const target = makeTarget();
  try {
    makeSubsystem(target, 'alpha');

    const compiledPipelineRunner = fs.readFileSync(
      new URL('../bin/pipeline-runner.js', import.meta.url),
      'utf-8',
    );
    assert.ok(
      compiledPipelineRunner.includes("writeStateFile(path.join(sessionDir, 'anatomy-park.json'), apState)"),
      'anatomy setup must publish anatomy-park.json through the shared tmp-rename writer',
    );

    setupAnatomyPark(session, target, 3, EXTENSION_ROOT, () => {});

    assert.deepStrictEqual(readAnatomyPark(session).subsystems, ['alpha']);
    assert.equal(readMicroverse(session).convergence_file, 'anatomy-park.json');
  } finally {
    fs.rmSync(session, { recursive: true, force: true });
    fs.rmSync(target, { recursive: true, force: true });
  }
});

test('pipeline scoped setup injects allowed_paths into microverse.json so final gate honors out-of-scope failures', async () => {
  const session = makeSession();
  const target = makeTarget();
  try {
    makeSubsystem(target, 'alpha');
    makeSubsystem(target, 'beta');
    writeState(session, target);
    fs.writeFileSync(
      path.join(session, 'scope.json'),
      JSON.stringify({ allowed_paths: ['alpha/f0.ts'], mode: 'branch', strategy: 'strict', head_sha: 'abc123' }),
    );

    setupAnatomyPark(session, target, 3, EXTENSION_ROOT, () => {}, {
      allowedPaths: ['alpha/f0.ts'],
      repoRoot: target,
    });

    const mv = readMicroverse(session);
    assert.deepStrictEqual(mv.allowed_paths, ['alpha/f0.ts']);

    const gateDir = path.join(session, 'gate');
    let remediatorCalled = false;
    const code = await finalizeGateMain({
      argv: [session, 'anatomy-park'],
      env: {},
      readStateForWorkingDirFn: () => ({ workingDir: target, backend: 'claude' }),
      loadSettingsFn: () => ({
        szechuan_max_remediation_cycles: 3,
        anatomy_park_max_remediation_cycles: 1,
        remediator_timeout_s: 60,
      }),
      runGateFn: async () => ({
        status: 'red',
        failures: [{
          check: 'lint',
          file: path.join(target, 'beta/f0.ts'),
          line: 1,
          ruleOrCode: 'no-any',
          message: 'out of scope',
          severity: 'error',
          occurrence_index: 0,
        }],
        baseline_used: false,
        allowed_paths_used: false,
        elapsed_ms: 5,
        total_raw_failure_count: 1,
        new_failures_vs_baseline: 0,
      }),
      spawnGateRemediatorMainFn: async () => {
        remediatorCalled = true;
        return 0;
      },
      spawnRemediatorFn: () => {
        remediatorCalled = true;
      },
      stdout: () => {},
      stderr: () => {},
    });

    assert.equal(code, 0, 'final gate should close when every failure is out of scope');
    assert.equal(remediatorCalled, false, 'remediator must not run for out-of-scope failures');

    const gateFiles = fs.readdirSync(gateDir);
    assert.equal(gateFiles.filter(f => f.startsWith('out_of_scope_failures_')).length, 1);
  } finally {
    fs.rmSync(session, { recursive: true, force: true });
    fs.rmSync(target, { recursive: true, force: true });
  }
});

test('resume: persisted scope.json still filters anatomy-park when refreshScope already entered the phase', () => {
  const session = makeSession();
  const target = makeTarget();
  try {
    makeSubsystem(target, 'alpha');
    makeSubsystem(target, 'beta');
    writeState(session, target);
    fs.writeFileSync(
      path.join(session, 'scope.json'),
      JSON.stringify({ allowed_paths: ['alpha/f0.ts'], mode: 'branch', strategy: 'strict', head_sha: 'abc123' }),
    );

    setupAnatomyPark(session, target, 3, EXTENSION_ROOT, () => {});

    const ap = readAnatomyPark(session);
    const mv = readMicroverse(session);
    assert.deepStrictEqual(ap.subsystems, ['alpha']);
    assert.deepStrictEqual(mv.allowed_paths, ['alpha/f0.ts']);
  } finally {
    fs.rmSync(session, { recursive: true, force: true });
    fs.rmSync(target, { recursive: true, force: true });
  }
});

test('resume: persisted scope.json promotes newer dead tmp before filtering anatomy-park', () => {
  const session = makeSession();
  const target = makeTarget();
  try {
    makeSubsystem(target, 'alpha');
    makeSubsystem(target, 'beta');
    writeState(session, target);
    const scopePath = path.join(session, 'scope.json');
    fs.writeFileSync(
      scopePath,
      JSON.stringify({ allowed_paths: ['beta/f0.ts'], mode: 'branch', strategy: 'strict', head_sha: 'old' }),
    );
    fs.writeFileSync(
      `${scopePath}.tmp.99999999`,
      JSON.stringify({ allowed_paths: ['alpha/f0.ts'], mode: 'branch', strategy: 'strict', head_sha: 'new' }),
    );
    fs.utimesSync(`${scopePath}.tmp.99999999`, new Date(Date.now() + 1_000), new Date(Date.now() + 1_000));

    setupAnatomyPark(session, target, 3, EXTENSION_ROOT, () => {});

    const ap = readAnatomyPark(session);
    const mv = readMicroverse(session);
    assert.deepStrictEqual(ap.subsystems, ['alpha']);
    assert.deepStrictEqual(mv.allowed_paths, ['alpha/f0.ts']);
    assert.equal(fs.existsSync(`${scopePath}.tmp.99999999`), false);
  } finally {
    fs.rmSync(session, { recursive: true, force: true });
    fs.rmSync(target, { recursive: true, force: true });
  }
});

// ---------------------------------------------------------------------------
// Standalone mode parity: filterBySubsystem with same fixture → same result
// ---------------------------------------------------------------------------

test('standalone filter parity: filterBySubsystem same fixture → identical to pipeline result', () => {
  const target = makeTarget();
  try {
    makeSubsystem(target, 'alpha');
    makeSubsystem(target, 'beta');
    makeSubsystem(target, 'gamma');
    makeSubsystem(target, 'delta');

    const allowedPaths = ['alpha/f0.ts', 'gamma/f1.ts'];
    const repoRoot = target;
    const allNames = ['alpha', 'beta', 'delta', 'gamma']; // sorted

    const result = filterBySubsystem(allNames, allowedPaths, target, repoRoot);
    assert.deepStrictEqual(result, ['alpha', 'gamma']);
  } finally {
    fs.rmSync(target, { recursive: true, force: true });
  }
});

// ---------------------------------------------------------------------------
// Phase 1 invariant: marker present — guarantees Phase 1 reads all subsystem files
// ---------------------------------------------------------------------------

test('phase-1 invariant marker present in anatomy-park.md', () => {
  const content = fs.readFileSync(ANATOMY_PARK_MD, 'utf-8');
  assert.ok(
    content.includes('<!-- scope-invariant: phase-1-reads-all-subsystem-files -->'),
    'anatomy-park.md must contain scope-invariant marker',
  );
});

// ---------------------------------------------------------------------------
// Standalone-mode ordering: scope-hook fires AFTER Step 6.5 creates scope.json
// ---------------------------------------------------------------------------

test('standalone scope-hook is placed after Step 6.5 (Resolve Scope) in anatomy-park.md', () => {
  const content = fs.readFileSync(ANATOMY_PARK_MD, 'utf-8');
  const hookIdx = content.indexOf('<!-- scope-hook: discovery-filter -->');
  const resolveStepIdx = content.indexOf('### Step 6.5: Resolve Scope');
  assert.ok(hookIdx > 0, 'scope-hook: discovery-filter marker missing from anatomy-park.md');
  assert.ok(resolveStepIdx > 0, 'Step 6.5 heading missing from anatomy-park.md');
  assert.ok(
    hookIdx > resolveStepIdx,
    'scope-hook must appear after Step 6.5 so scope.json exists when the filter fires',
  );
});

test('standalone scope-hook does not reference a nonexistent "full" mode', () => {
  const content = fs.readFileSync(ANATOMY_PARK_MD, 'utf-8');
  assert.ok(
    !/\bfull\b/.test(content.match(/<!-- scope-hook: discovery-filter -->[\s\S]*?\n\n/)?.[0] ?? ''),
    'scope-hook block must not reference a "full" mode (ScopeMode = branch|diff|paths only)',
  );
});

test('Step 7 references --allowed-paths-file after Step 6.5 (scope wiring for standalone mode)', () => {
  const content = fs.readFileSync(ANATOMY_PARK_MD, 'utf-8');
  const stepIdx = content.indexOf('### Step 6.5: Resolve Scope');
  const createIdx = content.indexOf('### Step 7: Create anatomy-park.json and microverse.json');
  const flagIdx = content.lastIndexOf('--allowed-paths-file');
  assert.ok(stepIdx > 0, 'Step 6.5 heading must exist in anatomy-park.md');
  assert.ok(createIdx > stepIdx, 'Step 7 must come after Step 6.5');
  assert.ok(
    flagIdx > createIdx,
    '--allowed-paths-file must appear in the init-microverse command after scope.json has been created',
  );
});

// ---------------------------------------------------------------------------
// Backcompat: omitted scope → all subsystems pass through unfiltered
// ---------------------------------------------------------------------------

test('backcompat: no scope arg → anatomy-park.json contains all 4 subsystems', () => {
  const session = makeSession();
  const target = makeTarget();
  try {
    makeSubsystem(target, 'alpha');
    makeSubsystem(target, 'beta');
    makeSubsystem(target, 'gamma');
    makeSubsystem(target, 'delta');

    // No scope passed — backcompat path
    setupAnatomyPark(session, target, 3, EXTENSION_ROOT, () => {});

    const ap = readAnatomyPark(session);
    const mv = readMicroverse(session);
    assert.equal(ap.subsystems.length, 4);
    assert.deepStrictEqual(ap.subsystems.sort(), ['alpha', 'beta', 'delta', 'gamma']);
    assert.equal(mv.allowed_paths, undefined);
  } finally {
    fs.rmSync(session, { recursive: true, force: true });
    fs.rmSync(target, { recursive: true, force: true });
  }
});

// ---------------------------------------------------------------------------
// Bundle bootstrap: scope.json canonical branch-mode shape
// ---------------------------------------------------------------------------

test('bundle bootstrap scope.json: mode is branch and allowed_paths is array of 5 strings', () => {
  const session = makeSession();
  try {
    const scopePath = path.join(session, 'scope.json');
    const bundleScope = {
      mode: 'branch',
      allowed_paths: [
        'extension/src/',
        'extension/tests/',
        'extension/CLAUDE.md',
        'prds/MASTER_PLAN.md',
        'prds/p1-bug-fix-bundle-2026-05-10.md',
      ],
      subsystems: ['bin', 'lib', 'services', 'types'],
      scope_base: 'main',
    };
    fs.writeFileSync(scopePath, JSON.stringify(bundleScope, null, 2));

    const parsed = JSON.parse(fs.readFileSync(scopePath, 'utf-8'));
    assert.equal(parsed.mode, 'branch');
    assert.ok(Array.isArray(parsed.allowed_paths));
    assert.equal(parsed.allowed_paths.length, 5);
    assert.ok(parsed.allowed_paths.every((p) => typeof p === 'string'));
    const unique = [...new Set(parsed.allowed_paths)];
    assert.equal(unique.length, 5, 'all 5 allowed_paths must be unique');
  } finally {
    fs.rmSync(session, { recursive: true, force: true });
  }
});

// ---------------------------------------------------------------------------
// AC-BUNDLE-APWS-01: worker-simulation — check-scope-diff rejects out-of-scope staged paths
// ---------------------------------------------------------------------------

test('AC-BUNDLE-APWS-01: check-scope-diff rejects out-of-scope staged paths in worker simulation', () => {
  const tmp = fs.realpathSync(fs.mkdtempSync(path.join(os.tmpdir(), 'apws-scope-sim-')));
  const dataRoot = path.join(tmp, 'data');
  const ticketId = 'test-ticket-apws7';

  try {
    // Init temp git repo
    spawnSync('git', ['init', '-q'], { cwd: tmp, timeout: 5_000 });
    spawnSync('git', ['config', 'user.email', 'test@test.com'], { cwd: tmp, timeout: 5_000 });
    spawnSync('git', ['config', 'user.name', 'Test'], { cwd: tmp, timeout: 5_000 });

    // Write scope.json with three allowed prefixes
    const scopePath = path.join(tmp, 'scope.json');
    fs.writeFileSync(scopePath, JSON.stringify({ allowed_paths: ['alpha/', 'beta/', 'gamma/'] }));

    // Stage in-scope file
    fs.mkdirSync(path.join(tmp, 'alpha'), { recursive: true });
    fs.writeFileSync(path.join(tmp, 'alpha', 'x.ts'), 'export const x = 1;\n');
    spawnSync('git', ['add', 'alpha/x.ts'], { cwd: tmp, timeout: 5_000 });

    // Stage out-of-scope file
    fs.mkdirSync(path.join(tmp, 'outside'), { recursive: true });
    fs.writeFileSync(path.join(tmp, 'outside', 'leaked.ts'), 'export const leaked = true;\n');
    spawnSync('git', ['add', 'outside/leaked.ts'], { cwd: tmp, timeout: 5_000 });

    // Spawn check-scope-diff.js against the temp repo with isolated PICKLE_DATA_ROOT
    const result = spawnSync(
      process.execPath,
      [CHECK_SCOPE_DIFF, '--scope-json', scopePath, '--ticket-id', ticketId],
      {
        encoding: 'utf-8',
        timeout: 10_000,
        cwd: tmp,
        env: { ...process.env, PICKLE_DATA_ROOT: dataRoot },
      },
    );

    // Assert 1: exit status 1 (outside_scope)
    assert.equal(result.status, 1, `expected exit 1; stderr: ${result.stderr}`);

    // Assert 2: stdout parses to outside_scope shape with outside/leaked.ts
    const parsed = JSON.parse(result.stdout.trim());
    assert.equal(parsed.status, 'outside_scope');
    assert.ok(
      Array.isArray(parsed.staged_paths_outside_scope) &&
        parsed.staged_paths_outside_scope.includes('outside/leaked.ts'),
      `staged_paths_outside_scope must include 'outside/leaked.ts'; got ${JSON.stringify(parsed.staged_paths_outside_scope)}`,
    );

    // Assert 3+4+5: worker_edit_outside_scope activity event in isolated data root
    const activityDir = path.join(dataRoot, 'activity');
    const jsonlFiles = fs.existsSync(activityDir)
      ? fs.readdirSync(activityDir).filter((f) => f.endsWith('.jsonl'))
      : [];
    const events = [];
    for (const f of jsonlFiles) {
      const content = fs.readFileSync(path.join(activityDir, f), 'utf-8');
      for (const line of content.split('\n').filter(Boolean)) {
        try { events.push(JSON.parse(line)); } catch { /* skip malformed */ }
      }
    }
    const scopeEvents = events.filter((e) => e.event === 'worker_edit_outside_scope');
    assert.equal(scopeEvents.length, 1, `expected 1 worker_edit_outside_scope event; got ${scopeEvents.length}`);

    const ev = scopeEvents[0];
    assert.equal(ev.ticket_id, ticketId, 'ticket_id must round-trip into the event');
    assert.ok(
      Array.isArray(ev.gate_payload?.staged_paths_outside_scope) &&
        ev.gate_payload.staged_paths_outside_scope.includes('outside/leaked.ts'),
      `gate_payload.staged_paths_outside_scope must include 'outside/leaked.ts'`,
    );
  } finally {
    fs.rmSync(tmp, { recursive: true, force: true });
  }
});
