// 认证工具单测（含 MSW 集成）
// 依据：02-architecture-design.md §13 测试策略、F12 localStorage token、ISSUE-004 REST 登录
// 覆盖：token 存储 / isAuthenticated / login REST / refreshAccessToken 竞态防护 / clearAuth

import {
  describe,
  it,
  expect,
  beforeAll,
  afterAll,
  afterEach,
  vi,
} from "vitest";
import { server } from "@/test/mocks/server";
import {
  getToken,
  getRefreshToken,
  setTokens,
  clearAuth,
  getUser,
  setUser,
  isAuthenticated,
  login,
  refreshAccessToken,
  getAuthHeaders,
} from "./auth";
import { mockParent } from "@/test/mocks/fixtures";

beforeAll(() => server.listen({ onUnhandledRequest: "error" }));
afterEach(() => server.resetHandlers());
afterAll(() => server.close());

describe("token 存储", () => {
  it("setTokens 写入 access/refresh/expiresAt", () => {
    setTokens("access-123", "refresh-456", 3600);
    expect(getToken()).toBe("access-123");
    expect(getRefreshToken()).toBe("refresh-456");
  });

  it("未设置 token 时返回 null", () => {
    expect(getToken()).toBeNull();
    expect(getRefreshToken()).toBeNull();
  });
});

describe("getUser / setUser", () => {
  it("setUser 后 getUser 返回相同对象", () => {
    setUser(mockParent);
    const user = getUser();
    expect(user).not.toBeNull();
    expect(user?.id).toBe(mockParent.id);
    expect(user?.email).toBe(mockParent.email);
  });

  it("getUser 解析无效 JSON 返回 null", () => {
    localStorage.setItem("parent_user_info", "{invalid json");
    expect(getUser()).toBeNull();
  });

  it("getUser 未设置时返回 null", () => {
    expect(getUser()).toBeNull();
  });
});

describe("isAuthenticated", () => {
  it("无 token 返回 false", () => {
    expect(isAuthenticated()).toBe(false);
  });

  it("有未过期 token 返回 true", () => {
    setTokens("access", "refresh", 3600);
    expect(isAuthenticated()).toBe(true);
  });

  it("有过期 token 返回 false", () => {
    setTokens("access", "refresh", -1); // 已过期
    expect(isAuthenticated()).toBe(false);
  });
});

describe("clearAuth", () => {
  it("清除所有认证信息", () => {
    setTokens("access", "refresh", 3600);
    setUser(mockParent);
    clearAuth();
    expect(getToken()).toBeNull();
    expect(getRefreshToken()).toBeNull();
    expect(getUser()).toBeNull();
  });
});

describe("login（REST，ISSUE-004）", () => {
  it("正确凭据返回用户和 token", async () => {
    const result = await login("parent@example.com", "password");
    expect(result.user.email).toBe("parent@example.com");
    expect(result.tokens.accessToken).toBeTruthy();
    expect(result.tokens.refreshToken).toBeTruthy();
    // 写入了 localStorage
    expect(getToken()).toBe(result.tokens.accessToken);
  });

  it("错误凭据抛出异常", async () => {
    await expect(login("wrong@example.com", "wrong")).rejects.toThrow(
      /邮箱或密码错误/,
    );
  });
});

describe("refreshAccessToken（竞态防护）", () => {
  it("有效 refresh token 返回新 access token", async () => {
    setTokens("access", "mock-refresh-token-parent-001", 3600);
    const newToken = await refreshAccessToken();
    expect(newToken).toBe("mock-access-token-parent-001-renewed");
    expect(getToken()).toBe("mock-access-token-parent-001-renewed");
  });

  it("无 refresh token 返回 null", async () => {
    clearAuth();
    const result = await refreshAccessToken();
    expect(result).toBeNull();
  });

  it("无效 refresh token 返回 null", async () => {
    setTokens("access", "invalid-refresh", 3600);
    const result = await refreshAccessToken();
    expect(result).toBeNull();
  });

  it("并发请求复用同一 refresh（仅触发一次 fetch）", async () => {
    setTokens("access", "mock-refresh-token-parent-001", 3600);
    const fetchSpy = vi.spyOn(globalThis, "fetch");
    const p1 = refreshAccessToken();
    const p2 = refreshAccessToken();
    const [t1, t2] = await Promise.all([p1, p2]);
    expect(t1).toBe(t2);
    // 竞态防护：两个并发调用只触发一次 refresh fetch（ARB-022 §24.4 双 /v1 前缀）
    const refreshCalls = fetchSpy.mock.calls.filter(([url]) =>
      String(url).includes("/api/v1/iam/v1/refresh"),
    );
    expect(refreshCalls).toHaveLength(1);
    fetchSpy.mockRestore();
  });
});

describe("getAuthHeaders", () => {
  it("无 token 返回空对象", () => {
    clearAuth();
    expect(getAuthHeaders()).toEqual({});
  });

  it("有 token 返回 Bearer header", () => {
    setTokens("my-token", "refresh", 3600);
    expect(getAuthHeaders()).toEqual({ Authorization: "Bearer my-token" });
  });
});

describe("logout", () => {
  it("清除认证信息", async () => {
    setTokens("access", "refresh", 3600);
    setUser(mockParent);
    const { logout } = await import("./auth");
    // mock window.location.href 赋值（jsdom 不允许直接 spy）
    const originalHref = window.location.href;
    let assignedHref: string | null = null;
    Object.defineProperty(window, "location", {
      value: {
        ...window.location,
        set href(v: string) {
          assignedHref = v;
        },
        get href() {
          return originalHref;
        },
      },
      configurable: true,
    });
    logout();
    expect(getToken()).toBeNull();
    expect(getUser()).toBeNull();
    expect(assignedHref).toBe("/login");
  });
});
