// @tier: fast
import { test } from 'node:test';
import assert from 'node:assert/strict';
import { EventEmitter } from 'node:events';
import * as fs from 'node:fs';
import * as os from 'node:os';
import * as path from 'node:path';
import {
  executeMainLoop,
  measureAndClassifyIteration,
  _deps,
} from '../bin/microverse-runner.js';
import {
  createMicroverseState,
  readMicroverseState,
  writeMicroverseState,
} from '../services/microverse-state.js';

function makeTempDir(prefix = 'pickle-mv-helper-') {
  return fs.realpathSync(fs.mkdtempSync(path.join(os.tmpdir(), prefix)));
}

function makeRunnerState(sessionDir, workingDir, overrides = {}) {
  return {
    active: true,
    working_dir: workingDir,
    step: 'implement',
    iteration: 0,
    max_iterations: 10,
    max_time_minutes: 60,
    worker_timeout_seconds: 0,
    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,
    tmux_mode: true,
    command_template: 'microverse.md',
    ...overrides,
  };
}

function makeMetric(validation) {
  return {
    description: 'score',
    validation,
    type: 'command',
    timeout_seconds: 5,
    tolerance: 2,
    direction: 'higher',
  };
}

function makeContext(sessionDir, workingDir, state, overrides = {}) {
  return {
    sessionDir,
    extensionRoot: path.resolve('.'),
    statePath: path.join(sessionDir, 'state.json'),
    workingDir,
    startTime: Date.now(),
    initialIteration: 0,
    enableFailureClassification: false,
    cgSettings: {
      enabled_convergence_files: ['anatomy-park.json'],
      regression_warning_threshold: 5,
      remediator_timeout_s: 600,
      baseline_max_age_iterations: 30,
      baseline_max_age_seconds: 14_400,
    },
    rateLimitWaitMinutes: 1,
    maxRateLimitRetries: 1,
    log: () => {},
    currentRunnerState: state,
    iteration: 1,
    consecutiveRateLimits: 0,
    preIterSha: 'pre',
    postIterSha: 'post',
    ...overrides,
  };
}

function makeSession(score) {
  const sessionDir = makeTempDir();
  const workingDir = makeTempDir();
  const scoreFile = path.join(workingDir, 'score.txt');
  fs.writeFileSync(scoreFile, `${score}\n`);
  const runnerState = makeRunnerState(sessionDir, workingDir);
  fs.writeFileSync(path.join(sessionDir, 'state.json'), JSON.stringify(runnerState, null, 2));
  const mv = createMicroverseState({
    prdPath: path.join(workingDir, 'prd.md'),
    metric: makeMetric('cat score.txt'),
    stallLimit: 5,
  });
  mv.status = 'iterating';
  mv.baseline_score = 50;
  writeMicroverseState(sessionDir, mv);
  return { sessionDir, workingDir, scoreFile, runnerState, mv };
}

test('measureAndClassifyIteration returns improved and records accepted history', async () => {
  const { sessionDir, workingDir, runnerState, mv } = makeSession(60);
  try {
    const ctx = makeContext(sessionDir, workingDir, runnerState);
    const result = await measureAndClassifyIteration(mv, { raw: '50', score: 50 }, ctx);
    assert.equal(result.kind, 'improved');
    assert.equal(mv.convergence.history[0].classification, 'improved');
    assert.equal(readMicroverseState(sessionDir).convergence.history[0].action, 'accept');
  } finally {
    fs.rmSync(sessionDir, { recursive: true, force: true });
    fs.rmSync(workingDir, { recursive: true, force: true });
  }
});

