import assert from 'node:assert/strict';
import { mkdtemp, readFile, writeFile } from 'node:fs/promises';
import { tmpdir } from 'node:os';
import { join } from 'node:path';
import test from 'node:test';
import { DEFAULT_CONTINUATION_POLICY } from '@platform-modules/chatgpt-orchestrator-protocol';
import { JsonFileRepository, emptyState, pruneTerminalRuns } from '../src/index.js';

const ids = () => ({
  runId: `run_${crypto.randomUUID()}`,
  workerId: `wrk_${crypto.randomUUID()}`,
  assignmentId: `asg_${crypto.randomUUID()}`,
  executionId: `exe_${crypto.randomUUID()}`,
});

test('json repository persists one atomic mutation across restart', async () => {
  const dir = await mkdtemp(join(tmpdir(), 'chatgpt-orch-'));
  const path = join(dir, 'state.json');
  const first = new JsonFileRepository(path);
  await first.mutate((state) => { state.idempotency['k'] = { resourceId: 'v', fingerprint: 'f' }; });

  const second = new JsonFileRepository(path);
  assert.equal(await second.read((state) => state.idempotency['k']?.resourceId), 'v');
  const persisted = await readFile(path, 'utf8');
  assert.doesNotThrow(() => JSON.parse(persisted));
});

test('repository serializes concurrent mutations without lost updates', async () => {
  const dir = await mkdtemp(join(tmpdir(), 'chatgpt-orch-concurrent-'));
  const path = join(dir, 'state.json');
  const repository = new JsonFileRepository(path);
  await Promise.all(Array.from({ length: 24 }, (_, index) => repository.mutate((state) => {
    state.idempotency[`k-${index}`] = { resourceId: `v-${index}`, fingerprint: `f-${index}` };
  })));
  assert.equal(await repository.read((state) => Object.keys(state.idempotency).length), 24);
});

test('repository loads older state files with conservative continuity defaults', async () => {
  const dir = await mkdtemp(join(tmpdir(), 'chatgpt-orch-old-state-'));
  const path = join(dir, 'state.json');
  const { runId, workerId, assignmentId } = ids();
  const now = new Date().toISOString();
  await writeFile(path, JSON.stringify({
    runs: { [runId]: { runId, title: 'legacy', rootWorkerId: workerId, state: 'active', createdAt: now, updatedAt: now } },
    workers: { [workerId]: { workerId, runId, name: '/root', parentWorkerId: null, executor: 'chatgpt-web', state: 'running', assignmentId, executionId: null, createdAt: now, updatedAt: now } },
    assignments: { [assignmentId]: { assignmentId, objective: 'legacy', constraints: [], acceptanceCriteria: [], dependencies: [], allowedScope: [], forbiddenScope: [], artifacts: [] } },
    messages: {}, events: {}, idempotency: {}, nextCursor: {},
  }));
  const repository = new JsonFileRepository(path);
  assert.deepEqual(await repository.read((state) => state.executorCommands), {});
  assert.deepEqual(await repository.read((state) => state.conversations), {});
  assert.deepEqual(await repository.read((state) => state.executionAttempts), {});
  const continuity = await repository.read((state) => state.workerContinuity[workerId]);
  assert.equal(continuity?.disposition, 'running');
  assert.equal(continuity?.policyOverride, null);
  assert.equal(continuity?.consecutiveExecutionAttemptsWithoutProgress, 0);
});

