// @vitest-environment jsdom
/**
 * Hook test for useUrlState — the history/popstate wiring around the pure
 * urlState lib. Verifies initial read from location.search, popstate re-read,
 * pushUrlState writing history (push vs replace for search-only changes), and
 * replaceUrlSearch normalizing without adding a history entry.
 */
import '../test/dom-setup';
import { describe, it, expect, beforeEach, vi } from 'vitest';
import { renderHook, act } from '@testing-library/react';
import { useUrlState } from './useUrlState';
import { buildUiUrlStateSearch, readUiUrlState } from '../lib/urlState';

function setSearch(search: string) {
  const url = `${window.location.pathname}${search}`;
  window.history.replaceState(null, '', url);
}

beforeEach(() => {
  // Reset to a clean root URL before each test.
  window.history.replaceState(null, '', '/');
});

describe('useUrlState', () => {
  it('reads initial state from location.search', () => {
    setSearch('?page=settings');
    const { result } = renderHook(() => useUrlState());
    expect(result.current.urlState.page).toBe('settings');
  });

  it('re-reads state on popstate', () => {
    const { result } = renderHook(() => useUrlState());
    act(() => {
      window.history.pushState(null, '', '/?page=help');
      window.dispatchEvent(new PopStateEvent('popstate'));
    });
    expect(result.current.urlState.page).toBe('help');
  });

  it('pushUrlState pushes a new history entry for a non-search change', () => {
    const { result } = renderHook(() => useUrlState());
    const pushSpy = vi.spyOn(window.history, 'pushState');
    const next = { ...readUiUrlState(), page: 'pieces' as const };
    act(() => result.current.pushUrlState(next));
    expect(pushSpy).toHaveBeenCalledTimes(1);
    expect(window.location.search).toContain('page=pieces');
    pushSpy.mockRestore();
  });

  it('pushUrlState replaces (no new entry) when only the search field changes', () => {
    // Baseline must be the canonical serialized form so the internal
    // urlSearchRef matches buildUiUrlStateSearch output (page=help is valid).
    setSearch('?page=help');
    const { result } = renderHook(() => useUrlState());
    const pushSpy = vi.spyOn(window.history, 'pushState');
    const replaceSpy = vi.spyOn(window.history, 'replaceState');
    const next = { ...readUiUrlState(), search: 'hello' };
    act(() => result.current.pushUrlState(next));
    expect(pushSpy).not.toHaveBeenCalled();
    expect(replaceSpy).toHaveBeenCalled();
    pushSpy.mockRestore();
    replaceSpy.mockRestore();
  });

  it('pushUrlState no-ops when the search string is unchanged', () => {
    // Start from a known canonical URL so urlSearchRef matches exactly.
    setSearch('?page=help');
    const { result } = renderHook(() => useUrlState());
    const current = readUiUrlState();
    // Make the URL already match what we are about to push.
    const search = buildUiUrlStateSearch(current);
    setSearch(search ? `?${search}` : '');
    const pushSpy = vi.spyOn(window.history, 'pushState');
    const replaceSpy = vi.spyOn(window.history, 'replaceState');
    act(() => result.current.pushUrlState(current));
    expect(pushSpy).not.toHaveBeenCalled();
    expect(replaceSpy).not.toHaveBeenCalled();
    pushSpy.mockRestore();
    replaceSpy.mockRestore();
  });

  it('replaceUrlSearch replaces history and syncs state', () => {
    const { result } = renderHook(() => useUrlState());
    const replaceSpy = vi.spyOn(window.history, 'replaceState');
    act(() => result.current.replaceUrlSearch('?page=usage'));
    expect(replaceSpy).toHaveBeenCalled();
    expect(window.location.search).toContain('page=usage');
    expect(result.current.urlState.page).toBe('usage');
    replaceSpy.mockRestore();
  });
});
