import { describe, it, expect, beforeEach, vi, afterEach } from "vitest";
import {
  setToken,
  getToken,
  clearToken,
  login,
  me,
  listModels,
  saveModel,
  gatewayOnline,
  groupPrompts,
  listPrompts,
  createPromptVersion,
  activatePrompt,
  deactivatePrompt,
  type PromptVersion,
} from "./api";

// 构造一个 fetch 响应桩。
function res(body: unknown, init?: { ok?: boolean; status?: number }): Response {
  return {
    ok: init?.ok ?? true,
    status: init?.status ?? 200,
    json: async () => body,
  } as unknown as Response;
}

const fetchMock = vi.fn();

beforeEach(() => {
  clearToken();
  localStorage.clear();
  fetchMock.mockReset();
  vi.stubGlobal("fetch", fetchMock);
});

afterEach(() => {
  vi.unstubAllGlobals();
});

describe("令牌持久化", () => {
  it("setToken 写入内存并持久化到 localStorage", () => {
    setToken("abc");
    expect(getToken()).toBe("abc");
    expect(localStorage.getItem("sdx_admin_token")).toBe("abc");
  });

  it("clearToken 清空内存与 localStorage", () => {
    setToken("abc");
    clearToken();
    expect(getToken()).toBe("");
    expect(localStorage.getItem("sdx_admin_token")).toBeNull();
  });
});

describe("authHeaders（经由带鉴权的请求观测）", () => {
  it("有令牌时带上 Bearer 头", async () => {
    setToken("tok123");
    fetchMock.mockResolvedValue(res({ models: [] }));
    await listModels("chat");
    const headers = (fetchMock.mock.calls[0][1] as RequestInit).headers as Record<string, string>;
    expect(headers.Authorization).toBe("Bearer tok123");
  });

  it("无令牌时不带 Authorization 头", async () => {
    fetchMock.mockResolvedValue(res({ models: [] }));
    await listModels("chat");
    const headers = (fetchMock.mock.calls[0][1] as RequestInit).headers as Record<string, string>;
    expect(headers.Authorization).toBeUndefined();
  });

  it("写请求带 JSON Content-Type", async () => {
    setToken("tok");
    fetchMock.mockResolvedValue(res({ id: "1" }));
    await saveModel({ kind: "chat", provider: "openai", base_url: "u", api_key: "k", model: "m" });
    const headers = (fetchMock.mock.calls[0][1] as RequestInit).headers as Record<string, string>;
    expect(headers["Content-Type"]).toBe("application/json");
  });
});

describe("guard：401 清令牌并广播登出", () => {
  it("401 时清令牌并派发 sdx:logout 事件", async () => {
    setToken("stale");
    const onLogout = vi.fn();
    window.addEventListener("sdx:logout", onLogout);
    fetchMock.mockResolvedValue(res({}, { ok: false, status: 401 }));

    await expect(listModels("chat")).rejects.toThrow();
    expect(getToken()).toBe("");
    expect(onLogout).toHaveBeenCalledOnce();
    window.removeEventListener("sdx:logout", onLogout);
  });

  it("403（已登录非管理员）照常抛错，但不清令牌/不登出", async () => {
    setToken("validuser");
    const onLogout = vi.fn();
    window.addEventListener("sdx:logout", onLogout);
    fetchMock.mockResolvedValue(res({}, { ok: false, status: 403 }));

    await expect(listModels("chat")).rejects.toThrow();
    expect(getToken()).toBe("validuser"); // 令牌保留
    expect(onLogout).not.toHaveBeenCalled();
    window.removeEventListener("sdx:logout", onLogout);
  });
});

describe("login", () => {
  it("成功时存令牌并返回用户", async () => {
    fetchMock.mockResolvedValue(
      res({ token: "t1", user: { id: "u1", email: "a@b.c" } }),
    );
    const user = await login("a@b.c", "pw");
    expect(user.id).toBe("u1");
    expect(getToken()).toBe("t1");
  });

  it("失败时抛出后端 error 文案且不存令牌", async () => {
    fetchMock.mockResolvedValue(res({ error: "密码错误" }, { ok: false, status: 401 }));
    await expect(login("a@b.c", "bad")).rejects.toThrow("密码错误");
    expect(getToken()).toBe("");
  });

  it("无 error 字段时回退到状态码文案", async () => {
    fetchMock.mockResolvedValue(res({}, { ok: false, status: 500 }));
    await expect(login("a@b.c", "pw")).rejects.toThrow("500");
  });
});

describe("me", () => {
  it("无令牌时直接返回 null（不发请求）", async () => {
    const u = await me();
    expect(u).toBeNull();
    expect(fetchMock).not.toHaveBeenCalled();
  });

  it("响应非 ok 时清令牌并返回 null", async () => {
    setToken("expired");
    fetchMock.mockResolvedValue(res({}, { ok: false, status: 401 }));
    expect(await me()).toBeNull();
    expect(getToken()).toBe("");
  });

  it("成功时返回用户", async () => {
    setToken("ok");
    fetchMock.mockResolvedValue(res({ user: { id: "u1", email: "a@b.c" } }));
    expect(await me()).toEqual({ id: "u1", email: "a@b.c" });
  });
});

