import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
/* eslint-disable @typescript-eslint/no-unsafe-assignment */
/* eslint-disable @typescript-eslint/no-non-null-assertion */
import { apiRequest, apiGet, apiPost, apiPatch, apiDelete } from "./client";

// Mock fetch globally
const mockFetch = vi.fn();
global.fetch = mockFetch;

describe("API Client", (): void => {
  beforeEach((): void => {
    mockFetch.mockClear();
  });

  afterEach((): void => {
    vi.resetAllMocks();
  });

  describe("apiRequest", (): void => {
    it("should make a successful GET request", async (): Promise<void> => {
      const mockData = { id: "1", name: "Test" };
      mockFetch.mockResolvedValueOnce({
        ok: true,
        json: () => Promise.resolve(mockData),
      });

      const result = await apiRequest<typeof mockData>("/test");

      expect(mockFetch).toHaveBeenCalledWith(
        "http://localhost:3001/test",
        expect.objectContaining({
          headers: expect.objectContaining({
            "Content-Type": "application/json",
          }),
          credentials: "include",
        })
      );
      expect(result).toEqual(mockData);
    });

    it("should include custom headers", async (): Promise<void> => {
      mockFetch.mockResolvedValueOnce({
        ok: true,
        json: () => Promise.resolve({}),
      });

      await apiRequest("/test", {
        headers: { Authorization: "Bearer token123" },
      });

      expect(mockFetch).toHaveBeenCalledWith(
        "http://localhost:3001/test",
        expect.objectContaining({
          headers: expect.objectContaining({
            "Content-Type": "application/json",
            Authorization: "Bearer token123",
          }),
        })
      );
    });

    it("should throw error on failed request", async (): Promise<void> => {
      mockFetch.mockResolvedValueOnce({
        ok: false,
        statusText: "Not Found",
        json: () =>
          Promise.resolve({
            code: "NOT_FOUND",
            message: "Resource not found",
          }),
      });

      await expect(apiRequest("/test")).rejects.toThrow("Resource not found");
    });

    it("should handle errors when JSON parsing fails", async (): Promise<void> => {
      mockFetch.mockResolvedValueOnce({
        ok: false,
        statusText: "Internal Server Error",
        json: () => Promise.reject(new Error("Invalid JSON")),
      });

      await expect(apiRequest("/test")).rejects.toThrow("Internal Server Error");
    });
  });

  describe("apiGet", (): void => {
    it("should make a GET request", async (): Promise<void> => {
      const mockData = { id: "1" };
      mockFetch.mockResolvedValueOnce({
        ok: true,
        json: () => Promise.resolve(mockData),
      });

      const result = await apiGet<typeof mockData>("/test");

      expect(mockFetch).toHaveBeenCalledWith(
        "http://localhost:3001/test",
        expect.objectContaining({ method: "GET" })
      );
      expect(result).toEqual(mockData);
    });
  });

  describe("apiPost", (): void => {
    it("should make a POST request with data", async (): Promise<void> => {
      const postData = { name: "New Item" };
      const mockResponse = { id: "1", ...postData };
      mockFetch.mockResolvedValueOnce({
        ok: true,
        json: () => Promise.resolve(mockResponse),
      });

      const result = await apiPost<typeof mockResponse>("/test", postData);

      expect(mockFetch).toHaveBeenCalledWith(
        "http://localhost:3001/test",
        expect.objectContaining({
          method: "POST",
          body: JSON.stringify(postData),
        })
      );
      expect(result).toEqual(mockResponse);
    });

    it("should make a POST request without data", async (): Promise<void> => {
      mockFetch.mockResolvedValueOnce({
        ok: true,
        json: () => Promise.resolve({}),
      });

      await apiPost("/test");

      expect(mockFetch).toHaveBeenCalledWith(
        "http://localhost:3001/test",
        expect.objectContaining({
          method: "POST",
          // When no data is provided, body property is not set (not undefined)
        })
      );

      // Verify body is not in the call
      const callArgs = mockFetch.mock.calls[0]![1] as RequestInit;
      expect(callArgs.body).toBeUndefined();
    });
  });

  describe("apiPatch", (): void => {
    it("should make a PATCH request with data", async (): Promise<void> => {
      const patchData = { name: "Updated" };
      const mockResponse = { id: "1", ...patchData };
      mockFetch.mockResolvedValueOnce({
        ok: true,
        json: () => Promise.resolve(mockResponse),
      });

      const result = await apiPatch<typeof mockResponse>("/test/1", patchData);

      expect(mockFetch).toHaveBeenCalledWith(
        "http://localhost:3001/test/1",
        expect.objectContaining({
          method: "PATCH",
          body: JSON.stringify(patchData),
        })
      );
      expect(result).toEqual(mockResponse);
    });
  });

  describe("apiDelete", (): void => {
    it("should make a DELETE request", async (): Promise<void> => {
      mockFetch.mockResolvedValueOnce({
        ok: true,
        json: () => Promise.resolve({ success: true }),
      });

      const result = await apiDelete<{ success: boolean }>("/test/1");

      expect(mockFetch).toHaveBeenCalledWith(
        "http://localhost:3001/test/1",
        expect.objectContaining({ method: "DELETE" })
      );
      expect(result).toEqual({ success: true });
    });
  });

  describe("error handling", (): void => {
    it("should handle network errors", async (): Promise<void> => {
      mockFetch.mockRejectedValueOnce(new Error("Network request failed"));

      await expect(apiGet("/test")).rejects.toThrow("Network request failed");
    });

    it("should handle 401 unauthorized errors", async (): Promise<void> => {
      mockFetch.mockResolvedValueOnce({
        ok: false,
        statusText: "Unauthorized",
        status: 401,
        json: () =>
          Promise.resolve({
            code: "UNAUTHORIZED",
            message: "Authentication required",
          }),
      });

      await expect(apiGet("/test")).rejects.toThrow("Authentication required");
    });

    it("should handle 403 forbidden errors", async (): Promise<void> => {
      mockFetch.mockResolvedValueOnce({
        ok: false,
        statusText: "Forbidden",
        status: 403,
        json: () =>
          Promise.resolve({
            code: "FORBIDDEN",
            message: "Access denied",
          }),
      });

      await expect(apiGet("/test")).rejects.toThrow("Access denied");
    });

    it("should handle 404 not found errors", async (): Promise<void> => {
      mockFetch.mockResolvedValueOnce({
        ok: false,
        statusText: "Not Found",
        status: 404,
        json: () =>
          Promise.resolve({
            code: "NOT_FOUND",
            message: "Resource not found",
          }),
      });

      await expect(apiGet("/test")).rejects.toThrow("Resource not found");
    });

    it("should handle 500 server errors", async (): Promise<void> => {
      mockFetch.mockResolvedValueOnce({
        ok: false,
        statusText: "Internal Server Error",
        status: 500,
        json: () =>
          Promise.resolve({
            code: "INTERNAL_ERROR",
            message: "Internal server error",
          }),
      });

      await expect(apiGet("/test")).rejects.toThrow("Internal server error");
    });

    it("should handle malformed JSON responses", async (): Promise<void> => {
      mockFetch.mockResolvedValueOnce({
        ok: true,
        json: () => Promise.reject(new Error("Unexpected token in JSON")),
      });

      await expect(apiGet("/test")).rejects.toThrow("Unexpected token in JSON");
    });

    it("should handle empty error responses", async (): Promise<void> => {
      mockFetch.mockResolvedValueOnce({
        ok: false,
        statusText: "Bad Request",
        status: 400,
        json: () => Promise.reject(new Error("No JSON body")),
      });

      await expect(apiGet("/test")).rejects.toThrow("Bad Request");
    });

    it("should handle timeout errors", async (): Promise<void> => {
      mockFetch.mockImplementationOnce(() => {
        return new Promise((_, reject) => {
          setTimeout(() => {
            reject(new Error("Request timeout"));
          }, 1);
        });
      });

      await expect(apiGet("/test")).rejects.toThrow("Request timeout");
    });

    it("should handle malformed error responses with details", async (): Promise<void> => {
      mockFetch.mockResolvedValueOnce({
        ok: false,
        statusText: "Validation Error",
        status: 422,
        json: () =>
          Promise.resolve({
            code: "VALIDATION_ERROR",
            message: "Invalid input",
            details: {
              fields: {
                email: "Invalid email format",
                password: "Password too short",
              },
            },
          }),
      });

      await expect(apiGet("/test")).rejects.toThrow("Invalid input");
    });

    it("should handle CORS errors", async (): Promise<void> => {
      mockFetch.mockRejectedValueOnce(new TypeError("Failed to fetch"));

      await expect(apiGet("/test")).rejects.toThrow("Failed to fetch");
    });

    it("should handle rate limit errors", async (): Promise<void> => {
      mockFetch.mockResolvedValueOnce({
        ok: false,
        statusText: "Too Many Requests",
        status: 429,
        json: () =>
          Promise.resolve({
            code: "RATE_LIMIT_EXCEEDED",
            message: "Too many requests. Please try again later.",
          }),
      });

      await expect(apiGet("/test")).rejects.toThrow("Too many requests. Please try again later.");
    });

    it("should handle connection refused errors", async (): Promise<void> => {
      mockFetch.mockRejectedValueOnce({
        name: "FetchError",
        message: "request to http://localhost:3001/test failed, reason: connect ECONNREFUSED",
      });

      await expect(apiGet("/test")).rejects.toMatchObject({
        message: expect.stringContaining("ECONNREFUSED"),
      });
    });
  });
});
