// Unit tests for the /tools/verfahrensablauf URL + scenario-localStorage
// state contract (t-paliad-308 / m/paliad#137). Run with `bun test`.
//
// The contract:
//   1. URL params (proceeding, side, target, trigger_date) define which
//      timeline kind the user is looking at — paste-able, shareable,
//      refresh-resistant.
//   2. localStorage (paliad.verfahrensablauf.scenario.*) holds the
//      per-user scenario tweaks (event_choices, court_id, flags,
//      show_hidden) — these never leak into a shared link.
//   3. On hydrate, URL wins. localStorage fills the rest.

import { describe, expect, test } from "bun:test";
import {
  APPEAL_TARGETS,
  SCENARIO_KEYS,
  SCENARIO_PREFIX,
  URL_KEYS,
  applyFiltersToSearch,
  hydrate,
  makeMemoryStorage,
  parseAppealTargetFromSearch,
  parseProceedingFromSearch,
  parseSideFromSearch,
  parseTriggerDateFromSearch,
  readBoolFlag,
  readCourtId,
  readEventChoices,
  readScenario,
  writeBoolFlag,
  writeCourtId,
  writeEventChoices,
} from "./verfahrensablauf-state";

describe("URL parsers — filter chips", () => {
  test("parseProceedingFromSearch returns empty string when absent", () => {
    expect(parseProceedingFromSearch("")).toBe("");
    expect(parseProceedingFromSearch("?side=claimant")).toBe("");
  });

  test("parseProceedingFromSearch echoes the raw value", () => {
    expect(parseProceedingFromSearch("?proceeding=upc.inf.cfi")).toBe("upc.inf.cfi");
    expect(parseProceedingFromSearch("?proceeding=upc.apl.unified&side=claimant")).toBe("upc.apl.unified");
  });

  test("parseSideFromSearch validates the enum", () => {
    expect(parseSideFromSearch("?side=claimant")).toBe("claimant");
    expect(parseSideFromSearch("?side=defendant")).toBe("defendant");
    expect(parseSideFromSearch("?side=neither")).toBe(null);
    expect(parseSideFromSearch("")).toBe(null);
  });

  test("parseAppealTargetFromSearch only accepts canonical slugs", () => {
    for (const t of APPEAL_TARGETS) {
      expect(parseAppealTargetFromSearch(`?target=${t}`)).toBe(t);
    }
    expect(parseAppealTargetFromSearch("?target=unknown")).toBe("");
    expect(parseAppealTargetFromSearch("")).toBe("");
  });

  test("parseTriggerDateFromSearch validates the ISO-date shape", () => {
    expect(parseTriggerDateFromSearch("?trigger_date=2026-05-26")).toBe("2026-05-26");
    expect(parseTriggerDateFromSearch("?trigger_date=2024-02-29")).toBe("2024-02-29"); // leap year
  });

  test("parseTriggerDateFromSearch rejects malformed and impossible dates", () => {
    expect(parseTriggerDateFromSearch("?trigger_date=2026-02-30")).toBe(""); // Feb 30
    expect(parseTriggerDateFromSearch("?trigger_date=2026-13-01")).toBe(""); // month 13
    expect(parseTriggerDateFromSearch("?trigger_date=tomorrow")).toBe("");
    expect(parseTriggerDateFromSearch("?trigger_date=2026-5-26")).toBe(""); // 1-digit month
    expect(parseTriggerDateFromSearch("")).toBe("");
  });
});

describe("URL encoder — applyFiltersToSearch", () => {
  test("empty filters preserve the existing query string", () => {
    expect(applyFiltersToSearch("?other=keep", {})).toBe("?other=keep");
  });

  test("setting a filter writes the canonical key", () => {
    expect(applyFiltersToSearch("", { proceeding: "upc.inf.cfi" })).toBe("?proceeding=upc.inf.cfi");
    expect(applyFiltersToSearch("", { side: "claimant" })).toBe("?side=claimant");
    expect(applyFiltersToSearch("", { target: "endentscheidung" })).toBe("?target=endentscheidung");
    expect(applyFiltersToSearch("", { triggerDate: "2026-05-26" })).toBe("?trigger_date=2026-05-26");
  });

  test("setting null / empty / undefined deletes the key", () => {
    expect(applyFiltersToSearch("?side=claimant", { side: null })).toBe("");
    expect(applyFiltersToSearch("?proceeding=upc.inf.cfi", { proceeding: "" })).toBe("");
    expect(applyFiltersToSearch("?target=endentscheidung", { target: "" })).toBe("");
    expect(applyFiltersToSearch("?trigger_date=2026-05-26", { triggerDate: "" })).toBe("");
  });

  test("invalid trigger_date is deleted (never written as-is)", () => {
    expect(applyFiltersToSearch("?trigger_date=2026-05-26", { triggerDate: "bogus" })).toBe("");
  });

  test("setting all four filters together emits all four keys", () => {
    const out = applyFiltersToSearch("", {
      proceeding: "upc.apl.unified",
      side: "defendant",
      target: "endentscheidung",
      triggerDate: "2026-05-26",
    });
    expect(out).toContain("proceeding=upc.apl.unified");
    expect(out).toContain("side=defendant");
    expect(out).toContain("target=endentscheidung");
    expect(out).toContain("trigger_date=2026-05-26");
  });

  test("other params (project, view) are preserved", () => {
    const out = applyFiltersToSearch("?project=abc&view=timeline", { side: "claimant" });
    expect(out).toContain("project=abc");
    expect(out).toContain("view=timeline");
    expect(out).toContain("side=claimant");
  });

  test("absent keys in the filter object don't touch existing URL values", () => {
    // Only updating side — proceeding should be untouched.
    const out = applyFiltersToSearch("?proceeding=upc.inf.cfi&side=defendant", { side: "claimant" });
    expect(out).toContain("proceeding=upc.inf.cfi");
    expect(out).toContain("side=claimant");
  });
});

