import { describe, expect, it } from "vitest";
import { SaveRepository, type StorageAdapter } from "./saveRepository";
import { groupDrop } from "../game/progression/loot";
import { RUN_BUFF_ORDER } from "../game/roguelike";

function memoryStorage(): StorageAdapter {
  const data = new Map<string, string>();
  return {
    getItem: (key) => data.get(key) ?? null,
    setItem: (key, value) => { data.set(key, value); },
  };
}

describe("SaveRepository", () => {
  it("creates exactly three offline-first slot views with timestamps", () => {
    const repository = new SaveRepository(memoryStorage(), () => "2026-07-10T12:00:00.000Z");
    repository.create(2, "Seraphine");

    const slots = repository.listLocal();
    expect(slots.map((slot) => slot.id)).toEqual([1, 2, 3]);
    expect(slots[1].local?.updatedAt).toBe("2026-07-10T12:00:00.000Z");
    expect(slots[1].local?.hunterName).toBe("Seraphine");
    expect(slots[1].online).toBeNull();
  });

  it("copies a local save into another slot without sharing nested state", () => {
    let now = "2026-07-10T12:00:00.000Z";
    const repository = new SaveRepository(memoryStorage(), () => now);
    repository.create(1, "Aelia");
    now = "2026-07-10T13:00:00.000Z";
    repository.copyLocal(1, 3);
    repository.updateLocal(3, (save) => ({
      ...save,
      healers: { ...save.healers, druid: { ...save.healers.druid, level: 99 } },
    }));

    const slots = repository.listLocal();
    expect(slots[0].local?.healers.druid.level).toBe(1);
    expect(slots[2].local?.healers.druid.level).toBe(99);
    expect(slots[2].local?.slotId).toBe(3);
  });

  it("replaces a local slot with a downloaded server snapshot", () => {
    let now = "2026-07-10T12:00:00.000Z";
    const repository = new SaveRepository(memoryStorage(), () => now);
    const serverSave = repository.create(1, "Aelia");
    serverSave.healers.priest.level = 40;
    now = "2026-07-10T14:00:00.000Z";
    repository.replaceLocal(serverSave);
    expect(repository.listLocal()[0].local?.healers.priest.level).toBe(40);
    expect(repository.listLocal()[0].local?.updatedAt).toBe(now);
  });

  it("deletes the local copy without inventing an online record", () => {
    const repository = new SaveRepository(memoryStorage(), () => "2026-07-10T12:00:00.000Z");
    repository.create(1, "Aelia");
    repository.deleteLocal(1);
    const slot = repository.listLocal()[0];
    expect(slot.local).toBeNull();
    expect(slot.online).toBeNull();
  });

  it("keeps class progression and inventories independent under one hunter name", () => {
    const repository = new SaveRepository(memoryStorage(), () => "2026-07-10T12:00:00.000Z");
    repository.create(1, "Aelia");
    repository.updateLocal(1, (save) => ({
      ...save,
      activeClassId: "druid",
      healers: {
        ...save.healers,
        druid: { level: 8, inventory: [...save.healers.druid.inventory, { ...save.healers.druid.inventory[0], id: "druid-drop" }] },
      },
    }));

    const save = repository.listLocal()[0].local!;
    expect(save.hunterName).toBe("Aelia");
    expect(save.activeClassId).toBe("druid");
    expect(save.healers.druid.level).toBe(8);
    expect(save.healers.priest.level).toBe(1);
    expect(save.healers.druid.inventory).toHaveLength(4);
    expect(save.healers.priest.inventory).toHaveLength(4);
  });

  it("resets every legacy save into fresh v5 progression while preserving identity and timestamp", () => {
    const storage = memoryStorage();
    const repository = new SaveRepository(storage, () => "2026-07-10T12:00:00.000Z");
    const created = repository.create(1, "Legacy");
    const legacy = {
      ...created,
      schemaVersion: 4,
      activeClassId: "druid",
      playSeconds: 999,
      healers: Object.fromEntries(Object.entries(created.healers).map(([id, healer]) => [id, { ...healer, level: 27 }])),
      stats: { totalBossKills: 22, flawlessClears: 9, alliesSaved: 4, healingDone: 1200, bossKills: { bulldrome: 22 }, highestRoguelikeRound: 12 },
      materials: [{ id: "legacy-boss-coin", name: "Legacy coin", quantity: 99, rarity: "common", itemLevel: 1, glyph: "R" }],
      collectionLog: { dropsFound: { "legacy-boss-coin": 99 }, petsFound: { "bulldrome-pet": 1 } },
      gearProgress: Object.fromEntries(Object.entries(created.gearProgress).map(([id, owner]) => [id, {
        ...owner,
        slots: Object.fromEntries(Object.entries(owner.slots).map(([slotId]) => [slotId, { level: 10 }])),
        infusionAbilityId: "priest-sanctuary",
      }])),
    } as Record<string, unknown>;
    storage.setItem("i-want-to-heal:saves:local:v1", JSON.stringify({ 1: legacy }));

    const migrated = repository.listLocal()[0].local!;
    expect(migrated.schemaVersion).toBe(5);
    expect(migrated.hunterName).toBe("Legacy");
    expect(migrated.activeClassId).toBe("priest");
    expect(migrated.playSeconds).toBe(0);
    expect(Object.values(migrated.healers).every((healer) => healer.level === 1 && healer.inventory.length > 0)).toBe(true);
    expect(migrated.stats).toEqual({ totalBossKills: 0, flawlessClears: 0, alliesSaved: 0, healingDone: 0, bossKills: {}, highestRoguelikeRound: 0, highestRogueTrialsEndlessKills: 0 });
    expect(migrated.materials).toEqual([]);
    expect(migrated.collectionLog).toEqual({ dropsFound: {}, petsFound: {} });
    expect(Object.values(migrated.gearProgress).every((owner) => owner.infusionAbilityId === null && owner.passiveInfusionId === null && Object.values(owner.slots).every((slot) => slot.level === 0))).toBe(true);
    expect(JSON.parse(storage.getItem("i-want-to-heal:saves:local:v1") ?? "{}")["1"].schemaVersion).toBe(5);
  });

  it("preserves valid v5 progression and group-drop inventory", () => {
    const storage = memoryStorage();
    const repository = new SaveRepository(storage, () => "2026-07-10T12:00:00.000Z");
    const created = repository.create(1, "Veteran");
    const drop = groupDrop("charge", "veteran");
    created.healers.priest.level = 8;
    created.stats = { ...created.stats, totalBossKills: 2, bossKills: { bulldrome: 2 } };
    created.stats.highestRogueTrialsEndlessKills = 14;
    created.materials = [{ id: drop.id, name: drop.name, quantity: 4, rarity: drop.rarity, itemLevel: drop.itemLevel, glyph: drop.glyph }];
    created.collectionLog = { dropsFound: { [drop.id]: 4 }, petsFound: { "bulldrome-pet": 1 } };
    storage.setItem("i-want-to-heal:saves:local:v1", JSON.stringify({ 1: created }));

    const migrated = repository.listLocal()[0].local!;
    expect(migrated.schemaVersion).toBe(5);
    expect(migrated.healers.priest.level).toBe(8);
    expect(migrated.stats.bossKills).toEqual({ bulldrome: 2 });
    expect(migrated.stats.highestRogueTrialsEndlessKills).toBe(14);
    expect(migrated.materials[0]).toMatchObject({ id: drop.id, quantity: 4 });
    expect(migrated.collectionLog).toEqual(created.collectionLog);
  });

  it("normalizes valid v5 infusion choices and discards stale ids", () => {
    const storage = memoryStorage();
    const repository = new SaveRepository(storage, () => "2026-07-10T12:00:00.000Z");
    const created = repository.create(1, "Infused");
    created.gearProgress.priest.infusionAbilityId = "priest-sanctuary";
    created.gearProgress.priest.passiveInfusionId = "restoring-grace" as never;
    created.gearProgress.druid.passiveInfusionId = "mend-echo";
    created.gearProgress.brann.infusionAbilityId = "removed-infusion";
    created.gearProgress.brann.passiveInfusionId = "deep-wells" as never;
    storage.setItem("i-want-to-heal:saves:local:v1", JSON.stringify({ 1: created }));

    const migrated = repository.listLocal()[0].local!;
    expect(migrated.schemaVersion).toBe(5);
    expect(migrated.gearProgress.priest.infusionAbilityId).toBe("priest-sanctuary");
    expect(migrated.gearProgress.priest.passiveInfusionId).toBeNull();
    expect(migrated.gearProgress.druid.passiveInfusionId).toBe("mend-echo");
    expect(migrated.gearProgress.brann.infusionAbilityId).toBeNull();
    expect(migrated.gearProgress.brann.passiveInfusionId).toBeNull();
  });

  it("persists every new roguelike buff as a healer passive infusion", () => {
    for (const passiveId of RUN_BUFF_ORDER) {
      const storage = memoryStorage();
      const repository = new SaveRepository(storage, () => "2026-07-10T12:00:00.000Z");
      const created = repository.create(1, "Infused");
      created.gearProgress.priest.passiveInfusionId = passiveId;
      storage.setItem("i-want-to-heal:saves:local:v1", JSON.stringify({ 1: created }));
      expect(repository.listLocal()[0].local?.gearProgress.priest.passiveInfusionId).toBe(passiveId);
    }
  });
});
