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

import type { CharacterCardView } from "@/lib/api/types";

import {
  buildGraphEdges,
  buildGraphNodes,
  buildRelationshipGraph,
  countDanglingRelations,
  DEFAULT_RELATION_LABEL,
  layoutGraph,
  roleStyle,
} from "./graphLayout";

// 最小角色卡工厂：CharacterCardView 多字段必填，测试只关心 name/role/relations。
function ch(
  name: string,
  role: string,
  relations: { name: string; kind?: string; note?: string | null }[] = [],
): CharacterCardView {
  return {
    name,
    role,
    traits: [],
    appearance: "",
    motive: "",
    backstory: "",
    arc: "",
    speech_tics: [],
    relations: relations.map((r) => ({
      name: r.name,
      kind: r.kind ?? "",
      note: r.note ?? null,
    })),
  };
}

describe("roleStyle", () => {
  it("主角关键词 → protagonist + 朱砂色", () => {
    expect(roleStyle("主角")).toEqual({
      group: "protagonist",
      colorVar: "--color-cinnabar",
    });
    expect(roleStyle("男主角").group).toBe("protagonist");
  });

  it("反派/对手/敌 → antagonist + 冲突色", () => {
    expect(roleStyle("反派").group).toBe("antagonist");
    expect(roleStyle("对手").group).toBe("antagonist");
    expect(roleStyle("宿敌").group).toBe("antagonist");
    expect(roleStyle("反派").colorVar).toBe("--color-conflict");
  });

  it("导师/师父 → mentor + info 色", () => {
    expect(roleStyle("导师").group).toBe("mentor");
    expect(roleStyle("师父").group).toBe("mentor");
    expect(roleStyle("导师").colorVar).toBe("--color-info");
  });

  it("CP/恋人 → ally（大小写不敏感）", () => {
    expect(roleStyle("CP").group).toBe("ally");
    expect(roleStyle("cp").group).toBe("ally");
    expect(roleStyle("恋人").group).toBe("ally");
  });

  it("配角/工具人 → supporting", () => {
    expect(roleStyle("配角").group).toBe("supporting");
    expect(roleStyle("工具人").group).toBe("supporting");
  });

  it("未知/空定位 → other（默认）", () => {
    expect(roleStyle("神秘存在").group).toBe("other");
    expect(roleStyle("").group).toBe("other");
    expect(roleStyle("  ").group).toBe("other");
  });
});

describe("buildGraphNodes", () => {
  it("每个唯一角色一个节点，id=name，role 决定分组/配色", () => {
    const nodes = buildGraphNodes([ch("林风", "主角"), ch("赵石", "对手")]);
    expect(nodes).toHaveLength(2);
    expect(nodes[0]).toMatchObject({
      id: "林风",
      position: { x: 0, y: 0 },
      data: {
        label: "林风",
        role: "主角",
        group: "protagonist",
        colorVar: "--color-cinnabar",
      },
    });
    expect(nodes[1]?.data.group).toBe("antagonist");
  });

  it("跳过空名与重名角色（先到先得）", () => {
    const nodes = buildGraphNodes([
      ch("林风", "主角"),
      ch("  ", "配角"),
      ch("林风", "对手"),
    ]);
    expect(nodes).toHaveLength(1);
    expect(nodes[0]?.data.role).toBe("主角");
  });

  it("名字两端空白被裁剪用作 id", () => {
    const nodes = buildGraphNodes([ch("  林风  ", "主角")]);
    expect(nodes[0]?.id).toBe("林风");
  });
});

