import {
  afterAll,
  afterEach,
  beforeAll,
  describe,
  expect,
  it,
  vi,
} from "vitest"

import { ApiError, apiClient, apiDownload } from "@/lib/api-client"

import { server } from "../mocks/server"

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

describe("apiClient", () => {
  it("makes GET request to correct URL", async () => {
    const fetchSpy = vi.spyOn(globalThis, "fetch")

    await apiClient("/members")

    expect(fetchSpy).toHaveBeenCalledWith(
      "/api/backend/members",
      expect.objectContaining({ method: "GET" })
    )
    fetchSpy.mockRestore()
  })

  it("does not set Content-Type for GET requests", async () => {
    const fetchSpy = vi.spyOn(globalThis, "fetch")

    await apiClient("/members")

    const callArgs = fetchSpy.mock.calls[0]
    const headers = callArgs[1]?.headers as Record<string, string>
    expect(headers["Content-Type"]).toBeUndefined()
    fetchSpy.mockRestore()
  })

  it("sets Content-Type for POST requests", async () => {
    const fetchSpy = vi.spyOn(globalThis, "fetch")

    await apiClient("/members", {
      method: "POST",
      body: { firstName: "Test" },
    })

    const callArgs = fetchSpy.mock.calls[0]
    const headers = callArgs[1]?.headers as Record<string, string>
    expect(headers["Content-Type"]).toBe("application/json")
    fetchSpy.mockRestore()
  })

  it("passes token as Authorization header when provided", async () => {
    const fetchSpy = vi.spyOn(globalThis, "fetch")

    await apiClient("/members", { token: "my-jwt-token" })

    const callArgs = fetchSpy.mock.calls[0]
    const headers = callArgs[1]?.headers as Record<string, string>
    expect(headers["Authorization"]).toBe("Bearer my-jwt-token")
    fetchSpy.mockRestore()
  })

  it("serializes query params correctly", async () => {
    const fetchSpy = vi.spyOn(globalThis, "fetch")

    await apiClient("/members", {
      params: { page: 0, size: 10, search: "Max", status: undefined },
    })

    const url = fetchSpy.mock.calls[0][0] as string
    expect(url).toContain("page=0")
    expect(url).toContain("size=10")
    expect(url).toContain("search=Max")
    expect(url).not.toContain("status")
    fetchSpy.mockRestore()
  })

  it("throws ApiError on non-2xx response with correct status/code/message", async () => {
    // Override the handler for this test
    const { http, HttpResponse } = await import("msw")
    server.use(
      http.get("/api/backend/fail-endpoint", () => {
        return HttpResponse.json(
          { code: "VALIDATION_ERROR", message: "Name is required" },
          { status: 422 }
        )
      })
    )

    try {
      await apiClient("/fail-endpoint")
      expect.fail("Should have thrown")
    } catch (error) {
      expect(error).toBeInstanceOf(ApiError)
      const apiError = error as ApiError
      expect(apiError.status).toBe(422)
      expect(apiError.code).toBe("VALIDATION_ERROR")
      expect(apiError.message).toBe("Name is required")
      expect(apiError.isAuthError).toBe(false)
      expect(apiError.isServerError).toBe(false)
    }
  })

  it("throws ApiError with status 0 on network failure", async () => {
    // Mock fetch to throw TypeError (real network failure behavior)
    const fetchSpy = vi
      .spyOn(globalThis, "fetch")
      .mockRejectedValueOnce(new TypeError("Failed to fetch"))

    try {
      await apiClient("/network-fail")
      expect.fail("Should have thrown")
    } catch (error) {
      expect(error).toBeInstanceOf(ApiError)
      const apiError = error as ApiError
      expect(apiError.status).toBe(0)
      expect(apiError.code).toBe("NETWORK_ERROR")
      expect(apiError.isNetworkError).toBe(true)
    }

    fetchSpy.mockRestore()
  })

  it("returns undefined for 204 No Content responses", async () => {
    const result = await apiClient<void>("/members/m1", { method: "DELETE" })
    expect(result).toBeUndefined()
  })
})

describe("apiDownload", () => {
  it("returns blob and filename from Content-Disposition", async () => {
    const { http, HttpResponse } = await import("msw")
    server.use(
      http.get("/api/backend/reports/download", () => {
        return new HttpResponse(new Blob(["pdf-content"]), {
          headers: {
            "Content-Type": "application/pdf",
            "Content-Disposition": 'attachment; filename="report.pdf"',
          },
        })
      })
    )

    const result = await apiDownload("/reports/download")
    expect(result.filename).toBe("report.pdf")
    expect(result.blob).toBeInstanceOf(Blob)
  })

  it("falls back to 'download' when no Content-Disposition", async () => {
    const { http, HttpResponse } = await import("msw")
    server.use(
      http.get("/api/backend/reports/no-header", () => {
        return new HttpResponse(new Blob(["csv-content"]), {
          headers: { "Content-Type": "text/csv" },
        })
      })
    )

    const result = await apiDownload("/reports/no-header")
    expect(result.filename).toBe("download")
  })

  it("throws ApiError on non-2xx download response", async () => {
    const { http, HttpResponse } = await import("msw")
    server.use(
      http.get("/api/backend/reports/forbidden", () => {
        return HttpResponse.json(
          { code: "FORBIDDEN", message: "Access denied" },
          { status: 403 }
        )
      })
    )

    try {
      await apiDownload("/reports/forbidden")
      expect.fail("Should have thrown")
    } catch (error) {
      expect(error).toBeInstanceOf(ApiError)
      const apiError = error as ApiError
      expect(apiError.status).toBe(403)
      expect(apiError.isAuthError).toBe(true)
    }
  })
})
