import { describe, it, expect, beforeEach, vi } from 'vitest'
import { renderHook, waitFor } from '@testing-library/react'
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
import { useSourceStore } from '../../providers/sources/store'
import type { AllExternalSources } from '../../providers/sources/types'
import { useFormula } from '../formula'

/**
 * Regression: after a runtime `setSource(id, newConfig)` (e.g. a registry
 * pattern that rewrites a source's `data` SQL when an external filter store
 * changes), widget hooks must refetch with the new query key.
 *
 * Two failure modes this test guards against:
 *
 * 1. A previous version wrapped the per-render `queryKey` / `queryFn`
 *    derivation in a `useMemo` listing `subscribedSources` /
 *    `subscribedFilters` in the deps and "touching" them via `void`. Under
 *    `babel-plugin-react-compiler` (used by this package's build), the
 *    compiler statically analyzed the callback body, saw those values were
 *    not observably read, and elided them — so a `setSource` update no longer
 *    triggered re-derivation.
 *
 * 2. The current implementation reads the source/filter slice via
 *    `useSourceSlices` in each per-model `useXxx` wrapper (see `formula.tsx`
 *    etc.) and threads the result into `createModel`'s imperative composition
 *    as `sourceState`. If a future refactor accidentally drops the
 *    subscription or stops feeding it into the query key, the hook would also
 *    stop reacting to `setSource`. This test pins the contract end-to-end via
 *    the public `useFormula` API.
 */
describe('createModel — setSource() reactivity', () => {
  beforeEach(() => {
    useSourceStore.setState({ sources: {}, filters: {} })
  })

  it('useFormula refetches after setSource() rewrites the source data', async () => {
    const queryClient = new QueryClient({
      defaultOptions: { queries: { retry: false, staleTime: Infinity } },
    })

    const formulaSpy = vi.fn(() => {
      // Echo back something that depends on the current store snapshot,
      // proving the queryFn ran against the *latest* source.
      const data = useSourceStore.getState().sources.demo as
        | { data?: string }
        | undefined
      return Promise.resolve({ value: data?.data ?? 'no-source' })
    })

    const setSource = useSourceStore.getState().setSource
    setSource<AllExternalSources>('demo', {
      type: 'query',
      data: 'SELECT 1 -- initial',
      connectionName: 'conn',
      apiBaseUrl: 'http://api',
    })
    const initial = useSourceStore.getState().sources.demo
    if (initial?.widgets) {
      ;(
        initial.widgets as unknown as { getFormula: typeof formulaSpy }
      ).getFormula = formulaSpy
    }

    const wrapper = ({ children }: { children: React.ReactNode }) => (
      <QueryClientProvider client={queryClient}>{children}</QueryClientProvider>
    )

    const { result } = renderHook(
      () =>
        useFormula({
          accessToken: 'tok',
          sourceId: 'demo',
          widgetProps: { column: 'value', operation: 'sum' },
        }),
      { wrapper },
    )

    await waitFor(() => {
      expect(result.current.data).toEqual({ value: 'SELECT 1 -- initial' })
    })
    expect(formulaSpy).toHaveBeenCalledTimes(1)

    // Re-register the same id with a fresh source (different `data`). Mirrors
    // the consumer pattern of a "source registry" effect that recomputes the
    // SQL whenever an external filter store changes.
    setSource<AllExternalSources>('demo', {
      type: 'query',
      data: 'SELECT 1 -- after',
      connectionName: 'conn',
      apiBaseUrl: 'http://api',
    })
    const next = useSourceStore.getState().sources.demo
    if (next?.widgets) {
      ;(
        next.widgets as unknown as { getFormula: typeof formulaSpy }
      ).getFormula = formulaSpy
    }

    await waitFor(() => {
      expect(result.current.data).toEqual({ value: 'SELECT 1 -- after' })
    })
    expect(formulaSpy).toHaveBeenCalledTimes(2)
  })

  it('useFormula queryKey changes when source data changes', async () => {
    const queryClient = new QueryClient({
      defaultOptions: { queries: { retry: false, staleTime: Infinity } },
    })

    const formulaSpy = vi.fn().mockResolvedValue({ value: 1 })

    const setSource = useSourceStore.getState().setSource
    setSource<AllExternalSources>('demo', {
      type: 'query',
      data: 'SELECT 1',
      connectionName: 'conn',
      apiBaseUrl: 'http://api',
    })
    const initial = useSourceStore.getState().sources.demo
    if (initial?.widgets) {
      ;(
        initial.widgets as unknown as { getFormula: typeof formulaSpy }
      ).getFormula = formulaSpy
    }

    const wrapper = ({ children }: { children: React.ReactNode }) => (
      <QueryClientProvider client={queryClient}>{children}</QueryClientProvider>
    )

    const { result, rerender } = renderHook(
      () =>
        useFormula({
          accessToken: 'tok',
          sourceId: 'demo',
          widgetProps: { column: 'value', operation: 'sum' },
        }),
      { wrapper },
    )

    await waitFor(() => expect(result.current.isSuccess).toBe(true))

    const initialKeys = queryClient
      .getQueryCache()
      .getAll()
      .map((q) => q.queryKey)
    expect(initialKeys).toHaveLength(1)

    setSource<AllExternalSources>('demo', {
      type: 'query',
      data: 'SELECT 2',
      connectionName: 'conn',
      apiBaseUrl: 'http://api',
    })
    const next = useSourceStore.getState().sources.demo
    if (next?.widgets) {
      ;(
        next.widgets as unknown as { getFormula: typeof formulaSpy }
      ).getFormula = formulaSpy
    }
    rerender()

    await waitFor(() => {
      const keys = queryClient
        .getQueryCache()
        .getAll()
        .map((q) => q.queryKey)
      expect(keys.length).toBeGreaterThan(1)
    })
  })
})
