import { createClient } from "@hyper-fetch/core";

import { getValidCacheData, getInitialState, initialState } from "helpers";
import { isEmpty, isEqual } from "utils";

describe("useTrackedState [ Utils ]", () => {
  describe("when isEmpty gets triggered", () => {
    it("should return true for empty array or object", async () => {
      expect(isEmpty([])).toBe(true);
      expect(isEmpty({})).toBe(true);
    });

    it("should return false", async () => {
      expect(isEmpty(null)).toBe(false);
      expect(isEmpty(new Date())).toBe(false);
      expect(isEmpty(NaN)).toBe(false);
      expect(isEmpty("")).toBe(false);
      expect(isEmpty(false)).toBe(false);
      expect(isEmpty(undefined)).toBe(false);
      expect(isEmpty(0)).toBe(false);
      expect(isEmpty(true)).toBe(false);
      expect(isEmpty(123)).toBe(false);
      expect(isEmpty("123")).toBe(false);
      expect(isEmpty([1, 2, 3])).toBe(false);
      expect(isEmpty({ something: 1 })).toBe(false);
    });
  });

  describe("when isEqual gets triggered", () => {
    it("should return false for non equal values", async () => {
      expect(isEqual(true, false)).toBe(false);
      expect(isEqual(null, {})).toBe(false);
      expect(isEqual(new Date(), {})).toBe(false);
      expect(isEqual(new Date(), null)).toBe(false);
      expect(isEqual(NaN, {})).toBe(false);
      expect(isEqual(NaN, null)).toBe(false);
      expect(isEqual("1", 1)).toBe(false);
      expect(isEqual(0, -1)).toBe(false);
      expect(isEqual("2", "3")).toBe(false);
      expect(isEqual([], [1])).toBe(false);
      expect(isEqual([1], [2])).toBe(false);
      expect(isEqual(["1"], [1])).toBe(false);
      expect(isEqual([null], [NaN])).toBe(false);
      expect(isEqual([null], [new Date()])).toBe(false);
      expect(isEqual([null], [{}])).toBe(false);
      expect(isEqual([new Date()], [{}])).toBe(false);
      expect(isEqual([NaN], [{}])).toBe(false);
      expect(isEqual([{ someKey: 1 }], [{ someKey: 2 }])).toBe(false);
      expect(isEqual([{ someKey: "1" }], [{ someKey: 1 }])).toBe(false);
      expect(isEqual([{ someKey: null }], [{ someKey: {} }])).toBe(false);
      expect(isEqual([{ someKey: null }], [{ someKey: NaN }])).toBe(false);
      expect(isEqual([{ someKey: null }], [{ someKey: new Date() }])).toBe(false);
      expect(isEqual([{ someKey: {} }], [{ someKey: new Date() }])).toBe(false);
      expect(isEqual([{ someKey: {} }], [{ someKey: NaN }])).toBe(false);
      expect(isEqual({ someKey: 1 }, { otherKey: 1 })).toBe(false);
      expect(isEqual({ someKey: 1 }, { someKey: 1, otherKey: 1 })).toBe(false);
      expect(isEqual({ someKey: 1 }, { someKey: 2 })).toBe(false);
      expect(isEqual({ someKey: "1" }, { someKey: 1 })).toBe(false);
      expect(isEqual({ someKey: null }, { someKey: {} })).toBe(false);
      expect(isEqual({ someKey: null }, { someKey: NaN })).toBe(false);
      expect(isEqual({ someKey: null }, { someKey: new Date() })).toBe(false);
      expect(isEqual({ someKey: {} }, { someKey: new Date() })).toBe(false);
      expect(isEqual({ someKey: {} }, { someKey: NaN })).toBe(false);
      expect(isEqual({ someKey: 1 }, { otherKey: 1 })).toBe(false);
      expect(isEqual({ someKey: 1 }, { someKey: 1, otherKey: 1 })).toBe(false);
    });

    it("should return true for equal arguments", async () => {
      const date = new Date();

      expect(isEqual(true, true)).toBe(true);
      expect(isEqual(false, false)).toBe(true);
      expect(isEqual(undefined, undefined)).toBe(true);
      expect(isEqual(null, null)).toBe(true);
      expect(isEqual(date, date)).toBe(true);
      expect(isEqual(date, date)).toBe(true);
      expect(isEqual(NaN, NaN)).toBe(true);
      expect(isEqual("1", "1")).toBe(true);
      expect(isEqual(0, 0)).toBe(true);
      expect(isEqual(1, 1)).toBe(true);
      expect(isEqual("2", "2")).toBe(true);
      expect(isEqual([], [])).toBe(true);
      expect(isEqual([1], [1])).toBe(true);
      expect(isEqual(["1"], ["1"])).toBe(true);
      expect(isEqual([NaN], [NaN])).toBe(true);
      expect(isEqual([date], [date])).toBe(true);
      expect(isEqual([null], [null])).toBe(true);
      expect(isEqual([{}], [{}])).toBe(true);
      expect(isEqual([{ someKey: 1 }], [{ someKey: 1 }])).toBe(true);
      expect(isEqual([{ someKey: "1" }], [{ someKey: "1" }])).toBe(true);
      expect(isEqual([{ someKey: null }], [{ someKey: null }])).toBe(true);
      expect(isEqual([{ someKey: NaN }], [{ someKey: NaN }])).toBe(true);
      expect(isEqual([{ someKey: date }], [{ someKey: date }])).toBe(true);
      expect(isEqual([{ someKey: {} }], [{ someKey: {} }])).toBe(true);
      expect(isEqual({ someKey: 1, otherKey: 1 }, { someKey: 1, otherKey: 1 })).toBe(true);
      expect(isEqual({ someKey: "1" }, { someKey: "1" })).toBe(true);
      expect(isEqual({ someKey: {} }, { someKey: {} })).toBe(true);
      expect(isEqual({ someKey: NaN }, { someKey: NaN })).toBe(true);
      expect(isEqual({ someKey: null }, { someKey: null })).toBe(true);
      expect(isEqual({ someKey: date }, { someKey: date })).toBe(true);
      expect(isEqual({}, {})).toBe(true);
      expect(isEqual([null], [null])).toBe(true);
    });

    it("should return false when comparison throws an error", async () => {
      // Create an object with a getter that throws
      const throwingObj = {
        get evil() {
          throw new Error("Comparison error");
        },
      };

      const obj1 = { prop: throwingObj };
      const obj2 = { prop: { key: 2 } };

      // Should return false when comparison throws
      expect(isEqual(obj1, obj2)).toBe(false);
    });
  });

  describe("when getValidCacheData gets triggered", () => {
    const mockRequest = {
      staleTime: 1000,
      client: {
        cache: {
          version: 1,
        },
      },
      cacheKey: "test-key",
      cacheTime: 2000,
    } as any;

    it("should return initialResponse data when cache is stale", () => {
      const initialResponse = {
        data: "test-data",
        status: 200,
        success: true,
        requestTimestamp: new Date().getTime(),
        responseTimestamp: new Date().getTime(),
      };

      const result = getValidCacheData(mockRequest, initialResponse, null);

      expect(result).toEqual(
        expect.objectContaining({
          data: "test-data",
          error: null,
          status: 200,
          success: true,
          extra: null,
          staleTime: 1000,
          version: 1,
          cacheKey: "test-key",
          cacheTime: 2000,
        }),
      );
    });

    it("should return null when no cache data and no initialResponse", () => {
      const result = getValidCacheData(mockRequest, null, null);
      expect(result).toBeNull();
    });

    it("should return cache data when not stale", () => {
      const currentTime = new Date().getTime();
      const cacheData = {
        data: "cached-data",
        error: null,
        status: 200,
        success: true,
        extra: null,
        responseTimestamp: currentTime, // Recent timestamp within stale time
      } as any;

      const result = getValidCacheData(mockRequest, null, cacheData);
      expect(result).toBe(cacheData);
    });

    it("should return stale cache data for stale-while-revalidate", () => {
      const staleTimestamp = new Date().getTime() - 5000; // Well beyond the 1000ms staleTime
      const cacheData = {
        data: "stale-data",
        error: null,
        status: 200,
        success: true,
        extra: null,
        responseTimestamp: staleTimestamp,
      } as any;

      const result = getValidCacheData(mockRequest, null, cacheData);
      expect(result).toBe(cacheData);
    });
  });

  describe("when getInitialState handles async responseMapper", () => {
    it("should return initialState when responseMapper returns a Promise", () => {
      const mockRequest = {
        client: {
          cache: { get: vi.fn().mockReturnValue(null) },
          defaultExtra: null,
        },
        queryKey: "test",
        cacheKey: "test",
        unstable_responseMapper: () => Promise.resolve({ data: "test" }),
      } as any;

      const mockDispatcher = {
        getQueue: vi.fn().mockReturnValue({ stopped: false }),
        hasRunningRequests: vi.fn().mockReturnValue(false),
      } as any;

      const result = getInitialState({
        initialResponse: { data: "test" },
        dispatcher: mockDispatcher,
        request: mockRequest,
        disabled: false,
        revalidate: false,
      });

      expect(result).toEqual(initialState);
    });
  });

  describe("when getInitialState handles sync responseMapper", () => {
    it("should apply sync responseMapper to cache data", () => {
      const currentTime = new Date().getTime();
      const cacheData = {
        data: "original-data",
        error: null,
        status: 200,
        success: true,
        extra: null,
        responseTimestamp: currentTime,
        retries: 0,
      };

      const client = createClient({ url: "http://localhost:3000" });
      const mockRequest = client.createRequest()({ endpoint: "/test" });
      mockRequest.unstable_responseMapper = (data: any) => ({
        ...data,
        data: `mapped-${data.data}`,
      });

      // eslint-disable-next-line @typescript-eslint/ban-ts-comment
      // @ts-ignore
      client.cache = {
        get: vi.fn().mockReturnValue(cacheData),
        version: "1",
      };

      const mockDispatcher = {
        getQueue: vi.fn().mockReturnValue({ stopped: false }),
        hasRunningRequests: vi.fn().mockReturnValue(false),
      } as any;

      const result = getInitialState({
        initialResponse: null,
        dispatcher: mockDispatcher,
        request: mockRequest,
        disabled: false,
        revalidate: false,
      });

      expect(result).toEqual(
        expect.objectContaining({
          data: "mapped-original-data",
          error: null,
          status: 200,
          success: true,
          loading: false,
        }),
      );
    });
  });
});
