import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import { createStorage } from "unstorage";
import memoryDriver from "unstorage/drivers/memory";

import { AuthStorage } from "@/data/storage/auth";
import { deviceIdentifier } from "@/utils/device_identifier";
import { Result } from "@/utils/result";
import { SpAsyncUtil } from "@/utils/storage";

import { ErrorCode } from "../api_result";
import { createHttpClient } from "../http_client";

function jsonResponse(body: unknown, init?: ResponseInit): Response {
  return new Response(JSON.stringify(body), {
    headers: { "Content-Type": "application/json" },
    ...init,
  });
}

function callUrl(call: Parameters<typeof fetch>): URL {
  const request = call[0];
  return new URL(request instanceof Request ? request.url : String(request));
}

function callHeaders(call: Parameters<typeof fetch>): Headers {
  const [request, init] = call;
  return request instanceof Request
    ? request.headers
    : new Headers(init?.headers);
}

describe("createHttpClient", () => {
  beforeEach(() => {
    process.env.NEXT_PUBLIC_API_BASE_URL = "https://api.example.test";
    SpAsyncUtil.setStorage(createStorage({ driver: memoryDriver() }));
    AuthStorage._resetForTests();
  });

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

  it("refreshes a business token and replays the original request once after 401", async () => {
    const authStorage = AuthStorage.getInstance();
    await authStorage.setLoginToken("old-login-token");
    await authStorage.setRefreshToken("refresh-token");

    const requestAuthHeaders: (string | null)[] = [];
    const fetchMock = vi.fn<typeof fetch>(async (input, init) => {
      const url = new URL(input instanceof Request ? input.url : String(input));
      const headers =
        input instanceof Request ? input.headers : new Headers(init?.headers);
      requestAuthHeaders.push(headers.get("Authorization"));

      if (url.pathname === "/api/auth/refresh") {
        return jsonResponse({
          success: true,
          data: {
            token: "new-login-token",
            refreshToken: "new-refresh-token",
          },
        });
      }

      if (
        url.pathname === "/api/chat/history" &&
        fetchMock.mock.calls.filter(
          (call) => callUrl(call).pathname === "/api/chat/history",
        ).length === 1
      ) {
        return jsonResponse(
          { message: "expired" },
          { status: 401, statusText: "Unauthorized" },
        );
      }

      return jsonResponse({ success: true, data: { ok: true } });
    });
    vi.stubGlobal("fetch", fetchMock);

    const http = createHttpClient();
    await expect(http("/api/chat/history")).resolves.toEqual({
      success: true,
      data: { ok: true },
    });

    expect(fetchMock).toHaveBeenCalledTimes(3);
    expect(fetchMock.mock.calls.map((call) => callUrl(call).pathname)).toEqual([
      "/api/chat/history",
      "/api/auth/refresh",
      "/api/chat/history",
    ]);
    expect(requestAuthHeaders).toEqual([
      "Bearer old-login-token",
      null,
      "Bearer new-login-token",
    ]);
  });

  it("rejects with an API unauthorized error when no refresh token exists", async () => {
    const authStorage = AuthStorage.getInstance();
    await authStorage.setLoginToken("old-login-token");

    const fetchMock = vi.fn<typeof fetch>(async () =>
      jsonResponse(
        { message: "expired" },
        { status: ErrorCode.httpUnauthorized, statusText: "Unauthorized" },
      ),
    );
    vi.stubGlobal("fetch", fetchMock);

    const http = createHttpClient();
    await expect(http("/api/chat/history")).rejects.toMatchObject({
      name: "ApiError",
      code: "HTTP_UNAUTHORIZED",
      status: ErrorCode.httpUnauthorized,
      message: "No refresh token available",
    });

    expect(fetchMock).toHaveBeenCalledTimes(1);
  });

  it("rejects with API status context when token refresh fails", async () => {
    const authStorage = AuthStorage.getInstance();
    await authStorage.setLoginToken("old-login-token");
    await authStorage.setRefreshToken("refresh-token");

    const fetchMock = vi.fn<typeof fetch>(async (input) => {
      const url = new URL(input instanceof Request ? input.url : String(input));
      if (url.pathname === "/api/auth/refresh") {
        return jsonResponse(
          { message: "refresh broken" },
          { status: ErrorCode.httpServerError, statusText: "Server Error" },
        );
      }
      return jsonResponse(
        { message: "expired" },
        { status: ErrorCode.httpUnauthorized, statusText: "Unauthorized" },
      );
    });
    vi.stubGlobal("fetch", fetchMock);

    const http = createHttpClient();
    await expect(http("/api/chat/history")).rejects.toMatchObject({
      name: "ApiError",
      code: "HTTP_SERVER_ERROR",
      status: ErrorCode.httpServerError,
      message: "Refresh failed",
    });

    expect(fetchMock.mock.calls.map((call) => callUrl(call).pathname)).toEqual([
      "/api/chat/history",
      "/api/auth/refresh",
    ]);
    await expect(authStorage.getLoginToken()).resolves.toEqual(Result.ok(null));
    await expect(authStorage.getRefreshToken()).resolves.toEqual(Result.ok(null));
  });

  it("rejects instead of replaying when the refreshed token cannot be stored", async () => {
    const authStorage = AuthStorage.getInstance();
    await authStorage.setLoginToken("old-login-token");
    await authStorage.setRefreshToken("refresh-token");
    vi.spyOn(authStorage, "setLoginToken").mockResolvedValue(
      Result.err(new Error("login token write failed")),
    );

    const fetchMock = vi.fn<typeof fetch>(async (input) => {
      const url = new URL(input instanceof Request ? input.url : String(input));
      if (url.pathname === "/api/auth/refresh") {
        return jsonResponse({
          success: true,
          data: {
            token: "new-login-token",
            refreshToken: "new-refresh-token",
          },
        });
      }
      return jsonResponse(
        { message: "expired" },
        { status: ErrorCode.httpUnauthorized, statusText: "Unauthorized" },
      );
    });
    vi.stubGlobal("fetch", fetchMock);

    const http = createHttpClient();
    await expect(http("/api/chat/history")).rejects.toMatchObject({
      message: "login token write failed",
    });

    expect(fetchMock.mock.calls.map((call) => callUrl(call).pathname)).toEqual([
      "/api/chat/history",
      "/api/auth/refresh",
    ]);
  });

  it("propagates refresh-token read failures instead of treating them as missing", async () => {
    const authStorage = AuthStorage.getInstance();
    await authStorage.setLoginToken("old-login-token");
    vi.spyOn(authStorage, "getRefreshToken").mockResolvedValue(
      Result.err(new Error("refresh token read failed")),
    );
    const fetchMock = vi.fn<typeof fetch>(async () =>
      jsonResponse(
        { message: "expired" },
        { status: ErrorCode.httpUnauthorized, statusText: "Unauthorized" },
      ),
    );
    vi.stubGlobal("fetch", fetchMock);

    const http = createHttpClient();
    await expect(http("/api/chat/history")).rejects.toMatchObject({
      message: "refresh token read failed",
    });

    expect(fetchMock).toHaveBeenCalledTimes(1);
  });

  it("propagates login-state read failures without attempting a guest refresh", async () => {
    const authStorage = AuthStorage.getInstance();
    await authStorage.setLoginToken("old-login-token");
    vi.spyOn(authStorage, "hasLoginToken").mockResolvedValue(
      Result.err(new Error("login state read failed")),
    );
    const fetchMock = vi.fn<typeof fetch>(async () =>
      jsonResponse(
        { message: "expired" },
        { status: ErrorCode.httpUnauthorized, statusText: "Unauthorized" },
      ),
    );
    vi.stubGlobal("fetch", fetchMock);

    const http = createHttpClient();
    await expect(http("/api/chat/history")).rejects.toMatchObject({
      message: "login state read failed",
    });

    expect(fetchMock).toHaveBeenCalledTimes(1);
  });

  it("rejects a guest refresh when the new guest token cannot be stored", async () => {
    const authStorage = AuthStorage.getInstance();
    await authStorage.setGuestToken("old-guest-token");
    vi.spyOn(deviceIdentifier, "getDeviceId").mockResolvedValue("device-1");
    vi.spyOn(authStorage, "setGuestToken").mockResolvedValue(
      Result.err(new Error("guest token write failed")),
    );
    const fetchMock = vi.fn<typeof fetch>(async (input) => {
      const url = new URL(input instanceof Request ? input.url : String(input));
      if (url.pathname === "/api/auth/guest") {
        return jsonResponse({
          success: true,
          data: { token: "new-guest-token", deviceId: "device-1" },
        });
      }
      return jsonResponse(
        { message: "expired" },
        { status: ErrorCode.httpUnauthorized, statusText: "Unauthorized" },
      );
    });
    vi.stubGlobal("fetch", fetchMock);

    const http = createHttpClient();
    await expect(http("/api/chat/history")).rejects.toMatchObject({
      message: "guest token write failed",
    });

    expect(fetchMock.mock.calls.map((call) => callUrl(call).pathname)).toEqual([
      "/api/chat/history",
      "/api/auth/guest",
    ]);
    await expect(authStorage.getGuestToken()).resolves.toEqual(Result.ok(null));
  });

  it("lets ofetch set JSON content headers for object bodies", async () => {
    const fetchMock = vi.fn<typeof fetch>(async () =>
      jsonResponse({ success: true }),
    );
    vi.stubGlobal("fetch", fetchMock);

    const http = createHttpClient();
    await http("/api/example", {
      method: "POST",
      body: { message: "hello" },
    });

    expect(callHeaders(fetchMock.mock.calls[0]).get("Content-Type")).toContain(
      "application/json",
    );
  });

  it("does not override multipart boundaries for FormData bodies", async () => {
    const fetchMock = vi.fn<typeof fetch>(async () =>
      jsonResponse({ success: true }),
    );
    vi.stubGlobal("fetch", fetchMock);
    const body = new FormData();
    body.append("content", "feedback");

    const http = createHttpClient();
    await http("/api/feedback", { method: "POST", body });

    expect(callHeaders(fetchMock.mock.calls[0]).get("Content-Type")).toBeNull();
  });
});
