import { createNavigationMock, renderWithUser } from "@repo/test-utils";
import { screen, waitFor } from "@testing-library/react";
import { useEffect, useRef, useState } from "react";
import { beforeEach, describe, expect, test } from "vitest";
import {
  type LocationStateDefinition,
  useLocationGetKey,
  useLocationGetState,
  useLocationSetState,
  useLocationState,
  useLocationStateValue,
} from "./hooks";
import { LocationStateProvider } from "./provider";
import { LOCATION_KEY_PREFIX } from "./stores";

const mockNavigation = createNavigationMock("/");
// @ts-expect-error
globalThis.navigation = mockNavigation;

beforeEach(() => {
  mockNavigation.navigate("/");
  sessionStorage.clear();
});

describe(useLocationState, () => {
  function LocationSyncCounter() {
    const [count, setCount] = useLocationState({
      name: "count",
      defaultValue: 0,
      storeName: "session",
    });
    return (
      <div>
        <h1>count: {count}</h1>
        <button type="button" onClick={() => setCount(count + 1)}>
          increment
        </button>
        <button type="button" onClick={() => setCount((prev) => prev + 1)}>
          increment with updater
        </button>
      </div>
    );
  }

  function LocationSyncCounterPage() {
    return (
      <LocationStateProvider>
        <LocationSyncCounter />
      </LocationStateProvider>
    );
  }

  test("The `count` can be updated.", async () => {
    // Arrange
    const { user } = renderWithUser(<LocationSyncCounterPage />);
    // Act
    await user.click(await screen.findByRole("button", { name: "increment" }));
    // Assert
    expect(screen.getByRole("heading")).toHaveTextContent("count: 1");
  });

  test("The `count` can be updated with updater.", async () => {
    // Arrange
    const { user } = renderWithUser(<LocationSyncCounterPage />);
    // Act
    await user.click(
      await screen.findByRole("button", { name: "increment with updater" }),
    );
    // Assert
    expect(screen.getByRole("heading")).toHaveTextContent("count: 1");
  });

  test("The `count` is reset at navigation.", async () => {
    // Arrange
    const { user } = renderWithUser(<LocationSyncCounterPage />);
    await user.click(await screen.findByRole("button", { name: "increment" }));
    // Act
    mockNavigation.navigate("/anywhere");
    // Assert
    await waitFor(() =>
      expect(screen.getByRole("heading")).toHaveTextContent("count: 0"),
    );
  });

  test("If there is a value in `sessionStorage`, it will be restored as the initial value.", async () => {
    // Arrange
    const key = mockNavigation.currentEntry?.key as string;
    sessionStorage.setItem(
      `${LOCATION_KEY_PREFIX}::value:${key}`,
      `{"count":2}`,
    );
    // Act
    renderWithUser(<LocationSyncCounterPage />);
    // Assert
    await waitFor(() =>
      expect(screen.getByRole("heading")).toHaveTextContent("count: 2"),
    );
  });
});

describe(useLocationStateValue, () => {
  function LocationSyncCounter() {
    const counter: LocationStateDefinition<number> = {
      name: "count",
      defaultValue: 0,
      storeName: "session",
    };
    const count = useLocationStateValue(counter);
    return <h1>count: {count}</h1>;
  }

  function LocationSyncCounterPage() {
    return (
      <LocationStateProvider>
        <LocationSyncCounter />
      </LocationStateProvider>
    );
  }

  test("If there is a value in `sessionStorage`, it will be restored as the initial value.", async () => {
    // Arrange
    const key = mockNavigation.currentEntry?.key as string;
    sessionStorage.setItem(
      `${LOCATION_KEY_PREFIX}::value:${key}`,
      `{"count":2}`,
    );
    // Act
    renderWithUser(<LocationSyncCounterPage />);
    // Assert
    await waitFor(() =>
      expect(screen.getByRole("heading")).toHaveTextContent("count: 2"),
    );
  });
});

