// Differential-Paritaet: Rust-WASM-Kernel (`src-tauri/kernel2d`, Feature "web")
// gegen die TS-Referenz `kernel2d.ts` — auf identischen Eingaben muessen beide
// bis auf ein funktionsspezifisches Epsilon dasselbe liefern (siehe PORT_PLAN §5).
// Kern der Migration: beweist, dass der Port die Semantik bitnah erhaelt.
//
// VORAUSSETZUNG: `npm run build:kernel2d` muss vorher gelaufen sein (das Paket
// `src/engine/pkgKernel2d` ist git-ignoriert). Init synchron via `initSync` mit
// den WASM-Bytes aus `readFileSync` (kein fetch im Node-Lauf).
//
// Zwei Testklassen, NIE gemischt: Zufalls-Paritaet (naiv==naiv) und Golden
// (explizite Grenzfaelle). Robuste Praedikate sind hier NICHT im Spiel (v1).

import { existsSync, readFileSync } from "node:fs";
import { fileURLToPath } from "node:url";
import { beforeAll, describe, expect, it } from "vitest";

import type { Vec2 } from "../model/types";
import {
  circleCircleIntersect,
  closestPointOnSegment,
  extendSegment,
  filletCorner,
  isCCW,
  joinChains,
  lineCircleIntersect,
  lineSegmentIntersect,
  offsetPolyline,
  offsetSegment,
  pointSegmentDistance,
  projectParam,
  removeSegment,
  segmentCircleIntersect,
  segmentIntersect,
  segmentPolylineHits,
  signedArea,
  splitAtIntersections,
  splitClosedByChord,
  splitPolylineAtParam,
  splitSegmentByCutters,
  trimPolyline,
  trimSegment,
  type Fillet,
  type Hit,
} from "./kernel2d";
import {
  ceilingArea,
  isValidOutline,
  normalizeOutline,
  outlineBBox,
  outlineCentroid,
  pointInOutline,
} from "./ceiling";
import { centroid, perimeter, polygonArea } from "./roomArea";
import {
  defaultStepCount,
  pointHitsStair,
  pointInPolygon as stairPointInPolygon,
  stairBBox,
  stairCut,
  stairGeometry,
  type StairGeometry as TSStairGeometry,
} from "./stair";
import { wallTypeThickness, type Opening, type Project, type Stair, type Wall, type WallType } from "../model/types";
import {
  detectRooms,
  pointInPolygon as rbPointInPolygon,
  roomFromPointInside,
  roomFromPointInsideFaces,
  type WallSegment,
  type WallFace,
} from "./roomBoundary";
import { wallReferenceOffset } from "../model/wall";
import {
  doorSymbol,
  openingCenter,
  openingGapQuad,
  openingInterval,
  openingJambs,
  wallAxisFrame,
  wallAxisLength,
  windowSymbol,
  type DoorSymbol,
  type WindowSymbol,
} from "./opening";

type Poly = { pts: Vec2[]; closed: boolean };

// Das WASM-Paket ist git-ignoriert und wird nur von `npm run build:kernel2d`
// erzeugt. Fehlt es, wird die Suite SAUBER uebersprungen (statt Collection-Fehler),
// damit `vitest run` ohne vorherigen WASM-Build gruen bleibt. Darum dynamischer
// Import erst in `beforeAll` (kein statischer Top-Level-Import des Pakets).
type Batch = (json: string) => string;
const wasmPath = fileURLToPath(
  new URL("../engine/pkgKernel2d/kernel2d_bg.wasm", import.meta.url),
);
const built = existsSync(wasmPath);
let K: Record<string, Batch>;

beforeAll(async () => {
  if (!built) return;
  const m = await import("../engine/pkgKernel2d/kernel2d.js");
  (m as { initSync: (o: { module: Buffer }) => unknown }).initSync({
    module: readFileSync(wasmPath),
  });
  K = m as unknown as Record<string, Batch>;
});

if (!built) {
  // eslint-disable-next-line no-console
  console.warn(
    "[kernel2d.parity] pkgKernel2d fehlt — `npm run build:kernel2d` fuer den Diff-Test noetig. Uebersprungen.",
  );
}

// ── Seed-basierter RNG (mulberry32), deterministisch ─────────────────────────
function mulberry32(seed: number): () => number {
  let s = seed >>> 0;
  return () => {
    s = (s + 0x6d2b79f5) | 0;
    let t = Math.imul(s ^ (s >>> 15), 1 | s);
    t = (t + Math.imul(t ^ (t >>> 7), 61 | t)) ^ t;
    return ((t ^ (t >>> 14)) >>> 0) / 4294967296;
  };
}

/** Koordinate in [-100,100] m, mit Clustern nahe 0 und im 1e-6..1e-3-Bereich. */
function coord(rng: () => number): number {
  const r = rng();
  if (r < 0.15) return (rng() * 2 - 1) * 1e-3; // nahe 0
  if (r < 0.25) return (rng() * 2 - 1) * 1e-6 + Math.round(rng() * 4 - 2); // Schwellen
  return (rng() * 2 - 1) * 100;
}
const v = (rng: () => number): Vec2 => ({ x: coord(rng), y: coord(rng) });

/** Zwei sich garantiert schneidende Strecken um ein Zentrum c. */
function crossingSegs(rng: () => number) {
  const c = v(rng);
  const ang1 = rng() * Math.PI;
  const ang2 = ang1 + 0.2 + rng() * (Math.PI - 0.4); // nicht (fast) parallel
  const d1 = { x: Math.cos(ang1), y: Math.sin(ang1) };
  const d2 = { x: Math.cos(ang2), y: Math.sin(ang2) };
  const ext = () => 0.1 + rng() * 3;
  return {
    a1: { x: c.x - d1.x * ext(), y: c.y - d1.y * ext() },
    a2: { x: c.x + d1.x * ext(), y: c.y + d1.y * ext() },
    b1: { x: c.x - d2.x * ext(), y: c.y - d2.y * ext() },
    b2: { x: c.x + d2.x * ext(), y: c.y + d2.y * ext() },
  };
}

// ── Vergleichs-Helfer (Struktur zuerst, dann Werte mit op-Epsilon) ───────────
/** Abs-ODER-relative Toleranz: eng bei ~1, skaliert bei grossen Werten. */
function closeNum(a: number, b: number, rel = 1e-9): boolean {
  return Math.abs(a - b) <= rel * Math.max(1, Math.abs(a), Math.abs(b));
}
function closeVec(a: Vec2, b: Vec2, rel = 1e-9): boolean {
  return closeNum(a.x, b.x, rel) && closeNum(a.y, b.y, rel);
}
function closeHit(a: Hit, b: Hit): boolean {
  return closeVec(a.point, b.point) && closeNum(a.t, b.t) && closeNum(a.s, b.s);
}
function eqPts(w: Vec2[], t: Vec2[]): boolean {
  return w.length === t.length && t.every((p, i) => closeVec(w[i], p));
}
function eqPtsLists(w: Vec2[][], t: Vec2[][]): boolean {
  return w.length === t.length && t.every((s, i) => eqPts(w[i], s));
}
function eqPolyList(w: Poly[], t: Poly[]): boolean {
  return w.length === t.length && t.every((pl, i) => w[i].closed === pl.closed && eqPts(w[i].pts, pl.pts));
}
function eqPairs(w: [Vec2, Vec2][], t: [Vec2, Vec2][]): boolean {
  return w.length === t.length && t.every((pr, i) => closeVec(w[i][0], pr[0]) && closeVec(w[i][1], pr[1]));
}
/** Ein paar lange Schneider-Segmente quer durch die BBox (als offene Cutter). */
function genCutters(rng: () => number): Poly[] {
  const k = 1 + Math.floor(rng() * 3);
  return Array.from({ length: k }, () => ({
    pts: [
      { x: -80, y: (rng() * 2 - 1) * 60 },
      { x: 80, y: (rng() * 2 - 1) * 60 },
    ],
    closed: false,
  }));
}
function genPolyline(rng: () => number, minN: number, maxN: number): Vec2[] {
  const n = minN + Math.floor(rng() * (maxN - minN + 1));
  return Array.from({ length: n }, () => ({ x: (rng() * 2 - 1) * 50, y: (rng() * 2 - 1) * 50 }));
}

const N = 300;

