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

import { Client } from "client";

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

describe("Client [ Auth ]", () => {
  const requestFixture = { data: "" };

  const refreshFixture = { token: "new-token" };
  const refreshEndpoint = "/refresh-token";

  let client = new Client({ url: "shared-base-url" });
  let request = client.createRequest<{ response: any }>()({ endpoint: "/shared-endpoint" }).setAuth(false);
  let authRequest = client.createRequest<{ response: any }>()({ endpoint: "/auth" }).setAuth(true);
  let refreshRequest = client.createRequest<{ response: any }>()({ endpoint: refreshEndpoint });

  beforeEach(() => {
    mockRequest(refreshRequest, { data: refreshFixture });
  });

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

  afterEach(() => {
    client = new Client({ url: "shared-base-url" });
    request = client.createRequest<{ response: any }>()({ endpoint: "/shared-endpoint" }).setAuth(false);
    authRequest = client.createRequest<{ response: any }>()({ endpoint: "/auth" }).setAuth(true);
    refreshRequest = client.createRequest<{ response: any }>()({ endpoint: refreshEndpoint });
    resetMocks();
    vi.resetAllMocks();
  });

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

  describe("When authenticated request get send", () => {
    it("should trigger auth middleware callback", async () => {
      mockRequest(authRequest, { data: requestFixture });

      const trigger = vi.fn();

      client.onAuth((cmd) => {
        trigger();
        return cmd;
      });

      await authRequest.send({});

      expect(trigger).toHaveBeenCalledTimes(1);
    });
  });

  describe("When non-authenticated request get send", () => {
    it("should trigger auth middleware callback", async () => {
      mockRequest(request, { data: requestFixture });

      const trigger = vi.fn();

      client.onAuth((cmd) => {
        trigger();
        return cmd;
      });

      await request.send({});

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

  describe("When token is out of date", () => {
    const interceptor = vi.fn();

    const handleErrorIntercept = (callback?: () => void) => {
      client.onError(async (res, req) => {
        interceptor();
        const { status } = res;

        if (!req.used && status === 401) {
          const { data } = await refreshRequest.send({});
          if (data) {
            callback?.();
            return req.setUsed(true).send({});
          }
        }
        return res;
      });
    };

    it("should intercept error response and get the data with new token", async () => {
      mockRequest(request, { status: 401 });

      handleErrorIntercept(() => mockRequest(request, { data: requestFixture }));

      const response = await request.send({});
      expect(response.data).toEqual(requestFixture);
      expect(interceptor).toHaveBeenCalledTimes(1);
    });
    it("should intercept error response and return error after another failure", async () => {
      const errorResponse = mockRequest(request, { status: 401 });

      handleErrorIntercept();
      const { data, error, status } = await request.send({});

      expect(status).toBe(401);
      expect(data).toBe(null);
      expect(error).toEqual(errorResponse);
      expect(interceptor).toHaveBeenCalledTimes(2);
    });

    it("should try to repeat request only once", async () => {
      mockRequest(request, { status: 401 });

      const retry = vi.fn();

      handleErrorIntercept(retry);

      await request.send({});

      expect(retry).toHaveBeenCalledTimes(1);
    });
  });
});
