import { describe, expect, it } from "vitest";

import type {
  CharacterCardView,
  WorldEntityCardView,
} from "@/lib/api/types";
import {
  buildCharacterGenerateRequest,
  buildCharacterIngestRequest,
  buildWorldGenerateRequest,
  clampCharacterCount,
  errorCode,
  extractIngestConflicts,
  generationErrorMessage,
  MAX_CHARACTER_COUNT,
  mergeCharacterCards,
  mergeWorldEntities,
  MIN_CHARACTER_COUNT,
  roleMixTotal,
  sanitizeRoleMix,
  updateCard,
  worldEntityRules,
} from "./cards";

const card = (over: Partial<CharacterCardView> = {}): CharacterCardView => ({
  name: "甲",
  role: "主角",
  appearance: "白衣如雪",
  motive: "为家族正名",
  backstory: "出身寒门",
  arc: "由怯转勇",
  ...over,
});

describe("buildWorldGenerateRequest", () => {
  it("trims brief", () => {
    expect(buildWorldGenerateRequest("  修真世界  ")).toEqual({
      brief: "修真世界",
    });
  });
});

describe("clampCharacterCount", () => {
  it("clamps to [MIN, MAX] and rounds", () => {
    expect(clampCharacterCount(0)).toBe(MIN_CHARACTER_COUNT);
    expect(clampCharacterCount(99)).toBe(MAX_CHARACTER_COUNT);
    expect(clampCharacterCount(3.6)).toBe(4);
    expect(clampCharacterCount(Number.NaN)).toBe(MIN_CHARACTER_COUNT);
  });
});

describe("buildCharacterGenerateRequest", () => {
  it("trims brief, clamps count, omits empty role", () => {
    expect(buildCharacterGenerateRequest("  剑修  ", 99)).toEqual({
      brief: "剑修",
      count: MAX_CHARACTER_COUNT,
    });
  });

  it("includes trimmed role when present", () => {
    expect(buildCharacterGenerateRequest("剑修", 2, "  对手  ")).toEqual({
      brief: "剑修",
      count: 2,
      role: "对手",
    });
  });

  it("uses role_mix (count=sum, no single role) when a mix is supplied", () => {
    expect(
      buildCharacterGenerateRequest("群像", 1, "忽略", {
        主角: 1,
        对手: 2,
        配角: 3,
      }),
    ).toEqual({
      brief: "群像",
      count: 6,
      role_mix: { 主角: 1, 对手: 2, 配角: 3 },
    });
  });

  it("falls back to count/role when role_mix is empty", () => {
    expect(buildCharacterGenerateRequest("群像", 4, "主角", {})).toEqual({
      brief: "群像",
      count: 4,
      role: "主角",
    });
  });
});

describe("sanitizeRoleMix", () => {
  it("trims roles, drops blanks, clamps counts to >=1, dedups", () => {
    expect(
      sanitizeRoleMix([
        { role: "  主角  ", count: 2 },
        { role: "", count: 3 }, // 空定位丢弃
        { role: "对手", count: 0 }, // clamp 到 1
        { role: "主角", count: 5 }, // 重复定位丢弃（先到先得）
      ]),
    ).toEqual({ 主角: 2, 对手: 1 });
  });

  it("caps the running total at MAX_CHARACTER_COUNT", () => {
    const out = sanitizeRoleMix([
      { role: "主角", count: 10 },
      { role: "对手", count: 5 }, // 只剩 2 额度
      { role: "配角", count: 3 }, // 无额度，丢弃
    ]);
    expect(out).toEqual({ 主角: 10, 对手: 2 });
    expect(roleMixTotal(out)).toBe(MAX_CHARACTER_COUNT);
  });

  it("returns empty object when no valid rows", () => {
    expect(sanitizeRoleMix([{ role: "  ", count: 3 }])).toEqual({});
  });
});

describe("roleMixTotal", () => {
  it("sums counts", () => {
    expect(roleMixTotal({ 主角: 1, 对手: 2 })).toBe(3);
    expect(roleMixTotal({})).toBe(0);
  });
});

describe("buildCharacterIngestRequest", () => {
  it("copies cards and carries acknowledge flag", () => {
    const cards = [card()];
    const req = buildCharacterIngestRequest(cards, true);
    expect(req.acknowledge_conflicts).toBe(true);
    expect(req.cards).toEqual(cards);
    expect(req.cards[0]).not.toBe(cards[0]); // immutable copy
  });

  it("defaults acknowledge to false", () => {
    expect(buildCharacterIngestRequest([card()]).acknowledge_conflicts).toBe(
      false,
    );
  });
});

