import {
  beforeEach,
  describe,
  expect,
  it,
  vi,
} from "vitest";
import { HttpError } from "../errors";
import { HttpClient } from "../httpClient";
import {
  publishAuthExpired,
  resetAuthExpiredNotification,
  subscribeAuthExpired,
} from "../../auth/authEvents";
import {
  DMS_ACCESS_TOKEN_KEY,
  tokenStore,
} from "../../auth/tokenStore";

class MemoryStorage implements Storage {
  private readonly values = new Map<string, string>();
  get length(): number { return this.values.size; }
  clear(): void { this.values.clear(); }
  getItem(key: string): string | null { return this.values.get(key) ?? null; }
  key(index: number): string | null { return [...this.values.keys()][index] ?? null; }
  removeItem(key: string): void { this.values.delete(key); }
  setItem(key: string, value: string): void { this.values.set(key, value); }
}

function installStorage(): void {
  Object.defineProperty(globalThis, "sessionStorage", {
    configurable: true,
    value: new MemoryStorage(),
  });
  Object.defineProperty(globalThis, "localStorage", {
    configurable: true,
    value: new MemoryStorage(),
  });
}

function jsonResponse(
  body: unknown,
  status = 200,
  headers: HeadersInit = {},
): Response {
  return new Response(JSON.stringify(body), {
    status,
    headers: { "Content-Type": "application/json", ...headers },
  });
}

function createClient(fetchImpl: typeof fetch, token: string | null = null) {
  return new HttpClient({
    baseUrl: "/api/v1",
    fetchImpl,
    tokenProvider: () => token,
    requestIdFactory: () => "generated-request-id",
  });
}

