/**
 * Render-path coverage for the 9 model files. Each existing per-model test
 * either checks that the export exists (`xxx.test.tsx`) or proves
 * imperative-vs-hook parity (`xxx-prefetch.test.tsx`). Neither exercises:
 *
 *   1. The `<XxxModel>` render-props component (the wrapper around `useXxx`).
 *   2. The `sourcesId ?? []` branch of `useXxx` — every existing test passes
 *      `sourceId` (single source), never `sourcesId` (multi-source array).
 *
 * Each model has identical surface for these branches, so we cover them all
 * here rather than touching 9 separate files.
 */
import { describe, it, expect, beforeEach, vi } from 'vitest'
import { renderHook, render, waitFor } from '@testing-library/react'
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'

import { useSourceStore } from '../providers/sources/store'
import type { QuerySource, Source } from '../providers/sources/types'

import {
  useAggregations,
  AggregationsModel,
  getAggregations,
} from './aggregations'
import { useCategories, CategoriesModel } from './categories'
import { useFeatures, FeaturesModel } from './features'
import { useFormula, FormulaModel } from './formula'
import { useHistogram, HistogramModel } from './histogram'
import { useRange, RangeModel } from './range'
import { useScatter, ScatterModel } from './scatter'
import { useSpread, SpreadModel } from './spread'
import { useTable, TableModel } from './table'
import { useTimeSeries, TimeSeriesModel } from './time-series'

function makeQueryClient() {
  return new QueryClient({
    defaultOptions: { queries: { retry: false, staleTime: Infinity } },
  })
}

function setupSource<TWidgetMethodName extends string>(
  id: string,
  methodName: TWidgetMethodName,
  resolved: unknown,
) {
  const widgetSpy = vi.fn().mockResolvedValue(resolved)
  const sourceConfig: Omit<Source<QuerySource>, 'id'> = {
    type: 'query',
    data: 'SELECT 1',
    connectionName: 'conn',
    apiBaseUrl: 'http://api',
  }
  useSourceStore.getState().setSource<QuerySource>(id, sourceConfig)
  const source = useSourceStore.getState().sources[id]
  if (source?.widgets) {
    ;(source.widgets as unknown as Record<string, unknown>)[methodName] =
      widgetSpy
  }
  return widgetSpy
}

function makeWrapper(client: QueryClient) {
  function Wrapper({ children }: { children: React.ReactNode }) {
    return <QueryClientProvider client={client}>{children}</QueryClientProvider>
  }
  return Wrapper
}

beforeEach(() => {
  useSourceStore.setState({ sources: {}, filters: {} })
})