describe.skipIf(!built)("kernel2d Rust-WASM ⇄ TS Paritaet — Zufall", () => {
  it("projectParam / closestPointOnSegment / pointSegmentDistance", () => {
    const rng = mulberry32(1);
    const qs = Array.from({ length: N }, () => ({ p: v(rng), a: v(rng), b: v(rng) }));

    const wProj = JSON.parse(K.project_param_batch_json(JSON.stringify(qs))) as number[];
    const wClose = JSON.parse(K.closest_point_batch_json(JSON.stringify(qs))) as Vec2[];
    const wDist = JSON.parse(K.point_segment_distance_batch_json(JSON.stringify(qs))) as number[];

    qs.forEach((q, i) => {
      expect(closeNum(wProj[i], projectParam(q.p, q.a, q.b)), `proj#${i}`).toBe(true);
      expect(closeVec(wClose[i], closestPointOnSegment(q.p, q.a, q.b)), `close#${i}`).toBe(true);
      expect(closeNum(wDist[i], pointSegmentDistance(q.p, q.a, q.b)), `dist#${i}`).toBe(true);
    });
  });

  it("segmentIntersect / lineSegmentIntersect (Zufall + garantierte Kreuzungen)", () => {
    const rng = mulberry32(2);
    const qs = Array.from({ length: N }, (_, i) =>
      i % 2 === 0
        ? { a1: v(rng), a2: v(rng), b1: v(rng), b2: v(rng) }
        : crossingSegs(rng),
    );
    const wSeg = JSON.parse(K.segment_intersect_batch_json(JSON.stringify(qs))) as (Hit | null)[];
    const wLine = JSON.parse(K.line_segment_intersect_batch_json(JSON.stringify(qs))) as (Hit | null)[];

    let hits = 0;
    qs.forEach((q, i) => {
      const t = segmentIntersect(q.a1, q.a2, q.b1, q.b2);
      // Struktur exakt: null ⇔ null.
      expect(wSeg[i] === null, `seg-null#${i}`).toBe(t === null);
      if (t && wSeg[i]) {
        expect(closeHit(wSeg[i]!, t), `seg#${i}`).toBe(true);
        hits++;
      }
      const tl = lineSegmentIntersect(q.a1, q.a2, q.b1, q.b2);
      expect(wLine[i] === null, `line-null#${i}`).toBe(tl === null);
      if (tl && wLine[i]) expect(closeHit(wLine[i]!, tl), `line#${i}`).toBe(true);
    });
    expect(hits, "keine einzige Kreuzung getroffen — Test waere aussagelos").toBeGreaterThan(50);
  });

  it("segmentPolylineHits (Struktur exakt + Werte)", () => {
    const rng = mulberry32(3);
    const qs = Array.from({ length: N }, () => {
      const n = 3 + Math.floor(rng() * 18);
      const pts = Array.from({ length: n }, () => v(rng));
      // Langer Schneider quer durch die BBox.
      return { a1: { x: -120, y: coord(rng) }, a2: { x: 120, y: coord(rng) }, pts, closed: rng() < 0.5 };
    });
    const wHits = JSON.parse(K.segment_polyline_hits_batch_json(JSON.stringify(qs))) as Hit[][];
    qs.forEach((q, i) => {
      const t = segmentPolylineHits(q.a1, q.a2, q.pts, q.closed);
      expect(wHits[i].length, `hits-len#${i}`).toBe(t.length);
      t.forEach((h, j) => expect(closeHit(wHits[i][j], h), `hit#${i}.${j}`).toBe(true));
    });
  });

  it("lineCircleIntersect / segmentCircleIntersect (Struktur + Werte)", () => {
    const rng = mulberry32(4);
    const qs = Array.from({ length: N }, () => ({
      a: v(rng),
      b: v(rng),
      center: v(rng),
      r: 0.01 + rng() * 50,
    }));
    const wLine = JSON.parse(K.line_circle_intersect_batch_json(JSON.stringify(qs))) as Vec2[][];
    const wSeg = JSON.parse(K.segment_circle_intersect_batch_json(JSON.stringify(qs))) as Vec2[][];
    qs.forEach((q, i) => {
      const tl = lineCircleIntersect(q.a, q.b, q.center, q.r);
      expect(wLine[i].length, `lc-len#${i}`).toBe(tl.length);
      tl.forEach((p, j) => expect(closeVec(wLine[i][j], p), `lc#${i}.${j}`).toBe(true));
      const ts = segmentCircleIntersect(q.a, q.b, q.center, q.r);
      expect(wSeg[i].length, `sc-len#${i}`).toBe(ts.length);
      ts.forEach((p, j) => expect(closeVec(wSeg[i][j], p), `sc#${i}.${j}`).toBe(true));
    });
  });

  it("circleCircleIntersect (Struktur + Werte)", () => {
    const rng = mulberry32(5);
    const qs = Array.from({ length: N }, () => ({
      c1: v(rng),
      r1: 0.01 + rng() * 50,
      c2: v(rng),
      r2: 0.01 + rng() * 50,
    }));
    const w = JSON.parse(K.circle_circle_intersect_batch_json(JSON.stringify(qs))) as Vec2[][];
    qs.forEach((q, i) => {
      const t = circleCircleIntersect(q.c1, q.r1, q.c2, q.r2);
      expect(w[i].length, `cc-len#${i}`).toBe(t.length);
      t.forEach((p, j) => expect(closeVec(w[i][j], p), `cc#${i}.${j}`).toBe(true));
    });
  });

  it("signedArea (rel 1e-9) / isCCW (Struktur exakt)", () => {
    const rng = mulberry32(6);
    const polys = Array.from({ length: N }, () => {
      const n = 3 + Math.floor(rng() * 8);
      return Array.from({ length: n }, () => v(rng));
    });
    const wArea = JSON.parse(K.signed_area_batch_json(JSON.stringify(polys))) as number[];
    const wCcw = JSON.parse(K.is_ccw_batch_json(JSON.stringify(polys))) as boolean[];
    polys.forEach((p, i) => {
      const a = signedArea(p);
      expect(Math.abs(wArea[i] - a) <= 1e-9 * Math.max(1, Math.abs(a)), `area#${i}`).toBe(true);
      expect(wCcw[i], `ccw#${i}`).toBe(isCCW(p));
    });
  });

  it("offsetSegment / offsetPolyline (Miter, Struktur + Werte rel 1e-9)", () => {
    const rng = mulberry32(7);
    const segs = Array.from({ length: N }, () => ({
      a: v(rng),
      b: v(rng),
      d: (rng() * 2 - 1) * 5,
    }));
    const wSeg = JSON.parse(K.offset_segment_batch_json(JSON.stringify(segs))) as [Vec2, Vec2][];
    segs.forEach((q, i) => {
      const [a, b] = offsetSegment(q.a, q.b, q.d);
      expect(closeVec(wSeg[i][0], a) && closeVec(wSeg[i][1], b), `offSeg#${i}`).toBe(true);
    });

    const polys = Array.from({ length: N }, () => {
      const n = 3 + Math.floor(rng() * 10);
      const pts = Array.from({ length: n }, () => v(rng));
      return { pts, d: (rng() * 2 - 1) * 5, closed: rng() < 0.5 };
    });
    const wPoly = JSON.parse(K.offset_polyline_batch_json(JSON.stringify(polys))) as Vec2[][];
    polys.forEach((q, i) => {
      const t = offsetPolyline(q.pts, q.d, q.closed);
      expect(wPoly[i].length, `offPoly-len#${i}`).toBe(t.length);
      t.forEach((p, j) => expect(closeVec(wPoly[i][j], p), `offPoly#${i}.${j}`).toBe(true));
    });
  });

  it("filletCorner (Struktur exakt + Werte, Winkel abs 1e-7)", () => {
    const rng = mulberry32(8);
    // Halb kontrolliert (garantiert gueltige Verrundung), halb Zufall (None-Paritaet).
    const qs = Array.from({ length: N }, (_, i) => {
      if (i % 2 === 0) {
        const corner = v(rng);
        const a0 = rng() * Math.PI * 2;
        const half = 0.3 + rng() * 0.9; // theta = 2*half ∈ [0.6, 2.4] rad (weg von 0/π)
        const legLen = 3 + rng() * 7;
        const rMax = legLen * Math.tan(half);
        const r = 0.1 + rng() * 0.8 * rMax;
        const dir = (ang: number) => ({ x: Math.cos(ang), y: Math.sin(ang) });
        const d1 = dir(a0 + half);
        const d2 = dir(a0 - half);
        return {
          corner,
          p1: { x: corner.x + d1.x * legLen, y: corner.y + d1.y * legLen },
          p2: { x: corner.x + d2.x * legLen, y: corner.y + d2.y * legLen },
          r,
        };
      }
      return { corner: v(rng), p1: v(rng), p2: v(rng), r: 0.1 + rng() * 5 };
    });
    const w = JSON.parse(K.fillet_corner_batch_json(JSON.stringify(qs))) as (Fillet | null)[];
    let valid = 0;
    qs.forEach((q, i) => {
      const t = filletCorner(q.corner, q.p1, q.p2, q.r);
      expect(w[i] === null, `fil-null#${i}`).toBe(t === null);
      if (t && w[i]) {
        const f = w[i]!;
        expect(closeVec(f.center, t.center), `fil-center#${i}`).toBe(true);
        expect(closeVec(f.tangentA, t.tangentA), `fil-tA#${i}`).toBe(true);
        expect(closeVec(f.tangentB, t.tangentB), `fil-tB#${i}`).toBe(true);
        expect(closeNum(f.radius, t.radius), `fil-r#${i}`).toBe(true);
        expect(Math.abs(f.startAngle - t.startAngle) <= 1e-7, `fil-sa#${i}`).toBe(true);
        expect(Math.abs(f.endAngle - t.endAngle) <= 1e-7, `fil-ea#${i}`).toBe(true);
        valid++;
      }
    });
    expect(valid, "keine gueltige Verrundung — Test waere aussagelos").toBeGreaterThan(50);
  });

  it("splitSegmentByCutters / trimSegment (Struktur + Werte)", () => {
    const rng = mulberry32(9);
    const qs = Array.from({ length: N }, () => ({
      a1: { x: -60, y: (rng() * 2 - 1) * 40 },
      a2: { x: 60, y: (rng() * 2 - 1) * 40 },
      cutters: genCutters(rng),
      pick: { x: (rng() * 2 - 1) * 60, y: (rng() * 2 - 1) * 40 },
    }));
    const wSplit = JSON.parse(K.split_segment_by_cutters_batch_json(JSON.stringify(qs))) as [Vec2, Vec2][][];
    const wTrim = JSON.parse(K.trim_segment_batch_json(JSON.stringify(qs))) as [Vec2, Vec2][][];
    qs.forEach((q, i) => {
      expect(eqPairs(wSplit[i], splitSegmentByCutters(q.a1, q.a2, q.cutters)), `split#${i}`).toBe(true);
      expect(eqPairs(wTrim[i], trimSegment(q.a1, q.a2, q.cutters, q.pick)), `trim#${i}`).toBe(true);
    });
  });

  it("trimPolyline / splitAtIntersections (offen+geschlossen, Struktur exakt)", () => {
    const rng = mulberry32(10);
    const qs = Array.from({ length: N }, () => ({
      pts: genPolyline(rng, 4, 9),
      closed: rng() < 0.5,
      cutters: genCutters(rng),
      pick: { x: (rng() * 2 - 1) * 50, y: (rng() * 2 - 1) * 50 },
      others: genCutters(rng).map((c) => c.pts),
    }));
    const wTrim = JSON.parse(K.trim_polyline_batch_json(JSON.stringify(qs))) as Poly[][];
    const wSplit = JSON.parse(
      K.split_at_intersections_batch_json(
        JSON.stringify(qs.map((q) => ({ targetPts: q.pts, closed: q.closed, others: q.others }))),
      ),
    ) as Vec2[][][];
    qs.forEach((q, i) => {
      expect(eqPolyList(wTrim[i], trimPolyline(q.pts, q.closed, q.cutters, q.pick)), `trimPoly#${i}`).toBe(true);
      expect(eqPtsLists(wSplit[i], splitAtIntersections(q.pts, q.closed, q.others)), `splitAt#${i}`).toBe(true);
    });
  });

  it("splitPolylineAtParam / splitClosedByChord / removeSegment", () => {
    const rng = mulberry32(11);
    const qs = Array.from({ length: N }, () => {
      const pts = genPolyline(rng, 4, 9);
      const n = pts.length;
      const closed = rng() < 0.5;
      return { pts, closed, edgeIndex: Math.floor(rng() * (closed ? n : n - 1)), t: rng() };
    });
    const wSap = JSON.parse(K.split_polyline_at_param_batch_json(JSON.stringify(qs))) as Vec2[][][];
    const wRem = JSON.parse(
      K.remove_segment_batch_json(
        JSON.stringify(qs.map((q) => ({ pts: q.pts, closed: q.closed, edgeIndex: q.edgeIndex }))),
      ),
    ) as Poly[];
    const chords = Array.from({ length: N }, () => {
      const pts = genPolyline(rng, 4, 8);
      const n = pts.length;
      const i = Math.floor(rng() * n);
      let j = Math.floor(rng() * n);
      if (j === i) j = (j + 1) % n;
      return { pts, i, ti: rng(), j, tj: rng() };
    });
    const wChord = JSON.parse(K.split_closed_by_chord_batch_json(JSON.stringify(chords))) as ([Vec2[], Vec2[]] | null)[];
    qs.forEach((q, i) => {
      expect(eqPtsLists(wSap[i], splitPolylineAtParam(q.pts, q.closed, q.edgeIndex, q.t)), `sap#${i}`).toBe(true);
      const tr = removeSegment(q.pts, q.closed, q.edgeIndex);
      expect(wRem[i].closed === tr.closed && eqPts(wRem[i].pts, tr.pts), `rem#${i}`).toBe(true);
    });
    chords.forEach((q, i) => {
      const t = splitClosedByChord(q.pts, q.i, q.ti, q.j, q.tj);
      expect((wChord[i] === null) === (t === null), `chord-null#${i}`).toBe(true);
      if (t && wChord[i]) {
        expect(eqPts(wChord[i]![0], t[0]) && eqPts(wChord[i]![1], t[1]), `chord#${i}`).toBe(true);
      }
    });
  });

  it("extendSegment (start/end, null-Paritaet + Werte)", () => {
    const rng = mulberry32(12);
    const qs = Array.from({ length: N }, (_, k) => {
      const c = { x: (rng() * 2 - 1) * 30, y: (rng() * 2 - 1) * 30 };
      return {
        a1: { x: c.x - 1, y: c.y },
        a2: { x: c.x + 1, y: c.y },
        end: (k % 2 === 0 ? "end" : "start") as "start" | "end",
        cutters: genCutters(rng).concat([
          { pts: [{ x: c.x + 5, y: -50 }, { x: c.x + 5, y: 50 }], closed: false },
        ]),
      };
    });
    const w = JSON.parse(K.extend_segment_batch_json(JSON.stringify(qs))) as ([Vec2, Vec2] | null)[];
    qs.forEach((q, i) => {
      const t = extendSegment(q.a1, q.a2, q.end, q.cutters);
      expect((w[i] === null) === (t === null), `ext-null#${i}`).toBe(true);
      if (t && w[i]) expect(closeVec(w[i]![0], t[0]) && closeVec(w[i]![1], t[1]), `ext#${i}`).toBe(true);
    });
  });

  it("joinChains (mergeable + Zufall, Struktur exakt)", () => {
    const rng = mulberry32(13);
    const groups = Array.from({ length: N }, () => {
      const base = genPolyline(rng, 4, 7);
      const chains: Poly[] = [];
      for (let k = 0; k < base.length - 1; k++) {
        const seg = [base[k], base[k + 1]];
        chains.push({ pts: rng() < 0.5 ? seg : [seg[1], seg[0]], closed: false });
      }
      if (rng() < 0.5) chains.push({ pts: genPolyline(rng, 2, 3), closed: false });
      for (let k = chains.length - 1; k > 0; k--) {
        const j = Math.floor(rng() * (k + 1));
        [chains[k], chains[j]] = [chains[j], chains[k]];
      }
      return chains;
    });
    const w = JSON.parse(K.join_chains_batch_json(JSON.stringify(groups))) as Poly[][];
    groups.forEach((g, i) => {
      expect(eqPolyList(w[i], joinChains(g)), `join#${i}`).toBe(true);
    });
  });
});