describe("HttpClient", () => {
  beforeEach(() => {
    installStorage();
    resetAuthExpiredNotification();
  });

  it("解析标准 JSON 成功响应并优先使用响应头 requestId", async () => {
    const fetchMock = vi.fn<typeof fetch>().mockResolvedValue(
      jsonResponse(
        { code: "OK", message: "success", data: { value: 1 }, requestId: "body-id" },
        200,
        { "X-Request-Id": "header-id" },
      ),
    );
    const result = await createClient(fetchMock).get<{ value: number }>("/health");
    expect(result.data).toEqual({ value: 1 });
    expect(result.requestId).toBe("header-id");
  });

  it("HTTP成功但业务 code 非 OK 时抛出 HttpError", async () => {
    const fetchMock = vi.fn<typeof fetch>().mockResolvedValue(
      jsonResponse({
        code: "DATA_VERSION_CONFLICT",
        message: "数据冲突",
        details: { currentRowVersion: 3 },
        requestId: "body-id",
      }),
    );
    await expect(createClient(fetchMock).get("/documents")).rejects.toMatchObject({
      name: "HttpError",
      httpStatus: 200,
      code: "DATA_VERSION_CONFLICT",
      requestId: "body-id",
    });
  });

  it("解析标准 JSON 错误", async () => {
    const fetchMock = vi.fn<typeof fetch>().mockResolvedValue(
      jsonResponse(
        {
          code: "ATTACHMENT_IN_USE",
          message: "附件正在使用",
          details: { mountedPlanCount: 2 },
          requestId: "error-id",
        },
        409,
      ),
    );
    await expect(createClient(fetchMock).delete("/attachments/1")).rejects.toMatchObject({
      httpStatus: 409,
      code: "ATTACHMENT_IN_USE",
      details: { mountedPlanCount: 2 },
      requestId: "error-id",
    });
  });

  it("将非 JSON 错误转换为统一 HttpError", async () => {
    const fetchMock = vi.fn<typeof fetch>().mockResolvedValue(
      new Response("<html>Bad gateway</html>", {
        status: 502,
        headers: { "Content-Type": "text/html", "X-Request-Id": "proxy-id" },
      }),
    );
    await expect(createClient(fetchMock).get("/documents")).rejects.toMatchObject({
      httpStatus: 502,
      code: "INTERNAL_ERROR",
      message: "<html>Bad gateway</html>",
      requestId: "proxy-id",
    });
  });

  it("将网络异常转换为带 cause 的统一 HttpError", async () => {
    const cause = new TypeError("offline");
    const fetchMock = vi.fn<typeof fetch>().mockRejectedValue(cause);
    try {
      await createClient(fetchMock).get("/documents");
      throw new Error("expected request to fail");
    } catch (error) {
      expect(error).toBeInstanceOf(HttpError);
      expect(error).toMatchObject({
        httpStatus: 0,
        code: "INTERNAL_ERROR",
        requestId: "generated-request-id",
        cause,
      });
    }
  });

  it("存在 Token 时自动注入 Bearer 头", async () => {
    const fetchMock = vi.fn<typeof fetch>().mockResolvedValue(
      jsonResponse({ code: "OK", message: "success", data: {}, requestId: "id" }),
    );
    await createClient(fetchMock, "access-token").get("/documents");
    const headers = new Headers(fetchMock.mock.calls[0][1]?.headers);
    expect(headers.get("Authorization")).toBe("Bearer access-token");
  });

  it("无 Token 时不发送 Authorization 头", async () => {
    const fetchMock = vi.fn<typeof fetch>().mockResolvedValue(
      jsonResponse({ code: "OK", message: "success", data: {}, requestId: "id" }),
    );
    await createClient(fetchMock).get("/health", { auth: false });
    const headers = new Headers(fetchMock.mock.calls[0][1]?.headers);
    expect(headers.has("Authorization")).toBe(false);
  });

  it("生成 requestId，并透传调用方提供的 requestId", async () => {
    const fetchMock = vi.fn<typeof fetch>()
      .mockResolvedValueOnce(
        jsonResponse({ code: "OK", message: "success", data: {}, requestId: "generated-request-id" }),
      )
      .mockResolvedValueOnce(
        jsonResponse({ code: "OK", message: "success", data: {}, requestId: "caller-id" }),
      );
    const client = createClient(fetchMock);
    await client.get("/health");
    await client.get("/health", { headers: { "X-Request-Id": "caller-id" } });
    expect(new Headers(fetchMock.mock.calls[0][1]?.headers).get("X-Request-Id")).toBe(
      "generated-request-id",
    );
    expect(new Headers(fetchMock.mock.calls[1][1]?.headers).get("X-Request-Id")).toBe(
      "caller-id",
    );
  });

  it("JSON 请求自动设置 Content-Type", async () => {
    const fetchMock = vi.fn<typeof fetch>().mockResolvedValue(
      jsonResponse({ code: "OK", message: "success", data: {}, requestId: "id" }),
    );
    await createClient(fetchMock).post("/example", { enabled: false });
    const init = fetchMock.mock.calls[0][1];
    expect(new Headers(init?.headers).get("Content-Type")).toBe("application/json");
    expect(init?.body).toBe('{"enabled":false}');
  });

  it("FormData 请求不手工设置 Content-Type", async () => {
    const fetchMock = vi.fn<typeof fetch>().mockResolvedValue(
      jsonResponse({ code: "OK", message: "success", data: {}, requestId: "id" }),
    );
    const form = new FormData();
    form.append("metadata", "{}");
    await createClient(fetchMock).post("/documents", form, {
      headers: { "Content-Type": "multipart/form-data" },
    });
    const headers = new Headers(fetchMock.mock.calls[0][1]?.headers);
    expect(headers.has("Content-Type")).toBe(false);
  });

  it("Blob 成功响应返回文件和必要响应头", async () => {
    const fetchMock = vi.fn<typeof fetch>().mockResolvedValue(
      new Response(new Blob(["pdf"], { type: "application/pdf" }), {
        status: 200,
        headers: {
          "Content-Type": "application/pdf",
          "Content-Disposition": "inline; filename=test.pdf",
          "X-Request-Id": "blob-id",
        },
      }),
    );
    const result = await createClient(fetchMock).getBlob("/documents/1/preview");
    expect(result.blob.size).toBe(3);
    expect(result.contentType).toBe("application/pdf");
    expect(result.contentDisposition).toBe("inline; filename=test.pdf");
    expect(result.requestId).toBe("blob-id");
  });

  it("Blob 接口返回 JSON 错误时不把错误当文件", async () => {
    const fetchMock = vi.fn<typeof fetch>().mockResolvedValue(
      jsonResponse(
        {
          code: "PREVIEW_UNAVAILABLE",
          message: "无法预览",
          details: null,
          requestId: "preview-id",
        },
        415,
      ),
    );
    await expect(
      createClient(fetchMock).getBlob("/documents/1/preview"),
    ).rejects.toMatchObject({
      httpStatus: 415,
      code: "PREVIEW_UNAVAILABLE",
      requestId: "preview-id",
    });
  });

  it("401 清理两处 Token 并只发布一次登录失效事件", async () => {
    sessionStorage.setItem(DMS_ACCESS_TOKEN_KEY, "session-token");
    localStorage.setItem(DMS_ACCESS_TOKEN_KEY, "local-token");
    let notificationCount = 0;
    const unsubscribe = subscribeAuthExpired(() => {
      notificationCount += 1;
    });
    const fetchMock = vi.fn<typeof fetch>().mockResolvedValue(
      jsonResponse(
        {
          code: "TOKEN_EXPIRED",
          message: "登录已失效",
          details: null,
          requestId: "expired-id",
        },
        401,
      ),
    );

    await Promise.allSettled([
      createClient(fetchMock).get("/documents"),
      createClient(fetchMock).get("/documents"),
    ]);

    expect(tokenStore.getAccessToken()).toBeNull();
    expect(notificationCount).toBe(1);
    expect(publishAuthExpired({ requestId: "again" })).toBe(false);
    unsubscribe();
  });

  it("无需认证的登录请求401不清理现有会话或发布失效事件", async () => {
    sessionStorage.setItem(DMS_ACCESS_TOKEN_KEY, "existing-token");
    let notificationCount = 0;
    const unsubscribe = subscribeAuthExpired(() => {
      notificationCount += 1;
    });
    const fetchMock = vi.fn<typeof fetch>().mockResolvedValue(
      jsonResponse(
        {
          code: "INVALID_CREDENTIALS",
          message: "用户名或密码错误",
          details: null,
          requestId: "login-id",
        },
        401,
      ),
    );

    await expect(
      createClient(fetchMock).post(
        "/auth/login",
        { username: "bad", password: "bad", keepSignedIn: false },
        { auth: false },
      ),
    ).rejects.toMatchObject({ code: "INVALID_CREDENTIALS" });

    expect(tokenStore.getAccessToken()).toBe("existing-token");
    expect(notificationCount).toBe(0);
    unsubscribe();
  });
});