describe('XxxModel render-props components render their children', () => {
  it('AggregationsModel renders children with hook result', async () => {
    const queryClient = makeQueryClient()
    const expected = { total_count: 1 }
    setupSource('agg-rp', 'getAggregations', expected)

    const childSpy = vi.fn(() => <span data-testid='agg-result' />)

    const { rerender } = render(
      <QueryClientProvider client={queryClient}>
        <AggregationsModel
          accessToken='tok'
          sourceId='agg-rp'
          widgetProps={{
            aggregations: [
              { column: 'id', operation: 'count', alias: 'total_count' },
            ],
          }}
        >
          {childSpy}
        </AggregationsModel>
      </QueryClientProvider>,
    )

    await waitFor(() => {
      expect(childSpy).toHaveBeenCalled()
    })
    rerender(<div />) // force unmount cleanup
  })

  it('CategoriesModel renders children', async () => {
    const queryClient = makeQueryClient()
    setupSource('cat-rp', 'getCategories', [{ name: 'A', value: 1 }])
    const childSpy = vi.fn(() => <span />)
    render(
      <QueryClientProvider client={queryClient}>
        <CategoriesModel
          accessToken='tok'
          sourceId='cat-rp'
          widgetProps={{ column: 'category', operation: 'count' }}
        >
          {childSpy}
        </CategoriesModel>
      </QueryClientProvider>,
    )
    await waitFor(() => expect(childSpy).toHaveBeenCalled())
  })

  it('FeaturesModel renders children', async () => {
    const queryClient = makeQueryClient()
    setupSource('feat-rp', 'getFeatures', { rows: [] })
    const childSpy = vi.fn(() => <span />)
    render(
      <QueryClientProvider client={queryClient}>
        <FeaturesModel
          accessToken='tok'
          sourceId='feat-rp'
          widgetProps={{
            columns: ['col1'],
            featureIds: ['f1'],
            dataType: 'polygons',
          }}
        >
          {childSpy}
        </FeaturesModel>
      </QueryClientProvider>,
    )
    await waitFor(() => expect(childSpy).toHaveBeenCalled())
  })

  it('FormulaModel renders children', async () => {
    const queryClient = makeQueryClient()
    setupSource('fm-rp', 'getFormula', { value: 42 })
    const childSpy = vi.fn(() => <span />)
    render(
      <QueryClientProvider client={queryClient}>
        <FormulaModel
          accessToken='tok'
          sourceId='fm-rp'
          widgetProps={{ column: 'pop', operation: 'sum' }}
        >
          {childSpy}
        </FormulaModel>
      </QueryClientProvider>,
    )
    await waitFor(() => expect(childSpy).toHaveBeenCalled())
  })

  it('HistogramModel renders children', async () => {
    const queryClient = makeQueryClient()
    setupSource('hist-rp', 'getHistogram', [0, 1, 2])
    const childSpy = vi.fn(() => <span />)
    render(
      <QueryClientProvider client={queryClient}>
        <HistogramModel
          accessToken='tok'
          sourceId='hist-rp'
          widgetProps={{ column: 'pop', ticks: [10, 20], operation: 'count' }}
        >
          {childSpy}
        </HistogramModel>
      </QueryClientProvider>,
    )
    await waitFor(() => expect(childSpy).toHaveBeenCalled())
  })

  it('RangeModel renders children', async () => {
    const queryClient = makeQueryClient()
    setupSource('range-rp', 'getRange', { min: 0, max: 1 })
    const childSpy = vi.fn(() => <span />)
    render(
      <QueryClientProvider client={queryClient}>
        <RangeModel
          accessToken='tok'
          sourceId='range-rp'
          widgetProps={{ column: 'pop' }}
        >
          {childSpy}
        </RangeModel>
      </QueryClientProvider>,
    )
    await waitFor(() => expect(childSpy).toHaveBeenCalled())
  })

  it('ScatterModel renders children', async () => {
    const queryClient = makeQueryClient()
    setupSource('scat-rp', 'getScatter', [[1, 2]])
    const childSpy = vi.fn(() => <span />)
    render(
      <QueryClientProvider client={queryClient}>
        <ScatterModel
          accessToken='tok'
          sourceId='scat-rp'
          widgetProps={{
            xColumn: 'x',
            yColumn: 'y',
            xAxisColumn: 'x',
            yAxisColumn: 'y',
          }}
        >
          {childSpy}
        </ScatterModel>
      </QueryClientProvider>,
    )
    await waitFor(() => expect(childSpy).toHaveBeenCalled())
  })

  it('SpreadModel renders children', async () => {
    const queryClient = makeQueryClient()
    setupSource('spread-rp', 'getSpread', [0, 1])
    const childSpy = vi.fn(() => <span />)
    render(
      <QueryClientProvider client={queryClient}>
        <SpreadModel
          accessToken='tok'
          sourceId='spread-rp'
          widgetProps={{ column: 'pop', operation: 'sum' }}
        >
          {childSpy}
        </SpreadModel>
      </QueryClientProvider>,
    )
    await waitFor(() => expect(childSpy).toHaveBeenCalled())
  })

  it('TableModel renders children', async () => {
    const queryClient = makeQueryClient()
    setupSource('table-rp', 'getTable', { rows: [], totalCount: 0 })
    const childSpy = vi.fn(() => <span />)
    render(
      <QueryClientProvider client={queryClient}>
        <TableModel
          accessToken='tok'
          sourceId='table-rp'
          widgetProps={{ columns: ['col1'], limit: 10 }}
        >
          {childSpy}
        </TableModel>
      </QueryClientProvider>,
    )
    await waitFor(() => expect(childSpy).toHaveBeenCalled())
  })

  it('TimeSeriesModel renders children', async () => {
    const queryClient = makeQueryClient()
    setupSource('ts-rp', 'getTimeSeries', { rows: [], categories: [] })
    const childSpy = vi.fn(() => <span />)
    render(
      <QueryClientProvider client={queryClient}>
        <TimeSeriesModel
          accessToken='tok'
          sourceId='ts-rp'
          widgetProps={{
            column: 'created_at',
            stepSize: 'day',
            operation: 'count',
          }}
        >
          {childSpy}
        </TimeSeriesModel>
      </QueryClientProvider>,
    )
    await waitFor(() => expect(childSpy).toHaveBeenCalled())
  })
})

