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

import { createInMemoryMonitorCentralStore, createMonitorCentralService, mapPostgresFindingRow, MONITOR_CENTRAL_MIGRATIONS, MonitorCentralConfigurationError, monitorCentralStableJson, normalizeMonitorCentralIngest, structuredMonitorCentralError } from "./monitor-central-persistence";

function service() { return createMonitorCentralService({ store: createInMemoryMonitorCentralStore(), pageSize: 50 }); }
function locator(runId: string) { return { ownerNode: "NC01", ownerLane: "v03", ownerPvc: "sentinel-pvc", stateDir: `/state/${runId}`, relativePath: "analysis/report.json", sha256: "sha256:abc", sizeBytes: 12, kind: "report", reachable: true }; }
function input(runId: string, updatedAt = "2026-07-12T10:00:00.000Z", scope: Record<string, string> = {}) { return { sentinelId: scope.sentinelId ?? "sentinel-a", node: scope.node ?? "NC01", lane: scope.lane ?? "v03", runId, updatedAt, status: "succeeded", payload: { runId, views: { summary: { state: "ready" } } }, findings: [{ id: `${runId}-0` }, { id: `${runId}-1` }], artifactLocators: [locator(runId)], timeline: [{ phase: "done" }] }; }
async function ingest(target: ReturnType<typeof service>, runId: string, updatedAt?: string, scope?: Record<string, string>) { return target.ingest(input(runId, updatedAt, scope)); }

test("schema covers runner FK, locator reachability, and atomic insert contract", () => {
  const schema = MONITOR_CENTRAL_MIGRATIONS.join("\n");
  for (const name of ["schema_migrations", "runners", "runs", "findings", "run_payloads", "artifact_locators", "timeline_events", "import_manifests"]) assert.match(schema, new RegExp(`monitor\\.${name}`));
  assert.match(schema, /foreign key \(sentinel_id, node, lane\) references monitor\.runners/u);
  assert.match(schema, /owner_pvc.*state_dir.*relative_path.*reachable/u);
  assert.doesNotMatch(schema, /blob|bytea/iu);
});

test("HTTP terminal ingest is unbound-safe, ignores caller hash, and maps conflict to 409", async () => {
  const target = service();
  const fetch = target.fetch;
  const first = await fetch(new Request("http://local/api/ingest", { method: "POST", body: JSON.stringify(input("run-1")) }));
  const repeated = await fetch(new Request("http://local/api/ingest", { method: "POST", body: JSON.stringify({ ...input("run-1"), payloadHash: "sha256:forged" }) }));
  const conflict = await fetch(new Request("http://local/api/ingest", { method: "POST", body: JSON.stringify({ ...input("run-1"), status: "blocked", payloadHash: "sha256:forged" }) }));
  assert.equal(first.status, 201);
  assert.equal((await first.json() as any).disposition, "inserted");
  assert.equal(repeated.status, 200);
  assert.equal((await repeated.json() as any).disposition, "idempotent");
  assert.equal(conflict.status, 409);
  assert.equal((await conflict.json() as any).error.code, "payload_hash_conflict");
  assert.equal((await fetch(new Request("http://local/health"))).status, 200);
});

test("health returns 503 when the central store is not ready", async () => {
  const store = createInMemoryMonitorCentralStore();
  const target = createMonitorCentralService({ store: { ...store, async ping() { throw new Error("postgres unavailable"); } }, pageSize: 50 });
  const response = await target.fetch(new Request("http://local/health"));
  assert.equal(response.status, 503);
  assert.equal((await response.json() as { ok: boolean }).ok, false);
});

test("full terminal envelope controls hash and concurrent same identity converges", async () => {
  const target = service();
  const base = { ...input("run-envelope"), payloadHash: "sha256:caller-controlled" };
  const results = await Promise.all([target.ingest(base), target.ingest(base)]);
  assert.deepEqual(new Set(results.map((item) => item.disposition)), new Set(["inserted", "idempotent"]));
  await assert.rejects(() => target.ingest({ ...base, findings: [{ id: "changed" }] }), { code: "payload_hash_conflict" });
  const availability = await target.ingest({ ...base, artifactLocators: [{ ...locator("run-envelope"), reachable: false }] });
  assert.equal(availability.disposition, "idempotent");
  const detail = await (await target.fetch(new Request("http://local/api/runs/run-envelope?sentinelId=sentinel-a&node=NC01&lane=v03"))).json() as any;
  assert.equal(detail.run.artifactLocators[0].reachable, false);
});

