import { describe, expect, test } from "bun:test";
import {
  type CalculatedDeadline,
  bucketDeadlinesIntoColumns,
  deadlineCardHtml,
} from "./verfahrensablauf-core";

// Regression tests for the editable→click-to-edit wiring on timeline date
// cells (m/paliad#59). When CardOpts.editable=true the card renderer must
// emit `class="… frist-date-edit"` with `data-rule-code` + `data-current-
// date` on the date span. Pages then attach a delegated click handler that
// resolves that selector to swap in an inline `<input type="date">`. If a
// future refactor drops the attrs, /tools/verfahrensablauf and
// /tools/fristenrechner both silently lose click-to-edit (no script error,
// nothing happens on click). These tests pin the contract.
//
// Fixture leaves ruleRef/legalSource* empty so deadlineCardHtml stays
// inside its non-DOM code paths (escHtml is DOM-backed and bun test runs
// in plain Node without jsdom).

const dl = (overrides: Partial<CalculatedDeadline> = {}): CalculatedDeadline => ({
  code: "upc-rop-12",
  name: "Klageerwiderung",
  nameEN: "Statement of Defence",
  party: "defendant",
  priority: "mandatory",
  ruleRef: "",
  dueDate: "2026-07-15",
  originalDate: "2026-07-15",
  wasAdjusted: false,
  isRootEvent: false,
  isCourtSet: false,
  ...overrides,
});

describe("deadlineCardHtml — editable=true emits click-to-edit attrs", () => {
  test("date span carries frist-date-edit class + data-rule-code + data-current-date", () => {
    const html = deadlineCardHtml(dl(), { showParty: true, editable: true });
    expect(html).toContain('class="timeline-date frist-date-edit"');
    expect(html).toContain('data-rule-code="upc-rop-12"');
    expect(html).toContain('data-current-date="2026-07-15"');
    expect(html).toContain('role="button"');
    expect(html).toContain('tabindex="0"');
  });

  test("editable=false (default) emits the date span without click-to-edit attrs", () => {
    const html = deadlineCardHtml(dl(), { showParty: true });
    expect(html).toContain("timeline-date");
    expect(html).not.toContain("data-rule-code=");
    expect(html).not.toContain('role="button"');
  });

  test("root event suppresses editable even when editable=true (root has no override semantic)", () => {
    const html = deadlineCardHtml(dl({ isRootEvent: true }), { showParty: true, editable: true });
    expect(html).not.toContain("data-rule-code=");
  });

  test("isCourtSet renders the court-set placeholder with click-to-edit so users can pin a real date", () => {
    const html = deadlineCardHtml(dl({ isCourtSet: true }), { showParty: true, editable: true });
    expect(html).toContain("timeline-court-set frist-date-edit");
    expect(html).toContain('data-rule-code="upc-rop-12"');
  });

  test("empty rule code with editable=true still suppresses click-to-edit (no anchor target)", () => {
    const html = deadlineCardHtml(dl({ code: "" }), { showParty: true, editable: true });
    expect(html).not.toContain("data-rule-code=");
  });
});

