import { describe, expect, test } from "bun:test";
import { mkdtempSync, readFileSync, appendFileSync } from "node:fs";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { Journal } from "../src/journal";
import type { Item } from "../src/schema";

function tmpPath(): string {
  const dir = mkdtempSync(join(tmpdir(), "overdeck-journal-"));
  return join(dir, "items.jsonl");
}

// ts is fixed: append() diffs serialized content, so a wall-clock ts would make
// two makeItem(id) calls differ whenever they straddle a millisecond boundary.
function makeItem(id: string, ts = "2026-07-17T00:00:00.000Z"): Item {
  return {
    id,
    source: "test",
    severity: "info",
    kind: "progress",
    title: "t",
    detail: "d",
    ts,
    actions: [],
  };
}

describe("journal restart-safe dedupe", () => {
  test("append writes a line, load replays it", () => {
    const path = tmpPath();
    const journal = new Journal(path);
    expect(journal.append(makeItem("a"))).toBe(true);

    const reloaded = new Journal(path);
    const items = reloaded.load();
    expect(items).toHaveLength(1);
    expect(items[0]?.id).toBe("a");
  });

  test("appending same id twice within a process is a no-op the second time", () => {
    const path = tmpPath();
    const journal = new Journal(path);
    expect(journal.append(makeItem("a"))).toBe(true);
    expect(journal.append(makeItem("a"))).toBe(false);

    const raw = readFileSync(path, "utf8").trim().split("\n");
    expect(raw).toHaveLength(1);
  });

  test("dedupe survives restart: unchanged item journaled before restart is not re-appended", () => {
    const path = tmpPath();
    const first = new Journal(path);
    first.append(makeItem("a"));

    const second = new Journal(path);
    second.load(); // simulate restart replay, seeds seenIds
    expect(second.append(makeItem("a"))).toBe(false);

    const raw = readFileSync(path, "utf8").trim().split("\n");
    expect(raw).toHaveLength(1);
  });

  test("load with no existing file returns empty array", () => {
    const dir = mkdtempSync(join(tmpdir(), "overdeck-journal-empty-"));
    const journal = new Journal(join(dir, "items.jsonl"));
    expect(journal.load()).toEqual([]);
  });
});

describe("journal event log: changed content, tombstones, replay order", () => {
  test("appending an id with changed content re-persists (does not no-op)", () => {
    const path = tmpPath();
    const journal = new Journal(path);
    const v1 = makeItem("a");
    const v2: Item = { ...v1, detail: "updated detail" };

    expect(journal.append(v1)).toBe(true);
    expect(journal.append(v2)).toBe(true);

    const raw = readFileSync(path, "utf8").trim().split("\n");
    expect(raw).toHaveLength(2);

    const reloaded = new Journal(path);
    const items = reloaded.load();
    expect(items).toHaveLength(1);
    expect(items[0]?.detail).toBe("updated detail");
  });

  test("tombstone replay: a resolved item does not resurrect after restart", () => {
    const path = tmpPath();
    const journal = new Journal(path);
    journal.append(makeItem("a"));
    journal.resolve("a", new Date().toISOString());

    const reloaded = new Journal(path);
    const items = reloaded.load();
    expect(items.find((i) => i.id === "a")).toBeUndefined();
  });

  test("interleaved upsert/tombstone order is respected on replay (genuinely last-write-wins)", () => {
    const path = tmpPath();
    const journal = new Journal(path);

    journal.append(makeItem("a")); // upsert a
    journal.resolve("a", new Date().toISOString()); // tombstone a
    const revived: Item = { ...makeItem("a"), detail: "revived" };
    journal.append(revived); // upsert a again, after the tombstone

    const reloaded = new Journal(path);
    const items = reloaded.load();
    const a = items.find((i) => i.id === "a");
    expect(a).toBeDefined();
    expect(a?.detail).toBe("revived");
  });

  test("a tombstone is not silently dropped as a malformed line", () => {
    const path = tmpPath();
    const journal = new Journal(path);
    journal.append(makeItem("b"));
    journal.resolve("b", new Date().toISOString());

    const raw = readFileSync(path, "utf8").trim().split("\n");
    expect(raw).toHaveLength(2);
    expect(JSON.parse(raw[1] ?? "")).toMatchObject({ resolved: true, id: "b" });

    const reloaded = new Journal(path);
    expect(reloaded.load()).toEqual([]);
  });

  test("malformed lines are skipped without throwing", () => {
    const path = tmpPath();
    const journal = new Journal(path);
    journal.append(makeItem("a"));
    appendFileSync(path, "not valid json\n");

    const reloaded = new Journal(path);
    expect(() => reloaded.load()).not.toThrow();
    expect(reloaded.load().map((i) => i.id)).toEqual(["a"]);
  });
});