describe('useXxx hooks accept sourcesId (multi-source path)', () => {
  it('useAggregations with sourcesId returns array result', async () => {
    const queryClient = makeQueryClient()
    setupSource('agg-multi-1', 'getAggregations', { v: 1 })
    setupSource('agg-multi-2', 'getAggregations', { v: 2 })

    const { result } = renderHook(
      () =>
        useAggregations({
          accessToken: 'tok',
          sourcesId: ['agg-multi-1', 'agg-multi-2'],
          widgetProps: {
            aggregations: [
              { column: 'id', operation: 'count', alias: 'count' },
            ],
          },
        }),
      { wrapper: makeWrapper(queryClient) },
    )

    await waitFor(() => {
      expect(result.current.isLoading).toBe(false)
    })
  })

  it('useCategories with sourcesId', async () => {
    const queryClient = makeQueryClient()
    setupSource('cat-multi-1', 'getCategories', [])
    const { result } = renderHook(
      () =>
        useCategories({
          accessToken: 'tok',
          sourcesId: ['cat-multi-1'],
          widgetProps: { column: 'c', operation: 'count' },
        }),
      { wrapper: makeWrapper(queryClient) },
    )
    await waitFor(() => expect(result.current.isLoading).toBe(false))
  })

  it('useFormula with sourcesId', async () => {
    const queryClient = makeQueryClient()
    setupSource('fm-multi', 'getFormula', { value: 1 })
    const { result } = renderHook(
      () =>
        useFormula({
          accessToken: 'tok',
          sourcesId: ['fm-multi'],
          widgetProps: { column: 'pop', operation: 'sum' },
        }),
      { wrapper: makeWrapper(queryClient) },
    )
    await waitFor(() => expect(result.current.isLoading).toBe(false))
  })

  it('useHistogram with sourcesId', async () => {
    const queryClient = makeQueryClient()
    setupSource('hist-multi', 'getHistogram', [])
    const { result } = renderHook(
      () =>
        useHistogram({
          accessToken: 'tok',
          sourcesId: ['hist-multi'],
          widgetProps: { column: 'pop', ticks: [10], operation: 'count' },
        }),
      { wrapper: makeWrapper(queryClient) },
    )
    await waitFor(() => expect(result.current.isLoading).toBe(false))
  })

  it('useRange with sourcesId', async () => {
    const queryClient = makeQueryClient()
    setupSource('range-multi', 'getRange', { min: 0, max: 1 })
    const { result } = renderHook(
      () =>
        useRange({
          accessToken: 'tok',
          sourcesId: ['range-multi'],
          widgetProps: { column: 'pop' },
        }),
      { wrapper: makeWrapper(queryClient) },
    )
    await waitFor(() => expect(result.current.isLoading).toBe(false))
  })

  it('useScatter with sourcesId', async () => {
    const queryClient = makeQueryClient()
    setupSource('scat-multi', 'getScatter', [])
    const { result } = renderHook(
      () =>
        useScatter({
          accessToken: 'tok',
          sourcesId: ['scat-multi'],
          widgetProps: {
            xColumn: 'x',
            yColumn: 'y',
            xAxisColumn: 'x',
            yAxisColumn: 'y',
          },
        }),
      { wrapper: makeWrapper(queryClient) },
    )
    await waitFor(() => expect(result.current.isLoading).toBe(false))
  })

  it('useSpread with sourcesId', async () => {
    const queryClient = makeQueryClient()
    setupSource('spread-multi', 'getSpread', [])
    const { result } = renderHook(
      () =>
        useSpread({
          accessToken: 'tok',
          sourcesId: ['spread-multi'],
          widgetProps: { column: 'pop', operation: 'sum' },
        }),
      { wrapper: makeWrapper(queryClient) },
    )
    await waitFor(() => expect(result.current.isLoading).toBe(false))
  })

  it('useTable with sourcesId', async () => {
    const queryClient = makeQueryClient()
    setupSource('table-multi', 'getTable', { rows: [], totalCount: 0 })
    const { result } = renderHook(
      () =>
        useTable({
          accessToken: 'tok',
          sourcesId: ['table-multi'],
          widgetProps: { columns: ['c'], limit: 1 },
        }),
      { wrapper: makeWrapper(queryClient) },
    )
    await waitFor(() => expect(result.current.isLoading).toBe(false))
  })

  it('useTimeSeries with sourcesId', async () => {
    const queryClient = makeQueryClient()
    setupSource('ts-multi', 'getTimeSeries', { rows: [], categories: [] })
    const { result } = renderHook(
      () =>
        useTimeSeries({
          accessToken: 'tok',
          sourcesId: ['ts-multi'],
          widgetProps: {
            column: 'created_at',
            stepSize: 'day',
            operation: 'count',
          },
        }),
      { wrapper: makeWrapper(queryClient) },
    )
    await waitFor(() => expect(result.current.isLoading).toBe(false))
  })

  it('useFeatures with sourcesId', async () => {
    const queryClient = makeQueryClient()
    setupSource('feat-multi', 'getFeatures', { rows: [] })
    const { result } = renderHook(
      () =>
        useFeatures({
          accessToken: 'tok',
          sourcesId: ['feat-multi'],
          widgetProps: {
            columns: ['c'],
            featureIds: ['f1'],
            dataType: 'polygons',
          },
        }),
      { wrapper: makeWrapper(queryClient) },
    )
    await waitFor(() => expect(result.current.isLoading).toBe(false))
  })
})