test("stable JSON accepts optional fields and in-memory view preserves the #1878 contract", async () => {
  assert.equal(monitorCentralStableJson({ optional: undefined, value: "ok", list: [undefined] }), '{"list":[null],"value":"ok"}');
  const store = createInMemoryMonitorCentralStore();
  const normalized = normalizeMonitorCentralIngest({ ...input("top-level-view"), payload: { views: { summary: { state: "ready" } }, optional: undefined } });
  await store.ingest(normalized);
  assert.deepEqual(await store.view({ sentinelId: "sentinel-a", node: "NC01", lane: "v03" }, "top-level-view", "summary"), { runId: "top-level-view", name: "summary", value: { state: "ready" } });
});

test("global and scoped latest use updatedAt then runId descending", async () => {
  const target = service();
  await ingest(target, "run-a", "2026-07-12T10:00:00.000Z");
  await ingest(target, "run-b", "2026-07-12T10:00:00.000Z");
  await ingest(target, "run-c", "2026-07-12T09:00:00.000Z", { sentinelId: "sentinel-b" });
  const overview = await (await target.fetch(new Request("http://local/api/overview"))).json() as any;
  const scoped = await (await target.fetch(new Request("http://local/api/overview?sentinelId=sentinel-b"))).json() as any;
  assert.equal(overview.overview.latest.runId, "run-b");
  assert.equal(scoped.overview.latest.runId, "run-c");
});

test("detail and view require full composite identity", async () => {
  const target = service();
  await ingest(target, "shared", undefined, { sentinelId: "sentinel-a" });
  await ingest(target, "shared", undefined, { sentinelId: "sentinel-b" });
  const missing = await target.fetch(new Request("http://local/api/runs/shared"));
  const detail = await (await target.fetch(new Request("http://local/api/runs/shared?sentinelId=sentinel-b&node=NC01&lane=v03"))).json() as any;
  const view = await (await target.fetch(new Request("http://local/api/runs/shared/views/summary?sentinelId=sentinel-b&node=NC01&lane=v03"))).json() as any;
  assert.equal(missing.status, 400);
  assert.equal((await missing.json() as any).error.code, "scope_required");
  assert.equal(detail.run.sentinelId, "sentinel-b");
  assert.equal(view.view.value.state, "ready");
});

test("findings cursor contains finding index and does not skip a same-run page", async () => {
  const target = service();
  await ingest(target, "run-findings");
  const first = await (await target.fetch(new Request("http://local/api/findings?limit=1"))).json() as any;
  const second = await (await target.fetch(new Request(`http://local/api/findings?limit=1&cursor=${first.nextCursor}`))).json() as any;
  assert.equal(first.items[0].findingIndex, 1);
  assert.equal(second.items[0].findingIndex, 0);
});

test("PG finding mapping preserves selected status and payload hash", () => {
  const row = mapPostgresFindingRow({ sentinel_id: "sentinel-a", node: "NC01", lane: "v03", run_id: "run-pg", updated_at: "2026-07-12T10:00:00.000Z", status: "blocked", payload_hash: "sha256:terminal", finding_index: 2, finding: { id: "pg" } });
  assert.deepEqual(row, { sentinelId: "sentinel-a", node: "NC01", lane: "v03", runId: "run-pg", updatedAt: "2026-07-12T10:00:00.000Z", status: "blocked", payloadHash: "sha256:terminal", findingIndex: 2, finding: { id: "pg" } });
});

test("run detail returns normalized locator metadata without artifact bytes", async () => {
  const target = service();
  await ingest(target, "run-detail");
  const response = await (await target.fetch(new Request("http://local/api/runs/run-detail?sentinelId=sentinel-a&node=NC01&lane=v03"))).json() as any;
  assert.deepEqual(response.run.artifactLocators[0], locator("run-detail"));
  assert.equal(response.valuesRedacted, true);
});

test("monitor workspace composes runs while runtime facts remain unavailable without metadata", async () => {
  const target = service();
  await target.ingest({ ...input("run-ok", "2026-07-12T11:00:00.000Z"), findings: [{ id: "finding-a" }] });
  await target.ingest({ ...input("run-failed", "2026-07-12T10:00:00.000Z"), status: "failed", findings: [{ id: "finding-b" }, { id: "finding-c" }] });
  const response = await target.fetch(new Request("http://local/api/monitor/workspace?sentinelId=sentinel-a&node=NC01&lane=v03&status=failed&from=2026-07-12T09:00:00.000Z&limit=20"));
  const body = await response.json() as any;
  assert.equal(body.selection.defaultRunId, "run-failed");
  assert.equal(body.runs.items[0].findingCount, 2);
  assert.equal(body.health.state, "degraded");
  assert.equal(body.freshness.state, "unavailable");
  assert.equal(body.cadence.state, "unavailable");
  assert.equal(body.schemaVersion, undefined);
  assert.equal(body.capability, undefined);
});