// ── Slice 1: roomArea / ceiling ───────────────────────────────────────────────

/** Zufaelliges konvexes Polygon mit n=3..10 Ecken in [-30,30]×[-30,30]. */
function genConvexPoly(rng: () => number, n: number): Vec2[] {
  // Einfache Methode: Einheitswinkel sortieren + skalieren.
  const angles = Array.from({ length: n }, () => rng() * 2 * Math.PI);
  angles.sort((a, b) => a - b);
  const rx = 1 + rng() * 20;
  const ry = 1 + rng() * 20;
  const cx = (rng() * 2 - 1) * 10;
  const cy = (rng() * 2 - 1) * 10;
  return angles.map((a) => ({ x: cx + rx * Math.cos(a), y: cy + ry * Math.sin(a) }));
}

describe.skipIf(!built)("kernel2d Rust-WASM ⇄ TS Paritaet — Slice 1: roomArea + ceiling", () => {
  it("polygonArea / perimeter / centroid (Zufallspolygone 3–10 Ecken, rel 1e-9)", () => {
    const rng = mulberry32(20);
    const polys = Array.from({ length: N }, () => {
      const n = 3 + Math.floor(rng() * 8);
      return genConvexPoly(rng, n);
    });
    const wArea = JSON.parse(K.polygon_area_batch_json(JSON.stringify(polys))) as number[];
    const wPerim = JSON.parse(K.perimeter_batch_json(JSON.stringify(polys))) as number[];
    const wCent = JSON.parse(K.centroid_batch_json(JSON.stringify(polys))) as Vec2[];

    polys.forEach((p, i) => {
      const a = polygonArea(p);
      const pe = perimeter(p);
      const c = centroid(p);
      expect(closeNum(wArea[i], a), `area#${i}`).toBe(true);
      expect(closeNum(wPerim[i], pe), `perim#${i}`).toBe(true);
      expect(closeVec(wCent[i], c), `centroid#${i}`).toBe(true);
    });
  });

  it("normalizeOutline: Struktur exakt (null↔null) + Werte + None-Paritaet", () => {
    const rng = mulberry32(21);
    // Haelfte: gueltige Polygone; Haelfte: degenerierte (< 3 Punkte oder kollinear).
    const inputs = Array.from({ length: N }, (_, i) => {
      if (i % 4 < 3) {
        const n = 3 + Math.floor(rng() * 8);
        return genConvexPoly(rng, n);
      }
      // Degeneriert: 0, 1 oder 2 Punkte.
      const k = Math.floor(rng() * 3);
      return Array.from({ length: k }, () => ({ x: rng() * 10, y: rng() * 10 }));
    });
    const w = JSON.parse(K.normalize_outline_batch_json(JSON.stringify(inputs))) as (Vec2[] | null)[];
    inputs.forEach((p, i) => {
      const t = normalizeOutline(p);
      expect(w[i] === null, `norm-null#${i}`).toBe(t === null);
      if (t !== null && w[i] !== null) {
        const wp = w[i]!;
        expect(wp.length, `norm-len#${i}`).toBe(t.length);
        t.forEach((pt, j) => expect(closeVec(wp[j], pt), `norm-pt#${i}.${j}`).toBe(true));
      }
    });
  });

  it("isValidOutline / ceilingArea (Struktur exakt + Werte)", () => {
    const rng = mulberry32(22);
    const polys = Array.from({ length: N }, (_, i) => {
      if (i % 5 < 4) return genConvexPoly(rng, 3 + Math.floor(rng() * 8));
      return Array.from({ length: Math.floor(rng() * 3) }, () => ({ x: rng() * 5, y: rng() * 5 }));
    });
    const wValid = JSON.parse(K.is_valid_outline_batch_json(JSON.stringify(polys))) as boolean[];
    const wArea = JSON.parse(K.ceiling_area_batch_json(JSON.stringify(polys))) as number[];

    polys.forEach((p, i) => {
      expect(wValid[i], `valid#${i}`).toBe(isValidOutline(p));
      expect(closeNum(wArea[i], ceilingArea(p)), `ceilArea#${i}`).toBe(true);
    });
  });

  it("outlineBBox / outlineCentroid (Werte rel 1e-9)", () => {
    const rng = mulberry32(23);
    const polys = Array.from({ length: N }, () => genConvexPoly(rng, 3 + Math.floor(rng() * 8)));
    const wBBox = JSON.parse(K.outline_bbox_batch_json(JSON.stringify(polys))) as {
      minX: number;
      minY: number;
      maxX: number;
      maxY: number;
    }[];
    const wCent = JSON.parse(K.outline_centroid_batch_json(JSON.stringify(polys))) as Vec2[];

    polys.forEach((p, i) => {
      const bb = outlineBBox(p);
      const c = outlineCentroid(p);
      expect(closeNum(wBBox[i].minX, bb.minX), `bbox-minX#${i}`).toBe(true);
      expect(closeNum(wBBox[i].minY, bb.minY), `bbox-minY#${i}`).toBe(true);
      expect(closeNum(wBBox[i].maxX, bb.maxX), `bbox-maxX#${i}`).toBe(true);
      expect(closeNum(wBBox[i].maxY, bb.maxY), `bbox-maxY#${i}`).toBe(true);
      expect(closeVec(wCent[i], c), `outCentroid#${i}`).toBe(true);
    });
  });

  it("pointInOutline (Struktur exakt: true↔true, Zufallspunkte innen+aussen)", () => {
    const rng = mulberry32(24);
    const qs = Array.from({ length: N }, () => {
      const n = 3 + Math.floor(rng() * 8);
      const outline = genConvexPoly(rng, n);
      // Schwerpunkt des Polygons als garantiert innerer Punkt.
      const cx = outline.reduce((s, p) => s + p.x, 0) / outline.length;
      const cy = outline.reduce((s, p) => s + p.y, 0) / outline.length;
      const p = rng() < 0.5 ? { x: cx, y: cy } : { x: (rng() * 2 - 1) * 50, y: (rng() * 2 - 1) * 50 };
      return { p, outline };
    });
    const w = JSON.parse(K.point_in_outline_batch_json(JSON.stringify(qs))) as boolean[];
    qs.forEach((q, i) => {
      expect(w[i], `pio#${i}`).toBe(pointInOutline(q.p, q.outline));
    });
  });
});

