// Components
import { VNumberInput } from '../VNumberInput'
import { VForm } from '@/components/VForm'
import { VLocaleProvider } from '@/components/VLocaleProvider'

// Utilities
import { click, commands, render, screen, userEvent } from '@test'
import { nextTick, ref } from 'vue'

describe('VNumberInput', () => {
  it.each([
    { precision: 0, typing: '---', expected: '-' }, // "-" is only allowed once
    { precision: 0, typing: '1-', expected: '1' }, // "-" is only at the start
    { precision: 1, typing: '.', expected: '.' }, // "." is allowed at the start
    { precision: 1, typing: '..', expected: '.' }, // "." is only allowed once
    { precision: 1, typing: '1...0', expected: '1.0' }, // "." is only allowed once
    { precision: 4, typing: '123.45.67', expected: '123.4567' }, // "." is only allowed once
    { precision: 1, typing: 'ab-c8+.iop9', expected: '-8.9' }, // Only numbers, "-", "." are allowed to type in
  ])('prevents NaN from arbitrary input', async ({ precision, typing, expected }) => {
    const { element } = render(() => <VNumberInput precision={ precision } />)
    await userEvent.click(element)
    await userEvent.keyboard(typing)
    expect(screen.getByCSS('input')).toHaveValue(expected)
  })

  describe('grouped input', () => {
    it.each([
      { locale: 'de', separators: {} },
      { locale: 'en', separators: { decimalSeparator: ',', groupSeparator: '.' } },
    ])('keeps the typed value on blur', async ({ locale, separators }) => {
      const model = ref<number | null>(null)
      render(() => (
        <VLocaleProvider locale={ locale }>
          <VNumberInput v-model={ model.value } grouping="auto" precision={ 2 } { ...separators } />
        </VLocaleProvider>
      ))

      const input = screen.getByCSS('input') as HTMLInputElement
      await userEvent.click(input)
      await userEvent.keyboard('1234,56')
      expect(input.value).toBe('1.234,56')

      await userEvent.click(document.body)

      expect(model.value).toBe(1234.56)
      expect(input.value).toBe('1.234,56')
    })
  })

  describe('locales using a non-ASCII minus sign', () => {
    it('keeps the value negative on blur', async () => {
      const model = ref(-1234.1234)
      render(() => (
        <VLocaleProvider locale="hr">
          <VNumberInput v-model={ model.value } precision={ null } />
        </VLocaleProvider>
      ))

      const input = screen.getByCSS('input') as HTMLInputElement
      expect(input.value).toBe('−1234,1234')

      await userEvent.click(input)
      await userEvent.click(document.body)

      expect(model.value).toBe(-1234.1234)
      expect(input.value).toBe('−1234,1234')
    })

    it('accepts a typed ASCII minus and renders the locale sign', async () => {
      const model = ref<number | null>(null)
      render(() => (
        <VLocaleProvider locale="hr">
          <VNumberInput v-model={ model.value } precision={ null } />
        </VLocaleProvider>
      ))

      const input = screen.getByCSS('input') as HTMLInputElement
      await userEvent.click(input)
      await userEvent.keyboard('-5')
      await userEvent.click(document.body)

      expect(model.value).toBe(-5)
      expect(input.value).toBe('−5')
    })
  })

  it('resets v-model to null when click:clear is triggered', async () => {
    const model = ref(5)
    render(() => (
      <VNumberInput
        clearable
        v-model={ model.value }
      />
    ))

    await userEvent.click(screen.getByLabelText('Clear'))
    expect(model.value).toBeNull()
  })

  // https://github.com/vuetifyjs/vuetify/issues/20337
  it('emits model-value when input value is a legit number within range of the max and min', async () => {
    const model = ref(null)
    const { element } = render(() => (
      <VNumberInput
        v-model={ model.value }
        min={ 5 }
        max={ 125 }
      />
    ))

    await userEvent.click(element)
    await userEvent.keyboard('1')
    expect(model.value).toBeNull()

    await userEvent.keyboard('0')
    expect(model.value).toBe(10)

    await userEvent.keyboard('0')
    expect(model.value).toBe(100)

    await userEvent.keyboard('0')
    expect(model.value).toBe(100)

    await userEvent.click(document.body)
    expect(model.value).toBe(125)
  })

  describe('readonly', () => {
    it('prevents mutation when readonly', async () => {
      const model = ref(1)

      const { element } = render(() => (
        <VNumberInput v-model={ model.value } readonly />
      ))

      await click(screen.getByTestId('increment'))
      expect(model.value).toBe(1)

      await click(screen.getByTestId('decrement'))
      expect(model.value).toBe(1)

      await click(element)
      await userEvent.keyboard('{ArrowUp}')
      expect(model.value).toBe(1)

      await userEvent.keyboard('{ArrowDown}')
      expect(model.value).toBe(1)
    })

    // https://github.com/vuetifyjs/vuetify/issues/22677
    it('does not mutate model when typing non-digit chars while readonly', async () => {
      const model = ref(42)

      const { element } = render(() => (
        <VNumberInput v-model={ model.value } readonly />
      ))

      await userEvent.click(element)
      // Select all text and type a non-digit character
      await userEvent.keyboard('{Control>}a{/Control}')
      await userEvent.keyboard(' ')
      expect(model.value).toBe(42)

      // Type arbitrary non-digit chars
      await userEvent.keyboard('abc')
      expect(model.value).toBe(42)
    })

    it('prevents mutation in readonly form', async () => {
      const model = ref(1)

      const { element } = render(() => (
        <VForm readonly>
          <VNumberInput v-model={ model.value } />
        </VForm>
      ))

      await click(screen.getByTestId('increment'))
      expect(model.value).toBe(1)

      await click(screen.getByTestId('decrement'))
      expect(model.value).toBe(1)

      await click(element)
      await userEvent.keyboard('{ArrowUp}')
      expect(model.value).toBe(1)

      await userEvent.keyboard('{ArrowDown}')
      expect(model.value).toBe(1)
    })

    it('keeps original value when readonly or disabled', async () => {
      const value1 = ref(120)
      const value2 = ref(-15)
      const value3 = ref(40.4)
      const value4 = ref(-8.6)

      render(() => (
        <>
          <VNumberInput
            class="readonly-input-1"
            v-model={ value1.value }
            min={ 0 }
            max={ 50 }
            readonly
          />
          <VNumberInput
            class="readonly-input-2"
            v-model={ value2.value }
            min={ 0 }
            max={ 50 }
            readonly
          />
          <VNumberInput
            class="disabled-input-1"
            v-model={ value3.value }
            precision={ 1 }
            min={ 0 }
            max={ 10 }
            disabled
          />
          <VNumberInput
            class="disabled-input-2"
            v-model={ value4.value }
            precision={ 1 }
            min={ 0 }
            max={ 10 }
            disabled
          />
        </>
      ))

      expect(screen.getByCSS('.readonly-input-1 input')).toHaveValue('120')
      expect(screen.getByCSS('.readonly-input-2 input')).toHaveValue('-15')
      expect(screen.getByCSS('.disabled-input-1 input')).toHaveValue('40.4')
      expect(screen.getByCSS('.disabled-input-2 input')).toHaveValue('-8.6')
    })
  })

  describe('native number input quirks', () => {
    it('should auto-clamp after interaction', async () => {
      const model = ref(1)
      render(() =>
        <VNumberInput min={ 5 } max={ 15 } v-model={ model.value } />
      )

      await expect.element(screen.getByCSS('input')).toHaveValue('1')
      expect(model.value).toBe(1)

      await userEvent.click(screen.getByCSS('input'))
      await userEvent.tab()

      await expect.element(screen.getByCSS('input')).toHaveValue('5')
      expect(model.value).toBe(5)
    })

    it('should apply increments within the range', async () => {
      const model = ref(20)
      render(() =>
        <VNumberInput min={ 5 } max={ 15 } v-model={ model.value } />
      )

      await expect.element(screen.getByCSS('input')).toHaveValue('20')
      expect(model.value).toBe(20)

      await userEvent.click(screen.getByCSS('input'))
      await userEvent.keyboard('{arrowDown}')

      await expect.element(screen.getByCSS('input')).toHaveValue('14')
      expect(model.value).toBe(14)
    })

    it('should auto-correct when incrementing against the limit', async () => {
      const model = ref(-33)
      render(() =>
        <VNumberInput min={ -10 } max={ 20 } v-model={ model.value } precision={ 2 } step={ 0.5 } />
      )

      await expect.element(screen.getByCSS('input')).toHaveValue('-33.00')
      expect(model.value).toBe(-33)

      await userEvent.click(screen.getByCSS('input'))
      await userEvent.keyboard('{arrowDown}')

      await expect.element(screen.getByCSS('input')).toHaveValue('-10')
      expect(model.value).toBe(-10)
    })

    it('should have increment enabled when range is entirely negative', async () => {
      const model = ref(null)
      render(() =>
        <VNumberInput min={ -10 } max={ -2 } v-model={ model.value } />
      )

      expect(screen.getByTestId('increment')).toBeEnabled()
      await userEvent.click(screen.getByTestId('increment'))
      expect(model.value).toBe(-2)
    })

    it('should have decrement enabled when range is entirely positive', async () => {
      const model = ref(null)
      render(() =>
        <VNumberInput min={ 2 } max={ 10 } v-model={ model.value } />
      )

      expect(screen.getByTestId('decrement')).toBeEnabled()
      await userEvent.click(screen.getByTestId('decrement'))
      expect(model.value).toBe(2)
    })

    it('supports decimal step', async () => {
      const model = ref(0)
      render(() => (
        <VNumberInput
          step={ 0.03 }
          precision={ 2 }
          v-model={ model.value }
        />
      ))

      await userEvent.click(screen.getByTestId('increment'))
      await expect.element(screen.getByCSS('input')).toHaveValue('0.03')
      expect(model.value).toBe(0.03)

      await userEvent.click(screen.getByTestId('increment'))
      await expect.element(screen.getByCSS('input')).toHaveValue('0.06')
      expect(model.value).toBe(0.06)

      await userEvent.click(screen.getByTestId('decrement'))
      await expect.element(screen.getByCSS('input')).toHaveValue('0.03')
      expect(model.value).toBe(0.03)

      await userEvent.click(screen.getByTestId('decrement'))
      await expect.element(screen.getByCSS('input')).toHaveValue('0.00')
      expect(model.value).toBe(0)
    })

    it('shows custom decimal separator when incrementing', async () => {
      const model = ref(0)
      render(() => (
        <VNumberInput
          step={ 0.07 }
          precision={ 2 }
          decimalSeparator=","
          v-model={ model.value }
        />
      ))

      await userEvent.click(screen.getByTestId('increment'))
      await expect.element(screen.getByCSS('input')).toHaveValue('0,07')
      expect(model.value).toBe(0.07)

      await userEvent.click(screen.getByTestId('increment'))
      await expect.element(screen.getByCSS('input')).toHaveValue('0,14')
      expect(model.value).toBe(0.14)

      await userEvent.click(screen.getByTestId('decrement'))
      await expect.element(screen.getByCSS('input')).toHaveValue('0,07')
      expect(model.value).toBe(0.07)

      await userEvent.click(screen.getByTestId('decrement'))
      await expect.element(screen.getByCSS('input')).toHaveValue('0,00')
      expect(model.value).toBe(0)
    })
  })

  it('should not fire @update:focus twice when clicking bottom of input', async () => {
    const onFocus = vi.fn()
    const { element } = render(() => (
      <VNumberInput onUpdate:focused={ onFocus } />
    ))

    await userEvent.click(element, { position: { x: 10, y: 55 } })

    expect(onFocus).toHaveBeenCalledTimes(1)
  })

  describe('accepts digits from pasted text', () => {
    it.each([
      { precision: 0, text: '-00123', expected: -123 },
      { precision: 2, text: '.250', expected: 0.25 },
      { precision: 3, text: '000.321', expected: 0.321 },
      { precision: 0, text: '100.99', expected: 100 },
      { precision: 1, text: '200.99', expected: 200.9 },
      { precision: 2, text: ' 1,250.32\n', expected: 1250.32 },
      { precision: 0, text: '1\'024.00 meters', expected: 1024 },
      { precision: 0, text: '-1123.', expected: -1123 },
    ])('should parse numbers correctly', async ({ precision, text, expected }) => {
      const model = ref(null)
      const { element } = render(() => (
        <VNumberInput
          v-model={ model.value }
          precision={ precision }
        />
      ))
      const input = element.querySelector('input') as HTMLInputElement
      input.focus()
      const lock = await commands.getLock()
      await navigator.clipboard.writeText(text)
      await userEvent.paste()
      await commands.releaseLock(lock)
      expect(model.value).toBe(expected)
    })

    it.each([
      { sep: ',', precision: 0, text: '-00123', expected: -123 },
      { sep: ',', precision: 2, text: ',250', expected: 0.25 },
      { sep: ',', precision: 3, text: '000,321', expected: 0.321 },
      { sep: ',', precision: 0, text: '100,99', expected: 100 },
      { sep: ',', precision: 1, text: '200,99', expected: 200.9 },
      { sep: ',', precision: 2, text: ' 1,250.32\n', expected: 1.25 },
      { sep: ',', precision: 0, text: '1\'024.00 meters', expected: 102400 },
      { sep: ',', precision: 0, text: '- 1123.', expected: -1123 },
      { sep: ',', precision: 0, text: '- 32,', expected: -32 },
    ])('should parse numbers with custom separator', async ({ sep, precision, text, expected }) => {
      const model = ref(null)
      const { element } = render(() => (
        <VNumberInput
          v-model={ model.value }
          decimalSeparator={ sep }
          precision={ precision }
        />
      ))
      const input = element.querySelector('input') as HTMLInputElement
      input.focus()
      const lock = await commands.getLock()
      await navigator.clipboard.writeText(text)
      await userEvent.paste()
      await commands.releaseLock(lock)
      input.blur()
      expect(model.value).toBe(expected)
    })

    // https://github.com/vuetifyjs/vuetify/issues/21828
    it('should only trim values from the end', async () => {
      const model = ref(0.01)
      render(() => (
        <VNumberInput
          v-model={ model.value }
          minFractionDigits={ 0 }
          precision={ 4 }
        />
      ))

      await userEvent.click(screen.getByCSS('input'))
      expect(screen.getByCSS('input')).toHaveValue('0.01')

      await userEvent.keyboard('{backspace}2')
      await userEvent.tab()
      expect(screen.getByCSS('input')).toHaveValue('0.02')
      expect(model.value).toBe(0.02)
    })
  })

  describe('fraction digits control', () => {
    it.each([
      { precision: 2, minFractionDigits: null, typing: '.312', expected: '0.31' },
      { precision: 2, minFractionDigits: null, typing: '12.', expected: '12.00' },
      { precision: 0, minFractionDigits: 0, typing: '42', expected: '42' },
      { precision: 0, minFractionDigits: 1, typing: '-1.321', expected: '-1321' }, // dot is ignored while typing
      { precision: 0, minFractionDigits: 1, typing: '2', expected: '2' },
      { precision: 0, minFractionDigits: 3, typing: '31.9', expected: '319' }, // dot is ignored while typing
      { precision: 5, minFractionDigits: 3, typing: '-92.21', expected: '-92.210' },
      { precision: 5, minFractionDigits: 3, typing: '-92.2132', expected: '-92.2132' },
      { precision: 5, minFractionDigits: 3, typing: '-92.21325555', expected: '-92.21325' },
      { precision: null, minFractionDigits: 0, typing: '8', expected: '8' },
      { precision: null, minFractionDigits: 1, typing: '8', expected: '8.0' },
      { precision: null, minFractionDigits: 2, typing: '-1.5', expected: '-1.50' },
      { precision: null, minFractionDigits: 2, typing: '-1.521', expected: '-1.521' },
    ])('applies flexible limit on fraction digits', async ({ precision, minFractionDigits, typing, expected }) => {
      const { element } = render(() => <VNumberInput precision={ precision } minFractionDigits={ minFractionDigits } />)
      await userEvent.click(element)
      await userEvent.keyboard(typing)
      await userEvent.click(document.body)
      expect(screen.getByCSS('input')).toHaveValue(expected)
    })
  })

  describe('should indicate range error', () => {
    // enable in 4.0.0
    it.todo('on mount', async () => {
      const model = ref(-13)
      const onChange = vi.fn()
      render(() => (
        <VNumberInput
          v-model={ model.value }
          min={ 5 }
          onUpdate:modelValue={ onChange }
        />
      ))

      await nextTick()
      expect(model.value).toBe(-13)
      expect(onChange).not.toHaveBeenCalled()
      expect(screen.getByCSS('.v-input')).toHaveClass('v-input--error')
    })

    it('while typing', async () => {
      const model = ref(null)
      const onChange = vi.fn()
      render(() => (
        <VNumberInput
          v-model={ model.value }
          min={ 5 }
          onUpdate:modelValue={ onChange }
        />
      ))

      const vInput = screen.getByCSS('.v-input')

      await userEvent.tab()
      await userEvent.keyboard('1')
      expect(vInput).toHaveClass('v-input--error')

      await userEvent.keyboard('2')
      expect(vInput).not.toHaveClass('v-input--error')
      expect(model.value).toBe(12)

      await userEvent.keyboard('{arrowLeft}{arrowLeft}-')
      expect(vInput).toHaveClass('v-input--error')
    })

    it('on external change', async () => {
      const model = ref(0)
      const onChange = vi.fn()
      render(() => (
        <VNumberInput
          v-model={ model.value }
          max={ 50 }
          onUpdate:modelValue={ onChange }
        />
      ))

      await nextTick()
      const vInput = screen.getByCSS('.v-input')
      expect(vInput).not.toHaveClass('v-input--error')

      model.value = 50.55 // will be rounded to 51
      await nextTick()
      expect(vInput).toHaveClass('v-input--error')

      model.value = 99
      await nextTick()
      expect(vInput).toHaveClass('v-input--error')

      model.value = 45
      await nextTick()
      expect(vInput).not.toHaveClass('v-input--error')
    })
  })

  describe('propagates native change event', () => {
    it('on typing then blur', async () => {
      const onChange = vi.fn()
      render(() => <VNumberInput onChange={ onChange } />)

      await userEvent.click(screen.getByCSS('input'))
      await userEvent.keyboard('42')
      await userEvent.tab()
      expect(onChange).toHaveBeenCalledTimes(1)
    })

    it('on increment/decrement controls', async () => {
      const onChange = vi.fn()
      render(() => <VNumberInput modelValue={ 5 } onChange={ onChange } />)

      await userEvent.click(screen.getByTestId('increment'))
      expect(onChange).toHaveBeenCalledTimes(1)

      await userEvent.click(screen.getByTestId('decrement'))
      expect(onChange).toHaveBeenCalledTimes(2)
    })

    it('on arrow up/down keys', async () => {
      const onChange = vi.fn()
      render(() => <VNumberInput modelValue={ 5 } onChange={ onChange } />)

      await userEvent.click(screen.getByCSS('input'))
      await userEvent.keyboard('{ArrowUp}')
      expect(onChange).toHaveBeenCalledTimes(1)

      await userEvent.keyboard('{ArrowDown}')
      expect(onChange).toHaveBeenCalledTimes(2)
    })
  })
})