test("monitor workspace filters before paging and counts findings beyond page size", async () => {
  const target = createMonitorCentralService({ store: createInMemoryMonitorCentralStore(), pageSize: 2 });
  await target.ingest({ ...input("new-1", "2026-07-12T13:00:00.000Z"), status: "succeeded", findings: [] });
  await target.ingest({ ...input("new-2", "2026-07-12T12:00:00.000Z"), status: "succeeded", findings: [] });
  await target.ingest({ ...input("older-match", "2026-07-12T11:00:00.000Z"), status: "failed", findings: [{ id: "f1" }, { id: "f2" }, { id: "f3" }] });
  const body = await (await target.fetch(new Request("http://local/api/monitor/workspace?sentinelId=sentinel-a&node=NC01&lane=v03&status=failed"))).json() as any;
  assert.equal(body.runs.items[0].runId, "older-match");
  assert.equal(body.runs.items[0].findingCount, 3);
  assert.equal(body.summary.totalRuns, 1);
  assert.equal(body.summary.findingCount, 3);
  assert.deepEqual(body.summary.statusCounts, { failed: 1 });
  assert.equal(body.runs.nextCursor, null);
});

test("monitor latest failure excludes running and unknown states", async () => {
  const target = service();
  await target.ingest({ ...input("failed-terminal", "2026-07-12T10:00:00.000Z"), status: "failed" });
  await target.ingest({ ...input("running-newer", "2026-07-12T12:00:00.000Z"), status: "running" });
  await target.ingest({ ...input("unknown-newest", "2026-07-12T13:00:00.000Z"), status: "unknown" });
  const body = await (await target.fetch(new Request("http://local/api/monitor/workspace?sentinelId=sentinel-a&node=NC01&lane=v03"))).json() as any;
  assert.equal(body.summary.latestFailureAt, "2026-07-12T10:00:00.000Z");
});

test("monitor workspace projects cadence freshness and health only from runtime metadata", async () => {
  const store = createInMemoryMonitorCentralStore();
  const normalized = normalizeMonitorCentralIngest(input("run-metadata"));
  await store.importSource({ manifestId: "monitor-workbench-runtime", sourceFingerprint: "sha256:runtime", rowCount: 1 }, [normalized], [
    { sentinelId: "sentinel-a", node: "NC01", lane: "v03", key: "monitor.health", value: { state: "healthy" }, updatedAt: "2026-07-12T10:01:00.000Z" },
    { sentinelId: "sentinel-a", node: "NC01", lane: "v03", key: "monitor.freshness", value: { state: "current", ageSeconds: 30 }, updatedAt: "2026-07-12T10:01:00.000Z" },
    { sentinelId: "sentinel-a", node: "NC01", lane: "v03", key: "monitor.cadence", value: { state: "available", label: "owning-yaml-projected" }, updatedAt: "2026-07-12T10:01:00.000Z" },
  ]);
  const target = createMonitorCentralService({ store, pageSize: 50 });
  const body = await (await target.fetch(new Request("http://local/api/monitor/workspace?sentinelId=sentinel-a&node=NC01&lane=v03"))).json() as any;
  assert.equal(body.health.state, "healthy");
  assert.equal(body.freshness.ageSeconds, 30);
  assert.equal(body.cadence.label, "owning-yaml-projected");
  assert.equal(body.cadence.sourceKey, "monitor.cadence");
});

test("monitor run detail projects stable ids without a client join", async () => {
  const target = service();
  await target.ingest({ ...input("run-workbench"), payload: { diagnostics: { state: "ready" } }, timeline: [{ id: "event-done", phase: "done" }], findings: [{ id: "finding-visible" }] });
  const response = await target.fetch(new Request("http://local/api/monitor/runs/run-workbench?sentinelId=sentinel-a&node=NC01&lane=v03"));
  const body = await response.json() as any;
  assert.equal(body.timeline[0].id, "event-done");
  assert.equal(body.findings[0].id, "finding-visible");
  assert.equal(body.artifacts[0].id, "artifact-0");
  assert.deepEqual(body.diagnostics, { state: "ready" });
});

test("configuration is explicit and missing environment projection fails structurally", async () => {
  const target = service();
  assert.equal((await target.health()).ok, true);
  assert.throws(() => createMonitorCentralService({ databaseUrl: "", pageSize: 50 }), MonitorCentralConfigurationError);
  assert.throws(() => createMonitorCentralService({ store: createInMemoryMonitorCentralStore() }), { code: "monitor_central_page_size_missing" });
  const result = spawnSync("bun", ["scripts/monitor-central-service.ts"], { cwd: process.cwd(), encoding: "utf8", env: { PATH: process.env.PATH ?? "" } });
  assert.equal(result.status, 2);
  assert.equal(JSON.parse(result.stdout).error.code, "monitor_central_runtime_config_missing");
});

test("structured error projection redacts database credentials", () => {
  const error = new Error("connect postgres://user:secret@example.test/monitor failed");
  assert.deepEqual(structuredMonitorCentralError(error), { code: "invalid_request", message: "connect postgres://<redacted> failed" });
});
