import { describe, expect, it } from "vitest";
import { AnimationClip as ThreeClip, Bone, SkinnedMesh } from "three";
import { GLTFLoader } from "three/examples/jsm/loaders/GLTFLoader.js";
import { assembleCharacter } from "./assemble.js";
import { CHARACTER_PRESETS } from "./presets.js";
import { listParts } from "./parts.js";

function parseGlb(glb: Uint8Array) {
  const loader = new GLTFLoader();
  const buffer = glb.buffer.slice(glb.byteOffset, glb.byteOffset + glb.byteLength) as ArrayBuffer;
  return new Promise<import("three/examples/jsm/loaders/GLTFLoader.js").GLTF>((resolve, reject) => {
    loader.parse(buffer, "", resolve, reject);
  });
}

describe("character creator", () => {
  it("exposes hair / upper / lower / feet / accessory parts", () => {
    expect(listParts("hair").length).toBeGreaterThan(2);
    expect(listParts("upper").length).toBeGreaterThan(3);
    expect(listParts("lower").length).toBeGreaterThan(1);
    expect(listParts("feet").length).toBeGreaterThan(1);
    expect(listParts("accessory").length).toBeGreaterThan(1);
  });

  it("assembles male and female presets into loadable skinned GLBs", async () => {
    const male = assembleCharacter(CHARACTER_PRESETS.find((p) => p.id === "survivor")!);
    const female = assembleCharacter(CHARACTER_PRESETS.find((p) => p.id === "researcher")!);

    expect(male.stats.triangles).toBeGreaterThan(400);
    expect(female.stats.triangles).toBeGreaterThan(400);
    expect(male.recipe.body).toBe("male");
    expect(female.recipe.body).toBe("female");

    for (const result of [male, female]) {
      const gltf = await parseGlb(result.glb);
      let skinned: SkinnedMesh | null = null;
      gltf.scene.traverse((object) => {
        if ((object as SkinnedMesh).isSkinnedMesh) skinned = object as SkinnedMesh;
      });
      expect(skinned).not.toBeNull();
      expect(skinned!.skeleton.bones.length).toBe(22);
      expect(gltf.animations.some((clip: ThreeClip) => clip.name === "Walk")).toBe(true);
      expect(gltf.animations.some((clip: ThreeClip) => clip.name === "Slumped")).toBe(true);

      const bones = new Map<string, Bone>();
      gltf.scene.traverse((object) => {
        if ((object as Bone).isBone) bones.set(object.name, object as Bone);
      });
      expect(bones.get("LeftForeArm")?.parent?.name).toBe("LeftArm");
    }
  });

  it("rejects body-locked parts on the wrong sex", () => {
    expect(() =>
      assembleCharacter({
        id: "bad",
        name: "Bad",
        body: "male",
        height: 1.7,
        palette: CHARACTER_PRESETS[0]!.palette,
        parts: { lower: "lower.skirt" },
      }),
    ).toThrow(/female/);
  });

  it("mass/muscle/fat sliders change torso girth", () => {
    const base = CHARACTER_PRESETS.find((p) => p.id === "survivor")!;
    const bare = {
      hair: "hair.none",
      upper: "upper.none",
      lower: "lower.none",
      feet: "feet.none",
      accessory: "accessory.none",
    } as const;
    const lean = assembleCharacter({
      ...base,
      id: "lean",
      bodyStyle: { mass: -1, muscle: -0.5, fat: 0 },
      parts: { ...bare },
    });
    const heavy = assembleCharacter({
      ...base,
      id: "heavy",
      bodyStyle: { mass: 1, muscle: 1, fat: 1 },
      parts: { ...bare },
    });

    // A-pose arms dominate mid-height X. Shoulder girdle (just below neck)
    // is where mass/muscle/fat expand the torso loft most clearly.
    const girdleHalfW = (positions: number[], height: number) => {
      const y0 = height * 0.78;
      const y1 = height * 0.86;
      let m = 0;
      for (let i = 0; i < positions.length; i += 3) {
        const y = positions[i + 1]!;
        if (y < y0 || y > y1) continue;
        m = Math.max(m, Math.abs(positions[i]!));
      }
      return m;
    };

    const leanW = girdleHalfW(lean.mesh.positions, base.height);
    const heavyW = girdleHalfW(heavy.mesh.positions, base.height);
    expect(heavyW).toBeGreaterThan(leanW * 1.35);
  });
});
