// @vitest-environment happy-dom
// Mirror the IS_REACT_ACT_ENVIRONMENT setup from useDrawerUrlState.test.ts
// so React doesn't log act() warnings about the createRoot render/unmount
// and the popstate-driven state updates.
(globalThis as { IS_REACT_ACT_ENVIRONMENT?: boolean }).IS_REACT_ACT_ENVIRONMENT = true;

import React, { act } from "react";
import { createRoot, type Root } from "react-dom/client";
import { describe, expect, it, vi, beforeEach, afterEach } from "vitest";
import { useRemitDrawerUrlState } from "./useRemitDrawerUrlState";

/**
 * Minimal renderHook shim — same pattern as the other hook tests.
 */
function renderHook<TResult>(setup: () => TResult): {
  result: { current: TResult | undefined };
  unmount: () => void;
} {
  const result: { current: TResult | undefined } = { current: undefined };
  const container = document.createElement("div");
  document.body.appendChild(container);

  function Probe() {
    result.current = setup();
    return null;
  }

  const root: Root = createRoot(container);
  act(() => {
    root.render(React.createElement(Probe));
  });

  return {
    result,
    unmount: () => {
      act(() => root.unmount());
      container.remove();
    },
  };
}

/**
 * Point happy-dom's URL at a known value. happy-dom v20 doesn't expose a
 * writable `window.location.search`, but `window.happyDOM.setURL` updates
 * the URL the window reports without triggering a navigation — exactly
 * what we want for mounting the hook at `/remittances?remit=REM-1` etc.
 */
function setLocation(url: string): void {
  (window as unknown as { happyDOM: { setURL: (u: string) => void } }).happyDOM.setURL(url);
}

describe("useRemitDrawerUrlState", () => {
  type PushState = (state: unknown, unused: string, url?: string | URL | null) => void;
  let pushStateMock: ReturnType<typeof vi.fn<PushState>>;
  let replaceStateMock: ReturnType<typeof vi.fn<PushState>>;

  beforeEach(() => {
    pushStateMock = vi.fn();
    replaceStateMock = vi.fn();

    vi.stubGlobal("history", {
      pushState: pushStateMock,
      replaceState: replaceStateMock,
      state: null,
    });
  });

  afterEach(() => {
    vi.unstubAllGlobals();
    setLocation("http://localhost/");
  });

  it("reads the ?remit= param from window.location.search on mount", () => {
    setLocation("http://localhost/remittances?remit=REM-1");

    const { result, unmount } = renderHook(() => useRemitDrawerUrlState());

    expect(result.current?.remitId).toBe("REM-1");
    expect(typeof result.current?.open).toBe("function");
    expect(typeof result.current?.close).toBe("function");
    expect(typeof result.current?.setRemitId).toBe("function");

    unmount();
  });

  it("returns null remitId when no ?remit= param is set", () => {
    setLocation("http://localhost/remittances");

    const { result, unmount } = renderHook(() => useRemitDrawerUrlState());

    expect(result.current?.remitId).toBeNull();

    unmount();
  });

  it("returns null remitId when ?remit= is present but empty", () => {
    setLocation("http://localhost/remittances?remit=");

    const { result, unmount } = renderHook(() => useRemitDrawerUrlState());

    expect(result.current?.remitId).toBeNull();

    unmount();
  });

  it("open(id) pushes a new history entry containing ?remit=id", () => {
    setLocation("http://localhost/remittances");

    const { result, unmount } = renderHook(() => useRemitDrawerUrlState());

    act(() => {
      result.current?.open("REM-2");
    });

    expect(pushStateMock).toHaveBeenCalledTimes(1);
    expect(replaceStateMock).not.toHaveBeenCalled();
    const urlArg = pushStateMock.mock.calls[0][2] as string;
    expect(urlArg).toContain("?remit=REM-2");

    unmount();
  });

  it("setRemitId(id) replaces the current history entry (no new entry) and does NOT pushState", () => {
    setLocation("http://localhost/remittances?remit=REM-1");

    const { result, unmount } = renderHook(() => useRemitDrawerUrlState());

    act(() => {
      result.current?.setRemitId("REM-3");
    });

    expect(replaceStateMock).toHaveBeenCalledTimes(1);
    expect(pushStateMock).not.toHaveBeenCalled();
    const urlArg = replaceStateMock.mock.calls[0][2] as string;
    expect(urlArg).toContain("?remit=REM-3");

    unmount();
  });

  it("open() and close() preserve other query params (only ?remit= is touched)", () => {
    setLocation("http://localhost/remittances?page=2&remit=REM-1");

    const { result, unmount } = renderHook(() => useRemitDrawerUrlState());

    act(() => {
      result.current?.open("REM-2");
    });
    const openUrl = pushStateMock.mock.calls[0][2] as string;
    expect(openUrl).toContain("page=2");
    expect(openUrl).toMatch(/[?&]remit=REM-2/);

    act(() => {
      result.current?.close();
    });
    const closeUrl = pushStateMock.mock.calls[1][2] as string;
    expect(closeUrl).toContain("page=2");
    expect(closeUrl).not.toContain("remit=");

    unmount();
  });

  it("close() pushes a new history entry with the ?remit= param stripped", () => {
    setLocation("http://localhost/remittances?remit=REM-1");

    const { result, unmount } = renderHook(() => useRemitDrawerUrlState());

    act(() => {
      result.current?.close();
    });

    expect(pushStateMock).toHaveBeenCalledTimes(1);
    expect(replaceStateMock).not.toHaveBeenCalled();
    const urlArg = pushStateMock.mock.calls[0][2] as string;
    expect(urlArg).not.toContain("?remit=");
    expect(result.current?.remitId).toBeNull();

    unmount();
  });

  it("updates remitId in response to popstate (browser back/forward)", async () => {
    setLocation("http://localhost/remittances?remit=REM-1");

    const { result, unmount } = renderHook(() => useRemitDrawerUrlState());

    expect(result.current?.remitId).toBe("REM-1");

    setLocation("http://localhost/remittances?remit=REM-2");
    await act(async () => {
      window.dispatchEvent(new PopStateEvent("popstate"));
      await Promise.resolve();
    });

    expect(result.current?.remitId).toBe("REM-2");

    unmount();
  });

  it("does not collide with the existing ?claim= param (orthogonal keys)", () => {
    // The remits drawer and the claims drawer are independent — both
    // can be open simultaneously in the future, and the hooks must not
    // stomp each other's URL state. Opening a remit must leave
    // ?claim= intact, and vice-versa.
    setLocation("http://localhost/?claim=CLM-1");

    const { result, unmount } = renderHook(() => useRemitDrawerUrlState());

    act(() => {
      result.current?.open("REM-1");
    });
    const openUrl = pushStateMock.mock.calls[0][2] as string;
    expect(openUrl).toContain("claim=CLM-1");
    expect(openUrl).toMatch(/[?&]remit=REM-1/);

    unmount();
  });
});
