import { useEffect } from 'react'
import { beforeEach, describe, expect, test, vi } from 'vitest'
import { act, waitFor } from '@testing-library/react'
import { GeoJsonLayer } from '@deck.gl/layers'
import { MaskExtension } from '@deck.gl/extensions'
import type { FeatureCollection, Polygon } from 'geojson'
import { LassoTools } from '../../index'
import { render } from '../../../tests/providers'
import { MAP_ID, MASK_ID } from '../../../tests/config'
import userEvent from '@testing-library/user-event'
import { useLassoToolsState } from './provider/provider'
import { useLayerStore } from '../../providers/layers/store'
import { useMapStore } from '../../providers/map/provider'
import {
  DEFAULT_LASSO_TOOLS_PROPS,
  MAP_LASSO_TOOLS_LAYER_ID,
  MAP_MASK_LAYER_ID,
} from './const'
import { EditType } from '../const'

describe('LassoTools', () => {
  beforeEach(() => {
    // Layer store is global; clear stale entries between tests so extension
    // dedup-merge logic cannot leak MaskExtension across cases.
    useLayerStore.setState({ layers: {} })
  })

  test('should change the enabled state', async () => {
    const { getByRole, getByText } = render(<LassoToolsWrapper />)

    const button = getByRole('button', { name: 'change enabled' })
    await userEvent.click(button)

    const enabledText = getByText('enabled: false')
    expect(enabledText).toBeTruthy()
  })

  test('should change the mode', async () => {
    const { getByRole, getByText } = render(<LassoToolsWrapper />)

    const button = getByRole('button', { name: 'change mode' })
    await userEvent.click(button)

    const modeText = getByText('mode: circle')
    expect(modeText).toBeTruthy()
  })

  test('should change visibility of a mask', async () => {
    const { getByRole, getByText } = render(<LassoToolsWrapper />)

    const button = getByRole('button', { name: 'change mask visibility' })
    await userEvent.click(button)

    const maskVisibleText = getByText('mask visible: false')
    expect(maskVisibleText).toBeTruthy()
  })

  test('should render children', () => {
    const { getByText } = render(
      <LassoTools mapId={MAP_ID}>
        <div>Test Child</div>
      </LassoTools>,
    )

    expect(getByText('Test Child')).toBeTruthy()
  })

  test('should set controller when enabled', async () => {
    const TestComponent = () => {
      const setEnabled = useLassoToolsState((state) => state.setEnabled)
      const enabled = useLassoToolsState((state) => state.data[MAP_ID]?.enabled)

      return (
        <LassoTools mapId={MAP_ID}>
          <button onClick={() => setEnabled(MAP_ID, true)}>Enable</button>
          <div>Enabled: {String(enabled)}</div>
        </LassoTools>
      )
    }

    const { getByRole, getByText } = render(<TestComponent />)
    const button = getByRole('button', { name: 'Enable' })

    await userEvent.click(button)

    // Verify enabled state changed
    expect(getByText('Enabled: true')).toBeTruthy()
  })

  test('should add and remove layers on mount/unmount', () => {
    const { unmount, container } = render(
      <LassoTools mapId={MAP_ID}>
        <div>Content</div>
      </LassoTools>,
    )

    // Component mounted successfully
    expect(container).toBeTruthy()

    // Unmount should cleanup
    unmount()
    expect(container.innerHTML).toBe('')
  })

  test('should handle setValue when adding values', async () => {
    const TestComponent = () => {
      const setValue = useLassoToolsState((state) => state.setValue)
      const values = useLassoToolsState((state) => state.data[MAP_ID]?.values)

      return (
        <LassoTools mapId={MAP_ID}>
          <button
            onClick={() =>
              setValue(MAP_ID, 'test-id', {
                geometry: {
                  type: 'Polygon',
                  coordinates: [
                    [
                      [0, 0],
                      [1, 0],
                      [1, 1],
                      [0, 1],
                      [0, 0],
                    ],
                  ],
                },
                properties: { label: 'Test', visible: true },
                type: 'mask',
              })
            }
          >
            Add Value
          </button>
          <div>Values count: {Object.keys(values ?? {}).length}</div>
        </LassoTools>
      )
    }

    const { getByRole, getByText } = render(<TestComponent />)
    const button = getByRole('button', { name: 'Add Value' })

    await userEvent.click(button)

    // Should have added a value
    expect(getByText(/Values count:/)).toBeTruthy()
  })

  test('should support custom defaultProps', () => {
    const customStyles = {
      colors: {
        editing: {
          border: [255, 0, 0, 255] as [number, number, number, number],
          point: [255, 0, 0, 255] as [number, number, number, number],
          fill: [255, 0, 0, 100] as [number, number, number, number],
        },
        finished: {
          border: [0, 255, 0, 255] as [number, number, number, number],
          fill: [0, 255, 0, 100] as [number, number, number, number],
        },
      },
    }

    const { container } = render(
      <LassoTools mapId={MAP_ID} defaultProps={{ styles: customStyles }}>
        <div>Content</div>
      </LassoTools>,
    )

    expect(container).toBeTruthy()
  })

  test('should support custom layerProps', () => {
    const customLayerProps = {
      mask: {
        data: {
          type: 'FeatureCollection' as const,
          features: [],
        },
        lineWidthMinPixels: 3,
      },
    }

    const { container } = render(
      <LassoTools mapId={MAP_ID} layerProps={customLayerProps}>
        <div>Content</div>
      </LassoTools>,
    )

    expect(container).toBeTruthy()
  })

  test('should handle mode changes between lasso and other modes', async () => {
    const TestComponent = () => {
      const setEnabled = useLassoToolsState((state) => state.setEnabled)
      const setModeSelected = useLassoToolsState(
        (state) => state.setModeSelected,
      )
      const modeSelected = useLassoToolsState(
        (state) => state.data[MAP_ID]?.modeSelected,
      )

      return (
        <LassoTools mapId={MAP_ID}>
          <button onClick={() => setEnabled(MAP_ID, true)}>Enable</button>
          <button onClick={() => setModeSelected(MAP_ID, 'lasso')}>
            Set Lasso
          </button>
          <button onClick={() => setModeSelected(MAP_ID, 'edit')}>
            Set Edit
          </button>
          <div>Mode: {modeSelected}</div>
        </LassoTools>
      )
    }

    const { getByRole, getByText } = render(<TestComponent />)

    const enableButton = getByRole('button', { name: 'Enable' })
    await userEvent.click(enableButton)

    const lassoButton = getByRole('button', { name: 'Set Lasso' })
    await userEvent.click(lassoButton)

    expect(getByText('Mode: lasso')).toBeTruthy()

    const editButton = getByRole('button', { name: 'Set Edit' })
    await userEvent.click(editButton)

    expect(getByText('Mode: edit')).toBeTruthy()
  })

  test('should update layers when enabled state changes', async () => {
    const setLayerSpy = vi.spyOn(useLayerStore.getState(), 'setLayer')

    const TestComponent = () => {
      const setEnabled = useLassoToolsState((state) => state.setEnabled)

      return (
        <LassoTools mapId={MAP_ID}>
          <button onClick={() => setEnabled(MAP_ID, true)}>Enable</button>
          <button onClick={() => setEnabled(MAP_ID, false)}>Disable</button>
        </LassoTools>
      )
    }

    const { getByRole } = render(<TestComponent />)

    const enableButton = getByRole('button', { name: 'Enable' })
    await userEvent.click(enableButton)

    expect(setLayerSpy).toHaveBeenCalled()

    setLayerSpy.mockClear()

    const disableButton = getByRole('button', { name: 'Disable' })
    await userEvent.click(disableButton)

    expect(setLayerSpy).toHaveBeenCalled()
  })

  test('should not inject MaskExtension into excluded layers', async () => {
    const { getByRole } = render(
      <ExcludedLayersHarness excludeLayerIds={['layer-a']} />,
    )

    await userEvent.click(getByRole('button', { name: 'Enable' }))
    await userEvent.click(getByRole('button', { name: 'Disable' }))

    const layerA = useLayerStore.getState().getLayer('layer-a')
    const layerB = useLayerStore.getState().getLayer('layer-b')

    expect(hasMaskExtension(layerB?.props.extensions)).toBe(true)
    expect(hasMaskExtension(layerA?.props.extensions)).toBe(false)
  })

  test('should inject MaskExtension into all non-excepted layers when excludeLayerIds is empty', async () => {
    const { getByRole } = render(<ExcludedLayersHarness />)

    await userEvent.click(getByRole('button', { name: 'Enable' }))
    await userEvent.click(getByRole('button', { name: 'Disable' }))

    const layerA = useLayerStore.getState().getLayer('layer-a')
    const layerB = useLayerStore.getState().getLayer('layer-b')

    expect(hasMaskExtension(layerA?.props.extensions)).toBe(true)
    expect(hasMaskExtension(layerB?.props.extensions)).toBe(true)
  })

  test('should re-include layer when excludeLayerIds prop is cleared', async () => {
    const { getByRole, rerender } = render(
      <ExcludedLayersHarness excludeLayerIds={['layer-a']} />,
    )

    await userEvent.click(getByRole('button', { name: 'Enable' }))
    await userEvent.click(getByRole('button', { name: 'Disable' }))

    expect(
      hasMaskExtension(
        useLayerStore.getState().getLayer('layer-a')?.props.extensions,
      ),
    ).toBe(false)

    rerender(<ExcludedLayersHarness excludeLayerIds={[]} />)

    await userEvent.click(getByRole('button', { name: 'Enable' }))
    await userEvent.click(getByRole('button', { name: 'Disable' }))

    expect(
      hasMaskExtension(
        useLayerStore.getState().getLayer('layer-a')?.props.extensions,
      ),
    ).toBe(true)
  })

  test('mask clips only the selected layers when maskLayerIds is set', async () => {
    const { getByRole } = render(
      <MaskLayerFilterHarness maskLayerIds={['layer-a']} />,
    )

    await userEvent.click(getByRole('button', { name: 'SetFilter' }))
    await userEvent.click(getByRole('button', { name: 'Enable' }))
    await userEvent.click(getByRole('button', { name: 'Disable' }))

    const maskId = (id: string) =>
      (useLayerStore.getState().getLayer(id)?.props as { maskId?: string })
        .maskId
    expect(maskId('layer-a')).toBe(MAP_MASK_LAYER_ID)
    expect(maskId('layer-b')).toBeUndefined()
  })

  test('mask clips all layers when maskLayerIds is null (default)', async () => {
    const { getByRole } = render(<MaskLayerFilterHarness maskLayerIds={null} />)

    await userEvent.click(getByRole('button', { name: 'Enable' }))
    await userEvent.click(getByRole('button', { name: 'Disable' }))

    const maskId = (id: string) =>
      (useLayerStore.getState().getLayer(id)?.props as { maskId?: string })
        .maskId
    expect(maskId('layer-a')).toBe(MAP_MASK_LAYER_ID)
    expect(maskId('layer-b')).toBe(MAP_MASK_LAYER_ID)
  })

  describe('onEdit commit behavior', () => {
    const movedGeometry: Polygon = {
      type: 'Polygon',
      coordinates: [
        [
          [10, 10],
          [11, 11],
          [12, 12],
          [10, 10],
        ],
      ],
    }

    type OnEdit = (event: {
      editType: string
      updatedData: FeatureCollection
    }) => void

    interface HarnessApi {
      setEnabled: (id: string, v: boolean) => void
      setModeSelected: (id: string, m: string | null) => void
      setSelectedId: (id: string, selectedId: string | null) => void
      setLocked: (locked: boolean) => void
      getEnabled: () => boolean | undefined
      getGeometry: () => unknown
      getMessages: () => unknown[] | undefined
    }

    function Harness({ apiRef }: { apiRef: { current: HarnessApi | null } }) {
      const setEnabled = useLassoToolsState((s) => s.setEnabled)
      const setModeSelected = useLassoToolsState((s) => s.setModeSelected)
      const setSelectedId = useLassoToolsState((s) => s.setSelectedId)
      const setValue = useLassoToolsState((s) => s.setValue)
      const enabled = useLassoToolsState((s) => s.data[MAP_ID]?.enabled)
      const properties = useLassoToolsState(
        (s) => s.data[MAP_ID]?.values[MASK_ID]?.properties,
      )
      const geometry = useLassoToolsState(
        (s) => s.data[MAP_ID]?.values[MASK_ID]?.geometry,
      )
      const messages = useLassoToolsState((s) => s.data[MAP_ID]?.messages)

      useEffect(() => {
        apiRef.current = {
          setEnabled,
          setModeSelected: setModeSelected as HarnessApi['setModeSelected'],
          setSelectedId,
          setLocked: (locked) =>
            setValue(MAP_ID, MASK_ID, {
              properties: { ...properties!, locked },
            }),
          getEnabled: () => enabled,
          getGeometry: () => geometry,
          getMessages: () => messages,
        }
      })
      return null
    }

    async function renderHarness() {
      const apiRef: { current: HarnessApi | null } = { current: null }
      render(
        <LassoTools mapId={MAP_ID}>
          <Harness apiRef={apiRef} />
        </LassoTools>,
      )
      await waitFor(() => expect(apiRef.current).not.toBeNull())
      let onEdit: OnEdit | undefined
      await waitFor(() => {
        const layer = useLayerStore
          .getState()
          .getLayer(MAP_LASSO_TOOLS_LAYER_ID)
        onEdit = (layer?.props as { onEdit?: OnEdit })?.onEdit
        expect(onEdit).toBeTypeOf('function')
      })
      return { api: apiRef, onEdit: onEdit! }
    }

    function makeUpdatedData(): FeatureCollection {
      return {
        type: 'FeatureCollection',
        features: [
          {
            type: 'Feature',
            id: MASK_ID,
            geometry: movedGeometry,
            properties: {
              label: 'Test mask label',
              visible: true,
              index: 0,
            },
          },
        ],
      }
    }

    function enterEditMode(api: { current: HarnessApi | null }) {
      act(() => {
        api.current!.setEnabled(MAP_ID, true)
        api.current!.setModeSelected(MAP_ID, 'edit')
      })
    }

    test('finishMovePosition commits geometry to the store and keeps edit mode active', async () => {
      const { api, onEdit } = await renderHarness()
      enterEditMode(api)

      act(() => {
        onEdit({
          editType: EditType.finishMovePosition,
          updatedData: makeUpdatedData(),
        })
      })

      expect(api.current!.getGeometry()).toEqual(movedGeometry)
      expect(api.current!.getEnabled()).toBe(true)
    })

    test('finishTranslating commits geometry to the store and keeps edit mode active', async () => {
      const { api, onEdit } = await renderHarness()
      enterEditMode(api)

      act(() => {
        onEdit({
          editType: EditType.finishTranslating,
          updatedData: makeUpdatedData(),
        })
      })

      expect(api.current!.getGeometry()).toEqual(movedGeometry)
      expect(api.current!.getEnabled()).toBe(true)
    })

    test('committing a self-intersecting polygon stores the repaired geometry and pushes a warning message', async () => {
      const { api, onEdit } = await renderHarness()
      enterEditMode(api)

      const bowtie: Polygon = {
        type: 'Polygon',
        coordinates: [
          [
            [0, 0],
            [2, 2],
            [2, 0],
            [0, 2],
            [0, 0],
          ],
        ],
      }

      act(() => {
        onEdit({
          editType: EditType.finishMovePosition,
          updatedData: {
            type: 'FeatureCollection',
            features: [
              {
                type: 'Feature',
                id: MASK_ID,
                geometry: bowtie,
                properties: { label: 'Bowtie', visible: true, index: 0 },
              },
            ],
          },
        })
      })

      expect((api.current!.getGeometry() as Polygon).type).toBe('MultiPolygon')
      expect(api.current!.getMessages()).toEqual([
        {
          id: MASK_ID,
          title: DEFAULT_LASSO_TOOLS_PROPS.labels.selfIntersectionRepaired,
          severity: 'warning',
        },
      ])
    })

    test('locking the selected geometry removes it from the editable selection (hides handles)', async () => {
      const { api } = await renderHarness()
      enterEditMode(api)

      const selectedFeatureIndexes = () =>
        (
          useLayerStore.getState().getLayer(MAP_LASSO_TOOLS_LAYER_ID)
            ?.props as { selectedFeatureIndexes?: number[] }
        ).selectedFeatureIndexes

      // Selecting the (unlocked) geometry makes it the editable target.
      act(() => {
        api.current!.setSelectedId(MAP_ID, MASK_ID)
      })
      await waitFor(() => {
        expect(selectedFeatureIndexes()).toEqual([0])
      })

      // Locking it must drop it from the editable selection so no edit handles
      // render, even though it stays selected for the toolbar.
      act(() => {
        api.current!.setLocked(true)
      })
      await waitFor(() => {
        expect(selectedFeatureIndexes()).toEqual([])
      })
    })

    async function getCurrentOnEdit(): Promise<OnEdit> {
      let onEdit: OnEdit | undefined
      await waitFor(() => {
        const layer = useLayerStore
          .getState()
          .getLayer(MAP_LASSO_TOOLS_LAYER_ID)
        onEdit = (layer?.props as { onEdit?: OnEdit })?.onEdit
        expect(onEdit).toBeTypeOf('function')
      })
      return onEdit!
    }

    test('addPosition (in edit mode) commits geometry and keeps edit mode active', async () => {
      const { api } = await renderHarness()
      enterEditMode(api)
      const onEdit = await getCurrentOnEdit()

      act(() => {
        onEdit({
          editType: EditType.addPosition,
          updatedData: makeUpdatedData(),
        })
      })

      expect(api.current!.getGeometry()).toEqual(movedGeometry)
      expect(api.current!.getEnabled()).toBe(true)
    })

    test('removePosition (in edit mode) commits geometry and keeps edit mode active', async () => {
      const { api } = await renderHarness()
      enterEditMode(api)
      const onEdit = await getCurrentOnEdit()

      act(() => {
        onEdit({
          editType: EditType.removePosition,
          updatedData: makeUpdatedData(),
        })
      })

      expect(api.current!.getGeometry()).toEqual(movedGeometry)
      expect(api.current!.getEnabled()).toBe(true)
    })

    test('addFeature still exits edit mode (regression)', async () => {
      const { api, onEdit } = await renderHarness()
      enterEditMode(api)

      act(() => {
        onEdit({
          editType: EditType.addFeature,
          updatedData: makeUpdatedData(),
        })
      })

      expect(api.current!.getGeometry()).toEqual(movedGeometry)
      expect(api.current!.getEnabled()).toBe(false)
    })

    test('movePosition (without finish) does not commit to the store', async () => {
      const { api, onEdit } = await renderHarness()
      enterEditMode(api)

      const originalGeometry = api.current!.getGeometry()

      act(() => {
        onEdit({
          editType: 'movePosition',
          updatedData: makeUpdatedData(),
        })
      })

      expect(api.current!.getGeometry()).toEqual(originalGeometry)
      expect(api.current!.getGeometry()).not.toEqual(movedGeometry)
    })
  })

  test('should handle multiple mask operations', async () => {
    const TestComponent = () => {
      const setVisibility = useLassoToolsState((state) => state.setVisibility)
      const enabled = useLassoToolsState((state) => state.data[MAP_ID]?.enabled)

      return (
        <LassoTools mapId={MAP_ID}>
          <button onClick={() => setVisibility(MAP_ID, MASK_ID, false)}>
            Hide Mask
          </button>
          <div>Enabled: {String(enabled)}</div>
        </LassoTools>
      )
    }

    const { getByRole, container } = render(<TestComponent />)

    const hideButton = getByRole('button', { name: 'Hide Mask' })
    await userEvent.click(hideButton)

    // Component should still be rendered
    expect(container).toBeTruthy()
  })
})

