// Tests der reinen Layout-Logik (DOSSIER A3): Blatt-Geometrie, Fabriken,
// immutables Viewport-CRUD, Master-Vererbung der Titelblock-Werte,
// Viewport→Ausschnitt-Auflösung + Massstab, Sichtbarkeits-Ableitung.

import { describe, expect, it } from "vitest";
import type {
  Layout,
  LayoutFolder,
  MasterLayout,
  Project,
  ViewSnapshot,
} from "../model/types";
import {
  addAnnotation,
  addViewport,
  buildLayoutTree,
  buildMasterTree,
  collectFolderLayouts,
  createAnnotation,
  createLayout,
  createLayoutFolder,
  createMasterLayout,
  createViewport,
  deleteFolder,
  deleteMasterFolder,
  effectiveSheetSizeMm,
  findMaster,
  findSnapshot,
  folderPdfPages,
  isDescendant,
  moveFolderToFolder,
  moveLayoutToFolder,
  PAPER_FORMATS,
  patchAnnotation,
  patchViewport,
  removeAnnotation,
  removeViewport,
  resolveTitleBlock,
  resolveViewportScale,
  sheetSizeMm,
  visibleCodesFromSnapshot,
} from "./layoutModel";

/** Minimaler Ausschnitt (nur die im Layout-System genutzten Felder tragen Werte). */
function snap(id: string, patch?: Partial<ViewSnapshot>): ViewSnapshot {
  return {
    id,
    name: `Snap ${id}`,
    viewType: "grundriss",
    view3d: "top",
    scaleDenominator: 100,
    detail: "mittel",
    activeLevelId: "floor-0",
    layerVisibility: { A: true, B: false, C: true },
    drawingVisibility: {},
    ...patch,
  };
}

/** Minimales Projekt-Gerüst mit optionalen Layout-Feldern. */
function project(patch?: Partial<Project>): Project {
  return {
    id: "p1",
    name: "Villa Test",
    lineStyles: [],
    hatches: [],
    components: [],
    wallTypes: [],
    drawingLevels: [],
    layers: [],
    walls: [],
    doors: [],
    drawings2d: [],
    ...patch,
  };
}

describe("sheetSizeMm", () => {
  it("liefert A4/A3 in Hoch- und Querformat", () => {
    expect(sheetSizeMm("a4", "portrait")).toEqual({ w: 210, h: 297 });
    expect(sheetSizeMm("a4", "landscape")).toEqual({ w: 297, h: 210 });
    expect(sheetSizeMm("a3", "portrait")).toEqual({ w: 297, h: 420 });
    expect(sheetSizeMm("a3", "landscape")).toEqual({ w: 420, h: 297 });
  });
});

describe("Fabriken", () => {
  it("createLayout: leer, ohne Master", () => {
    const l = createLayout("l1", "Blatt 1", "a3", "landscape");
    expect(l).toEqual({
      id: "l1",
      name: "Blatt 1",
      paper: "a3",
      orientation: "landscape",
      viewports: [],
    });
    expect(l.masterId).toBeUndefined();
  });

  it("createMasterLayout: leerer Titelblock + Rahmen an", () => {
    const m = createMasterLayout("m1", "Master", "a4", "portrait");
    expect(m.titleBlock).toEqual({});
    expect(m.border).toBe(true);
  });

  it("createViewport: Defaults + optionaler Massstab", () => {
    const v = createViewport("v1", "s1");
    expect(v).toMatchObject({ id: "v1", snapshotId: "s1", xMm: 20, yMm: 20, widthMm: 120, heightMm: 90 });
    expect(v.scaleDenominator).toBeUndefined();
    const v2 = createViewport("v2", "s1", { xMm: 5, scaleDenominator: 50 });
    expect(v2.xMm).toBe(5);
    expect(v2.scaleDenominator).toBe(50);
  });
});

