/**
 * Branch coverage for create-model.tsx focusing on:
 * - useQueryProps `enabled` propagation (truthy/falsy + default)
 * - additionalFilter branches via pickAdditionalFilter (single, multi-flat,
 *   multi-per-source, undefined, empty)
 * - queryFn error paths (rejected promise with Error / non-Error)
 *
 * Uses useFormula as the carrier because it's the simplest model wrapper —
 * the branches live in createModel.useModelHook, which all per-model hooks
 * funnel into.
 */
import { describe, it, expect, beforeEach, vi } from 'vitest'
import { renderHook, waitFor } from '@testing-library/react'
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
import { FilterType } from '@carto/api-client'

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

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

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

function setupSource(id: string, 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 { getFormula: typeof widgetSpy }
    ).getFormula = widgetSpy
  }
  return widgetSpy
}

function setupRejectingSource(id: string, reason: unknown) {
  const widgetSpy = vi.fn().mockRejectedValue(reason)
  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 { getFormula: typeof widgetSpy }
    ).getFormula = widgetSpy
  }
  return widgetSpy
}

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

describe('createModel.useModelHook — useQueryProps.enabled propagation', () => {
  it('does not fire the query when useQueryProps.enabled is false', async () => {
    const queryClient = makeQueryClient()
    const widgetSpy = setupSource('cm-disabled', { value: 1 })

    renderHook(
      () =>
        useFormula({
          accessToken: 'tok',
          sourceId: 'cm-disabled',
          widgetProps: { column: 'pop', operation: 'sum' },
          useQueryProps: { enabled: false },
        }),
      { wrapper: makeWrapper(queryClient) },
    )

    // Briefly wait — query should never fire
    await new Promise((r) => setTimeout(r, 50))
    expect(widgetSpy).not.toHaveBeenCalled()
  })

  it('fires the query when useQueryProps.enabled is omitted (default true)', async () => {
    const queryClient = makeQueryClient()
    const widgetSpy = setupSource('cm-enabled-default', { value: 1 })

    const { result } = renderHook(
      () =>
        useFormula({
          accessToken: 'tok',
          sourceId: 'cm-enabled-default',
          widgetProps: { column: 'pop', operation: 'sum' },
        }),
      { wrapper: makeWrapper(queryClient) },
    )

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

  it('forwards additional useQueryProps (e.g. staleTime) without breaking', async () => {
    const queryClient = makeQueryClient()
    setupSource('cm-extra-props', { value: 9 })

    const { result } = renderHook(
      () =>
        useFormula({
          accessToken: 'tok',
          sourceId: 'cm-extra-props',
          widgetProps: { column: 'pop', operation: 'sum' },
          useQueryProps: { enabled: true, refetchOnMount: false },
        }),
      { wrapper: makeWrapper(queryClient) },
    )

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

describe('createModel.useModelHook — additionalFilter / pickAdditionalFilter', () => {
  const filter = [{ column: 'a', type: FilterType.IN, values: ['x'] }] as const

  it('threads a single-source additionalFilter array', async () => {
    const queryClient = makeQueryClient()
    const widgetSpy = setupSource('cm-add-single', { value: 1 })

    renderHook(
      () =>
        useFormula({
          accessToken: 'tok',
          sourceId: 'cm-add-single',
          widgetProps: { column: 'pop', operation: 'sum' },
          additionalFilter: filter as never,
        }),
      { wrapper: makeWrapper(queryClient) },
    )

    await waitFor(() => expect(widgetSpy).toHaveBeenCalled())
  })

  it('threads a multi-source additionalFilter that is one flat array (applies to all)', async () => {
    const queryClient = makeQueryClient()
    setupSource('cm-add-multi-flat-1', { value: 1 })
    setupSource('cm-add-multi-flat-2', { value: 2 })

    const { result } = renderHook(
      () =>
        useFormula({
          accessToken: 'tok',
          sourcesId: ['cm-add-multi-flat-1', 'cm-add-multi-flat-2'],
          widgetProps: { column: 'pop', operation: 'sum' },
          additionalFilter: filter as never,
        }),
      { wrapper: makeWrapper(queryClient) },
    )

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

  it('threads a multi-source additionalFilter that is a 2D array (per source)', async () => {
    const queryClient = makeQueryClient()
    setupSource('cm-add-multi-2d-1', { value: 1 })
    setupSource('cm-add-multi-2d-2', { value: 2 })

    const { result } = renderHook(
      () =>
        useFormula({
          accessToken: 'tok',
          sourcesId: ['cm-add-multi-2d-1', 'cm-add-multi-2d-2'],
          widgetProps: { column: 'pop', operation: 'sum' },
          additionalFilter: [
            [{ column: 'a', type: FilterType.IN, values: ['x'] }],
            [{ column: 'b', type: FilterType.IN, values: ['y'] }],
          ] as never,
        }),
      { wrapper: makeWrapper(queryClient) },
    )

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

  it('handles an empty additionalFilter array (returns undefined branch)', async () => {
    const queryClient = makeQueryClient()
    setupSource('cm-add-empty', { value: 1 })

    const { result } = renderHook(
      () =>
        useFormula({
          accessToken: 'tok',
          sourceId: 'cm-add-empty',
          widgetProps: { column: 'pop', operation: 'sum' },
          additionalFilter: [] as never,
        }),
      { wrapper: makeWrapper(queryClient) },
    )

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

describe('createModel.useModelHook — queryFn error paths', () => {
  it('rethrows an Error instance from the widget method', async () => {
    const queryClient = makeQueryClient()
    setupRejectingSource('cm-err-instance', new Error('boom'))

    const { result } = renderHook(
      () =>
        useFormula({
          accessToken: 'tok',
          sourceId: 'cm-err-instance',
          widgetProps: { column: 'pop', operation: 'sum' },
        }),
      { wrapper: makeWrapper(queryClient) },
    )

    await waitFor(() => {
      expect(result.current.error).toBeInstanceOf(Error)
    })
    expect(result.current.error?.message).toContain('boom')
  })

  it('wraps a non-Error rejection in a new Error (instanceof Error false branch)', async () => {
    const queryClient = makeQueryClient()
    setupRejectingSource('cm-err-string', 'string-rejection')

    const { result } = renderHook(
      () =>
        useFormula({
          accessToken: 'tok',
          sourceId: 'cm-err-string',
          widgetProps: { column: 'pop', operation: 'sum' },
        }),
      { wrapper: makeWrapper(queryClient) },
    )

    await waitFor(() => {
      expect(result.current.error).toBeInstanceOf(Error)
    })
    expect(result.current.error?.message).toContain('string-rejection')
    expect(result.current.error?.message).toContain('cm-err-string')
  })

  it('returns array of data when multi-source and all sources succeed', async () => {
    const queryClient = makeQueryClient()
    setupSource('cm-multi-ok-1', { value: 1 })
    setupSource('cm-multi-ok-2', { value: 2 })

    const { result } = renderHook(
      () =>
        useFormula({
          accessToken: 'tok',
          sourcesId: ['cm-multi-ok-1', 'cm-multi-ok-2'],
          widgetProps: { column: 'pop', operation: 'sum' },
        }),
      { wrapper: makeWrapper(queryClient) },
    )

    await waitFor(() => expect(result.current.isLoading).toBe(false))
    // multi-source returns an array → `isSingleSource ? data.at(0) : data`
    // takes the `: data` branch
    expect(Array.isArray(result.current.data)).toBe(true)
  })
})