// Pure column-routing behaviour. Originally pinned by m/paliad#81
// (side + appellant axes), re-framed by m/paliad#88: the column
// axis is now "Unsere Seite vs Gegnerseite" ("WE always on the
// left") instead of the misleading Proaktiv/Reaktiv pair.
// Hits bucketDeadlinesIntoColumns directly so the assertions stay
// in pure-Node territory (renderColumnsBody goes through escHtml ->
// document.createElement which isn't available in plain bun test).
//
// Scenario fixture mirrors the UPC Appeal "both parties" case m
// pasted into #81: every filing rule carries party='both' so the
// legacy mirror path duplicates every row across both columns.
// With ?appellant= set, the duplicate must collapse to a single
// row in the appellant's column.
describe("bucketDeadlinesIntoColumns — side+appellant column routing (m/paliad#81, #88)", () => {
  const both = (name: string, due: string): CalculatedDeadline => ({
    code: name,
    name,
    nameEN: name,
    party: "both",
    priority: "mandatory",
    ruleRef: "",
    dueDate: due,
    originalDate: due,
    wasAdjusted: false,
    isRootEvent: false,
    isCourtSet: false,
  });
  const partySpecific = (party: string, name: string, due: string): CalculatedDeadline => ({
    ...both(name, due),
    party,
  });

  test("default (no opts) mirrors 'both' rules into ours AND opponent — legacy behaviour preserved", () => {
    const rows = bucketDeadlinesIntoColumns([both("Notice of Appeal", "2026-07-23")]);
    expect(rows).toHaveLength(1);
    expect(rows[0].ours.map((d) => d.name)).toEqual(["Notice of Appeal"]);
    expect(rows[0].opponent.map((d) => d.name)).toEqual(["Notice of Appeal"]);
    expect(rows[0].court).toHaveLength(0);
  });

  test("default (no side) places claimant on the left (ours) — 'we are claimant' fallback", () => {
    const rows = bucketDeadlinesIntoColumns([
      partySpecific("claimant", "Klageschrift", "2026-01-01"),
      partySpecific("defendant", "Klageerwiderung", "2026-04-01"),
    ]);
    expect(rows[0].ours.map((d) => d.name)).toEqual(["Klageschrift"]);
    expect(rows[1].opponent.map((d) => d.name)).toEqual(["Klageerwiderung"]);
  });

  test("appellant=claimant collapses 'both' rules into ours when side=claimant (or default)", () => {
    const rows = bucketDeadlinesIntoColumns(
      [both("Notice of Appeal", "2026-07-23"), both("Statement of Grounds", "2026-09-23")],
      { appellant: "claimant" },
    );
    expect(rows.map((r) => r.ours.map((d) => d.name))).toEqual([
      ["Notice of Appeal"],
      ["Statement of Grounds"],
    ]);
    rows.forEach((r) => expect(r.opponent).toHaveLength(0));
  });

  test("appellant=defendant collapses 'both' rules into opponent when side=null/claimant", () => {
    const rows = bucketDeadlinesIntoColumns(
      [both("Notice of Appeal", "2026-07-23")],
      { appellant: "defendant" },
    );
    expect(rows[0].ours).toHaveLength(0);
    expect(rows[0].opponent.map((d) => d.name)).toEqual(["Notice of Appeal"]);
  });

  test("side=defendant flips which party owns 'ours' vs 'opponent' — WE always on the left", () => {
    // User is on the defendant side: defendant filings land in 'ours'
    // (left), claimant filings land in 'opponent' (right). Court rules
    // stay in court regardless of side.
    const rows = bucketDeadlinesIntoColumns(
      [
        partySpecific("claimant", "Klageschrift", "2026-01-01"),
        partySpecific("defendant", "Klageerwiderung", "2026-04-01"),
        partySpecific("court", "Urteil", "2026-10-01"),
      ],
      { side: "defendant" },
    );
    expect(rows[0].opponent.map((d) => d.name)).toEqual(["Klageschrift"]);
    expect(rows[1].ours.map((d) => d.name)).toEqual(["Klageerwiderung"]);
    expect(rows[2].court.map((d) => d.name)).toEqual(["Urteil"]);
  });

  test("side=defendant + appellant=defendant routes 'both' into 'ours' (user's own column)", () => {
    // The user is the defendant AND the appellant, so the appellant's
    // column == the user's own column == ours after the swap.
    const rows = bucketDeadlinesIntoColumns(
      [both("Notice of Appeal", "2026-07-23")],
      { side: "defendant", appellant: "defendant" },
    );
    expect(rows[0].ours.map((d) => d.name)).toEqual(["Notice of Appeal"]);
    expect(rows[0].opponent).toHaveLength(0);
  });

  test("side=defendant + appellant=claimant routes 'both' into opponent (claimant ≠ us)", () => {
    // Side flip + appellant axis combined: the claimant is the appellant
    // but NOT us, so the collapsed 'both' row lands in the opponent
    // column (right). This is the UPC Appeal "they appealed, we
    // respond" scenario.
    const rows = bucketDeadlinesIntoColumns(
      [both("Notice of Appeal", "2026-07-23")],
      { side: "defendant", appellant: "claimant" },
    );
    expect(rows[0].opponent.map((d) => d.name)).toEqual(["Notice of Appeal"]);
    expect(rows[0].ours).toHaveLength(0);
  });

  test("rows align across columns by dueDate so same-day events stay on one grid row", () => {
    const sameDate = "2026-07-23";
    const rows = bucketDeadlinesIntoColumns([
      partySpecific("claimant", "A", sameDate),
      partySpecific("defendant", "B", sameDate),
      partySpecific("court", "C", sameDate),
    ]);
    expect(rows).toHaveLength(1);
    expect(rows[0].ours.map((d) => d.name)).toEqual(["A"]);
    expect(rows[0].opponent.map((d) => d.name)).toEqual(["B"]);
    expect(rows[0].court.map((d) => d.name)).toEqual(["C"]);
  });

  test("unscheduled rows (no dueDate) trail dated rows, preserving declaration order", () => {
    const rows = bucketDeadlinesIntoColumns([
      partySpecific("court", "Oral Hearing", ""),
      partySpecific("claimant", "Statement of Claim", "2026-01-01"),
      partySpecific("court", "Decision", ""),
    ]);
    expect(rows.map((r) => [r.ours, r.court, r.opponent].flat().map((d) => d.name))).toEqual([
      ["Statement of Claim"],
      ["Oral Hearing"],
      ["Decision"],
    ]);
  });
});