describe("Viewport-CRUD (immutabel)", () => {
  const base = addViewport(createLayout("l1", "L", "a4", "portrait"), createViewport("v1", "s1"));

  it("addViewport verändert das Original nicht", () => {
    const before = createLayout("l1", "L", "a4", "portrait");
    const after = addViewport(before, createViewport("v1", "s1"));
    expect(before.viewports).toHaveLength(0);
    expect(after.viewports).toHaveLength(1);
    expect(after).not.toBe(before);
  });

  it("removeViewport entfernt per Id", () => {
    const two = addViewport(base, createViewport("v2", "s2"));
    const one = removeViewport(two, "v1");
    expect(one.viewports.map((v) => v.id)).toEqual(["v2"]);
  });

  it("patchViewport ändert nur das Ziel, immutabel", () => {
    const patched = patchViewport(base, "v1", { xMm: 42, scaleDenominator: 50 });
    expect(patched.viewports[0]).toMatchObject({ xMm: 42, scaleDenominator: 50 });
    expect(base.viewports[0].xMm).toBe(20);
    // Unbekannte Id → No-op (gleiche Werte, neue Liste).
    const noop = patchViewport(base, "nope", { xMm: 1 });
    expect(noop.viewports[0].xMm).toBe(20);
  });
});

// ── Annotationen (Linie/Rechteck/Text) ───────────────────────────────────────

describe("createAnnotation — Defaults je Art", () => {
  it("line: Default-Geometrie ohne Farbe/Strichstärke", () => {
    const a = createAnnotation("a1", "line");
    expect(a).toEqual({ id: "a1", kind: "line", x1Mm: 20, y1Mm: 20, x2Mm: 80, y2Mm: 20 });
  });
  it("line: init überschreibt einzelne Felder + optionale Farbe/Strichstärke", () => {
    const a = createAnnotation("a1", "line", { x1Mm: 5, color: "#f00", weightMm: 0.5 });
    expect(a).toMatchObject({ x1Mm: 5, y1Mm: 20, color: "#f00", weightMm: 0.5 });
  });
  it("rect: Default-Grösse", () => {
    const a = createAnnotation("a2", "rect");
    expect(a).toEqual({ id: "a2", kind: "rect", xMm: 20, yMm: 20, widthMm: 40, heightMm: 30 });
  });
  it("text: Default-Zeilenhöhe 3.5mm, leerer Text", () => {
    const a = createAnnotation("a3", "text");
    expect(a).toEqual({ id: "a3", kind: "text", xMm: 20, yMm: 20, text: "", heightMm: 3.5 });
  });
  it("text: init setzt Inhalt + Höhe", () => {
    const a = createAnnotation("a3", "text", { text: "EG", heightMm: 5 });
    expect(a).toMatchObject({ text: "EG", heightMm: 5 });
  });
});

describe("Annotations-CRUD (immutabel)", () => {
  const base = createLayout("l1", "L", "a4", "portrait");

  it("addAnnotation verändert das Original nicht", () => {
    const after = addAnnotation(base, createAnnotation("a1", "line"));
    expect(base.annotations).toBeUndefined();
    expect(after.annotations).toHaveLength(1);
    expect(after).not.toBe(base);
  });

  it("addAnnotation baut auf einer fehlenden `annotations`-Liste auf (Alt-Layout)", () => {
    const legacy: Layout = { id: "old", name: "Alt", paper: "a4", orientation: "portrait", viewports: [] };
    const after = addAnnotation(legacy, createAnnotation("a1", "rect"));
    expect(after.annotations).toHaveLength(1);
    expect(legacy.annotations).toBeUndefined();
  });

  it("removeAnnotation entfernt per Id", () => {
    let l = addAnnotation(base, createAnnotation("a1", "line"));
    l = addAnnotation(l, createAnnotation("a2", "rect"));
    const removed = removeAnnotation(l, "a1");
    expect(removed.annotations!.map((a) => a.id)).toEqual(["a2"]);
  });

  it("removeAnnotation auf Alt-Layout ohne `annotations` bleibt heil (No-op)", () => {
    const legacy: Layout = { id: "old", name: "Alt", paper: "a4", orientation: "portrait", viewports: [] };
    const r = removeAnnotation(legacy, "nope");
    expect(r.annotations).toEqual([]);
  });

  it("patchAnnotation ändert nur das Ziel, immutabel", () => {
    const l = addAnnotation(base, createAnnotation("a1", "line", { color: "#111" }));
    const patched = patchAnnotation(l, "a1", { x1Mm: 42, color: "#0f0" });
    expect(patched.annotations![0]).toMatchObject({ x1Mm: 42, color: "#0f0" });
    expect(l.annotations![0].color).toBe("#111");
  });

  it("patchAnnotation: unbekannte Id → No-op (gleiche Werte, neue Liste)", () => {
    const l = addAnnotation(base, createAnnotation("a1", "line"));
    const noop = patchAnnotation(l, "nope", { x1Mm: 1 });
    expect((noop.annotations![0] as { x1Mm: number }).x1Mm).toBe(20);
  });

  it("patchAnnotation auf einer Text-Annotation ändert den Inhalt", () => {
    const l = addAnnotation(base, createAnnotation("t1", "text", { text: "EG" }));
    const patched = patchAnnotation(l, "t1", { text: "1. OG" });
    expect(patched.annotations![0]).toMatchObject({ text: "1. OG" });
  });
});