describe("buildGraphEdges", () => {
  const known = new Set(["林风", "赵石", "青禾"]);

  it("kind → 边标签", () => {
    const edges = buildGraphEdges(
      [ch("林风", "主角", [{ name: "赵石", kind: "宿敌" }])],
      known,
    );
    expect(edges).toHaveLength(1);
    expect(edges[0]).toMatchObject({
      source: "林风",
      target: "赵石",
      label: "宿敌",
    });
  });

  it("kind 为空 → 回退到命名常量默认标签", () => {
    const edges = buildGraphEdges(
      [ch("林风", "主角", [{ name: "青禾", kind: "" }])],
      known,
    );
    expect(edges[0]?.label).toBe(DEFAULT_RELATION_LABEL);
  });

  it("悬空边（目标未入库）被跳过", () => {
    const edges = buildGraphEdges(
      [ch("林风", "主角", [{ name: "不存在的人", kind: "师父" }])],
      known,
    );
    expect(edges).toHaveLength(0);
  });

  it("自指关系被跳过", () => {
    const edges = buildGraphEdges(
      [ch("林风", "主角", [{ name: "林风", kind: "内心" }])],
      known,
    );
    expect(edges).toHaveLength(0);
  });

  it("互指关系无向去重为单边", () => {
    const edges = buildGraphEdges(
      [
        ch("林风", "主角", [{ name: "赵石", kind: "宿敌" }]),
        ch("赵石", "对手", [{ name: "林风", kind: "宿敌" }]),
      ],
      known,
    );
    expect(edges).toHaveLength(1);
  });

  it("边 id 唯一", () => {
    const edges = buildGraphEdges(
      [
        ch("林风", "主角", [
          { name: "赵石", kind: "宿敌" },
          { name: "青禾", kind: "青梅竹马" },
        ]),
      ],
      known,
    );
    const ids = edges.map((e) => e.id);
    expect(new Set(ids).size).toBe(ids.length);
  });
});

describe("countDanglingRelations", () => {
  it("统计引用未入库角色的关系数", () => {
    const known = new Set(["林风"]);
    const count = countDanglingRelations(
      [
        ch("林风", "主角", [
          { name: "赵石", kind: "宿敌" }, // 悬空
          { name: "青禾", kind: "青梅竹马" }, // 悬空
        ]),
      ],
      known,
    );
    expect(count).toBe(2);
  });

  it("全部已入库时为 0", () => {
    const known = new Set(["林风", "赵石"]);
    const count = countDanglingRelations(
      [ch("林风", "主角", [{ name: "赵石", kind: "宿敌" }])],
      known,
    );
    expect(count).toBe(0);
  });
});

describe("layoutGraph", () => {
  const nodes = buildGraphNodes([ch("林风", "主角"), ch("赵石", "对手")]);
  const edges = buildGraphEdges(
    [ch("林风", "主角", [{ name: "赵石", kind: "宿敌" }])],
    new Set(["林风", "赵石"]),
  );

  it("同输入产出相同坐标（确定性）", () => {
    const a = layoutGraph(nodes, edges);
    const b = layoutGraph(nodes, edges);
    expect(a).toEqual(b);
  });

  it("dagre 赋予非零坐标并保持节点数据", () => {
    const laid = layoutGraph(nodes, edges);
    expect(laid).toHaveLength(2);
    for (const n of laid) {
      expect(Number.isFinite(n.position.x)).toBe(true);
      expect(Number.isFinite(n.position.y)).toBe(true);
      expect(n.data.label).toBeTruthy();
    }
    // 两节点经布局后不再重叠于原点。
    expect(laid[0]?.position).not.toEqual(laid[1]?.position);
  });

  it("不可变：不修改传入节点，返回新对象", () => {
    const before = structuredClone(nodes);
    const laid = layoutGraph(nodes, edges);
    expect(nodes).toEqual(before); // 入参未被修改
    expect(laid[0]).not.toBe(nodes[0]); // 新对象
  });
});

describe("buildRelationshipGraph", () => {
  it("端到端：布局后节点 + 有效边 + 悬空计数", () => {
    const graph = buildRelationshipGraph([
      ch("林风", "主角", [
        { name: "赵石", kind: "宿敌" },
        { name: "幽灵", kind: "谜团" }, // 悬空
      ]),
      ch("赵石", "对手", [{ name: "林风", kind: "宿敌" }]),
    ]);
    expect(graph.nodes).toHaveLength(2);
    expect(graph.edges).toHaveLength(1);
    expect(graph.danglingCount).toBe(1);
    // 已布局（非全零）。
    const positions = graph.nodes.map((n) => `${n.position.x},${n.position.y}`);
    expect(new Set(positions).size).toBe(2);
  });

  it("空输入 → 空图", () => {
    const graph = buildRelationshipGraph([]);
    expect(graph.nodes).toEqual([]);
    expect(graph.edges).toEqual([]);
    expect(graph.danglingCount).toBe(0);
  });
});
