import { describe, it, expect, vi } from 'vitest'
import {
  valueUtils,
  inputTypeUtils,
  formUtils,
  calendarUtils,
  eventUtils,
  cssUtils,
  dateProcessingUtils,
  keyboardUtils,
} from './datepicker-utils'
import { createRef } from 'lit/directives/ref.js'

const mockIsIOS = vi.hoisted(() => vi.fn(() => false))

vi.mock('shared-utils/device-utils', () => ({
  isIOS: mockIsIOS,
}))

describe('datepicker-utils', () => {
  describe('valueUtils', () => {
    describe('validateRangeOrder', () => {
      it('should return true for incomplete ranges', () => {
        expect(valueUtils.validateRangeOrder([])).toBe(true)
        expect(valueUtils.validateRangeOrder(['2024-01-01'])).toBe(true)
      })

      it('should return true for valid date ranges', () => {
        expect(valueUtils.validateRangeOrder(['2024-01-01', '2024-01-31'])).toBe(true)
        expect(valueUtils.validateRangeOrder(['2024-01-01', '2024-01-01'])).toBe(true)
      })

      it('should return false for invalid date ranges', () => {
        expect(valueUtils.validateRangeOrder(['2024-01-31', '2024-01-01'])).toBe(false)
      })
    })

    describe('sortDates', () => {
      it('should sort dates chronologically', () => {
        const dates = ['2024-03-15', '2024-01-10', '2024-02-20']
        const sorted = valueUtils.sortDates(dates)
        expect(sorted).toEqual(['2024-01-10', '2024-02-20', '2024-03-15'])
      })

      it('should handle empty array', () => {
        expect(valueUtils.sortDates([])).toEqual([])
      })
    })

    describe('filterSelectableDates', () => {
      it('should filter dates outside min/max range', () => {
        const dates = ['2024-01-01', '2024-01-15', '2024-01-31']
        const filtered = valueUtils.filterSelectableDates(dates, '2024-01-10', '2024-01-20')
        expect(filtered).toEqual(['2024-01-15'])
      })

      it('should filter excluded dates', () => {
        const dates = ['2024-01-01', '2024-01-02', '2024-01-03']
        const filtered = valueUtils.filterSelectableDates(
          dates,
          null,
          null,
          ['2024-01-02'],
          undefined,
        )
        expect(filtered).toEqual(['2024-01-01', '2024-01-03'])
      })

      it('should filter excluded weekdays', () => {
        const dates = ['2024-01-01', '2024-01-06', '2024-01-07'] // Mon, Sat, Sun
        const filtered = valueUtils.filterSelectableDates(dates, null, null, undefined, ['0', '6']) // Exclude weekends
        expect(filtered).toEqual(['2024-01-01'])
      })
    })
  })

  describe('inputTypeUtils', () => {
    describe('getInputType', () => {
      it('should return "text" for iOS devices', () => {
        mockIsIOS.mockReturnValue(true)
        const type = inputTypeUtils.getInputType()
        expect(type).toBe('text')
      })

      it('should return "date" for non-iOS devices', () => {
        mockIsIOS.mockReturnValue(false)
        const type = inputTypeUtils.getInputType()
        expect(type).toBe('date')
      })
    })
  })

  describe('formUtils', () => {
    describe('submitForm', () => {
      it('should call requestSubmit on the form if available', () => {
        const mockForm = { requestSubmit: vi.fn() }
        const element = {
          internals: { form: mockForm },
        } as any

        formUtils.submitForm(element)
        expect(mockForm.requestSubmit).toHaveBeenCalledOnce()
      })

      it('should not throw if form is not available', () => {
        const element = {} as any
        expect(() => formUtils.submitForm(element)).not.toThrow()
      })
    })

    describe('submitFormOrFallback', () => {
      it('should submit form if internals has form', () => {
        const mockForm = { requestSubmit: vi.fn() }
        const fallback = vi.fn()
        const internals = { form: mockForm }

        formUtils.submitFormOrFallback(internals, fallback)
        expect(mockForm.requestSubmit).toHaveBeenCalledOnce()
        expect(fallback).not.toHaveBeenCalled()
      })

      it('should call fallback if no form available', () => {
        const fallback = vi.fn()
        formUtils.submitFormOrFallback(null, fallback)
        expect(fallback).toHaveBeenCalledOnce()
      })

      it('should call fallback if internals has no form', () => {
        const fallback = vi.fn()
        const internals = {}
        formUtils.submitFormOrFallback(internals, fallback)
        expect(fallback).toHaveBeenCalledOnce()
      })
    })

    describe('validateDateInput', () => {
      it('should return early if input has no value', () => {
        const input = { value: '' } as HTMLInputElement
        const internals = { setValidity: vi.fn() }
        formUtils.validateDateInput(input, internals)
        expect(internals.setValidity).not.toHaveBeenCalled()
      })

      it('should set rangeUnderflow validity when value is below minimum', () => {
        const input = { value: '2024-01-01' } as HTMLInputElement
        const internals = { setValidity: vi.fn() }
        const strings = { forms: { messages: { rangeUnderflow: 'Too early' } } }

        formUtils.validateDateInput(input, internals, '2024-01-10', null, strings)
        expect(internals.setValidity).toHaveBeenCalledWith(
          { rangeUnderflow: true },
          'Too early',
          input,
        )
      })

      it('should set rangeOverflow validity when value is above maximum', () => {
        const input = { value: '2024-01-31' } as HTMLInputElement
        const internals = { setValidity: vi.fn() }
        const strings = { forms: { messages: { rangeOverflow: 'Too late' } } }

        formUtils.validateDateInput(input, internals, null, '2024-01-20', strings)
        expect(internals.setValidity).toHaveBeenCalledWith(
          { rangeOverflow: true },
          'Too late',
          input,
        )
      })

      it('should use default messages when strings not provided', () => {
        const input = { value: '2024-01-01' } as HTMLInputElement
        const internals = { setValidity: vi.fn() }

        formUtils.validateDateInput(input, internals, '2024-01-10', null)
        expect(internals.setValidity).toHaveBeenCalledWith(
          { rangeUnderflow: true },
          'Value is below minimum',
          input,
        )
      })
    })
  })

  describe('calendarUtils', () => {
    describe('addToSelected', () => {
      it('should return early if target has no value', () => {
        const event = { target: { value: '' } } as any
        const calendarRef = createRef()
        calendarUtils.addToSelected(event, calendarRef as any)
        // Should not throw
      })

      it('should clear input value after processing', () => {
        const mockCalendar = { handleDateSelect: vi.fn() }
        const target = { value: '2024-01-15' }
        const event = { target } as any
        const calendarRef = { value: mockCalendar } as any

        calendarUtils.addToSelected(event, calendarRef)
        expect(target.value).toBe('')
      })

      it('should call handleDateSelect with valid date', () => {
        const mockCalendar = { handleDateSelect: vi.fn() }
        const target = { value: '2024-01-15' }
        const event = { target } as any
        const calendarRef = { value: mockCalendar } as any

        calendarUtils.addToSelected(event, calendarRef)
        expect(mockCalendar.handleDateSelect).toHaveBeenCalled()
      })

      it('should respect min/max constraints', () => {
        const mockCalendar = { handleDateSelect: vi.fn() }
        const target = { value: '2024-01-05' }
        const event = { target } as any
        const calendarRef = { value: mockCalendar } as any

        calendarUtils.addToSelected(event, calendarRef, '2024-01-10', '2024-01-20')
        expect(mockCalendar.handleDateSelect).not.toHaveBeenCalled()
        expect(target.value).toBe('')
      })
    })

    describe('handleCalendarPosition', () => {
      it('should return early if refs are not available', () => {
        const popupRef = createRef()
        const inputRef = createRef()
        expect(() =>
          calendarUtils.handleCalendarPosition(popupRef as any, inputRef as any),
        ).not.toThrow()
      })

      it('should position calendar below input by default', () => {
        const mockPopup = { style: { top: '' }, getBoundingClientRect: () => ({ height: 300 }) }
        const mockInput = {
          getBoundingClientRect: () => ({ height: 40, top: 100 }),
          parentElement: null,
        }
        const popupRef = { value: mockPopup } as any
        const inputRef = { value: mockInput } as any

        calendarUtils.handleCalendarPosition(popupRef, inputRef)
        expect(mockPopup.style.top).toBe('100%')
      })

      it('should position calendar above input if not enough space below', () => {
        const mockPopup = { style: { top: '' }, getBoundingClientRect: () => ({ height: 400 }) }
        const mockInput = {
          getBoundingClientRect: () => ({ height: 40, top: window.innerHeight - 100 }),
          parentElement: null,
        }
        const popupRef = { value: mockPopup } as any
        const inputRef = { value: mockInput } as any

        calendarUtils.handleCalendarPosition(popupRef, inputRef)
        expect(mockPopup.style.top).toContain('calc(100%')
        expect(mockPopup.style.top).toContain('px')
      })

      it('should account for counter when hasCounter is true', () => {
        const mockPopup = { style: { top: '' }, getBoundingClientRect: () => ({ height: 300 }) }
        const mockInput = {
          getBoundingClientRect: () => ({ height: 40, top: 100 }),
          parentElement: null,
        }
        const popupRef = { value: mockPopup } as any
        const inputRef = { value: mockInput } as any

        calendarUtils.handleCalendarPosition(popupRef, inputRef, true)
        expect(mockPopup.style.top).toBe('calc(100% - 30px)')
      })
    })
  })

  describe('eventUtils', () => {
    describe('createDocumentClickListener', () => {
      it('should return a function', () => {
        const listener = eventUtils.createDocumentClickListener(
          createRef() as any,
          null,
          createRef() as any,
          () => true,
          vi.fn(),
          vi.fn(),
        )
        expect(typeof listener).toBe('function')
      })

      it('should call onBlur and hideCalendar when clicking outside', () => {
        const onBlur = vi.fn()
        const hideCalendar = vi.fn()
        const inputRef = { value: { contains: () => false } } as any
        const btnRef = { value: { contains: () => false } } as any

        const listener = eventUtils.createDocumentClickListener(
          inputRef,
          null,
          btnRef,
          () => true,
          onBlur,
          hideCalendar,
        )

        const event = {
          target: document.createElement('div'),
        } as any

        listener(event)
        expect(onBlur).toHaveBeenCalledOnce()
        expect(hideCalendar).toHaveBeenCalledOnce()
      })

      it('should not call handlers when clicking inside input', () => {
        const onBlur = vi.fn()
        const hideCalendar = vi.fn()
        const inputRef = { value: { contains: () => true } } as any
        const btnRef = { value: { contains: () => false } } as any

        const listener = eventUtils.createDocumentClickListener(
          inputRef,
          null,
          btnRef,
          () => true,
          onBlur,
          hideCalendar,
        )

        const event = {
          target: document.createElement('div'),
        } as any

        listener(event)
        expect(onBlur).not.toHaveBeenCalled()
        expect(hideCalendar).not.toHaveBeenCalled()
      })
    })

    describe('createDocumentKeydownListener', () => {
      it('should return a function', () => {
        const listener = eventUtils.createDocumentKeydownListener(() => true, vi.fn())
        expect(typeof listener).toBe('function')
      })

      it('should call hideCalendar on Escape key', () => {
        const hideCalendar = vi.fn()
        const listener = eventUtils.createDocumentKeydownListener(() => true, hideCalendar)

        const event = { key: 'Escape' } as KeyboardEvent
        listener(event)
        expect(hideCalendar).toHaveBeenCalledOnce()
      })

      it('should not call hideCalendar if calendar is not open', () => {
        const hideCalendar = vi.fn()
        const listener = eventUtils.createDocumentKeydownListener(() => false, hideCalendar)

        const event = { key: 'Escape' } as KeyboardEvent
        listener(event)
        expect(hideCalendar).not.toHaveBeenCalled()
      })

      it('should not call hideCalendar on other keys', () => {
        const hideCalendar = vi.fn()
        const listener = eventUtils.createDocumentKeydownListener(() => true, hideCalendar)

        const event = { key: 'Enter' } as KeyboardEvent
        listener(event)
        expect(hideCalendar).not.toHaveBeenCalled()
      })
    })

    describe('handleFocusOut', () => {
      it('should call onBlur and hideCalendar when focus leaves element', () => {
        const onBlur = vi.fn()
        const hideCalendar = vi.fn()
        const element = { contains: () => false } as any
        const event = { target: document.createElement('div') } as any

        eventUtils.handleFocusOut(event, element, onBlur, hideCalendar)
        expect(onBlur).toHaveBeenCalledOnce()
        expect(hideCalendar).toHaveBeenCalledOnce()
      })

      it('should not call handlers when focus stays within element', () => {
        const onBlur = vi.fn()
        const hideCalendar = vi.fn()
        const element = { contains: () => true } as any
        const event = { target: document.createElement('div') } as any

        eventUtils.handleFocusOut(event, element, onBlur, hideCalendar)
        expect(onBlur).not.toHaveBeenCalled()
        expect(hideCalendar).not.toHaveBeenCalled()
      })
    })
  })

  describe('cssUtils', () => {
    describe('getInputClasses', () => {
      it('should return base classes', () => {
        const classes = cssUtils.getInputClasses(false, false, false, false)
        expect(classes['pkt-input']).toBe(true)
        expect(classes['pkt-datepicker__input']).toBe(true)
      })

      it('should include fullwidth class when fullwidth is true', () => {
        const classes = cssUtils.getInputClasses(true, false, false, false)
        expect(classes['pkt-input--fullwidth']).toBe(true)
      })

      it('should include hasrangelabels class when showRangeLabels is true', () => {
        const classes = cssUtils.getInputClasses(false, true, false, false)
        expect(classes['pkt-datepicker--hasrangelabels']).toBe(true)
      })

      it('should include multiple class when multiple is true', () => {
        const classes = cssUtils.getInputClasses(false, false, true, false)
        expect(classes['pkt-datepicker--multiple']).toBe(true)
      })

      it('should include range class when range is true', () => {
        const classes = cssUtils.getInputClasses(false, false, false, true)
        expect(classes['pkt-datepicker--range']).toBe(true)
      })

      it('should include ios-readonly-hack when readonly is false and inputType is text', () => {
        const classes = cssUtils.getInputClasses(false, false, false, false, false, 'text')
        expect(classes['ios-readonly-hack']).toBe(true)
      })
    })

    describe('getButtonClasses', () => {
      it('should return button classes', () => {
        const classes = cssUtils.getButtonClasses()
        expect(classes['pkt-input-icon']).toBe(true)
        expect(classes['pkt-btn']).toBe(true)
        expect(classes['pkt-btn--icon-only']).toBe(true)
        expect(classes['pkt-btn--tertiary']).toBe(true)
        expect(classes['pkt-datepicker__calendar-button']).toBe(true)
      })
    })

    describe('getRangeLabelClasses', () => {
      it('should return correct classes when showRangeLabels is true', () => {
        const classes = cssUtils.getRangeLabelClasses(true)
        expect(classes['pkt-input-prefix']).toBe(true)
        expect(classes['pkt-hide']).toBe(false)
      })

      it('should return correct classes when showRangeLabels is false', () => {
        const classes = cssUtils.getRangeLabelClasses(false)
        expect(classes['pkt-input-prefix']).toBe(false)
        expect(classes['pkt-hide']).toBe(true)
      })
    })
  })

  describe('dateProcessingUtils', () => {
    describe('processDateSelection', () => {
      it('should return first date for single selection', () => {
        const result = dateProcessingUtils.processDateSelection(['2024-01-15'], false, false)
        expect(result).toBe('2024-01-15')
      })

      it('should return empty string when no dates for single selection', () => {
        const result = dateProcessingUtils.processDateSelection([], false, false)
        expect(result).toBe('')
      })

      it('should return comma-separated dates for multiple selection', () => {
        const result = dateProcessingUtils.processDateSelection(
          ['2024-01-15', '2024-01-20'],
          true,
          false,
        )
        expect(result).toBe('2024-01-15,2024-01-20')
      })

      it('should return comma-separated dates for range selection', () => {
        const result = dateProcessingUtils.processDateSelection(
          ['2024-01-15', '2024-01-20'],
          false,
          true,
        )
        expect(result).toBe('2024-01-15,2024-01-20')
      })
    })

    describe('updateInputValues', () => {
      it('should return early if inputRef has no value', () => {
        const inputRef = { value: null } as any
        expect(() =>
          dateProcessingUtils.updateInputValues(inputRef, null, [], false, false, vi.fn()),
        ).not.toThrow()
      })

      it('should update both inputs for range', () => {
        const input = { value: '' } as any
        const inputTo = { value: '' } as any
        const inputRef = { value: input } as any
        const inputRefTo = { value: inputTo } as any
        const manageValidity = vi.fn()

        dateProcessingUtils.updateInputValues(
          inputRef,
          inputRefTo,
          ['2024-01-15', '2024-01-20'],
          true,
          false,
          manageValidity,
        )

        expect(input.value).toBe('2024-01-15')
        expect(inputTo.value).toBe('2024-01-20')
        expect(manageValidity).toHaveBeenCalledTimes(2)
      })

      it('should update single input for non-multiple, non-range', () => {
        const input = { value: '' } as any
        const inputRef = { value: input } as any
        const manageValidity = vi.fn()

        dateProcessingUtils.updateInputValues(
          inputRef,
          null,
          ['2024-01-15'],
          false,
          false,
          manageValidity,
        )

        expect(input.value).toBe('2024-01-15')
        expect(manageValidity).toHaveBeenCalledOnce()
      })

      it('should not update input for multiple selection', () => {
        const input = { value: 'initial' } as any
        const inputRef = { value: input } as any
        const manageValidity = vi.fn()

        dateProcessingUtils.updateInputValues(
          inputRef,
          null,
          ['2024-01-15'],
          false,
          true,
          manageValidity,
        )

        expect(input.value).toBe('initial')
        expect(manageValidity).not.toHaveBeenCalled()
      })
    })

    describe('processRangeBlur', () => {
      it('should call manageValidity and handleDateSelect when target has value', () => {
        const target = { value: '2024-01-15' } as any
        const event = { target } as any
        const mockCalendar = { handleDateSelect: vi.fn() }
        const calendarRef = { value: mockCalendar } as any
        const clearInputValue = vi.fn()
        const manageValidity = vi.fn()

        dateProcessingUtils.processRangeBlur(
          event,
          ['2024-01-10', '2024-01-20'],
          calendarRef,
          clearInputValue,
          manageValidity,
        )

        expect(manageValidity).toHaveBeenCalledWith(target)
        expect(clearInputValue).not.toHaveBeenCalled()
        expect(mockCalendar.handleDateSelect).toHaveBeenCalled()
      })

      it('should clear input value when target is empty but values[0] exists', () => {
        const target = { value: '' } as any
        const event = { target } as any
        const mockCalendar = { handleDateSelect: vi.fn() }
        const calendarRef = { value: mockCalendar } as any
        const clearInputValue = vi.fn()
        const manageValidity = vi.fn()

        dateProcessingUtils.processRangeBlur(
          event,
          ['2024-01-10'],
          calendarRef,
          clearInputValue,
          manageValidity,
        )

        expect(clearInputValue).toHaveBeenCalled()
        expect(manageValidity).not.toHaveBeenCalled()
        expect(mockCalendar.handleDateSelect).not.toHaveBeenCalled()
      })
    })
  })

  describe('keyboardUtils', () => {
    describe('handleInputKeydown', () => {
      it('should call toggleCalendar on Space key', () => {
        const toggleCalendar = vi.fn()
        const event = { key: ' ', preventDefault: vi.fn() } as any

        keyboardUtils.handleInputKeydown(event, toggleCalendar)
        expect(event.preventDefault).toHaveBeenCalled()
        expect(toggleCalendar).toHaveBeenCalledWith(event)
      })

      it('should call submitForm on Enter key when provided', () => {
        const toggleCalendar = vi.fn()
        const submitForm = vi.fn()
        const event = { key: 'Enter', preventDefault: vi.fn() } as any

        keyboardUtils.handleInputKeydown(event, toggleCalendar, submitForm)
        expect(event.preventDefault).toHaveBeenCalled()
        expect(submitForm).toHaveBeenCalled()
        expect(toggleCalendar).not.toHaveBeenCalled()
      })

      it('should call focusNextInput on Enter when submitForm not provided', () => {
        const toggleCalendar = vi.fn()
        const focusNextInput = vi.fn()
        const event = { key: 'Enter', preventDefault: vi.fn() } as any

        keyboardUtils.handleInputKeydown(event, toggleCalendar, undefined, focusNextInput)
        expect(event.preventDefault).toHaveBeenCalled()
        expect(focusNextInput).toHaveBeenCalled()
      })

      it('should call blurInput on Enter when neither submitForm nor focusNextInput provided', () => {
        const toggleCalendar = vi.fn()
        const blurInput = vi.fn()
        const event = { key: 'Enter', preventDefault: vi.fn() } as any

        keyboardUtils.handleInputKeydown(event, toggleCalendar, undefined, undefined, blurInput)
        expect(event.preventDefault).toHaveBeenCalled()
        expect(blurInput).toHaveBeenCalled()
      })

      it('should call commaHandler on comma key when provided', () => {
        const toggleCalendar = vi.fn()
        const commaHandler = vi.fn()
        const event = { key: ',', preventDefault: vi.fn() } as any

        keyboardUtils.handleInputKeydown(
          event,
          toggleCalendar,
          undefined,
          undefined,
          undefined,
          commaHandler,
        )
        expect(event.preventDefault).toHaveBeenCalled()
        expect(commaHandler).toHaveBeenCalledWith(event)
      })

      it('should call blurInput on comma key when commaHandler not provided', () => {
        const toggleCalendar = vi.fn()
        const blurInput = vi.fn()
        const event = { key: ',', preventDefault: vi.fn() } as any

        keyboardUtils.handleInputKeydown(event, toggleCalendar, undefined, undefined, blurInput)
        expect(event.preventDefault).toHaveBeenCalled()
        expect(blurInput).toHaveBeenCalled()
      })
    })

    describe('handleButtonKeydown', () => {
      it('should call toggleCalendar on Enter key', () => {
        const toggleCalendar = vi.fn()
        const event = { key: 'Enter', preventDefault: vi.fn() } as any

        keyboardUtils.handleButtonKeydown(event, toggleCalendar)
        expect(event.preventDefault).toHaveBeenCalled()
        expect(toggleCalendar).toHaveBeenCalledWith(event)
      })

      it('should call toggleCalendar on Space key', () => {
        const toggleCalendar = vi.fn()
        const event = { key: ' ', preventDefault: vi.fn() } as any

        keyboardUtils.handleButtonKeydown(event, toggleCalendar)
        expect(event.preventDefault).toHaveBeenCalled()
        expect(toggleCalendar).toHaveBeenCalledWith(event)
      })

      it('should not call toggleCalendar on other keys', () => {
        const toggleCalendar = vi.fn()
        const event = { key: 'a', preventDefault: vi.fn() } as any

        keyboardUtils.handleButtonKeydown(event, toggleCalendar)
        expect(toggleCalendar).not.toHaveBeenCalled()
      })
    })
  })
})