describe("resolveViewportScale", () => {
  const s = snap("s1", { scaleDenominator: 200 });
  it("Viewport-Override gewinnt", () => {
    expect(resolveViewportScale(createViewport("v", "s1", { scaleDenominator: 50 }), s)).toBe(50);
  });
  it("sonst Ausschnitt-Massstab", () => {
    expect(resolveViewportScale(createViewport("v", "s1"), s)).toBe(200);
  });
  it("Fallback ohne beides", () => {
    expect(resolveViewportScale(createViewport("v", "s1"), undefined, 100)).toBe(100);
  });
});

describe("findSnapshot / findMaster", () => {
  const p = project({
    viewSnapshots: [snap("s1"), snap("s2")],
    masterLayouts: [createMasterLayout("m1", "M1", "a4", "portrait")],
  });
  it("findSnapshot", () => {
    expect(findSnapshot(p, "s2")?.id).toBe("s2");
    expect(findSnapshot(p, "x")).toBeUndefined();
  });
  it("findMaster nur bei gesetzter masterId", () => {
    const bound: Layout = { ...createLayout("l", "L", "a4", "portrait"), masterId: "m1" };
    expect(findMaster(p, bound)?.id).toBe("m1");
    expect(findMaster(p, createLayout("l2", "L2", "a4", "portrait"))).toBeUndefined();
    // Dangling masterId → undefined.
    expect(findMaster(p, { ...bound, masterId: "gone" })).toBeUndefined();
  });
});

describe("resolveTitleBlock — Master-Vererbung + Defaults", () => {
  const p = project({ name: "Villa Test", viewSnapshots: [snap("s1", { scaleDenominator: 50 })] });

  it("ohne Master: Defaults aus Projekt/Layout + Massstab aus erstem Viewport", () => {
    let l = createLayout("l1", "EG Grundriss", "a4", "portrait");
    l = addViewport(l, createViewport("v1", "s1"));
    const tb = resolveTitleBlock(p, l, undefined, "2026-07-07");
    expect(tb.projectName).toBe("Villa Test");
    expect(tb.sheetName).toBe("EG Grundriss");
    expect(tb.scale).toBe("1:50");
    expect(tb.date).toBe("2026-07-07");
    expect(tb.author).toBe("");
  });

  it("Master-Werte übersteuern die Defaults", () => {
    const master: MasterLayout = {
      id: "m1",
      name: "M",
      paper: "a4",
      orientation: "portrait",
      titleBlock: { projectName: "Kundenprojekt", author: "GV", scale: "1:100" },
    };
    const l = createLayout("l1", "EG", "a4", "portrait");
    const tb = resolveTitleBlock(p, l, master, "2026-07-07");
    expect(tb.projectName).toBe("Kundenprojekt");
    expect(tb.author).toBe("GV");
    expect(tb.scale).toBe("1:100"); // Master-Wert schlägt den Viewport-Default
    expect(tb.sheetName).toBe("EG"); // leer im Master → Layout-Name
  });

  it("leere/whitespace Master-Felder fallen auf Defaults zurück", () => {
    const master: MasterLayout = {
      id: "m1",
      name: "M",
      paper: "a4",
      orientation: "portrait",
      titleBlock: { projectName: "  ", sheetName: "" },
    };
    const l = createLayout("l1", "OG", "a4", "portrait");
    const tb = resolveTitleBlock(p, l, master, "2026-07-07");
    expect(tb.projectName).toBe("Villa Test");
    expect(tb.sheetName).toBe("OG");
    expect(tb.scale).toBe("—"); // kein Viewport → kein Massstab
  });
});