test('measureAndClassifyIteration returns regressed and rolls back', async () => {
  const { sessionDir, workingDir, runnerState, mv } = makeSession(40);
  const originalReset = _deps.resetToSha;
  let rolledBackTo = null;
  try {
    _deps.resetToSha = (sha) => { rolledBackTo = sha; };
    const ctx = makeContext(sessionDir, workingDir, runnerState, { preIterSha: 'rollback-sha' });
    const result = await measureAndClassifyIteration(mv, { raw: '50', score: 50 }, ctx);
    assert.deepEqual(result, { kind: 'regressed', rollback: true });
    assert.equal(rolledBackTo, 'rollback-sha');
    assert.equal(mv.convergence.history[0].action, 'revert');
    assert.equal(mv.failed_approaches.length, 1);
  } finally {
    _deps.resetToSha = originalReset;
    fs.rmSync(sessionDir, { recursive: true, force: true });
    fs.rmSync(workingDir, { recursive: true, force: true });
  }
});

test('measureAndClassifyIteration returns unchanged for held score and increments stall', async () => {
  const { sessionDir, workingDir, runnerState, mv } = makeSession(51);
  try {
    const ctx = makeContext(sessionDir, workingDir, runnerState);
    const result = await measureAndClassifyIteration(mv, { raw: '50', score: 50 }, ctx);
    assert.deepEqual(result, { kind: 'unchanged' });
    assert.equal(mv.convergence.history[0].classification, 'held');
    assert.equal(mv.convergence.stall_counter, 1);
  } finally {
    fs.rmSync(sessionDir, { recursive: true, force: true });
    fs.rmSync(workingDir, { recursive: true, force: true });
  }
});

test('measureAndClassifyIteration returns failed judge_timeout for command metric timeouts', async () => {
  const { sessionDir, workingDir, runnerState, mv } = makeSession(60);
  mv.key_metric.validation = 'sleep 10 && echo 60';
  mv.key_metric.timeout_seconds = 1;
  try {
    const ctx = makeContext(sessionDir, workingDir, runnerState);
    const result = await measureAndClassifyIteration(mv, { raw: '50', score: 50 }, ctx);
    assert.deepEqual(result, { kind: 'failed', exitReason: 'judge_timeout' });
  } finally {
    fs.rmSync(sessionDir, { recursive: true, force: true });
    fs.rmSync(workingDir, { recursive: true, force: true });
  }
});

test('measureAndClassifyIteration returns failed baseline_unmeasurable_unrecoverable on command metric spawn failure', async () => {
  const { sessionDir, workingDir, runnerState, mv } = makeSession(60);
  const originalSpawn = _deps.spawn;
  const originalSleep = _deps.sleep;
  try {
    _deps.sleep = async () => {};
    _deps.spawn = () => {
      const child = new EventEmitter();
      child.stdout = new EventEmitter();
      child.stdout.setEncoding = () => {};
      child.stderr = new EventEmitter();
      child.stderr.setEncoding = () => {};
      child.stdin = { end() {} };
      child.kill = () => true;
      queueMicrotask(() => {
        const err = new Error('spawn /bin/sh EACCES');
        err.code = 'EACCES';
        child.emit('error', err);
      });
      return child;
    };
    const ctx = makeContext(sessionDir, workingDir, runnerState);
    const result = await measureAndClassifyIteration(mv, { raw: '50', score: 50 }, ctx);
    assert.deepEqual(result, { kind: 'failed', exitReason: 'baseline_unmeasurable_unrecoverable' });
  } finally {
    _deps.spawn = originalSpawn;
    _deps.sleep = originalSleep;
    fs.rmSync(sessionDir, { recursive: true, force: true });
    fs.rmSync(workingDir, { recursive: true, force: true });
  }
});