// ── Slice 2: stair ────────────────────────────────────────────────────────────

/** Erzeugt einen einfachen Stair-Stub mit geometrisch relevanten Feldern. */
function genStair(
  rng: () => number,
  shape: "straight" | "L" | "spiral",
): Stair & { totalRise: number } {
  const start = { x: (rng() * 2 - 1) * 10, y: (rng() * 2 - 1) * 10 };
  const angle = rng() * Math.PI * 2;
  const dir = { x: Math.cos(angle), y: Math.sin(angle) };
  const runLength = 1 + rng() * 5;
  const width = 0.8 + rng() * 1.5;
  const stepCount = 3 + Math.floor(rng() * 12);
  const totalRise = 0.5 + rng() * 3;
  const up = rng() < 0.5 ? true : false;
  const base: Stair = {
    id: "test",
    type: "stair",
    floorId: "f1",
    categoryCode: "40",
    shape,
    start,
    dir,
    runLength,
    width,
    stepCount,
    up,
  };
  if (shape === "L") {
    return { ...base, run2Length: 1 + rng() * 4, turn: rng() < 0.5 ? 1 : -1, totalRise };
  }
  if (shape === "spiral") {
    return {
      ...base,
      center: { x: start.x + rng() * 2, y: start.y + rng() * 2 },
      radius: 0.5 + rng() * 2,
      sweep: 90 + rng() * 270,
      totalRise,
    };
  }
  return { ...base, totalRise };
}

/** Prueft Tritt-Listen-Gleichheit: Laenge, dann je Tritt index/baseRise/topRise/pts. */
function eqTreads(
  w: { pts: Vec2[]; index: number; topRise: number; baseRise: number }[],
  t: TSStairGeometry["treads"],
): boolean {
  if (w.length !== t.length) return false;
  return t.every((tr, i) => {
    const wtr = w[i];
    if (wtr.index !== tr.index) return false;
    if (!closeNum(wtr.topRise, tr.topRise)) return false;
    if (!closeNum(wtr.baseRise, tr.baseRise)) return false;
    if (!eqPts(wtr.pts, tr.pts)) return false;
    return true;
  });
}