describe("visibleCodesFromSnapshot", () => {
  it("nur sichtbare Codes landen im Set", () => {
    const codes = visibleCodesFromSnapshot(snap("s1"));
    expect([...codes].sort()).toEqual(["A", "C"]);
    expect(codes.has("B")).toBe(false);
  });
});

// ── Freie Grösse (custom übersteuert Papier) ──────────────────────────────────

describe("sheetSizeMm / effectiveSheetSizeMm — freie Grösse", () => {
  it("custom übersteuert paper/orientation, wenn beide positiv gesetzt", () => {
    expect(sheetSizeMm("a4", "portrait", 500, 300)).toEqual({ w: 500, h: 300 });
  });
  it("fällt auf paper zurück, wenn custom fehlt oder nicht positiv ist", () => {
    expect(sheetSizeMm("a4", "portrait", undefined, 300)).toEqual({ w: 210, h: 297 });
    expect(sheetSizeMm("a3", "landscape", 0, 300)).toEqual({ w: 420, h: 297 });
  });
  it("effectiveSheetSizeMm liest die Felder eines Layouts/Masters", () => {
    expect(
      effectiveSheetSizeMm({ paper: "a4", orientation: "portrait" }),
    ).toEqual({ w: 210, h: 297 });
    expect(
      effectiveSheetSizeMm({
        paper: "a4",
        orientation: "portrait",
        customWidthMm: 250,
        customHeightMm: 250,
      }),
    ).toEqual({ w: 250, h: 250 });
  });
});

describe("createMasterLayout — freie Grösse", () => {
  it("übernimmt custom Breite/Höhe, wenn übergeben", () => {
    const m = createMasterLayout("m", "M", "a4", "portrait", {
      widthMm: 400,
      heightMm: 250,
    });
    expect(m.customWidthMm).toBe(400);
    expect(m.customHeightMm).toBe(250);
  });
});

// ── Ordner-Baum (CRUD + Aufbau) ───────────────────────────────────────────────

function lay(id: string, folderId?: string): Layout {
  return {
    id,
    name: id,
    paper: "a4",
    orientation: "portrait",
    viewports: [],
    ...(folderId ? { folderId } : {}),
  };
}

describe("createLayoutFolder", () => {
  it("Wurzelordner ohne parentId", () => {
    expect(createLayoutFolder("f1", "Ordner")).toEqual({ id: "f1", name: "Ordner" });
  });
  it("Unterordner mit parentId", () => {
    expect(createLayoutFolder("f2", "Sub", "f1")).toEqual({
      id: "f2",
      name: "Sub",
      parentId: "f1",
    });
  });
  it("Master-Ordner trägt kind:\"master\"", () => {
    expect(createLayoutFolder("m1", "Master-Ordner", undefined, "master")).toEqual({
      id: "m1",
      name: "Master-Ordner",
      kind: "master",
    });
  });
  it("kind \"layout\"/undefined wird NICHT gesetzt (rückwärtskompatibel)", () => {
    expect(createLayoutFolder("l1", "Layout-Ordner", undefined, "layout")).toEqual({
      id: "l1",
      name: "Layout-Ordner",
    });
    expect(createLayoutFolder("l2", "Layout-Ordner", "p", undefined)).toEqual({
      id: "l2",
      name: "Layout-Ordner",
      parentId: "p",
    });
  });
});

describe("buildLayoutTree", () => {
  it("gruppiert Layouts in Ordner + Wurzel; Unterordner verschachtelt", () => {
    const folders: LayoutFolder[] = [
      createLayoutFolder("A", "A"),
      createLayoutFolder("B", "B", "A"),
    ];
    const layouts = [lay("root1"), lay("inA", "A"), lay("inB", "B")];
    const tree = buildLayoutTree(folders, layouts);
    expect(tree.rootLayouts.map((l) => l.id)).toEqual(["root1"]);
    expect(tree.rootFolders).toHaveLength(1);
    const a = tree.rootFolders[0];
    expect(a.folder.id).toBe("A");
    expect(a.layouts.map((l) => l.id)).toEqual(["inA"]);
    expect(a.subfolders).toHaveLength(1);
    expect(a.subfolders[0].folder.id).toBe("B");
    expect(a.subfolders[0].layouts.map((l) => l.id)).toEqual(["inB"]);
  });

  it("verwaiste Referenzen landen auf der Wurzel", () => {
    const tree = buildLayoutTree(
      [createLayoutFolder("orphan", "O", "missing")],
      [lay("ghost", "gone")],
    );
    // Ordner mit unbekanntem parent → Wurzel; Layout mit unbekanntem folder → Wurzel.
    expect(tree.rootFolders.map((f) => f.folder.id)).toEqual(["orphan"]);
    expect(tree.rootLayouts.map((l) => l.id)).toEqual(["ghost"]);
  });
});

