// Unit-Tests für den IFC4-Export (exportIfcSpf) — erste vollständige Scheibe.
//   • Strukturelle Validität: KEINE dangling references (jede #N-Referenz ist
//     definiert), KEINE doppelten Entity-IDs — der wichtigste Test.
//   • Header/FILE_SCHEMA('IFC4') vorhanden.
//   • Je "floor"-Geschoss genau ein IfcBuildingStorey.
//   • Wandanzahl → IfcWall-Anzahl.
//   • Eine Öffnung ⇒ IfcOpeningElement + IfcRelVoidsElement (+ IfcDoor/
//     IfcWindow + IfcRelFillsElement).
//   • GUID-Format (22 Zeichen, gültiger Zeichensatz), deterministisch.
//   • Leeres Projekt ⇒ valider Minimal-IFC (Project/Site/Building, kein Crash).
//
// Fixture-Muster gespiegelt von exportSchedule.test.ts.

import { describe, it, expect } from "vitest";
import { exportIfcSpf, ifcGuid } from "./exportIfc";
import type {
  Ceiling,
  ExtrudedSolid,
  Opening,
  Project,
  Stair,
  Wall,
} from "../model/types";

/**
 * Minimalprojekt: 2 Geschosse (EG + OG, EG auch ein "section"-Level, das
 * NICHT zu einem Storey werden darf) + 2 Wände (Wandtyp T=0.4) + 1 Decke
 * (Deckentyp T=0.2) + 1 Tür + 1 Fenster (an W1 gehostet) + 1 Treppe +
 * 1 Extrusion.
 */
function fixtureProject(): Project {
  const walls: Wall[] = [
    {
      id: "W1",
      type: "wall",
      floorId: "eg",
      categoryCode: "20",
      start: { x: 0, y: 0 },
      end: { x: 5, y: 0 },
      wallTypeId: "aw",
      height: 2.6,
    },
    {
      id: "W2",
      type: "wall",
      floorId: "eg",
      categoryCode: "20",
      start: { x: 5, y: 0 },
      end: { x: 5, y: 4 },
      wallTypeId: "aw",
      height: 2.6,
    },
  ];
  const ceilings: Ceiling[] = [
    {
      id: "D1",
      type: "ceiling",
      floorId: "eg",
      categoryCode: "30",
      outline: [
        { x: 0, y: 0 },
        { x: 5, y: 0 },
        { x: 5, y: 4 },
        { x: 0, y: 4 },
      ],
      wallTypeId: "dt",
      ceilingTypeId: "dt",
    },
  ];
  const openings: Opening[] = [
    {
      id: "T1",
      type: "opening",
      hostWallId: "W1",
      categoryCode: "21",
      kind: "door",
      position: 1,
      width: 0.9,
      height: 2.1,
      sillHeight: 0,
    },
    {
      id: "F1",
      type: "opening",
      hostWallId: "W2",
      categoryCode: "21",
      kind: "window",
      position: 1,
      width: 1.2,
      height: 1.5,
      sillHeight: 0.9,
    },
  ];
  const stairs: Stair[] = [
    {
      id: "S1",
      type: "stair",
      floorId: "eg",
      categoryCode: "40",
      shape: "straight",
      start: { x: 0, y: 0 },
      dir: { x: 1, y: 0 },
      runLength: 3,
      width: 1.2,
      totalRise: 2.6,
      stepCount: 16,
    },
  ];
  const extrudedSolids: ExtrudedSolid[] = [
    {
      id: "E1",
      type: "extrudedSolid",
      levelId: "eg",
      points: [
        { x: 0, y: 0 },
        { x: 2, y: 0 },
        { x: 2, y: 3 },
        { x: 0, y: 3 },
      ],
      height: 2.5,
    },
  ];
  return {
    id: "t",
    name: "Testprojekt",
    lineStyles: [],
    hatches: [],
    components: [{ id: "c", name: "C", color: "#ccc", hatchId: "none", joinPriority: 10 }],
    wallTypes: [{ id: "aw", name: "Aussenwand", layers: [{ componentId: "c", thickness: 0.4 }] }],
    ceilingTypes: [{ id: "dt", name: "Betondecke", layers: [{ componentId: "c", thickness: 0.2 }] }],
    drawingLevels: [
      { id: "eg", name: "EG", kind: "floor", visible: true, locked: false, floorHeight: 2.6, cutHeight: 1.0, baseElevation: 0 },
      { id: "og", name: "OG", kind: "floor", visible: true, locked: false, floorHeight: 2.6, cutHeight: 1.0, baseElevation: 2.6 },
      { id: "schnitt-a", name: "Schnitt A", kind: "section", visible: true, locked: false, linePoints: [{ x: 0, y: 0 }, { x: 1, y: 0 }], directionSign: 1 },
    ],
    layers: [{ code: "20", name: "Wände", color: "#0a0a0a", lw: 0.5, visible: true, locked: false }],
    walls,
    doors: [],
    openings,
    ceilings,
    stairs,
    extrudedSolids,
    rooms: [],
    drawings2d: [],
    context: [],
  } as Project;
}

