import { describe, it, expect, vi } from 'vitest'
import { CustomMeasureAreaMode } from './custom-measurement-area.class'
import { MapMeasurementUnitAuto } from '../const'
import type {
  ModeProps,
  FeatureCollection,
} from '@deck.gl-community/editable-layers'
import type {
  BasePointerEvent,
  PointerMoveEvent,
} from 'node_modules/@deck.gl-community/editable-layers/dist/edit-modes/types'

describe('CustomMeasureAreaMode', () => {
  const mockLastPointerMoveEvent: PointerMoveEvent = {
    picks: [],
    screenCoords: [0, 0],
    mapCoords: [0, 0],
    sourceEvent: null,
    cancelPan: vi.fn(),
    pointerDownPicks: null,
    pointerDownScreenCoords: null,
    pointerDownMapCoords: null,
    isDragging: false,
  }

  const mockProps: ModeProps<FeatureCollection> = {
    data: {
      type: 'FeatureCollection',
      features: [],
    },
    onEdit: vi.fn(),
    onUpdateCursor: vi.fn(),
    selectedIndexes: [],
    lastPointerMoveEvent: mockLastPointerMoveEvent,
    modeConfig: {},
    cursor: null,
  }

  describe('constructor', () => {
    it('should initialize with default values', () => {
      const mode = new CustomMeasureAreaMode()
      expect(mode._isMeasuringSessionFinished).toBe(false)
      expect(mode.area).toBe(0)
      expect(mode.tooltips).toEqual([])
      expect(mode.resultPolygon).toEqual({
        type: 'FeatureCollection',
        features: [],
      })
    })
  })

  describe('handleClick', () => {
    it('should handle first click and add position', () => {
      const mode = new CustomMeasureAreaMode()
      const mockEvent: BasePointerEvent = {
        mapCoords: [0, 0],
        screenCoords: [0, 0],
        picks: [],
        sourceEvent: new MouseEvent('click'),
      }

      mode.handleClick(mockEvent, mockProps)

      expect(mockProps.onEdit).toHaveBeenCalled()
    })

    it('should restart measurement session when finished', () => {
      const mode = new CustomMeasureAreaMode()
      mode._isMeasuringSessionFinished = true
      mode.area = 100

      const mockEvent: BasePointerEvent = {
        mapCoords: [1, 1],
        screenCoords: [100, 100],
        picks: [],
        sourceEvent: new MouseEvent('click'),
      }

      mode.handleClick(mockEvent, mockProps)

      expect(mode._isMeasuringSessionFinished).toBe(false)
      expect(mode.area).toBeGreaterThanOrEqual(0)
    })

    it('should calculate area after clicks', () => {
      const mode = new CustomMeasureAreaMode()

      const mockEvent1: BasePointerEvent = {
        mapCoords: [0, 0],
        screenCoords: [0, 0],
        picks: [],
        sourceEvent: new MouseEvent('click'),
      }

      const mockEvent2: BasePointerEvent = {
        mapCoords: [1, 0],
        screenCoords: [100, 0],
        picks: [],
        sourceEvent: new MouseEvent('click'),
      }

      const mockEvent3: BasePointerEvent = {
        mapCoords: [1, 1],
        screenCoords: [100, 100],
        picks: [],
        sourceEvent: new MouseEvent('click'),
      }

      mode.handleClick(mockEvent1, mockProps)
      mode.handleClick(mockEvent2, mockProps)
      mode.handleClick(mockEvent3, mockProps)

      expect(mode.area).toBeGreaterThan(0)
    })

    it('should not add position when allowOverlapping returns true', () => {
      const mode = new CustomMeasureAreaMode()

      const propsWithPreventOverlapping: ModeProps<FeatureCollection> = {
        ...mockProps,
        modeConfig: {
          preventOverlappingLines: true,
        },
      }

      // Create clicks that will result in overlapping lines
      const mockClick1: BasePointerEvent = {
        mapCoords: [0, 0],
        screenCoords: [0, 0],
        picks: [],
        sourceEvent: new MouseEvent('click'),
      }
      const mockClick2: BasePointerEvent = {
        mapCoords: [2, 2],
        screenCoords: [200, 200],
        picks: [],
        sourceEvent: new MouseEvent('click'),
      }
      const mockClick3: BasePointerEvent = {
        mapCoords: [0, 2],
        screenCoords: [0, 200],
        picks: [],
        sourceEvent: new MouseEvent('click'),
      }
      const mockClick4: BasePointerEvent = {
        mapCoords: [2, 0],
        screenCoords: [200, 0],
        picks: [],
        sourceEvent: new MouseEvent('click'),
      }

      mode.handleClick(mockClick1, propsWithPreventOverlapping)
      mode.handleClick(mockClick2, propsWithPreventOverlapping)
      mode.handleClick(mockClick3, propsWithPreventOverlapping)

      vi.clearAllMocks()

      // This click would create an overlapping line
      mode.handleClick(mockClick4, propsWithPreventOverlapping)

      // onEdit should still be called, but without addTentativePosition
      const onEditMock = vi.mocked(propsWithPreventOverlapping.onEdit)
      const calls = onEditMock.mock.calls
      // The call should not have editType: 'addTentativePosition'
      const hasAddTentativePositionCall = calls.some(
        (call) => call[0]?.editType === 'addTentativePosition',
      )
      expect(hasAddTentativePositionCall).toBe(false)
    })

    it('should handle click on edit handle to finalize', () => {
      const mode = new CustomMeasureAreaMode()

      // Add three clicks to create a valid polygon
      const mockClick1: BasePointerEvent = {
        mapCoords: [0, 0],
        screenCoords: [0, 0],
        picks: [],
        sourceEvent: new MouseEvent('click'),
      }
      const mockClick2: BasePointerEvent = {
        mapCoords: [1, 0],
        screenCoords: [100, 0],
        picks: [],
        sourceEvent: new MouseEvent('click'),
      }
      const mockClick3: BasePointerEvent = {
        mapCoords: [1, 1],
        screenCoords: [100, 100],
        picks: [],
        sourceEvent: new MouseEvent('click'),
      }

      mode.handleClick(mockClick1, mockProps)
      mode.handleClick(mockClick2, mockProps)
      mode.handleClick(mockClick3, mockProps)

      vi.clearAllMocks()

      // Click on first point (edit handle)
      const mockEditHandleClick: BasePointerEvent = {
        mapCoords: [0, 0],
        screenCoords: [0, 0],
        picks: [
          {
            index: 0,
            isGuide: true,
            object: {
              properties: {
                guideType: 'editHandle',
                positionIndexes: [0],
              },
            },
          },
        ],
        sourceEvent: new MouseEvent('click'),
      }

      mode.handleClick(mockEditHandleClick, mockProps)

      // Should finalize the edition
      expect(mode._isMeasuringSessionFinished).toBe(true)
    })
  })

  describe('handleKeyUp', () => {
    it('should finalize measurement on Enter with valid polygon', () => {
      const mode = new CustomMeasureAreaMode()

      // Add three clicks to create a valid polygon
      const mockClick1: BasePointerEvent = {
        mapCoords: [0, 0],
        screenCoords: [0, 0],
        picks: [],
        sourceEvent: new MouseEvent('click'),
      }
      const mockClick2: BasePointerEvent = {
        mapCoords: [1, 0],
        screenCoords: [100, 0],
        picks: [],
        sourceEvent: new MouseEvent('click'),
      }
      const mockClick3: BasePointerEvent = {
        mapCoords: [1, 1],
        screenCoords: [100, 100],
        picks: [],
        sourceEvent: new MouseEvent('click'),
      }

      mode.handleClick(mockClick1, mockProps)
      mode.handleClick(mockClick2, mockProps)
      mode.handleClick(mockClick3, mockProps)

      vi.clearAllMocks()

      const mockKeyEvent = new KeyboardEvent('keyup', { key: 'Enter' })
      mode.handleKeyUp(mockKeyEvent, mockProps)

      expect(mockProps.onEdit).toHaveBeenCalled()
    })

    it('should reset on Enter with insufficient points', () => {
      const mode = new CustomMeasureAreaMode()

      // Add only two clicks (not enough for a polygon)
      const mockClick1: BasePointerEvent = {
        mapCoords: [0, 0],
        screenCoords: [0, 0],
        picks: [],
        sourceEvent: new MouseEvent('click'),
      }
      const mockClick2: BasePointerEvent = {
        mapCoords: [1, 0],
        screenCoords: [100, 0],
        picks: [],
        sourceEvent: new MouseEvent('click'),
      }

      mode.handleClick(mockClick1, mockProps)
      mode.handleClick(mockClick2, mockProps)

      vi.clearAllMocks()

      const mockKeyEvent = new KeyboardEvent('keyup', { key: 'Enter' })
      mode.handleKeyUp(mockKeyEvent, mockProps)

      expect(mockProps.onEdit).toHaveBeenCalledWith(
        expect.objectContaining({
          editType: 'cancelFeature',
        }),
      )
    })

    it('should reset measurement on Escape', () => {
      const mode = new CustomMeasureAreaMode()

      const mockClick: BasePointerEvent = {
        mapCoords: [0, 0],
        screenCoords: [0, 0],
        picks: [],
        sourceEvent: new MouseEvent('click'),
      }

      mode.handleClick(mockClick, mockProps)
      vi.clearAllMocks()

      const mockKeyEvent = new KeyboardEvent('keyup', { key: 'Escape' })
      mode.handleKeyUp(mockKeyEvent, mockProps)

      expect(mockProps.onEdit).toHaveBeenCalledWith(
        expect.objectContaining({
          editType: 'cancelFeature',
          editContext: {},
        }),
      )
    })
  })

  describe('handlePointerMove', () => {
    it('should return early when measurement session is finished', () => {
      const mode = new CustomMeasureAreaMode()
      mode._isMeasuringSessionFinished = true

      const mockEvent: PointerMoveEvent = {
        picks: [],
        screenCoords: [50, 50],
        mapCoords: [0.5, 0.5],
        sourceEvent: null,
        cancelPan: vi.fn(),
        pointerDownPicks: null,
        pointerDownScreenCoords: null,
        pointerDownMapCoords: null,
        isDragging: false,
      }

      const initialTooltips = [...mode.tooltips]
      mode.handlePointerMove(mockEvent, mockProps)

      expect(mode.tooltips).toEqual(initialTooltips)
    })

    it('should handle pointer move during active session', () => {
      const mode = new CustomMeasureAreaMode()

      // Start a measurement
      const mockClick: BasePointerEvent = {
        mapCoords: [0, 0],
        screenCoords: [0, 0],
        picks: [],
        sourceEvent: new MouseEvent('click'),
      }

      mode.handleClick(mockClick, mockProps)

      const mockEvent: PointerMoveEvent = {
        picks: [],
        screenCoords: [50, 50],
        mapCoords: [0.5, 0.5],
        sourceEvent: null,
        cancelPan: vi.fn(),
        pointerDownPicks: null,
        pointerDownScreenCoords: null,
        pointerDownMapCoords: null,
        isDragging: false,
      }

      mode.handlePointerMove(mockEvent, mockProps)
      // Should not throw and should update internal state
      expect(mode._isMeasuringSessionFinished).toBe(false)
    })
  })

  describe('resetClickSequence', () => {
    it('should reset all measurement state', () => {
      const mode = new CustomMeasureAreaMode()
      mode.area = 100
      mode.tooltips = [{ position: [0, 0], text: 'test' }]
      mode._isMeasuringSessionFinished = true

      mode.resetClickSequence()

      expect(mode.area).toBe(0)
      expect(mode.tooltips).toEqual([])
      expect(mode._isMeasuringSessionFinished).toBe(false)
    })
  })

  describe('getGuides', () => {
    it('should return result polygon when measurement is finished', () => {
      const mode = new CustomMeasureAreaMode()
      mode._isMeasuringSessionFinished = true
      mode.resultPolygon = {
        type: 'FeatureCollection',
        features: [
          {
            type: 'Feature',
            geometry: {
              type: 'Polygon',
              coordinates: [
                [
                  [0, 0],
                  [1, 0],
                  [1, 1],
                  [0, 0],
                ],
              ],
            },
            properties: {},
          },
        ],
      }

      const guides = mode.getGuides(mockProps)

      expect(guides).toEqual(mode.resultPolygon)
    })

    it('should return super guides when measurement is not finished', () => {
      const mode = new CustomMeasureAreaMode()
      mode._isMeasuringSessionFinished = false

      const guides = mode.getGuides(mockProps)

      expect(guides).toBeDefined()
    })
  })

  describe('getTooltips', () => {
    it('should return current tooltips', () => {
      const mode = new CustomMeasureAreaMode()
      const mockTooltips = [{ position: [0, 0], text: 'test' }]
      mode.tooltips = mockTooltips

      const tooltips = mode.getTooltips()

      expect(tooltips).toEqual(mockTooltips)
    })
  })

  describe('isFinalizedEdition', () => {
    it('should return true when clicking first point with enough clicks', () => {
      const mode = new CustomMeasureAreaMode()
      const clickSequence = [
        [0, 0],
        [1, 0],
        [1, 1],
      ]
      const clickedEditHandle = {
        properties: { positionIndexes: [0] },
      }

      const result = mode.isFinalizedEdition(clickSequence, clickedEditHandle)

      expect(result).toBe(true)
    })

    it('should return true when clicking last point with enough clicks', () => {
      const mode = new CustomMeasureAreaMode()
      const clickSequence = [
        [0, 0],
        [1, 0],
        [1, 1],
      ]
      const clickedEditHandle = {
        properties: { positionIndexes: [2] },
      }

      const result = mode.isFinalizedEdition(clickSequence, clickedEditHandle)

      expect(result).toBe(true)
    })

    it('should return false when not enough clicks', () => {
      const mode = new CustomMeasureAreaMode()
      const clickSequence = [
        [0, 0],
        [1, 0],
      ]
      const clickedEditHandle = {
        properties: { positionIndexes: [0] },
      }

      const result = mode.isFinalizedEdition(clickSequence, clickedEditHandle)

      expect(result).toBe(false)
    })

    it('should return false when no edit handle', () => {
      const mode = new CustomMeasureAreaMode()
      const clickSequence = [
        [0, 0],
        [1, 0],
        [1, 1],
      ]

      const result = mode.isFinalizedEdition(clickSequence, null)

      expect(result).toBe(false)
    })

    it('should return false when edit handle is undefined', () => {
      const mode = new CustomMeasureAreaMode()
      const clickSequence = [
        [0, 0],
        [1, 0],
        [1, 1],
      ]

      const result = mode.isFinalizedEdition(clickSequence, undefined)

      expect(result).toBe(false)
    })

    it('should return false when clicking middle point', () => {
      const mode = new CustomMeasureAreaMode()
      const clickSequence = [
        [0, 0],
        [1, 0],
        [1, 1],
        [0, 1],
      ]
      const clickedEditHandle = {
        properties: { positionIndexes: [1] }, // Middle point
      }

      const result = mode.isFinalizedEdition(clickSequence, clickedEditHandle)

      expect(result).toBe(false)
    })
  })

  describe('allowOverlapping', () => {
    it('should return false when not enough points', () => {
      const mode = new CustomMeasureAreaMode()
      const result = mode.allowOverlapping(mockProps)

      expect(result).toBe(false)
    })

    it('should return false when preventOverlappingLines is not set', () => {
      const mode = new CustomMeasureAreaMode()

      const mockClick1: BasePointerEvent = {
        mapCoords: [0, 0],
        screenCoords: [0, 0],
        picks: [],
        sourceEvent: new MouseEvent('click'),
      }
      const mockClick2: BasePointerEvent = {
        mapCoords: [1, 0],
        screenCoords: [100, 0],
        picks: [],
        sourceEvent: new MouseEvent('click'),
      }
      const mockClick3: BasePointerEvent = {
        mapCoords: [1, 1],
        screenCoords: [100, 100],
        picks: [],
        sourceEvent: new MouseEvent('click'),
      }

      mode.handleClick(mockClick1, mockProps)
      mode.handleClick(mockClick2, mockProps)
      mode.handleClick(mockClick3, mockProps)

      const result = mode.allowOverlapping(mockProps)

      expect(result).toBe(false)
    })

    it('should check for overlaps when preventOverlappingLines is enabled', () => {
      const mode = new CustomMeasureAreaMode()

      const propsWithPreventOverlapping: ModeProps<FeatureCollection> = {
        ...mockProps,
        modeConfig: {
          preventOverlappingLines: true,
        },
      }

      // Create a simple triangle first (definitely non-overlapping)
      const mockClick1: BasePointerEvent = {
        mapCoords: [0, 0],
        screenCoords: [0, 0],
        picks: [],
        sourceEvent: new MouseEvent('click'),
      }
      const mockClick2: BasePointerEvent = {
        mapCoords: [1, 0],
        screenCoords: [100, 0],
        picks: [],
        sourceEvent: new MouseEvent('click'),
      }
      const mockClick3: BasePointerEvent = {
        mapCoords: [0.5, 1],
        screenCoords: [50, 100],
        picks: [],
        sourceEvent: new MouseEvent('click'),
      }

      mode.handleClick(mockClick1, propsWithPreventOverlapping)
      mode.handleClick(mockClick2, propsWithPreventOverlapping)
      mode.handleClick(mockClick3, propsWithPreventOverlapping)

      const result = mode.allowOverlapping(propsWithPreventOverlapping)

      // allowOverlapping checks current line against existing lines
      // For a valid polygon, this should not have overlaps at this stage
      expect(typeof result).toBe('boolean')
    })

    it('should return true when lines overlap with preventOverlappingLines enabled', () => {
      const mode = new CustomMeasureAreaMode()

      const propsWithPreventOverlapping: ModeProps<FeatureCollection> = {
        ...mockProps,
        modeConfig: {
          preventOverlappingLines: true,
        },
      }

      // Create a bow-tie/figure-8 pattern that creates overlapping lines
      const mockClick1: BasePointerEvent = {
        mapCoords: [0, 0],
        screenCoords: [0, 0],
        picks: [],
        sourceEvent: new MouseEvent('click'),
      }
      const mockClick2: BasePointerEvent = {
        mapCoords: [2, 2],
        screenCoords: [200, 200],
        picks: [],
        sourceEvent: new MouseEvent('click'),
      }
      const mockClick3: BasePointerEvent = {
        mapCoords: [0, 2],
        screenCoords: [0, 200],
        picks: [],
        sourceEvent: new MouseEvent('click'),
      }
      const mockClick4: BasePointerEvent = {
        mapCoords: [2, 0],
        screenCoords: [200, 0],
        picks: [],
        sourceEvent: new MouseEvent('click'),
      }

      mode.handleClick(mockClick1, propsWithPreventOverlapping)
      mode.handleClick(mockClick2, propsWithPreventOverlapping)
      mode.handleClick(mockClick3, propsWithPreventOverlapping)
      mode.handleClick(mockClick4, propsWithPreventOverlapping)

      const result = mode.allowOverlapping(propsWithPreventOverlapping)

      // Should return true because the lines cross
      expect(result).toBe(true)
    })
  })

  describe('updateTooltip', () => {
    it('should update area and tooltips', () => {
      const mode = new CustomMeasureAreaMode()
      const polygon = {
        type: 'Polygon' as const,
        coordinates: [
          [
            [0, 0],
            [1, 0],
            [1, 1],
            [0, 1],
            [0, 0],
          ],
        ],
      }

      const propsWithConfig: ModeProps<FeatureCollection> = {
        ...mockProps,
        modeConfig: {
          unit: MapMeasurementUnitAuto['m-km'],
          locale: 'en-US',
        },
      }

      mode.updateTooltip(polygon, propsWithConfig)

      expect(mode.area).toBeGreaterThan(0)
      expect(mode.tooltips.length).toBeGreaterThan(0)
    })
  })

  describe('getTooltipText', () => {
    it('should format area text for metric units', () => {
      const mode = new CustomMeasureAreaMode()

      const result = mode.getTooltipText({
        meters: 1500000,
        locale: 'en-US',
        unit: MapMeasurementUnitAuto['m-km'],
      })

      expect(result).toBeDefined()
      expect(typeof result).toBe('string')
    })

    it('should format area text for imperial units', () => {
      const mode = new CustomMeasureAreaMode()

      const result = mode.getTooltipText({
        meters: 1000000,
        locale: 'en-US',
        unit: MapMeasurementUnitAuto['ft-mi'],
      })

      expect(result).toBeDefined()
      expect(typeof result).toBe('string')
    })

    it('should respect custom text options', () => {
      const mode = new CustomMeasureAreaMode()

      const result = mode.getTooltipText({
        meters: 1234567.89,
        locale: 'en-US',
        unit: MapMeasurementUnitAuto['m-km'],
        options: {
          minimumFractionDigits: 0,
          maximumFractionDigits: 0,
        },
      })

      expect(result).toBeDefined()
      expect(typeof result).toBe('string')
    })
  })

  describe('resetEdition', () => {
    it('should reset measurement and call onEdit', () => {
      const mode = new CustomMeasureAreaMode()
      mode.area = 100

      mode.resetEdition(mockProps)

      expect(mockProps.onEdit).toHaveBeenCalledWith({
        updatedData: mockProps.data,
        editType: 'cancelFeature',
        editContext: {},
      })
    })
  })

  describe('finalizeEdition', () => {
    it('should complete the polygon and update state', () => {
      const mode = new CustomMeasureAreaMode()

      // Add clicks to create a polygon
      const mockClick1: BasePointerEvent = {
        mapCoords: [0, 0],
        screenCoords: [0, 0],
        picks: [],
        sourceEvent: new MouseEvent('click'),
      }
      const mockClick2: BasePointerEvent = {
        mapCoords: [1, 0],
        screenCoords: [100, 0],
        picks: [],
        sourceEvent: new MouseEvent('click'),
      }
      const mockClick3: BasePointerEvent = {
        mapCoords: [1, 1],
        screenCoords: [100, 100],
        picks: [],
        sourceEvent: new MouseEvent('click'),
      }

      mode.handleClick(mockClick1, mockProps)
      mode.handleClick(mockClick2, mockProps)
      mode.handleClick(mockClick3, mockProps)

      vi.clearAllMocks()

      mode.finalizeEdition(mockProps)

      expect(mode._isMeasuringSessionFinished).toBe(true)
    })
  })
})