// ── Master-Baum + Layout/Master-Trennung (kind) ──────────────────────────────

function mas(id: string, folderId?: string): MasterLayout {
  return {
    id,
    name: id,
    paper: "a4",
    orientation: "portrait",
    titleBlock: {},
    ...(folderId ? { folderId } : {}),
  };
}

describe("buildMasterTree", () => {
  it("gruppiert Master in Master-Ordner + Wurzel; Unterordner verschachtelt", () => {
    const folders: LayoutFolder[] = [
      createLayoutFolder("A", "A", undefined, "master"),
      createLayoutFolder("B", "B", "A", "master"),
    ];
    const masters = [mas("root1"), mas("inA", "A"), mas("inB", "B")];
    const tree = buildMasterTree(folders, masters);
    expect(tree.rootMasters.map((m) => m.id)).toEqual(["root1"]);
    expect(tree.rootFolders).toHaveLength(1);
    const a = tree.rootFolders[0];
    expect(a.folder.id).toBe("A");
    expect(a.masters.map((m) => m.id)).toEqual(["inA"]);
    expect(a.subfolders[0].masters.map((m) => m.id)).toEqual(["inB"]);
  });
});

describe("Layout-/Master-Ordner bleiben getrennt (kind-Filter)", () => {
  it("Master-Ordner erscheint NICHT im Layout-Baum und umgekehrt", () => {
    const folders: LayoutFolder[] = [
      createLayoutFolder("L", "Layout-Ordner"), // kind fehlt → Layout
      createLayoutFolder("M", "Master-Ordner", undefined, "master"),
    ];
    const layouts = [lay("l1", "L"), lay("lm", "M")]; // lm zeigt fälschlich auf Master-Ordner
    const masters = [mas("m1", "M"), mas("ml", "L")]; // ml zeigt fälschlich auf Layout-Ordner

    // Wie das Panel filtert: Layout-Baum nur kind!=="master", Master-Baum nur kind==="master".
    const layoutTree = buildLayoutTree(
      folders.filter((f) => f.kind !== "master"),
      layouts,
    );
    const masterTree = buildMasterTree(
      folders.filter((f) => f.kind === "master"),
      masters,
    );

    // Layout-Baum kennt nur den Layout-Ordner; das auf M zeigende Layout fällt auf die Wurzel.
    expect(layoutTree.rootFolders.map((f) => f.folder.id)).toEqual(["L"]);
    expect(layoutTree.rootLayouts.map((l) => l.id)).toEqual(["lm"]);
    // Master-Baum kennt nur den Master-Ordner; der auf L zeigende Master fällt auf die Wurzel.
    expect(masterTree.rootFolders.map((f) => f.folder.id)).toEqual(["M"]);
    expect(masterTree.rootMasters.map((m) => m.id)).toEqual(["ml"]);
    expect(masterTree.rootFolders[0].masters.map((m) => m.id)).toEqual(["m1"]);
  });
});

describe("deleteMasterFolder", () => {
  it("zieht Master + Unterordner auf die Elternebene und erhält kind:\"master\"", () => {
    const folders: LayoutFolder[] = [
      createLayoutFolder("A", "A", undefined, "master"),
      createLayoutFolder("B", "B", "A", "master"),
    ];
    const masters = [mas("a1", "A"), mas("b1", "B")];
    const r = deleteMasterFolder("A", folders, masters);
    expect(r.folders.map((f) => f.id)).toEqual(["B"]);
    // B auf Wurzel hochgezogen, aber weiterhin Master-Ordner.
    expect(r.folders[0].parentId).toBeUndefined();
    expect(r.folders[0].kind).toBe("master");
    expect(r.masters.find((m) => m.id === "a1")!.folderId).toBeUndefined();
    expect(r.masters.find((m) => m.id === "b1")!.folderId).toBe("B");
  });
});

