import assert from 'node:assert/strict';
import { spawnSync } from 'node:child_process';
import { existsSync, readdirSync, readFileSync, rmSync } from 'node:fs';
import { join } from 'node:path';
import { execPath } from 'node:process';
import { afterEach, describe, it } from 'node:test';
import { fileURLToPath } from 'node:url';

const REPO_ROOT = join(fileURLToPath(new URL('.', import.meta.url)), '..');
const SCRIPT = join(REPO_ROOT, 'scripts', 'run-logged.mjs');
const WIP_DIR = join(REPO_ROOT, 'wip');

function runWrapper(logname, command, args = [], env = {}) {
  return spawnSync(execPath, [SCRIPT, logname, command, ...args], {
    cwd: REPO_ROOT,
    encoding: 'utf-8',
    env: { ...process.env, ...env },
    timeout: 30_000,
  });
}

function parseLogPath(stdout) {
  const match = stdout.match(/^log: (.+)$/m);
  assert.ok(match, `no "log: " line in wrapper stdout:\n${stdout}`);
  return join(REPO_ROOT, match[1]);
}

function cleanupWip(logname) {
  if (!existsSync(WIP_DIR)) return;
  for (const f of readdirSync(WIP_DIR)) {
    if (f.startsWith(logname)) rmSync(join(WIP_DIR, f));
  }
}

afterEach(() => {
  cleanupWip('selftest-success');
  cleanupWip('selftest-failure');
  cleanupWip('selftest-timeout');
  cleanupWip('selftest-badtimeout');
});

describe('run-logged', () => {
  it('exits 0, writes log containing child output, and writes .exit file on success', () => {
    const result = runWrapper('selftest-success', execPath, [
      '-e',
      "process.stdout.write('hi'); process.exit(0)",
    ]);

    assert.equal(result.status, 0, `wrapper exited ${result.status}; stderr=${result.stderr}`);

    const logPath = parseLogPath(result.stdout);
    assert.ok(existsSync(logPath), `log file not found: ${logPath}`);
    assert.match(readFileSync(logPath, 'utf-8'), /hi/);

    const exitPath = logPath.replace(/\.log$/, '.exit');
    assert.ok(existsSync(exitPath), `.exit file not found: ${exitPath}`);
    assert.equal(readFileSync(exitPath, 'utf-8').trim(), '0');
  });

  it('falls back to the default timeout and warns when AGENT_CMD_TIMEOUT_SECONDS is invalid', () => {
    const result = runWrapper('selftest-badtimeout', execPath, ['-e', 'process.exit(0)'], {
      AGENT_CMD_TIMEOUT_SECONDS: 'not-a-number',
    });

    assert.equal(result.status, 0, `wrapper exited ${result.status}; stderr=${result.stderr}`);
    assert.match(result.stderr, /ignoring invalid AGENT_CMD_TIMEOUT_SECONDS=not-a-number/);

    const exitPath = parseLogPath(result.stdout).replace(/\.log$/, '.exit');
    assert.equal(readFileSync(exitPath, 'utf-8').trim(), '0');
  });

  it('exits with child exit code and writes it to .exit file on failure', () => {
    const result = runWrapper('selftest-failure', execPath, ['-e', 'process.exit(3)']);

    assert.equal(result.status, 3, `wrapper exited ${result.status}; stderr=${result.stderr}`);

    const logPath = parseLogPath(result.stdout);
    const exitPath = logPath.replace(/\.log$/, '.exit');
    assert.ok(existsSync(exitPath), `.exit file not found: ${exitPath}`);
    assert.equal(readFileSync(exitPath, 'utf-8').trim(), '3');
  });

  it('exits 124, writes 124 to .exit, and appends TIMEOUT marker on timeout', () => {
    const result = runWrapper('selftest-timeout', execPath, ['-e', 'setTimeout(()=>{}, 60000)'], {
      AGENT_CMD_TIMEOUT_SECONDS: '1',
    });

    assert.equal(result.status, 124, `wrapper exited ${result.status}; stderr=${result.stderr}`);

    const logPath = parseLogPath(result.stdout);
    assert.ok(existsSync(logPath), `log file not found: ${logPath}`);
    assert.match(readFileSync(logPath, 'utf-8'), /TIMEOUT/);

    const exitPath = logPath.replace(/\.log$/, '.exit');
    assert.ok(existsSync(exitPath), `.exit file not found: ${exitPath}`);
    assert.equal(readFileSync(exitPath, 'utf-8').trim(), '124');
  });

  it('does not overwrite the 124 .exit file after the SIGKILL triggers child close', async () => {
    const result = runWrapper('selftest-timeout', execPath, ['-e', 'setTimeout(()=>{}, 60000)'], {
      AGENT_CMD_TIMEOUT_SECONDS: '1',
    });

    assert.equal(result.status, 124, `wrapper exited ${result.status}; stderr=${result.stderr}`);

    const logPath = parseLogPath(result.stdout);
    const exitPath = logPath.replace(/\.log$/, '.exit');
    assert.equal(readFileSync(exitPath, 'utf-8').trim(), '124');

    await new Promise((resolveSleep) => setTimeout(resolveSleep, 250));
    assert.equal(readFileSync(exitPath, 'utf-8').trim(), '124');
  });
});