import { Client } from "client";
import type { CacheValueType } from "cache";
import { xhrExtra } from "http-adapter";
import { scopeKey } from "request";

describe("Client [ Hydration ]", () => {
  let client = new Client({ url: "shared-base-url" });
  let request = client.createRequest<{ response: any }>()({ endpoint: "shared-base-endpoint" });

  beforeEach(() => {
    client = new Client({ url: "shared-base-url" });
    request = client.createRequest<{ response: any }>()({ endpoint: "shared-base-endpoint" });
  });

  describe("When using hydrate", () => {
    it("should not dehydrate from empty cache", async () => {
      const dehydratedResponse = request.dehydrate();

      expect(dehydratedResponse).toBeUndefined();
    });
    it("should dehydrate from cache", async () => {
      const mockedRequest = request.setMock(() => ({ data: "test", status: 200 }));
      const response = await mockedRequest.send();

      const dehydratedResponse = mockedRequest.dehydrate();

      expect(dehydratedResponse).toStrictEqual(expect.objectContaining({ cacheKey: request.cacheKey, response }));

      const newClient = new Client({ url: "shared-base-url" });

      expect(newClient.cache.get(mockedRequest.cacheKey)).not.toBeDefined();

      newClient.hydrate([dehydratedResponse]);

      expect(newClient.cache.get(mockedRequest.cacheKey)).toStrictEqual(
        expect.objectContaining({ ...response, cacheKey: request.cacheKey, staleTime: request.staleTime }),
      );
    });
    it("should dehydrate from response", async () => {
      const mockedRequest = request.setMock(() => ({ data: "test", status: 200 }));
      const response = await mockedRequest.send();

      const dehydratedResponse = mockedRequest.dehydrate({ response });

      expect(dehydratedResponse).toStrictEqual(expect.objectContaining({ cacheKey: request.cacheKey, response }));

      const newClient = new Client({ url: "shared-base-url" });

      expect(newClient.cache.get(mockedRequest.cacheKey)).not.toBeDefined();

      newClient.hydrate([dehydratedResponse]);

      expect(newClient.cache.get(mockedRequest.cacheKey)).toStrictEqual(
        expect.objectContaining({ ...response, cacheKey: request.cacheKey, staleTime: request.staleTime }),
      );
    });
    it("should not override cache data", async () => {
      const mockedRequest = request.setMock(() => ({ data: "test", status: 200 }));
      const response = await mockedRequest.send();

      const dehydratedResponse = mockedRequest.dehydrate({ response, override: false });

      expect(dehydratedResponse).toStrictEqual(expect.objectContaining({ cacheKey: request.cacheKey, response }));

      const newClient = new Client({ url: "shared-base-url" });
      const cacheData = {
        data: "123456789",
        status: 200,
        error: null,
        success: true,
        extra: {},
        responseTimestamp: +new Date(),
        requestTimestamp: +new Date(),
        addedTimestamp: +new Date(),
        triggerTimestamp: +new Date(),
        retries: 0,
        isCanceled: false,
        isOffline: false,
        hydrated: false,
        cached: true,
      } as CacheValueType;

      newClient.cache.set(mockedRequest, cacheData);

      expect(newClient.cache.get(mockedRequest.cacheKey)).toStrictEqual({
        ...cacheData,
        cacheKey: mockedRequest.cacheKey,
        cacheTime: mockedRequest.cacheTime,
        staleTime: mockedRequest.staleTime,
        version: newClient.cache.version,
        scope: null,
      });

      newClient.hydrate([dehydratedResponse]);

      expect(newClient.cache.get(mockedRequest.cacheKey)).toStrictEqual({
        ...cacheData,
        cacheKey: mockedRequest.cacheKey,
        cacheTime: mockedRequest.cacheTime,
        staleTime: mockedRequest.staleTime,
        version: newClient.cache.version,
        scope: null,
      });
    });
    it("should handle null/undefined items in hydration data", async () => {
      const mockedRequest = request.setMock(() => ({ data: "test", status: 200 }));
      await mockedRequest.send();
      const dehydratedResponse = mockedRequest.dehydrate();

      const newClient = new Client({ url: "shared-base-url" });
      // Should not throw when processing null/undefined items
      newClient.hydrate([dehydratedResponse, null, undefined]);

      expect(newClient.cache.get(mockedRequest.cacheKey)).toBeDefined();
    });
    it("should apply function-based options", async () => {
      const mockedRequest = request.setMock(() => ({ data: "test", status: 200 }));
      await mockedRequest.send();
      const dehydratedResponse = mockedRequest.dehydrate();

      const newClient = new Client({ url: "shared-base-url" });
      const optionsFn = vi.fn().mockReturnValue({ cache: false });

      newClient.hydrate([dehydratedResponse], optionsFn);

      expect(optionsFn).toHaveBeenCalledWith(dehydratedResponse);
      // Since cache is false, data should not be in cache
      expect(newClient.cache.get(mockedRequest.cacheKey)).toBeUndefined();
    });
    it("should merge options correctly", async () => {
      const mockedRequest = request.setMock(() => ({ data: "test", status: 200 }));
      const response = await mockedRequest.send();
      // Test with fallback options in dehydrated response
      const dehydratedResponse = mockedRequest.dehydrate({
        response,
      });

      const newClient = new Client({ url: "shared-base-url" });

      // Add global options
      newClient.hydrate([dehydratedResponse], {
        staleTime: 3000,
        cacheTime: 2000,
        override: true,
      });

      const cachedData = newClient.cache.get(mockedRequest.cacheKey);
      expect(cachedData).toBeDefined();
      expect(cachedData?.staleTime).toBe(3000);
      expect(cachedData?.cacheTime).toBe(2000);
    });
    it("should respect cache option", async () => {
      const mockedRequest = request.setMock(() => ({ data: "test", status: 200 }));
      await mockedRequest.send();
      const dehydratedResponse = mockedRequest.dehydrate();

      const newClient = new Client({ url: "shared-base-url" });

      newClient.hydrate([dehydratedResponse], { cache: false });

      expect(newClient.cache.get(mockedRequest.cacheKey)).toBeUndefined();
    });
    it("should override if override is false and cache is not set", async () => {
      const mockedRequest = request.setMock(() => ({ data: "test", status: 200 }));
      await mockedRequest.send();
      const dehydratedResponse = mockedRequest.dehydrate();

      client.cache.clear();

      const spy = vi.spyOn(client.cache, "set");
      client.hydrate([dehydratedResponse], { override: false });

      expect(spy).toHaveBeenCalled();
    });

    it("should hydrate and read scoped cache via scopeKey", async () => {
      const scoped = request.setScope("hydr-scope").setMock(() => ({ data: "scoped-val", status: 200 }));
      const response = await scoped.send();
      const dehydratedResponse = scoped.dehydrate();
      expect(dehydratedResponse).toEqual(expect.objectContaining({ scope: "hydr-scope", cacheKey: scoped.cacheKey }));

      const newClient = new Client({ url: "shared-base-url" });
      const sk = scopeKey(scoped.cacheKey, "hydr-scope");
      expect(newClient.cache.get(scoped.cacheKey)).not.toBeDefined();

      newClient.hydrate([dehydratedResponse]);

      expect(newClient.cache.get(sk)).toStrictEqual(
        expect.objectContaining({ ...response, cacheKey: scoped.cacheKey, staleTime: scoped.staleTime }),
      );
    });

    it("should skip hydrate when override is false and scoped slot already has data", async () => {
      const scoped = request.setScope("no-over").setMock(() => ({ data: "a", status: 200 }));
      await scoped.send();
      const ts = Date.now();
      const dehydratedResponse = scoped.dehydrate({
        response: {
          data: "b",
          status: 200,
          success: true,
          error: null,
          extra: xhrExtra,
          requestTimestamp: ts,
          responseTimestamp: ts,
        },
      });

      const newClient = new Client({ url: "shared-base-url" });
      const existingReq = newClient
        .createRequest<{ response: any }>()({ endpoint: "shared-base-endpoint" })
        .setScope("no-over");
      const sk = scopeKey(existingReq.cacheKey, "no-over");

      newClient.cache.set(existingReq.setCache(true), {
        data: "existing",
        error: null,
        status: 200,
        success: true,
        extra: xhrExtra,
        retries: 0,
        requestTimestamp: Date.now(),
        responseTimestamp: Date.now(),
        addedTimestamp: Date.now(),
        triggerTimestamp: Date.now(),
        isCanceled: false,
        isOffline: false,
        willRetry: false,
      });

      newClient.hydrate([dehydratedResponse], { override: false });

      expect(newClient.cache.get(sk)?.data).toBe("existing");
    });
  });
});