describe("collectFolderLayouts", () => {
  it("liefert direkte Blätter zuerst, dann rekursiv Unterordner (PDF-Reihenfolge)", () => {
    const folders: LayoutFolder[] = [
      createLayoutFolder("A", "A"),
      createLayoutFolder("B", "B", "A"),
    ];
    const layouts = [lay("root1"), lay("a1", "A"), lay("a2", "A"), lay("b1", "B")];
    expect(collectFolderLayouts("A", folders, layouts).map((l) => l.id)).toEqual([
      "a1",
      "a2",
      "b1",
    ]);
    expect(collectFolderLayouts("B", folders, layouts).map((l) => l.id)).toEqual([
      "b1",
    ]);
  });
  it("unbekannter Ordner → leer", () => {
    expect(collectFolderLayouts("nope", [], [lay("x")])).toEqual([]);
  });
});

describe("deleteFolder", () => {
  it("zieht Layouts + Unterordner auf die Elternebene (kein Datenverlust)", () => {
    const folders: LayoutFolder[] = [
      createLayoutFolder("A", "A"),
      createLayoutFolder("B", "B", "A"),
    ];
    const layouts = [lay("a1", "A"), lay("b1", "B")];
    // A (Wurzel) löschen → a1 auf Wurzel, B verliert parent → Wurzel.
    const r = deleteFolder("A", folders, layouts);
    expect(r.folders.map((f) => f.id)).toEqual(["B"]);
    expect(r.folders[0].parentId).toBeUndefined();
    expect(r.layouts.find((l) => l.id === "a1")!.folderId).toBeUndefined();
    expect(r.layouts.find((l) => l.id === "b1")!.folderId).toBe("B");
  });
  it("Unterordner löschen zieht Kinder zum Grosselter", () => {
    const folders: LayoutFolder[] = [
      createLayoutFolder("A", "A"),
      createLayoutFolder("B", "B", "A"),
    ];
    const layouts = [lay("b1", "B")];
    const r = deleteFolder("B", folders, layouts);
    expect(r.folders.map((f) => f.id)).toEqual(["A"]);
    expect(r.layouts[0].folderId).toBe("A");
  });
});

describe("folderPdfPages", () => {
  it("plant Seiten in Baum-Reihenfolge mit effektiver Blattgrösse", () => {
    const master: MasterLayout = createMasterLayout("m", "M", "a3", "landscape");
    const layouts: Layout[] = [
      { ...lay("p1", "F"), masterId: "m" }, // erbt A3 quer vom Master
      { ...lay("p2", "F"), customWidthMm: 500, customHeightMm: 200 }, // freie Grösse
      lay("p3", "F"), // A4 hoch (Default)
    ];
    const p = project({
      layoutFolders: [createLayoutFolder("F", "F")],
      layouts,
      masterLayouts: [master],
    });
    const pages = folderPdfPages(p, "F");
    expect(pages.map((x) => x.layout.id)).toEqual(["p1", "p2", "p3"]);
    expect(pages[0]).toMatchObject({ widthMm: 420, heightMm: 297 }); // Master A3 quer
    expect(pages[1]).toMatchObject({ widthMm: 500, heightMm: 200 }); // custom
    expect(pages[2]).toMatchObject({ widthMm: 210, heightMm: 297 }); // A4 hoch
  });
  it("leerer/unbekannter Ordner → keine Seiten", () => {
    expect(folderPdfPages(project(), "nope")).toEqual([]);
  });
});

