import { describe, expect, it } from "vitest";
import {
  box,
  computeFlatNormals,
  createMesh,
  domeShell,
  loft,
  tube,
  vertexCount,
  type MeshData,
  type Vec3Tuple,
} from "./MeshBuilder.js";

const COLOR = { r: 180, g: 140, b: 100 };

/** Mean of triangle normals dotted with (centroid − meshCentre). Outward > 0. */
function outwardScore(mesh: MeshData): number {
  computeFlatNormals(mesh);

  let cx = 0;
  let cy = 0;
  let cz = 0;
  const n = vertexCount(mesh);
  for (let i = 0; i < n; i++) {
    cx += mesh.positions[i * 3]!;
    cy += mesh.positions[i * 3 + 1]!;
    cz += mesh.positions[i * 3 + 2]!;
  }
  cx /= n;
  cy /= n;
  cz /= n;

  let score = 0;
  let faces = 0;
  for (let i = 0; i < mesh.indices.length; i += 3) {
    const a = mesh.indices[i]! * 3;
    const b = mesh.indices[i + 1]! * 3;
    const c = mesh.indices[i + 2]! * 3;

    const px = (mesh.positions[a]! + mesh.positions[b]! + mesh.positions[c]!) / 3;
    const py = (mesh.positions[a + 1]! + mesh.positions[b + 1]! + mesh.positions[c + 1]!) / 3;
    const pz = (mesh.positions[a + 2]! + mesh.positions[b + 2]! + mesh.positions[c + 2]!) / 3;

    // Use the geometric normal of the triangle (not the averaged vertex normal)
    // so the score measures winding, not smoothing.
    const abx = mesh.positions[b]! - mesh.positions[a]!;
    const aby = mesh.positions[b + 1]! - mesh.positions[a + 1]!;
    const abz = mesh.positions[b + 2]! - mesh.positions[a + 2]!;
    const acx = mesh.positions[c]! - mesh.positions[a]!;
    const acy = mesh.positions[c + 1]! - mesh.positions[a + 1]!;
    const acz = mesh.positions[c + 2]! - mesh.positions[a + 2]!;
    let nx = aby * acz - abz * acy;
    let ny = abz * acx - abx * acz;
    let nz = abx * acy - aby * acx;
    const len = Math.hypot(nx, ny, nz) || 1;
    nx /= len;
    ny /= len;
    nz /= len;

    score += nx * (px - cx) + ny * (py - cy) + nz * (pz - cz);
    faces++;
  }

  return faces > 0 ? score / faces : 0;
}

describe("mesh winding", () => {
  it("emits outward-facing fronts on a vertical tube", () => {
    const mesh = createMesh();
    const from: Vec3Tuple = [0, 0, 0];
    const to: Vec3Tuple = [0, 1, 0];
    tube(mesh, from, to, 0.2, 0.15, 0.8, COLOR, { sides: 6, sections: 2 });
    expect(outwardScore(mesh)).toBeGreaterThan(0.05);
  });

  it("emits outward-facing fronts on a diagonal tube (A-pose arm case)", () => {
    const mesh = createMesh();
    tube(mesh, [0, 1, 0], [0.4, 0.4, 0.1], 0.08, 0.06, 0.9, COLOR, { sides: 6, sections: 3 });
    expect(outwardScore(mesh)).toBeGreaterThan(0.05);
  });

  it("emits outward-facing fronts on a keyframed loft (ludus torso path)", () => {
    const mesh = createMesh();
    loft(
      mesh,
      [
        { t: 0, c: [0, 0.9, 0], rx: 0.14, rz: 0.1 },
        { t: 0.4, c: [0, 1.1, 0.01], rx: 0.11, rz: 0.08 },
        { t: 0.75, c: [0, 1.3, 0.015], rx: 0.16, rz: 0.1 },
        { t: 1, c: [0, 1.45, 0.02], rx: 0.06, rz: 0.055 },
      ],
      COLOR,
      { sides: 6, rings: 8 },
    );
    expect(outwardScore(mesh)).toBeGreaterThan(0.05);
  });

  it("emits outward-facing fronts on an axis-aligned box", () => {
    const mesh = createMesh();
    box(mesh, [0, 0.5, 0], [0.2, 0.3, 0.15], COLOR);
    expect(outwardScore(mesh)).toBeGreaterThan(0.05);
  });

  it("emits outward-facing fronts on a hair dome shell", () => {
    const mesh = createMesh();
    // Angled skull axis (hairline → crown), face +Z — same as the head mesh.
    const from: Vec3Tuple = [0, 1.52, 0.02];
    const to: Vec3Tuple = [0, 1.66, 0.01];
    domeShell(mesh, from, to, 0.1, 0.1, COLOR, {
      sections: 4,
      sides: 8,
      frontScale: 0.92,
      frontScaleTop: 0.7,
      backScale: 1.1,
      topScale: 0.82,
      faceDir: [0, 0, 1],
    });
    expect(outwardScore(mesh)).toBeGreaterThan(0.04);

    // Crown fan specifically: average normal near the apex should point along +axis.
    const axis: Vec3Tuple = [to[0] - from[0], to[1] - from[1], to[2] - from[2]];
    const al = Math.hypot(axis[0], axis[1], axis[2]) || 1;
    const axisN: Vec3Tuple = [axis[0] / al, axis[1] / al, axis[2] / al];
    const apex: Vec3Tuple = [to[0] + axisN[0] * 0.01, to[1] + axisN[1] * 0.01, to[2] + axisN[2] * 0.01];
    let score = 0;
    let faces = 0;
    for (let i = 0; i < mesh.indices.length; i += 3) {
      const ia = mesh.indices[i]! * 3;
      const ib = mesh.indices[i + 1]! * 3;
      const ic = mesh.indices[i + 2]! * 3;
      const px = (mesh.positions[ia]! + mesh.positions[ib]! + mesh.positions[ic]!) / 3;
      const py = (mesh.positions[ia + 1]! + mesh.positions[ib + 1]! + mesh.positions[ic + 1]!) / 3;
      const pz = (mesh.positions[ia + 2]! + mesh.positions[ib + 2]! + mesh.positions[ic + 2]!) / 3;
      if (Math.hypot(px - apex[0], py - apex[1], pz - apex[2]) > 0.08) continue;
      const abx = mesh.positions[ib]! - mesh.positions[ia]!;
      const aby = mesh.positions[ib + 1]! - mesh.positions[ia + 1]!;
      const abz = mesh.positions[ib + 2]! - mesh.positions[ia + 2]!;
      const acx = mesh.positions[ic]! - mesh.positions[ia]!;
      const acy = mesh.positions[ic + 1]! - mesh.positions[ia + 1]!;
      const acz = mesh.positions[ic + 2]! - mesh.positions[ia + 2]!;
      let nx = aby * acz - abz * acy;
      let ny = abz * acx - abx * acz;
      let nz = abx * acy - aby * acx;
      const len = Math.hypot(nx, ny, nz) || 1;
      score += (nx / len) * axisN[0] + (ny / len) * axisN[1] + (nz / len) * axisN[2];
      faces++;
    }
    expect(faces).toBeGreaterThan(0);
    expect(score / faces).toBeGreaterThan(0.2);
  });
});