describe.skipIf(!built)("kernel2d Rust-WASM ⇄ TS Paritaet — Slice 2: stair", () => {
  it("defaultStepCount (Struktur exakt + Werte)", () => {
    const rng = mulberry32(30);
    const qs = Array.from({ length: N }, () => ({
      totalRise: 0.1 + rng() * 5,
      runLength: rng() * 8,
    }));
    const w = JSON.parse(K.default_step_count_batch_json(JSON.stringify(qs))) as number[];
    qs.forEach((q, i) => {
      expect(w[i], `dsc#${i}`).toBe(defaultStepCount(q.totalRise, q.runLength));
    });
  });

  it("stairGeometry gerade: Tritte + Werte (Struktur exakt, rel 1e-9)", () => {
    const rng = mulberry32(31);
    const qs = Array.from({ length: 100 }, () => {
      const s = genStair(rng, "straight");
      return { stair: s, totalRise: s.totalRise };
    });
    const wGeos = JSON.parse(K.stair_geometry_batch_json(JSON.stringify(qs))) as {
      treads: { pts: Vec2[]; index: number; topRise: number; baseRise: number }[];
      landing: Vec2[] | null;
      riserHeight: number;
      treadDepth: number;
      runLine: Vec2[];
      totalRise: number;
    }[];
    qs.forEach((q, i) => {
      const t = stairGeometry(q.stair, q.totalRise);
      const w = wGeos[i];
      expect(eqTreads(w.treads, t.treads), `treads#${i}`).toBe(true);
      expect(w.landing === null, `landing-null#${i}`).toBe(t.landing === null);
      expect(closeNum(w.riserHeight, t.riserHeight), `riser#${i}`).toBe(true);
      expect(closeNum(w.treadDepth, t.treadDepth), `depth#${i}`).toBe(true);
      expect(eqPts(w.runLine, t.runLine), `runLine#${i}`).toBe(true);
    });
  });

  it("stairGeometry L + spiral: Struktur exakt + Werte", () => {
    const rng = mulberry32(32);
    const lqs = Array.from({ length: 50 }, () => {
      const s = genStair(rng, "L");
      return { stair: s, totalRise: s.totalRise };
    });
    const spiralqs = Array.from({ length: 50 }, () => {
      const s = genStair(rng, "spiral");
      return { stair: s, totalRise: s.totalRise };
    });
    const wL = JSON.parse(K.stair_geometry_batch_json(JSON.stringify(lqs))) as {
      treads: { pts: Vec2[]; index: number; topRise: number; baseRise: number }[];
      landing: Vec2[] | null;
      riserHeight: number;
      treadDepth: number;
      runLine: Vec2[];
    }[];
    lqs.forEach((q, i) => {
      const t = stairGeometry(q.stair, q.totalRise);
      const w = wL[i];
      expect(eqTreads(w.treads, t.treads), `L-treads#${i}`).toBe(true);
      expect(w.landing !== null, `L-landing#${i}`).toBe(t.landing !== null);
      if (t.landing && w.landing) expect(eqPts(w.landing, t.landing), `L-landingPts#${i}`).toBe(true);
      expect(eqPts(w.runLine, t.runLine), `L-runLine#${i}`).toBe(true);
    });
    const wSp = JSON.parse(K.stair_geometry_batch_json(JSON.stringify(spiralqs))) as typeof wL;
    spiralqs.forEach((q, i) => {
      const t = stairGeometry(q.stair, q.totalRise);
      const w = wSp[i];
      expect(eqTreads(w.treads, t.treads), `sp-treads#${i}`).toBe(true);
      expect(eqPts(w.runLine, t.runLine), `sp-runLine#${i}`).toBe(true);
    });
  });

  it("stairCut (Struktur exakt: Indizes + breakLine-null)", () => {
    const rng = mulberry32(33);
    const qs = Array.from({ length: 100 }, () => {
      const s = genStair(rng, "straight");
      const geo = stairGeometry(s, s.totalRise);
      const cutRise = rng() * s.totalRise * 1.1;
      return { geo, cutRise };
    });
    const w = JSON.parse(K.stair_cut_batch_json(JSON.stringify(qs))) as {
      belowIndices: number[];
      aboveIndices: number[];
      breakLine: [Vec2, Vec2][] | null;
    }[];
    qs.forEach((q, i) => {
      const t = stairCut(q.geo, q.cutRise);
      expect(w[i].belowIndices.length, `cut-below-len#${i}`).toBe(t.belowIndices.length);
      expect(w[i].aboveIndices.length, `cut-above-len#${i}`).toBe(t.aboveIndices.length);
      expect(w[i].belowIndices, `cut-below#${i}`).toEqual(t.belowIndices);
      expect(w[i].aboveIndices, `cut-above#${i}`).toEqual(t.aboveIndices);
      expect(w[i].breakLine === null, `cut-break-null#${i}`).toBe(t.breakLine === null);
    });
  });

  it("stairBBox (Werte rel 1e-9)", () => {
    const rng = mulberry32(34);
    const geos = Array.from({ length: 100 }, () => {
      const s = genStair(rng, "straight");
      return stairGeometry(s, s.totalRise);
    });
    const w = JSON.parse(K.stair_bbox_batch_json(JSON.stringify(geos))) as {
      minX: number; minY: number; maxX: number; maxY: number;
    }[];
    geos.forEach((g, i) => {
      const t = stairBBox(g);
      expect(closeNum(w[i].minX, t.minX), `bbox-minX#${i}`).toBe(true);
      expect(closeNum(w[i].minY, t.minY), `bbox-minY#${i}`).toBe(true);
      expect(closeNum(w[i].maxX, t.maxX), `bbox-maxX#${i}`).toBe(true);
      expect(closeNum(w[i].maxY, t.maxY), `bbox-maxY#${i}`).toBe(true);
    });
  });

  it("pointInPolygon / pointHitsStair (Struktur exakt)", () => {
    const rng = mulberry32(35);
    const pipQs = Array.from({ length: N }, () => {
      const n = 3 + Math.floor(rng() * 8);
      const poly = genConvexPoly(rng, n);
      const cx = poly.reduce((s, p) => s + p.x, 0) / poly.length;
      const cy = poly.reduce((s, p) => s + p.y, 0) / poly.length;
      const p = rng() < 0.5 ? { x: cx, y: cy } : { x: (rng() * 2 - 1) * 50, y: (rng() * 2 - 1) * 50 };
      return { p, poly };
    });
    const wPip = JSON.parse(K.point_in_polygon_batch_json(JSON.stringify(pipQs))) as boolean[];
    pipQs.forEach((q, i) => {
      expect(wPip[i], `pip#${i}`).toBe(stairPointInPolygon(q.p, q.poly));
    });

    const phsQs = Array.from({ length: 50 }, () => {
      const s = genStair(rng, "straight");
      const geo = stairGeometry(s, s.totalRise);
      const p = { x: s.start.x + rng() * 6 - 1, y: s.start.y + (rng() * 2 - 1) * 2 };
      return { p, geo };
    });
    const wPhs = JSON.parse(K.point_hits_stair_batch_json(JSON.stringify(phsQs))) as boolean[];
    phsQs.forEach((q, i) => {
      expect(wPhs[i], `phs#${i}`).toBe(pointHitsStair(q.p, q.geo));
    });
  });
});

// ── Slice 3: roomBoundary ─────────────────────────────────────────────────────

/** Erzeugt eine Menge rechtwinkliger Wandsegmente, die ein einfaches Rechteck
 *  (oder L-Form) bilden. */
function genRectWalls(rng: () => number): WallSegment[] {
  const x0 = (rng() * 2 - 1) * 5;
  const y0 = (rng() * 2 - 1) * 5;
  const w = 2 + rng() * 5;
  const h = 2 + rng() * 5;
  const t = 0.1 + rng() * 0.3;
  return [
    { a: { x: x0, y: y0 }, b: { x: x0 + w, y: y0 }, thickness: t },
    { a: { x: x0 + w, y: y0 }, b: { x: x0 + w, y: y0 + h }, thickness: t },
    { a: { x: x0 + w, y: y0 + h }, b: { x: x0, y: y0 + h }, thickness: t },
    { a: { x: x0, y: y0 + h }, b: { x: x0, y: y0 }, thickness: t },
  ];
}

/** Erzeugt WallFace-Liste aus einem geschlossenen Polygon. */
function polyToFaces(pts: Vec2[]): WallFace[] {
  const n = pts.length;
  return Array.from({ length: n }, (_, i) => ({ a: pts[i], b: pts[(i + 1) % n] }));
}

