import { readdirSync } from "node:fs";
import path from "node:path";
import { fileURLToPath } from "node:url";
import { getBounds, NodeIO } from "@gltf-transform/core";
import { ALL_EXTENSIONS } from "@gltf-transform/extensions";
import { MeshoptDecoder } from "meshoptimizer";
import { describe, expect, it } from "vitest";
import {
  CHARACTER_BACK_PROP_MODEL_IDS,
  CHARACTER_WEAPON_MODEL_IDS,
  CLAUDECRAFT_WEAPON_CATALOG,
  LEGACY_WEAPON_ID_ALIASES,
  TWO_HANDED_WEAPON_IDS,
  heldItemForModel,
  isWeaponCatalogId,
  preferredWeaponAssetFile,
  resolveCharacterWeaponModelId,
  resolveWeaponCatalogId,
  weaponDefinition,
  weaponDefinitionsForSlot,
  weaponIdsForCategory,
  weaponIdsForGrip,
  weaponIdsForSlot,
  weaponSupportsSlot,
  weaponUsesBothHands,
} from "./weaponCatalog";

const RUNTIME_DIRECTORY = fileURLToPath(
  new URL("../assets/game/models/claudecraft/weapons", import.meta.url),
);

function glbFiles(directory: string) {
  return readdirSync(directory)
    .filter((file) => file.endsWith(".glb"))
    .sort();
}

