import { waitFor } from "@testing-library/dom";
import { createHttpMockingServer } from "@hyper-fetch/testing";

import { Client } from "client";

const { resetMocks, startServer, stopServer, mockRequest } = createHttpMockingServer();

describe("Request [ retryOnError ]", () => {
  let client = new Client({ url: "shared-base-url" });

  beforeAll(() => {
    startServer();
  });

  beforeEach(() => {
    client = new Client({ url: "shared-base-url" });
    resetMocks();
    vi.resetAllMocks();
  });

  afterAll(() => {
    stopServer();
  });

  describe("When setting retryOnError via setRetryOnError", () => {
    it("should set the retryOnError callback", () => {
      const request = client.createRequest()({ endpoint: "/users", retry: 2 });
      const callback = () => true;

      expect(request.retryOnError).toBeUndefined();
      const updated = request.setRetryOnError(callback);
      expect(updated.retryOnError).toBeDefined();
    });

    it("should inherit retryOnError through clone", () => {
      const request = client.createRequest()({ endpoint: "/users", retry: 2 });
      const callback = () => true;

      const updated = request.setRetryOnError(callback);
      const cloned = updated.setHeaders({ "x-custom": "test" });

      expect(cloned.retryOnError).toBeDefined();
    });

    it("should not affect original when clone retryOnError is modified", () => {
      const request = client.createRequest()({ endpoint: "/users", retry: 2 });
      const callback = () => true;
      const updated = request.setRetryOnError(callback);
      const cloned = updated.setHeaders({ "x-custom": "test" });
      cloned.retryOnError = () => false;

      expect(updated.retryOnError).toBeDefined();
      expect(updated.retryOnError!(null as any)).toBe(true);
    });

    it("should return a new request instance (immutable)", () => {
      const request = client.createRequest()({ endpoint: "/users", retry: 2 });
      const updated = request.setRetryOnError(() => true);

      expect(updated).not.toBe(request);
      expect(request.retryOnError).toBeUndefined();
    });
  });

  describe("When retryOnError returns true (allow retry)", () => {
    it("should retry the request as normal", async () => {
      const request = client.createRequest()({
        endpoint: "/users",
        retry: 2,
        retryTime: 0,
      });
      mockRequest(request, { status: 400, delay: 0 });

      const retryOnErrorSpy = vi.fn().mockReturnValue(true);
      const retryingRequest = request.setRetryOnError(retryOnErrorSpy);

      const spy = vi.spyOn(client.adapter, "fetch");
      retryingRequest.send();

      await waitFor(() => {
        expect(spy).toHaveBeenCalledTimes(3);
      });

      // Called once per attempt where retry is still available (not on the last exhausted attempt)
      expect(retryOnErrorSpy).toHaveBeenCalledTimes(2);
    });

    it("should call retryOnError with the response object", async () => {
      const request = client.createRequest()({
        endpoint: "/users",
        retry: 1,
        retryTime: 0,
      });
      mockRequest(request, { status: 500, delay: 0 });

      const retryOnErrorSpy = vi.fn().mockReturnValue(true);
      const retryingRequest = request.setRetryOnError(retryOnErrorSpy);

      retryingRequest.send();

      await waitFor(() => {
        expect(retryOnErrorSpy).toHaveBeenCalledTimes(1);
      });

      const firstCallArg = retryOnErrorSpy.mock.calls[0][0];
      expect(firstCallArg).toHaveProperty("error");
      expect(firstCallArg).toHaveProperty("status");
      expect(firstCallArg).toHaveProperty("success", false);
      expect(firstCallArg).toHaveProperty("data");
      expect(firstCallArg.status).toBe(500);
    });
  });

  describe("When retryOnError returns false (stop retry)", () => {
    it("should not retry when retryOnError returns false", async () => {
      const request = client.createRequest()({
        endpoint: "/users",
        retry: 3,
        retryTime: 0,
      });
      mockRequest(request, { status: 404, delay: 0 });

      const retryingRequest = request.setRetryOnError(() => false);

      const spy = vi.spyOn(client.adapter, "fetch");
      retryingRequest.send();

      await waitFor(() => {
        expect(spy).toHaveBeenCalledTimes(1);
      });

      await new Promise((r) => {
        setTimeout(r, 50);
      });
      expect(spy).toHaveBeenCalledTimes(1);
    });

    it("should resolve the send promise immediately when retryOnError returns false", async () => {
      const request = client.createRequest()({
        endpoint: "/users",
        retry: 3,
        retryTime: 0,
      });
      mockRequest(request, { status: 404, delay: 0 });

      const retryingRequest = request.setRetryOnError(() => false);

      const { status, success } = await retryingRequest.send();
      expect(status).toBe(404);
      expect(success).toBe(false);
    });

    it("should stop retrying based on status code (e.g. 404)", async () => {
      const request = client.createRequest()({
        endpoint: "/users",
        retry: 5,
        retryTime: 0,
      });
      mockRequest(request, { status: 404, delay: 0 });

      const retryingRequest = request.setRetryOnError((response) => {
        return response.status !== 404;
      });

      const spy = vi.spyOn(client.adapter, "fetch");
      const result = await retryingRequest.send();

      expect(spy).toHaveBeenCalledTimes(1);
      expect(result.status).toBe(404);
      expect(result.success).toBe(false);
    });

    it("should stop retrying based on error content", async () => {
      const request = client.createRequest()({
        endpoint: "/users",
        retry: 5,
        retryTime: 0,
      });
      mockRequest(request, { status: 403, delay: 0 });

      const retryingRequest = request.setRetryOnError((response) => {
        return response.status !== 403;
      });

      const spy = vi.spyOn(client.adapter, "fetch");
      const result = await retryingRequest.send();

      expect(spy).toHaveBeenCalledTimes(1);
      expect(result.status).toBe(403);
    });
  });

  describe("When retryOnError conditionally allows or disallows retries", () => {
    it("should retry on 500 but not on 404", async () => {
      let callCount = 0;
      const request = client.createRequest()({
        endpoint: "/users",
        retry: 5,
        retryTime: 0,
      });

      const retryOnErrorSpy = vi.fn().mockImplementation((response) => {
        return response.status !== 404;
      });
      const retryingRequest = request.setRetryOnError(retryOnErrorSpy);

      const originalFetch = client.adapter.fetch.bind(client.adapter);
      vi.spyOn(client.adapter, "fetch").mockImplementation(async (...args) => {
        callCount += 1;
        if (callCount === 1) {
          mockRequest(request, { status: 500, delay: 0 });
        } else {
          mockRequest(request, { status: 404, delay: 0 });
        }
        return originalFetch(...args);
      });

      mockRequest(request, { status: 500, delay: 0 });
      const result = await retryingRequest.send();

      expect(result.status).toBe(404);
      expect(result.success).toBe(false);
    });
  });

  describe("When retryOnError is not set", () => {
    it("should retry normally without retryOnError", async () => {
      const request = client.createRequest()({
        endpoint: "/users",
        retry: 2,
        retryTime: 0,
      });
      mockRequest(request, { status: 400, delay: 0 });

      const spy = vi.spyOn(client.adapter, "fetch");
      request.send();

      await waitFor(() => {
        expect(spy).toHaveBeenCalledTimes(3);
      });
    });

    it("should not retry when retry is 0 and no retryOnError", async () => {
      const request = client.createRequest()({
        endpoint: "/users",
        retry: 0,
      });
      mockRequest(request, { status: 400, delay: 0 });

      const spy = vi.spyOn(client.adapter, "fetch");
      request.send();

      await waitFor(() => {
        expect(spy).toHaveBeenCalledTimes(1);
      });
    });
  });

  describe("When retryOnError interacts with successful responses", () => {
    it("should not call retryOnError on successful response", async () => {
      const request = client.createRequest<{ response: { name: string } }>()({
        endpoint: "/users",
        retry: 3,
        retryTime: 0,
      });
      mockRequest(request, { status: 200, delay: 0, data: { name: "John" } });

      const retryOnErrorSpy = vi.fn().mockReturnValue(true);
      const retryingRequest = request.setRetryOnError(retryOnErrorSpy);

      const result = await retryingRequest.send();

      expect(result.success).toBe(true);
      expect(retryOnErrorSpy).not.toHaveBeenCalled();
    });
  });

  describe("When retryOnError is used with send() callbacks", () => {
    it("should fire onResponse when retryOnError stops retries", async () => {
      const request = client.createRequest()({
        endpoint: "/users",
        retry: 3,
        retryTime: 0,
      });
      mockRequest(request, { status: 404, delay: 0 });

      const onResponseSpy = vi.fn();
      const retryingRequest = request.setRetryOnError((response) => response.status !== 404);

      await retryingRequest.send({ onResponse: onResponseSpy });

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

  describe("When retryOnError is used with $hooks", () => {
    it("should fire $hooks.onResponse when retryOnError stops retries", async () => {
      const request = client.createRequest()({
        endpoint: "/users",
        retry: 3,
        retryTime: 0,
      });
      mockRequest(request, { status: 404, delay: 0 });

      const hookSpy = vi.fn();
      const retryingRequest = request.setRetryOnError((response) => response.status !== 404);
      retryingRequest.$hooks.onResponse(hookSpy);

      await retryingRequest.send();

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

  describe("When using setRetry chained with setRetryOnError", () => {
    it("should respect both retry count and retryOnError", async () => {
      const request = client
        .createRequest()({ endpoint: "/users" })
        .setRetry(3)
        .setRetryTime(0)
        .setRetryOnError(() => true);

      mockRequest(request, { status: 400, delay: 0 });

      const spy = vi.spyOn(client.adapter, "fetch");
      request.send();

      await waitFor(() => {
        expect(spy).toHaveBeenCalledTimes(4);
      });
    });

    it("should stop early when retryOnError returns false mid-retry", async () => {
      let attempt = 0;
      const request = client
        .createRequest()({ endpoint: "/users" })
        .setRetry(5)
        .setRetryTime(0)
        .setRetryOnError(() => {
          attempt += 1;
          return attempt <= 2;
        });

      mockRequest(request, { status: 500, delay: 0 });

      const spy = vi.spyOn(client.adapter, "fetch");
      request.send();

      await waitFor(() => {
        expect(spy).toHaveBeenCalledTimes(3);
      });

      await new Promise((r) => {
        setTimeout(r, 50);
      });
      expect(spy).toHaveBeenCalledTimes(3);
    });
  });
});