describe.skipIf(!built)("kernel2d Rust-WASM ⇄ TS Paritaet — Slice 3: roomBoundary", () => {
  it("rbPointInPolygon (roomBoundary-Variante, kein Nenner-Guard, Struktur exakt)", () => {
    const rng = mulberry32(40);
    const qs = Array.from({ length: N }, () => {
      const n = 3 + Math.floor(rng() * 8);
      const poly = genConvexPoly(rng, n);
      const cx = poly.reduce((s, p) => s + p.x, 0) / poly.length;
      const cy = poly.reduce((s, p) => s + p.y, 0) / poly.length;
      const p = rng() < 0.5 ? { x: cx, y: cy } : { x: (rng() * 2 - 1) * 50, y: (rng() * 2 - 1) * 50 };
      return { p, poly };
    });
    const w = JSON.parse(K.rb_point_in_polygon_batch_json(JSON.stringify(qs))) as boolean[];
    qs.forEach((q, i) => {
      expect(w[i], `rbpip#${i}`).toBe(rbPointInPolygon(q.p, q.poly));
    });
  });

  it("detectRooms — Rechteck-Grundriss: gleiche Anzahl + Reihenfolge Raeume", () => {
    const rng = mulberry32(41);
    const qs = Array.from({ length: 50 }, () => ({
      walls: genRectWalls(rng),
      gapTol: 0.05,
      minArea: 0.05,
      offsetToInner: true,
    }));
    const w = JSON.parse(K.detect_rooms_batch_json(JSON.stringify(qs))) as Vec2[][][];
    qs.forEach((q, i) => {
      const t = detectRooms(q.walls, { gapTol: q.gapTol, minArea: q.minArea, offsetToInner: q.offsetToInner });
      // Gleiche Anzahl Raeume.
      expect(w[i].length, `dr-count#${i}`).toBe(t.length);
      // Je Raum: gleiche Punktanzahl (Reihenfolge-abhaengig, aber deterministisch).
      t.forEach((room, j) => {
        expect(w[i][j]?.length, `dr-roomLen#${i}.${j}`).toBe(room.length);
        room.forEach((pt, k) => expect(closeVec(w[i][j][k], pt), `dr-pt#${i}.${j}.${k}`).toBe(true));
      });
    });
  });

  it("detectRooms ohne Offset + degeneriert (< 3 Waende → leer)", () => {
    const rng = mulberry32(42);
    const qs = Array.from({ length: 30 }, () => ({
      walls: genRectWalls(rng),
      gapTol: 0.05,
      minArea: 0.05,
      offsetToInner: false,
    }));
    const w = JSON.parse(K.detect_rooms_batch_json(JSON.stringify(qs))) as Vec2[][][];
    qs.forEach((q, i) => {
      const t = detectRooms(q.walls, { gapTol: q.gapTol, minArea: q.minArea, offsetToInner: q.offsetToInner });
      expect(w[i].length, `drno-count#${i}`).toBe(t.length);
    });
    // Degeneriert: < 3 Waende → leer.
    const deg = [{ walls: [{ a: { x: 0, y: 0 }, b: { x: 1, y: 0 }, thickness: 0.2 }], gapTol: 0.05, minArea: 0.05, offsetToInner: true }];
    const wd = JSON.parse(K.detect_rooms_batch_json(JSON.stringify(deg))) as Vec2[][][];
    expect(wd[0].length).toBe(detectRooms(deg[0].walls, {}).length);
    expect(wd[0].length).toBe(0);
  });

  it("roomFromPointInside — Schwerpunkt als Saatpunkt findet den Raum (nicht null)", () => {
    const rng = mulberry32(43);
    const qs = Array.from({ length: 50 }, () => {
      const walls = genRectWalls(rng);
      // Mittelpunkt des Rechtecks als Saatpunkt.
      const xs = walls.flatMap((w) => [w.a.x, w.b.x]);
      const ys = walls.flatMap((w) => [w.a.y, w.b.y]);
      const cx = (Math.min(...xs) + Math.max(...xs)) / 2;
      const cy = (Math.min(...ys) + Math.max(...ys)) / 2;
      return { point: { x: cx, y: cy }, walls, gapTol: 0.05, offsetToInner: true };
    });
    const w = JSON.parse(K.room_from_point_inside_batch_json(JSON.stringify(qs))) as (Vec2[] | null)[];
    qs.forEach((q, i) => {
      const t = roomFromPointInside(q.point, q.walls, { gapTol: q.gapTol, offsetToInner: q.offsetToInner });
      expect(w[i] === null, `rfpi-null#${i}`).toBe(t === null);
      if (t !== null && w[i] !== null) {
        expect(w[i]!.length, `rfpi-len#${i}`).toBe(t.length);
        t.forEach((pt, j) => expect(closeVec(w[i]![j], pt), `rfpi-pt#${i}.${j}`).toBe(true));
      }
    });
  });

  it("roomFromPointInsideFaces — Struktur exakt (null↔null, Laenge, Punkte)", () => {
    const rng = mulberry32(44);
    const qs = Array.from({ length: 30 }, () => {
      const n = 4 + Math.floor(rng() * 4);
      const poly = genConvexPoly(rng, n);
      const faces = polyToFaces(poly);
      const cx = poly.reduce((s, p) => s + p.x, 0) / poly.length;
      const cy = poly.reduce((s, p) => s + p.y, 0) / poly.length;
      const inOrOut = rng() < 0.7 ? { x: cx, y: cy } : { x: (rng() * 2 - 1) * 40, y: (rng() * 2 - 1) * 40 };
      return { point: inOrOut, wallFaces: faces, gapTol: 0.05 };
    });
    const w = JSON.parse(K.room_from_point_inside_faces_batch_json(JSON.stringify(qs))) as (Vec2[] | null)[];
    qs.forEach((q, i) => {
      const t = roomFromPointInsideFaces(q.point, q.wallFaces, q.gapTol);
      expect(w[i] === null, `rfpif-null#${i}`).toBe(t === null);
      if (t !== null && w[i] !== null) {
        expect(w[i]!.length, `rfpif-len#${i}`).toBe(t.length);
        t.forEach((pt, j) => expect(closeVec(w[i]![j], pt), `rfpif-pt#${i}.${j}`).toBe(true));
      }
    });
  });
});

// ── Slice 4: opening ──────────────────────────────────────────────────────────
// `opening.ts` ist model-gekoppelt (Wall/Opening/Project) — die geometrisch
// reinen Kerne wurden mit abgeflachter Signatur (Vec2/number) portiert
// (PORT_PLAN §1). `thickness`/`refOffset` werden hier — wie beim Rust-Port —
// bereits aufgeloest uebergeben (`wallTypeThickness`/`wallReferenceOffset`,
// nicht ueber `getWallType`/ein volles `Project` im WASM-Aufruf).

/** Zufaellige, NICHT entartete Wand (Laenge 0.5..15.5 m). */
function genWall(rng: () => number): Wall {
  const start = v(rng);
  const angle = rng() * Math.PI * 2;
  const length = 0.5 + rng() * 15;
  const end = { x: start.x + Math.cos(angle) * length, y: start.y + Math.sin(angle) * length };
  const refs = ["left", "center", "right"] as const;
  return {
    id: "w1",
    type: "wall",
    floorId: "f1",
    categoryCode: "20",
    start,
    end,
    wallTypeId: "wt1",
    height: 2.4,
    referenceLine: refs[Math.floor(rng() * 3)],
  };
}

/** Einschichtiger WallType mit zufaelliger Dicke (0.15..0.5 m). */
function genWallType(rng: () => number): WallType {
  return { id: "wt1", name: "wt", layers: [{ componentId: "c1", thickness: 0.15 + rng() * 0.35 }] };
}

/** Minimales Project — `getWallType` liest nur `wallTypes`. */
function makeProject(wallType: WallType): Project {
  return { wallTypes: [wallType] } as unknown as Project;
}

/** Zufaellige Oeffnung auf `wall`: position/width teils ausserhalb der Achse
 *  oder negativ (Strukturabdeckung fuer den null-Zweig von `openingInterval`). */