describe("URL round-trip — encode then parse yields the same value", () => {
  test("proceeding", () => {
    const enc = applyFiltersToSearch("", { proceeding: "upc.inf.cfi" });
    expect(parseProceedingFromSearch(enc)).toBe("upc.inf.cfi");
  });

  test("side", () => {
    const enc = applyFiltersToSearch("", { side: "defendant" });
    expect(parseSideFromSearch(enc)).toBe("defendant");
  });

  test("target", () => {
    const enc = applyFiltersToSearch("", { target: "kostenentscheidung" });
    expect(parseAppealTargetFromSearch(enc)).toBe("kostenentscheidung");
  });

  test("trigger_date", () => {
    const enc = applyFiltersToSearch("", { triggerDate: "2026-05-26" });
    expect(parseTriggerDateFromSearch(enc)).toBe("2026-05-26");
  });
});

describe("Scenario localStorage helpers", () => {
  test("SCENARIO_PREFIX is paliad.verfahrensablauf.scenario and all keys live under it", () => {
    expect(SCENARIO_PREFIX).toBe("paliad.verfahrensablauf.scenario");
    for (const key of Object.values(SCENARIO_KEYS)) {
      expect(key.startsWith(SCENARIO_PREFIX + ".")).toBe(true);
    }
  });

  test("readEventChoices returns [] on empty storage", () => {
    const s = makeMemoryStorage();
    expect(readEventChoices(s)).toEqual([]);
  });

  test("writeEventChoices + readEventChoices round-trip", () => {
    const s = makeMemoryStorage();
    const choices = [
      { submission_code: "upc.inf.cfi.r12", choice_kind: "appellant" as const, choice_value: "claimant" },
      { submission_code: "upc.inf.cfi.r30", choice_kind: "include_ccr" as const, choice_value: "1" },
    ];
    writeEventChoices(s, choices);
    expect(readEventChoices(s)).toEqual(choices);
  });

  test("writeEventChoices([]) clears the key (removeItem semantic, not empty string)", () => {
    const s = makeMemoryStorage();
    writeEventChoices(s, [{ submission_code: "r1", choice_kind: "skip", choice_value: "1" }]);
    expect(s.getItem(SCENARIO_KEYS.eventChoices)).not.toBe(null);
    writeEventChoices(s, []);
    expect(s.getItem(SCENARIO_KEYS.eventChoices)).toBe(null);
  });

  test("readEventChoices ignores unknown choice_kind values", () => {
    const s = makeMemoryStorage();
    s.setItem(SCENARIO_KEYS.eventChoices, "r1:appellant=claimant,r2:bogus=x,r3:skip=1");
    expect(readEventChoices(s)).toEqual([
      { submission_code: "r1", choice_kind: "appellant", choice_value: "claimant" },
      { submission_code: "r3", choice_kind: "skip", choice_value: "1" },
    ]);
  });

  test("readCourtId returns '' on empty storage, echoes stored value otherwise", () => {
    const s = makeMemoryStorage();
    expect(readCourtId(s)).toBe("");
    writeCourtId(s, "UPC-LD-MUC");
    expect(readCourtId(s)).toBe("UPC-LD-MUC");
  });

  test("writeCourtId('') removes the key", () => {
    const s = makeMemoryStorage();
    writeCourtId(s, "UPC-LD-MUC");
    expect(s.getItem(SCENARIO_KEYS.courtId)).toBe("UPC-LD-MUC");
    writeCourtId(s, "");
    expect(s.getItem(SCENARIO_KEYS.courtId)).toBe(null);
  });

  test("readBoolFlag / writeBoolFlag round-trip with removeItem on false", () => {
    const s = makeMemoryStorage();
    expect(readBoolFlag(s, SCENARIO_KEYS.ccr)).toBe(false);
    writeBoolFlag(s, SCENARIO_KEYS.ccr, true);
    expect(readBoolFlag(s, SCENARIO_KEYS.ccr)).toBe(true);
    expect(s.getItem(SCENARIO_KEYS.ccr)).toBe("1");
    writeBoolFlag(s, SCENARIO_KEYS.ccr, false);
    expect(readBoolFlag(s, SCENARIO_KEYS.ccr)).toBe(false);
    expect(s.getItem(SCENARIO_KEYS.ccr)).toBe(null);
  });

  test("readScenario returns all fields defaulted on empty storage", () => {
    const s = makeMemoryStorage();
    expect(readScenario(s)).toEqual({
      eventChoices: [],
      courtId: "",
      ccr: false,
      infAmend: false,
      revAmend: false,
      revCci: false,
      showHidden: false,
    });
  });
});

