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

import { Client } from "client";

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

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

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

  beforeEach(() => {
    resetMocks();
    vi.resetAllMocks();
    client = new Client({ url: "shared-base-url" });
    request = client.createRequest()({ endpoint: "shared-base-endpoint" });
  });

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

  describe("When request manager events get triggered", () => {
    it("should trigger request lifecycle events", async () => {
      mockRequest(request);

      const spy1 = vi.fn();
      const spy2 = vi.fn();
      const spy3 = vi.fn();
      const spy4 = vi.fn();
      const spy5 = vi.fn();
      const spy6 = vi.fn();

      client.requestManager.events.onRequestStartByQueue(request.queryKey, spy1);
      client.requestManager.events.onResponseStartByQueue(request.queryKey, spy2);
      client.requestManager.events.onUploadProgressByQueue(request.queryKey, spy3);
      client.requestManager.events.onDownloadProgressByQueue(request.queryKey, spy4);
      client.requestManager.events.onResponseByCache(request.cacheKey, spy5);

      const requestId = client.fetchDispatcher.add(request);

      client.requestManager.events.onResponseById(requestId, spy6);

      await waitFor(() => {
        expect(spy1).toHaveBeenCalledTimes(1);
        expect(spy2).toHaveBeenCalledTimes(1);
        expect(spy3.mock.calls.length).toBeGreaterThanOrEqual(1);
        expect(spy4.mock.calls.length).toBeGreaterThanOrEqual(1);
        expect(spy5).toHaveBeenCalledTimes(1);
        expect(spy6).toHaveBeenCalledTimes(1);
      });
    });

    it("should trigger global event handlers", async () => {
      mockRequest(request);

      const loadingSpy = vi.fn();
      const requestStartSpy = vi.fn();
      const responseStartSpy = vi.fn();
      const uploadProgressSpy = vi.fn();
      const downloadProgressSpy = vi.fn();
      const responseSpy = vi.fn();
      const abortSpy = vi.fn();
      const removeSpy = vi.fn();

      // Set up global event listeners
      client.requestManager.events.onLoading(loadingSpy);
      client.requestManager.events.onRequestStart(requestStartSpy);
      client.requestManager.events.onResponseStart(responseStartSpy);
      client.requestManager.events.onUploadProgress(uploadProgressSpy);
      client.requestManager.events.onDownloadProgress(downloadProgressSpy);
      client.requestManager.events.onResponse(responseSpy);
      client.requestManager.events.onAbort(abortSpy);
      client.requestManager.events.onRemove(removeSpy);

      // Trigger request lifecycle
      const requestId = client.fetchDispatcher.add(request);

      await sleep(5);

      // Trigger abort and remove events
      client.requestManager.abortByKey(request.abortKey);

      await waitFor(() => {
        expect(loadingSpy).toHaveBeenCalledTimes(2);
        expect(requestStartSpy).toHaveBeenCalledTimes(1);
        expect(abortSpy).toHaveBeenCalledTimes(1);
        expect(removeSpy).toHaveBeenCalledTimes(1);
      });

      // Verify event data structure
      expect(loadingSpy).toHaveBeenCalledWith(
        expect.objectContaining({
          requestId,
          request,
        }),
      );

      expect(requestStartSpy).toHaveBeenCalledWith(
        expect.objectContaining({
          requestId,
          request,
        }),
      );

      expect(uploadProgressSpy).toHaveBeenCalledWith(
        expect.objectContaining({
          requestId,
          request,
          loaded: expect.toBeNumber(),
          progress: expect.toBeNumber(),
          total: expect.toBeNumber(),
        }),
      );
    });

    it("should properly remove global event listeners", async () => {
      mockRequest(request);
      const spy = vi.fn();

      // Add and immediately remove event listener
      const removeListener = client.requestManager.events.onLoading(spy);
      removeListener();

      // Trigger request
      client.fetchDispatcher.add(request);

      await sleep(50);

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

    it("should properly cleanup all types of event listeners", async () => {
      mockRequest(request);
      const requestId = "test-request-id";
      const { queryKey } = request;
      const { cacheKey } = request;
      const { abortKey } = request;

      const spies = {
        loading: vi.fn(),
        loadingByQueue: vi.fn(),
        loadingByCache: vi.fn(),
        loadingById: vi.fn(),
        requestStart: vi.fn(),
        requestStartByQueue: vi.fn(),
        requestStartById: vi.fn(),
        responseStart: vi.fn(),
        responseStartByQueue: vi.fn(),
        responseStartById: vi.fn(),
        uploadProgress: vi.fn(),
        uploadProgressByQueue: vi.fn(),
        uploadProgressById: vi.fn(),
        downloadProgress: vi.fn(),
        downloadProgressByQueue: vi.fn(),
        downloadProgressById: vi.fn(),
        response: vi.fn(),
        responseByCache: vi.fn(),
        responseById: vi.fn(),
        abort: vi.fn(),
        abortByKey: vi.fn(),
        abortById: vi.fn(),
        remove: vi.fn(),
        removeByQueue: vi.fn(),
        removeById: vi.fn(),
      };

      // Set up and immediately cleanup all event listeners
      const cleanups = [
        client.requestManager.events.onLoading(spies.loading),
        client.requestManager.events.onLoadingByQueue(queryKey, spies.loadingByQueue),
        client.requestManager.events.onLoadingByCache(cacheKey, spies.loadingByCache),
        client.requestManager.events.onLoadingById(requestId, spies.loadingById),
        client.requestManager.events.onRequestStart(spies.requestStart),
        client.requestManager.events.onRequestStartByQueue(queryKey, spies.requestStartByQueue),
        client.requestManager.events.onRequestStartById(requestId, spies.requestStartById),
        client.requestManager.events.onResponseStart(spies.responseStart),
        client.requestManager.events.onResponseStartByQueue(queryKey, spies.responseStartByQueue),
        client.requestManager.events.onResponseStartById(requestId, spies.responseStartById),
        client.requestManager.events.onUploadProgress(spies.uploadProgress),
        client.requestManager.events.onUploadProgressByQueue(queryKey, spies.uploadProgressByQueue),
        client.requestManager.events.onUploadProgressById(requestId, spies.uploadProgressById),
        client.requestManager.events.onDownloadProgress(spies.downloadProgress),
        client.requestManager.events.onDownloadProgressByQueue(queryKey, spies.downloadProgressByQueue),
        client.requestManager.events.onDownloadProgressById(requestId, spies.downloadProgressById),
        client.requestManager.events.onResponse(spies.response),
        client.requestManager.events.onResponseByCache(cacheKey, spies.responseByCache),
        client.requestManager.events.onResponseById(requestId, spies.responseById),
        client.requestManager.events.onAbort(spies.abort),
        client.requestManager.events.onAbortByKey(abortKey, spies.abortByKey),
        client.requestManager.events.onAbortById(requestId, spies.abortById),
        client.requestManager.events.onRemove(spies.remove),
        client.requestManager.events.onRemoveByQueue(queryKey, spies.removeByQueue),
        client.requestManager.events.onRemoveById(requestId, spies.removeById),
      ];

      // Execute all cleanup functions
      cleanups.forEach((cleanup) => cleanup());

      // Trigger a full request lifecycle
      client.fetchDispatcher.add(request);
      await sleep(5);
      client.requestManager.abortByKey(request.abortKey);

      await sleep(50);

      // Verify no callbacks were called after cleanup
      Object.values(spies).forEach((spy) => {
        expect(spy).not.toHaveBeenCalled();
      });
    });
  });
  describe("When using deduplicated event listeners", () => {
    it("should trigger onDeduplicated callback when emitDeduplicated fires", () => {
      const spy = vi.fn();
      client.requestManager.events.onDeduplicated(spy);

      const eventData = { request, requestId: "test-id", deduplicatedRequest: request };
      client.requestManager.events.emitDeduplicated(eventData);

      expect(spy).toHaveBeenCalledTimes(1);
      expect(spy).toHaveBeenCalledWith(eventData);
    });

    it("should trigger onDeduplicatedByQueue callback for matching queryKey", () => {
      const spy = vi.fn();
      client.requestManager.events.onDeduplicatedByQueue(request.queryKey, spy);

      const eventData = { request, requestId: "test-id", deduplicatedRequest: request };
      client.requestManager.events.emitDeduplicated(eventData);

      expect(spy).toHaveBeenCalledTimes(1);
      expect(spy).toHaveBeenCalledWith(eventData);
    });

    it("should trigger onDeduplicatedByCache callback for matching cacheKey", () => {
      const spy = vi.fn();
      client.requestManager.events.onDeduplicatedByCache(request.cacheKey, spy);

      const eventData = { request, requestId: "test-id", deduplicatedRequest: request };
      client.requestManager.events.emitDeduplicated(eventData);

      expect(spy).toHaveBeenCalledTimes(1);
      expect(spy).toHaveBeenCalledWith(eventData);
    });

    it("should trigger onDeduplicatedById callback for matching requestId", () => {
      const spy = vi.fn();
      const requestId = "specific-request-id";
      client.requestManager.events.onDeduplicatedById(requestId, spy);

      const eventData = { request, requestId, deduplicatedRequest: request };
      client.requestManager.events.emitDeduplicated(eventData);

      expect(spy).toHaveBeenCalledTimes(1);
      expect(spy).toHaveBeenCalledWith(eventData);
    });

    it("should cleanup onDeduplicated listener", () => {
      const spy = vi.fn();
      const cleanup = client.requestManager.events.onDeduplicated(spy);
      cleanup();

      client.requestManager.events.emitDeduplicated({ request, requestId: "test-id", deduplicatedRequest: request });

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

    it("should cleanup onDeduplicatedByQueue listener", () => {
      const spy = vi.fn();
      const cleanup = client.requestManager.events.onDeduplicatedByQueue(request.queryKey, spy);
      cleanup();

      client.requestManager.events.emitDeduplicated({ request, requestId: "test-id", deduplicatedRequest: request });

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

    it("should cleanup onDeduplicatedByCache listener", () => {
      const spy = vi.fn();
      const cleanup = client.requestManager.events.onDeduplicatedByCache(request.cacheKey, spy);
      cleanup();

      client.requestManager.events.emitDeduplicated({ request, requestId: "test-id", deduplicatedRequest: request });

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

    it("should cleanup onDeduplicatedById listener", () => {
      const spy = vi.fn();
      const requestId = "specific-request-id";
      const cleanup = client.requestManager.events.onDeduplicatedById(requestId, spy);
      cleanup();

      client.requestManager.events.emitDeduplicated({ request, requestId, deduplicatedRequest: request });

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

  describe("When request manager aborts the request", () => {
    it("should allow to abort request by id", async () => {
      mockRequest(request);
      const spy1 = vi.fn();
      const spy2 = vi.fn();

      const requestId = client.fetchDispatcher.add(request);
      client.requestManager.events.onAbortByKey(request.abortKey, spy1);
      client.requestManager.events.onAbortById(requestId, spy2);

      await sleep(5);

      client.requestManager.abortByKey(request.abortKey);
      await waitFor(() => {
        expect(spy1).toHaveBeenCalledTimes(1);
        expect(spy2).toHaveBeenCalledTimes(1);
      });

      expect(client.appManager.isFocused).toBeTrue();
    });
    it("should allow to abort all requests", async () => {
      mockRequest(request);
      const spy1 = vi.fn();
      const spy2 = vi.fn();

      const requestId = client.fetchDispatcher.add(request);
      client.requestManager.events.onAbortByKey(request.abortKey, spy1);
      client.requestManager.events.onAbortById(requestId, spy2);

      await sleep(5);

      client.requestManager.abortAll();
      await waitFor(() => {
        expect(spy1).toHaveBeenCalledTimes(1);
        expect(spy2).toHaveBeenCalledTimes(1);
      });

      expect(client.appManager.isFocused).toBeTrue();
    });
    it("should not throw when removing non-existing controller key", async () => {
      expect(() => client.requestManager.abortByKey("fake-key")).not.toThrow();
    });

    it("should emit abort event once", async () => {
      mockRequest(request);
      const spy1 = vi.fn();

      client.fetchDispatcher.add(request);
      client.requestManager.events.onAbortByKey(request.abortKey, spy1);

      await sleep(5);
      client.requestManager.abortAll();

      await sleep(50);
      expect(spy1).toHaveBeenCalledTimes(1);

      expect(client.appManager.isFocused).toBeTrue();
    });
  });
});
