import { describe, test } from "node:test";
import assert from "node:assert/strict";
import { spawnSync } from "node:child_process";
import { fileURLToPath } from "node:url";
import { mkdtempSync, rmSync, writeFileSync, readFileSync } from "node:fs";
import { join } from "node:path";
import { tmpdir } from "node:os";

import { addSpecific, emptyBank, specificId, saveBank } from "../src/bank.js";

// Resolve the package root so the subprocess `src/cli.ts` path + the `tsx` loader
// resolve regardless of the runner's cwd.
const bankDir = fileURLToPath(new URL("..", import.meta.url));

function run(args: string[]): { status: number | null; stdout: string; stderr: string } {
  const res = spawnSync("node", ["--import", "tsx", "src/cli.ts", ...args], {
    encoding: "utf8",
    cwd: bankDir,
  });
  return { status: res.status, stdout: res.stdout, stderr: res.stderr };
}

const tmp = () => mkdtempSync(join(tmpdir(), "specifics-cli-usage-"));

const CONTENT = "Vi målte 40 prosent kortere ledetid etter omleggingen.";

/** A bank + an edition-state whose slot-map binds one slot to the bank specific. */
function fixture(dir: string): { bankPath: string; editionPath: string; id: string } {
  const bankPath = join(dir, "specifics-bank.json");
  const id = specificId(CONTENT);
  const bank = addSpecific(emptyBank(), {
    type: "number",
    content: CONTENT,
    topicTags: ["ai"],
    provenance: { capturedAt: "2026-06-20", source: "test" },
  }).bank;
  saveBank(bankPath, bank);

  const editionPath = join(dir, "edition-state.json");
  writeFileSync(
    editionPath,
    JSON.stringify({
      currentArticle: "05",
      articles: {
        "05": {
          title: "Da pipelinen brøt sammen",
          livedSpecifics: {
            status: "bound",
            slots: [
              { slotId: "kp1", kind: "key-point", label: "ledetid", binding: { type: "specific", specificId: id } },
              { slotId: "kp2", kind: "key-point", label: "annet", binding: { type: "abstrakt", rationale: "bevisst" } },
            ],
          },
        },
      },
    }),
    "utf8",
  );
  return { bankPath, editionPath, id };
}

describe("specifics-bank CLI — record-usage (Step 8 lock)", () => {
  test("stamps the edition on the bound specifics and persists it", () => {
    const dir = tmp();
    try {
      const { bankPath, editionPath, id } = fixture(dir);

      const res = run(["record-usage", "--edition", editionPath, "--edition-id", "seres/05", "--bank", bankPath]);
      assert.equal(res.status, 0, res.stderr);

      const onDisk = JSON.parse(readFileSync(bankPath, "utf8"));
      const stamped = onDisk.specifics.find((s: { id: string }) => s.id === id);
      assert.deepEqual(stamped.usedIn, ["seres/05"]);
    } finally {
      rmSync(dir, { recursive: true, force: true });
    }
  });

  test("only specific-bound slots are stamped — abstrakt/ekstern consume no bank material", () => {
    const dir = tmp();
    try {
      const { bankPath, editionPath } = fixture(dir);
      const res = run(["record-usage", "--edition", editionPath, "--edition-id", "seres/05", "--bank", bankPath, "--json"]);
      assert.equal(res.status, 0, res.stderr);

      const out = JSON.parse(res.stdout);
      assert.equal(out.recorded.length, 1);
      assert.deepEqual(out.unknown, []);
    } finally {
      rmSync(dir, { recursive: true, force: true });
    }
  });

  test("re-running after a pivot re-lock is idempotent", () => {
    const dir = tmp();
    try {
      const { bankPath, editionPath, id } = fixture(dir);
      run(["record-usage", "--edition", editionPath, "--edition-id", "seres/05", "--bank", bankPath]);
      const again = run(["record-usage", "--edition", editionPath, "--edition-id", "seres/05", "--bank", bankPath, "--json"]);
      assert.equal(again.status, 0, again.stderr);

      const out = JSON.parse(again.stdout);
      assert.deepEqual(out.recorded, []);
      assert.equal(out.alreadyRecorded.length, 1);

      const onDisk = JSON.parse(readFileSync(bankPath, "utf8"));
      const stamped = onDisk.specifics.find((s: { id: string }) => s.id === id);
      assert.deepEqual(stamped.usedIn, ["seres/05"], "a re-lock must not double-stamp");
    } finally {
      rmSync(dir, { recursive: true, force: true });
    }
  });

  test("an edition with no bound specifics is a no-op, exit 0", () => {
    const dir = tmp();
    try {
      const { bankPath } = fixture(dir);
      const empty = join(dir, "empty-edition.json");
      writeFileSync(empty, JSON.stringify({ currentArticle: "06", articles: { "06": {} } }), "utf8");

      const res = run(["record-usage", "--edition", empty, "--edition-id", "seres/06", "--bank", bankPath, "--json"]);
      assert.equal(res.status, 0, res.stderr);
      assert.deepEqual(JSON.parse(res.stdout).recorded, []);
    } finally {
      rmSync(dir, { recursive: true, force: true });
    }
  });

  test("bad invocations exit 2", () => {
    const dir = tmp();
    try {
      const { bankPath, editionPath } = fixture(dir);
      assert.equal(run(["record-usage", "--bank", bankPath]).status, 2); // no --edition
      assert.equal(
        run(["record-usage", "--edition", editionPath, "--bank", bankPath]).status,
        2, // no --edition-id: an unattributable use is worse than none
      );
    } finally {
      rmSync(dir, { recursive: true, force: true });
    }
  });
});
