import { createClient } from "@hyper-fetch/core";
import { act, waitFor } from "@testing-library/react";
import { createHttpMockingServer } from "@hyper-fetch/testing";

import { renderUseTrackedState } from "../../../utils";

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

describe("useTrackedState [ Actions ]", () => {
  let client = createClient({ url: "http://localhost:3000" });
  let request = client.createRequest<{ response: any }>()({ endpoint: "test" });

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

  afterEach(() => {
    resetMocks();
  });

  afterAll(() => {
    stopServer();
    vi.resetModules();
    vi.resetAllMocks();
    client.clear();
  });

  beforeEach(() => {
    client = createClient({ url: "http://localhost:3000" });
    request = client.createRequest<{ response: any }>()({ endpoint: "test" });
    request.setMock(() => {
      return {
        data: 123,
        status: 200,
      };
    });
  });

  describe("when updating the local state", () => {
    it("should allow to set data", async () => {
      const value = { test: 1 };
      const { result } = renderUseTrackedState(request);

      act(() => {
        result.current[1].setData(value);
      });

      expect(result.current[0].data).toBe(value);
    });
    it("should allow to set error", async () => {
      const value = { test: 1 };
      const { result } = renderUseTrackedState(request);

      act(() => {
        result.current[1].setError(value as unknown as Error);
      });

      expect(result.current[0].error).toBe(value);
    });
    it("should allow to set loading", async () => {
      const { result } = renderUseTrackedState(request);

      act(() => {
        result.current[1].setLoading(true);
      });

      expect(result.current[0].loading).toBeTrue();
    });
    it("should allow to set status", async () => {
      const { result } = renderUseTrackedState(request);

      act(() => {
        result.current[1].setStatus(900);
      });

      expect(result.current[0].status).toBe(900);
    });
    it("should allow to set success", async () => {
      const { result } = renderUseTrackedState(request);

      act(() => {
        result.current[1].setSuccess(false);
      });

      await waitFor(() => {
        expect(result.current[0].success).toBe(false);
      });
    });
    it("should allow to set extra", async () => {
      const extra = { headers: { test: "1" } };
      const { result } = renderUseTrackedState(request);

      act(() => {
        result.current[1].setExtra(extra);
      });

      await waitFor(() => {
        expect(result.current[0].extra).toBe(extra);
      });
    });
    it("should allow to set response timestamp", async () => {
      const value = new Date();
      const { result } = renderUseTrackedState(request);

      act(() => {
        result.current[1].setResponseTimestamp(value);
      });

      expect(result.current[0].responseTimestamp).toBe(value);
    });
    it("should allow to set request timestamp", async () => {
      const value = new Date();
      const { result } = renderUseTrackedState(request);

      act(() => {
        result.current[1].setRequestTimestamp(value);
      });

      expect(result.current[0].requestTimestamp).toBe(value);
    });
    it("should allow to set retries", async () => {
      const { result } = renderUseTrackedState(request);

      act(() => {
        result.current[1].setRetries(999);
      });

      expect(result.current[0].retries).toBe(999);
    });
  });
  describe("when using setData action", () => {
    it("should not update cache", async () => {
      await request.send();
      const { result } = renderUseTrackedState(request);

      // Then update state without emitting to hooks
      act(() => {
        result.current[1].setData("new-value");
      });

      // Cache should retain the initial value
      const cacheData = request.client.cache.get(request.cacheKey);
      expect(cacheData?.data).toBe(123);
      // Local state should have the new value
      expect(result.current[0].data).toBe("new-value");
    });

    it("should handle null previous data in function updates", async () => {
      const { result } = renderUseTrackedState(request);

      act(() => {
        result.current[1].setData((prev: any) => {
          expect(prev).toBe(null);
          return "new-value";
        });
      });

      const cacheData = request.client.cache.get(request.cacheKey);
      expect(cacheData?.data).toBeUndefined();
      expect(result.current[0].data).toBe("new-value");
    });
  });
  describe("when using setError action", () => {
    it("should handle null previous error in function updates", async () => {
      const { result } = renderUseTrackedState(request);

      act(() => {
        result.current[1].setError((prev: Error | null) => {
          expect(prev).toBe(null);
          return new Error("new error");
        });
      });

      expect(result.current[0].error).toBeInstanceOf(Error);
      expect(result.current[0].success).toBe(false);
    });

    it("should not update cache when emitToCache is false", async () => {
      const { result } = renderUseTrackedState(request);
      const initialError = new Error("initial error");

      // Set initial error in cache
      act(() => {
        result.current[1].setError(initialError);
      });

      // Update local state only
      const newError = new Error("new error");
      act(() => {
        result.current[1].setError(newError);
      });

      // Cache should retain initial error
      const cacheData = request.client.cache.get(request.cacheKey);
      expect(cacheData).toBe(undefined);
      // Local state should have new error
      expect(result.current[0].error).toBe(newError);
    });
  });
  describe("when using setLoading action", () => {
    it("should not emit loading event when emitToHooks is false", async () => {
      const { result } = renderUseTrackedState(request);
      const spy = vi.spyOn(request.client.requestManager.events, "emitLoading");

      act(() => {
        result.current[1].setLoading(true);
      });

      expect(result.current[0].loading).toBe(true);
      expect(spy).not.toHaveBeenCalled();
    });

    it("should accept a function updater", async () => {
      const { result } = renderUseTrackedState(request);

      act(() => {
        result.current[1].setLoading((prev: boolean | null) => {
          expect(prev).toBe(false);
          return true;
        });
      });

      expect(result.current[0].loading).toBe(true);
    });
  });
  describe("when using setStatus action", () => {
    it("should handle null previous status in function updates", async () => {
      const { result } = renderUseTrackedState(request);

      act(() => {
        result.current[1].setStatus((prev: number | null) => {
          expect(prev).toBe(null);
          return 404;
        });
      });

      expect(result.current[0].status).toBe(404);
    });

    it("should not update cache", async () => {
      const { result } = renderUseTrackedState(request);
      const spy = vi.spyOn(request.client.cache, "update");

      // Then update state without emitting to cache
      act(() => {
        result.current[1].setStatus(404);
      });

      expect(spy).toHaveBeenCalledTimes(0);
      // Local state should have the new value
      expect(result.current[0].status).toBe(404);
    });
  });
  describe("when using setSuccess action", () => {
    it("should handle null previous success in function updates", async () => {
      const { result } = renderUseTrackedState(request);

      act(() => {
        result.current[1].setSuccess((prev) => {
          expect(prev).toBe(false);
          return true;
        });
      });

      expect(result.current[0].success).toBe(true);
    });
  });
  describe("when using setExtra action", () => {
    it("should handle null previous extra in function updates", async () => {
      const { result } = renderUseTrackedState(request);

      act(() => {
        result.current[1].setExtra((prev: any) => {
          expect(prev).not.toStrictEqual({ count: 1, headers: {} });
          return { count: 1, headers: {} };
        });
      });

      expect(result.current[0].extra).toEqual({ count: 1, headers: {} });
    });
  });
  describe("when using setRetries action", () => {
    it("should handle initial retries value in function updates", async () => {
      const { result } = renderUseTrackedState(request);

      act(() => {
        result.current[1].setRetries((prev) => {
          expect(prev).toBe(0);
          return 5;
        });
      });

      expect(result.current[0].retries).toBe(5);
    });
  });
  describe("when using setResponseTimestamp action", () => {
    it("should handle null previous responseTimestamp in function updates", async () => {
      const { result } = renderUseTrackedState(request);
      const newDate = new Date();

      act(() => {
        result.current[1].setResponseTimestamp((prev: Date | null) => {
          expect(prev).toBe(null);
          return newDate;
        });
      });

      expect(result.current[0].responseTimestamp).toBe(newDate);
    });

    it("should handle undefined responseTimestamp in function updates", async () => {
      const { result } = renderUseTrackedState(request);

      act(() => {
        result.current[1].setResponseTimestamp((prev: Date | null) => {
          expect(prev).toBe(null);
          return undefined as any;
        });
      });

      expect(result.current[0].responseTimestamp).toBe(undefined);
    });

    it("should handle string date conversion in function updates", async () => {
      const { result } = renderUseTrackedState(request);
      const dateStr = "2024-01-01T00:00:00.000Z";
      const expectedDate = new Date(dateStr);

      act(() => {
        result.current[1].setResponseTimestamp(expectedDate);
      });

      expect(+result.current[0].responseTimestamp!).toBe(+expectedDate);
    });

    it("should handle converting previous string date in function updates", async () => {
      const reqNoGarbageCollection = request.setCacheTime(Infinity);
      await reqNoGarbageCollection.send();
      const { result } = renderUseTrackedState(reqNoGarbageCollection);
      const initialDateStr = "2024-01-01T00:00:00.000Z";
      const expectedInitialDate = new Date(initialDateStr);
      const laterDate = new Date(expectedInitialDate.getTime() + 1000);

      // First set a string date
      act(() => {
        result.current[1].setResponseTimestamp(expectedInitialDate);
      });

      await waitFor(() => {
        expect(result.current[0].responseTimestamp).toBe(expectedInitialDate);
      });

      // Then update with function that receives the converted date
      act(() => {
        result.current[1].setResponseTimestamp((prev: Date | null) => {
          expect(+prev!).toBe(+expectedInitialDate);
          return laterDate;
        });
      });

      expect(+result.current[0].responseTimestamp!).toBe(+laterDate);
    });
  });
  describe("when using setRequestTimestamp action", () => {
    it("should handle null previous requestTimestamp in function updates", async () => {
      const { result } = renderUseTrackedState(request);
      const newDate = new Date();

      act(() => {
        result.current[1].setRequestTimestamp((prev: Date | null) => {
          expect(prev).toBe(null);
          return newDate;
        });
      });

      expect(result.current[0].requestTimestamp).toBe(newDate);
    });

    it("should use true as default value for emitToCache parameter", async () => {
      const reqNoGarbageCollection = request.setCacheTime(Infinity);
      await reqNoGarbageCollection.send();
      const { result } = renderUseTrackedState(reqNoGarbageCollection);
      const newDate = new Date();

      act(() => {
        result.current[1].setRequestTimestamp(newDate);
      });

      await waitFor(() => {
        expect(result.current[0].requestTimestamp).toBeDefined();
      });
    });

    it("should not update when function returns the same timestamp", async () => {
      const { result } = renderUseTrackedState(request);
      const date = new Date();

      act(() => {
        result.current[1].setRequestTimestamp(date);
      });

      expect(result.current[0].requestTimestamp).toBe(date);

      act(() => {
        result.current[1].setRequestTimestamp(date);
      });

      expect(result.current[0].requestTimestamp).toBe(date);
    });

    it("should convert existing prev timestamp in function updates", async () => {
      const { result } = renderUseTrackedState(request);
      const initialDate = new Date("2024-06-01T00:00:00.000Z");
      const laterDate = new Date("2024-06-02T00:00:00.000Z");

      act(() => {
        result.current[1].setRequestTimestamp(initialDate);
      });

      expect(result.current[0].requestTimestamp).toBe(initialDate);

      act(() => {
        result.current[1].setRequestTimestamp((prev: Date | null) => {
          expect(prev).toBeInstanceOf(Date);
          return laterDate;
        });
      });

      expect(result.current[0].requestTimestamp).toBe(laterDate);
    });
  });
  describe("when using setStatus with same value", () => {
    it("should not re-render when value unchanged", async () => {
      const { result } = renderUseTrackedState(request);

      act(() => {
        result.current[1].setStatus(200);
      });

      expect(result.current[0].status).toBe(200);

      act(() => {
        result.current[1].setStatus(200);
      });

      expect(result.current[0].status).toBe(200);
    });
  });
  describe("when using setSuccess with same value", () => {
    it("should not re-render when value unchanged", async () => {
      const { result } = renderUseTrackedState(request);

      act(() => {
        result.current[1].setSuccess(true);
      });

      expect(result.current[0].success).toBe(true);

      act(() => {
        result.current[1].setSuccess(true);
      });

      expect(result.current[0].success).toBe(true);
    });
  });
  describe("when using setExtra with same value", () => {
    it("should not re-render when value unchanged", async () => {
      const { result } = renderUseTrackedState(request);
      const extra = { headers: { "x-test": "1" } };

      act(() => {
        result.current[1].setExtra(extra);
      });

      expect(result.current[0].extra).toBe(extra);

      act(() => {
        result.current[1].setExtra(extra);
      });

      expect(result.current[0].extra).toBe(extra);
    });
  });
  describe("when using setRetries with same value", () => {
    it("should not re-render when value unchanged", async () => {
      const { result } = renderUseTrackedState(request);

      act(() => {
        result.current[1].setRetries(3);
      });

      expect(result.current[0].retries).toBe(3);

      act(() => {
        result.current[1].setRetries(3);
      });

      expect(result.current[0].retries).toBe(3);
    });
  });
  describe("when using setResponseTimestamp with same value", () => {
    it("should not re-render when value unchanged", async () => {
      const { result } = renderUseTrackedState(request);
      const date = new Date();

      act(() => {
        result.current[1].setResponseTimestamp(date);
      });

      expect(result.current[0].responseTimestamp).toBe(date);

      act(() => {
        result.current[1].setResponseTimestamp(date);
      });

      expect(result.current[0].responseTimestamp).toBe(date);
    });
  });
  describe("when using clearState action", () => {
    it("should reset all state to initial values", async () => {
      const { result } = renderUseTrackedState(request);

      act(() => {
        result.current[1].setData({ test: 1 });
        result.current[1].setError(new Error("test"));
        result.current[1].setLoading(true);
        result.current[1].setStatus(200);
        result.current[1].setSuccess(true);
        result.current[1].setRetries(5);
      });

      expect(result.current[0].data).toEqual({ test: 1 });
      expect(result.current[0].loading).toBe(true);

      act(() => {
        result.current[1].clearState();
      });

      expect(result.current[0].data).toBe(null);
      expect(result.current[0].error).toBe(null);
      expect(result.current[0].loading).toBe(false);
      expect(result.current[0].status).toBe(null);
      expect(result.current[0].success).toBe(false);
      expect(result.current[0].extra).toBe(null);
      expect(result.current[0].retries).toBe(0);
      expect(result.current[0].responseTimestamp).toBe(null);
      expect(result.current[0].requestTimestamp).toBe(null);
    });
  });
});
