import assert from "node:assert/strict";
import { test } from "node:test";

import type { CanonicalTaskRuntimeSnapshot } from "../../pm/resolveCanonicalTaskRuntimeState.ts";
import {
  THREAD_SCHEDULE_SCHEMA_VERSION,
  buildThreadScheduleSnapshot,
} from "../ThreadScheduleKernel.ts";

function runtimeSnapshot(
  taskId: string,
  state: CanonicalTaskRuntimeSnapshot["state"],
  conflicts: CanonicalTaskRuntimeSnapshot["conflicts"] = [],
): CanonicalTaskRuntimeSnapshot {
  return {
    state,
    phase: state,
    queue_state: state,
    task_id: taskId,
    role: "DEV",
    agent_id: "DEV-01",
    session_id: state === "running" ? `session-${taskId}` : null,
    run_id: null,
    attempt_id: null,
    lease_id: null,
    report_id: null,
    report_status: null,
    has_live_task_session: state === "running",
    has_valid_active_lease: state === "running",
    has_formal_report: false,
    reason_code: `state_${state}`,
    summary: state,
    suggested_action: state === "projection_conflict" ? "inspect_conflict" : "wait",
    observed_at: "2026-08-10T12:00:00.000Z",
    startup_grace_ms: 30_000,
    evidence: [],
    conflicts,
    history: {
      last_session_id: null,
      last_started_at: null,
      last_ended_at: null,
      last_session_status: null,
      last_failure_code: null,
      last_failure_category: null,
      is_first_turn_abort: false,
      session_unsettled: false,
      historical_failure_recoverable: false,
    },
    business_decision: null,
  };
}

const rootTask = {
  task_id: "TASK-20260810-100",
  root_task_id: "TASK-20260810-100",
  thread_key: "submission-v160",
  sender: "ADMIN",
  recipient: "PM",
  subject: "V1.6.0",
  scope: "active",
};

const childTask = {
  task_id: "TASK-20260810-101",
  root_task_id: "TASK-20260810-100",
  parent: "TASK-20260810-100",
  thread_key: "submission-v160",
  sender: "PM",
  recipient: "DEV",
  subject: "Implement kernel",
  scope: "active",
  due_at: "2026-08-10T11:30:00.000Z",
};

function baseInput() {
  return {
    projectId: "codeflowmu",
    threadKey: "submission-v160",
    rootTaskId: "TASK-20260810-100",
    tasks: [rootTask, childTask],
    reports: [
      {
        report_id: "REPORT-20260810-101-DEV-to-PM",
        task_id: "TASK-20260810-101",
        thread_key: "submission-v160",
        status: "done",
      },
    ],
    runtimeSnapshots: [
      runtimeSnapshot("TASK-20260810-100", "waiting_report"),
      runtimeSnapshot("TASK-20260810-101", "running"),
    ],
    now: new Date("2026-08-10T12:00:00.000Z"),
  };
}

test("thread schedule is permutation-invariant and contains no business decision", () => {
  const input = baseInput();
  const first = buildThreadScheduleSnapshot(input);
  const second = buildThreadScheduleSnapshot({
    ...input,
    tasks: [...input.tasks].reverse(),
    reports: [...input.reports].reverse(),
    runtimeSnapshots: [...input.runtimeSnapshots].reverse(),
  });

  assert.equal(first.schema_version, THREAD_SCHEDULE_SCHEMA_VERSION);
  assert.equal(first.source_digest, second.source_digest);
  assert.equal(first.revision, second.revision);
  assert.deepEqual(first.work_items, second.work_items);
  assert.equal(first.business_decision, null);
  assert.equal(first.negative_list.rule_count, 15);
  assert.equal(first.negative_list.rule_ids.length, 15);
  assert.equal(first.negative_list.final_authority, "ADMIN");
});

test("negative-list approval waits for ADMIN without turning into a business verdict", () => {
  const snapshot = buildThreadScheduleSnapshot({
    projectId: "codeflowmu",
    threadKey: "submission-v160",
    rootTaskId: "TASK-20260810-100",
    tasks: [rootTask, childTask],
    approvals: [
      {
        approval_id: "APPROVAL-001",
        task_id: "TASK-20260810-101",
        thread_key: "submission-v160",
        status: "pending_approval",
      },
    ],
    runtimeSnapshots: [runtimeSnapshot("TASK-20260810-101", "waiting_report")],
    now: new Date("2026-08-10T12:00:00.000Z"),
  });

  const attention = snapshot.attention.find(
    (row) => row.kind === "authorization_required",
  );
  assert.equal(attention?.owner_role, "ADMIN");
  assert.deepEqual(attention?.evidence_refs, ["APPROVAL-001"]);
  assert.equal(snapshot.business_decision, null);
  assert.ok(
    snapshot.expected_events.some(
      (row) => row.kind === "approval_decision" && row.owner_role === "ADMIN",
    ),
  );
});

test("projection conflict and overdue facts request Agent attention only", () => {
  const snapshot = buildThreadScheduleSnapshot({
    projectId: "codeflowmu",
    threadKey: "submission-v160",
    rootTaskId: "TASK-20260810-100",
    tasks: [rootTask, childTask],
    runtimeSnapshots: [
      runtimeSnapshot("TASK-20260810-101", "projection_conflict", [
        {
          code: "SESSION_LEASE_CONFLICT",
          sources: ["live_session", "active_lease"],
          detail: "session and lease disagree",
        },
      ]),
    ],
    now: new Date("2026-08-10T12:00:00.000Z"),
  });

  assert.equal(snapshot.conflicts.length, 1);
  assert.ok(
    snapshot.attention.some((row) => row.kind === "runtime_projection_conflict"),
  );
  assert.ok(
    snapshot.attention.some((row) => row.kind === "expected_event_overdue"),
  );
  assert.equal(snapshot.business_decision, null);
});

test("thread-only evidence attaches to the root instead of every child", () => {
  const snapshot = buildThreadScheduleSnapshot({
    projectId: "codeflowmu",
    threadKey: "submission-v160",
    rootTaskId: "TASK-20260810-100",
    tasks: [rootTask, childTask],
    issues: [
      {
        issue_id: "ISSUE-THREAD-001",
        thread_key: "submission-v160",
        status: "active",
      },
    ],
    now: new Date("2026-08-10T12:00:00.000Z"),
  });

  const root = snapshot.work_items.find((row) => row.task_id === rootTask.task_id);
  const child = snapshot.work_items.find((row) => row.task_id === childTask.task_id);
  assert.deepEqual(root?.issue_ids, ["ISSUE-THREAD-001"]);
  assert.deepEqual(child?.issue_ids, []);
});

test("unrelated thread evidence does not change the schedule digest", () => {
  const baseline = buildThreadScheduleSnapshot(baseInput());
  const withUnrelatedEvidence = buildThreadScheduleSnapshot({
    ...baseInput(),
    reports: [
      ...(baseInput().reports ?? []),
      {
        report_id: "REPORT-OTHER-001",
        task_id: "TASK-20990101-999",
        thread_key: "another-thread",
        status: "done",
      },
    ],
    issues: [
      {
        issue_id: "ISSUE-OTHER-001",
        task_id: "TASK-20990101-999",
        thread_key: "another-thread",
        status: "active",
      },
    ],
  });

  assert.equal(withUnrelatedEvidence.source_digest, baseline.source_digest);
  assert.equal(withUnrelatedEvidence.revision, baseline.revision);
});
