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

import {
  normalizeField,
  trendId,
  emptyStore,
  loadStore,
  saveStore,
  addTrend,
  queryByTopic,
  history,
} from "../src/store.js";
import { SCHEMA_VERSION } from "../src/types.js";
import type { TrendStore } from "../src/types.js";

const tmp = () => mkdtempSync(join(tmpdir(), "trends-store-"));

describe("trends store", () => {
  describe("normalizeField + trendId", () => {
    test("normalizeField collapses whitespace + lowercases", () => {
      assert.equal(normalizeField("  AI   Trends\n2026 "), "ai trends 2026");
    });

    test("id is deterministic and (title+url)-derived", () => {
      assert.equal(
        trendId("AI agents go mainstream", "https://example.com/a"),
        trendId("AI agents go mainstream", "https://example.com/a"),
      );
    });

    test("id is stable across case + surrounding/collapsed whitespace in title AND url", () => {
      const a = trendId("AI agents go mainstream", "https://example.com/article");
      const b = trendId("  ai   AGENTS go mainstream ", "  HTTPS://EXAMPLE.COM/article ");
      assert.equal(a, b, "normalization should collapse case + whitespace on both fields");
    });

    test("different title yields a different id (same url)", () => {
      assert.notEqual(
        trendId("AI agents go mainstream", "https://example.com/a"),
        trendId("AI copilots go mainstream", "https://example.com/a"),
      );
    });

    test("different url yields a different id (same title)", () => {
      assert.notEqual(
        trendId("Same headline", "https://example.com/a"),
        trendId("Same headline", "https://example.com/b"),
      );
    });
  });

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

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

    test("saveStore creates the parent dir, then loadStore round-trips", () => {
      const dir = tmp();
      const path = join(dir, "nested", "trends.json");
      try {
        let store: TrendStore = emptyStore();
        store = addTrend(store, {
          title: "Norway opens public-sector AI sandbox",
          url: "https://example.com/sandbox",
          source: "tavily",
          capturedAt: "2026-06-20",
          topics: ["public-sector", "ai-policy"],
          summary: "A regulatory sandbox for public bodies.",
        }).store;
        saveStore(path, store);
        assert.ok(existsSync(path));
        const loaded = loadStore(path);
        assert.equal(loaded.trends.length, 1);
        assert.equal(loaded.trends[0].title, "Norway opens public-sector AI sandbox");
        assert.equal(loaded.trends[0].source, "tavily");
        assert.equal(loaded.trends[0].summary, "A regulatory sandbox for public bodies.");
      } finally {
        rmSync(dir, { recursive: true, force: true });
      }
    });
  });

  describe("addTrend", () => {
    test("adds a new trend with a computed id and reports added:true", () => {
      const res = addTrend(emptyStore(), {
        title: "Agentic workflows hit production",
        url: "https://example.com/agentic",
        source: "websearch",
        capturedAt: "2026-06-20",
        topics: ["agents", "engineering"],
      });
      assert.equal(res.added, true);
      assert.equal(res.merged, false);
      assert.equal(res.store.trends.length, 1);
      assert.equal(res.store.trends[0].id, trendId(res.store.trends[0].title, res.store.trends[0].url));
    });

    test("preserves title/url/summary verbatim (normalizes only for the id)", () => {
      const title = "  Mixed   CASE Headline  ";
      const url = "  https://EXAMPLE.com/Path  ";
      const summary = "  A   summary with   spacing.  ";
      const res = addTrend(emptyStore(), {
        title,
        url,
        source: "manual",
        capturedAt: "2026-06-20",
        topics: ["x"],
        summary,
      });
      assert.equal(res.store.trends[0].title, title, "title must not be mutated");
      assert.equal(res.store.trends[0].url, url, "url must not be mutated");
      assert.equal(res.store.trends[0].summary, summary, "summary must not be mutated");
    });

    test("omitting summary stores no summary key", () => {
      const res = addTrend(emptyStore(), {
        title: "No summary here",
        url: "https://example.com/n",
        source: "manual",
        capturedAt: "2026-06-20",
        topics: ["x"],
      });
      assert.equal("summary" in res.store.trends[0], false);
    });

    test("dedupes on normalized title+url: same trend twice → one entry, topics unioned", () => {
      let store = emptyStore();
      store = addTrend(store, {
        title: "Reasoning models reshape RAG",
        url: "https://example.com/rag",
        source: "tavily",
        capturedAt: "2026-06-19",
        topics: ["rag"],
      }).store;
      const res2 = addTrend(store, {
        title: "  reasoning   MODELS reshape rag ",
        url: " HTTPS://EXAMPLE.COM/rag ",
        source: "gemini",
        capturedAt: "2026-06-20",
        topics: ["search", "rag"],
      });
      assert.equal(res2.added, false, "duplicate title+url must not add a second entry");
      assert.equal(res2.merged, true, "new topics on a duplicate must report merged:true");
      assert.equal(res2.store.trends.length, 1);
      assert.deepEqual(
        [...res2.store.trends[0].topics].sort(),
        ["rag", "search"],
        "topics must be unioned on re-capture",
      );
    });

    test("a duplicate keeps the first sighting's source + capturedAt (provenance of first sight)", () => {
      let store = emptyStore();
      store = addTrend(store, {
        title: "Same trend",
        url: "https://example.com/s",
        source: "first-source",
        capturedAt: "2026-06-01",
        topics: ["a"],
      }).store;
      store = addTrend(store, {
        title: "Same trend",
        url: "https://example.com/s",
        source: "second-source",
        capturedAt: "2026-06-20",
        topics: ["b"],
      }).store;
      assert.equal(store.trends[0].source, "first-source");
      assert.equal(store.trends[0].capturedAt, "2026-06-01");
    });

    test("a duplicate with no new topics reports merged:false", () => {
      let store = emptyStore();
      store = addTrend(store, {
        title: "Stable trend",
        url: "https://example.com/st",
        source: "tavily",
        capturedAt: "2026-06-01",
        topics: ["a", "b"],
      }).store;
      const res2 = addTrend(store, {
        title: "Stable trend",
        url: "https://example.com/st",
        source: "tavily",
        capturedAt: "2026-06-02",
        topics: ["b", "a"],
      });
      assert.equal(res2.added, false);
      assert.equal(res2.merged, false, "no new tags → merged:false");
    });
  });

  describe("queryByTopic", () => {
    const seed = (): TrendStore => {
      let store = emptyStore();
      store = addTrend(store, {
        title: "Trend A",
        url: "https://example.com/a",
        source: "tavily",
        capturedAt: "2026-05-01",
        topics: ["agents", "engineering"],
      }).store;
      store = addTrend(store, {
        title: "Trend B",
        url: "https://example.com/b",
        source: "tavily",
        capturedAt: "2026-06-01",
        topics: ["agents", "engineering", "public-sector"],
      }).store;
      store = addTrend(store, {
        title: "Trend C",
        url: "https://example.com/c",
        source: "manual",
        capturedAt: "2026-06-10",
        topics: ["privacy"],
      }).store;
      return store;
    };

    test("ranks hits by topic overlap (desc)", () => {
      // B carries both queried topics; A carries only "agents" → distinct overlaps.
      const hits = queryByTopic(seed(), ["agents", "public-sector"]);
      assert.equal(hits.length, 2);
      assert.equal(hits[0].trend.title, "Trend B"); // overlap 2
      assert.equal(hits[0].topicOverlap, 2);
      assert.equal(hits[1].trend.title, "Trend A"); // overlap 1 (agents only)
      assert.equal(hits[1].topicOverlap, 1);
    });

    test("matching is case-insensitive on topics", () => {
      const hits = queryByTopic(seed(), ["AGENTS"]);
      assert.equal(hits.length, 2);
    });

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

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

  describe("history", () => {
    const seed = (): TrendStore => {
      let store = emptyStore();
      for (const [title, capturedAt] of [
        ["Oldest", "2026-04-01"],
        ["Middle", "2026-05-01"],
        ["Newest", "2026-06-01"],
      ] as const) {
        store = addTrend(store, {
          title,
          url: `https://example.com/${title}`,
          source: "tavily",
          capturedAt,
          topics: ["x"],
        }).store;
      }
      return store;
    };

    test("returns all trends newest-first", () => {
      const h = history(seed());
      assert.deepEqual(h.map((t) => t.title), ["Newest", "Middle", "Oldest"]);
    });

    test("respects an inclusive `since` filter", () => {
      const h = history(seed(), { since: "2026-05-01" });
      assert.deepEqual(h.map((t) => t.title), ["Newest", "Middle"]);
    });

    test("respects `limit`", () => {
      const h = history(seed(), { limit: 1 });
      assert.deepEqual(h.map((t) => t.title), ["Newest"]);
    });
  });
});