test('measureAndClassifyIteration consumes structured LLM judge ledger before numeric comparison', async () => {
  const sessionDir = makeTempDir('pickle-mv-llm-session-');
  const workingDir = makeTempDir('pickle-mv-llm-work-');
  const runnerState = makeRunnerState(sessionDir, workingDir, { backend: 'claude' });
  const judgeOutput = {
    score: 40,
    violations: [
      {
        id: 'new-violation',
        path: 'src/foo.ts',
        line: 12,
        rule: 'no-any',
        severity: 'high',
        description: 'new violation',
      },
    ],
    resolved: ['old-violation'],
    new: ['new-violation'],
    remaining: [],
  };
  const mv = createMicroverseState({
    prdPath: path.join(workingDir, 'prd.md'),
    metric: {
      description: 'quality',
      validation: 'improve code quality',
      type: 'llm',
      timeout_seconds: 60,
      tolerance: 2,
      direction: 'higher',
      judge_model: 'claude-sonnet-4-6',
    },
    stallLimit: 3,
  });
  mv.status = 'iterating';
  mv.baseline_score = 40;
  fs.writeFileSync(path.join(sessionDir, 'state.json'), JSON.stringify(runnerState, null, 2));
  writeMicroverseState(sessionDir, mv);

  process.env['PICKLE_JUDGE_LEGACY_SPAWN'] = '1';
  const originalExec = _deps.execFileSync;
  try {
    _deps.execFileSync = (_cmd, args) => {
      if (Array.isArray(args) && args[0] === '--version') return 'Claude Code 2.1.126';
      return JSON.stringify(judgeOutput);
    };
    const ctx = makeContext(sessionDir, workingDir, runnerState, {
      iteration: 2,
      preIterSha: 'a'.repeat(40),
      postIterSha: 'b'.repeat(40),
    });
    const result = await measureAndClassifyIteration(mv, { raw: '40', score: 40 }, ctx);
    assert.deepEqual(result, { kind: 'improved', metric: { raw: JSON.stringify(judgeOutput), score: 40 } });
    assert.equal(mv.convergence.history[0].classification, 'improved');
    assert.deepEqual(
      mv.violation_ledger?.map(({ path: filePath, line, rule, first_seen_iter, last_seen_iter }) => ({
        path: filePath,
        line,
        rule,
        first_seen_iter,
        last_seen_iter,
      })),
      [{
        path: 'src/foo.ts',
        line: 12,
        rule: 'no-any',
        first_seen_iter: 2,
        last_seen_iter: 2,
      }],
    );
  } finally {
    delete process.env['PICKLE_JUDGE_LEGACY_SPAWN'];
    _deps.execFileSync = originalExec;
    fs.rmSync(sessionDir, { recursive: true, force: true });
    fs.rmSync(workingDir, { recursive: true, force: true });
  }
});

test('measureAndClassifyIteration drops resolved violations from the live ledger before the next judge pass', async () => {
  const sessionDir = makeTempDir('pickle-mv-llm-resolved-session-');
  const workingDir = makeTempDir('pickle-mv-llm-resolved-work-');
  const runnerState = makeRunnerState(sessionDir, workingDir, { backend: 'claude' });
  const firstJudgeOutput = {
    score: 40,
    violations: [
      {
        id: 'repeat-violation',
        path: 'src/foo.ts',
        line: 12,
        rule: 'no-any',
        severity: 'high',
        description: 'new violation',
      },
    ],
    resolved: [],
    new: ['repeat-violation'],
    remaining: [],
  };
  const secondJudgeOutput = {
    score: 40,
    violations: [],
    resolved: ['repeat-violation'],
    new: [],
    remaining: [],
  };
  const mv = createMicroverseState({
    prdPath: path.join(workingDir, 'prd.md'),
    metric: {
      description: 'quality',
      validation: 'improve code quality',
      type: 'llm',
      timeout_seconds: 60,
      tolerance: 2,
      direction: 'higher',
      judge_model: 'claude-sonnet-4-6',
    },
    stallLimit: 3,
  });
  mv.status = 'iterating';
  mv.baseline_score = 40;
  fs.writeFileSync(path.join(sessionDir, 'state.json'), JSON.stringify(runnerState, null, 2));
  writeMicroverseState(sessionDir, mv);

  process.env['PICKLE_JUDGE_LEGACY_SPAWN'] = '1';
  const originalExec = _deps.execFileSync;
  const originalReset = _deps.resetToSha;
  let pass = 0;
  try {
    _deps.resetToSha = () => {};
    _deps.execFileSync = (_cmd, args) => {
      if (Array.isArray(args) && args[0] === '--version') return 'Claude Code 2.1.126';
      pass += 1;
      return JSON.stringify(pass === 1 ? firstJudgeOutput : secondJudgeOutput);
    };
    const firstCtx = makeContext(sessionDir, workingDir, runnerState, {
      iteration: 2,
      preIterSha: 'a'.repeat(40),
      postIterSha: 'b'.repeat(40),
    });
    const secondCtx = makeContext(sessionDir, workingDir, runnerState, {
      iteration: 3,
      preIterSha: 'c'.repeat(40),
      postIterSha: 'd'.repeat(40),
    });

    await measureAndClassifyIteration(mv, { raw: '40', score: 40 }, firstCtx);
    assert.equal(mv.violation_ledger?.length, 1, 'first pass should seed the live ledger');

    await measureAndClassifyIteration(mv, { raw: '40', score: 40 }, secondCtx);
    assert.deepEqual(mv.violation_ledger, [], 'resolved violations must be removed from the live ledger');
  } finally {
    delete process.env['PICKLE_JUDGE_LEGACY_SPAWN'];
    _deps.resetToSha = originalReset;
    _deps.execFileSync = originalExec;
    fs.rmSync(sessionDir, { recursive: true, force: true });
    fs.rmSync(workingDir, { recursive: true, force: true });
  }
});

