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

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

import {
  applyPlanPatch,
  canAdvance,
  canGeneratePlan,
  canSubmit,
  clampStep,
  emptyWizardForm,
  mapPlanResultToWizardForm,
  STEP_TITLES,
  toCreateRequest,
  toPlanSeedRequest,
  WIZARD_STEPS,
  type WizardForm,
} from "./wizard";

const fullPlan: ProjectPlanView = {
  title_candidates: ["逐光而行", "剑试九霄"],
  setting: "上古仙门倾轧的洞天世界",
  narrative_structure: "黄金三章立钩 → 拜师 → 宗门大比爆发",
  story_core: "废柴少年觉醒禁忌血脉，逆势封神的代价",
  ending_design: "登顶后归隐，自我救赎收束",
  tone: "热血",
};

describe("wizard step metadata", () => {
  it("provides one title per step", () => {
    expect(STEP_TITLES).toHaveLength(WIZARD_STEPS);
  });

  it("ends on a confirmation step rather than an empty world step", () => {
    expect(STEP_TITLES[WIZARD_STEPS - 1]).toBe("确认与提交");
  });
});

describe("wizard step gating", () => {
  it("blocks advancing past step 1 without a title", () => {
    expect(canAdvance(1, emptyWizardForm)).toBe(false);
  });

  it("allows advancing step 1 once title is set", () => {
    expect(canAdvance(1, { ...emptyWizardForm, title: "逐光而行" })).toBe(true);
  });

  it("allows advancing later steps even when fields are empty", () => {
    expect(canAdvance(3, emptyWizardForm)).toBe(true);
  });

  it("clamps step within bounds", () => {
    expect(clampStep(0)).toBe(1);
    expect(clampStep(WIZARD_STEPS + 3)).toBe(WIZARD_STEPS);
    expect(clampStep(3)).toBe(3);
  });

  it("requires a title to submit", () => {
    expect(canSubmit(emptyWizardForm)).toBe(false);
    expect(canSubmit({ ...emptyWizardForm, title: "x" })).toBe(true);
  });
});

describe("toCreateRequest", () => {
  it("maps form to snake_case request, nulling empty optionals", () => {
    const form: WizardForm = {
      title: "  逐光而行  ",
      genre: "玄幻",
      logline: "",
      sellingPoints: ["逆袭", "系统流"],
      structure: "三幕",
      premise: "  ",
      protagonist: "",
      theme: "抗争",
      tone: "",
      endingType: "",
      narrativePov: "",
    };
    expect(toCreateRequest(form)).toEqual({
      title: "逐光而行",
      genre: "玄幻",
      logline: null,
      premise: null,
      theme: "抗争",
      selling_points: ["逆袭", "系统流"],
      structure: "三幕",
      tone: null,
      ending_type: null,
      narrative_pov: null,
    });
  });

  // ④ 立项新增基调/结局/视角：camelCase 表单 → snake_case 请求，空值落 null。
  it("maps tone, endingType, narrativePov to snake_case fields", () => {
    const form: WizardForm = {
      ...emptyWizardForm,
      title: "书",
      tone: "热血",
      endingType: "HE",
      narrativePov: "第一人称",
    };
    const req = toCreateRequest(form);
    expect(req.tone).toBe("热血");
    expect(req.ending_type).toBe("HE");
    expect(req.narrative_pov).toBe("第一人称");
  });

  it("nulls empty tone, endingType, narrativePov", () => {
    const req = toCreateRequest({ ...emptyWizardForm, title: "书" });
    expect(req.tone).toBeNull();
    expect(req.ending_type).toBeNull();
    expect(req.narrative_pov).toBeNull();
  });

  // QA H1 回归：立意（premise）与主角/金手指（protagonist）是两个独立输入，
  // 不能互相覆盖——提交时各占一段合并进 premise。
  it("merges premise and protagonist into premise without overwriting", () => {
    const form: WizardForm = {
      ...emptyWizardForm,
      title: "书",
      premise: "凡人逆袭的代价",
      protagonist: "主角林川，金手指=吞噬术，限制：每次吞噬折寿",
    };
    const req = toCreateRequest(form);
    expect(req.premise).toBe(
      "凡人逆袭的代价\n\n主角/金手指：主角林川，金手指=吞噬术，限制：每次吞噬折寿",
    );
  });

  it("keeps protagonist alone when premise is empty", () => {
    const form: WizardForm = {
      ...emptyWizardForm,
      title: "书",
      protagonist: "主角设定",
    };
    expect(toCreateRequest(form).premise).toBe("主角/金手指：主角设定");
  });
});

