import { describe, it, expect, vi, beforeEach } from 'vitest'
import { renderHook, waitFor } from '@testing-library/react'
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
import { useStats } from './use-stats'
import type {
  Source,
  GeoJSONSource,
  AllExternalSources,
  QuerySource,
  TilesetSource,
} from '../providers/sources/types'
import type { Feature } from 'geojson'
import * as React from 'react'
import { ApiVersion, TileFormat } from '@carto/api-client'
import type { NumberStatsResponse, StringStatsResponse } from './types'

// Mock the external dependencies
vi.mock('./get-query-stats', () => ({
  getQueryStats: vi.fn().mockResolvedValue({
    type: 'NUMBER',
    attribute: 'value',
    min: 0,
    max: 100,
    avg: 50,
    sum: 5000,
  }),
}))

vi.mock('./get-table-stats', () => ({
  getTableStats: vi.fn().mockResolvedValue({
    type: 'NUMBER',
    attribute: 'value',
    min: 0,
    max: 100,
    avg: 50,
    sum: 5000,
  }),
}))

vi.mock('@carto/api-client', () => ({
  ApiVersion: {
    V3: 'v3',
  },
  TileFormat: {
    MVT: 'mvt',
  },
  vectorTilesetSource: vi.fn().mockResolvedValue({
    tilestats: {
      layers: [
        {
          attributes: [
            {
              attribute: 'value',
              type: 'number',
              min: 0,
              max: 100,
            },
          ],
        },
      ],
    },
  }),
}))

vi.mock('../models/utils/use-query-key', () => ({
  useQueryKey: vi.fn(
    ({
      queryKeyPrefix,
      sourceId,
    }: {
      queryKeyPrefix: string[]
      sourceId: string
    }) => [...queryKeyPrefix, sourceId] as string[],
  ),
}))

