// Unit-Tests für den STL-/OBJ-Mesh-Export (reines Modul, siehe exportMesh.ts).
//   • OBJ: ≥1 v/f, alle f-Indizes innerhalb der Vertexzahl, alle Koordinaten endlich.
//   • STL: solid/endsolid-Rahmen, Facettenzahl = Dreieckszahl, je Facette genau 3 vertex-Zeilen.
//   • Dreieckszahl-Plausibilität: isolierte Wand-Box (12) und isoliertes N-Eck-Prisma (4N-4).
//   • leeres Projekt ⇒ gültige leere Datei (kein Crash).

import { describe, it, expect } from "vitest";
import { exportObj, exportStl } from "./exportMesh";
import type { Project, Wall, Ceiling, ExtrudedSolid, Opening } from "../model/types";

/** Gemeinsame Ressourcen-Basis (Komponente/Wandtyp/Deckentyp/Geschoss/Ebene). */
function baseProject(): Project {
  return {
    id: "t",
    name: "T",
    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 },
    ],
    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;
}

/** Eine einzelne, frei stehende Wand (kein Nachbar ⇒ kein Gehrungs-/Anschlussschnitt). */
function projectWithOneWall(): Project {
  const p = baseProject();
  const wall: Wall = {
    id: "W1",
    type: "wall",
    floorId: "eg",
    categoryCode: "20",
    start: { x: 0, y: 0 },
    end: { x: 5, y: 0 },
    wallTypeId: "aw",
    height: 2.6,
  };
  p.walls = [wall];
  return p;
}

/**
 * Eine frei stehende Wand mit EINER Öffnung (Fenster ODER Tür). Das Fenster
 * (sillHeight>0) liegt vollständig im Wand-Inneren (vier Laibungen); die Tür
 * (sillHeight 0) berührt die Wand-UK (keine untere Laibung).
 */
function projectWithOpening(kind: "window" | "door"): Project {
  const p = projectWithOneWall();
  const opening: Opening = {
    id: kind === "window" ? "F1" : "T1",
    type: "opening",
    hostWallId: "W1",
    categoryCode: "21",
    kind,
    position: 2,
    width: 1,
    height: kind === "window" ? 1.5 : 2.1,
    sillHeight: kind === "window" ? 0.9 : 0,
  };
  p.openings = [opening];
  return p;
}

/** Eine einzelne Extrusion mit N-Eck-Profil (kein Wand-/Deckenkontext). */
function projectWithPolygonExtrusion(pts: { x: number; y: number }[], height = 2.5): Project {
  const p = baseProject();
  const solid: ExtrudedSolid = {
    id: "E1",
    type: "extrudedSolid",
    levelId: "eg",
    points: pts,
    height,
  };
  p.extrudedSolids = [solid];
  return p;
}

/** Kombiniertes Fixture-Projekt: 2 Wände (Eckstoss), 1 Decke, 1 Extrusion. */
function fixtureProject(): Project {
  const p = baseProject();
  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 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,
    },
  ];
  p.walls = walls;
  p.ceilings = ceilings;
  p.extrudedSolids = extrudedSolids;
  return p;
}

/** Parst die `v`-Zeilen eines OBJ-Strings zu Koordinaten-Tripeln. */
function parseObjVertices(obj: string): number[][] {
  return obj
    .split("\n")
    .filter((l) => l.startsWith("v "))
    .map((l) => l.slice(2).trim().split(/\s+/).map(Number));
}

/** Parst die `f`-Zeilen eines OBJ-Strings zu 1-basierten Index-Tripeln. */
function parseObjFaces(obj: string): number[][] {
  return obj
    .split("\n")
    .filter((l) => l.startsWith("f "))
    .map((l) => l.slice(2).trim().split(/\s+/).map(Number));
}