describe("gatewayOnline", () => {
  it("healthz ok 时为 true", async () => {
    fetchMock.mockResolvedValue(res(null, { ok: true }));
    expect(await gatewayOnline()).toBe(true);
  });

  it("healthz 非 ok 时为 false", async () => {
    fetchMock.mockResolvedValue(res(null, { ok: false, status: 503 }));
    expect(await gatewayOnline()).toBe(false);
  });

  it("网络异常时吞掉错误并返回 false", async () => {
    fetchMock.mockRejectedValue(new Error("ECONNREFUSED"));
    expect(await gatewayOnline()).toBe(false);
  });
});

// —— Prompt 控制面 ——
function pv(key: string, version: number, active = false): PromptVersion {
  return { key, version, active, note: "", content: `c${version}` };
}

describe("groupPrompts（控制面分组：纯函数）", () => {
  it("保持 keys 注册顺序，未建版本的 key 也成组（active=null）", () => {
    const gs = groupPrompts({ keys: ["a", "b", "c"], versions: [pv("b", 1)] });
    expect(gs.map((g) => g.key)).toEqual(["a", "b", "c"]);
    expect(gs[0]).toMatchObject({ key: "a", versions: [], active: null });
    expect(gs[1].versions).toHaveLength(1);
  });

  it("每组版本号倒序，挑出激活版", () => {
    const gs = groupPrompts({ keys: ["k"], versions: [pv("k", 1), pv("k", 3, true), pv("k", 2)] });
    expect(gs[0].versions.map((v) => v.version)).toEqual([3, 2, 1]);
    expect(gs[0].active?.version).toBe(3);
  });

  it("无激活版时 active 为 null（回退代码默认）", () => {
    const gs = groupPrompts({ keys: ["k"], versions: [pv("k", 1), pv("k", 2)] });
    expect(gs[0].active).toBeNull();
  });

  it("DB 出现 Known 未列的 key 也容错展示（追加在已知之后）", () => {
    const gs = groupPrompts({ keys: ["known"], versions: [pv("orphan", 1, true)] });
    expect(gs.map((g) => g.key)).toEqual(["known", "orphan"]);
    expect(gs[1].active?.version).toBe(1);
  });
});

describe("listPrompts", () => {
  it("缺字段时回退为空数组（不崩）", async () => {
    setToken("t");
    fetchMock.mockResolvedValue(res({}));
    expect(await listPrompts()).toEqual({ keys: [], versions: [] });
  });

  it("非 ok 抛错", async () => {
    setToken("t");
    fetchMock.mockResolvedValue(res({}, { ok: false, status: 502 }));
    await expect(listPrompts()).rejects.toThrow("502");
  });

  it("命中 /api/v1/prompts（不在 /admin 前缀下）", async () => {
    setToken("t");
    fetchMock.mockResolvedValue(res({ keys: [], versions: [] }));
    await listPrompts();
    expect(fetchMock.mock.calls[0][0]).toMatch(/\/api\/v1\/prompts$/);
  });
});

describe("createPromptVersion", () => {
  it("成功返回新版本号", async () => {
    setToken("t");
    fetchMock.mockResolvedValue(res({ key: "k", version: 4 }));
    expect(await createPromptVersion("k", "body", "note")).toBe(4);
    const body = JSON.parse((fetchMock.mock.calls[0][1] as RequestInit).body as string);
    expect(body).toEqual({ key: "k", content: "body", note: "note" });
  });

  it("失败抛后端 error 文案", async () => {
    setToken("t");
    fetchMock.mockResolvedValue(res({ error: "key/content required" }, { ok: false, status: 400 }));
    await expect(createPromptVersion("k", "", "")).rejects.toThrow("key/content required");
  });
});

describe("activatePrompt", () => {
  it("成功无异常", async () => {
    setToken("t");
    fetchMock.mockResolvedValue(res({ activated: true }));
    await expect(activatePrompt("k", 2)).resolves.toBeUndefined();
  });

  it("已激活但广播失败(warn) → 当作错误抛出提醒运维", async () => {
    setToken("t");
    fetchMock.mockResolvedValue(res({ warn: "已激活但广播失败: nats down" }));
    await expect(activatePrompt("k", 2)).rejects.toThrow("广播失败");
  });

  it("HTTP 失败抛 error", async () => {
    setToken("t");
    fetchMock.mockResolvedValue(res({ error: "key/version required" }, { ok: false, status: 400 }));
    await expect(activatePrompt("k", 0)).rejects.toThrow("key/version required");
  });
});

describe("deactivatePrompt", () => {
  it("成功无异常并带上 key", async () => {
    setToken("t");
    fetchMock.mockResolvedValue(res({ deactivated: true }));
    await deactivatePrompt("k");
    const body = JSON.parse((fetchMock.mock.calls[0][1] as RequestInit).body as string);
    expect(body).toEqual({ key: "k" });
  });

  it("失败抛错", async () => {
    setToken("t");
    fetchMock.mockResolvedValue(res({ error: "boom" }, { ok: false, status: 502 }));
    await expect(deactivatePrompt("k")).rejects.toThrow("boom");
  });
});
