import { describe, expect, test } from "bun:test";
import {
  diffChecklists,
  parseSpecSourceCategory,
  parseUpstreamChecklist,
  serializeSpecSource,
  slugFromUrl,
} from "./checklist-sync";
import { CATEGORY_META, type SpecCategory, type SpecItem } from "./types";

const META = { id: "foundations" as const, label: "Foundations" };

const ITEMS: SpecItem[] = [
  {
    id: "foundations/doctype",
    category: "foundations",
    title: "The HTML doctype",
    status: "required",
    description: "Every HTML document must start with <!doctype html>.",
    url: "https://specification.website/spec/foundations/doctype/",
  },
  {
    id: "foundations/popover-api",
    category: "foundations",
    title: "Popover API",
    status: "recommended",
    description: "Replace scripted modals with a native top-layer primitive.",
    url: "https://specification.website/spec/foundations/popover-api/",
  },
];

describe("slugFromUrl", () => {
  test("strips trailing slashes and keeps the last segment", () => {
    expect(slugFromUrl("https://x.test/spec/seo/robots-txt/")).toBe(
      "robots-txt",
    );
    expect(slugFromUrl("https://x.test/spec/seo/robots-txt")).toBe(
      "robots-txt",
    );
  });
});

describe("spec-source round-trip", () => {
  test("parse(serialize(items)) restitue les items à l'identique", () => {
    const md = serializeSpecSource(META, ITEMS);
    expect(parseSpecSourceCategory(md, META.id)).toEqual(ITEMS);
  });

  test("le format sérialisé est le format historique exact", () => {
    const md = serializeSpecSource(META, ITEMS.slice(0, 1));
    expect(md).toBe(
      `# The Website Specification — checklist (1 items)\n\n## Foundations\n- [ ] **The HTML doctype** _(required)_ — Every HTML document must start with <!doctype html>.\n      https://specification.website/spec/foundations/doctype/\n`,
    );
  });
});

describe("parseSpecSourceCategory", () => {
  test("jette si l'URL manque après un item", () => {
    const md = "- [ ] **X** _(required)_ — desc\npas une url";
    expect(() => parseSpecSourceCategory(md, "seo")).toThrow(/Missing URL/);
  });

  test("jette sur slug dupliqué dans la catégorie", () => {
    const md = serializeSpecSource(META, [ITEMS[0], { ...ITEMS[0] }]);
    expect(() => parseSpecSourceCategory(md, META.id)).toThrow(/Duplicate/);
  });

  test("ignore les lignes non-item (header, heading, vides)", () => {
    const md = serializeSpecSource(META, ITEMS);
    const items = parseSpecSourceCategory(md, META.id);
    expect(items).toHaveLength(2);
  });
});

// Fixture minimale au format checklist.md upstream : préambule, heading par
// label, ligne de description de catégorie non indentée, items avec description
// indentée 2 espaces (une wrappée sur 3 lignes), chevrons échappés.
function upstreamFixture(): string {
  const cats = CATEGORY_META.map((meta) => {
    const slug = `${meta.id}-topic`;
    return `## ${meta.label}\n\nBlurb de la catégorie ${meta.label}.\n\n- [ ] [Topic \\<${meta.id}\\>](https://specification.website/spec/${meta.id}/${slug}/) — Required\n  Desc de ${meta.id}\n  wrappée sur\n  trois lignes.\n`;
  });
  return `# The Website Specification — Checklist\n\nEvery spec item, grouped by category.\n\nSource: https://specification.website/checklist/ · Licensed CC BY 4.0\n\n${cats.join("\n")}`;
}

describe("parseUpstreamChecklist", () => {
  test("parse la fixture : 10 catégories dans l'ordre upstream, labels mappés", () => {
    const cats = parseUpstreamChecklist(upstreamFixture());
    expect(cats.map((c) => c.id)).toEqual(CATEGORY_META.map((c) => c.id));
    // "Well-Known URIs" → well-known, "Internationalisation" → i18n
    expect(cats.find((c) => c.label === "Well-Known URIs")?.id).toBe(
      "well-known",
    );
    expect(cats.find((c) => c.label === "Internationalisation")?.id).toBe(
      "i18n",
    );
  });

  test("déséchappe les chevrons, lowercase le statut, joint le wrap multi-lignes", () => {
    const cats = parseUpstreamChecklist(upstreamFixture());
    const item = cats[0].items[0];
    expect(item.title).toBe("Topic <foundations>");
    expect(item.status).toBe("required");
    expect(item.description).toBe(
      "Desc de foundations wrappée sur trois lignes.",
    );
    expect(item.id).toBe("foundations/foundations-topic");
  });

  test("jette sur heading de catégorie inconnu", () => {
    const md = upstreamFixture().replace("## Foundations", "## Quantum");
    expect(() => parseUpstreamChecklist(md)).toThrow(/Unknown upstream/);
  });

  test("jette si une catégorie attendue manque", () => {
    const md = upstreamFixture().replace("## Privacy", "## SEO");
    expect(() => parseUpstreamChecklist(md)).toThrow(/Duplicate|missing/);
  });

  test("jette sur item sans description", () => {
    const md = `## Foundations\n\n- [ ] [X](https://x.test/spec/foundations/x/) — Required\n\n## SEO\n`;
    expect(() => parseUpstreamChecklist(md)).toThrow(/Missing description/);
  });

  test("préserve l'ordre des items dans une catégorie", () => {
    const md = `${upstreamFixture()}\n`.replace(
      "  trois lignes.\n",
      "  trois lignes.\n- [ ] [Second](https://specification.website/spec/foundations/second/) — Avoid\n  Un anti-pattern.\n",
    );
    const cats = parseUpstreamChecklist(md);
    expect(cats[0].items.map((i) => i.id)).toEqual([
      "foundations/foundations-topic",
      "foundations/second",
    ]);
    expect(cats[0].items[1].status).toBe("avoid");
  });
});

describe("diffChecklists", () => {
  const base: SpecCategory[] = [
    {
      ...CATEGORY_META[0],
      items: [
        ITEMS[0],
        { ...ITEMS[1] },
        {
          id: "foundations/gone",
          category: "foundations",
          title: "Gone",
          status: "optional",
          description: "Bientôt supprimé.",
          url: "https://specification.website/spec/foundations/gone/",
        },
      ],
    },
  ];

  test("détecte added / removed / statusChanged / textChanged", () => {
    const after: SpecCategory[] = [
      {
        ...CATEGORY_META[0],
        items: [
          { ...ITEMS[0], status: "recommended" }, // status change
          { ...ITEMS[1], description: "Réécrit." }, // text change
          {
            id: "foundations/new-topic",
            category: "foundations",
            title: "New",
            status: "required",
            description: "Nouveau.",
            url: "https://specification.website/spec/foundations/new-topic/",
          },
        ],
      },
    ];
    const diff = diffChecklists(base, after);
    expect(diff.added.map((i) => i.id)).toEqual(["foundations/new-topic"]);
    expect(diff.removed.map((i) => i.id)).toEqual(["foundations/gone"]);
    expect(diff.statusChanged).toEqual([
      { id: "foundations/doctype", from: "required", to: "recommended" },
    ]);
    expect(diff.textChanged).toEqual(["foundations/popover-api"]);
  });

  test("diff vide quand rien ne change", () => {
    const diff = diffChecklists(base, base);
    expect(diff.added).toEqual([]);
    expect(diff.removed).toEqual([]);
    expect(diff.statusChanged).toEqual([]);
    expect(diff.textChanged).toEqual([]);
  });
});
