/**
 * Unit-Tests für `stairOutline` (Plan-Aussenlinie).
 *
 * Prüft:
 *  • Gerade Treppe → 4-Punkte-Rechteck (polygon, keine spiral).
 *  • L-Treppe → 6-Punkte-L-Polygon via Linien-Schnittpunkten.
 *  • Wendeltreppe → SpiralOutlineSegments (2 Bögen, 2 radiale Linien, kein polygon).
 *
 * Die genauen Koordinaten-Werte werden nur grob geprüft (Vorzeichen, Länge,
 * Schliessen); die Geometrie-Logik wird via Abstand-Invarianten verifiziert.
 */

import { describe, it, expect } from "vitest";
import { stairOutline, stairGeometry } from "./stair";
import type { Stair, Vec2 } from "../model/types";

/** Einfache Distanz-Funktion. */
const dist = (a: Vec2, b: Vec2) => Math.hypot(b.x - a.x, b.y - a.y);

/** Minimale gerade Treppe nach rechts (+X). */
const geradeStair: Stair = {
  id: "g1",
  type: "stair",
  floorId: "eg",
  categoryCode: "40",
  shape: "straight",
  start: { x: 0, y: 0 },
  dir: { x: 1, y: 0 },
  runLength: 3.0,
  width: 1.2,
  stepCount: 16,
};

/** L-Treppe: erster Lauf +X, zweiter Lauf +Y (turn = +1). */
const lStair: Stair = {
  id: "l1",
  type: "stair",
  floorId: "eg",
  categoryCode: "40",
  shape: "L",
  start: { x: 0, y: 0 },
  dir: { x: 1, y: 0 },
  runLength: 2.0,
  run2Length: 2.0,
  turn: 1,
  width: 1.2,
  stepCount: 14,
};

/** Wendeltreppe: 270° im Gegenuhrzeigersinn, Radius 1.5 m. */
const wendelStair: Stair = {
  id: "w1",
  type: "stair",
  floorId: "eg",
  categoryCode: "40",
  shape: "spiral",
  start: { x: 1.5, y: 0 },
  dir: { x: 1, y: 0 },
  runLength: 3.0, // bei Wendel: Pflichtfeld, inhaltlich nicht genutzt
  center: { x: 0, y: 0 },
  radius: 1.5,
  sweep: 270,
  width: 1.2,
  stepCount: 12,
};

describe("stairOutline — gerade Treppe", () => {
  const totalRise = 3.0;
  const result = stairOutline(geradeStair, totalRise);

  it("liefert polygon, keine spiral", () => {
    expect(result.polygon).not.toBeNull();
    expect(result.spiral).toBeNull();
  });

  it("polygon hat genau 4 Ecken", () => {
    expect(result.polygon!.length).toBe(4);
  });

  it("Lauflaenge entspricht runLength", () => {
    // Start-Kante (x=0) vs End-Kante (x=3): Abstand der gleichseitigen Kanten = 3.0 m.
    const pts = result.polygon!;
    // pts[0] und pts[1] liegen bei x≈0, pts[2] und pts[3] bei x≈runLength.
    const startX = (pts[0].x + pts[1].x) / 2;
    const endX   = (pts[2].x + pts[3].x) / 2;
    expect(Math.abs(endX - startX)).toBeCloseTo(geradeStair.runLength, 5);
  });

  it("Breite entspricht stair.width", () => {
    const pts = result.polygon!;
    // Seitenkante 0→3 und 1→2 haben die Breite = stair.width.
    const w = dist(pts[0], pts[1]);
    expect(w).toBeCloseTo(geradeStair.width, 5);
  });
});

describe("stairOutline — L-Treppe", () => {
  const totalRise = 3.0;
  const result = stairOutline(lStair, totalRise);

  it("liefert polygon, keine spiral", () => {
    expect(result.polygon).not.toBeNull();
    expect(result.spiral).toBeNull();
  });

  it("polygon hat 6 Ecken (L-Form)", () => {
    // 4 Aussen-Eck + 2 Innen-Eck via Linien-Schnittpunkten = 6 Punkte.
    expect(result.polygon!.length).toBe(6);
  });

  it("keine NaN-Koordinaten", () => {
    for (const p of result.polygon!) {
      expect(isFinite(p.x)).toBe(true);
      expect(isFinite(p.y)).toBe(true);
    }
  });
});