describe("Claudecraft weapon catalog", () => {
  it("catalogs every checked-in canonical asset exactly once", () => {
    const ids = CLAUDECRAFT_WEAPON_CATALOG.map((entry) => entry.id);
    const files = CLAUDECRAFT_WEAPON_CATALOG.map((entry) => entry.sourceFile);
    const canonicalRuntimeFiles = glbFiles(RUNTIME_DIRECTORY)
      .filter((file) => !file.endsWith("-uastc.glb"));

    expect(CLAUDECRAFT_WEAPON_CATALOG).toHaveLength(55);
    expect(new Set(ids)).toHaveLength(55);
    expect(new Set(files)).toHaveLength(55);
    expect([...files].sort()).toEqual(canonicalRuntimeFiles);
  });

  it("references every optimized variant exactly once", () => {
    const optimizedFiles = CLAUDECRAFT_WEAPON_CATALOG
      .map((entry) => entry.optimizedFile)
      .filter((file): file is NonNullable<typeof file> => file !== null)
      .sort();
    const optimizedRuntimeFiles = glbFiles(RUNTIME_DIRECTORY)
      .filter((file) => file.endsWith("-uastc.glb"));

    expect(optimizedFiles).toHaveLength(7);
    expect(new Set(optimizedFiles)).toHaveLength(7);
    expect(optimizedFiles).toEqual(optimizedRuntimeFiles);
  });

  it("has tracked runtime copies for every referenced source asset", () => {
    const referencedFiles = CLAUDECRAFT_WEAPON_CATALOG
      .flatMap((entry) => entry.optimizedFile
        ? [entry.sourceFile, entry.optimizedFile]
        : [entry.sourceFile])
      .sort();

    expect(glbFiles(RUNTIME_DIRECTORY)).toEqual(referencedFiles);
  });

  it("keeps category coverage stable", () => {
    expect(Object.fromEntries([
      "axe",
      "crossbow",
      "dagger",
      "halberd",
      "hammer",
      "quiver",
      "scythe",
      "shield",
      "spear",
      "spellbook",
      "staff",
      "sword",
      "wand",
    ].map((category) => [category, weaponIdsForCategory(category as Parameters<typeof weaponIdsForCategory>[0]).length]))).toEqual({
      axe: 9,
      crossbow: 3,
      dagger: 5,
      halberd: 1,
      hammer: 4,
      quiver: 1,
      scythe: 1,
      shield: 4,
      spear: 1,
      spellbook: 1,
      staff: 8,
      sword: 13,
      wand: 4,
    });
  });

  it("derives held items while keeping back-only props separate", () => {
    expect(CHARACTER_WEAPON_MODEL_IDS).toHaveLength(54);
    expect(CHARACTER_WEAPON_MODEL_IDS).not.toContain("cc/quiver");
    expect(CHARACTER_BACK_PROP_MODEL_IDS).toEqual(["cc/quiver"]);
    expect(heldItemForModel("cc/adv_dagger")).toEqual({
      modelId: "cc/adv_dagger",
      grip: "dagger",
    });
    expect(weaponDefinitionsForSlot("back").map((entry) => entry.id)).toEqual(["cc/quiver"]);
  });

  it("identifies valid two-handed and long-weapon models", () => {
    expect(TWO_HANDED_WEAPON_IDS).toHaveLength(18);
    expect(new Set(TWO_HANDED_WEAPON_IDS)).toHaveLength(18);
    expect(TWO_HANDED_WEAPON_IDS.every((id) => isWeaponCatalogId(id))).toBe(true);
    expect(TWO_HANDED_WEAPON_IDS.every((id) => CHARACTER_WEAPON_MODEL_IDS.includes(id))).toBe(true);
    expect(weaponUsesBothHands("cc/staff_d")).toBe(true);
    expect(weaponUsesBothHands("cc/crossbow_1handed")).toBe(false);
    expect(TWO_HANDED_WEAPON_IDS).not.toContain("cc/quiver");
  });

  it("filters by slot, category, and attachment grip", () => {
    expect(weaponSupportsSlot("cc/adv_sword_1handed", "main")).toBe(true);
    expect(weaponSupportsSlot("cc/adv_sword_1handed", "off")).toBe(true);
    expect(weaponSupportsSlot("cc/adv_sword_2handed", "off")).toBe(false);
    expect(weaponIdsForSlot("off")).toContain("cc/shield_round");
    expect(weaponIdsForCategory("scythe")).toEqual(["cc/scythe"]);
    expect(weaponIdsForGrip("polearm")).toEqual([
      "cc/halberd",
      "cc/scythe",
      "cc/spear_a",
    ]);
    expect(weaponDefinition("cc/wand_b").category).toBe("wand");
  });

  it("resolves canonical and legacy save IDs without accepting prototype keys", () => {
    expect(LEGACY_WEAPON_ID_ALIASES).toEqual({
      "druid-staff": "cc/adv_druid_staff",
      sword: "cc/adv_sword_1handed",
      shield: "cc/shield_badge",
      wand: "cc/adv_wand",
      spellbook: "cc/spellbook_open",
    });
    expect(resolveWeaponCatalogId("cc/sword_g")).toBe("cc/sword_g");
    expect(resolveWeaponCatalogId("sword")).toBe("cc/adv_sword_1handed");
    expect(resolveCharacterWeaponModelId("spellbook")).toBe("cc/spellbook_open");
    expect(resolveCharacterWeaponModelId("cc/quiver")).toBeNull();
    expect(resolveWeaponCatalogId("toString")).toBeNull();
    expect(isWeaponCatalogId("__proto__")).toBe(false);
    expect(resolveWeaponCatalogId(null)).toBeNull();
  });

  it("prefers UASTC only when a catalog entry provides it", () => {
    expect(preferredWeaponAssetFile("cc/adv_wand")).toBe("adv_wand-uastc.glb");
    expect(preferredWeaponAssetFile("cc/adv_wand", false)).toBe("adv_wand.glb");
    expect(preferredWeaponAssetFile("cc/wand_a")).toBe("wand_a.glb");
  });

  it("keeps every runtime design finite, static, and inside the mobile geometry budget", async () => {
    await MeshoptDecoder.ready;
    const io = new NodeIO()
      .registerExtensions(ALL_EXTENSIONS)
      .registerDependencies({ "meshopt.decoder": MeshoptDecoder });

    for (const definition of CLAUDECRAFT_WEAPON_CATALOG) {
      const runtimeFiles = definition.optimizedFile
        ? [definition.sourceFile, definition.optimizedFile]
        : [definition.sourceFile];
      for (const runtimeFile of runtimeFiles) {
        const contractLabel = `${definition.id} (${runtimeFile})`;
        const document = await io.read(path.join(RUNTIME_DIRECTORY, runtimeFile));
        const root = document.getRoot();
        const scene = root.listScenes()[0];
        const bounds = getBounds(scene);
        const values = [...bounds.min, ...bounds.max];
        const triangleCount = root.listMeshes().reduce((meshTotal, mesh) => meshTotal
          + mesh.listPrimitives().reduce((primitiveTotal, primitive) => {
            const elementCount = primitive.getIndices()?.getCount()
              ?? primitive.getAttribute("POSITION")?.getCount()
              ?? 0;
            return primitiveTotal + Math.floor(elementCount / 3);
          }, 0), 0);

        expect(values.every(Number.isFinite), contractLabel).toBe(true);
        expect(bounds.max.some((value, index) => value > bounds.min[index]), contractLabel).toBe(true);
        expect(triangleCount, contractLabel).toBeGreaterThan(0);
        expect(triangleCount, contractLabel).toBeLessThanOrEqual(2_000);
        expect(root.listAnimations(), contractLabel).toHaveLength(0);
        expect(root.listSkins(), contractLabel).toHaveLength(0);
      }
    }
  });
});