describe("Hydration order — URL wins, localStorage fills the rest", () => {
  test("URL fills filter chips, localStorage fills scenario state", () => {
    const s = makeMemoryStorage();
    writeCourtId(s, "UPC-LD-MUC");
    writeBoolFlag(s, SCENARIO_KEYS.showHidden, true);
    writeBoolFlag(s, SCENARIO_KEYS.ccr, true);
    const out = hydrate(
      "?proceeding=upc.inf.cfi&side=defendant&target=endentscheidung&trigger_date=2026-05-26",
      s,
    );
    // URL-sourced
    expect(out.proceeding).toBe("upc.inf.cfi");
    expect(out.side).toBe("defendant");
    expect(out.target).toBe("endentscheidung");
    expect(out.triggerDate).toBe("2026-05-26");
    // localStorage-sourced
    expect(out.courtId).toBe("UPC-LD-MUC");
    expect(out.showHidden).toBe(true);
    expect(out.ccr).toBe(true);
  });

  test("absent URL → all filter fields are empty/null, localStorage still hydrates scenario", () => {
    const s = makeMemoryStorage();
    writeCourtId(s, "UPC-LD-MUC");
    const out = hydrate("", s);
    expect(out.proceeding).toBe("");
    expect(out.side).toBe(null);
    expect(out.target).toBe("");
    expect(out.triggerDate).toBe("");
    expect(out.courtId).toBe("UPC-LD-MUC");
  });

  test("absent localStorage → URL still fills filter chips, scenario defaults", () => {
    const s = makeMemoryStorage();
    const out = hydrate(
      "?proceeding=upc.apl.unified&side=claimant&target=anordnung&trigger_date=2026-07-01",
      s,
    );
    expect(out.proceeding).toBe("upc.apl.unified");
    expect(out.side).toBe("claimant");
    expect(out.target).toBe("anordnung");
    expect(out.triggerDate).toBe("2026-07-01");
    expect(out.courtId).toBe("");
    expect(out.eventChoices).toEqual([]);
    expect(out.showHidden).toBe(false);
  });

  test("a shared link doesn't leak the recipient's scenario state in", () => {
    // Two storages: m's (loaded with court + flags) and a recipient's
    // (empty). The same URL should reproduce filter chips identically
    // but leave each user's scenario state untouched.
    const mStorage = makeMemoryStorage();
    writeCourtId(mStorage, "UPC-LD-MUC");
    writeBoolFlag(mStorage, SCENARIO_KEYS.ccr, true);
    const recipientStorage = makeMemoryStorage();

    const sharedURL = "?proceeding=upc.inf.cfi&side=defendant&trigger_date=2026-05-26";

    const mView = hydrate(sharedURL, mStorage);
    const recipientView = hydrate(sharedURL, recipientStorage);

    // Filter chips identical
    expect(mView.proceeding).toBe(recipientView.proceeding);
    expect(mView.side).toBe(recipientView.side);
    expect(mView.triggerDate).toBe(recipientView.triggerDate);

    // Scenario state diverges — recipient sees defaults
    expect(mView.courtId).toBe("UPC-LD-MUC");
    expect(recipientView.courtId).toBe("");
    expect(mView.ccr).toBe(true);
    expect(recipientView.ccr).toBe(false);
  });
});

describe("URL key constants match the documented contract", () => {
  test("URL_KEYS uses the spec'd snake_case names", () => {
    expect(URL_KEYS.proceeding).toBe("proceeding");
    expect(URL_KEYS.side).toBe("side");
    expect(URL_KEYS.target).toBe("target");
    expect(URL_KEYS.triggerDate).toBe("trigger_date");
  });
});
