/**
 * Shared component-test render helpers.
 *
 * Most components use @tanstack/react-query, so they must be rendered inside a
 * QueryClientProvider. `renderWithProviders` wraps the UI in a fresh, retry-off
 * QueryClient (deterministic: failed queries surface immediately instead of
 * retrying). Pair with `// @vitest-environment jsdom` + `import './dom-setup'`.
 */
import { render, type RenderOptions } from '@testing-library/react';
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import { useState, useCallback, type ReactElement, type ReactNode } from 'react';

export function makeQueryClient(): QueryClient {
  return new QueryClient({
    defaultOptions: {
      queries: { retry: false, gcTime: 0, staleTime: 0 },
      mutations: { retry: false },
    },
  });
}

export function renderWithProviders(
  ui: ReactElement,
  options?: Omit<RenderOptions, 'wrapper'> & { queryClient?: QueryClient },
) {
  const queryClient = options?.queryClient ?? makeQueryClient();
  function Wrapper({ children }: { children: ReactNode }) {
    return <QueryClientProvider client={queryClient}>{children}</QueryClientProvider>;
  }
  return { queryClient, ...render(ui, { wrapper: Wrapper, ...options }) };
}

/** Immutably set a value at a dot-separated path (mirrors ConfigForm.setNestedValue). */
function setNestedValue(obj: any, path: string, value: any): any {
  const keys = path.split('.');
  if (keys.length === 1) return { ...obj, [keys[0]]: value };
  const [first, ...rest] = keys;
  return { ...obj, [first]: setNestedValue(obj[first] ?? {}, rest.join('.'), value) };
}

/**
 * Render a settings `*Form` with a REAL stateful config, the way ConfigForm
 * drives it in production: onChange writes back into `config` via a dot-path
 * setter, so controlled inputs actually update as the user types.
 *
 * Without this, a static `config` prop leaves controlled inputs frozen and
 * multi-character typing fires onChange with stale/garbled values. Pass the
 * form's component plus an initial config; the returned `onChange` spy still
 * records every call for assertions, and `getConfig()` returns the live draft.
 */
export function renderStatefulForm(
  Form: (props: { config: any; onChange: (path: string, value: any) => void; overriddenByEnv: Record<string, boolean> }) => ReactElement,
  initialConfig: any,
  options?: { overriddenByEnv?: Record<string, boolean>; onChangeSpy?: (path: string, value: any) => void },
) {
  let latest = initialConfig;
  function Host() {
    const [config, setConfig] = useState<any>(initialConfig);
    latest = config;
    const onChange = useCallback((path: string, value: any) => {
      options?.onChangeSpy?.(path, value);
      setConfig((prev: any) => setNestedValue(prev, path, value));
    }, []);
    return <Form config={config} onChange={onChange} overriddenByEnv={options?.overriddenByEnv ?? {}} />;
  }
  const utils = renderWithProviders(<Host />);
  return { ...utils, getConfig: () => latest };
}
