import { test } from "node:test";
import assert from "node:assert/strict";
import {
  DocumentCache,
  IncludeResolveCache,
  IndexRecordsCache,
  InvalidationsEpoch,
} from "../out/indexer/caches.js";
import { resolve } from "node:path";

const stat = { mtimeMs: 1, size: 1, birthtimeMs: 1, ctimeMs: 1 };

function parsed(path, elements) {
  return {
    file: { path, stat },
    parse: { root: { name: "r" }, elements: new Array(elements), errors: [] },
    records: null,
    lineMap: null,
  };
}

test("DocumentCache evicts the largest tree when over the element budget", () => {
  const cache = new DocumentCache(64, 100);
  cache.set(parsed("a.xml", 60));
  cache.set(parsed("b.xml", 60));
  assert.equal(cache.get("a.xml"), undefined, "largest tree evicted first");
  assert.ok(cache.get("b.xml"));
});

test("DocumentCache evicts least recently used when over capacity", () => {
  const cache = new DocumentCache(2, 1_000_000);
  cache.set(parsed("a.xml", 10));
  cache.set(parsed("b.xml", 10));
  cache.set(parsed("c.xml", 10));
  assert.equal(cache.get("a.xml"), undefined);
  assert.ok(cache.get("b.xml"));
  assert.ok(cache.get("c.xml"));
});

test("DocumentCache invalidate frees budget", () => {
  const cache = new DocumentCache(64, 100);
  cache.set(parsed("a.xml", 60));
  cache.invalidate("a.xml");
  cache.set(parsed("b.xml", 60));
  assert.ok(cache.get("b.xml"), "invalidating a freed its budget share");
  cache.set(parsed("c.xml", 60));
  assert.equal(cache.get("a.xml"), undefined);
  assert.ok(cache.get("c.xml"));
});

test("IndexRecordsCache stores and invalidates entries", () => {
  const cache = new IndexRecordsCache();
  const entry = {
    stat,
    records: { assets: [], defines: [], includes: [], rootXiIncludes: [], nestedXiIncludes: [], references: [] },
    kind: "full",
  };
  cache.set("a.xml", entry);
  assert.equal(cache.get("a.xml"), entry);
  cache.invalidate("a.xml");
  assert.equal(cache.get("a.xml"), undefined);
});

test("IndexRecordsCache exposes entries for disk persistence", () => {
  const cache = new IndexRecordsCache();
  const entry = {
    stat,
    records: { assets: [], defines: [], includes: [], rootXiIncludes: [], nestedXiIncludes: [], references: [] },
    kind: "full",
  };
  cache.set("a.xml", entry);
  const list = [...cache.entries()];
  assert.equal(list.length, 1);
  assert.equal(list[0][0], resolve("a.xml").toLowerCase());
  assert.equal(list[0][1], entry);
});

test("IncludeResolveCache stores sources and manifest lookups", () => {
  const cache = new IncludeResolveCache();
  const key = "dir|DATA:static.xml";
  const result = { path: "C:/sdk/static.xml", prefix: "DATA", raw: "DATA:static.xml" };
  assert.equal(cache.get(key), undefined);
  cache.set(key, result);
  assert.equal(cache.get(key), result);
  assert.equal(cache.getManifest("static.xml"), undefined);
  cache.setManifest("static.xml", "C:/sdk/builtmods/static.manifest");
  assert.equal(cache.getManifest("static.xml"), "C:/sdk/builtmods/static.manifest");
  cache.clear();
  assert.equal(cache.get(key), undefined);
  assert.equal(cache.getManifest("static.xml"), undefined);
});

test("InvalidationsEpoch tracks changes since a snapshot", () => {
  const epoch = new InvalidationsEpoch();
  const before = epoch.snapshot();
  assert.equal(epoch.changedSince(before), false);
  epoch.mark();
  assert.equal(epoch.changedSince(before), true);
  assert.equal(epoch.changedSince(epoch.snapshot()), false);
  epoch.mark();
  epoch.mark();
  assert.equal(epoch.current, 3);
});