describe('useStats', () => {
  let queryClient: QueryClient

  beforeEach(() => {
    queryClient = new QueryClient({
      defaultOptions: {
        queries: {
          retry: false,
        },
      },
    })
  })

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

  it('should return disabled state when source is undefined', () => {
    const { result } = renderHook(
      () =>
        useStats({
          source: undefined,
          attribute: 'value',
          accessToken: 'test-token',
        }),
      { wrapper },
    )

    expect(result.current.isPending).toBe(true)
    expect(result.current.data).toBeUndefined()
  })

  it('should handle query source type', async () => {
    const source: Source<QuerySource> = {
      id: 'test-source',
      type: 'query',
      data: 'SELECT * FROM table',
      connectionName: 'test-conn',
      apiBaseUrl: 'https://api.test.com',
      apiVersion: ApiVersion.V3,
      queryParameters: {},
    }

    const { result } = renderHook(
      () =>
        useStats({
          source,
          attribute: 'value',
          accessToken: 'test-token',
        }),
      { wrapper },
    )

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

    expect(result.current.data).toBeDefined()
    expect(result.current.data?.type).toBe('number')
    expect(result.current.data?.attribute).toBe('value')
  })

  it('should handle table source type', async () => {
    const source: Source<AllExternalSources> = {
      id: 'test-source',
      type: 'table',
      data: 'my_table',
      connectionName: 'test-conn',
      apiBaseUrl: 'https://api.test.com',
      apiVersion: ApiVersion.V3,
    }

    const { result } = renderHook(
      () =>
        useStats({
          source,
          attribute: 'value',
          accessToken: 'test-token',
        }),
      { wrapper },
    )

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

    expect(result.current.data).toBeDefined()
    expect(result.current.data?.type).toBe('number')
  })

  it('should handle tileset source type', async () => {
    const source: Source<TilesetSource> = {
      id: 'test-source',
      type: 'tileset',
      data: 'my_tileset',
      connectionName: 'test-conn',
      apiBaseUrl: 'https://api.test.com',
      clientId: 'test-client',
      tileFormat: TileFormat.MVT,
      spatialDataBounds: [0, 0, 10, 10],
    }

    const { result } = renderHook(
      () =>
        useStats({
          source,
          attribute: 'value',
          accessToken: 'test-token',
        }),
      { wrapper },
    )

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

    expect(result.current.data).toBeDefined()
  })

  it('should handle geojson source with number attributes', async () => {
    const features: Feature[] = [
      {
        type: 'Feature',
        geometry: { type: 'Point', coordinates: [0, 0] },
        properties: { value: 10, name: 'A' },
      },
      {
        type: 'Feature',
        geometry: { type: 'Point', coordinates: [1, 1] },
        properties: { value: 20, name: 'B' },
      },
      {
        type: 'Feature',
        geometry: { type: 'Point', coordinates: [2, 2] },
        properties: { value: 30, name: 'C' },
      },
    ]

    const source: Source<GeoJSONSource> = {
      id: 'test-source',
      type: 'geojson',
      data: features,
    }

    const { result } = renderHook(
      () =>
        useStats({
          source,
          attribute: 'value',
          accessToken: 'test-token',
        }),
      { wrapper },
    )

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

    expect(result.current.data).toBeDefined()
    expect(result.current.data?.type).toBe('number')
    const data = result.current.data as NumberStatsResponse
    expect(data?.min).toBe(10)
    expect(data?.max).toBe(30)
    expect(data?.sum).toBe(60)
    expect(data?.avg).toBe(20)
    expect(data?.attribute).toBe('value')
  })

  it('should handle geojson source with string attributes', async () => {
    const features: Feature[] = [
      {
        type: 'Feature',
        geometry: { type: 'Point', coordinates: [0, 0] },
        properties: { category: 'A', value: 10 },
      },
      {
        type: 'Feature',
        geometry: { type: 'Point', coordinates: [1, 1] },
        properties: { category: 'B', value: 20 },
      },
      {
        type: 'Feature',
        geometry: { type: 'Point', coordinates: [2, 2] },
        properties: { category: 'A', value: 30 },
      },
    ]

    const source: Source<GeoJSONSource> = {
      id: 'test-source',
      type: 'geojson',
      data: features,
    }

    const { result } = renderHook(
      () =>
        useStats({
          source,
          attribute: 'category',
          accessToken: 'test-token',
        }),
      { wrapper },
    )

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

    expect(result.current.data).toBeDefined()
    expect(result.current.data?.type).toBe('string')
    const data = result.current.data as StringStatsResponse
    expect(data.categories).toBeDefined()
    expect(data.categories).toHaveLength(2)
    expect(data.attribute).toBe('category')
  })

  it('should calculate quantiles for number stats', async () => {
    const features: Feature[] = Array.from({ length: 100 }, (_, i) => ({
      type: 'Feature' as const,
      geometry: { type: 'Point' as const, coordinates: [i, i] },
      properties: { value: i },
    }))

    const source: Source<GeoJSONSource> = {
      id: 'test-source',
      type: 'geojson',
      data: features,
    }

    const { result } = renderHook(
      () =>
        useStats({
          source,
          attribute: 'value',
          accessToken: 'test-token',
        }),
      { wrapper },
    )

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

    const data = result.current.data as NumberStatsResponse
    expect(data.quantiles).toBeDefined()
    expect(data.quantiles[3]).toBeDefined()
    expect(data.quantiles[4]).toBeDefined()
    expect(data.quantiles[5]).toBeDefined()
  })

  it('should handle enabled prop', () => {
    const source: Source<QuerySource> = {
      id: 'test-source',
      type: 'query',
      data: 'SELECT * FROM table',
      connectionName: 'test-conn',
      apiBaseUrl: 'https://api.test.com',
    }

    const { result } = renderHook(
      () =>
        useStats({
          source,
          attribute: 'value',
          accessToken: 'test-token',
          useQueryProps: { enabled: false },
        }),
      { wrapper },
    )

    expect(result.current.isPending).toBe(true)
    expect(result.current.fetchStatus).toBe('idle')
  })

  it('should lowercase type in select', async () => {
    const features: Feature[] = [
      {
        type: 'Feature',
        geometry: { type: 'Point', coordinates: [0, 0] },
        properties: { value: 10 },
      },
    ]

    const source: Source<GeoJSONSource> = {
      id: 'test-source',
      type: 'geojson',
      data: features,
    }

    const { result } = renderHook(
      () =>
        useStats({
          source,
          attribute: 'value',
          accessToken: 'test-token',
        }),
      { wrapper },
    )

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

    // Type should be lowercased
    expect(result.current.data?.type).toBe('number')
    expect(result.current.data?.type).not.toBe('NUMBER')
  })

  it('should throw error for unsupported source type', async () => {
    const unsupportedSource = {
      id: 'unsupported-source',
      type: 'raster',
      data: 'some-raster-data',
      credentials: {
        apiBaseUrl: 'https://api.test.com',
        accessToken: 'test-token',
      },
      filters: {},
    } as never

    const { result } = renderHook(
      () =>
        useStats({
          source: unsupportedSource,
          attribute: 'value',
          accessToken: 'test-token',
        }),
      { wrapper },
    )

    await waitFor(() => {
      expect(result.current.isError).toBe(true)
    })

    expect(result.current.error?.message).toContain('Unsupported source type')
  })

  it('should handle geojson with mixed null/undefined values', async () => {
    const features: Feature[] = [
      {
        type: 'Feature',
        geometry: { type: 'Point', coordinates: [0, 0] },
        properties: { value: null },
      },
      {
        type: 'Feature',
        geometry: { type: 'Point', coordinates: [1, 1] },
        properties: {},
      },
      {
        type: 'Feature',
        geometry: { type: 'Point', coordinates: [2, 2] },
        properties: { value: 25 },
      },
    ]

    const source: Source<GeoJSONSource> = {
      id: 'test-source',
      type: 'geojson',
      data: features,
    }

    const { result } = renderHook(
      () =>
        useStats({
          source,
          attribute: 'value',
          accessToken: 'test-token',
        }),
      { wrapper },
    )

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

    expect(result.current.data?.type).toBe('number')
  })

  it('should handle geojson with all null values defaulting to string stats', async () => {
    const features: Feature[] = [
      {
        type: 'Feature',
        geometry: { type: 'Point', coordinates: [0, 0] },
        properties: { value: null },
      },
      {
        type: 'Feature',
        geometry: { type: 'Point', coordinates: [1, 1] },
        properties: {},
      },
    ]

    const source: Source<GeoJSONSource> = {
      id: 'test-source',
      type: 'geojson',
      data: features,
    }

    const { result } = renderHook(
      () =>
        useStats({
          source,
          attribute: 'value',
          accessToken: 'test-token',
        }),
      { wrapper },
    )

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

    expect(result.current.data?.type).toBe('string')
  })

  it('should handle tileset source with missing attribute error', async () => {
    const source: Source<TilesetSource> = {
      id: 'test-source',
      type: 'tileset',
      data: 'my_tileset',
      connectionName: 'test-conn',
      apiBaseUrl: 'https://api.test.com',
      clientId: 'test-client',
      tileFormat: TileFormat.MVT,
      spatialDataBounds: [0, 0, 10, 10],
    }

    // Mock vectorTilesetSource to return empty tilestats
    const { vectorTilesetSource } = await import('@carto/api-client')
    vi.mocked(vectorTilesetSource).mockResolvedValueOnce({
      tiles: [],
      tilestats: {
        layers: [
          // @ts-expect-error intentional wrong structure for testing
          {
            attributes: [], // Empty attributes array
          },
        ],
      },
    })

    const { result } = renderHook(
      () =>
        useStats({
          source,
          attribute: 'nonexistent',
          accessToken: 'test-token',
        }),
      { wrapper },
    )

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

    expect(result.current.error?.message).toContain(
      'not found in tileset attributes',
    )
  })
})