/** Sammelt alle definierten Entity-IDs (`#N=`) und alle referenzierten `#N`. */
function collectIds(spf: string): { defined: Set<number>; referenced: Set<number>; duplicates: number[] } {
  const defined = new Set<number>();
  const duplicates: number[] = [];
  const referenced = new Set<number>();
  for (const line of spf.split("\n")) {
    const defMatch = /^#(\d+)=/.exec(line);
    if (defMatch) {
      const id = Number(defMatch[1]);
      if (defined.has(id)) duplicates.push(id);
      defined.add(id);
    }
    const refs = line.matchAll(/#(\d+)/g);
    for (const r of refs) {
      // Der erste Treffer je Zeile ist ggf. die Definition selbst — trotzdem
      // harmlos mitgezählt, da sie ja in `defined` steht (Selbstreferenz-Check
      // unten prüft nur: JEDE referenzierte ID muss iRGENDWO definiert sein).
      referenced.add(Number(r[1]));
    }
  }
  return { defined, referenced, duplicates };
}

describe("exportIfcSpf — IFC4-Export", () => {
  it("erzeugt keine dangling references und keine doppelten Entity-IDs", () => {
    const spf = exportIfcSpf(fixtureProject());
    const { defined, referenced, duplicates } = collectIds(spf);
    expect(duplicates).toEqual([]);
    const dangling = [...referenced].filter((id) => !defined.has(id));
    expect(dangling).toEqual([]);
    expect(defined.size).toBeGreaterThan(0);
  });

  it("trägt einen gültigen IFC4-Header", () => {
    const spf = exportIfcSpf(fixtureProject());
    expect(spf).toContain("ISO-10303-21;");
    expect(spf).toContain("FILE_SCHEMA(('IFC4'));");
    expect(spf).toContain("END-ISO-10303-21;");
  });

  it("erzeugt je 'floor'-Geschoss genau ein IfcBuildingStorey (Schnitte NICHT)", () => {
    const spf = exportIfcSpf(fixtureProject());
    const storeyLines = spf.split("\n").filter((l) => l.includes("=IFCBUILDINGSTOREY("));
    expect(storeyLines).toHaveLength(2); // EG + OG, NICHT "Schnitt A"
    expect(storeyLines.some((l) => l.includes("'EG'"))).toBe(true);
    expect(storeyLines.some((l) => l.includes("'OG'"))).toBe(true);
  });

  it("bildet jede Wand auf genau ein IfcWall ab", () => {
    const spf = exportIfcSpf(fixtureProject());
    const wallLines = spf.split("\n").filter((l) => l.includes("=IFCWALL("));
    expect(wallLines).toHaveLength(2);
  });

  it("Tür/Fenster bleiben eigene Objekte (IfcDoor/IfcWindow); das Loch steckt im Wand-Mesh (kein IfcOpeningElement/Void/Fill)", () => {
    const spf = exportIfcSpf(fixtureProject());
    const lines = spf.split("\n");
    // Tür + Fenster als eigene Objekte erhalten.
    expect(lines.filter((l) => l.includes("=IFCDOOR("))).toHaveLength(1);
    expect(lines.filter((l) => l.includes("=IFCWINDOW("))).toHaveLength(1);
    // Bewusste Abwägung: Wand ist jetzt ein Face-Set mit ausgeschnittenem Loch —
    // die frühere IfcOpeningElement-Void/Fill-Semantik entfällt (siehe Dateikopf).
    expect(lines.filter((l) => l.includes("=IFCOPENINGELEMENT("))).toHaveLength(0);
    expect(lines.filter((l) => l.includes("=IFCRELVOIDSELEMENT("))).toHaveLength(0);
    expect(lines.filter((l) => l.includes("=IFCRELFILLSELEMENT("))).toHaveLength(0);
  });

  it("bildet Wände als IfcTriangulatedFaceSet (IfcCartesianPointList3D + CoordIndex) statt Profil-Extrusion ab", () => {
    const spf = exportIfcSpf(fixtureProject());
    const lines = spf.split("\n");
    // Genau ein Face-Set + eine Punktliste je Wand (2 Wände).
    expect(lines.filter((l) => l.includes("=IFCTRIANGULATEDFACESET("))).toHaveLength(2);
    expect(lines.filter((l) => l.includes("=IFCCARTESIANPOINTLIST3D("))).toHaveLength(2);
    // Wand-Shape ist als Tessellation deklariert (nicht mehr SweptSolid).
    expect(spf).toContain("'Tessellation'");
    // Face-Set-CoordIndex referenziert 1-basierte Punkt-Indizes (Tripel-Listen).
    const fs = lines.find((l) => l.includes("=IFCTRIANGULATEDFACESET("));
    expect(fs).toMatch(/\(\(\d+,\d+,\d+\)/);
  });

  it("bildet Decke, Treppe und Extrusion auf die erwarteten Entity-Typen ab", () => {
    const spf = exportIfcSpf(fixtureProject());
    const lines = spf.split("\n");
    expect(lines.filter((l) => l.includes("=IFCSLAB("))).toHaveLength(1);
    expect(lines.filter((l) => l.includes("=IFCSTAIR("))).toHaveLength(1);
    expect(lines.filter((l) => l.includes("=IFCBUILDINGELEMENTPROXY("))).toHaveLength(1);
  });

  it("das Wand-Face-Set enthält das ausgeschnittene Fenster-Loch (Loch-Vertices + volle Dreieckszahl, keine dangling refs)", () => {
    // W2 hat das Fenster F1 (position 1, width 1.2, sill 0.9, height 1.5) → das
    // Loch liegt voll im Wand-Inneren; das Face-Set der Wand hat exakt die
    // Rust-Zerlegung: 8 Langseiten-Teilrechtecke ×2×2 + 8 (Deckel/Boden/Kappen)
    // + 8 (4 Laibungen) = 48 Dreiecke, 144 Punkte.
    const spf = exportIfcSpf(fixtureProject());
    const lines = spf.split("\n");
    const faceSets = lines.filter((l) => l.includes("=IFCTRIANGULATEDFACESET("));
    // Das Loch-Face-Set hat 48 CoordIndex-Tripel.
    const triCounts = faceSets.map((l) => (l.match(/\(\d+,\d+,\d+\)/g) ?? []).length);
    expect(triCounts).toContain(48);
    // Zugehörige Punktliste hat 144 Punkte (3er-Koordinaten-Tupel).
    const pointLists = lines.filter((l) => l.includes("=IFCCARTESIANPOINTLIST3D("));
    const ptCounts = pointLists.map((l) => (l.match(/\([^()]*,[^()]*,[^()]*\)/g) ?? []).length);
    expect(ptCounts).toContain(144);
    // Keine dangling refs (der Kern-Invarianten-Check gilt auch mit Face-Sets).
    const { defined, referenced } = collectIds(spf);
    expect([...referenced].filter((id) => !defined.has(id))).toEqual([]);
  });

  it("Wand-Face-Sets sind NACH AUSSEN orientiert (positives Volumen) — Regression gegen die Reflexions-Wicklung, die die Wand hohl machte", () => {
    // Der IFC-Achsen-Swap (x,y,z)→(x,z,y) ist eine Reflexion und kehrte die
    // Dreiecks-Wicklung um → Normalen zeigten nach INNEN → Viewer cullten die
    // Vorderseiten → Wand wirkte oben/unten offen. Nach dem Wicklungs-Ausgleich
    // muss das signierte Volumen jedes Wandkörpers POSITIV sein (aussen orientiert).
    const spf = exportIfcSpf(fixtureProject());
    const lines = spf.split("\n");
    const byId = new Map<number, string>();
    for (const l of lines) {
      const m = /^#(\d+)=/.exec(l);
      if (m) byId.set(Number(m[1]), l);
    }
    const parsePoints = (line: string): number[][] =>
      [...line.matchAll(/\(([-\d.]+),([-\d.]+),([-\d.]+)\)/g)].map((m) => [
        Number(m[1]),
        Number(m[2]),
        Number(m[3]),
      ]);
    const faceSets = lines.filter((l) => l.includes("=IFCTRIANGULATEDFACESET("));
    expect(faceSets.length).toBeGreaterThan(0);
    let closedCount = 0;
    for (const fs of faceSets) {
      const ptListId = Number(/=IFCTRIANGULATEDFACESET\(#(\d+),/.exec(fs)![1]);
      const closed = /=IFCTRIANGULATEDFACESET\(#\d+,\$,([^,]+),/.exec(fs)![1];
      if (closed === ".T.") closedCount++;
      const pts = parsePoints(byId.get(ptListId)!);
      const tris = [...fs.matchAll(/\((\d+),(\d+),(\d+)\)/g)].map((m) => [
        Number(m[1]) - 1,
        Number(m[2]) - 1,
        Number(m[3]) - 1,
      ]);
      // 6× signiertes Volumen Σ v0·(v1×v2): > 0 ⇒ Normalen zeigen nach aussen.
      let vol6 = 0;
      for (const [a, b, c] of tris) {
        const [ax, ay, az] = pts[a];
        const [bx, by, bz] = pts[b];
        const [cx, cy, cz] = pts[c];
        vol6 += ax * (by * cz - bz * cy) + ay * (bz * cx - bx * cz) + az * (bx * cy - by * cx);
      }
      expect(vol6).toBeGreaterThan(0);
    }
    // Beide Wände sind geschlossene Prisma-Körper (Fenster = Durchgangsloch,
    // Tür = umlaufende П-Kerbe) → Closed=.T. bei beiden.
    expect(closedCount).toBe(2);
  });

  it("GUIDs sind 22 Zeichen lang, nutzen den gültigen IFC-Zeichensatz und sind deterministisch", () => {
    const guid = ifcGuid("W1");
    expect(guid).toHaveLength(22);
    expect(guid).toMatch(/^[0-9A-Za-z_$]{22}$/);
    expect(ifcGuid("W1")).toBe(guid); // stabil über Re-Export
    expect(ifcGuid("W2")).not.toBe(guid); // unterschiedliche IDs → unterschiedliche GUIDs
  });

  it("leeres Projekt ⇒ valider Minimal-IFC (Project/Site/Building, kein Crash)", () => {
    const proj = fixtureProject();
    proj.walls = [];
    proj.ceilings = [];
    proj.openings = [];
    proj.stairs = [];
    proj.extrudedSolids = [];
    proj.drawingLevels = [];
    const spf = exportIfcSpf(proj);
    expect(spf).toContain("=IFCPROJECT(");
    expect(spf).toContain("=IFCSITE(");
    expect(spf).toContain("=IFCBUILDING(");
    const { defined, referenced, duplicates } = collectIds(spf);
    expect(duplicates).toEqual([]);
    expect([...referenced].filter((id) => !defined.has(id))).toEqual([]);
  });
});
