import { fileURLToPath } from "node:url";
import { NodeIO, type Document } from "@gltf-transform/core";
import { ALL_EXTENSIONS } from "@gltf-transform/extensions";
import { MeshoptDecoder } from "meshoptimizer";
import { beforeAll, describe, expect, it } from "vitest";
import { CHARACTER_PART_CATALOG } from "./characterAppearance";
import type { MemberId } from "./types";

const BODY_ASSETS = ["druid", "mage", "ranger", "knight", "rogue"] as const;
const REQUIRED_SHARED_CLIPS = ["Death_A", "Hit_A", "Idle", "Running_A", "Walking_A", "Spellcasting"];
const REQUIRED_RIG_NODES = ["root", "hips", "spine", "chest", "head", "handslot.r", "handslot.l"];
const ASSET_BY_MEMBER: Record<MemberId, typeof BODY_ASSETS[number]> = {
  aelia: "druid",
  brann: "knight",
  nia: "ranger",
  orin: "mage",
  vale: "rogue",
};

const io = new NodeIO()
  .registerExtensions(ALL_EXTENSIONS)
  .registerDependencies({ "meshopt.decoder": MeshoptDecoder });

function assetPath(name: typeof BODY_ASSETS[number]) {
  return fileURLToPath(new URL(`../assets/game/models/claudecraft/chars/players/${name}-uastc.glb`, import.meta.url));
}

describe("shared healer animation rig assets", () => {
  const documents = new Map<typeof BODY_ASSETS[number], Document>();

  beforeAll(async () => {
    await MeshoptDecoder.ready;
    await Promise.all(BODY_ASSETS.map(async (name) => {
      documents.set(name, await io.read(assetPath(name)));
    }));
  });

  it("keeps the same named joint and socket contract on every healer body", () => {
    const druidSkin = documents.get("druid")!.getRoot().listSkins()[0];
    const druidJoints = druidSkin.listJoints().map((joint) => joint.getName()).sort();
    const druidRestPose = new Map(druidSkin.listJoints().map((joint) => [joint.getName(), {
      parent: joint.getParentNode()?.getName() ?? null,
      translation: joint.getTranslation(),
      rotation: joint.getRotation(),
      scale: joint.getScale(),
    }]));

    for (const name of BODY_ASSETS) {
      const root = documents.get(name)!.getRoot();
      const nodeNames = new Set(root.listNodes().map((node) => node.getName()));
      for (const requiredNode of REQUIRED_RIG_NODES) expect(nodeNames.has(requiredNode), `${name}:${requiredNode}`).toBe(true);
      for (const skin of root.listSkins()) {
        expect(skin.listJoints().map((joint) => joint.getName()).sort(), name).toEqual(druidJoints);
        for (const joint of skin.listJoints()) {
          expect({
            parent: joint.getParentNode()?.getName() ?? null,
            translation: joint.getTranslation(),
            rotation: joint.getRotation(),
            scale: joint.getScale(),
          }, `${name}:${joint.getName()}`).toEqual(druidRestPose.get(joint.getName()));
        }
      }
    }
  });

  it("lets every body resolve all targets from Aelia's shared animation set", () => {
    const druidRoot = documents.get("druid")!.getRoot();
    const clipNames = new Set(druidRoot.listAnimations().map((animation) => animation.getName()));
    for (const clipName of REQUIRED_SHARED_CLIPS) expect(clipNames.has(clipName), clipName).toBe(true);

    const animationTargets = new Set(druidRoot.listAnimations().flatMap((animation) =>
      animation.listChannels().map((channel) => channel.getTargetNode()?.getName()).filter((name): name is string => Boolean(name)),
    ));
    for (const name of BODY_ASSETS) {
      const nodeNames = new Set(documents.get(name)!.getRoot().listNodes().map((node) => node.getName()));
      const missingTargets = [...animationTargets].filter((target) => !nodeNames.has(target));
      expect(missingTargets, name).toEqual([]);
    }
  });

  it("keeps every modular catalog node present in its source GLB", () => {
    for (const [partId, definition] of Object.entries(CHARACTER_PART_CATALOG)) {
      const assetName = ASSET_BY_MEMBER[definition.sourceMemberId];
      const nodes = documents.get(assetName)!.getRoot().listNodes();
      for (const nodeName of definition.nodeNames) {
        const node = nodes.find((candidate) => candidate.getName() === nodeName);
        expect(node, `${partId}:${nodeName}`).toBeDefined();
        expect(node?.getMesh(), `${partId}:${nodeName}:mesh`).not.toBeNull();
        expect(node?.getSkin(), `${partId}:${nodeName}:skin`).not.toBeNull();
      }
    }
  });
});