function genOpening(rng: () => number, wall: Wall, kind: "window" | "door"): Opening {
  const axis = wallAxisLength(wall);
  const position = (rng() * 1.6 - 0.3) * axis;
  const width = rng() < 0.1 ? -0.5 + rng() * 0.5 : 0.3 + rng() * Math.max(0.3, axis);
  const swings = ["left", "right"] as const;
  const hinges = ["start", "end"] as const;
  const dirs = ["in", "out"] as const;
  return {
    id: "o1",
    type: "opening",
    hostWallId: wall.id,
    categoryCode: "21",
    kind,
    position,
    width,
    height: 1.2,
    sillHeight: kind === "door" ? 0 : 0.8,
    wingCount: rng() < 0.5 ? undefined : 1 + Math.floor(rng() * 4),
    hinge: rng() < 0.3 ? undefined : hinges[Math.floor(rng() * 2)],
    swing: rng() < 0.3 ? undefined : swings[Math.floor(rng() * 2)],
    swingAngle: rng() < 0.3 ? undefined : 30 + rng() * 120,
    openingDir: rng() < 0.3 ? undefined : dirs[Math.floor(rng() * 2)],
  };
}

describe.skipIf(!built)("kernel2d Rust-WASM ⇄ TS Paritaet — Slice 4: opening", () => {
  it("wallAxisLength (Zufallswaende, rel 1e-9)", () => {
    const rng = mulberry32(50);
    const walls = Array.from({ length: N }, () => genWall(rng));
    const qs = walls.map((wl) => ({ start: wl.start, end: wl.end }));
    const w = JSON.parse(K.wall_axis_length_batch_json(JSON.stringify(qs))) as number[];
    walls.forEach((wl, i) => {
      expect(closeNum(w[i], wallAxisLength(wl)), `axisLen#${i}`).toBe(true);
    });
  });

  it("openingInterval: Struktur exakt (null-Zweig inklusive) + Werte", () => {
    const rng = mulberry32(51);
    const cases = Array.from({ length: N }, () => {
      const wall = genWall(rng);
      return { wall, o: genOpening(rng, wall, "window") };
    });
    const qs = cases.map(({ wall, o }) => ({
      start: wall.start,
      end: wall.end,
      position: o.position,
      width: o.width,
    }));
    const w = JSON.parse(K.opening_interval_batch_json(JSON.stringify(qs))) as (
      | { from: number; to: number }
      | null
    )[];
    cases.forEach(({ wall, o }, i) => {
      const t = openingInterval(wall, o);
      expect(w[i] === null, `iv-null#${i}`).toBe(t === null);
      if (t && w[i]) {
        expect(closeNum(w[i]!.from, t.from), `iv-from#${i}`).toBe(true);
        expect(closeNum(w[i]!.to, t.to), `iv-to#${i}`).toBe(true);
      }
    });
  });

  it("wallAxisFrame (u/n, rel 1e-9)", () => {
    const rng = mulberry32(52);
    const walls = Array.from({ length: N }, () => genWall(rng));
    const qs = walls.map((wl) => ({ start: wl.start, end: wl.end }));
    const w = JSON.parse(K.wall_axis_frame_batch_json(JSON.stringify(qs))) as { u: Vec2; n: Vec2 }[];
    walls.forEach((wl, i) => {
      const t = wallAxisFrame(wl);
      expect(closeVec(w[i].u, t.u), `frame-u#${i}`).toBe(true);
      expect(closeVec(w[i].n, t.n), `frame-n#${i}`).toBe(true);
    });
  });

  it("openingJambs / openingCenter: Struktur exakt (null-Zweig) + Werte", () => {
    const rng = mulberry32(53);
    const cases = Array.from({ length: N }, () => {
      const wall = genWall(rng);
      return { wall, o: genOpening(rng, wall, "window") };
    });
    const qs = cases.map(({ wall, o }) => ({
      start: wall.start,
      end: wall.end,
      position: o.position,
      width: o.width,
    }));
    const wJ = JSON.parse(K.opening_jambs_batch_json(JSON.stringify(qs))) as (
      | { jambStart: Vec2; jambEnd: Vec2 }
      | null
    )[];
    const wC = JSON.parse(K.opening_center_batch_json(JSON.stringify(qs))) as (Vec2 | null)[];
    cases.forEach(({ wall, o }, i) => {
      const tJ = openingJambs(wall, o);
      expect(wJ[i] === null, `jambs-null#${i}`).toBe(tJ === null);
      if (tJ && wJ[i]) {
        expect(closeVec(wJ[i]!.jambStart, tJ.jambStart), `jambs-start#${i}`).toBe(true);
        expect(closeVec(wJ[i]!.jambEnd, tJ.jambEnd), `jambs-end#${i}`).toBe(true);
      }
      const tC = openingCenter(wall, o);
      expect(wC[i] === null, `center-null#${i}`).toBe(tC === null);
      if (tC && wC[i]) expect(closeVec(wC[i]!, tC), `center#${i}`).toBe(true);
    });
  });

  it("openingGapQuad / windowSymbol: Struktur exakt + Werte (rel 1e-9)", () => {
    const rng = mulberry32(54);
    const cases = Array.from({ length: 150 }, () => {
      const wall = genWall(rng);
      const wallType = genWallType(rng);
      wall.wallTypeId = wallType.id;
      const project = makeProject(wallType);
      const o = genOpening(rng, wall, "window");
      const total = wallTypeThickness(wallType);
      const refOffset = wallReferenceOffset(wall, total);
      const glassCount = 1 + Math.floor(rng() * 3);
      return { wall, o, project, total, refOffset, glassCount };
    });

    const qsGap = cases.map((c) => ({
      start: c.wall.start,
      end: c.wall.end,
      position: c.o.position,
      width: c.o.width,
      thickness: c.total,
      refOffset: c.refOffset,
    }));
    const wGap = JSON.parse(K.opening_gap_quad_batch_json(JSON.stringify(qsGap))) as (Vec2[] | null)[];
    cases.forEach((c, i) => {
      const t = openingGapQuad(c.project, c.wall, c.o);
      expect(wGap[i] === null, `gap-null#${i}`).toBe(t === null);
      if (t && wGap[i]) expect(eqPts(wGap[i]!, t), `gap-pts#${i}`).toBe(true);
    });

    const qsWin = cases.map((c) => ({
      start: c.wall.start,
      end: c.wall.end,
      position: c.o.position,
      width: c.o.width,
      thickness: c.total,
      refOffset: c.refOffset,
      glassCount: c.glassCount,
      wingCount: c.o.wingCount,
    }));
    const wWin = JSON.parse(K.window_symbol_batch_json(JSON.stringify(qsWin))) as (WindowSymbol | null)[];
    cases.forEach((c, i) => {
      const t = windowSymbol(c.project, c.wall, c.o, c.glassCount);
      expect(wWin[i] === null, `win-null#${i}`).toBe(t === null);
      if (t && wWin[i]) {
        expect(eqPts(wWin[i]!.frame, t.frame), `win-frame#${i}`).toBe(true);
        expect(eqPairs(wWin[i]!.glassLines, t.glassLines), `win-glass#${i}`).toBe(true);
        expect(eqPairs(wWin[i]!.mullionLines, t.mullionLines), `win-mullion#${i}`).toBe(true);
      }
    });
  });

  it("doorSymbol: Struktur exakt + Werte (Default-Parameter inklusive)", () => {
    const rng = mulberry32(55);
    const cases = Array.from({ length: 150 }, () => {
      const wall = genWall(rng);
      return { wall, o: genOpening(rng, wall, "door") };
    });
    const qs = cases.map(({ wall, o }) => ({
      start: wall.start,
      end: wall.end,
      position: o.position,
      width: o.width,
      swing: o.swing,
      openingDir: o.openingDir,
      hinge: o.hinge,
      swingAngle: o.swingAngle,
    }));
    const w = JSON.parse(K.door_symbol_batch_json(JSON.stringify(qs))) as (DoorSymbol | null)[];
    cases.forEach(({ wall, o }, i) => {
      const t = doorSymbol(wall, o);
      expect(w[i] === null, `door-null#${i}`).toBe(t === null);
      if (t && w[i]) {
        expect(closeVec(w[i]!.hinge, t.hinge), `door-hinge#${i}`).toBe(true);
        expect(closeVec(w[i]!.openEnd, t.openEnd), `door-open#${i}`).toBe(true);
        expect(closeVec(w[i]!.closedEnd, t.closedEnd), `door-closed#${i}`).toBe(true);
        expect(closeNum(w[i]!.radius, t.radius), `door-radius#${i}`).toBe(true);
        expect(closeVec(w[i]!.jambStart, t.jambStart), `door-jambStart#${i}`).toBe(true);
        expect(closeVec(w[i]!.jambEnd, t.jambEnd), `door-jambEnd#${i}`).toBe(true);
        expect(closeVec(w[i]!.normal, t.normal), `door-normal#${i}`).toBe(true);
      }
    });
  });

  it("Golden: entartetes Intervall (Breite 0 / ausserhalb der Achse) → null; wingCount-Clamp >4 → 3 Pfosten", () => {
    const wall: Wall = {
      id: "w",
      type: "wall",
      floorId: "f",
      categoryCode: "20",
      start: { x: 0, y: 0 },
      end: { x: 5, y: 0 },
      wallTypeId: "wt",
      height: 2.4,
      referenceLine: "center",
    };
    const wallType: WallType = { id: "wt", name: "wt", layers: [{ componentId: "c", thickness: 0.24 }] };
    const project = makeProject(wallType);

    // Breite 0 → entartet.
    const oZero: Opening = {
      id: "o",
      type: "opening",
      hostWallId: "w",
      categoryCode: "21",
      kind: "window",
      position: 2,
      width: 0,
      height: 1.2,
      sillHeight: 0.8,
    };
    const ivZero = JSON.parse(
      K.opening_interval_batch_json(
        JSON.stringify([{ start: wall.start, end: wall.end, position: oZero.position, width: oZero.width }]),
      ),
    ) as ({ from: number; to: number } | null)[];
    expect(ivZero[0] === null).toBe(true);
    expect(ivZero[0] === null).toBe(openingInterval(wall, oZero) === null);

    // Vollstaendig ausserhalb der Achse.
    const oOut: Opening = { ...oZero, position: 10, width: 1 };
    const ivOut = JSON.parse(
      K.opening_interval_batch_json(
        JSON.stringify([{ start: wall.start, end: wall.end, position: oOut.position, width: oOut.width }]),
      ),
    ) as ({ from: number; to: number } | null)[];
    expect(ivOut[0] === null).toBe(true);
    expect(ivOut[0] === null).toBe(openingInterval(wall, oOut) === null);

    // wingCount > 4 wird auf 4 geklemmt → 3 Mittelpfosten.
    const total = wallTypeThickness(wallType);
    const refOffset = wallReferenceOffset(wall, total);
    const oWings: Opening = {
      id: "o",
      type: "opening",
      hostWallId: "w",
      categoryCode: "21",
      kind: "window",
      position: 1,
      width: 2,
      height: 1.2,
      sillHeight: 0.8,
      wingCount: 9,
    };
    const wWin = JSON.parse(
      K.window_symbol_batch_json(
        JSON.stringify([
          {
            start: wall.start,
            end: wall.end,
            position: oWings.position,
            width: oWings.width,
            thickness: total,
            refOffset,
            glassCount: 1,
            wingCount: oWings.wingCount,
          },
        ]),
      ),
    ) as (WindowSymbol | null)[];
    const tWin = windowSymbol(project, wall, oWings, 1);
    expect(tWin).not.toBeNull();
    expect(wWin[0]).not.toBeNull();
    expect(wWin[0]!.mullionLines.length).toBe(tWin!.mullionLines.length);
    expect(wWin[0]!.mullionLines.length).toBe(3);
    expect(eqPairs(wWin[0]!.mullionLines, tWin!.mullionLines)).toBe(true);
  });
});

