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 { SpAsyncUtil } from "@/utils";

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));
}

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

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

  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: Array<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",
    ]);
  });
});
