import { describe, it, expect, beforeEach } from 'vitest'
import { FilterType, type AddFilterOptions } from '@carto/api-client'
import { useSourceStore } from '../../providers/sources/store'
import { getModelQueryOptions } from './get-model-query-options'
import type {
  AllExternalSources,
  QuerySource,
} from '../../providers/sources/types'

describe('getModelQueryOptions', () => {
  const queryKeyPrefix = ['categories']
  const accessToken = 'tok-1'
  const noopMethod = () => () => Promise.resolve(null)

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

  it('returns enabled=false when source is not registered', () => {
    const opts = getModelQueryOptions({
      queryKeyPrefix,
      getWidgetMethod: noopMethod,
      accessToken,
      sourceId: 'missing',
      widgetProps: { column: 'c' },
    })
    expect(opts.enabled).toBe(false)
    // First key segment is still the sourceId / prefix even when source is missing
    expect(opts.queryKey[0]).toBe('missing')
    expect(opts.queryKey[1]).toBe('categories')
  })

  it('returns enabled=true once the source is registered', () => {
    useSourceStore.getState().setSource<AllExternalSources>('src1', {
      type: 'query',
      data: 'SELECT 1',
      connectionName: 'conn',
      apiBaseUrl: 'http://api',
    })
    const opts = getModelQueryOptions({
      queryKeyPrefix,
      getWidgetMethod: noopMethod,
      accessToken,
      sourceId: 'src1',
      widgetProps: { column: 'population' },
    })
    expect(opts.enabled).toBe(true)
  })

  it('produces a queryKey of the documented shape: [sourceId, ...prefix, data, ?queryParameters, widgetProps]', () => {
    useSourceStore.getState().setSource<QuerySource>('src1', {
      type: 'query',
      data: 'SELECT * FROM t',
      connectionName: 'conn',
      apiBaseUrl: 'http://api',
      queryParameters: { limit: 100 },
    })

    const widgetProps = { column: 'population', operation: 'count' as const }
    const { queryKey } = getModelQueryOptions({
      queryKeyPrefix,
      getWidgetMethod: noopMethod,
      accessToken,
      sourceId: 'src1',
      widgetProps,
    })

    expect(queryKey).toEqual([
      'src1',
      'categories',
      'SELECT * FROM t',
      JSON.stringify({ limit: 100 }),
      JSON.stringify(widgetProps),
    ])
  })

  it('appends the merged filter (base + additionalFilter) to the queryKey', () => {
    useSourceStore.getState().setSource<AllExternalSources>('src1', {
      type: 'query',
      data: 'SELECT * FROM t',
      connectionName: 'conn',
      apiBaseUrl: 'http://api',
    })
    useSourceStore
      .getState()
      .setFilters('src1', [
        { column: 'category', type: FilterType.IN, values: ['A', 'B'] },
      ])
    const additionalFilter: AddFilterOptions[] = [
      { column: 'status', type: FilterType.IN, values: ['active'] },
    ]
    const widgetProps = { column: 'population', operation: 'count' as const }

    const { queryKey } = getModelQueryOptions({
      queryKeyPrefix,
      getWidgetMethod: noopMethod,
      accessToken,
      sourceId: 'src1',
      widgetProps,
      additionalFilter,
    })

    // The last segment is the merged filter — base ∪ additional.
    const mergedFilter = JSON.parse(queryKey.at(-1) as string) as Record<
      string,
      unknown
    >
    expect(mergedFilter).toEqual({
      category: { in: { values: ['A', 'B'] } },
      status: { in: { values: ['active'] } },
    })
  })

  it('resolves array column to the correct slice using sourceIndex/totalSources', async () => {
    useSourceStore.getState().setSource<AllExternalSources>('src1', {
      type: 'query',
      data: 'SELECT 1',
      connectionName: 'conn',
      apiBaseUrl: 'http://api',
    })

    let captured: unknown
    const opts = getModelQueryOptions({
      queryKeyPrefix,
      getWidgetMethod: () => (params) => {
        captured = params
        return Promise.resolve({ ok: true } as unknown as null)
      },
      accessToken,
      sourceId: 'src1',
      widgetProps: { column: ['col_a', 'col_b'], operation: 'count' as const },
      sourceIndex: 1,
      totalSources: 2,
    })

    await opts.queryFn({ signal: new AbortController().signal })
    expect(captured).toMatchObject({ column: 'col_b' })
  })

  it('resolves a per-source operation array to the slice for this source', async () => {
    useSourceStore.getState().setSource<AllExternalSources>('src1', {
      type: 'query',
      data: 'SELECT 1',
      connectionName: 'conn',
      apiBaseUrl: 'http://api',
    })

    let captured: unknown
    const opts = getModelQueryOptions({
      queryKeyPrefix,
      getWidgetMethod: () => (params) => {
        captured = params
        return Promise.resolve({ ok: true } as unknown as null)
      },
      accessToken,
      sourceId: 'src1',
      widgetProps: {
        column: ['col_a', 'col_b'],
        operation: ['sum', 'count'],
      },
      sourceIndex: 1,
      totalSources: 2,
    })

    await opts.queryFn({ signal: new AbortController().signal })
    // Heterogeneous per-source operations must not leak the raw array into the
    // request — source 1 gets the scalar 'count'.
    expect(captured).toMatchObject({ column: 'col_b', operation: 'count' })
  })

  it('does not leak a raw array for a masked (undefined) per-source prop', async () => {
    useSourceStore.getState().setSource<AllExternalSources>('src1', {
      type: 'query',
      data: 'SELECT 1',
      connectionName: 'conn',
      apiBaseUrl: 'http://api',
    })

    let captured: Record<string, unknown> = {}
    const opts = getModelQueryOptions({
      queryKeyPrefix,
      getWidgetMethod: () => (params) => {
        captured = params as Record<string, unknown>
        return Promise.resolve({ ok: true } as unknown as null)
      },
      accessToken,
      sourceId: 'src1',
      // Masked second source: spatialFilter is undefined for index 1. The raw
      // array must not survive into the request body.
      widgetProps: {
        column: 'c',
        operation: 'count' as const,
        spatialFilter: [{ kind: 'poly' }, undefined],
      },
      sourceIndex: 1,
      totalSources: 2,
    })

    await opts.queryFn({ signal: new AbortController().signal })
    expect(captured.spatialFilter).toBeUndefined()
    expect(Array.isArray(captured.spatialFilter)).toBe(false)
  })

  it('throws when an array prop length does not match totalSources', () => {
    useSourceStore.getState().setSource<AllExternalSources>('src1', {
      type: 'query',
      data: 'SELECT 1',
      connectionName: 'conn',
      apiBaseUrl: 'http://api',
    })

    expect(() =>
      getModelQueryOptions({
        queryKeyPrefix,
        getWidgetMethod: noopMethod,
        accessToken,
        sourceId: 'src1',
        widgetProps: { column: ['a', 'b', 'c'] },
        sourceIndex: 0,
        totalSources: 2,
      }),
    ).toThrow(/column array length \(3\) must match sources length \(2\)/i)
  })

  it('queryFn calls the widgetMethod with merged filters and resolved widget props', async () => {
    useSourceStore.getState().setSource<AllExternalSources>('src1', {
      type: 'query',
      data: 'SELECT * FROM t',
      connectionName: 'conn',
      apiBaseUrl: 'http://api',
    })
    useSourceStore
      .getState()
      .setFilters('src1', [
        { column: 'category', type: FilterType.IN, values: ['A'] },
      ])

    let captured: unknown
    const opts = getModelQueryOptions({
      queryKeyPrefix,
      getWidgetMethod: () => (params) => {
        captured = params
        return Promise.resolve({ ok: true } as unknown as null)
      },
      accessToken,
      sourceId: 'src1',
      widgetProps: { column: ['population'], operation: 'count' as const },
    })

    const ctrl = new AbortController()
    await opts.queryFn({ signal: ctrl.signal })

    expect(captured).toMatchObject({
      column: 'population',
      operation: 'count',
      filters: { category: { in: { values: ['A'] } } },
    })
  })

  it('queryFn rejects when source is missing', async () => {
    const opts = getModelQueryOptions({
      queryKeyPrefix,
      getWidgetMethod: noopMethod,
      accessToken,
      sourceId: 'missing',
      widgetProps: { column: 'c' },
    })
    const ctrl = new AbortController()
    await expect(opts.queryFn({ signal: ctrl.signal })).rejects.toThrow(
      /no widget model/,
    )
  })
})
