import { fireEvent, waitFor } from "@testing-library/react";
import * as React from "react";
import { ErrorBoundary } from "react-error-boundary";

import * as QueriesObserverModule from "@tanstack/query-core/queriesObserver";
import { describe, expect, it } from "vitest";

import type { QueryFunctionContext } from "@tanstack/query-core";
import type {
  QueryClient,
  QueryFunction,
  QueryKey,
  QueryObserverResult,
  UseQueryOptions,
  UseQueryResult,
} from "../../react-query";
import { QueriesObserver, QueryCache, useQueries } from "../../react-query";
import {
  createQueryClient,
  expectType,
  expectTypeNotAny,
  queryKey,
  renderWithClient,
  sleep,
} from "../utils";

describe("useQueries", () => {
  const queryCache = new QueryCache();
  const queryClient = createQueryClient({ queryCache });

  it("should return the correct states", async () => {
    const key1 = queryKey();
    const key2 = queryKey();
    const results: UseQueryResult[][] = [];

    function Page() {
      const result = useQueries({
        queries: [
          {
            queryKey: key1,
            queryFn: async () => {
              await sleep(10);
              return 1;
            },
          },
          {
            queryKey: key2,
            queryFn: async () => {
              await sleep(200);
              return 2;
            },
          },
        ],
      });
      results.push(result);

      return (
        <div>
          <div>
            data1: {String(result[0].data ?? "null")}, data2:{" "}
            {String(result[1].data ?? "null")}
          </div>
        </div>
      );
    }

    const rendered = renderWithClient(queryClient, <Page />);

    await waitFor(() => rendered.getByText("data1: 1, data2: 2"));

    expect(results.length).toBe(3);
    expect(results[0]).toMatchObject([
      { data: undefined },
      { data: undefined },
    ]);
    expect(results[1]).toMatchObject([{ data: 1 }, { data: undefined }]);
    expect(results[2]).toMatchObject([{ data: 1 }, { data: 2 }]);
  });

  it("should keep previous data if amount of queries is the same", async () => {
    const key1 = queryKey();
    const key2 = queryKey();
    const states: UseQueryResult[][] = [];

    function Page() {
      const [count, setCount] = React.useState(1);
      const result = useQueries({
        queries: [
          {
            queryKey: [key1, count],
            keepPreviousData: true,
            queryFn: async () => {
              await sleep(10);
              return count * 2;
            },
          },
          {
            queryKey: [key2, count],
            keepPreviousData: true,
            queryFn: async () => {
              await sleep(35);
              return count * 5;
            },
          },
        ],
      });
      states.push(result);

      const isFetching = result.some((r) => r.isFetching);

      return (
        <div>
          <div>
            data1: {String(result[0].data ?? "null")}, data2:{" "}
            {String(result[1].data ?? "null")}
          </div>
          <div>isFetching: {String(isFetching)}</div>
          <button onClick={() => setCount((prev) => prev + 1)}>inc</button>
        </div>
      );
    }

    const rendered = renderWithClient(queryClient, <Page />);

    await waitFor(() => rendered.getByText("data1: 2, data2: 5"));
    fireEvent.click(rendered.getByRole("button", { name: /inc/i }));

    await waitFor(() => rendered.getByText("data1: 4, data2: 10"));
    await waitFor(() => rendered.getByText("isFetching: false"));

    expect(states[states.length - 1]).toMatchObject([
      { status: "success", data: 4, isPreviousData: false, isFetching: false },
      { status: "success", data: 10, isPreviousData: false, isFetching: false },
    ]);
  });

  it("should keep previous data for variable amounts of useQueries", async () => {
    const key = queryKey();
    const states: UseQueryResult[][] = [];

    function Page() {
      const [count, setCount] = React.useState(2);
      const result = useQueries({
        queries: Array.from({ length: count }, (_, i) => ({
          queryKey: [key, count, i + 1],
          keepPreviousData: true,
          queryFn: async () => {
            await sleep(35 * (i + 1));
            return (i + 1) * count * 2;
          },
        })),
      });

      states.push(result);

      const isFetching = result.some((r) => r.isFetching);

      return (
        <div>
          <div>data: {result.map((it) => it.data).join(",")}</div>
          <div>isFetching: {String(isFetching)}</div>
          <button onClick={() => setCount((prev) => prev + 1)}>inc</button>
        </div>
      );
    }

    const rendered = renderWithClient(queryClient, <Page />);

    await waitFor(() => rendered.getByText("data: 4,8"));
    fireEvent.click(rendered.getByRole("button", { name: /inc/i }));

    await waitFor(() => rendered.getByText("data: 6,12,18"));
    await waitFor(() => rendered.getByText("isFetching: false"));

    expect(states[states.length - 1]).toMatchObject([
      { status: "success", data: 6, isPreviousData: false, isFetching: false },
      { status: "success", data: 12, isPreviousData: false, isFetching: false },
      { status: "success", data: 18, isPreviousData: false, isFetching: false },
    ]);
  });

  it("should keep previous data when switching between queries", async () => {
    const key = queryKey();
    const states: UseQueryResult[][] = [];

    function Page() {
      const [series1, setSeries1] = React.useState(1);
      const [series2, setSeries2] = React.useState(2);
      const ids = [series1, series2];

      const result = useQueries({
        queries: ids.map((id) => {
          return {
            queryKey: [key, id],
            queryFn: async () => {
              await sleep(5);
              return id * 5;
            },
            keepPreviousData: true,
          };
        }),
      });

      states.push(result);

      const isFetching = result.some((r) => r.isFetching);

      return (
        <div>
          <div>
            data1: {String(result[0]?.data ?? "null")}, data2:{" "}
            {String(result[1]?.data ?? "null")}
          </div>
          <div>isFetching: {String(isFetching)}</div>
          <button onClick={() => setSeries2(3)}>setSeries2</button>
          <button onClick={() => setSeries1(2)}>setSeries1</button>
        </div>
      );
    }

    const rendered = renderWithClient(queryClient, <Page />);

    await waitFor(() => rendered.getByText("data1: 5, data2: 10"));
    fireEvent.click(rendered.getByRole("button", { name: /setSeries2/i }));

    await waitFor(() => rendered.getByText("data1: 5, data2: 15"));
    fireEvent.click(rendered.getByRole("button", { name: /setSeries1/i }));

    await waitFor(() => rendered.getByText("data1: 10, data2: 15"));
    await waitFor(() => rendered.getByText("isFetching: false"));

    expect(states[states.length - 1]).toMatchObject([
      { status: "success", data: 10, isPreviousData: false, isFetching: false },
      { status: "success", data: 15, isPreviousData: false, isFetching: false },
    ]);
  });

  it("should not go to infinite render loop with previous data when toggling queries", async () => {
    const key = queryKey();
    const states: UseQueryResult[][] = [];

    function Page() {
      const [enableId1, setEnableId1] = React.useState(true);
      const ids = enableId1 ? [1, 2] : [2];

      const result = useQueries({
        queries: ids.map((id) => {
          return {
            queryKey: [key, id],
            queryFn: async () => {
              await sleep(5);
              return id * 5;
            },
            keepPreviousData: true,
          };
        }),
      });

      states.push(result);

      const isFetching = result.some((r) => r.isFetching);

      return (
        <div>
          <div>
            data1: {String(result[0]?.data ?? "null")}, data2:{" "}
            {String(result[1]?.data ?? "null")}
          </div>
          <div>isFetching: {String(isFetching)}</div>
          <button onClick={() => setEnableId1(false)}>set1Disabled</button>
          <button onClick={() => setEnableId1(true)}>set2Enabled</button>
        </div>
      );
    }

    const rendered = renderWithClient(queryClient, <Page />);

    await waitFor(() => rendered.getByText("data1: 5, data2: 10"));
    fireEvent.click(rendered.getByRole("button", { name: /set1Disabled/i }));

    await waitFor(() => rendered.getByText("data1: 10, data2: null"));
    await waitFor(() => rendered.getByText("isFetching: false"));
    fireEvent.click(rendered.getByRole("button", { name: /set2Enabled/i }));

    await waitFor(() => rendered.getByText("data1: 5, data2: 10"));
    await waitFor(() => rendered.getByText("isFetching: false"));

    await waitFor(() => expect(states.length).toBe(6));

    expect(states[0]).toMatchObject([
      {
        status: "loading",
        data: undefined,
        isPreviousData: false,
        isFetching: true,
      },
      {
        status: "loading",
        data: undefined,
        isPreviousData: false,
        isFetching: true,
      },
    ]);
    expect(states[1]).toMatchObject([
      { status: "success", data: 5, isPreviousData: false, isFetching: false },
      { status: "success", data: 10, isPreviousData: false, isFetching: false },
    ]);
    expect(states[2]).toMatchObject([
      { status: "success", data: 10, isPreviousData: false, isFetching: false },
    ]);
    expect(states[3]).toMatchObject([
      { status: "success", data: 5, isPreviousData: false, isFetching: true },
      { status: "success", data: 10, isPreviousData: false, isFetching: false },
    ]);
    expect(states[4]).toMatchObject([
      { status: "success", data: 5, isPreviousData: false, isFetching: true },
      { status: "success", data: 10, isPreviousData: false, isFetching: false },
    ]);
    expect(states[5]).toMatchObject([
      { status: "success", data: 5, isPreviousData: false, isFetching: false },
      { status: "success", data: 10, isPreviousData: false, isFetching: false },
    ]);
  });

  it("handles type parameter - tuple of tuples", async () => {
    const key1 = queryKey();
    const key2 = queryKey();
    const key3 = queryKey();

    // @ts-expect-error (Page component is not rendered)
    // eslint-disable-next-line
    function Page() {
      const result1 = useQueries<[[number], [string], [string[], boolean]]>({
        queries: [
          {
            queryKey: key1,
            queryFn: () => 1,
          },
          {
            queryKey: key2,
            queryFn: () => "string",
          },
          {
            queryKey: key3,
            queryFn: () => ["string[]"],
          },
        ],
      });
      expectType<QueryObserverResult<number, unknown>>(result1[0]);
      expectType<QueryObserverResult<string, unknown>>(result1[1]);
      expectType<QueryObserverResult<string[], boolean>>(result1[2]);
      expectType<number | undefined>(result1[0].data);
      expectType<string | undefined>(result1[1].data);
      expectType<string[] | undefined>(result1[2].data);
      expectType<boolean | null>(result1[2].error);

      // TData (3rd element) takes precedence over TQueryFnData (1st element)
      const result2 = useQueries<
        [[string, unknown, string], [string, unknown, number]]
      >({
        queries: [
          {
            queryKey: key1,
            queryFn: () => "string",
            select: (a) => {
              expectType<string>(a);
              expectTypeNotAny(a);
              return a.toLowerCase();
            },
          },
          {
            queryKey: key2,
            queryFn: () => "string",
            select: (a) => {
              expectType<string>(a);
              expectTypeNotAny(a);
              return parseInt(a);
            },
          },
        ],
      });
      expectType<QueryObserverResult<string, unknown>>(result2[0]);
      expectType<QueryObserverResult<number, unknown>>(result2[1]);
      expectType<string | undefined>(result2[0].data);
      expectType<number | undefined>(result2[1].data);

      // types should be enforced
      useQueries<[[string, unknown, string], [string, boolean, number]]>({
        queries: [
          {
            queryKey: key1,
            queryFn: () => "string",
            select: (a) => {
              expectType<string>(a);
              expectTypeNotAny(a);
              return a.toLowerCase();
            },
            onSuccess: (a) => {
              expectType<string>(a);
              expectTypeNotAny(a);
            },
            placeholderData: "string",
            // @ts-expect-error (initialData: string)
            initialData: 123,
          },
          {
            queryKey: key2,
            queryFn: () => "string",
            select: (a) => {
              expectType<string>(a);
              expectTypeNotAny(a);
              return parseInt(a);
            },
            onSuccess: (a) => {
              expectType<number>(a);
              expectTypeNotAny(a);
            },
            onError: (e) => {
              expectType<boolean>(e);
              expectTypeNotAny(e);
            },
            placeholderData: "string",
            // @ts-expect-error (initialData: string)
            initialData: 123,
          },
        ],
      });

      // field names should be enforced
      useQueries<[[string]]>({
        queries: [
          {
            queryKey: key1,
            queryFn: () => "string",
            // @ts-expect-error (invalidField)
            someInvalidField: [],
          },
        ],
      });
    }
  });

  it("handles type parameter - tuple of objects", async () => {
    const key1 = queryKey();
    const key2 = queryKey();
    const key3 = queryKey();

    // @ts-expect-error (Page component is not rendered)
    // eslint-disable-next-line
    function Page() {
      const result1 = useQueries<
        [
          { queryFnData: number },
          { queryFnData: string },
          { queryFnData: string[]; error: boolean }
        ]
      >({
        queries: [
          {
            queryKey: key1,
            queryFn: () => 1,
          },
          {
            queryKey: key2,
            queryFn: () => "string",
          },
          {
            queryKey: key3,
            queryFn: () => ["string[]"],
          },
        ],
      });
      expectType<QueryObserverResult<number, unknown>>(result1[0]);
      expectType<QueryObserverResult<string, unknown>>(result1[1]);
      expectType<QueryObserverResult<string[], boolean>>(result1[2]);
      expectType<number | undefined>(result1[0].data);
      expectType<string | undefined>(result1[1].data);
      expectType<string[] | undefined>(result1[2].data);
      expectType<boolean | null>(result1[2].error);

      // TData (data prop) takes precedence over TQueryFnData (queryFnData prop)
      const result2 = useQueries<
        [
          { queryFnData: string; data: string },
          { queryFnData: string; data: number }
        ]
      >({
        queries: [
          {
            queryKey: key1,
            queryFn: () => "string",
            select: (a) => {
              expectType<string>(a);
              expectTypeNotAny(a);
              return a.toLowerCase();
            },
          },
          {
            queryKey: key2,
            queryFn: () => "string",
            select: (a) => {
              expectType<string>(a);
              expectTypeNotAny(a);
              return parseInt(a);
            },
          },
        ],
      });
      expectType<QueryObserverResult<string, unknown>>(result2[0]);
      expectType<QueryObserverResult<number, unknown>>(result2[1]);
      expectType<string | undefined>(result2[0].data);
      expectType<number | undefined>(result2[1].data);

      // can pass only TData (data prop) although TQueryFnData will be left unknown
      const result3 = useQueries<[{ data: string }, { data: number }]>({
        queries: [
          {
            queryKey: key1,
            queryFn: () => "string",
            select: (a) => {
              expectType<unknown>(a);
              expectTypeNotAny(a);
              return a as string;
            },
          },
          {
            queryKey: key2,
            queryFn: () => "string",
            select: (a) => {
              expectType<unknown>(a);
              expectTypeNotAny(a);
              return a as number;
            },
          },
        ],
      });
      expectType<QueryObserverResult<string, unknown>>(result3[0]);
      expectType<QueryObserverResult<number, unknown>>(result3[1]);
      expectType<string | undefined>(result3[0].data);
      expectType<number | undefined>(result3[1].data);

      // types should be enforced
      useQueries<
        [
          { queryFnData: string; data: string },
          { queryFnData: string; data: number; error: boolean }
        ]
      >({
        queries: [
          {
            queryKey: key1,
            queryFn: () => "string",
            select: (a) => {
              expectType<string>(a);
              expectTypeNotAny(a);
              return a.toLowerCase();
            },
            onSuccess: (a) => {
              expectType<string>(a);
              expectTypeNotAny(a);
            },
            placeholderData: "string",
            // @ts-expect-error (initialData: string)
            initialData: 123,
          },
          {
            queryKey: key2,
            queryFn: () => "string",
            select: (a) => {
              expectType<string>(a);
              expectTypeNotAny(a);
              return parseInt(a);
            },
            onSuccess: (a) => {
              expectType<number>(a);
              expectTypeNotAny(a);
            },
            onError: (e) => {
              expectType<boolean>(e);
              expectTypeNotAny(e);
            },
            placeholderData: "string",
            // @ts-expect-error (initialData: string)
            initialData: 123,
          },
        ],
      });

      // field names should be enforced
      useQueries<[{ queryFnData: string }]>({
        queries: [
          {
            queryKey: key1,
            queryFn: () => "string",
            // @ts-expect-error (invalidField)
            someInvalidField: [],
          },
        ],
      });
    }
  });

  it("handles array literal without type parameter to infer result type", async () => {
    const key1 = queryKey();
    const key2 = queryKey();
    const key3 = queryKey();
    const key4 = queryKey();

    // @ts-expect-error (Page component is not rendered)
    // eslint-disable-next-line
    function Page() {
      // Array.map preserves TQueryFnData
      const result1 = useQueries({
        queries: Array(50).map((_, i) => ({
          queryKey: ["key", i] as const,
          queryFn: () => i + 10,
        })),
      });
      expectType<QueryObserverResult<number, unknown>[]>(result1);
      expectType<number | undefined>(result1[0]?.data);

      // Array.map preserves TData
      const result2 = useQueries({
        queries: Array(50).map((_, i) => ({
          queryKey: ["key", i] as const,
          queryFn: () => i + 10,
          select: (data: number) => data.toString(),
        })),
      });
      expectType<QueryObserverResult<string, unknown>[]>(result2);

      const result3 = useQueries({
        queries: [
          {
            queryKey: key1,
            queryFn: () => 1,
          },
          {
            queryKey: key2,
            queryFn: () => "string",
          },
          {
            queryKey: key3,
            queryFn: () => ["string[]"],
            select: () => 123,
          },
        ],
      });
      expectType<QueryObserverResult<number, unknown>>(result3[0]);
      expectType<QueryObserverResult<string, unknown>>(result3[1]);
      expectType<QueryObserverResult<number, unknown>>(result3[2]);
      expectType<number | undefined>(result3[0].data);
      expectType<string | undefined>(result3[1].data);
      // select takes precedence over queryFn
      expectType<number | undefined>(result3[2].data);

      // initialData/placeholderData are enforced
      useQueries({
        queries: [
          {
            queryKey: key1,
            queryFn: () => "string",
            placeholderData: "string",
            // @ts-expect-error (initialData: string)
            initialData: 123,
          },
          {
            queryKey: key2,
            queryFn: () => 123,
            // @ts-expect-error (placeholderData: number)
            placeholderData: "string",
            initialData: 123,
          },
        ],
      });

      // select / onSuccess / onSettled params are "indirectly" enforced
      useQueries({
        queries: [
          // unfortunately TS will not suggest the type for you
          {
            queryKey: key1,
            queryFn: () => "string",
            // @ts-expect-error (noImplicitAny)
            onSuccess: (a) => null,
            // @ts-expect-error (noImplicitAny)
            onSettled: (a) => null,
          },
          // however you can add a type to the callback
          {
            queryKey: key2,
            queryFn: () => "string",
            onSuccess: (a: string) => {
              expectType<string>(a);
              expectTypeNotAny(a);
            },
            onSettled: (a: string | undefined) => {
              expectType<string | undefined>(a);
              expectTypeNotAny(a);
            },
          },
          // the type you do pass is enforced
          {
            queryKey: key3,
            queryFn: () => "string",
            // @ts-expect-error (only accepts string)
            onSuccess: (a: number) => null,
          },
          {
            queryKey: key4,
            queryFn: () => "string",
            select: (a: string) => parseInt(a),
            // @ts-expect-error (select is defined => only accepts number)
            onSuccess: (a: string) => null,
            onSettled: (a: number | undefined) => {
              expectType<number | undefined>(a);
              expectTypeNotAny(a);
            },
          },
        ],
      });

      // callbacks are also indirectly enforced with Array.map
      useQueries({
        // @ts-expect-error (onSuccess only accepts string)
        queries: Array(50).map((_, i) => ({
          queryKey: ["key", i] as const,
          queryFn: () => i + 10,
          select: (data: number) => data.toString(),
          onSuccess: (_data: number) => null,
        })),
      });
      useQueries({
        queries: Array(50).map((_, i) => ({
          queryKey: ["key", i] as const,
          queryFn: () => i + 10,
          select: (data: number) => data.toString(),
          onSuccess: (_data: string) => null,
        })),
      });

      // results inference works when all the handlers are defined
      const result4 = useQueries({
        queries: [
          {
            queryKey: key1,
            queryFn: () => "string",
            // @ts-expect-error (noImplicitAny)
            onSuccess: (a) => null,
            // @ts-expect-error (noImplicitAny)
            onSettled: (a) => null,
          },
          {
            queryKey: key2,
            queryFn: () => "string",
            onSuccess: (a: string) => {
              expectType<string>(a);
              expectTypeNotAny(a);
            },
            onSettled: (a: string | undefined) => {
              expectType<string | undefined>(a);
              expectTypeNotAny(a);
            },
          },
          {
            queryKey: key4,
            queryFn: () => "string",
            select: (a: string) => parseInt(a),
            onSuccess: (_a: number) => null,
            onSettled: (a: number | undefined) => {
              expectType<number | undefined>(a);
              expectTypeNotAny(a);
            },
          },
        ],
      });
      expectType<QueryObserverResult<string, unknown>>(result4[0]);
      expectType<QueryObserverResult<string, unknown>>(result4[1]);
      expectType<QueryObserverResult<number, unknown>>(result4[2]);

      // handles when queryFn returns a Promise
      const result5 = useQueries({
        queries: [
          {
            queryKey: key1,
            queryFn: () => Promise.resolve("string"),
            onSuccess: (a: string) => {
              expectType<string>(a);
              expectTypeNotAny(a);
            },
            // @ts-expect-error (refuses to accept a Promise)
            onSettled: (a: Promise<string>) => null,
          },
        ],
      });
      expectType<QueryObserverResult<string, unknown>>(result5[0]);

      // Array as const does not throw error
      const result6 = useQueries({
        queries: [
          {
            queryKey: ["key1"],
            queryFn: () => "string",
          },
          {
            queryKey: ["key1"],
            queryFn: () => 123,
          },
        ],
      } as const);
      expectType<QueryObserverResult<string, unknown>>(result6[0]);
      expectType<QueryObserverResult<number, unknown>>(result6[1]);

      // field names should be enforced - array literal
      useQueries({
        queries: [
          {
            queryKey: key1,
            queryFn: () => "string",
            // @ts-expect-error (invalidField)
            someInvalidField: [],
          },
        ],
      });

      // field names should be enforced - Array.map() result
      useQueries({
        // @ts-expect-error (invalidField)
        queries: Array(10).map(() => ({
          someInvalidField: "",
        })),
      });

      // field names should be enforced - array literal
      useQueries({
        queries: [
          {
            queryKey: key1,
            queryFn: () => "string",
            // @ts-expect-error (invalidField)
            someInvalidField: [],
          },
        ],
      });

      // supports queryFn using fetch() to return Promise<any> - Array.map() result
      useQueries({
        queries: Array(50).map((_, i) => ({
          queryKey: ["key", i] as const,
          queryFn: () =>
            fetch("return Promise<any>").then((resp) => resp.json()),
        })),
      });

      // supports queryFn using fetch() to return Promise<any> - array literal
      useQueries({
        queries: [
          {
            queryKey: key1,
            queryFn: () =>
              fetch("return Promise<any>").then((resp) => resp.json()),
          },
        ],
      });
    }
  });

  it("handles strongly typed queryFn factories and useQueries wrappers", () => {
    // QueryKey + queryFn factory
    type QueryKeyA = ["queryA"];
    const getQueryKeyA = (): QueryKeyA => ["queryA"];
    type GetQueryFunctionA = () => QueryFunction<number, QueryKeyA>;
    const getQueryFunctionA: GetQueryFunctionA = () => async () => {
      return 1;
    };
    type SelectorA = (data: number) => [number, string];
    const getSelectorA = (): SelectorA => (data) => [data, data.toString()];

    type QueryKeyB = ["queryB", string];
    const getQueryKeyB = (id: string): QueryKeyB => ["queryB", id];
    type GetQueryFunctionB = () => QueryFunction<string, QueryKeyB>;
    const getQueryFunctionB: GetQueryFunctionB = () => async () => {
      return "1";
    };
    type SelectorB = (data: string) => [string, number];
    const getSelectorB = (): SelectorB => (data) => [data, +data];

    // Wrapper with strongly typed array-parameter
    function useWrappedQueries<
      TQueryFnData,
      TError,
      TData,
      TQueryKey extends QueryKey
    >(queries: UseQueryOptions<TQueryFnData, TError, TData, TQueryKey>[]) {
      return useQueries({
        queries: queries.map(
          // no need to type the mapped query
          (query) => {
            const { queryFn: fn, queryKey: key, onError: err } = query;
            expectType<QueryFunction<TQueryFnData, TQueryKey> | undefined>(fn);
            return {
              queryKey: key,
              onError: err,
              queryFn: fn
                ? (ctx: QueryFunctionContext<TQueryKey>) => {
                    expectType<TQueryKey>(ctx.queryKey);
                    return fn.call({}, ctx);
                  }
                : undefined,
            };
          }
        ),
      });
    }

    // @ts-expect-error (Page component is not rendered)
    // eslint-disable-next-line
    function Page() {
      const result = useQueries({
        queries: [
          {
            queryKey: getQueryKeyA(),
            queryFn: getQueryFunctionA(),
          },
          {
            queryKey: getQueryKeyB("id"),
            queryFn: getQueryFunctionB(),
          },
        ],
      });
      expectType<QueryObserverResult<number, unknown>>(result[0]);
      expectType<QueryObserverResult<string, unknown>>(result[1]);

      const withSelector = useQueries({
        queries: [
          {
            queryKey: getQueryKeyA(),
            queryFn: getQueryFunctionA(),
            select: getSelectorA(),
          },
          {
            queryKey: getQueryKeyB("id"),
            queryFn: getQueryFunctionB(),
            select: getSelectorB(),
          },
        ],
      });
      expectType<QueryObserverResult<[number, string], unknown>>(
        withSelector[0]
      );
      expectType<QueryObserverResult<[string, number], unknown>>(
        withSelector[1]
      );

      const withWrappedQueries = useWrappedQueries(
        Array(10).map(() => ({
          queryKey: getQueryKeyA(),
          queryFn: getQueryFunctionA(),
          select: getSelectorA(),
        }))
      );

      expectType<QueryObserverResult<number | undefined, unknown>[]>(
        withWrappedQueries
      );
    }
  });

  it("should not change state if unmounted", async () => {
    const key1 = queryKey();

    // We have to mock the QueriesObserver to not unsubscribe
    // the listener when the component is unmounted
    class QueriesObserverMock extends QueriesObserver {
      subscribe(listener: any) {
        super.subscribe(listener);
        return () => void 0;
      }
    }

    const QueriesObserverSpy = vi
      .spyOn(QueriesObserverModule, "QueriesObserver")
      .mockImplementation((fn) => {
        return new QueriesObserverMock(fn);
      });

    function Queries() {
      useQueries({
        queries: [
          {
            queryKey: key1,
            queryFn: async () => {
              await sleep(10);
              return 1;
            },
          },
        ],
      });

      return (
        <div>
          <span>queries</span>
        </div>
      );
    }

    function Page() {
      const [mounted, setMounted] = React.useState(true);

      return (
        <div>
          <button onClick={() => setMounted(false)}>unmount</button>
          {mounted && <Queries />}
        </div>
      );
    }

    const { getByText } = renderWithClient(queryClient, <Page />);
    fireEvent.click(getByText("unmount"));

    // Should not display the console error
    // "Warning: Can't perform a React state update on an unmounted component"

    await sleep(20);
    QueriesObserverSpy.mockRestore();
  });

  describe("with custom context", () => {
    it("should return the correct states", async () => {
      const context = React.createContext<QueryClient | undefined>(undefined);

      const key1 = queryKey();
      const key2 = queryKey();
      const results: UseQueryResult[][] = [];

      function Page() {
        const result = useQueries({
          context,
          queries: [
            {
              queryKey: key1,
              queryFn: async () => {
                await sleep(5);
                return 1;
              },
            },
            {
              queryKey: key2,
              queryFn: async () => {
                await sleep(10);
                return 2;
              },
            },
          ],
        });
        results.push(result);

        return (
          <div>
            <div>data1: {result[0].data}</div>
            <div>data2: {result[1].data}</div>
          </div>
        );
      }

      const rendered = renderWithClient(queryClient, <Page />, { context });

      await waitFor(() => {
        rendered.getByText("data1: 1");
        rendered.getByText("data2: 2");
      });

      expect(results[0]).toMatchObject([
        { data: undefined },
        { data: undefined },
      ]);
      expect(results[results.length - 1]).toMatchObject([
        { data: 1 },
        { data: 2 },
      ]);
    });

    it("should throw if the context is necessary and is not passed to useQueries", async () => {
      const context = React.createContext<QueryClient | undefined>(undefined);

      const key1 = queryKey();
      const key2 = queryKey();
      const results: UseQueryResult[][] = [];

      function Page() {
        const result = useQueries({
          queries: [
            {
              queryKey: key1,
              queryFn: async () => 1,
            },
            {
              queryKey: key2,
              queryFn: async () => 2,
            },
          ],
        });
        results.push(result);
        return null;
      }

      const rendered = renderWithClient(
        queryClient,
        <ErrorBoundary fallbackRender={() => <div>error boundary</div>}>
          <Page />
        </ErrorBoundary>,
        { context }
      );

      await waitFor(() => rendered.getByText("error boundary"));
    });
  });

  it("should throw error if in one of queries' queryFn throws and useErrorBoundary is in use", async () => {
    const key1 = queryKey();
    const key2 = queryKey();
    const key3 = queryKey();
    const key4 = queryKey();

    function Page() {
      useQueries({
        queries: [
          {
            queryKey: key1,
            queryFn: () =>
              Promise.reject(
                new Error(
                  "this should not throw because useErrorBoundary is not set"
                )
              ),
          },
          {
            queryKey: key2,
            queryFn: () => Promise.reject(new Error("single query error")),
            useErrorBoundary: true,
            retry: false,
          },
          {
            queryKey: key3,
            queryFn: async () => 2,
          },
          {
            queryKey: key4,
            queryFn: async () =>
              Promise.reject(
                new Error("this should not throw because query#2 already did")
              ),
            useErrorBoundary: true,
            retry: false,
          },
        ],
      });

      return null;
    }

    const rendered = renderWithClient(
      queryClient,
      <ErrorBoundary
        fallbackRender={({ error }) => (
          <div>
            <div>error boundary</div>
            <div>{error.message}</div>
          </div>
        )}
      >
        <Page />
      </ErrorBoundary>
    );

    await waitFor(() => rendered.getByText("error boundary"));
    await waitFor(() => rendered.getByText("single query error"));
  });

  it("should throw error if in one of queries' queryFn throws and useErrorBoundary function resolves to true", async () => {
    const key1 = queryKey();
    const key2 = queryKey();
    const key3 = queryKey();
    const key4 = queryKey();

    function Page() {
      useQueries({
        queries: [
          {
            queryKey: key1,
            queryFn: () =>
              Promise.reject(
                new Error(
                  "this should not throw because useErrorBoundary function resolves to false"
                )
              ),
            useErrorBoundary: () => false,
            retry: false,
          },
          {
            queryKey: key2,
            queryFn: async () => 2,
          },
          {
            queryKey: key3,
            queryFn: () => Promise.reject(new Error("single query error")),
            useErrorBoundary: () => true,
            retry: false,
          },
          {
            queryKey: key4,
            queryFn: async () =>
              Promise.reject(
                new Error("this should not throw because query#3 already did")
              ),
            useErrorBoundary: true,
            retry: false,
          },
        ],
      });

      return null;
    }

    const rendered = renderWithClient(
      queryClient,
      <ErrorBoundary
        fallbackRender={({ error }) => (
          <div>
            <div>error boundary</div>
            <div>{error.message}</div>
          </div>
        )}
      >
        <Page />
      </ErrorBoundary>
    );

    await waitFor(() => rendered.getByText("error boundary"));
    await waitFor(() => rendered.getByText("single query error"));
  });
});
