import { describe, expect, it } from "vitest";
import { assembleCharacter } from "./assemble.js";
import { buildBaseBody } from "./body.js";
import { applyCoverage, PART } from "./coverage.js";
import { CHARACTER_PRESETS } from "./presets.js";
import { measurementsFor } from "./proportions.js";
import { buildSkeleton } from "../rig/skeleton.js";
import { vertexCount } from "../mesh/MeshBuilder.js";

const palette = CHARACTER_PRESETS[0]!.palette;

function bareMale() {
  const skeleton = buildSkeleton(measurementsFor("male"), { height: 1.78 });
  return { skeleton, mesh: buildBaseBody(skeleton, "male", palette, null) };
}

describe("body region tags", () => {
  it("tags every base body vertex with a region", () => {
    const { mesh } = bareMale();
    const n = vertexCount(mesh);
    expect(mesh.parts.length).toBe(n);
    const untagged = mesh.parts.filter((p) => p === PART.NONE).length;
    expect(untagged).toBe(0);
    // Both arms, both legs, both feet, plus head / neck / torso.
    expect(new Set(mesh.parts).size).toBe(9);
  });

  it("runs t from 0 to 1 along each region", () => {
    const { mesh } = bareMale();
    for (const part of [PART.TORSO, PART.ARM_L, PART.LEG_R, PART.HEAD]) {
      const ts = mesh.ts.filter((_, i) => mesh.parts[i] === part);
      expect(Math.min(...ts)).toBeLessThanOrEqual(0.05);
      expect(Math.max(...ts)).toBeGreaterThanOrEqual(0.95);
    }
  });
});

describe("hiding skin under clothing", () => {
  it("leaves the body untouched when nothing is worn", () => {
    const { mesh } = bareMale();
    const result = applyCoverage(mesh, []);
    expect(result.mesh).toBe(mesh);
    expect(result.dropped).toBe(0);
  });

  it("drops enclosed triangles and keeps the rest", () => {
    const { mesh } = bareMale();
    const before = mesh.indices.length / 3;
    const result = applyCoverage(mesh, [{ part: PART.TORSO, tMin: -0.01, tMax: 0.97 }]);

    expect(result.dropped).toBeGreaterThan(0);
    expect(result.kept).toBeGreaterThan(0);
    expect(result.kept + result.recessed + result.dropped).toBe(before);
    expect(result.mesh.indices.length / 3).toBe(result.kept + result.recessed);

    // Nothing outside the claimed region may be dropped: the head still has to
    // be a closed surface after a shirt hides the chest.
    const heads = result.mesh.parts.filter((p) => p === PART.HEAD).length;
    const headsBefore = mesh.parts.filter((p) => p === PART.HEAD).length;
    expect(heads).toBe(headsBefore);
  });

  it("recesses the rim inward rather than dropping it", () => {
    const { mesh } = bareMale();
    // A band ending mid-torso: the skin around t=0.5 is the collar of the hole.
    const result = applyCoverage(mesh, [{ part: PART.TORSO, tMin: -0.01, tMax: 0.5 }], {
      recessDepth: 0.02,
    });
    expect(result.recessed).toBeGreaterThan(0);

    // Recessed skin must sit inside the original surface, never outside it.
    const radius = (m: typeof mesh, i: number) =>
      Math.hypot(m.positions[i * 3]!, m.positions[i * 3 + 2]!);
    let moved = 0;
    for (let i = 0; i < vertexCount(result.mesh); i++) {
      if (result.mesh.parts[i] !== PART.TORSO) continue;
      const t = result.mesh.ts[i]!;
      if (t < 0.35 || t > 0.5) continue;
      const original = mesh.ts.findIndex(
        (v, j) => mesh.parts[j] === PART.TORSO && Math.abs(v - t) < 1e-6,
      );
      if (original < 0) continue;
      if (radius(result.mesh, i) < radius(mesh, original) - 1e-4) moved++;
    }
    expect(moved).toBeGreaterThan(0);
  });

  it("hides several hundred triangles on a fully dressed preset", () => {
    const dressed = assembleCharacter(CHARACTER_PRESETS.find((p) => p.id === "survivor")!);
    expect(dressed.stats.skinHidden).toBeGreaterThan(200);
    // The recess exists so openings read as skin, so it must not be empty
    // either — a figure with every rim dropped shows hollow cuffs.
    expect(dressed.stats.skinRecessed).toBeGreaterThan(0);
  });
});
