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

import { Client, interceptRequest, interceptResponse, stringifyDefaultOptions } from "client";
import { stringifyQueryParams } from "http-adapter";

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

const options = {
  ...stringifyDefaultOptions,
  encode: true,
};

describe("Client [ Utils ]", () => {
  let client = new Client({ url: "shared-base-url" });
  client.adapter.setQueryParamsMapperConfig(options);
  let request = client.createRequest<{ payload: FormData; response: any }>()({ endpoint: "shared-nase-endpoint" });

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

  beforeEach(() => {
    client = new Client({ url: "shared-base-url" });
    client.adapter.setQueryParamsMapperConfig(options);
    request = client.createRequest<{ response: any; payload: FormData }>()({ endpoint: "shared-nase-endpoint" });
    resetMocks();
  });

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

  describe("When using stringifyQueryParams util", () => {
    it("should encode falsy values", async () => {
      const newClient = new Client({ url: "shared-base-url" });
      newClient.adapter.setQueryParamsMapperConfig({
        encode: false,
        arrayFormat: "bracket",
      });
      expect(
        newClient.adapter.unstable_queryParamsMapper({ value: { test: new Date("9,9,2020").toISOString() } }),
      ).toBe(`?value={"test":"${new Date("9,9,2020").toISOString()}"}`);
      expect(
        newClient.adapter.unstable_queryParamsMapper({
          page: 0,
          status: ["status1", "status2"],
          lastFreeDay: '{"from":"2023-06-20T10:37:27.508Z","to":"2023-06-24T10:37:27.508Z"}',
          account: "test-account/123+323-123|111:;",
        }),
      ).toBe(
        '?page=0&status[]=status1&status[]=status2&lastFreeDay={"from":"2023-06-20T10:37:27.508Z","to":"2023-06-24T10:37:27.508Z"}&account=test-account/123+323-123|111:;',
      );
    });
    it("should encode falsy values", async () => {
      expect(stringifyQueryParams({ value: 0 }, options)).toBe("?value=0");
      expect(stringifyQueryParams({ value: "" }, { ...options, skipEmptyString: false })).toBe("?value=");
      expect(stringifyQueryParams({ value: null }, { ...options, skipNull: false })).toBe("?value=null");
      expect(stringifyQueryParams({ value: "" }, { ...options, skipEmptyString: true })).toBe("");
      expect(stringifyQueryParams({ value: undefined }, { ...options, skipEmptyString: true })).toBe("");
      expect(stringifyQueryParams({ value: null }, { ...options, skipNull: true })).toBe("");
    });
    it("should encode date value to isoString by default", async () => {
      expect(stringifyQueryParams({ value: new Date("2023-05-12T22:32:32") }, options)).toInclude(`?value=2023-05-12`);
    });
    it("should encode truthy values", async () => {
      expect(stringifyQueryParams({ value: 10 }, options)).toBe("?value=10");
      expect(stringifyQueryParams({ value: "test" }, options)).toBe("?value=test");
      expect(stringifyQueryParams({ value: [1, 2, 3] }, options)).toBe("?value[]=1&value[]=2&value[]=3");
      expect(stringifyQueryParams({ value: { data: "test" } }, options)).toBe("?value=%7B%22data%22%3A%22test%22%7D");
      expect(
        stringifyQueryParams(
          {
            value: {
              filter: {
                firstName: "john",
                other: {
                  hobbies: ["dancing", "singing"],
                },
              },
            },
          },
          options,
        ),
      ).toBe(
        "?value=%7B%22filter%22%3A%7B%22firstName%22%3A%22john%22%2C%22other%22%3A%7B%22hobbies%22%3A%5B%22dancing%22%2C%22singing%22%5D%7D%7D%7D",
      );
    });
    it("should encode not allowed characters values", async () => {
      expect(stringifyQueryParams({ value: "[]" }, options)).toBe("?value=%5B%5D");
      expect(stringifyQueryParams({ value: "/" }, options)).toBe("?value=%2F");
      expect(stringifyQueryParams({ value: "," }, options)).toBe("?value=%2C");
      expect(stringifyQueryParams({ value: "|" }, options)).toBe("?value=%7C");
      expect(stringifyQueryParams({ value: ":" }, options)).toBe("?value=%3A");
      expect(stringifyQueryParams({ value: ";" }, options)).toBe("?value=%3B");
      expect(stringifyQueryParams({ value: "+" }, options)).toBe("?value=%2B");
      expect(stringifyQueryParams({ value: "(" }, options)).toBe("?value=%28");
      expect(stringifyQueryParams({ value: ")" }, options)).toBe("?value=%29");
      expect(stringifyQueryParams({ value: "*" }, options)).toBe("?value=%2A");
      expect(stringifyQueryParams({ value: "&" }, options)).toBe("?value=%26");
      expect(stringifyQueryParams({ value: "^" }, options)).toBe("?value=%5E");
      expect(stringifyQueryParams({ value: "%" }, options)).toBe("?value=%25");
      expect(stringifyQueryParams({ value: "$" }, options)).toBe("?value=%24");
      expect(stringifyQueryParams({ value: "#" }, options)).toBe("?value=%23");
      expect(stringifyQueryParams({ value: "@" }, options)).toBe("?value=%40");
      expect(stringifyQueryParams({ value: "!" }, options)).toBe("?value=%21");
    });
    it("should encode array values with different formats", async () => {
      expect(
        stringifyQueryParams({ value: [1, 2, 3] }, { ...options, arraySeparator: "", arrayFormat: "bracket" }),
      ).toBe("?value[]=1&value[]=2&value[]=3");
      expect(
        stringifyQueryParams(
          { value: [1, 2, 3] },
          { ...options, arraySeparator: "", arrayFormat: "bracket-separator" },
        ),
      ).toBe("?value[]=1|2|3");
      expect(stringifyQueryParams({ value: [1, 2, 3] }, { ...options, arraySeparator: "", arrayFormat: "comma" })).toBe(
        "?value=1,2,3",
      );
      expect(stringifyQueryParams({ value: [1, 2, 3] }, { ...options, arraySeparator: "", arrayFormat: "index" })).toBe(
        "?value[0]=1&value[1]=2&value[2]=3",
      );
      expect(stringifyQueryParams({ value: [1, 2, 3] }, { ...options, arraySeparator: "", arrayFormat: "none" })).toBe(
        "?value=1&value=2&value=3",
      );
      expect(
        stringifyQueryParams({ value: [1, 2, 3] }, { ...options, arraySeparator: "", arrayFormat: "separator" }),
      ).toBe("?value=1|2|3");
      expect(
        stringifyQueryParams({ value: [1, 2, 3] }, { ...options, arrayFormat: "separator", arraySeparator: "#" }),
      ).toBe("?value=1#2#3");
    });
    it("should encode values without strict option", async () => {
      expect(stringifyQueryParams({ value: "![]" }, { ...options, encode: true, strict: false })).toBe(
        "?value=!%5B%5D",
      );
      expect(stringifyQueryParams({ value: "![]" }, { ...options, encode: false, strict: false })).toBe("?value=![]");
      expect(stringifyQueryParams({ value: "![]" }, { ...options, encode: false, strict: true })).toBe("?value=![]");
    });
    it("should add the question mark", async () => {
      expect(stringifyQueryParams("something", options)).toBe("?something");
      expect(stringifyQueryParams("?something", options)).toBe("?something");
    });
    it("should return empty string when no query params are passed", async () => {
      expect(stringifyQueryParams("", options)).toBe("");
      expect(stringifyQueryParams({}, options)).toBe("");
      expect(stringifyQueryParams(null, options)).toBe("");
      expect(stringifyQueryParams(undefined, options)).toBe("");
    });
  });

  describe("When using headerMapper", () => {
    it("should assign default headers with headers mapper", async () => {
      const defaultHeaders = {};
      const headers = client.adapter.unstable_headerMapper(request, undefined);

      expect(headers).toEqual(defaultHeaders);
    });
    it("should assign form data headers", async () => {
      const defaultHeaders = {};
      const data = new FormData();
      const formDataRequest = request.setPayload(data);
      const headers = client.adapter.unstable_headerMapper(formDataRequest, undefined);

      expect(headers).toEqual(defaultHeaders);
    });
    it("should not re-assign form data headers", async () => {
      const defaultHeaders = { "Content-Type": "some-custom-format" };
      const formDataRequest = request.setHeaders(defaultHeaders);
      const headers = client.adapter.unstable_headerMapper(formDataRequest, undefined);

      expect(headers).toEqual(defaultHeaders);
    });
    it("should add content type header for object payload", async () => {
      const defaultHeaders = { "Content-Type": "application/json" };
      const data = { data: [] } as any;
      const formDataRequest = request.setPayload(data);
      const headers = client.adapter.unstable_headerMapper(formDataRequest, undefined);

      expect(headers).toEqual(defaultHeaders);
    });
  });

  describe("When using payloadMapper", () => {
    it("should allow to stringify payload", async () => {
      const data = { data: [] };
      const payload = client.adapter.unstable_payloadMapper(data, undefined);

      expect(payload).toEqual(JSON.stringify(data));
    });
    it("should not stringify FormData payload", async () => {
      const data = new FormData();
      const payload = client.adapter.unstable_payloadMapper(data, undefined);

      expect(payload).toEqual(data);
    });
    it("should stringify null payload", async () => {
      const data = null;
      const payload = client.adapter.unstable_payloadMapper(data, undefined);

      expect(payload).toEqual(JSON.stringify(data));
    });
    it("should not stringify undefined payload", async () => {
      const payload = client.adapter.unstable_payloadMapper(undefined, undefined);

      expect(payload).toBeUndefined();
    });
    it("should not stringify invalid payload", async () => {
      const payload = client.adapter.unstable_payloadMapper(() => null, undefined);
      expect(payload).toBeUndefined();
    });
  });

  describe("When using stringifyValue util", () => {
    it("should allow to stringify payload", async () => {
      const data = { data: [] };
      const payload = client.adapter.unstable_payloadMapper(data, undefined);

      expect(payload).toEqual(JSON.stringify(data));
    });
    it("should not stringify FormData payload", async () => {
      const data = new FormData();
      const payload = client.adapter.unstable_payloadMapper(data, undefined);

      expect(payload).toEqual(data);
    });
    it("should stringify null payload", async () => {
      const data = null;
      const payload = client.adapter.unstable_payloadMapper(data, undefined);

      expect(payload).toEqual(JSON.stringify(data));
    });
    it("should not stringify undefined payload", async () => {
      const payload = client.adapter.unstable_payloadMapper(undefined, undefined);

      expect(payload).toBeUndefined();
    });
    it("should not stringify invalid payload", async () => {
      const data: Record<string, unknown> = {};
      data.a = { b: data };

      const payload = client.adapter.unstable_payloadMapper(data, undefined);
      expect(payload).toBe("");
    });
  });

  describe("when interceptors are disabled", () => {
    it("should skip request interceptors when disabled", async () => {
      const mockInterceptor = vi.fn((req) => req);
      const req = client.createRequest<{ response: any }>()({
        endpoint: "shared-base-endpoint",
        disableRequestInterceptors: true,
      });

      const result = await interceptRequest([mockInterceptor], req);

      expect(mockInterceptor).not.toHaveBeenCalled();
      expect(result).toBe(req);
    });

    it("should skip response interceptors when disabled", async () => {
      const mockInterceptor = vi.fn((res) => res);
      const response = {
        data: { test: "data" },
        status: 200,
        success: true,
      } as any;
      const req = client.createRequest<{ response: any }>()({
        endpoint: "shared-base-endpoint",
        disableResponseInterceptors: true,
      });

      const result = await interceptResponse([mockInterceptor], response, req);

      expect(mockInterceptor).not.toHaveBeenCalled();
      expect(result).toBe(response);
    });

    it("should run interceptors when not disabled", async () => {
      const mockInterceptor = vi.fn((req) => req);

      const req = client.createRequest<{ response: any }>()({
        endpoint: "shared-base-endpoint",
        disableRequestInterceptors: false,
      });

      await interceptRequest([mockInterceptor], req);

      expect(mockInterceptor).toHaveBeenCalledWith(req);
    });
  });
});
