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

import {
  appendPhase,
  editionFacts,
  readEditionState,
  saveEditionState,
  seriesRootFromStatePath,
} from "../src/editionState.js";
import type { EditionStateFile } from "../src/types.js";

const tmp = () => mkdtempSync(join(tmpdir(), "editions-state-"));

const AT = "2026-07-21T09:30:00.000Z";

function state(): EditionStateFile {
  return {
    schemaVersion: 1,
    series: { slug: "seres", title: "Seres" },
    currentArticle: "05",
    currentPhase: "skeleton-pitch",
    articles: {
      "04": { title: "Forrige", phase: "scheduling", phaseLog: [{ phase: "scheduling", completedAt: AT }] },
      "05": { title: "Da pipelinen brøt sammen", phase: "skeleton-pitch", phaseLog: [] },
    },
  } as unknown as EditionStateFile;
}

describe("editionState — facts", () => {
  test("reads series, edition id, title and phase from the state file", () => {
    const facts = editionFacts(state());
    assert.deepEqual(facts, {
      series: "seres",
      editionId: "05",
      title: "Da pipelinen brøt sammen",
      currentPhase: "skeleton-pitch",
    });
  });

  test("a missing currentArticle is an error, not a guess", () => {
    const broken = { ...state(), currentArticle: undefined } as unknown as EditionStateFile;
    assert.throws(() => editionFacts(broken), /currentArticle/);
  });

  test("a currentArticle with no matching articles entry is an error", () => {
    const broken = { ...state(), currentArticle: "99" } as unknown as EditionStateFile;
    assert.throws(() => editionFacts(broken), /99/);
  });

  test("a missing currentPhase is an error — the register must never mirror a guessed phase", () => {
    const broken = { ...state(), currentPhase: undefined } as unknown as EditionStateFile;
    assert.throws(() => editionFacts(broken), /currentPhase/);
  });

  test("an untitled article still resolves — the title is telemetry, not a gate", () => {
    const s = state();
    delete (s.articles["05"] as Record<string, unknown>).title;
    assert.equal(editionFacts(s).title, "");
  });
});

describe("editionState — phaseLog", () => {
  test("appends {phase, completedAt} to the article's log", () => {
    const { state: next, appended } = appendPhase(state(), "05", "skeleton-pitch", AT);
    assert.equal(appended, true);
    assert.deepEqual(next.articles["05"].phaseLog, [{ phase: "skeleton-pitch", completedAt: AT }]);
  });

  test("re-running the same transition does not double-log", () => {
    const once = appendPhase(state(), "05", "skeleton-pitch", AT).state;
    const { state: twice, appended } = appendPhase(once, "05", "skeleton-pitch", "2026-07-21T10:00:00.000Z");
    assert.equal(appended, false);
    assert.equal(twice.articles["05"].phaseLog?.length, 1);
    assert.equal(twice.articles["05"].phaseLog?.[0].completedAt, AT, "the first stamp is the real one");
  });

  test("a phase repeated AFTER another phase is logged again — /linkedin:pivot re-runs cleared gates", () => {
    let s = appendPhase(state(), "05", "factcheck-sweep", AT).state;
    s = appendPhase(s, "05", "editorial-review", "2026-07-21T11:00:00.000Z").state;
    const { state: pivoted, appended } = appendPhase(s, "05", "factcheck-sweep", "2026-07-22T08:00:00.000Z");
    assert.equal(appended, true);
    assert.equal(pivoted.articles["05"].phaseLog?.length, 3);
    assert.deepEqual(
      pivoted.articles["05"].phaseLog?.map((e) => e.phase),
      ["factcheck-sweep", "editorial-review", "factcheck-sweep"],
    );
  });

  test("an article written before phaseLog existed gets the field created", () => {
    const s = state();
    delete (s.articles["05"] as Record<string, unknown>).phaseLog;
    const { state: next } = appendPhase(s, "05", "research", AT);
    assert.deepEqual(next.articles["05"].phaseLog, [{ phase: "research", completedAt: AT }]);
  });

  test("other articles are left alone", () => {
    const next = appendPhase(state(), "05", "research", AT).state;
    assert.equal(next.articles["04"].phaseLog?.length, 1);
    assert.equal(next.articles["04"].phaseLog?.[0].phase, "scheduling");
  });

  test("an unknown article is an error, not a silently created one", () => {
    assert.throws(() => appendPhase(state(), "99", "research", AT), /99/);
  });
});

describe("editionState — io", () => {
  test("save then read round-trips, pretty-printed with a trailing newline", () => {
    const dir = tmp();
    try {
      const path = join(dir, "linkedin", "edition-state.json");
      saveEditionState(path, appendPhase(state(), "05", "research", AT).state);
      const raw = readFileSync(path, "utf8");
      assert.ok(raw.endsWith("\n"));
      assert.ok(raw.includes('\n  "schemaVersion"'), "must stay human-diffable (2-space indent)");
      const back = readEditionState(path);
      assert.equal(back.articles["05"].phaseLog?.[0].phase, "research");
    } finally {
      rmSync(dir, { recursive: true, force: true });
    }
  });

  test("reading a missing state file is an error — the register mirrors state, it never invents it", () => {
    const dir = tmp();
    try {
      assert.throws(() => readEditionState(join(dir, "absent.json")));
    } finally {
      rmSync(dir, { recursive: true, force: true });
    }
  });

  test("the series root is derived from the conventional <serie>/linkedin/edition-state.json path", () => {
    assert.equal(seriesRootFromStatePath("/series/seres/linkedin/edition-state.json"), "/series/seres");
  });
});
