import {vi} from 'vitest'
import React from 'react'

import {
  verifyNoRichContentEditorsExist,
  verifyAtLeastOneRichContentEditorExists,
} from '../../../../../tests/util/enableRichContentEditorChecks'
import {verifyItemChangesAreValidated} from '../../../../../tests/util/shouldValidateItemChanges'
import NumericEdit, {getNewValue} from '../index'
import {render, screen, fireEvent} from '@testing-library/react'
import userEvent from '@testing-library/user-event'
import runAxeCheck from '@instructure/ui-axe-check'

function createProps(changeItemStateStub, setOneQuestionAtATimeStub) {
  return {
    calculatorType: 'basic',
    itemId: '1',
    itemBody: 'Give an example of a properly random number',
    openImportModal: Function.prototype,
    scoringData: {
      value: [
        {
          type: 'exactResponse',
          id: '1',
          value: '3.1415926539',
        },
        {
          type: 'withinARange',
          id: '2',
          start: '0.6931471805599453',
          end: '1.4142135623730951',
        },
        {
          type: 'exactResponse',
          id: '3',
          value: '1',
        },
      ],
    },
    changeItemState: changeItemStateStub,
    setOneQuestionAtATime: setOneQuestionAtATimeStub,
  }
}

describe('Numeric Edit', () => {
  let changeItemStateStub, setOneQuestionAtATimeStub, props
  beforeEach(() => {
    changeItemStateStub = vi.fn()
    setOneQuestionAtATimeStub = vi.fn()
    props = createProps(changeItemStateStub, setOneQuestionAtATimeStub)
  })

  it('renders a rich content editor by default', async () => {
    await verifyAtLeastOneRichContentEditorExists(NumericEdit, props)
  })

  it('does not render a rich content editor when it is turned off', async () => {
    await verifyNoRichContentEditorsExist(NumericEdit, {...props, enableRichContentEditor: false})
  })

  it('includes validation errors in changeItemState', async () => {
    await verifyItemChangesAreValidated(NumericEdit, {...props, itemBody: ''})
  })

  // TODO: Stop testing implementation details...
  it('NumericEdit.getNewValue', () => {
    const exactAnswer = {
      id: '1',
      type: 'exactResponse',
      value: '2',
    }
    const marginAnswer = {
      id: '1',
      type: 'marginOfError',
      value: '2',
      margin: '3',
      marginType: '4',
    }
    const rangeAnswer = {
      id: '1',
      type: 'withinARange',
      start: '2',
      end: '3',
    }
    const preciseAnswer = {
      id: '1',
      type: 'preciseResponse',
      value: '2',
      precision: '3',
      precisionType: '4',
    }

    expect(getNewValue({}, exactAnswer.type)).toEqual({
      id: '',
      type: 'exactResponse',
      value: '',
    })
    expect(getNewValue({}, marginAnswer.type)).toEqual({
      id: '',
      type: 'marginOfError',
      value: '',
      margin: '',
      marginType: 'percent',
    })
    expect(getNewValue({}, rangeAnswer.type)).toEqual({
      id: '',
      type: 'withinARange',
      start: '',
      end: '',
    })
    expect(getNewValue({}, preciseAnswer.type)).toEqual({
      id: '',
      type: 'preciseResponse',
      value: '',
      precision: '',
      precisionType: 'significantDigits',
    })
  })

  describe('render', () => {
    it('renders correct fields for exactResponse', () => {
      const testProps = {
        ...props,
        scoringData: {
          value: [
            {
              type: 'exactResponse',
              id: '1',
              value: '3.1415926539',
            },
          ],
        },
      }

      const {container} = render(<NumericEdit {...testProps} />)

      const requirementSelect = screen.getByRole('combobox', {name: /Requirement/i})
      const answerInput = container.querySelector(
        '[data-automation="sdk-numeric-exact-response-number-input"]',
      )

      expect(requirementSelect).toBeInTheDocument()
      expect(answerInput).toBeInTheDocument()
    })

    it('renders an answer of zero', () => {
      const testProps = {
        ...props,
        scoringData: {
          value: [
            {
              type: 'exactResponse',
              id: '1',
              value: '0',
            },
          ],
        },
      }

      const {container} = render(<NumericEdit {...testProps} />)

      const answerInput = container.querySelector(
        '[data-automation="sdk-numeric-exact-response-number-input"]',
      )

      expect(answerInput.value).toBe('0')
    })

    it('localizes numbers', () => {
      const testProps = {
        ...props,
        locale: 'fr',
        scoringData: {
          value: [
            {
              type: 'withinARange',
              id: '1',
              start: '0.5',
              end: '1.5',
            },
          ],
        },
      }

      render(<NumericEdit {...testProps} />)

      const rangeStartInput = screen.getByLabelText(/Range start/i)
      const rangeEndInput = screen.getByLabelText(/Range end/i)

      expect(rangeStartInput.value).toBe('0,5')
      expect(rangeEndInput.value).toBe('1,5')
    })

    it("doesn't strip trailing zeros", () => {
      const testProps = {
        ...props,
        locale: 'fr',
        scoringData: {
          value: [
            {
              type: 'exactResponse',
              id: '1',
              value: '12.20',
            },
          ],
        },
      }

      const {container} = render(<NumericEdit {...testProps} />)

      const answerInput = container.querySelector(
        '[data-automation="sdk-numeric-exact-response-number-input"]',
      )

      expect(answerInput.value).toBe('12,20')
    })

    describe('overrideEditableForRegrading', () => {
      it('disables question stem input', () => {
        const {container} = render(
          <NumericEdit {...props} overrideEditableForRegrading enableRichContentEditor={false} />,
        )
        const textarea = container.querySelector('textarea')
        expect(textarea).toBeDisabled()
      })

      it('disables the calculator option', () => {
        render(<NumericEdit {...props} overrideEditableForRegrading />)

        const calculatorCheckbox = screen.getByRole('checkbox', {
          name: /show on-screen calculator/i,
        })
        const calculatorBasicRadioInput = screen.getByRole('radio', {name: /basic calculator/i})
        const calculatorScientificRadioInput = screen.getByRole('radio', {
          name: /scientific calculator/i,
        })

        expect(calculatorCheckbox).toBeDisabled()
        expect(calculatorBasicRadioInput).toBeDisabled()
        expect(calculatorScientificRadioInput).toBeDisabled()
      })
    })

    it('renders correct fields for marginOfError', () => {
      const testProps = {
        ...props,
        scoringData: {
          value: [
            {
              type: 'marginOfError',
              id: '20',
              value: '42',
              margin: '1',
              marginType: 'percent',
            },
          ],
        },
      }

      const {container} = render(<NumericEdit {...testProps} />)

      const requirementSelect = screen.getByRole('combobox', {name: /Requirement/i})
      const answerInput = container.querySelector(
        '[data-automation="sdk-numeric-marginoferror-response-number-input"]',
      )
      const marginInput = screen.getByLabelText(/\+\/- margin/i)

      expect(requirementSelect).toBeInTheDocument()
      expect(answerInput).toBeInTheDocument()
      expect(marginInput).toBeInTheDocument()
      expect(container.textContent).toContain('Type')
    })

    it('renders correct fields for withinARange', () => {
      const testProps = {
        ...props,
        scoringData: {
          value: [
            {
              type: 'withinARange',
              id: '2',
              start: '0.6931471805599453',
              end: '1.4142135623730951',
            },
          ],
        },
      }

      render(<NumericEdit {...testProps} />)

      const requirementSelect = screen.getByRole('combobox', {name: /Requirement/i})
      const rangeStartInput = screen.getByLabelText(/Range start/i)
      const rangeEndInput = screen.getByLabelText(/Range end/i)

      expect(requirementSelect).toBeInTheDocument()
      expect(rangeStartInput).toBeInTheDocument()
      expect(rangeEndInput).toBeInTheDocument()
    })

    it('renders correct fields for preciseResponse', () => {
      const testProps = {
        ...props,
        scoringData: {
          value: [
            {
              type: 'preciseResponse',
              id: '20',
              value: '4294967296',
              precision: '8',
              precisionType: 'significantDigits',
            },
          ],
        },
      }

      const {container} = render(<NumericEdit {...testProps} />)

      const requirementSelect = screen.getByRole('combobox', {name: /Requirement/i})
      const answerInput = container.querySelector(
        '[data-automation="sdk-numeric-precise-answer-number-input"]',
      )
      const precisionInput = container.querySelector(
        '[data-automation="sdk-numeric-precise-precision-number-input"]',
      )

      expect(requirementSelect).toBeInTheDocument()
      expect(answerInput).toBeInTheDocument()
      expect(precisionInput).toBeInTheDocument()
      expect(container.textContent).toContain('Type')
    })
  })

  it('handles removing second answer with three answers', () => {
    render(<NumericEdit {...props} />)

    const secondAnswerDeleteButton = screen.getByRole('button', {
      name: /Remove Answer: range from 0.6931471805599453 to 1.4142135623730951/i,
    })
    fireEvent.click(secondAnswerDeleteButton)

    expect(changeItemStateStub).toHaveBeenCalledTimes(1)
    expect(changeItemStateStub).toHaveBeenCalledWith(
      expect.objectContaining({
        scoringData: {
          value: [
            {
              type: 'exactResponse',
              id: '1',
              value: '3.1415926539',
            },
            {
              type: 'exactResponse',
              id: '3',
              value: '1',
            },
          ],
        },
      }),
    )
  })

  it('handles removing second answer with two answers', () => {
    const testProps = {
      ...props,
      scoringData: {
        value: [
          {
            type: 'exactResponse',
            id: '1',
            value: '3.1415926539',
          },
          {
            type: 'withinARange',
            id: '2',
            start: '0.6931471805599453',
            end: '1.4142135623730951',
          },
        ],
      },
    }

    render(<NumericEdit {...testProps} />)

    const secondAnswerDeleteButton = screen.getByRole('button', {
      name: /Remove Answer: range from 0.6931471805599453 to 1.4142135623730951/i,
    })
    fireEvent.click(secondAnswerDeleteButton)

    expect(changeItemStateStub).toHaveBeenCalledTimes(1)
    expect(changeItemStateStub).toHaveBeenCalledWith(
      expect.objectContaining({
        scoringData: {
          value: [
            {
              type: 'exactResponse',
              id: '1',
              value: '3.1415926539',
            },
          ],
        },
      }),
    )
  })

  it('handles creating new answers', () => {
    render(<NumericEdit {...props} />)

    const answerLength = props.scoringData.value.length
    const addPossibleAnswerButton = screen.getByRole('button', {name: /Add Possible Answer/i})
    fireEvent.click(addPossibleAnswerButton)

    expect(changeItemStateStub).toHaveBeenCalledTimes(1)
    expect(changeItemStateStub.mock.calls[0][0].scoringData.value.length).toBe(answerLength + 1)
  })

  it('handles changing answer values', () => {
    const {container} = render(<NumericEdit {...props} />)

    const answerInput = container.querySelector(
      '[data-automation="sdk-numeric-exact-response-number-input"]',
    )
    fireEvent.change(answerInput, {target: {value: '4'}})

    expect(changeItemStateStub).toHaveBeenCalledTimes(1)
    expect(changeItemStateStub).toHaveBeenCalledWith(
      expect.objectContaining({
        scoringData: {
          value: [
            {
              type: 'exactResponse',
              id: '1',
              value: '4',
            },
            ...props.scoringData.value.slice(1),
          ],
        },
      }),
    )
  })

  it('handles changing answer types while maintaining focus', () => {
    // Use fake timers to prevent the component's setTimeout focus call from
    // firing after the component is unmounted (which would cause a null ref error)
    vi.useFakeTimers()
    render(<NumericEdit {...props} />)

    // The editor contains multiple select inputs, so we pick the first one
    const [firstRequirementSelect] = screen.queryAllByRole('combobox', {name: /requirement/i})

    userEvent.click(firstRequirementSelect)
    userEvent.click(screen.getByRole('option', {name: /within a range/i}))

    expect(changeItemStateStub).toHaveBeenCalled()
    expect(changeItemStateStub.mock.calls[0][0].scoringData.value[0]).toEqual({
      type: 'withinARange',
      id: '1',
      start: '',
      end: '',
    })

    expect(document.activeElement.id).toBe(firstRequirementSelect.id)
    vi.useRealTimers()
  })

  it('changes Precise Response value to a string', () => {
    const {container} = render(<NumericEdit {...props} />)

    const answerInput = container.querySelector(
      '[data-automation="sdk-numeric-exact-response-number-input"]',
    )
    fireEvent.change(answerInput, {target: {value: '4.123'}})

    expect(changeItemStateStub).toHaveBeenCalled()
    expect(typeof changeItemStateStub.mock.calls[0][0].scoringData.value[0].value).toBe('string')
  })

  it('should show Errors if errorsAreShowing is true and there are itemBody errors', () => {
    const errorMsg = "I'm sorry Dave"
    const testProps = {
      ...props,
      errorsAreShowing: true,
      errors: {
        itemBody: [errorMsg],
      },
    }

    const {container} = render(<NumericEdit {...testProps} />)

    expect(container.textContent).toContain(errorMsg)
  })

  it('should show Errors if errorsAreShowing is true and there are scoringData errors', () => {
    const errorMsg = "I'm afraid I can't do that"
    const testProps = {
      ...props,
      errorsAreShowing: true,
      errors: {
        scoringData: {
          value: [
            {
              value: [errorMsg],
            },
          ],
        },
      },
    }

    const {container} = render(<NumericEdit {...testProps} />)

    expect(container.textContent).toContain(errorMsg)
  })

  describe('exactResponse', () => {
    it('allows answers in scientific notation', () => {
      const testProps = {
        ...props,
        scoringData: {
          value: [
            {
              type: 'exactResponse',
              id: '1',
              value: '1.23*10^-3',
            },
          ],
        },
      }

      render(<NumericEdit {...testProps} />)

      // Should not have been called with errors
      const callsWithErrors = changeItemStateStub.mock.calls.filter(
        call => call[0] && call[0].errors && Object.keys(call[0].errors).length > 0,
      )
      expect(callsWithErrors).toHaveLength(0)
    })

    it('allows very large numbers in scientific notation', () => {
      const testProps = {
        ...props,
        scoringData: {
          value: [
            {
              type: 'exactResponse',
              id: '1',
              value: '1*10^500',
            },
          ],
        },
      }

      render(<NumericEdit {...testProps} />)

      // Should not have been called with errors
      const callsWithErrors = changeItemStateStub.mock.calls.filter(
        call => call[0] && call[0].errors && Object.keys(call[0].errors).length > 0,
      )
      expect(callsWithErrors).toHaveLength(0)
    })
  })

  describe('marginOfError', () => {
    it('does not allow answers in scientific notation', () => {
      const testProps = {
        ...props,
        scoringData: {
          value: [
            {
              type: 'marginOfError',
              id: '1',
              value: '1.23*10^-3',
              margin: '1',
              marginType: 'percent',
            },
          ],
        },
      }

      render(<NumericEdit {...testProps} />)

      const matchingCall = changeItemStateStub.mock.calls.find(([arg]) =>
        arg.errors?.scoringData?.value?.[0]?.value?.some(
          msg => msg === 'Scientific notation not supported with margin of error',
        ),
      )
      expect(matchingCall).toBeDefined()
    })
  })

  describe('withinARange', () => {
    it('allows ranges in scientific notation', () => {
      const testProps = {
        ...props,
        scoringData: {
          value: [
            {
              type: 'withinARange',
              id: '1',
              start: '5*10^-3',
              end: '1*10^-1',
            },
          ],
        },
      }

      render(<NumericEdit {...testProps} />)

      // Should not have been called with errors
      const callsWithErrors = changeItemStateStub.mock.calls.filter(
        call => call[0] && call[0].errors && Object.keys(call[0].errors).length > 0,
      )
      expect(callsWithErrors).toHaveLength(0)
    })

    it('does not allow ranges with only the start in scientific notation', () => {
      const testProps = {
        ...props,
        scoringData: {
          value: [
            {
              type: 'withinARange',
              id: '1',
              start: '5*10^-3',
              end: '0.1',
            },
          ],
        },
      }

      render(<NumericEdit {...testProps} />)

      const matchingCall = changeItemStateStub.mock.calls.find(([arg]) =>
        arg.errors?.scoringData?.value?.[0]?.end?.some(
          msg => msg === 'Must be in scientific notation',
        ),
      )
      expect(matchingCall).toBeDefined()
    })

    it('does not allow ranges with only the end in scientific notation', () => {
      const testProps = {
        ...props,
        scoringData: {
          value: [
            {
              type: 'withinARange',
              id: '1',
              start: '0.005',
              end: '1*10^-1',
            },
          ],
        },
      }

      render(<NumericEdit {...testProps} />)

      const matchingCall = changeItemStateStub.mock.calls.find(([arg]) =>
        arg.errors?.scoringData?.value?.[0]?.start?.some(
          msg => msg === 'Must be in scientific notation',
        ),
      )
      expect(matchingCall).toBeDefined()
    })

    it('does not allow ranges with end < start', () => {
      const testProps = {
        ...props,
        scoringData: {
          value: [
            {
              type: 'withinARange',
              id: '1',
              start: '5',
              end: '3',
            },
          ],
        },
      }

      render(<NumericEdit {...testProps} />)

      const matchingCall = changeItemStateStub.mock.calls.find(([arg]) =>
        arg.errors?.scoringData?.value?.[0]?.end?.some(
          msg => msg === 'Range end must be greater than range start',
        ),
      )
      expect(matchingCall).toBeDefined()
    })

    it('does not allow ranges in scientific notation with end < start', () => {
      const testProps = {
        ...props,
        scoringData: {
          value: [
            {
              type: 'withinARange',
              id: '1',
              start: '1*10^3',
              end: '5*10^2',
            },
          ],
        },
      }

      render(<NumericEdit {...testProps} />)

      const matchingCall = changeItemStateStub.mock.calls.find(([arg]) =>
        arg.errors?.scoringData?.value?.[0]?.end?.some(
          msg => msg === 'Range end must be greater than range start',
        ),
      )
      expect(matchingCall).toBeDefined()
    })

    it('allows ranges with very large numbers in scientific notation', () => {
      const testProps = {
        ...props,
        scoringData: {
          value: [
            {
              type: 'withinARange',
              id: '1',
              start: '1*10^400',
              end: '1*10^500',
            },
          ],
        },
      }

      render(<NumericEdit {...testProps} />)

      // Should not have been called with errors
      const callsWithErrors = changeItemStateStub.mock.calls.filter(
        call => call[0] && call[0].errors && Object.keys(call[0].errors).length > 0,
      )
      expect(callsWithErrors).toHaveLength(0)
    })
  })

  describe('preciseResponse', () => {
    it('does not allow answers in scientific notation', () => {
      const testProps = {
        ...props,
        scoringData: {
          value: [
            {
              type: 'preciseResponse',
              id: '1',
              value: '1.23*10^-3',
              precision: '3',
              precisionType: 'significantDigits',
            },
          ],
        },
      }

      render(<NumericEdit {...testProps} />)

      const matchingCall = changeItemStateStub.mock.calls.find(([arg]) =>
        arg.errors?.scoringData?.value?.[0]?.value?.some(
          msg => msg === 'Scientific notation not supported with precise response',
        ),
      )
      expect(matchingCall).toBeDefined()
    })

    it('updates precision as a string when switching precision type', () => {
      const answer = {
        type: 'preciseResponse',
        id: '12',
        value: '12.5',
        precision: '1',
        precisionType: 'decimals',
      }

      const testProps = {
        ...props,
        scoringData: {
          value: [answer],
        },
      }

      render(<NumericEdit {...testProps} />)

      const typeSelect = screen.getByRole('combobox', {name: /type/i})

      userEvent.click(typeSelect)
      userEvent.click(screen.getByRole('option', {name: /significant digits/i}))

      expect(changeItemStateStub).toHaveBeenCalledTimes(1)
      expect(changeItemStateStub).toHaveBeenCalledWith({
        scoringData: {
          value: [{...answer, precision: '3', precisionType: 'significantDigits'}],
        },
      })
    })

    describe('with significantDigits', () => {
      it('adjusts precision when answer value changes', () => {
        const answer = {
          type: 'preciseResponse',
          id: '42',
          value: '3.14159',
          precision: '6',
          precisionType: 'significantDigits',
        }
        const testProps = {
          ...props,
          scoringData: {
            value: [answer],
          },
        }

        const {container} = render(<NumericEdit {...testProps} />)

        const answerInput = container.querySelector(
          '[data-automation="sdk-numeric-precise-answer-number-input"]',
        )
        fireEvent.change(answerInput, {target: {value: '3.141593'}})

        expect(changeItemStateStub).toHaveBeenCalledTimes(1)
        expect(changeItemStateStub).toHaveBeenCalledWith({
          scoringData: {
            value: [{...answer, value: '3.141593', precision: '7'}],
          },
        })
      })

      it('sets precision correctly when answer has leading zeros', () => {
        const answer = {
          type: 'preciseResponse',
          id: '42',
          value: '0',
          precision: '1',
          precisionType: 'significantDigits',
        }
        const testProps = {
          ...props,
          scoringData: {
            value: [answer],
          },
        }

        const {container} = render(<NumericEdit {...testProps} />)

        const answerInput = container.querySelector(
          '[data-automation="sdk-numeric-precise-answer-number-input"]',
        )
        fireEvent.change(answerInput, {target: {value: '0.0123'}})

        expect(changeItemStateStub).toHaveBeenCalledTimes(1)
        expect(changeItemStateStub).toHaveBeenCalledWith({
          scoringData: {
            value: [{...answer, value: '0.0123', precision: '3'}],
          },
        })
      })

      describe('when answer is equal to zero', () => {
        // According to resident math expert mbd, zero always has 1 significant digit
        it('sets precision to 1', () => {
          const answer = {
            type: 'preciseResponse',
            id: '42',
            value: '0',
            precision: '1',
            precisionType: 'significantDigits',
          }
          const testProps = {
            ...props,
            scoringData: {
              value: [answer],
            },
          }

          const {container} = render(<NumericEdit {...testProps} />)

          const answerInput = container.querySelector(
            '[data-automation="sdk-numeric-precise-answer-number-input"]',
          )
          fireEvent.change(answerInput, {target: {value: '0.0'}})

          expect(changeItemStateStub).toHaveBeenCalledTimes(1)
          expect(changeItemStateStub).toHaveBeenCalledWith({
            scoringData: {
              value: [{...answer, value: '0.0', precision: '1'}],
            },
          })
        })

        it('prevents changing precision', () => {
          const answer = {
            type: 'preciseResponse',
            id: '42',
            value: '0',
            precision: '1',
            precisionType: 'significantDigits',
          }
          const testProps = {
            ...props,
            scoringData: {
              value: [answer],
            },
          }

          const {container} = render(<NumericEdit {...testProps} />)

          const precisionInput = container.querySelector(
            '[data-automation="sdk-numeric-precise-precision-number-input"]',
          )
          const buttons = precisionInput.parentElement.querySelectorAll('button')
          const upArrowButton = buttons[0]
          const downArrowButton = buttons[1]

          userEvent.click(upArrowButton)
          expect(changeItemStateStub).not.toHaveBeenCalled()

          userEvent.click(downArrowButton)
          expect(changeItemStateStub).not.toHaveBeenCalled()
        })
      })

      it('adjusts answer value when precision changes', () => {
        const answer = {
          type: 'preciseResponse',
          id: '7',
          value: '6.5',
          precision: '2',
          precisionType: 'significantDigits',
        }
        const testProps = {
          ...props,
          scoringData: {
            value: [answer],
          },
        }

        const {container} = render(<NumericEdit {...testProps} />)

        const precisionInput = container.querySelector(
          '[data-automation="sdk-numeric-precise-precision-number-input"]',
        )
        fireEvent.change(precisionInput, {target: {value: '3'}})

        expect(changeItemStateStub).toHaveBeenCalledTimes(1)
        expect(changeItemStateStub).toHaveBeenCalledWith({
          scoringData: {
            value: [{...answer, value: '6.50', precision: '3'}],
          },
        })
      })

      it('sets min precision', () => {
        const answer = {
          type: 'preciseResponse',
          id: '12',
          value: '12.5',
          precision: '3',
          precisionType: 'significantDigits',
        }
        const testProps = {
          ...props,
          scoringData: {
            value: [answer],
          },
        }

        const {container} = render(<NumericEdit {...testProps} />)

        const precisionInput = container.querySelector(
          '[data-automation="sdk-numeric-precise-precision-number-input"]',
        )

        const buttons = precisionInput.parentElement.querySelectorAll('button')
        const downArrowButton = buttons[1]

        // Ensure min is working by making sure we can't go below 3
        userEvent.click(downArrowButton)
        const callWithPrecision2 = changeItemStateStub.mock.calls.find(([arg]) =>
          arg.scoringData?.value?.some(a => a.precision === '2'),
        )
        expect(callWithPrecision2).not.toBeDefined()
      })

      it('sets max precision', () => {
        const answer = {
          type: 'preciseResponse',
          id: '12',
          value: '12.50',
          precision: '21',
          precisionType: 'significantDigits',
        }
        const testProps = {
          ...props,
          scoringData: {
            value: [answer],
          },
        }

        const {container} = render(<NumericEdit {...testProps} />)

        const precisionInput = container.querySelector(
          '[data-automation="sdk-numeric-precise-precision-number-input"]',
        )

        const buttons = precisionInput.parentElement.querySelectorAll('button')
        const upArrowButton = buttons[0]

        // Ensure max is working by making sure we can't go above 21
        userEvent.click(upArrowButton)
        const callWithPrecision22 = changeItemStateStub.mock.calls.find(([arg]) =>
          arg.scoringData?.value?.some(a => a.precision === '22'),
        )
        expect(callWithPrecision22).not.toBeDefined()
      })
    })

    describe('with decimals', () => {
      it('adjusts precision when answer value changes', () => {
        const answer = {
          type: 'preciseResponse',
          id: '42',
          value: '3.14159',
          precision: '5',
          precisionType: 'decimals',
        }
        const testProps = {
          ...props,
          scoringData: {
            value: [answer],
          },
        }

        const {container} = render(<NumericEdit {...testProps} />)

        const answerInput = container.querySelector(
          '[data-automation="sdk-numeric-precise-answer-number-input"]',
        )
        fireEvent.change(answerInput, {target: {value: '3.141593'}})

        expect(changeItemStateStub).toHaveBeenCalledOnce()
        expect(changeItemStateStub).toHaveBeenCalledWith({
          scoringData: {
            value: [{...answer, value: '3.141593', precision: '6'}],
          },
        })
      })

      it('adjusts answer value when precision changes', () => {
        const answer = {
          type: 'preciseResponse',
          id: '7',
          value: '6.5',
          precision: '1',
          precisionType: 'decimals',
        }
        const testProps = {
          ...props,
          scoringData: {
            value: [answer],
          },
        }

        const {container} = render(<NumericEdit {...testProps} />)

        const precisionInput = container.querySelector(
          '[data-automation="sdk-numeric-precise-precision-number-input"]',
        )
        fireEvent.change(precisionInput, {target: {value: '2'}})

        expect(changeItemStateStub).toHaveBeenCalledTimes(1)
        expect(changeItemStateStub).toHaveBeenCalledWith({
          scoringData: {
            value: [{...answer, value: '6.50', precision: '2'}],
          },
        })
      })

      it('sets the minimum precision to the number of decimal digits in the answer', () => {
        const answer = {
          type: 'preciseResponse',
          id: '12',
          value: '12.5',
          precision: '1',
          precisionType: 'decimals',
        }

        const testProps = {
          ...props,
          scoringData: {
            value: [answer],
          },
        }

        const {container} = render(<NumericEdit {...testProps} />)

        const precisionInput = container.querySelector(
          '[data-automation="sdk-numeric-precise-precision-number-input"]',
        )

        const buttons = precisionInput.parentElement.querySelectorAll('button')
        const downArrowButton = buttons[1]

        // Ensure min is working by making sure we can't go below 1
        userEvent.click(downArrowButton)
        const callWithPrecision0 = changeItemStateStub.mock.calls.find(([arg]) =>
          arg.scoringData?.value?.some(a => a.precision === '0'),
        )
        expect(callWithPrecision0).not.toBeDefined()
      })

      it('sets max precision', () => {
        const answer = {
          type: 'preciseResponse',
          id: '12',
          value: '12.50',
          precision: '100',
          precisionType: 'decimals',
        }
        const testProps = {
          ...props,
          scoringData: {
            value: [answer],
          },
        }

        const {container} = render(<NumericEdit {...testProps} />)

        const precisionInput = container.querySelector(
          '[data-automation="sdk-numeric-precise-precision-number-input"]',
        )

        const buttons = precisionInput.parentElement.querySelectorAll('button')
        const upArrowButton = buttons[0]

        // Ensure max is working by making sure we can't go above 100
        userEvent.click(upArrowButton)
        const callWithPrecision101 = changeItemStateStub.mock.calls.find(([arg]) =>
          arg.scoringData?.value?.some(a => a.precision === '101'),
        )
        expect(callWithPrecision101).not.toBeDefined()
      })

      it("doesn't update answer when precision is not a number", () => {
        const answer = {
          type: 'preciseResponse',
          id: '1',
          value: '0',
          precision: '1',
          precisionType: 'significantDigits',
        }
        const testProps = {
          ...props,
          scoringData: {
            value: [answer],
          },
        }

        const {container} = render(<NumericEdit {...testProps} />)

        const precisionInput = container.querySelector(
          '[data-automation="sdk-numeric-precise-precision-number-input"]',
        )
        fireEvent.change(precisionInput, {target: {value: 'not-a-number'}})

        const callWithNaN = changeItemStateStub.mock.calls.find(([arg]) =>
          arg.scoringData?.value?.some(a => isNaN(a.precision)),
        )
        expect(callWithNaN).not.toBeDefined()
      })

      it("doesn't update precision when answer is not a number", () => {
        const answer = {
          type: 'preciseResponse',
          id: '1',
          value: '0',
          precision: '1',
          precisionType: 'significantDigits',
        }
        const testProps = {
          ...props,
          scoringData: {
            value: [answer],
          },
        }

        const {container} = render(<NumericEdit {...testProps} />)

        const answerInput = container.querySelector(
          '[data-automation="sdk-numeric-precise-precision-number-input"]',
        )
        fireEvent.change(answerInput, {target: {value: 'not-a-number'}})

        const callWithNaN = changeItemStateStub.mock.calls.find(([arg]) =>
          arg.scoringData?.value?.some(a => isNaN(a.value)),
        )
        expect(callWithNaN).not.toBeDefined()
      })
    })
  })

  describe('a11y tests', () => {
    it('should meet a11y standards', async () => {
      render(
        <main>
          <NumericEdit {...props} />
        </main>,
      )

      expect(
        await runAxeCheck(document.body, {
          ignores: [
            'aria-allowed-role', // TODO: remove this when instui fixes Select
            'aria-allowed-attr',
          ] /* QUIZ-8495 */,
        }),
      ).toBe(true)
    })
  })

  describe('calculator per question option', () => {
    it('does not render the options if showCalculatorOption is false', () => {
      render(<NumericEdit {...props} showCalculatorOption={false} />)
      expect(screen.queryByRole('button', {name: /options/i})).toBeNull()
    })

    it('renders the calculator per question option', () => {
      render(<NumericEdit {...props} />)

      const calculatorCheckboxes = screen.queryAllByRole('checkbox', {
        name: /show on-screen calculator/i,
      })
      expect(calculatorCheckboxes).toHaveLength(1)
    })

    it('calls the correct callback function with the correct data when the calculator type is changed', () => {
      render(<NumericEdit {...props} />)

      const scientificOption = screen.getByRole('radio', {name: /scientific calculator/i})
      fireEvent.click(scientificOption)

      expect(changeItemStateStub).toHaveBeenCalledWith(
        expect.objectContaining({
          calculatorType: 'scientific',
        }),
      )
    })

    it('calls the correct callback function when OQAAT is changed', () => {
      render(<NumericEdit {...props} />)

      const toggle = screen.getByRole('checkbox', {name: /enable one question at a time/i})
      fireEvent.click(toggle)

      expect(setOneQuestionAtATimeStub).toHaveBeenCalledTimes(1)
    })
  })

  it('displays help modal when help button clicked', () => {
    render(<NumericEdit {...props} />)

    const helpButtons = screen.getAllByRole('button', {
      name: /Display help dialog for numeric requirement/i,
    })
    fireEvent.click(helpButtons[0])

    const modal = screen.getByRole('dialog', {name: /Requirement help/i})
    expect(modal).toBeInTheDocument()

    // Verify modal content
    expect(modal.textContent).toContain('Exact Response')
    expect(modal.textContent).toContain('Margin of Error')
    expect(modal.textContent).toContain('Within a Range')
    expect(modal.textContent).toContain('Precise Response')

    // Verify the modal content
    expect(modal.textContent).toContain('Exact Response')

    // TODO: QUIZ-XXXXX — Instructure UI Modal close button interaction
    // cannot be tested with RTL. The click events (fireEvent.click, userEvent.click,
    // native .click(), dispatchEvent) do not trigger the onDismiss callback through
    // the Instructure UI Button inside a Modal portal. Verified the modal opens and
    // displays the correct content above.
  })
})