// ⑤ AI 立项方案 → 向导映射（含 ④ 字段 tone；错配字段折进 premise，不静默丢内容）。
describe("mapPlanResultToWizardForm", () => {
  it("maps title candidate, structure and tone to their wizard homes", () => {
    const patch = mapPlanResultToWizardForm(fullPlan);
    // 书名候选 → title（取首个）。
    expect(patch.title).toBe("逐光而行");
    // 叙事结构 → structure。
    expect(patch.structure).toBe("黄金三章立钩 → 拜师 → 宗门大比爆发");
    // ④ 字段：基调 → tone。
    expect(patch.tone).toBe("热血");
  });

  it("folds setting and ending design into premise without dropping content", () => {
    const patch = mapPlanResultToWizardForm(fullPlan);
    // 故事核心 + 时空背景 + 结局设计 逐段拼进 premise（无独立向导字段，不静默丢）。
    expect(patch.premise).toContain("废柴少年觉醒禁忌血脉，逆势封神的代价");
    expect(patch.premise).toContain("时空背景：上古仙门倾轧的洞天世界");
    expect(patch.premise).toContain("结局设计：登顶后归隐，自我救赎收束");
  });

  it("does not fabricate endingType or narrativePov (④ presets left for author)", () => {
    // 结局取向/叙事视角是预设枚举，无法从自由文本可靠反推 → 不进 patch（不臆造）。
    const patch = mapPlanResultToWizardForm(fullPlan);
    expect(patch.endingType).toBeUndefined();
    expect(patch.narrativePov).toBeUndefined();
  });

  it("omits empty fields from the patch (parse resilience)", () => {
    const empty: ProjectPlanView = {
      title_candidates: [],
      setting: "",
      narrative_structure: "",
      story_core: "",
      ending_design: "",
      tone: "",
    };
    expect(mapPlanResultToWizardForm(empty)).toEqual({});
  });
});

// applyPlanPatch：按字段并入、仅填空字段（保护作者已编辑内容，非 bulk 覆盖）。
describe("applyPlanPatch", () => {
  it("fills only empty fields, preserving author-edited values", () => {
    const form: WizardForm = {
      ...emptyWizardForm,
      title: "作者已取的书名",
      genre: "仙侠",
    };
    const patch = { title: "AI书名", structure: "三幕", tone: "热血" };
    const next = applyPlanPatch(form, patch);
    // 已编辑的 title 受保护、不被覆盖；空的 structure/tone 才回填。
    expect(next.title).toBe("作者已取的书名");
    expect(next.structure).toBe("三幕");
    expect(next.tone).toBe("热血");
    // 未在 patch 中的字段原样保留。
    expect(next.genre).toBe("仙侠");
  });

  it("treats whitespace-only fields as empty (fillable)", () => {
    const form: WizardForm = { ...emptyWizardForm, premise: "   " };
    const next = applyPlanPatch(form, { premise: "AI 立意" });
    expect(next.premise).toBe("AI 立意");
  });

  it("returns a new object without mutating the input form", () => {
    const form: WizardForm = { ...emptyWizardForm };
    const next = applyPlanPatch(form, { tone: "热血" });
    expect(next).not.toBe(form);
    expect(form.tone).toBe("");
  });
});

// canGeneratePlan：种子门控（题材 + logline 都非空）。
describe("canGeneratePlan", () => {
  it("requires both genre and logline", () => {
    expect(canGeneratePlan(emptyWizardForm)).toBe(false);
    expect(
      canGeneratePlan({ ...emptyWizardForm, genre: "仙侠" }),
    ).toBe(false);
    expect(
      canGeneratePlan({ ...emptyWizardForm, logline: "一句话" }),
    ).toBe(false);
    expect(
      canGeneratePlan({ ...emptyWizardForm, genre: "仙侠", logline: "一句话" }),
    ).toBe(true);
  });

  it("rejects whitespace-only seeds", () => {
    expect(
      canGeneratePlan({ ...emptyWizardForm, genre: "  ", logline: "  " }),
    ).toBe(false);
  });
});

// toPlanSeedRequest：向导草稿 → 生成请求（snake_case，空值省略）。
describe("toPlanSeedRequest", () => {
  it("serializes seed and filled fields, omitting empties", () => {
    const form: WizardForm = {
      ...emptyWizardForm,
      genre: "都市",
      logline: "外卖骑手觉醒读心术。",
      title: "听风者",
      sellingPoints: ["逆袭"],
      endingType: "HE",
    };
    const req = toPlanSeedRequest(form);
    expect(req.genre).toBe("都市");
    expect(req.logline).toBe("外卖骑手觉醒读心术。");
    expect(req.title).toBe("听风者");
    expect(req.ending_type).toBe("HE");
    expect(req.selling_points).toEqual(["逆袭"]);
    // 空字段省略（undefined），不发空串。
    expect(req.premise).toBeUndefined();
    expect(req.tone).toBeUndefined();
  });
});

// wizard→create 流程：归一后的请求体经 mock 客户端提交，返回新建项目 id。
describe("wizard create flow (mocked client)", () => {
  it("submits the normalized body and resolves a project id", async () => {
    const post = async (
      _path: string,
      opts: { body: ReturnType<typeof toCreateRequest> },
    ): Promise<{ data: { id: string }; error: null }> => {
      expect(opts.body.title).toBe("青冥录");
      expect(opts.body.selling_points).toEqual(["群像"]);
      return { data: { id: "proj-123" }, error: null };
    };

    const form: WizardForm = {
      ...emptyWizardForm,
      title: "青冥录",
      sellingPoints: ["群像"],
    };
    const { data } = await post("/projects", { body: toCreateRequest(form) });
    expect(data.id).toBe("proj-123");
  });
});
