import { sleep } from "@hyper-fetch/testing";

import type { CacheValueType } from "cache";
import { createCache } from "../../utils";
import { Client } from "client";
import { xhrExtra } from "http-adapter";
import { Plugin } from "plugin";

describe("Cache [ Events ]", () => {
  const cacheKey = "test";

  let client = new Client({ url: "shared-base-url" });
  let request = client.createRequest<{ response: any }>()({ endpoint: "shared-endpoint", cacheKey, cache: true });
  let cache = createCache(client);
  const spy = vi.fn();

  const cacheData: CacheValueType = {
    data: null,
    error: null,
    status: 200,
    success: true,
    extra: xhrExtra,
    retries: 0,
    requestTimestamp: +new Date(),
    responseTimestamp: +new Date(),
    addedTimestamp: +new Date(),
    triggerTimestamp: +new Date(),
    cached: true,
    isCanceled: false,
    isOffline: false,
    willRetry: false,
    cacheKey: request.cacheKey,
    scope: null,
    staleTime: request.staleTime,
    version: cache.version,
    cacheTime: 300000,
  };

  beforeEach(() => {
    client = new Client({ url: "shared-base-url" });
    request = client.createRequest<{ response: any }>()({ endpoint: "shared-endpoint", cacheKey, cache: true });
    cache = createCache(client);
    vi.resetAllMocks();
  });

  describe("when options events are triggered", () => {
    it("should trigger onChange event when data is set", async () => {
      const plugin = new Plugin({ name: "change" }).onCacheItemChange(spy);
      const newCache = createCache(client.addPlugin(plugin));

      newCache.set(request, cacheData);

      expect(spy).toHaveBeenCalledTimes(1);
      expect(spy).toHaveBeenCalledWith({
        cache: newCache,
        cacheKey: request.cacheKey,
        newData: cacheData,
        prevData: null,
      });
    });
    it("should trigger onDelete event when data is deleted", async () => {
      const plugin = new Plugin({ name: "delete" }).onCacheItemDelete(spy);
      const newCache = createCache(client.addPlugin(plugin));

      newCache.delete(request.cacheKey);

      expect(spy).toHaveBeenCalledTimes(1);
      expect(spy).toHaveBeenCalledWith({ cache: newCache, cacheKey: request.cacheKey });
    });
    it("should trigger onData event when data is set", async () => {
      const plugin = new Plugin({ name: "data" });
      const newCache = createCache(client.addPlugin(plugin));

      newCache.events.onData(spy);
      newCache.set(request, cacheData);

      expect(spy).toHaveBeenCalledTimes(1);
      expect(spy).toHaveBeenCalledWith(
        expect.objectContaining({
          data: null,
          cacheKey: request.cacheKey,
        }),
      );
    });
    it("should trigger onDelete event when data is deleted", async () => {
      const plugin = new Plugin({ name: "delete" });
      const newCache = createCache(client.addPlugin(plugin));

      newCache.events.onDelete(spy);
      newCache.delete(request.cacheKey);

      expect(spy).toHaveBeenCalledTimes(1);
      expect(spy).toHaveBeenCalledWith(request.cacheKey);
    });
    it("should trigger onDeleteByKey event when specific cache key is deleted", async () => {
      const plugin = new Plugin({ name: "deleteByKey" });
      const newCache = createCache(client.addPlugin(plugin));

      newCache.events.onDeleteByKey(request.cacheKey, spy);
      newCache.delete(request.cacheKey);

      expect(spy).toHaveBeenCalledTimes(1);
    });
    it("should properly cleanup onData listener when unmount function is called", async () => {
      const plugin = new Plugin({ name: "data" });
      const newCache = createCache(client.addPlugin(plugin));

      const unmount = newCache.events.onData(spy);
      unmount();
      newCache.set(request, cacheData);

      expect(spy).not.toHaveBeenCalled();
    });
    it("should properly cleanup onDataByKey listener when unmount function is called", async () => {
      const plugin = new Plugin({ name: "dataByKey" });
      const newCache = createCache(client.addPlugin(plugin));

      const unmount = newCache.events.onDataByKey(request.cacheKey, spy);
      unmount();
      newCache.set(request, cacheData);

      expect(spy).not.toHaveBeenCalled();
    });
    it("should properly cleanup onDelete listener when unmount function is called", async () => {
      const plugin = new Plugin({ name: "delete" });
      const newCache = createCache(client.addPlugin(plugin));

      const unmount = newCache.events.onDelete(spy);
      unmount();
      newCache.delete(request.cacheKey);

      expect(spy).not.toHaveBeenCalled();
    });
    it("should properly cleanup onDeleteByKey listener when unmount function is called", async () => {
      const plugin = new Plugin({ name: "deleteByKey" });
      const newCache = createCache(client.addPlugin(plugin));

      const unmount = newCache.events.onDeleteByKey(request.cacheKey, spy);
      unmount();
      newCache.delete(request.cacheKey);

      expect(spy).not.toHaveBeenCalled();
    });
  });
  describe("when invalidate event is triggered", () => {
    it("should invalidate cache using cache key", async () => {
      cache.set(request, {
        data: {},
        error: null,
        status: 200,
        success: true,
        extra: xhrExtra,
        retries: 0,
        requestTimestamp: +new Date(),
        responseTimestamp: +new Date(),
        addedTimestamp: +new Date(),
        triggerTimestamp: +new Date(),
        isCanceled: false,
        isOffline: false,
        willRetry: false,
      });
      cache.events.onInvalidateByKey(cacheKey, spy);
      await cache.invalidate(cacheKey);
      await sleep(1);
      expect(spy).toHaveBeenCalledTimes(1);
    });
    it("should invalidate cache using regex", async () => {
      cache.set(request, {
        data: null,
        error: null,
        status: 200,
        success: true,
        extra: xhrExtra,
        retries: 0,
        requestTimestamp: +new Date(),
        responseTimestamp: +new Date(),
        addedTimestamp: +new Date(),
        triggerTimestamp: +new Date(),
        isCanceled: false,
        isOffline: false,
        willRetry: false,
      });
      cache.events.onInvalidateByKey(cacheKey, spy);
      await cache.invalidate(new RegExp(cacheKey));
      await sleep(1);
      expect(spy).toHaveBeenCalledTimes(1);
    });
    it("should trigger onInvalidate event when cache is invalidated", async () => {
      cache.set(request, cacheData);
      cache.events.onInvalidate(spy);

      await cache.invalidate(cacheKey);
      await sleep(1);

      expect(spy).toHaveBeenCalledTimes(1);
      expect(spy).toHaveBeenCalledWith(cacheKey);
    });
    it("should properly cleanup onInvalidate listener when unmount function is called", async () => {
      cache.set(request, cacheData);
      const unmount = cache.events.onInvalidate(spy);

      unmount();
      await cache.invalidate(cacheKey);
      await sleep(1);

      expect(spy).not.toHaveBeenCalled();
    });
    it("should properly cleanup onInvalidateByKey listener when unmount function is called", async () => {
      cache.set(request, cacheData);
      const unmount = cache.events.onInvalidateByKey(cacheKey, spy);

      unmount();
      await cache.invalidate(cacheKey);
      await sleep(1);

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