describe("updateCard", () => {
  it("returns a new card with patch applied (no mutation)", () => {
    const original = card();
    const next = updateCard(original, { name: "乙" });
    expect(next.name).toBe("乙");
    expect(original.name).toBe("甲");
    expect(next).not.toBe(original);
  });

  it("patches appearance/motive without dropping the other fields", () => {
    const original = card();
    const next = updateCard(original, {
      appearance: "玄色劲装",
      motive: "复仇",
    });
    expect(next.appearance).toBe("玄色劲装");
    expect(next.motive).toBe("复仇");
    expect(next.backstory).toBe(original.backstory);
    expect(original.appearance).toBe("白衣如雪"); // no mutation
  });
});

describe("extractIngestConflicts", () => {
  it("narrows CONFLICT_UNRESOLVED envelope details", () => {
    const out = extractIngestConflicts({
      error: {
        code: "CONFLICT_UNRESOLVED",
        details: {
          conflicts: [
            { type: "性格漂移", where: "第3章", refs: ["甲"], suggestion: "改" },
            "junk",
          ],
          conflict_count: 2,
        },
      },
    });
    expect(out).toEqual({
      conflictCount: 2,
      conflicts: [
        {
          type: "性格漂移",
          where: "第3章",
          refs: ["甲"],
          suggestion: "改",
          original: null,
          replacement: null,
        },
        {
          type: "未分类",
          where: "",
          refs: [],
          suggestion: "",
          original: null,
          replacement: null,
        },
      ],
    });
  });

  it("returns null for non-conflict errors", () => {
    expect(extractIngestConflicts({ error: { code: "LLM_UNAVAILABLE" } })).toBeNull();
    expect(extractIngestConflicts(null)).toBeNull();
  });

  it("falls back conflict_count to conflicts length", () => {
    const out = extractIngestConflicts({
      error: {
        code: "CONFLICT_UNRESOLVED",
        details: { conflicts: [{ type: "设定违例" }] },
      },
    });
    expect(out?.conflictCount).toBe(1);
  });
});

describe("errorCode / generationErrorMessage", () => {
  it("extracts code and maps 503 to settings prompt", () => {
    expect(errorCode({ error: { code: "LLM_UNAVAILABLE" } })).toBe(
      "LLM_UNAVAILABLE",
    );
    expect(generationErrorMessage({ error: { code: "LLM_UNAVAILABLE" } })).toContain(
      "设置",
    );
    expect(generationErrorMessage({ error: { code: "INTERNAL" } })).toContain(
      "重试",
    );
  });
});

describe("worldEntityRules", () => {
  it("returns rules or empty array", () => {
    expect(worldEntityRules({ type: "势力", name: "宗门", rules: ["a"] })).toEqual([
      "a",
    ]);
    expect(worldEntityRules({ type: "势力", name: "宗门" })).toEqual([]);
  });
});

describe("mergeCharacterCards", () => {
  it("appends session cards not already in the persisted list", () => {
    const persisted = [card({ name: "甲" })];
    const session = [card({ name: "乙" })];
    const out = mergeCharacterCards(persisted, session);
    expect(out.map((c) => c.name)).toEqual(["甲", "乙"]);
  });

  it("dedups by name (persisted wins, no duplicate on reload echo)", () => {
    const persisted = [card({ name: "甲", role: "主角" })];
    const session = [card({ name: "甲", role: "对手" })];
    const out = mergeCharacterCards(persisted, session);
    expect(out).toHaveLength(1);
    expect(out[0]?.role).toBe("主角"); // persisted version kept
  });

  it("returns persisted list when no session cards", () => {
    const persisted = [card({ name: "甲" })];
    expect(mergeCharacterCards(persisted, [])).toEqual(persisted);
  });

  it("preserves relations on the merged cards (Codex relation display)", () => {
    const persisted = [
      card({
        name: "甲",
        relations: [{ name: "乙", kind: "宿敌", note: "灭门之仇" }],
      }),
    ];
    const out = mergeCharacterCards(persisted, []);
    expect(out[0]?.relations).toEqual([
      { name: "乙", kind: "宿敌", note: "灭门之仇" },
    ]);
  });
});

describe("mergeWorldEntities", () => {
  const entity = (
    over: Partial<WorldEntityCardView> = {},
  ): WorldEntityCardView => ({ type: "势力", name: "宗门", ...over });

  it("appends entities not already present (keyed by type+name)", () => {
    const persisted = [entity({ name: "天剑宗" })];
    const session = [entity({ name: "魔渊" })];
    expect(mergeWorldEntities(persisted, session).map((e) => e.name)).toEqual([
      "天剑宗",
      "魔渊",
    ]);
  });

  it("dedups by type+name; same name different type kept separate", () => {
    const persisted = [entity({ type: "势力", name: "玄" })];
    const session = [
      entity({ type: "势力", name: "玄" }),
      entity({ type: "地点", name: "玄" }),
    ];
    const out = mergeWorldEntities(persisted, session);
    expect(out).toHaveLength(2);
    expect(out.map((e) => e.type)).toEqual(["势力", "地点"]);
  });
});
