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

import { addQueueElement, client, createRequest, renderUseQueue } from "../../utils";

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

describe("useQueue [ Base ]", () => {
  let request = createRequest({ method: "POST" });

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

  afterEach(() => {
    resetMocks();
  });

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

  beforeEach(() => {
    vi.resetModules();
    client.clear();
    request = createRequest({ method: "POST" });
  });

  describe("given hook is mounting", () => {
    describe("when queue is processing requests", () => {
      it("should initialize with all processed requests", async () => {
        mockRequest(request);
        addQueueElement(request, { stop: true });
        const { result } = renderUseQueue(request);
        expect(result.current.requests).toHaveLength(1);
      });
      it("should remove finished requests from queue", async () => {
        mockRequest(request);
        addQueueElement(request);
        const { result } = renderUseQueue(request);
        await waitFor(() => {
          expect(result.current.requests).toHaveLength(0);
        });
      });
    });
  });
  describe("given queue is empty", () => {
    describe("when request is added to queue", () => {
      it("should update the requests values", async () => {
        mockRequest(request);
        let requestId = "";
        const progress = {
          total: 200,
          loaded: 100,
          progress: 0.5,
          timeLeft: 2000,
          sizeLeft: 100,
          startTimestamp: +new Date(),
        };
        const { result } = renderUseQueue(request);
        act(() => {
          requestId = addQueueElement(request, { stop: true });
        });
        await waitFor(() => {
          expect(result.current.requests).toHaveLength(1);
        });
        act(() => {
          client.requestManager.events.emitDownloadProgress({
            ...progress,
            requestId,
            request,
          });
        });
        await waitFor(() => {
          expect(result.current.requests[0].downloading).toStrictEqual(progress);
        });
      });
      it("should update upload progress of requests", async () => {
        mockRequest(request);
        let requestId = "";
        const progress = {
          total: 200,
          loaded: 100,
          progress: 0.5,
          timeLeft: 2000,
          sizeLeft: 100,
          startTimestamp: +new Date(),
        };
        const { result } = renderUseQueue(request);
        act(() => {
          requestId = addQueueElement(request, { stop: true });
        });
        await waitFor(() => {
          expect(result.current.requests).toHaveLength(1);
        });
        act(() => {
          client.requestManager.events.emitUploadProgress({
            ...progress,
            requestId,
            request,
          });
        });
        await waitFor(() => {
          expect(result.current.requests[0].uploading).toStrictEqual(progress);
        });
      });
    });
  });
  describe("given keepFinishedRequests is true", () => {
    describe("when requests finish", () => {
      it("should keep finished requests in the list", async () => {
        mockRequest(request);
        addQueueElement(request, { stop: true });
        const { result } = renderUseQueue(request, { keepFinishedRequests: true });

        expect(result.current.requests).toHaveLength(1);

        addQueueElement(request, { stop: true });

        await waitFor(() => {
          expect(result.current.requests.length).toBeGreaterThanOrEqual(1);
        });
      });
    });
  });
  describe("given queue events fire", () => {
    describe("when a request succeeds", () => {
      it("should mark the request as success", async () => {
        mockRequest(request);
        let requestId = "";
        const { result } = renderUseQueue(request);

        act(() => {
          requestId = addQueueElement(request, { stop: true });
        });

        await waitFor(() => {
          expect(result.current.requests).toHaveLength(1);
        });

        act(() => {
          client.requestManager.events.emitResponse({
            requestId,
            response: {
              data: "ok",
              error: null,
              status: 200,
              success: true,
              extra: {},
              responseTimestamp: +new Date(),
              requestTimestamp: +new Date(),
            },
            request,
          } as any);
        });

        await waitFor(() => {
          const req = result.current.requests.find((r: any) => r.requestId === requestId);
          expect(req?.success).toBe(true);
        });
      });
    });
    describe("when a request fails", () => {
      it("should mark the request as failed", async () => {
        mockRequest(request);
        let requestId = "";
        const { result } = renderUseQueue(request);

        act(() => {
          requestId = addQueueElement(request, { stop: true });
        });

        await waitFor(() => {
          expect(result.current.requests).toHaveLength(1);
        });

        act(() => {
          client.requestManager.events.emitResponse({
            requestId,
            response: {
              data: null,
              error: { message: "fail" },
              status: 500,
              success: false,
              extra: {},
              responseTimestamp: +new Date(),
              requestTimestamp: +new Date(),
            },
            request,
          } as any);
        });

        await waitFor(() => {
          const req = result.current.requests.find((r: any) => r.requestId === requestId);
          expect(req?.failed).toBe(true);
        });
      });
    });
    describe("when a request is aborted", () => {
      it("should mark the request as canceled", async () => {
        mockRequest(request);
        let requestId = "";
        const { result } = renderUseQueue(request);

        act(() => {
          requestId = addQueueElement(request, { stop: true });
        });

        await waitFor(() => {
          expect(result.current.requests).toHaveLength(1);
        });

        act(() => {
          client.requestManager.events.emitAbort({ requestId, request } as any);
        });

        await waitFor(() => {
          const req = result.current.requests.find((r: any) => r.requestId === requestId);
          expect(req?.canceled).toBe(true);
        });
      });
    });
    describe("when a request is removed", () => {
      it("should mark the request as removed", async () => {
        mockRequest(request);
        let requestId = "";
        const { result } = renderUseQueue(request);

        act(() => {
          requestId = addQueueElement(request, { stop: true });
        });

        await waitFor(() => {
          expect(result.current.requests).toHaveLength(1);
        });

        act(() => {
          client.requestManager.events.emitRemove({ requestId, request } as any);
        });

        await waitFor(() => {
          const req = result.current.requests.find((r: any) => r.requestId === requestId);
          expect(req?.removed).toBe(true);
        });
      });

      it("should not mark non-matching requests when event fires", async () => {
        mockRequest(request);
        let requestId = "";
        const { result } = renderUseQueue(request);

        act(() => {
          requestId = addQueueElement(request, { stop: true });
        });

        await waitFor(() => {
          expect(result.current.requests).toHaveLength(1);
        });

        act(() => {
          client.requestManager.events.emitResponse({
            requestId: "non-matching-id",
            response: {
              data: null,
              error: { message: "fail" },
              status: 500,
              success: false,
              extra: {},
              responseTimestamp: +new Date(),
              requestTimestamp: +new Date(),
            },
            request,
          } as any);
        });

        await waitFor(() => {
          const req = result.current.requests.find((r: any) => r.requestId === requestId);
          expect(req?.failed).toBe(false);
          expect(req?.success).toBe(false);
        });

        act(() => {
          client.requestManager.events.emitResponse({
            requestId: "non-matching-id-2",
            response: {
              data: "ok",
              error: null,
              status: 200,
              success: true,
              extra: {},
              responseTimestamp: +new Date(),
              requestTimestamp: +new Date(),
            },
            request,
          } as any);
        });

        await waitFor(() => {
          const req = result.current.requests.find((r: any) => r.requestId === requestId);
          expect(req?.success).toBe(false);
        });

        act(() => {
          client.requestManager.events.emitAbort({ requestId: "non-matching-id-3", request } as any);
        });

        await waitFor(() => {
          const req = result.current.requests.find((r: any) => r.requestId === requestId);
          expect(req?.canceled).toBe(false);
        });

        act(() => {
          client.requestManager.events.emitRemove({ requestId: "non-matching-id-4", request } as any);
        });

        await waitFor(() => {
          const req = result.current.requests.find((r: any) => r.requestId === requestId);
          expect(req?.removed).toBe(false);
        });
      });

      it("should not mark as removed if already succeeded", async () => {
        mockRequest(request);
        let requestId = "";
        const { result } = renderUseQueue(request);

        act(() => {
          requestId = addQueueElement(request, { stop: true });
        });

        await waitFor(() => {
          expect(result.current.requests).toHaveLength(1);
        });

        act(() => {
          client.requestManager.events.emitResponse({
            requestId,
            response: {
              data: "ok",
              error: null,
              status: 200,
              success: true,
              extra: {},
              responseTimestamp: +new Date(),
              requestTimestamp: +new Date(),
            },
            request,
          } as any);
        });

        await waitFor(() => {
          const req = result.current.requests.find((r: any) => r.requestId === requestId);
          expect(req?.success).toBe(true);
        });

        act(() => {
          client.requestManager.events.emitRemove({ requestId, request } as any);
        });

        await waitFor(() => {
          const req = result.current.requests.find((r: any) => r.requestId === requestId);
          expect(req?.removed).toBe(false);
        });
      });
    });
  });
});