// Aux
function hasMaskExtension(extensions: unknown): boolean {
  if (!Array.isArray(extensions)) return false
  return extensions.some((ext) => ext instanceof MaskExtension)
}

function ExcludedLayersHarness({
  excludeLayerIds,
}: {
  excludeLayerIds?: string[]
}) {
  const setEnabled = useLassoToolsState((state) => state.setEnabled)
  const addLayers = useMapStore((state) => state.addLayers)

  useEffect(() => {
    useLayerStore.getState().setLayer('layer-a', {
      type: GeoJsonLayer,
      props: { id: 'layer-a', pickable: true },
    })
    useLayerStore.getState().setLayer('layer-b', {
      type: GeoJsonLayer,
      props: { id: 'layer-b', pickable: true },
    })
    addLayers(MAP_ID, ['layer-a', 'layer-b'])
  }, [addLayers])

  return (
    <LassoTools mapId={MAP_ID} excludeLayerIds={excludeLayerIds}>
      <button onClick={() => setEnabled(MAP_ID, true)}>Enable</button>
      <button onClick={() => setEnabled(MAP_ID, false)}>Disable</button>
    </LassoTools>
  )
}

function MaskLayerFilterHarness({
  maskLayerIds,
}: {
  maskLayerIds: string[] | null
}) {
  const setEnabled = useLassoToolsState((state) => state.setEnabled)
  const setMaskLayerIds = useLassoToolsState((state) => state.setMaskLayerIds)
  const addLayers = useMapStore((state) => state.addLayers)

  useEffect(() => {
    useLayerStore.getState().setLayer('layer-a', {
      type: GeoJsonLayer,
      props: { id: 'layer-a', pickable: true },
    })
    useLayerStore.getState().setLayer('layer-b', {
      type: GeoJsonLayer,
      props: { id: 'layer-b', pickable: true },
    })
    addLayers(MAP_ID, ['layer-a', 'layer-b'])
  }, [addLayers])

  return (
    <LassoTools mapId={MAP_ID}>
      <button onClick={() => setEnabled(MAP_ID, true)}>Enable</button>
      <button onClick={() => setEnabled(MAP_ID, false)}>Disable</button>
      <button onClick={() => setMaskLayerIds(MAP_ID, maskLayerIds)}>
        SetFilter
      </button>
    </LassoTools>
  )
}

function LassoToolsWrapper() {
  return (
    <LassoTools mapId={MAP_ID}>
      <LassoTools.Controls mapId={MAP_ID}>
        {({
          enabled,
          setEnabled,
          modeSelected,
          setModeSelected,
          values,
          setVisibility,
        }) => {
          return (
            <div>
              <button onClick={() => setEnabled(false)}>change enabled</button>
              <p>enabled: {String(enabled)}</p>

              <button onClick={() => setModeSelected('circle')}>
                change mode
              </button>
              <p>mode: {modeSelected}</p>

              <button
                onClick={() => {
                  setVisibility(MASK_ID, false)
                }}
              >
                change mask visibility
              </button>
              <p>mask visible: {String(values.mask[0]?.visible)}</p>
            </div>
          )
        }}
      </LassoTools.Controls>
    </LassoTools>
  )
}