describe("exportObj — Wavefront-OBJ-Export", () => {
  it("liefert ≥1 v und ≥1 f, alle f-Indizes innerhalb der Vertexzahl, alle Koordinaten endlich", () => {
    const obj = exportObj(fixtureProject());
    const verts = parseObjVertices(obj);
    const faces = parseObjFaces(obj);
    expect(verts.length).toBeGreaterThan(0);
    expect(faces.length).toBeGreaterThan(0);
    for (const v of verts) {
      expect(v).toHaveLength(3);
      for (const c of v) expect(Number.isFinite(c)).toBe(true);
    }
    for (const f of faces) {
      expect(f).toHaveLength(3);
      for (const idx of f) {
        expect(idx).toBeGreaterThanOrEqual(1);
        expect(idx).toBeLessThanOrEqual(verts.length);
      }
    }
  });

  it("gruppiert Bauteil-Vorkommen als o-Objekte (Wand/Decke/Extrusion)", () => {
    const obj = exportObj(fixtureProject());
    expect(obj).toContain("o Wand_W1");
    expect(obj).toContain("o Wand_W2");
    expect(obj).toContain("o Decke_D1");
    expect(obj).toContain("o Extrusion_E1");
  });

  it("leeres Projekt ⇒ gültige Datei ohne v/f (kein Crash)", () => {
    const obj = exportObj(baseProject());
    expect(obj.split("\n").some((l) => l.startsWith("v "))).toBe(false);
    expect(obj.split("\n").some((l) => l.startsWith("f "))).toBe(false);
    expect(obj.length).toBeGreaterThan(0);
  });
});