describe("PAGE_MM / Standard-Formate A0–A6 + B0–B6", () => {
  it("PAPER_FORMATS listet 14 Formate (A-Reihe, dann B-Reihe)", () => {
    expect(PAPER_FORMATS).toEqual([
      "a0", "a1", "a2", "a3", "a4", "a5", "a6",
      "b0", "b1", "b2", "b3", "b4", "b5", "b6",
    ]);
  });
  it("liefert offizielle ISO-216-Masse (Hochformat, mm)", () => {
    expect(sheetSizeMm("a0", "portrait")).toEqual({ w: 841, h: 1189 });
    expect(sheetSizeMm("a1", "portrait")).toEqual({ w: 594, h: 841 });
    expect(sheetSizeMm("a2", "portrait")).toEqual({ w: 420, h: 594 });
    expect(sheetSizeMm("a5", "portrait")).toEqual({ w: 148, h: 210 });
    expect(sheetSizeMm("a6", "portrait")).toEqual({ w: 105, h: 148 });
    expect(sheetSizeMm("b0", "portrait")).toEqual({ w: 1000, h: 1414 });
    expect(sheetSizeMm("b3", "portrait")).toEqual({ w: 353, h: 500 });
    expect(sheetSizeMm("b6", "portrait")).toEqual({ w: 125, h: 176 });
  });
  it("Querformat dreht die Basis (Beispiel A0)", () => {
    expect(sheetSizeMm("a0", "landscape")).toEqual({ w: 1189, h: 841 });
  });
  it("jedes Format hat eine definierte Grösse", () => {
    for (const f of PAPER_FORMATS) {
      const s = sheetSizeMm(f, "portrait");
      expect(s.w).toBeGreaterThan(0);
      expect(s.h).toBeGreaterThan(0);
    }
  });
});

describe("isDescendant — Zyklus-Schutz", () => {
  const folders: LayoutFolder[] = [
    createLayoutFolder("A", "A"),
    createLayoutFolder("B", "B", "A"),
    createLayoutFolder("C", "C", "B"),
    createLayoutFolder("X", "X"),
  ];
  it("ein Ordner ist sein eigener Nachfahre (self)", () => {
    expect(isDescendant(folders, "A", "A")).toBe(true);
  });
  it("erkennt direkte + tiefe Nachfahren", () => {
    expect(isDescendant(folders, "B", "A")).toBe(true);
    expect(isDescendant(folders, "C", "A")).toBe(true);
  });
  it("nicht-verwandte Ordner sind keine Nachfahren", () => {
    expect(isDescendant(folders, "X", "A")).toBe(false);
    expect(isDescendant(folders, "A", "B")).toBe(false);
  });
});

describe("moveLayoutToFolder", () => {
  const layouts = [lay("l1", "A"), lay("l2")];
  it("setzt folderId auf den Zielordner", () => {
    const r = moveLayoutToFolder(layouts, "l2", "A");
    expect(r.find((l) => l.id === "l2")!.folderId).toBe("A");
  });
  it("null → zurück auf Wurzel (folderId entfernt)", () => {
    const r = moveLayoutToFolder(layouts, "l1", null);
    expect(r.find((l) => l.id === "l1")!.folderId).toBeUndefined();
    expect("folderId" in r.find((l) => l.id === "l1")!).toBe(false);
  });
  it("unbekannte Id → unverändert", () => {
    expect(moveLayoutToFolder(layouts, "nope", "A")).toEqual(layouts);
  });
});

describe("moveFolderToFolder", () => {
  const folders: LayoutFolder[] = [
    createLayoutFolder("A", "A"),
    createLayoutFolder("B", "B", "A"),
    createLayoutFolder("X", "X"),
  ];
  it("setzt parentId auf den Zielordner", () => {
    const r = moveFolderToFolder(folders, "X", "A");
    expect(r.find((f) => f.id === "X")!.parentId).toBe("A");
  });
  it("null → zurück auf Wurzel (parentId entfernt)", () => {
    const r = moveFolderToFolder(folders, "B", null);
    expect(r.find((f) => f.id === "B")!.parentId).toBeUndefined();
  });
  it("kind bleibt beim Wurzel-Verschieben erhalten (Master-Ordner)", () => {
    const withMaster: LayoutFolder[] = [
      createLayoutFolder("P", "P", undefined, "master"),
      createLayoutFolder("Q", "Q", "P", "master"),
    ];
    const r = moveFolderToFolder(withMaster, "Q", null);
    const q = r.find((f) => f.id === "Q")!;
    expect(q.parentId).toBeUndefined();
    expect(q.kind).toBe("master");
  });
  it("Zyklus (in eigenen Nachfahren) → No-op, Original zurück", () => {
    expect(moveFolderToFolder(folders, "A", "B")).toEqual(folders);
  });
  it("in sich selbst → No-op", () => {
    expect(moveFolderToFolder(folders, "A", "A")).toEqual(folders);
  });
});
