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

import { Dispatcher } from "dispatcher";
import { createDispatcher, createAdapter } from "../../utils";
import { Client } from "client";
import { Plugin } from "plugin";

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

describe("Dispatcher [ Queue ]", () => {
  const adapterSpy = vi.fn();

  let adapter = createAdapter({ callback: adapterSpy });
  let client = new Client({ url: "shared-base-url" }).setAdapter(adapter);
  let dispatcher = createDispatcher(client);

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

  beforeEach(() => {
    vi.resetAllMocks();
    resetMocks();
    client.clear();
    adapter = createAdapter({ callback: adapterSpy });
    client = new Client({ url: "shared-base-url" }).setAdapter(adapter);
    dispatcher = createDispatcher(client);
  });

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

  describe("When using dispatcher add method", () => {
    it("should add request to the dispatcher storage and trigger it", async () => {
      const request = client.createRequest()({ endpoint: "shared-base-endpoint" });
      mockRequest(request);

      const loadingSpy = vi.fn();
      client.requestManager.events.onLoadingByQueue(request.queryKey, loadingSpy);
      const requestId = dispatcher.add(request);

      await waitFor(() => {
        expect(requestId).toBeString();
        // expect(adapterSpy).toHaveBeenCalledTimes(1);
        expect(loadingSpy).toHaveBeenCalledTimes(1);
        expect(dispatcher.getIsActiveQueue(request.queryKey)).toBeTrue();
        expect(dispatcher.getQueueRequestCount(request.queryKey)).toBe(1);
      });
    });
    it("should add running request and delete it once data is fetched", async () => {
      const request = client.createRequest()({ endpoint: "shared-base-endpoint" });
      mockRequest(request, { delay: 1 });

      dispatcher.add(request);

      expect(dispatcher.getAllRunningRequests()).toHaveLength(1);
      await waitFor(() => {
        expect(dispatcher.getAllRunningRequests()).toHaveLength(0);
        expect(dispatcher.getQueue(request.queryKey).requests).toHaveLength(0);
      });
    });
    it("should deduplicate requests and return ongoing requestId", async () => {
      const request = client.createRequest()({ endpoint: "shared-base-endpoint", deduplicate: true });
      mockRequest(request);

      const spy = vi.spyOn(dispatcher, "performRequest");

      const requestId = dispatcher.add(request);
      const deduplicatedId = dispatcher.add(request);

      expect(requestId).toBe(deduplicatedId);
      expect(spy).toHaveBeenCalledTimes(1);
      expect(dispatcher.getAllRunningRequests()).toHaveLength(1);
    });
    it("should queue the queued request", async () => {
      const request = client.createRequest()({ endpoint: "shared-base-endpoint", queued: true });
      mockRequest(request);

      const spy = vi.spyOn(dispatcher, "flushQueue");

      dispatcher.add(request);
      dispatcher.add(request);

      expect(spy).toHaveBeenCalledTimes(2);
      expect(dispatcher.getAllRunningRequests()).toHaveLength(1);
    });
    it("should send all concurrent requests", async () => {
      const request = client.createRequest()({ endpoint: "shared-base-endpoint", queued: false });
      mockRequest(request);

      const spy = vi.spyOn(dispatcher, "performRequest");

      dispatcher.add(request);
      dispatcher.add(request);

      expect(spy).toHaveBeenCalledTimes(2);
      expect(dispatcher.getAllRunningRequests()).toHaveLength(2);
    });
    it("should send all concurrent requests without any duplicates", async () => {
      const request = client.createRequest()({ endpoint: "shared-base-endpoint", queued: false });
      mockRequest(request, { delay: 40 });

      const spy1 = vi.spyOn(dispatcher, "performRequest");
      const spy2 = vi.spyOn(client.requestManager.events, "emitResponse");

      setTimeout(() => {
        dispatcher.add(request);
      }, 0);
      setTimeout(() => {
        dispatcher.add(request);
      }, 10);
      setTimeout(() => {
        dispatcher.add(request);
      }, 20);

      await sleep(100);

      expect(spy1).toHaveBeenCalledTimes(3);
      expect(spy2).toHaveBeenCalledTimes(3);
    });
  });
  describe("When using dispatcher performRequest method", () => {
    it("should trigger fetch adapter", async () => {
      const request = client.createRequest()({ endpoint: "shared-base-endpoint" });
      mockRequest(request);

      const spy = vi.spyOn(client.adapter, "fetch");
      const storageElement = dispatcher.createStorageItem(request);
      dispatcher.performRequest(storageElement);

      expect(spy).toHaveBeenCalledTimes(1);
    });
    it("should not trigger fetch adapter when app is offline", async () => {
      const request = client.createRequest()({ endpoint: "shared-base-endpoint" });
      mockRequest(request);

      client.appManager.setOnline(false);
      const spy = vi.spyOn(client.adapter, "fetch");
      const storageElement = dispatcher.createStorageItem(request);
      await dispatcher.performRequest(storageElement);

      expect(spy).toHaveBeenCalledTimes(0);
    });
    it("should trigger all requests when going back from offline", async () => {
      const request = client.createRequest()({ endpoint: "shared-base-endpoint" });
      mockRequest(request);

      const spy = vi.spyOn(client.adapter, "fetch");
      client.appManager.setOnline(false);
      dispatcher.add(request.setQueryKey("test1"));
      dispatcher.add(request.setQueryKey("test2"));
      dispatcher.add(request.setQueryKey("test3"));

      await sleep(5);

      expect(spy).toHaveBeenCalledTimes(0);
      client.appManager.setOnline(true);

      await sleep(5);

      expect(spy).toHaveBeenCalledTimes(3);
    });
    it("should not trigger one storage element two times at the same time", async () => {
      const request = client.createRequest()({ endpoint: "shared-base-endpoint" });
      mockRequest(request);

      const spy = vi.spyOn(client.adapter, "fetch");
      const storageElement = dispatcher.createStorageItem(request);
      dispatcher.performRequest(storageElement);
      dispatcher.performRequest(storageElement);

      expect(spy).toHaveBeenCalledTimes(1);
    });
  });
  describe("When retrying requests", () => {
    it("should retry failed request", async () => {
      const spy = vi.fn();
      const spyDelete = vi.fn();
      const plugin = new Plugin({ name: "delete" }).onDispatcherItemDeleted(spyDelete);
      const customClient = new Client({
        url: "shared-base-url",
        fetchDispatcher: () => new Dispatcher(),
      }).addPlugin(plugin);

      const request = customClient.createRequest()({ endpoint: "shared-base-endpoint", retry: 1, retryTime: 0 });
      mockRequest(request, { status: 400, delay: 0 });

      customClient.onRequest((cmd) => {
        spy();
        return cmd;
      });
      customClient.fetchDispatcher.add(request);

      await waitFor(() => {
        expect(spyDelete).toHaveBeenCalledTimes(1);
        expect(spy).toHaveBeenCalledTimes(2);
      });
    });
    it("should retry multiple times", async () => {
      const request = client.createRequest()({ endpoint: "shared-base-endpoint", retry: 2, retryTime: 0 });
      mockRequest(request, { status: 400, delay: 0 });

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

      await waitFor(() => {
        expect(spy).toHaveBeenCalledTimes(3);
      });
    });
    it("should not retry failed request when request 'retry' option is disabled", async () => {
      const request = client.createRequest()({ endpoint: "shared-base-endpoint", retry: 0 });
      mockRequest(request, { status: 400, delay: 0 });

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

      await waitFor(() => {
        expect(spy).toHaveBeenCalledTimes(1);
      });
    });
    it("should not retry failed request in offline mode", async () => {
      const request = client.createRequest()({ endpoint: "shared-base-endpoint", retry: 0 });
      mockRequest(request, { status: 400, delay: 5 });

      const spy = vi.spyOn(client.adapter, "fetch");
      dispatcher.add(request);
      await sleep(5);
      client.appManager.setOnline(false);
      await waitFor(() => {
        expect(spy).toHaveBeenCalledTimes(1);
      });
    });
  });
  describe("When flushing requests", () => {
    it("should flush all queues request", async () => {
      const firstRequest = client.createRequest()({ endpoint: "shared-base-endpoint", queryKey: "1" });
      const secondRequest = client.createRequest()({ endpoint: "shared-base-endpoint", queryKey: "2" });
      mockRequest(firstRequest);
      mockRequest(secondRequest);

      const spy = vi.spyOn(client.adapter, "fetch");
      client.appManager.setOnline(false);

      dispatcher.add(firstRequest);
      dispatcher.add(firstRequest);
      dispatcher.add(secondRequest);
      dispatcher.add(secondRequest);

      await sleep(5);
      expect(spy).toHaveBeenCalledTimes(0);
      client.appManager.setOnline(true);
      dispatcher.flush();
      await waitFor(() => {
        expect(spy).toHaveBeenCalledTimes(4);
      });
    });
    it("should not trigger flush methods when queue is empty", async () => {
      const spy = vi.spyOn(dispatcher, "performRequest");
      dispatcher.flushQueue("fake-queue");
      expect(spy).not.toHaveBeenCalled();
    });
    it("should not trigger flushQueue when queue is processing", async () => {
      const request = client.createRequest()({ endpoint: "shared-base-endpoint", queued: true });
      mockRequest(request, { delay: 1 });

      const spy = vi.spyOn(dispatcher, "performRequest");
      dispatcher.add(request);
      dispatcher.add(request);

      dispatcher.flushQueue(request.queryKey);
      dispatcher.flushQueue(request.queryKey);
      dispatcher.flushQueue(request.queryKey);

      expect(spy).toHaveBeenCalledTimes(1);

      await waitFor(() => {
        expect(spy).toHaveBeenCalledTimes(2);
      });
    });
    it("should not trigger flushQueue when having ongoing request", async () => {
      const request = client.createRequest()({ endpoint: "shared-base-endpoint", queued: true });
      mockRequest(request, { delay: 1 });

      const spy = vi.spyOn(dispatcher, "performRequest");
      const jsonRequest = dispatcher.createStorageItem(request);
      dispatcher.addQueueItem(request.queryKey, jsonRequest);
      dispatcher.addRunningRequest(request.queryKey, jsonRequest.requestId, request);

      dispatcher.flushQueue(request.queryKey);

      expect(spy).toHaveBeenCalledTimes(0);
    });

    it("should not trigger flushQueue on stopped requests", async () => {
      const request = client.createRequest()({ endpoint: "shared-base-endpoint", queued: true });
      mockRequest(request, { delay: 1 });

      const spy = vi.spyOn(client.adapter, "fetch");
      const jsonRequest = dispatcher.createStorageItem(request);
      dispatcher.addQueueItem(request.queryKey, jsonRequest);
      dispatcher.stopRequest(request.queryKey, jsonRequest.requestId);

      dispatcher.flushQueue(request.queryKey);

      expect(spy).toHaveBeenCalledTimes(0);
    });
    it("should not duplicate ongoing requests using flushQueue", async () => {
      const request = client.createRequest()({ endpoint: "shared-base-endpoint" });
      mockRequest(request, { delay: 30 });

      const spy = vi.spyOn(dispatcher, "performRequest");
      dispatcher.add(request);
      dispatcher.add(request);

      await sleep(5);

      dispatcher.add(request);

      await sleep(5);

      expect(spy).toHaveBeenCalledTimes(3);
    });
    it("should not throw on missing items", async () => {
      dispatcher.getQueue = () => null as any;
      dispatcher.getQueuesKeys = () => ["test1"];

      await expect(dispatcher.flush()).resolves.not.toThrow();
    });
    it("should not throw on missing requests", async () => {
      expect(() => dispatcher.stopRequest("test1", "test2")).not.toThrow();
    });
  });
  describe("When starting and stopping queue", () => {
    it("should stop queue from being send", async () => {
      const request = client.createRequest()({ endpoint: "shared-base-endpoint", queryKey: "1" });
      mockRequest(request);

      const spy = vi.spyOn(client.adapter, "fetch");
      dispatcher.stop(request.queryKey);
      dispatcher.add(request);
      dispatcher.add(request);

      await waitFor(() => {
        expect(spy).toHaveBeenCalledTimes(0);
        expect(dispatcher.getIsActiveQueue(request.queryKey)).toBeFalse();
      });
    });
    it("should stop queue and cancel ongoing requests", async () => {
      const request = client.createRequest()({ endpoint: "shared-base-endpoint", queryKey: "1" });
      mockRequest(request);

      const spy = vi.spyOn(client.adapter, "fetch");
      const firstSpy = vi.fn();
      const secondSpy = vi.fn();

      const firstRequestId = dispatcher.add(request);
      const secondRequestId = dispatcher.add(request);
      client.requestManager.events.onAbortById(firstRequestId, firstSpy);
      client.requestManager.events.onAbortById(secondRequestId, secondSpy);

      await sleep(1);

      dispatcher.stop(request.queryKey);

      await waitFor(() => {
        expect(spy).toHaveBeenCalledTimes(2);
        expect(firstSpy).toHaveBeenCalledTimes(1);
        expect(secondSpy).toHaveBeenCalledTimes(1);
        expect(dispatcher.getIsActiveQueue(request.queryKey)).toBeFalse();
      });
    });
    it("should start previously stopped queue", async () => {
      const request = client.createRequest()({ endpoint: "shared-base-endpoint", queryKey: "1" });
      mockRequest(request);

      const spy = vi.spyOn(client.adapter, "fetch");
      dispatcher.stop(request.queryKey);
      dispatcher.add(request);
      dispatcher.add(request);
      dispatcher.start(request.queryKey);
      expect(dispatcher.getIsActiveQueue(request.queryKey)).toBeTrue();

      await waitFor(() => {
        expect(spy).toHaveBeenCalledTimes(2);
      });
    });
    it("should pause queue and finish ongoing requests", async () => {
      const request = client.createRequest()({ endpoint: "shared-base-endpoint", queryKey: "1" });
      mockRequest(request);

      const spy = vi.spyOn(client.adapter, "fetch");
      const firstSpy = vi.fn();
      const secondSpy = vi.fn();

      const firstRequestId = dispatcher.add(request);
      const secondRequestId = dispatcher.add(request);
      client.requestManager.events.onAbortById(firstRequestId, firstSpy);
      client.requestManager.events.onAbortById(secondRequestId, secondSpy);

      await sleep(1);

      dispatcher.pause(request.queryKey);

      await waitFor(() => {
        expect(spy).toHaveBeenCalledTimes(2);
        expect(firstSpy).toHaveBeenCalledTimes(0);
        expect(secondSpy).toHaveBeenCalledTimes(0);
        expect(dispatcher.getIsActiveQueue(request.queryKey)).toBeFalse();
      });
    });
    it("should not remove requests from queue storage on stop", async () => {
      const request = client.createRequest()({ endpoint: "shared-base-endpoint" });
      mockRequest(request);

      dispatcher.add(request);
      await sleep(2);
      dispatcher.stop(request.queryKey);

      expect(dispatcher.getQueue(request.queryKey).requests).toHaveLength(1);
    });
    it("should not remove request from queue storage on stopRequest", async () => {
      const request = client.createRequest()({ endpoint: "shared-base-endpoint" });
      mockRequest(request);

      const requestId = dispatcher.add(request);
      await sleep(2);
      dispatcher.stopRequest(request.queryKey, requestId);

      expect(dispatcher.getQueue(request.queryKey).requests).toHaveLength(1);
    });
  });

  describe("When request is canceled", () => {
    it("should remove request from queue", async () => {
      const request = client.createRequest()({ endpoint: "shared-base-endpoint" });
      mockRequest(request);

      dispatcher.add(request);
      await sleep(1);
      client.requestManager.abortAll();
      await waitFor(() => {
        expect(dispatcher.getQueue(request.queryKey).requests).toHaveLength(0);
      });
    });
  });

  describe("When request is not offline", () => {
    it("should remove request from queue", async () => {
      const request = client.createRequest()({ endpoint: "shared-base-endpoint", offline: false });
      mockRequest(request, { status: 400 });

      dispatcher.add(request);
      await sleep(1);
      client.appManager.setOnline(false);
      await waitFor(() => {
        expect(dispatcher.getQueue(request.queryKey).requests).toHaveLength(0);
      });
    });
  });
});
