// @vitest-environment happy-dom
// Mirror the IS_REACT_ACT_ENVIRONMENT setup from useProviderDrawerUrlState.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 { useAckDrawerUrlState } from "./useAckDrawerUrlState";

/**
 * 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 `/acks?ack=42` etc.
 */
function setLocation(url: string): void {
  (window as unknown as { happyDOM: { setURL: (u: string) => void } }).happyDOM.setURL(url);
}

describe("useAckDrawerUrlState", () => {
  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 ?ack= param from window.location.search on mount", () => {
    setLocation("http://localhost/acks?ack=42");

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

    expect(result.current?.ackId).toBe("42");
    expect(typeof result.current?.open).toBe("function");
    expect(typeof result.current?.close).toBe("function");
    expect(typeof result.current?.setAckId).toBe("function");

    unmount();
  });

  it("returns null ackId when no ?ack= param is set", () => {
    setLocation("http://localhost/acks");

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

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

    unmount();
  });

  it("returns null ackId when ?ack= is present but empty", () => {
    setLocation("http://localhost/acks?ack=");

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

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

    unmount();
  });

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

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

    act(() => {
      result.current?.open("42");
    });

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

    unmount();
  });

  it("setAckId(id) replaces the current history entry (no new entry) and does NOT pushState", () => {
    setLocation("http://localhost/acks?ack=42");

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

    act(() => {
      result.current?.setAckId("43");
    });

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

    unmount();
  });

  it("open() and close() preserve other query params (only ?ack= is touched)", () => {
    setLocation("http://localhost/acks?sort=date&ack=42");

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

    act(() => {
      result.current?.open("43");
    });
    const openUrl = pushStateMock.mock.calls[0][2] as string;
    expect(openUrl).toContain("sort=date");
    expect(openUrl).toMatch(/[?&]ack=43/);

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

    unmount();
  });

  it("close() pushes a new history entry with the ?ack= param stripped", () => {
    setLocation("http://localhost/acks?ack=42");

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

    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("?ack=");
    expect(result.current?.ackId).toBeNull();

    unmount();
  });

  it("updates ackId in response to popstate (browser back/forward)", async () => {
    setLocation("http://localhost/acks?ack=42");

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

    expect(result.current?.ackId).toBe("42");

    setLocation("http://localhost/acks?ack=43");
    await act(async () => {
      window.dispatchEvent(new PopStateEvent("popstate"));
      await Promise.resolve();
    });

    expect(result.current?.ackId).toBe("43");

    unmount();
  });

  it("does not collide with the existing ?claim=, ?remit=, or ?provider= params (orthogonal keys)", () => {
    // The acks drawer is independent of the claim/remit/provider
    // drawers — opening an ack must leave the others intact so a user
    // can deep-link to multiple states simultaneously (though the UI
    // currently only shows one drawer at a time, the URL params don't
    // know that).
    setLocation("http://localhost/?claim=CLM-1&remit=REM-1&provider=1881068062");

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

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

    unmount();
  });
});
