import { act, render, renderHook, screen } from "@testing-library/react";
import { hydrateRoot } from "react-dom/client";
import { renderToString } from "react-dom/server";
import type { BreakpointsType } from ".";
import { DEFAULT_BREAKPOINTS, useBreakpoint } from ".";

const TestComponent = () => {
  const { breakpoint } = useBreakpoint();

  return <div>{breakpoint.size}</div>;
};

const customWrapper = ({ children }: any) => (
  <div>
    <TestComponent />
    {children}
  </div>
);

describe("useBreakpoint", () => {
  let originalInnerWidth: number;

  beforeAll(() => {
    originalInnerWidth = window.innerWidth;
    Object.defineProperty(window, "innerWidth", {
      writable: true,
      configurable: true,
    });
  });

  const setWindowWidth = (value: number) => {
    Reflect.set(window, "innerWidth", value);
    window.dispatchEvent(new Event("resize"));
  };

  afterEach(() => {
    // Reset window width to original value
    Reflect.set(window, "innerWidth", originalInnerWidth);
  });

  describe("given no breakpoints are provided through a Context", () => {
    it("returns default breakpoint", () => {
      setWindowWidth(300);
      render(<TestComponent />);

      expect(screen.getByText(DEFAULT_BREAKPOINTS[0].size)).toBeInTheDocument();
    });
  });

  describe.each([
    [300, "xs"],
    [600, "sm"],
    [670, "md"],
    [800, "md"],
    [1000, "lg"],
    [1200, "xl"],
    [1400, "xxl"],
  ])("given window width %s", (size, expected) => {
    it(`it returns breakpoint ${expected}`, () => {
      setWindowWidth(size);

      const { result } = renderHook(() => useBreakpoint());

      expect(result.current.breakpoint.size).toEqual(expected);
    });
  });

  describe("up", () => {
    const table = [
      [
        300,
        [
          [true, "xs"],
          [false, "sm"],
          [false, "md"],
          [false, "lg"],
          [false, "xl"],
          [false, "xxl"],
        ],
      ],
      [
        600,
        [
          [true, "xs"],
          [true, "sm"],
          [false, "md"],
          [false, "lg"],
          [false, "xl"],
          [false, "xxl"],
        ],
      ],
      [
        800,
        [
          [true, "xs"],
          [true, "sm"],
          [true, "md"],
          [false, "lg"],
          [false, "xl"],
          [false, "xxl"],
        ],
      ],
      [
        1000,
        [
          [true, "xs"],
          [true, "sm"],
          [true, "md"],
          [true, "lg"],
          [false, "xl"],
          [false, "xxl"],
        ],
      ],
      [
        1200,
        [
          [true, "xs"],
          [true, "sm"],
          [true, "md"],
          [true, "lg"],
          [true, "xl"],
          [false, "xxl"],
        ],
      ],
      [
        1400,
        [
          [true, "xs"],
          [true, "sm"],
          [true, "md"],
          [true, "lg"],
          [true, "xl"],
          [true, "xxl"],
        ],
      ],
    ] as const;

    describe.each(table)("given window width %s", (value, expected) => {
      it.each([...expected])(
        "it returns %s for breakpoint %s",
        (expectedResult, breakpoint) => {
          setWindowWidth(value);

          const { result } = renderHook(() => useBreakpoint());
          expect(result.current.up(breakpoint)).toBe(expectedResult);
        },
      );
    });
  });

  describe("down", () => {
    const table = [
      [
        300,
        [
          [true, "xs"],
          [true, "sm"],
          [true, "md"],
          [true, "lg"],
          [true, "xl"],
          [true, "xxl"],
        ],
      ],
      [
        600,
        [
          [false, "xs"],
          [true, "sm"],
          [true, "md"],
          [true, "lg"],
          [true, "xl"],
          [true, "xxl"],
        ],
      ],
      [
        800,
        [
          [false, "xs"],
          [false, "sm"],
          [true, "md"],
          [true, "lg"],
          [true, "xl"],
          [true, "xxl"],
        ],
      ],
      [
        1000,
        [
          [false, "xs"],
          [false, "sm"],
          [false, "md"],
          [true, "lg"],
          [true, "xl"],
          [true, "xxl"],
        ],
      ],
      [
        1200,
        [
          [false, "xs"],
          [false, "sm"],
          [false, "md"],
          [false, "lg"],
          [true, "xl"],
          [true, "xxl"],
        ],
      ],
      [
        1400,
        [
          [false, "xs"],
          [false, "sm"],
          [false, "md"],
          [false, "lg"],
          [false, "xl"],
          [true, "xxl"],
        ],
      ],
    ] as const;

    describe.each(table)("given window width %s", (value, expected) => {
      it.each([...expected])(
        "it returns %s for breakpoint %s",
        (expectedResult, breakpoint) => {
          setWindowWidth(value);

          const { result } = renderHook(() => useBreakpoint());
          expect(result.current.down(breakpoint)).toBe(expectedResult);
        },
      );
    });
  });

  describe("respects custom breakpoints when supplied", () => {
    const customBreakpoints = [
      { size: "xs", value: 320 },
      { size: "sm", value: 480 },
      { size: "md", value: 600 },
      { size: "lg", value: 850 },
      { size: "xl", value: 1024 },
    ] as BreakpointsType;

    it("returns default breakpoint", () => {
      setWindowWidth(0);
      const { result } = renderHook(() => useBreakpoint(customBreakpoints));
      expect(result.current.breakpoint.size).toEqual(customBreakpoints[0].size);
    });

    describe.each([
      [300, "xs"],
      [490, "sm"],
      [600, "md"],
      [800, "md"],
      [860, "lg"],
      [1000, "lg"],
      [1400, "xl"],
    ])("given window width %s", (size, expected) => {
      it(`it returns breakpoint ${expected}`, () => {
        setWindowWidth(size);

        const { result } = renderHook(() => useBreakpoint(customBreakpoints), {
          wrapper: customWrapper,
        });

        expect(result.current.breakpoint.size).toEqual(expected);
      });
    });

    describe("up", () => {
      const table = [
        [
          300,
          [
            [true, "xs"],
            [false, "sm"],
            [false, "md"],
            [false, "lg"],
            [false, "xl"],
            [false, "xxl"],
          ],
        ],
        [
          600,
          [
            [true, "xs"],
            [true, "sm"],
            [true, "md"],
            [false, "lg"],
            [false, "xl"],
            [false, "xxl"],
          ],
        ],
        [
          800,
          [
            [true, "xs"],
            [true, "sm"],
            [true, "md"],
            [false, "lg"],
            [false, "xl"],
            [false, "xxl"],
          ],
        ],
        [
          1000,
          [
            [true, "xs"],
            [true, "sm"],
            [true, "md"],
            [true, "lg"],
            [false, "xl"],
            [false, "xxl"],
          ],
        ],
        [
          1200,
          [
            [true, "xs"],
            [true, "sm"],
            [true, "md"],
            [true, "lg"],
            [true, "xl"],
            [false, "xxl"],
          ],
        ],
        [
          1400,
          [
            [true, "xs"],
            [true, "sm"],
            [true, "md"],
            [true, "lg"],
            [true, "xl"],
            [false, "xxl"],
          ],
        ],
      ] as const;

      describe.each(table)("given window width %s", (value, expected) => {
        it.each([...expected])(
          "it returns %s for breakpoint %s",
          (expectedResult, breakpoint) => {
            setWindowWidth(value);

            const { result } = renderHook(() =>
              useBreakpoint(customBreakpoints),
            );
            expect(result.current.up(breakpoint)).toBe(expectedResult);
          },
        );
      });
    });

    describe("down", () => {
      const table = [
        [
          300,
          [
            [true, "xs"],
            [true, "sm"],
            [true, "md"],
            [true, "lg"],
            [true, "xl"],
          ],
        ],
        [
          600,
          [
            [false, "xs"],
            [true, "sm"],
            [true, "md"],
            [true, "lg"],
            [true, "xl"],
          ],
        ],
        [
          800,
          [
            [false, "xs"],
            [false, "sm"],
            [true, "md"],
            [true, "lg"],
            [true, "xl"],
          ],
        ],
        [
          1000,
          [
            [false, "xs"],
            [false, "sm"],
            [false, "md"],
            [true, "lg"],
            [true, "xl"],
          ],
        ],
        [
          1200,
          [
            [false, "xs"],
            [false, "sm"],
            [false, "md"],
            [false, "lg"],
            [true, "xl"],
          ],
        ],
        [
          1400,
          [
            [false, "xs"],
            [false, "sm"],
            [false, "md"],
            [false, "lg"],
            [false, "xl"],
          ],
        ],
      ] as const;

      describe.each(table)("given window width %s", (value, expected) => {
        it.each([...expected])(
          "it returns %s for breakpoint %s",
          (expectedResult, breakpoint) => {
            setWindowWidth(value);

            const { result } = renderHook(() => useBreakpoint());
            expect(result.current.down(breakpoint)).toBe(expectedResult);
          },
        );
      });
    });
  });

  describe("resize after mount", () => {
    it("updates the breakpoint after the debounce delay", () => {
      vi.useFakeTimers();
      setWindowWidth(300);

      const { result } = renderHook(() => useBreakpoint());
      expect(result.current.breakpoint.size).toBe("xs");

      act(() => {
        Reflect.set(window, "innerWidth", 1400);
        window.dispatchEvent(new Event("resize"));
      });

      // Before debounce settles, still xs
      expect(result.current.breakpoint.size).toBe("xs");

      act(() => {
        vi.advanceTimersByTime(200);
      });

      expect(result.current.breakpoint.size).toBe("xxl");
      vi.useRealTimers();
    });
  });

  describe("hydration", () => {
    it("does not produce hydration mismatches", () => {
      // Window is xxl — deliberately different from default xs breakpoint
      setWindowWidth(1400);

      const view = renderToString(<TestComponent />);
      // Server should render the default (xs) regardless of window width
      expect(view).toContain("xs");

      // React 19 throws on hydration mismatch rather than logging to
      // console.error, so a mismatch here will fail the test automatically.
      const container = document.createElement("div");
      const parsed = new DOMParser().parseFromString(view, "text/html");
      container.append(...parsed.body.childNodes);
      document.body.appendChild(container);

      act(() => {
        hydrateRoot(container, <TestComponent />);
      });

      document.body.removeChild(container);
    });
  });
});