describe(useLocationSetState, () => {
  function LocationSyncCounter() {
    const counter: LocationStateDefinition<number> = {
      name: "counter",
      defaultValue: 0,
      storeName: "session",
    };
    const rendered = useRef(1);
    const setCount = useLocationSetState(counter);
    useEffect(() => {
      rendered.current++;
    }, []);

    return (
      <div>
        <h1>rendered: {rendered.current}</h1>
        <button type="button" onClick={() => setCount((prev) => prev + 1)}>
          increment
        </button>
      </div>
    );
  }

  function LocationSyncCounterPage() {
    return (
      <LocationStateProvider>
        <LocationSyncCounter />
      </LocationStateProvider>
    );
  }

  test("`setCount` does not re-render.", async () => {
    // Arrange
    const { user } = renderWithUser(<LocationSyncCounterPage />);
    // Act
    await user.click(await screen.findByRole("button", { name: "increment" }));
    // Assert
    expect(screen.getByRole("heading")).toHaveTextContent("rendered: 1");
  });
});

describe(useLocationGetState, () => {
  function LocationSyncOnceCounter() {
    const counter: LocationStateDefinition<number> = {
      name: "count",
      defaultValue: 0,
      storeName: "session",
    };
    const getState = useLocationGetState(counter);
    const [, setState] = useState(false);

    return (
      <>
        <h1>count: {getState()}</h1>
        <button type="button" onClick={() => setState((prev) => !prev)}>
          force render
        </button>
      </>
    );
  }

  function LocationSyncCounterPage() {
    return (
      <LocationStateProvider>
        <LocationSyncOnceCounter />
      </LocationStateProvider>
    );
  }

  test("If there is a value in `sessionStorage`, it is not re-rendered.", async () => {
    // Arrange
    const key = mockNavigation.currentEntry?.key as string;
    sessionStorage.setItem(
      `${LOCATION_KEY_PREFIX}::value:${key}`,
      `{"count":2}`,
    );
    // Act
    renderWithUser(<LocationSyncCounterPage />);
    // Assert
    await waitFor(() =>
      expect(screen.getByRole("heading")).toHaveTextContent("count: 0"),
    );
  });

  test("Always get the latest values.", async () => {
    // Arrange
    const key = mockNavigation.currentEntry?.key as string;
    sessionStorage.setItem(
      `${LOCATION_KEY_PREFIX}::value:${key}`,
      `{"count":2}`,
    );
    const { user } = renderWithUser(<LocationSyncCounterPage />);
    // Act
    await user.click(
      await screen.findByRole("button", { name: "force render" }),
    );
    // Assert
    expect(screen.getByRole("heading")).toHaveTextContent("count: 2");
  });
});

describe(useLocationGetKey, () => {
  function LocationSyncKeyDisplay() {
    const getLocationKey = useLocationGetKey();
    const [key, setKey] = useState<string | undefined>();

    return (
      <div>
        <h1>key: {key ?? "undefined"}</h1>
        <button type="button" onClick={() => setKey(getLocationKey())}>
          get key
        </button>
      </div>
    );
  }

  function LocationSyncKeyPage() {
    return (
      <LocationStateProvider>
        <LocationSyncKeyDisplay />
      </LocationStateProvider>
    );
  }

  test("Returns a function that gets the current location key", async () => {
    // Arrange
    const { user } = renderWithUser(<LocationSyncKeyPage />);
    // Act
    await user.click(await screen.findByRole("button", { name: "get key" }));
    // Assert
    expect(screen.getByRole("heading")).not.toHaveTextContent("key: undefined");
  });

  test("Does not cause re-renders when location key changes", async () => {
    // Arrange
    const { user } = renderWithUser(<LocationSyncKeyPage />);
    await user.click(screen.getByRole("button", { name: "get key" }));
    const initialKey = screen.getByRole("heading", { level: 1 }).textContent;
    // Act
    mockNavigation.navigate("/new-path");
    // Assert
    expect(screen.getByRole("heading", { level: 1 })).toHaveTextContent(
      initialKey!,
    );
  });

  test("Always gets the latest key when re-render", async () => {
    // Arrange
    const initialKey = mockNavigation.currentEntry?.key as string;
    const { user } = renderWithUser(<LocationSyncKeyPage />);
    mockNavigation.navigate("/new-path");
    // Act
    await user.click(screen.getByRole("button", { name: "get key" }));
    // Assert
    const displayedKey = screen.getByRole("heading", { level: 1 }).textContent;
    expect(displayedKey).not.toContain("undefined");
    expect(displayedKey).not.toBe(`key: ${initialKey}`); // Should be different from initial key
  });
});