describe("exportStl — ASCII-STL-Export", () => {
  it("hat solid/endsolid-Rahmen, je Facette genau 3 vertex-Zeilen, Facettenzahl = Dreieckszahl", () => {
    const stl = exportStl(fixtureProject());
    const lines = stl.split("\n").filter((l) => l.length > 0);
    expect(lines[0]).toBe("solid dossier");
    expect(lines[lines.length - 1]).toBe("endsolid dossier");
    const facetCount = lines.filter((l) => l.startsWith("facet normal")).length;
    const loopCount = lines.filter((l) => l === "outer loop").length;
    const endloopCount = lines.filter((l) => l === "endloop").length;
    const endfacetCount = lines.filter((l) => l === "endfacet").length;
    const vertexCount = lines.filter((l) => l.startsWith("vertex ")).length;
    expect(facetCount).toBeGreaterThan(0);
    expect(loopCount).toBe(facetCount);
    expect(endloopCount).toBe(facetCount);
    expect(endfacetCount).toBe(facetCount);
    expect(vertexCount).toBe(facetCount * 3);
  });

  it("leeres Projekt ⇒ gültiges leeres solid-Gerüst (kein Crash)", () => {
    const stl = exportStl(baseProject());
    expect(stl).toBe("solid dossier\nendsolid dossier\n");
  });

  it("Dreieckszahl-Plausibilität: eine frei stehende Wand ergibt genau 1 Box (12 Dreiecke, 8 Ecken)", () => {
    const stl = exportStl(projectWithOneWall());
    const facetCount = stl.split("\n").filter((l) => l.startsWith("facet normal")).length;
    expect(facetCount).toBe(12);
    const obj = exportObj(projectWithOneWall());
    expect(parseObjVertices(obj)).toHaveLength(8);
  });

  it("Dreieckszahl-Plausibilität: ein N-Eck-Profil ergibt ein Prisma mit 4N-4 Dreiecken (2N Ecken)", () => {
    // Rechteck (N=4): 2 Kappen à 2 Dreiecke + 4 Seitenquads à 2 Dreiecke = 12 = 4·4−4.
    const rectStl = exportStl(
      projectWithPolygonExtrusion([
        { x: 0, y: 0 },
        { x: 2, y: 0 },
        { x: 2, y: 1 },
        { x: 0, y: 1 },
      ]),
    );
    expect(rectStl.split("\n").filter((l) => l.startsWith("facet normal")).length).toBe(12);

    // Konvexes Fünfeck (N=5): 4·5−4 = 16 Dreiecke, 10 Ecken.
    const pentagon = [
      { x: 0, y: 0 },
      { x: 2, y: 0 },
      { x: 2.5, y: 1.5 },
      { x: 1, y: 2.5 },
      { x: -0.5, y: 1.5 },
    ];
    const pentaStl = exportStl(projectWithPolygonExtrusion(pentagon));
    expect(pentaStl.split("\n").filter((l) => l.startsWith("facet normal")).length).toBe(16);
    const pentaObj = exportObj(projectWithPolygonExtrusion(pentagon));
    expect(parseObjVertices(pentaObj)).toHaveLength(10);
  });

  it("Fenster wird als echtes Loch ausgeschnitten (Gitter-Zerlegung + 4 Laibungen, 48 Dreiecke statt 12)", () => {
    // Fenster [x 2..3] × [y 0.9..2.4] vollständig im Wand-Inneren. Erwartung
    // (= Rust-Zerlegung `extrude_layer_segment_with_holes`): 8 solide Langseiten-
    // Teilrechtecke (3×3-Gitter minus Loch) × 2 Seiten × 2 Dreiecke = 32,
    // + Deckel/Boden/2 Stirnkappen = 8, + 4 Laibungen × 2 = 8 → 48 Dreiecke.
    const stl = exportStl(projectWithOpening("window"));
    const facetCount = stl.split("\n").filter((l) => l.startsWith("facet normal")).length;
    expect(facetCount).toBe(48);

    // Die volle Box hätte NUR Ecken bei y∈{0,2.6}, x∈{0,5}. Der Ausschnitt
    // führt echte Vertices an den Loch-Rändern ein — Beweis, dass das Loch
    // wirklich in der Fläche steckt (nicht bloss eine übergelegte Scheibe).
    const verts = parseObjVertices(exportObj(projectWithOpening("window")));
    const near = (a: number, b: number) => Math.abs(a - b) < 1e-6;
    expect(verts.some((v) => near(v[1], 0.9))).toBe(true); // Brüstungs-OK
    expect(verts.some((v) => near(v[1], 2.4))).toBe(true); // Sturz-UK
    expect(verts.some((v) => near(v[0], 2))).toBe(true); // linke Loch-Kante
    expect(verts.some((v) => near(v[0], 3))).toBe(true); // rechte Loch-Kante
    // Deutlich mehr Vertices als die 8-Ecken-Vollbox.
    expect(verts.length).toBe(144);
  });

  it("Tür berührt die Wand-UK: keine untere Laibung (36 Dreiecke, keine Brüstungs-Zerlegung)", () => {
    // Tür [x 2..3] × [y 0..2.1]: Boden bekommt die Lücke [2,3] (statt einer
    // unteren Laibung) → 5 Langseiten-Teilrechtecke × 2 × 2 = 20, Deckel(1)=2,
    // Boden(2 Streifen)=4, 2 Stirnkappen=4, 3 Laibungen (links/rechts/oben)=6 → 36.
    const stl = exportStl(projectWithOpening("door"));
    const facetCount = stl.split("\n").filter((l) => l.startsWith("facet normal")).length;
    expect(facetCount).toBe(36);
    const verts = parseObjVertices(exportObj(projectWithOpening("door")));
    const near = (a: number, b: number) => Math.abs(a - b) < 1e-6;
    // Sturz-UK bei y=2.1 vorhanden, aber KEIN Brüstungsvertex (Tür sitzt am Boden).
    expect(verts.some((v) => near(v[1], 2.1))).toBe(true);
    expect(verts.every((v) => v[1] >= -1e-6 && v[1] <= 2.6 + 1e-6)).toBe(true);
  });

  it("facet-Normalen sind Einheitsvektoren aus dem Kreuzprodukt der Dreiecksecken", () => {
    const stl = exportStl(projectWithOneWall());
    const facetLines = stl.split("\n").filter((l) => l.startsWith("facet normal"));
    for (const line of facetLines) {
      const [nx, ny, nz] = line.slice("facet normal ".length).trim().split(/\s+/).map(Number);
      const len = Math.hypot(nx, ny, nz);
      expect(len).toBeGreaterThan(0.99);
      expect(len).toBeLessThan(1.01);
    }
  });
});
