import { act, renderHook } from "@testing-library/react";

import { wait } from "../../__test__/utils/wait";
import { InputDeviceProvider, useInputDevice } from ".";

const KNOWN_ERRORS = [
  "InputDeviceProvider is missing in the tree",
  "Consider adding an error boundary to your tree",
];

const isKnownError = (arg: unknown) => {
  if (typeof arg !== "string") {
    return;
  }

  return KNOWN_ERRORS.some((err) => arg.includes(err));
};

describe("input device hook", () => {
  afterEach(() => {
    jest.restoreAllMocks();
  });

  it("throws without provider", () => {
    const originalError = console.error;

    jest.spyOn(console, "error").mockImplementation((...args: unknown[]) => {
      if (isKnownError(args[0])) {
        return;
      }
      return originalError(...args);
    });

    expect(() => renderHook(() => useInputDevice())).toThrowError(
      "InputDeviceProvider is missing in the tree"
    );
  });

  it("returns defined default value", () => {
    const wrapper: React.FC<{ children: React.ReactNode }> = ({ children }) => (
      <InputDeviceProvider>{children}</InputDeviceProvider>
    );

    const { result } = renderHook(() => useInputDevice("none"), { wrapper });
    expect(result.current).toEqual("none");
  });

  it("returns mouse by default", () => {
    const wrapper: React.FC<{ children: React.ReactNode }> = ({ children }) => (
      <InputDeviceProvider>{children}</InputDeviceProvider>
    );

    const { result } = renderHook(() => useInputDevice(), { wrapper });
    expect(result.current).toEqual("mouse");
  });

  describe("reacts on different events", () => {
    it("touch event 1", () => {
      const wrapper: React.FC<{ children: React.ReactNode }> = ({ children }) => (
        <InputDeviceProvider>{children}</InputDeviceProvider>
      );

      const { result, rerender } = renderHook(() => useInputDevice("def"), { wrapper });
      expect(result.current).toEqual("def");

      const touchEvent1 = new Event("touchmove", {
        bubbles: true,
      }) as MediaQueryListEvent;
      act(() => {
        window.dispatchEvent(touchEvent1);
      });
      rerender();
      expect(result.current).toEqual("touch");
    });

    it("touch event 2", () => {
      const wrapper: React.FC<{ children: React.ReactNode }> = ({ children }) => (
        <InputDeviceProvider>{children}</InputDeviceProvider>
      );

      const { result, rerender } = renderHook(() => useInputDevice("def"), { wrapper });
      expect(result.current).toEqual("def");

      const touchEvent2 = new Event("touchend", {
        bubbles: true,
      }) as MediaQueryListEvent;
      act(() => {
        window.dispatchEvent(touchEvent2);
      });
      rerender();
      expect(result.current).toEqual("touch");
    });

    it("mouse event 1", () => {
      const wrapper: React.FC<{ children: React.ReactNode }> = ({ children }) => (
        <InputDeviceProvider>{children}</InputDeviceProvider>
      );

      const { result, rerender } = renderHook(() => useInputDevice("def"), { wrapper });
      expect(result.current).toEqual("def");

      const mouseEvent1 = new Event("mousemove", {
        bubbles: true,
      }) as MediaQueryListEvent;
      act(() => {
        window.dispatchEvent(mouseEvent1);
      });
      rerender();
      expect(result.current).toEqual("mouse");
    });

    it("mouse event 2", () => {
      const wrapper: React.FC<{ children: React.ReactNode }> = ({ children }) => (
        <InputDeviceProvider>{children}</InputDeviceProvider>
      );

      const { result, rerender } = renderHook(() => useInputDevice("def"), { wrapper });
      expect(result.current).toEqual("def");

      const mouseEvent2 = new Event("wheel", {
        bubbles: true,
      }) as MediaQueryListEvent;
      act(() => {
        window.dispatchEvent(mouseEvent2);
      });
      rerender();
      expect(result.current).toEqual("mouse");
    });

    it("instantly changes from mouse to touch", () => {
      const wrapper: React.FC<{ children: React.ReactNode }> = ({ children }) => (
        <InputDeviceProvider>{children}</InputDeviceProvider>
      );

      const { result, rerender } = renderHook(() => useInputDevice("def"), { wrapper });
      expect(result.current).toEqual("def");

      const mouseEvent2 = new Event("wheel", {
        bubbles: true,
      }) as MediaQueryListEvent;
      act(() => {
        window.dispatchEvent(mouseEvent2);
      });
      rerender();
      expect(result.current).toEqual("mouse");

      const touchEvent1 = new Event("touchmove", {
        bubbles: true,
      }) as MediaQueryListEvent;
      act(() => {
        window.dispatchEvent(touchEvent1);
      });
      rerender();
      expect(result.current).toEqual("touch");
    });

    it("does not instantly changes from touch to mouse to prevent bugs", async () => {
      const wrapper: React.FC<{ children: React.ReactNode }> = ({ children }) => (
        <InputDeviceProvider>{children}</InputDeviceProvider>
      );

      const { result, rerender } = renderHook(() => useInputDevice("def"), { wrapper });
      expect(result.current).toEqual("def");

      const touchEvent1 = new Event("touchmove", {
        bubbles: true,
      }) as MediaQueryListEvent;
      act(() => {
        window.dispatchEvent(touchEvent1);
      });
      rerender();
      expect(result.current).toEqual("touch");

      const mouseEvent1 = new Event("wheel", {
        bubbles: true,
      }) as MediaQueryListEvent;
      act(() => {
        window.dispatchEvent(mouseEvent1);
      });
      rerender();
      expect(result.current).toEqual("touch");

      await wait(350);

      act(() => {
        window.dispatchEvent(mouseEvent1);
      });
      rerender();
      expect(result.current).toEqual("mouse");
    });
  });
});
