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

import {
  normalizeContent,
  specificId,
  emptyBank,
  loadBank,
  saveBank,
  addSpecific,
  queryByTopic,
} from "../src/bank.js";
import { SCHEMA_VERSION } from "../src/types.js";
import type { Bank } from "../src/types.js";

const prov = (source = "test", capturedAt = "2026-06-20") => ({ source, capturedAt });

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

describe("specifics-bank store", () => {
  describe("normalizeContent + specificId", () => {
    test("id is deterministic and content-derived", () => {
      assert.equal(specificId("Sparte to dager."), specificId("Sparte to dager."));
    });

    test("id is stable across case + surrounding/collapsed whitespace", () => {
      const a = specificId("Sparte to dager.");
      const b = specificId("  sparte   TO    dager. ");
      assert.equal(a, b, "normalization should collapse case + whitespace");
    });

    test("different content yields different ids", () => {
      assert.notEqual(specificId("Sparte to dager."), specificId("Sparte tre dager."));
    });

    test("normalizeContent collapses whitespace + lowercases", () => {
      assert.equal(normalizeContent("  Hei   DU\n da "), "hei du da");
    });
  });

  describe("emptyBank + load/save round-trip", () => {
    test("emptyBank has the schema version and no specifics", () => {
      const b = emptyBank();
      assert.equal(b.schemaVersion, SCHEMA_VERSION);
      assert.deepEqual(b.specifics, []);
    });

    test("loadBank on a missing file returns an empty bank (never throws)", () => {
      const dir = tmp();
      try {
        const b = loadBank(join(dir, "does-not-exist.json"));
        assert.equal(b.schemaVersion, SCHEMA_VERSION);
        assert.deepEqual(b.specifics, []);
      } finally {
        rmSync(dir, { recursive: true, force: true });
      }
    });

    test("saveBank then loadBank round-trips", () => {
      const dir = tmp();
      const path = join(dir, "bank.json");
      try {
        let bank: Bank = emptyBank();
        bank = addSpecific(bank, {
          type: "number",
          content: "Saksbehandleren sparte to dager i uka.",
          topicTags: ["saksbehandling", "produktivitet"],
          provenance: prov("seres/05"),
          verification: "unverified",
        }).bank;
        saveBank(path, bank);
        assert.ok(existsSync(path));
        const loaded = loadBank(path);
        assert.equal(loaded.specifics.length, 1);
        assert.equal(loaded.specifics[0].content, "Saksbehandleren sparte to dager i uka.");
        assert.equal(loaded.specifics[0].type, "number");
      } finally {
        rmSync(dir, { recursive: true, force: true });
      }
    });
  });

  describe("addSpecific", () => {
    test("adds a new specific with a computed id and reports added:true", () => {
      const res = addSpecific(emptyBank(), {
        type: "named-case",
        content: "En kontraktsjurist hos en kunde automatiserte avtalegjennomgang.",
        topicTags: ["juss", "automatisering"],
        provenance: prov(),
        verification: "n/a",
      });
      assert.equal(res.added, true);
      assert.equal(res.bank.specifics.length, 1);
      assert.equal(res.bank.specifics[0].id, specificId(res.bank.specifics[0].content));
      assert.equal(res.bank.specifics[0].status, "active");
    });

    test("preserves the operator's content verbatim (normalizes only for id)", () => {
      const content = "  KI-en   nådde IKKE arkivdataene.  ";
      const res = addSpecific(emptyBank(), {
        type: "what-broke",
        content,
        topicTags: ["data"],
        provenance: prov(),
      });
      assert.equal(res.bank.specifics[0].content, content, "content must not be mutated");
    });

    test("dedupes on normalized content: same material twice → one entry, tags unioned", () => {
      let bank = emptyBank();
      bank = addSpecific(bank, {
        type: "number",
        content: "Sparte to dager.",
        topicTags: ["produktivitet"],
        provenance: prov("seres/04"),
      }).bank;
      const res2 = addSpecific(bank, {
        type: "number",
        content: "  sparte   to dager. ",
        topicTags: ["saksbehandling"],
        provenance: prov("seres/05"),
      });
      assert.equal(res2.added, false, "duplicate content must not add a second entry");
      assert.equal(res2.bank.specifics.length, 1);
      assert.deepEqual(
        [...res2.bank.specifics[0].topicTags].sort(),
        ["produktivitet", "saksbehandling"],
        "tags must be unioned on re-capture",
      );
    });

    test("defaults verification to unverified and status to active when omitted", () => {
      const res = addSpecific(emptyBank(), {
        type: "contrarian",
        content: "De fleste KI-pilotene feiler på data, ikke på modellvalg.",
        topicTags: ["data"],
        provenance: prov(),
      });
      assert.equal(res.bank.specifics[0].verification, "unverified");
      assert.equal(res.bank.specifics[0].status, "active");
    });
  });

  describe("queryByTopic", () => {
    const seed = (): Bank => {
      let bank = emptyBank();
      bank = addSpecific(bank, {
        type: "named-case",
        content: "Saksbehandler-case A.",
        topicTags: ["saksbehandling", "arkiv"],
        provenance: prov("seres/04", "2026-05-01"),
      }).bank;
      bank = addSpecific(bank, {
        type: "number",
        content: "Tallet B.",
        topicTags: ["saksbehandling", "produktivitet", "ledelse"],
        provenance: prov("seres/05", "2026-06-01"),
      }).bank;
      bank = addSpecific(bank, {
        type: "other",
        content: "Irrelevant C.",
        topicTags: ["privatliv"],
        provenance: prov("manual", "2026-06-10"),
      }).bank;
      return bank;
    };

    test("ranks hits by tag overlap (desc)", () => {
      const hits = queryByTopic(seed(), ["saksbehandling", "produktivitet"]);
      assert.equal(hits.length, 2);
      assert.equal(hits[0].specific.content, "Tallet B."); // overlap 2
      assert.equal(hits[0].tagOverlap, 2);
      assert.equal(hits[1].specific.content, "Saksbehandler-case A."); // overlap 1
      assert.equal(hits[1].tagOverlap, 1);
    });

    test("returns [] when nothing matches", () => {
      assert.deepEqual(queryByTopic(seed(), ["ingenting"]), []);
    });

    test("excludes archived specifics", () => {
      const bank = seed();
      bank.specifics[1].status = "archived"; // archive "Tallet B."
      const hits = queryByTopic(bank, ["saksbehandling"]);
      assert.equal(hits.length, 1);
      assert.equal(hits[0].specific.content, "Saksbehandler-case A.");
    });

    test("ties on overlap are broken by capturedAt (newest first)", () => {
      const hits = queryByTopic(seed(), ["saksbehandling"]); // both A and B overlap 1
      assert.equal(hits[0].specific.content, "Tallet B."); // 2026-06-01 newer than 2026-05-01
      assert.equal(hits[1].specific.content, "Saksbehandler-case A.");
    });
  });
});