describe("stairOutline — Wendeltreppe", () => {
  const totalRise = 3.0;
  const result = stairOutline(wendelStair, totalRise);

  it("liefert spiral, kein polygon", () => {
    expect(result.spiral).not.toBeNull();
    expect(result.polygon).toBeNull();
  });

  it("zwei radiale Schliesslinien (Start + Ende)", () => {
    expect(result.spiral!.lines.length).toBe(2);
  });

  it("zwei Kreisboegen (Innen + Aussen)", () => {
    expect(result.spiral!.arcs.length).toBe(2);
  });

  it("Innenradius < Aussenradius", () => {
    const [arcIn, arcOut] = result.spiral!.arcs;
    expect(arcIn.r).toBeLessThan(arcOut.r);
  });

  it("Startlinie Innen-Punkt liegt auf Innenkreis", () => {
    const arcIn = result.spiral!.arcs[0];
    const [lineStart] = result.spiral!.lines;
    const [innerPt] = lineStart;
    const d = dist({ x: arcIn.cx, y: arcIn.cy }, innerPt);
    expect(d).toBeCloseTo(arcIn.r, 4);
  });
});

describe("stairOutline — Referenz links/mitte/rechts (Item 4)", () => {
  const totalRise = 3.0;
  // Gerade Treppe entlang +X, Breite 1.2 m. Perp (linke Normale) = (0, +1).
  const base: Stair = { ...geradeStair, referenz: "mitte" };
  const mitte = stairOutline(base, totalRise).polygon!;
  const links = stairOutline({ ...base, referenz: "links" }, totalRise).polygon!;
  const rechts = stairOutline({ ...base, referenz: "rechts" }, totalRise).polygon!;

  /** Mittlere Y-Höhe eines Rechteck-Polygons. */
  const midY = (pts: Vec2[]) => pts.reduce((s, p) => s + p.y, 0) / pts.length;

  it("Breite bleibt bei allen Referenzen gleich", () => {
    for (const poly of [mitte, links, rechts]) {
      expect(dist(poly[0], poly[1])).toBeCloseTo(geradeStair.width, 5);
    }
  });

  it("referenz=links: Treppe liegt RECHTS der Achse (−n) → Mitte um width/2 nach −Y", () => {
    // Achse (start/dir) = linke Kante; die Fläche versetzt sich in −n = (0,−1).
    expect(midY(links) - midY(mitte)).toBeCloseTo(-geradeStair.width / 2, 5);
  });

  it("referenz=rechts: Treppe liegt LINKS der Achse (+n) → Mitte um width/2 nach +Y", () => {
    expect(midY(rechts) - midY(mitte)).toBeCloseTo(+geradeStair.width / 2, 5);
  });

  it("bei referenz=links liegt die Achse (y=0) auf der linken Kante der Treppe", () => {
    // Linke Kante = max-Y-Kante der Fläche (n = +Y). Sie soll bei y=0 liegen.
    const maxY = Math.max(...links.map((p) => p.y));
    expect(maxY).toBeCloseTo(0, 5);
  });

  it("mitte entspricht dem Default (kein referenz-Feld gesetzt)", () => {
    const noRef = stairOutline({ ...geradeStair }, totalRise).polygon!;
    for (let i = 0; i < noRef.length; i++) {
      expect(noRef[i].x).toBeCloseTo(mitte[i].x, 6);
      expect(noRef[i].y).toBeCloseTo(mitte[i].y, 6);
    }
  });
});

describe("stairGeometry — Lauflinie bleibt visuell zentriert (Item 4)", () => {
  const totalRise = 3.0;
  const links = stairGeometry({ ...geradeStair, referenz: "links" }, totalRise);
  const mitte = stairGeometry({ ...geradeStair, referenz: "mitte" }, totalRise);

  it("Lauflinie liegt auf der Treppen-Mitte, nicht auf der Referenzkante (y=0)", () => {
    // Bei referenz=links liegt die Mitte bei y = −width/2 = −0.6.
    for (const p of links.runLine) {
      expect(p.y).toBeCloseTo(-geradeStair.width / 2, 5);
    }
    // Bei mitte liegt die Lauflinie auf y=0 (Achse = Mitte).
    for (const p of mitte.runLine) {
      expect(p.y).toBeCloseTo(0, 5);
    }
  });

  it("Tritte sind bei referenz=links um −width/2 versetzt", () => {
    const dy = links.treads[0].pts[0].y - mitte.treads[0].pts[0].y;
    expect(dy).toBeCloseTo(-geradeStair.width / 2, 5);
  });
});