describe('aggregations: transformAggregationsToSql via real getAggregations', () => {
  // The existing aggregations.test.tsx copies the private function locally and
  // tests the copy. The real `transformAggregationsToSql` inside aggregations.tsx
  // is only invoked when a `custom` operation is present. Exercise that here.
  it('invokes transformAggregationsToSql for custom operations through getAggregations', async () => {
    const queryClient = makeQueryClient()
    const widgetSpy = vi.fn().mockResolvedValue({ unique_users: 7 })
    setupSource('agg-custom', 'getAggregations', { unique_users: 7 })
    const source = useSourceStore.getState().sources['agg-custom']
    if (source?.widgets) {
      ;(
        source.widgets as unknown as { getAggregations: typeof widgetSpy }
      ).getAggregations = widgetSpy
    }

    await queryClient.prefetchQuery(
      getAggregations({
        accessToken: 'tok',
        sourceId: 'agg-custom',
        widgetProps: {
          aggregations: [
            {
              operation: 'custom',
              aggregationExp: 'COUNT(DISTINCT user_id)',
              alias: 'unique_users',
            },
            { column: 'id', operation: 'count', alias: 'total' },
          ],
        },
      }),
    )

    expect(widgetSpy).toHaveBeenCalledOnce()
    // The adaptedAggregations should be a joined string when custom is present
    const callArg = widgetSpy.mock.calls[0]![0] as { aggregations: unknown }
    expect(typeof callArg.aggregations).toBe('string')
    expect(callArg.aggregations).toContain('AS unique_users')
  })

  it.each(['count', 'sum', 'avg', 'min', 'max'] as const)(
    'transformAggregationsToSql handles %s with a custom op to force the join path',
    async (op) => {
      const queryClient = makeQueryClient()
      const widgetSpy = vi.fn().mockResolvedValue({ res: 1 })
      setupSource(`agg-${op}`, 'getAggregations', { res: 1 })
      const source = useSourceStore.getState().sources[`agg-${op}`]
      if (source?.widgets) {
        ;(
          source.widgets as unknown as { getAggregations: typeof widgetSpy }
        ).getAggregations = widgetSpy
      }

      await queryClient.prefetchQuery(
        getAggregations({
          accessToken: 'tok',
          sourceId: `agg-${op}`,
          widgetProps: {
            aggregations: [
              {
                column: 'col1',
                operation: op,
                alias: `${op}_alias`,
              },
              // a custom op alongside, to force transformAggregationsToSql to run
              {
                operation: 'custom',
                aggregationExp: 'COUNT(DISTINCT user_id)',
                alias: 'unique_users',
              },
            ],
          },
        }),
      )

      const callArg = widgetSpy.mock.calls[0]![0] as { aggregations: string }
      expect(callArg.aggregations).toContain(`${op.toUpperCase()}(col1)`)
    },
  )

  it('keeps standard aggregations as array when no custom operation', async () => {
    const queryClient = makeQueryClient()
    const widgetSpy = vi.fn().mockResolvedValue({ total: 1 })
    setupSource('agg-std', 'getAggregations', { total: 1 })
    const source = useSourceStore.getState().sources['agg-std']
    if (source?.widgets) {
      ;(
        source.widgets as unknown as { getAggregations: typeof widgetSpy }
      ).getAggregations = widgetSpy
    }

    await queryClient.prefetchQuery(
      getAggregations({
        accessToken: 'tok',
        sourceId: 'agg-std',
        widgetProps: {
          aggregations: [{ column: 'id', operation: 'count', alias: 'total' }],
        },
      }),
    )

    const callArg = widgetSpy.mock.calls[0]![0] as { aggregations: unknown }
    expect(Array.isArray(callArg.aggregations)).toBe(true)
  })
})