test('continuity records and execution sequence survive restart exactly', async () => {
  const dir = await mkdtemp(join(tmpdir(), 'chatgpt-orch-continuity-'));
  const path = join(dir, 'state.json');
  const repository = new JsonFileRepository(path);
  const { runId, workerId, assignmentId, executionId } = ids();
  const now = new Date().toISOString();
  await repository.mutate((state) => {
    state.runs[runId] = { runId, title: 'continuity', rootWorkerId: workerId, state: 'active', createdAt: now, updatedAt: now };
    state.workers[workerId] = { workerId, runId, name: '/root', parentWorkerId: null, executor: 'chatgpt-web', state: 'running', assignmentId, executionId, createdAt: now, updatedAt: now };
    state.assignments[assignmentId] = { assignmentId, objective: 'work', constraints: [], acceptanceCriteria: [], dependencies: [], allowedScope: [], forbiddenScope: [], artifacts: [] };
    state.runContinuationPolicies[runId] = {
      ...DEFAULT_CONTINUATION_POLICY,
      mode: 'auto', idleGraceMs: 0, requireProgressHeartbeat: false,
      stallWarningAfterNoProgressAttempts: 10, stallPauseAfterNoProgressAttempts: 20,
      stallWarningAfterNoProgressMs: 1_800_000, stallPauseAfterNoProgressMs: 3_600_000,
    };
    state.workerContinuity[workerId] = {
      workerId, disposition: 'awaiting_execution', policyOverride: null, humanWait: null, dependencyWait: null,
      pauseReason: null, pauseAfterCurrentTurn: false, resumeAfterCurrentTurn: false, pendingExecutionId: executionId,
      idleGraceDueAt: now, activeKeepaliveDueAt: null, wakeNotBefore: null, pendingWakeReasons: [],
      lastContinuationSubmittedAt: null, lastRoutineSteerAt: null, deadmanRecoveryCount: 0, lastDeadmanRecoveryAt: null,
      lastProgressCursor: 7, lastProgressAt: now, consecutiveExecutionAttemptsWithoutProgress: 2,
      stallWarningAt: null, stallPausedAt: null, updatedAt: now,
    };
    state.executionAttempts[executionId] = {
      executionId, runId, workerId, conversationId: 'conversation-1', sequence: 3, reason: 'auto_resume', resumeOfExecutionId: null,
      state: 'scheduled', scheduledAt: now, submittedAt: null, generatingAt: null, idleAt: null, terminalAt: null,
      progressCursorAtStart: 7, lastProgressCursor: 7, wakeReasons: [], continuationCommandId: null, idempotencyKey: 'resume:3',
      outcome: null, errorCode: null, errorDetail: null,
    };
    state.executionSequences[workerId] = 3;
    state.executionIdempotency['resume:3'] = { executionId, fingerprint: 'fingerprint' };
  });

  const restarted = new JsonFileRepository(path);
  assert.equal(await restarted.read((state) => state.executionSequences[workerId]), 3);
  assert.equal(await restarted.read((state) => state.workerContinuity[workerId]?.pendingExecutionId), executionId);
  assert.equal(await restarted.read((state) => state.executionAttempts[executionId]?.sequence), 3);
});

test('retention removes continuity, attempts, and execution idempotency with terminal run', () => {
  const state = emptyState();
  const { runId, workerId, assignmentId, executionId } = ids();
  const old = '2020-01-01T00:00:00.000Z';
  state.runs[runId] = { runId, title: 'old', rootWorkerId: workerId, state: 'completed', createdAt: old, updatedAt: old };
  state.workers[workerId] = { workerId, runId, name: '/root', parentWorkerId: null, executor: 'chatgpt-web', state: 'completed', assignmentId, executionId, createdAt: old, updatedAt: old };
  state.assignments[assignmentId] = { assignmentId, objective: 'done', constraints: [], acceptanceCriteria: [], dependencies: [], allowedScope: [], forbiddenScope: [], artifacts: [] };
  state.workerContinuity[workerId] = {
    workerId, disposition: 'terminal', policyOverride: null, humanWait: null, dependencyWait: null, pauseReason: null,
    pauseAfterCurrentTurn: false, resumeAfterCurrentTurn: false, pendingExecutionId: null, idleGraceDueAt: null,
    activeKeepaliveDueAt: null, wakeNotBefore: null, pendingWakeReasons: [], lastContinuationSubmittedAt: null,
    lastRoutineSteerAt: null, deadmanRecoveryCount: 0, lastDeadmanRecoveryAt: null,
    lastProgressCursor: null, lastProgressAt: null, consecutiveExecutionAttemptsWithoutProgress: 0,
    stallWarningAt: null, stallPausedAt: null, updatedAt: old,
  };
  state.runContinuationPolicies[runId] = {
    ...DEFAULT_CONTINUATION_POLICY,
    mode: 'manual', idleGraceMs: 3000, requireProgressHeartbeat: false,
    stallWarningAfterNoProgressAttempts: 10, stallPauseAfterNoProgressAttempts: 20,
    stallWarningAfterNoProgressMs: 1_800_000, stallPauseAfterNoProgressMs: 3_600_000,
  };
  state.executionAttempts[executionId] = {
    executionId, runId, workerId, conversationId: null, sequence: 1, reason: 'initial', resumeOfExecutionId: null,
    state: 'completed', scheduledAt: old, submittedAt: old, generatingAt: old, idleAt: old, terminalAt: old,
    progressCursorAtStart: null, lastProgressCursor: null, wakeReasons: [], continuationCommandId: null, idempotencyKey: 'initial',
    outcome: 'worker_completed', errorCode: null, errorDetail: null,
  };
  state.executionSequences[workerId] = 1;
  state.executionIdempotency.initial = { executionId, fingerprint: 'f' };

  assert.deepEqual(pruneTerminalRuns(state, '2021-01-01T00:00:00.000Z'), [runId]);
  assert.equal(state.workerContinuity[workerId], undefined);
  assert.equal(state.executionAttempts[executionId], undefined);
  assert.equal(state.executionSequences[workerId], undefined);
  assert.equal(state.runContinuationPolicies[runId], undefined);
  assert.equal(state.executionIdempotency.initial, undefined);
});
