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

import { mintContentId } from "../src/id.js";
import {
  parsePublishedRecord,
  serializePublishedRecord,
  type PublishedRecord,
} from "../src/ingest.js";

const baseRec = (body: string): PublishedRecord => ({
  id: mintContentId(body),
  provenance: "published",
  published_date: "2026-05-26",
  captured_at: "2026-06-23",
  source: "manual",
  specifics: [],
  trends: [],
  body,
});

/** Round-trip helper: parse∘serialize must equal the original record. */
function roundTrip(rec: PublishedRecord): PublishedRecord {
  return parsePublishedRecord(serializePublishedRecord(rec));
}

describe("mintContentId — verbatim-body content hash (SC3)", () => {
  test("deterministic: same body → same id", () => {
    const body = "Jeg lærte noe om dømmekraft i dag.\n\nDel 1 av serien.";
    assert.equal(mintContentId(body), mintContentId(body));
  });

  test("returns a 12-hex id", () => {
    assert.match(mintContentId("any body"), /^[0-9a-f]{12}$/);
  });

  test("different bodies → different ids", () => {
    assert.notEqual(mintContentId("post A"), mintContentId("post B"));
  });

  test("whitespace/case differences mint DIFFERENT ids (no normalize-collision, B2 guard)", () => {
    // Under specifics-bank normalizeContent these would collapse to the SAME id —
    // proving the post-body hash is byte-identity, not normalized.
    assert.notEqual(mintContentId("Hello World"), mintContentId("hello  world"));
    assert.notEqual(
      mintContentId("Line one\nLine two"),
      mintContentId("Line one Line two"),
    );
  });
});

describe("PublishedRecord grammar — parse∘serialize identity (SC2, B1 edge battery)", () => {
  const bodies: Record<string, string> = {
    "plain multi-paragraph": "Hook line.\n\nBody paragraph one.\n\nBody paragraph two.",
    "empty body": "",
    "single newline": "\n",
    "trailing newlines": "post text\n\n",
    "body starting with ---": "---\nlooks like a separator but is body",
    "body containing a \\n---\\n sentinel mid-text": "before\n---\nafter the fake separator",
    "header-shaped first line": "id: 0123456789ab\nprovenance: published",
    "special chars ] | quotes": 'value with ] and | and "quotes" inside',
  };

  for (const [label, body] of Object.entries(bodies)) {
    test(`round-trips: ${label}`, () => {
      const rec = baseRec(body);
      assert.deepEqual(roundTrip(rec), rec);
    });
  }

  test("serialized form has the fixed 5-line header then the --- sentinel", () => {
    const text = serializePublishedRecord(baseRec("hi"));
    const [header, ...rest] = text.split("\n---\n");
    assert.equal(header.split("\n").length, 5, "exactly 5 header lines before the sentinel");
    assert.equal(rest.join("\n---\n"), "hi", "body is everything after the FIRST sentinel");
    assert.match(text, /^id: [0-9a-f]{12}\nprovenance: published\n/);
  });

  test("a published/ record with non-published provenance is rejected (corruption signal)", () => {
    const corrupt = serializePublishedRecord(baseRec("x")).replace(
      "provenance: published",
      "provenance: ai-draft",
    );
    assert.throws(() => parsePublishedRecord(corrupt), /provenance/i);
  });
});

describe("SB-S3c — raw-material ids on the published record", () => {
  const ids2 = ["aaaaaaaaaaaa", "bbbbbbbbbbbb"];

  // SC1 — round-trip with non-empty specifics/trends, order preserved.
  test("SC1: parse∘serialize round-trips a record carrying specifics/trends (order preserved)", () => {
    const rec: PublishedRecord = { ...baseRec("a tagged post"), specifics: ids2, trends: ["cccccccccccc"] };
    assert.deepEqual(roundTrip(rec), rec);
  });

  // SC2 — empty arrays → unchanged 5-line header; a pre-S3c fixture round-trips byte-identically.
  test("SC2: empty specifics/trends serialize byte-identically to a pre-S3c record", () => {
    const oldText =
      "id: 0123456789ab\nprovenance: published\npublished_date: 2026-05-26\n" +
      "captured_at: 2026-06-23\nsource: manual\n---\nthe body text";
    assert.equal(serializePublishedRecord(parsePublishedRecord(oldText)), oldText);
    // and the live empty-array record still emits exactly 5 header lines.
    const text = serializePublishedRecord(baseRec("hi"));
    assert.equal(text.split("\n---\n")[0].split("\n").length, 5);
  });

  // SC2b — when present, the new lines are appended AFTER source: and before the sentinel.
  test("SC2b: non-empty specifics/trends are appended after source:, before the --- sentinel", () => {
    const text = serializePublishedRecord({ ...baseRec("x"), specifics: [ids2[0]], trends: [ids2[1]] });
    const header = text.split("\n---\n")[0];
    assert.match(header, /source: manual\nspecifics: aaaaaaaaaaaa\ntrends: bbbbbbbbbbbb$/);
  });

  // SC3 (parse side) — a non-12-hex id in the header throws, never silently dropped.
  test("SC3: a malformed specifics/trends id throws on parse", () => {
    const bad =
      serializePublishedRecord(baseRec("x")).replace(
        "source: manual",
        "source: manual\nspecifics: NOTAHEXID",
      );
    assert.throws(() => parsePublishedRecord(bad), /bad specifics id/i);
  });
});