test('executeMainLoop replays convergence mutation fixture order', async () => {
  const fixturePath = path.join('tests', 'fixtures', 'microverse', 'convergence-mutations.json');
  const fixture = JSON.parse(fs.readFileSync(fixturePath, 'utf-8'));
  const sessionDir = makeTempDir('pickle-mv-replay-session-');
  const workingDir = makeTempDir('pickle-mv-replay-work-');
  const scoreFile = path.join(workingDir, 'score.txt');
  const runnerState = makeRunnerState(sessionDir, workingDir, { max_iterations: 5 });
  const originalRunIteration = _deps.runIteration;
  const originalGetHeadSha = _deps.getHeadSha;
  const originalSleep = _deps.sleep;
  const originalReset = _deps.resetToSha;
  const scores = [60, 61, 62];
  const shas = ['pre001', 'abc001', 'pre002', 'abc002', 'pre003', 'abc003'];
  const postShas = [];
  try {
    fs.writeFileSync(path.join(sessionDir, 'state.json'), JSON.stringify(runnerState, null, 2));
    const mv = createMicroverseState({
      prdPath: path.join(workingDir, 'prd.md'),
      metric: { ...makeMetric('cat score.txt'), tolerance: 5 },
      stallLimit: 2,
    });
    mv.status = 'iterating';
    mv.baseline_score = fixture.cycle[0].after.baseline_score;
    writeMicroverseState(sessionDir, mv);

    let iterationIndex = 0;
    _deps.runIteration = async () => {
      fs.writeFileSync(scoreFile, `${scores[iterationIndex++]}\n`);
      return { completion: 'success', exitCode: 0, timedOut: false, wallSeconds: 1 };
    };
    _deps.getHeadSha = () => {
      const sha = shas.shift() ?? 'abc003';
      if (sha.startsWith('abc')) postShas.push(sha);
      return sha;
    };
    _deps.sleep = async () => {};
    _deps.resetToSha = () => {};

    const ctx = makeContext(sessionDir, workingDir, runnerState, {
      iteration: 0,
      startTime: Date.now(),
    });
    await executeMainLoop(mv, ctx);
    const actual = readMicroverseState(sessionDir);
    const expectedAfter = fixture.cycle[2].after.convergence;
    const actualMutations = actual.convergence.history.map(({ score, iteration, action, classification }, index) => ({
      score,
      iteration,
      sha: postShas[index],
      ...(action === 'accept' && classification === 'improved' ? { action } : {}),
      classification,
    }));
    assert.deepStrictEqual(
      actualMutations,
      expectedAfter.history,
    );
    assert.equal(actual.convergence.stall_counter, expectedAfter.stall_counter);
  } finally {
    _deps.runIteration = originalRunIteration;
    _deps.getHeadSha = originalGetHeadSha;
    _deps.sleep = originalSleep;
    _deps.resetToSha = originalReset;
    fs.rmSync(sessionDir, { recursive: true, force: true });
    fs.rmSync(workingDir, { recursive: true, force: true });
  }
});