describe.skipIf(!built)("kernel2d Rust-WASM ⇄ TS Paritaet — Golden (Grenzfaelle)", () => {
  it("parallele/kollineare Strecken → null (beide)", () => {
    const qs = [
      { a1: { x: 0, y: 0 }, a2: { x: 2, y: 0 }, b1: { x: 0, y: 1 }, b2: { x: 2, y: 1 } }, // parallel
      { a1: { x: 0, y: 0 }, a2: { x: 4, y: 0 }, b1: { x: 1, y: 0 }, b2: { x: 3, y: 0 } }, // kollinear
      { a1: { x: 0, y: 0 }, a2: { x: 0, y: 0 }, b1: { x: 1, y: 1 }, b2: { x: 2, y: 2 } }, // Null-Laenge
    ];
    const w = JSON.parse(K.segment_intersect_batch_json(JSON.stringify(qs))) as (Hit | null)[];
    qs.forEach((q, i) => {
      const t = segmentIntersect(q.a1, q.a2, q.b1, q.b2);
      expect(w[i] === null, `#${i}`).toBe(t === null);
      if (t && w[i]) expect(closeHit(w[i]!, t)).toBe(true);
    });
  });

  it("tangentiale Gerade → genau 1 Punkt; konzentrische/getrennte Kreise → leer", () => {
    const lc = [{ a: { x: -1, y: 1 }, b: { x: 1, y: 1 }, center: { x: 0, y: 0 }, r: 1 }]; // Tangente
    const wlc = JSON.parse(K.line_circle_intersect_batch_json(JSON.stringify(lc))) as Vec2[][];
    expect(wlc[0].length).toBe(lineCircleIntersect(lc[0].a, lc[0].b, lc[0].center, lc[0].r).length);
    expect(wlc[0].length).toBe(1);

    const cc = [
      { c1: { x: 0, y: 0 }, r1: 1, c2: { x: 0, y: 0 }, r2: 2 }, // konzentrisch → leer
      { c1: { x: 0, y: 0 }, r1: 1, c2: { x: 5, y: 0 }, r2: 1 }, // getrennt → leer
      { c1: { x: 0, y: 0 }, r1: 2, c2: { x: 1, y: 0 }, r2: 1 }, // innen tangential → 1
    ];
    const wcc = JSON.parse(K.circle_circle_intersect_batch_json(JSON.stringify(cc))) as Vec2[][];
    cc.forEach((q, i) => {
      const t = circleCircleIntersect(q.c1, q.r1, q.c2, q.r2);
      expect(wcc[i].length, `cc#${i}`).toBe(t.length);
      t.forEach((p, j) => expect(closeVec(wcc[i][j], p)).toBe(true));
    });
  });

  it("degeneriertes Null-Flaeche-Polygon (kollinear) → Flaeche 0, nicht CCW", () => {
    const polys = [[{ x: 0, y: 0 }, { x: 1, y: 1 }, { x: 2, y: 2 }]];
    const wArea = JSON.parse(K.signed_area_batch_json(JSON.stringify(polys))) as number[];
    const wCcw = JSON.parse(K.is_ccw_batch_json(JSON.stringify(polys))) as boolean[];
    expect(closeNum(wArea[0], signedArea(polys[0]))).toBe(true);
    expect(Math.abs(wArea[0])).toBeLessThan(1e-12);
    expect(wCcw[0]).toBe(isCCW(polys[0]));
  });

  it("Offset L-Ecke (Gehrung) und Fillet rechter Winkel / kollinear→null / zu gross→null", () => {
    const off = [{ pts: [{ x: 0, y: 0 }, { x: 1, y: 0 }, { x: 1, y: 1 }], d: 0.5, closed: false }];
    const wOff = JSON.parse(K.offset_polyline_batch_json(JSON.stringify(off))) as Vec2[][];
    const tOff = offsetPolyline(off[0].pts, off[0].d, off[0].closed);
    expect(wOff[0].length).toBe(tOff.length);
    tOff.forEach((p, j) => expect(closeVec(wOff[0][j], p)).toBe(true));

    const fil = [
      { corner: { x: 0, y: 0 }, p1: { x: 5, y: 0 }, p2: { x: 0, y: 5 }, r: 1 }, // rechter Winkel
      { corner: { x: 0, y: 0 }, p1: { x: 1, y: 0 }, p2: { x: -1, y: 0 }, r: 0.5 }, // kollinear → null
      { corner: { x: 0, y: 0 }, p1: { x: 0.1, y: 0 }, p2: { x: 0, y: 0.1 }, r: 10 }, // zu gross → null
    ];
    const wFil = JSON.parse(K.fillet_corner_batch_json(JSON.stringify(fil))) as (Fillet | null)[];
    fil.forEach((q, i) => {
      const t = filletCorner(q.corner, q.p1, q.p2, q.r);
      expect(wFil[i] === null, `golden-fil#${i}`).toBe(t === null);
      if (t && wFil[i]) {
        expect(closeVec(wFil[i]!.center, t.center)).toBe(true);
        expect(closeVec(wFil[i]!.tangentA, t.tangentA)).toBe(true);
      }
    });
  });
});
