import {vi} from 'vitest'
import React from 'react'
import update from 'immutability-helper'

import FormulaEdit from '../index'

import {
  isScientificNotation,
  parseScientificNotation,
  scientificNotationToNumber,
} from '@instructure/quiz-scientific-notation'
import {
  verifyAtLeastOneRichContentEditorExists,
  verifyNoRichContentEditorsExist,
} from '../../../../../tests/util/enableRichContentEditorChecks'
import {verifyItemChangesAreValidated} from '../../../../../tests/util/shouldValidateItemChanges'
import {render, screen, waitFor, fireEvent} from '@testing-library/react'
import userEvent from '@testing-library/user-event'
import runAxeCheck from '@instructure/ui-axe-check'
import {loadMathjs} from '../../common/util'

// Mock RichContentInput so we can trigger onChange in tests
vi.mock('@instructure/quiz-rce/components/RichContentInput/index', async importOriginal => {
  const actual = await importOriginal()
  const React = await import('react')
  return {
    ...actual,
    RichContentInput: function MockRCE({
      onChange,
      defaultContent,
      disabled,
      readOnly,
      messages,
      ...rest
    }) {
      return React.createElement(
        'div',
        {'data-automation': 'sdk-rce-question-stem'},
        React.createElement('textarea', {
          defaultValue: defaultContent || '',
          disabled: disabled || readOnly || false,
          readOnly: readOnly || false,
          onChange: e => onChange && onChange({}, {editorContent: e.target.value}),
        }),
        messages &&
          // eslint-disable-next-line react/no-array-index-key
          messages.map((msg, i) => React.createElement('span', {key: i}, msg.text || msg)),
      )
    },
  }
})

// Pre-load mathjs so withAsyncDeps doesn't render a loading spinner
beforeAll(async () => {
  await loadMathjs()
})

function createProps(
  changeItemStateStub,
  openImportModalStub,
  notifyScreenReaderStub,
  setOneQuestionAtATimeStub,
) {
  return {
    calculatorType: 'basic',
    changeItemState: changeItemStateStub,
    errorsAreShowing: false,
    interactionData: {},
    itemBody: 'find `x` + `y`',
    openImportModal: openImportModalStub,
    notifyScreenreader: notifyScreenReaderStub,
    setOneQuestionAtATime: setOneQuestionAtATimeStub,
    scoringData: {
      value: {
        answerCount: '2',
        answerPrecision: 3,
        formula: 'x+y',
        numeric: {
          type: 'marginOfError',
          margin: '0',
          marginType: 'absolute',
        },
        generatedSolutions: [
          {
            output: 3,
            inputs: [
              {name: 'x', value: 1},
              {name: 'y', value: 2},
            ],
          },
          {
            output: 10,
            inputs: [
              {name: 'x', value: 3},
              {name: 'y', value: 7},
            ],
          },
        ],
        variables: [
          {name: 'x', min: 0, max: 5, precision: 0},
          {name: 'y', min: 0, max: 5, precision: 0},
        ],
      },
    },
  }
}

const assertGeneratedSolutionsCleared = changeItemStateStub => {
  const arg = changeItemStateStub.mock.lastCall[0]
  expect(arg.scoringData.value.generatedSolutions).toEqual([])
}

describe('***** Formula edit old tests *****', () => {
  const changeItemStateStub = vi.fn()
  const openImportModalStub = vi.fn()
  const notifyScreenReaderStub = vi.fn()
  const setOneQuestionAtATimeStub = vi.fn()
  const defaultProps = createProps(
    changeItemStateStub,
    openImportModalStub,
    notifyScreenReaderStub,
    setOneQuestionAtATimeStub,
  )

  afterEach(() => {
    changeItemStateStub.mockClear()
    openImportModalStub.mockClear()
    notifyScreenReaderStub.mockClear()
  })

  describe('override editable for regrading', () => {
    it('renders a disabled stem', () => {
      const {container} = render(<FormulaEdit {...defaultProps} overrideEditableForRegrading />)
      const rceTextarea = container.querySelector(
        '[data-automation="sdk-rce-question-stem"] textarea',
      )
      expect(rceTextarea.disabled).toBe(true)
    })
  })

  describe('changing itemBody', () => {
    const runTest = newItemBody => {
      const {container} = render(<FormulaEdit {...defaultProps} />)
      const rceTextarea = container.querySelector(
        '[data-automation="sdk-rce-question-stem"] textarea',
      )
      fireEvent.change(rceTextarea, {target: {value: newItemBody}})
    }

    it('updates when only text changes', () => {
      runTest('find `x` + `y`!!!')

      const arg = changeItemStateStub.mock.lastCall[0]
      expect(arg.itemBody).toBe('find `x` + `y`!!!')
    })

    it('clears the generated solutions', () => {
      runTest('find `x` + `y`!!!')
      assertGeneratedSolutionsCleared(changeItemStateStub)
    })

    it('updates when a new variable is added', () => {
      runTest('find `x` + `y` + `a`')

      const arg = changeItemStateStub.mock.lastCall[0]
      expect(arg.itemBody).toBe('find `x` + `y` + `a`')
      expect(arg.scoringData.value.variables).toEqual([
        {name: 'a', min: 0, max: 10, precision: 0},
        {name: 'x', min: 0, max: 5, precision: 0},
        {name: 'y', min: 0, max: 5, precision: 0},
      ])
    })

    it('updates when a variable is removed', () => {
      runTest('find [x + `y`')

      const arg = changeItemStateStub.mock.lastCall[0]
      expect(arg.itemBody).toBe('find [x + `y`')
      expect(arg.scoringData.value.variables).toEqual([{name: 'y', min: 0, max: 5, precision: 0}])
    })

    it('updates when a new variable is changed', () => {
      runTest('find `xx` + `y`')

      const arg = changeItemStateStub.mock.lastCall[0]
      expect(arg.itemBody).toBe('find `xx` + `y`')
      expect(arg.scoringData.value.variables).toEqual([
        {name: 'xx', min: 0, max: 10, precision: 0},
        {name: 'y', min: 0, max: 5, precision: 0},
      ])
    })
  })
})

describe('Formula edit', () => {
  let defaultProps
  let changeItemStateStub
  let openImportModalStub
  let notifyScreenReaderStub
  let setOneQuestionAtATimeStub

  beforeEach(() => {
    changeItemStateStub = vi.fn()
    openImportModalStub = vi.fn()
    notifyScreenReaderStub = vi.fn()
    setOneQuestionAtATimeStub = vi.fn()

    defaultProps = createProps(
      changeItemStateStub,
      openImportModalStub,
      notifyScreenReaderStub,
      setOneQuestionAtATimeStub,
    )
  })

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

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

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

  it('renders a row for each variable', () => {
    const {container} = render(<FormulaEdit {...defaultProps} />)
    const trs = container.querySelectorAll('[data-section="variable_definitions"] tr')

    expect(trs).toHaveLength(3)
  })

  describe('override editable for regrading', () => {
    it('renders number inputs with the correct disabled state', () => {
      render(<FormulaEdit {...defaultProps} overrideEditableForRegrading />)

      const xVariableMinInput = screen.getByLabelText(/Minimum value for variable x/i)
      const xVariableMaxInput = screen.getByLabelText(/Maximum value for variable x/i)
      const xVariableDecimalsInput = screen.getByLabelText(/decimals of precision for variable x/i)
      const yVariableMinInput = screen.getByLabelText(/Minimum value for variable y/i)
      const yVariableMaxInput = screen.getByLabelText(/Maximum value for variable y/i)
      const yVariableDecimalsInput = screen.getByLabelText(/decimals of precision for variable y/i)
      const numberOfSolutionsInput = document.querySelector(
        '[data-automation="sdk-number-of-solutions-input"]',
      )
      const decimalPlacesInput = screen.getByLabelText(/Decimal places/i)
      const marginOfErrorInput = screen.getByLabelText(/\+\/- margin of error/i)

      expect(xVariableMinInput.disabled).toBe(true)
      expect(xVariableMaxInput.disabled).toBe(true)
      expect(xVariableDecimalsInput.disabled).toBe(true)
      expect(yVariableMinInput.disabled).toBe(true)
      expect(yVariableMaxInput.disabled).toBe(true)
      expect(yVariableDecimalsInput.disabled).toBe(true)
      expect(numberOfSolutionsInput.disabled).toBe(true)
      expect(decimalPlacesInput.disabled).toBe(true)
      expect(marginOfErrorInput.disabled).toBe(false)
    })

    it('disables the calculator option', () => {
      render(<FormulaEdit {...defaultProps} 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.disabled).toBe(true)
      expect(calculatorBasicRadioInput.disabled).toBe(true)
      expect(calculatorScientificRadioInput.disabled).toBe(true)
    })
  })

  describe('range data', () => {
    const varBlurredProperly = (field, inputVal, canonical, localized) => {
      const {rerender} = render(<FormulaEdit {...defaultProps} />)
      const minMax = field === 'min' ? 'Minimum' : 'Maximum'
      const input = screen.getAllByLabelText(new RegExp(`${minMax} value for variable`, 'i'))[0]

      fireEvent.change(input, {target: {value: inputVal}})
      fireEvent.blur(input)

      const arg = changeItemStateStub.mock.lastCall[0]
      // scoringData contains a canonical representation e.g. '4200.07' (en)
      expect(arg.scoringData.value.variables[0][field]).toBe(canonical)
      assertGeneratedSolutionsCleared(changeItemStateStub)

      const props = update(defaultProps, {
        scoringData: {
          value: {
            variables: {
              0: {
                [field]: {
                  $set: canonical,
                },
              },
            },
          },
        },
      })

      // setup new props to pass back in and render as would normally happen on blur
      rerender(<FormulaEdit {...props} />)

      // the input itself would be localized e.g. '4 200,07' (fr)
      expect(input.value).toBe(localized)
    }

    const varChangedProperly = (field, inputVal, outputVal) => {
      render(<FormulaEdit {...defaultProps} />)
      const minMax = field === 'min' ? 'Minimum' : 'Maximum'
      const input = screen.getAllByLabelText(new RegExp(`${minMax} value for variable`, 'i'))[0]
      fireEvent.change(input, {target: {value: inputVal}})

      expect(input.value).toBe(outputVal)
    }

    const reactInputsProperly = (field, initialValue) => {
      render(<FormulaEdit {...defaultProps} />)
      const minMax = field === 'min' ? 'Minimum' : 'Maximum'
      const input = screen.getAllByLabelText(new RegExp(`${minMax} value for variable`, 'i'))[0]

      // test initial default
      expect(input.value).toBe(initialValue)
      // it can be blank
      fireEvent.change(input, {target: {value: ''}})
      expect(input.value).toBe('')
      // it takes a number input
      fireEvent.change(input, {target: {value: '2'}})
      expect(input.value).toBe('2')
      // blank input is changed to default on blur
      fireEvent.change(input, {target: {value: ''}})
      expect(input.value).toBe('')
      fireEvent.blur(input)
      expect(input.value).toBe(initialValue)
      // test '-' input is changed to default on blur
      fireEvent.change(input, {target: {value: '-'}})
      expect(input.value).toBe('-')
      fireEvent.blur(input)
      expect(input.value).toBe(initialValue)
    }

    describe('minimum input', () => {
      it('reacts properly on input changes', () => {
        reactInputsProperly('min', '0')
      })

      it('takes intermediate inputs on change', () => {
        varChangedProperly('min', '-', '-')
      })

      it('takes a non-digit character on change', () => {
        varChangedProperly('min', 'Q', 'Q')
      })

      it('handles changes with valid number appropriately', () => {
        varChangedProperly('min', '2.99', '2.99')
      })

      it('handle precision changes appropriately on blur', () => {
        varBlurredProperly('min', '2.99', '3', '3')
      })

      it('formats properly on blur', () => {
        varBlurredProperly('min', '500000', '500000', '500,000')
      })
    })

    describe('maximum input', () => {
      it('reacts properly on input changes', () => {
        reactInputsProperly('max', '5')
      })

      it('takes intermediate inputs on change', () => {
        varChangedProperly('max', '-', '-')
      })

      it('takes a non-digit character on change', () => {
        varChangedProperly('max', 'Q', 'Q')
      })

      it('handles changes with valid number appropriately', () => {
        varChangedProperly('max', '2.99', '2.99')
      })

      it('handles precision changes appropriately on blur', () => {
        varBlurredProperly('max', '2.99', '3', '3')
      })

      it('formats properly on blur', () => {
        varBlurredProperly('max', '500000', '500000', '500,000')
      })
    })

    it('changes the precision', async () => {
      render(<FormulaEdit {...defaultProps} />)
      const input = screen.getAllByLabelText(/decimals of precision for variable/i)[0]
      fireEvent.change(input, {target: {value: '2'}})

      await waitFor(() => {
        const arg = changeItemStateStub.mock.lastCall[0]
        expect(arg.scoringData.value.variables[0].min).toBe('0.00')
        expect(arg.scoringData.value.variables[0].max).toBe('5.00')
        expect(arg.scoringData.value.variables[0].precision).toBe('2')
        assertGeneratedSolutionsCleared(changeItemStateStub)
      })
    })

    for (const field of ['min', 'max']) {
      it(`doesn't call changeItemState on ${field} blur if the value hasn't changed`, () => {
        render(<FormulaEdit {...defaultProps} />)
        const minMax = field === 'min' ? 'Minimum' : 'Maximum'
        const input = screen.getByLabelText(new RegExp(`${minMax} value for variable x`, 'i'))
        const variable = defaultProps.scoringData.value.variables.find(v => v.name === 'x')
        fireEvent.change(input, {target: {value: variable[field].toString()}})
        fireEvent.blur(input)
        expect(changeItemStateStub).not.toHaveBeenCalled()
      })
    }

    it("doesn't call changeItemState on precision blur if the value hasn't changed", () => {
      render(<FormulaEdit {...defaultProps} />)
      const input = screen.getByLabelText(/decimals of precision for variable x/i)
      const variable = defaultProps.scoringData.value.variables.find(v => v.name === 'x')
      fireEvent.change(input, {target: {value: variable.precision.toString()}})
      fireEvent.blur(input)
      expect(changeItemStateStub).not.toHaveBeenCalled()
    })
  })

  describe('formula section', () => {
    it('updates the formula', () => {
      const {container} = render(<FormulaEdit {...defaultProps} />)
      const questionStemInput = container.querySelector('[data-section="formula"] textarea')
      fireEvent.change(questionStemInput, {target: {value: 'x*y'}})

      const arg = changeItemStateStub.mock.lastCall[0]
      expect(arg.scoringData.value.formula).toBe('x*y')
      assertGeneratedSolutionsCleared(changeItemStateStub)
    })

    it('updates the number of solutions', () => {
      render(<FormulaEdit {...defaultProps} />)
      const numberOfSolutionsInput = document.querySelector(
        '[data-automation="sdk-number-of-solutions-input"]',
      )

      fireEvent.change(numberOfSolutionsInput, {target: {value: '5'}})

      const arg = changeItemStateStub.mock.lastCall[0]
      expect(arg.scoringData.value.answerCount).toBe('5')
      assertGeneratedSolutionsCleared(changeItemStateStub)
    })

    it('updates the answer precision and clears generated solutions', () => {
      render(<FormulaEdit {...defaultProps} />)
      const decimalPlacesInput = screen.getByLabelText(/Decimal places/i)
      fireEvent.change(decimalPlacesInput, {target: {value: '5'}})

      const arg = changeItemStateStub.mock.lastCall[0]
      expect(arg.scoringData.value.answerPrecision).toBe(5)
      assertGeneratedSolutionsCleared(changeItemStateStub)
    })

    it('updates the margin of error', () => {
      render(<FormulaEdit {...defaultProps} />)
      const marginOfErrorInput = screen.getByLabelText(/\+\/- margin of error/i)
      fireEvent.change(marginOfErrorInput, {target: {value: '5'}})

      const arg = changeItemStateStub.mock.lastCall[0]
      expect(arg.scoringData.value.numeric.margin).toBe('5')
    })

    it('normalizes the margin of error', () => {
      render(<FormulaEdit {...defaultProps} />)
      const marginOfErrorInput = screen.getByLabelText(/\+\/- margin of error/i)
      fireEvent.change(marginOfErrorInput, {target: {value: '5,000'}})

      const arg = changeItemStateStub.mock.lastCall[0]
      expect(arg.scoringData.value.numeric.margin).toBe('5000')
    })

    it('normalizes an invalid margin of error', () => {
      render(<FormulaEdit {...defaultProps} />)

      const marginOfErrorInput = screen.getByRole('spinbutton', {name: /\+\/- margin of error/i})

      userEvent.clear(marginOfErrorInput)
      userEvent.type(marginOfErrorInput, '5.5.5.5')
      userEvent.tab()

      const arg = changeItemStateStub.mock.lastCall[0]
      expect(arg.scoringData.value.numeric.margin).toBe('555.5')
    })

    it('updates the margin of error type', () => {
      render(<FormulaEdit {...defaultProps} />)

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

      userEvent.click(marginTypeSelect)
      userEvent.click(screen.getByRole('option', {name: /percent/i}))

      const arg = changeItemStateStub.mock.lastCall[0]
      expect(arg.scoringData.value.numeric.marginType).toBe('percent')
    })

    it("doesn't call changeItemState on number of solutions blur if the value hasn't changed", () => {
      render(<FormulaEdit {...defaultProps} />)
      const input = document.querySelector('[data-automation="sdk-number-of-solutions-input"]')
      fireEvent.change(input, {
        target: {value: defaultProps.scoringData.value.answerCount.toString()},
      })
      fireEvent.blur(input)
      expect(changeItemStateStub).not.toHaveBeenCalled()
    })

    it("doesn't call changeItemState on answer precision blur if the value hasn't changed", () => {
      render(<FormulaEdit {...defaultProps} />)
      const input = screen.getByLabelText(/Decimal places/i)
      fireEvent.change(input, {
        target: {value: defaultProps.scoringData.value.answerPrecision.toString()},
      })
      fireEvent.blur(input)
      expect(changeItemStateStub).not.toHaveBeenCalled()
    })

    it("doesn't call changeItemState on margin of error blur if the value hasn't changed", () => {
      render(<FormulaEdit {...defaultProps} />)
      const input = screen.getByLabelText(/\+\/- margin of error/i)
      fireEvent.change(input, {
        target: {value: defaultProps.scoringData.value.numeric.margin.toString()},
      })
      fireEvent.blur(input)
      expect(changeItemStateStub).not.toHaveBeenCalled()
    })

    it('generates solutions on button click', async () => {
      const props = update(defaultProps, {
        scoringData: {
          value: {
            answerCount: {$set: '5'},
            generatedSolutions: {$set: []},
          },
        },
      })

      render(<FormulaEdit {...props} />)
      const generateButton = screen.getByRole('button', {name: /generate/i})
      fireEvent.click(generateButton)

      await waitFor(() => {
        const arg = changeItemStateStub.mock.lastCall[0]
        expect(arg.scoringData.value.generatedSolutions.length).toBe(5)
        expect(notifyScreenReaderStub).toHaveBeenCalledWith(
          'Solutions updated. We were able to find 5 solutions.',
        )
      })
    })

    it('handles numeric variables stored as strings', async () => {
      const props = update(defaultProps, {
        scoringData: {
          value: {
            variables: {
              $set: [
                {name: 'x', min: '5', max: '10', precision: '0'},
                {name: 'y', min: '1', max: '29', precision: '0'},
              ],
            },
            answerCount: {$set: '1'},
            generatedSolutions: {$set: []},
          },
        },
      })

      render(<FormulaEdit {...props} />)
      const generateButton = screen.getByRole('button', {name: /generate/i})
      fireEvent.click(generateButton)

      await waitFor(() => {
        const arg = changeItemStateStub.mock.lastCall[0]
        expect(arg.scoringData.value.generatedSolutions.length).toBe(1)
      })
    })

    describe('errors in min/max for a variable', () => {
      let props

      beforeEach(() => {
        props = update(defaultProps, {
          scoringData: {
            value: {
              variables: {
                $set: [{name: 'x', min: 7, max: 5, precision: 0}],
              },
              generatedSolutions: {$set: []},
            },
          },
        })
      })

      it('does not generate solutions', () => {
        const {container} = render(<FormulaEdit {...props} />)

        const generateButton = screen.getByRole('button', {name: /generate/i})
        fireEvent.click(generateButton)

        expect(container.textContent).toContain('Error in formula setup. See above for details.')
      })

      it('notifies screenreader of variable errors', () => {
        render(<FormulaEdit {...props} />)

        const generateButton = screen.getByRole('button', {name: /generate/i})
        fireEvent.click(generateButton)

        expect(notifyScreenReaderStub).toHaveBeenCalled()
        expect(notifyScreenReaderStub.mock.lastCall[0]).toContain(
          'The following error prevented generating solutions:',
        )
        expect(notifyScreenReaderStub.mock.lastCall[0]).toContain('variables containing errors: x')
      })

      it('does not notify screenreader if no variable errors', () => {
        render(<FormulaEdit {...defaultProps} />)

        const generateButton = screen.getByRole('button', {name: /generate/i})
        fireEvent.click(generateButton)

        expect(notifyScreenReaderStub).not.toHaveBeenCalled()
      })
    })
  })

  it('displays errors when errors are showing', () => {
    const testProps = {
      ...defaultProps,
      itemBody: '',
      errorsAreShowing: true,
    }

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

    expect(container.textContent).toContain('Question Stem cannot be blank')
  })

  it('does not display errors when errors are not showing', () => {
    const testProps = {
      ...defaultProps,
      itemBody: '',
      errorsAreShowing: false,
    }

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

    expect(container.textContent).not.toContain('Question Stem cannot be blank')
  })

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

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

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

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

      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(<FormulaEdit {...defaultProps} />)

      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(<FormulaEdit {...defaultProps} />)

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

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

  it('sets numeric type to exactResponse when scientific notation checkbox checked', async () => {
    render(<FormulaEdit {...defaultProps} />)

    const scientificNotationCheckbox = screen.getByLabelText(/Display as Scientific Notation/i)
    fireEvent.click(scientificNotationCheckbox)

    await waitFor(() => {
      expect(changeItemStateStub).toHaveBeenCalledWith(
        expect.objectContaining({
          scoringData: expect.objectContaining({
            value: expect.objectContaining({
              numeric: expect.objectContaining({
                type: 'exactResponse',
              }),
              generatedSolutions: [],
            }),
          }),
        }),
      )
    })
  })

  it('sets numeric type to marginOfError when scientific notation checkbox unchecked', async () => {
    const scoringData = {
      ...defaultProps.scoringData,
      value: {
        ...defaultProps.scoringData.value,
        generatedSolutions: [],
        scientificNotation: true,
      },
    }
    render(<FormulaEdit {...defaultProps} scoringData={scoringData} />)

    const scientificNotationCheckbox = screen.getByLabelText(/Display as Scientific Notation/i)
    fireEvent.click(scientificNotationCheckbox)

    await waitFor(() => {
      expect(changeItemStateStub).toHaveBeenCalledWith(
        expect.objectContaining({
          scoringData: expect.objectContaining({
            value: expect.objectContaining({
              numeric: expect.objectContaining({
                type: 'marginOfError',
                marginType: 'absolute',
                margin: 0,
              }),
              generatedSolutions: [],
            }),
          }),
        }),
      )
    })
  })

  describe('with variables in scientific notation', () => {
    const scientificNotationScoringData = {
      value: {
        answerCount: 3,
        answerPrecision: 1,
        formula: 'x + y',
        numeric: {},
        variables: [
          {
            name: 'x',
            min: '5.0*10^-2',
            max: '2.5*10^-1',
            precision: 1,
          },
          {
            name: 'y',
            min: '-0.01',
            max: '0.01',
            precision: 2,
          },
        ],
        generatedSolutions: [],
      },
    }

    it('generates inputs in scientific notation', async () => {
      render(<FormulaEdit {...defaultProps} scoringData={scientificNotationScoringData} />)

      const generateButton = screen.getByRole('button', {name: /generate/i})
      fireEvent.click(generateButton)

      await waitFor(() => {
        expect(changeItemStateStub).toHaveBeenCalled()
        const {scoringData} = changeItemStateStub.mock.lastCall[0]
        const {generatedSolutions} = scoringData.value
        expect(Array.isArray(generatedSolutions)).toBe(true)

        const xs = generatedSolutions.map(
          solution => solution.inputs.find(input => input.name === 'x').value,
        )
        for (const x of xs) {
          expect(isScientificNotation(x)).toBe(true)
          expect(scientificNotationToNumber(x)).toBeGreaterThanOrEqual(5e-2)
          expect(scientificNotationToNumber(x)).toBeLessThanOrEqual(2.5e-1)
        }
      })
    })

    it('generates outputs in decimal notation', async () => {
      const scoringData = {
        value: {
          ...scientificNotationScoringData.value,
          scientificNotation: false,
        },
      }
      render(<FormulaEdit {...defaultProps} scoringData={scoringData} />)

      const generateButton = screen.getByRole('button', {name: /generate/i})
      fireEvent.click(generateButton)

      await waitFor(() => {
        expect(changeItemStateStub).toHaveBeenCalled()
        const {scoringData} = changeItemStateStub.mock.lastCall[0]
        const {generatedSolutions} = scoringData.value
        expect(Array.isArray(generatedSolutions)).toBe(true)

        const outputs = generatedSolutions.map(solution => solution.output)
        for (const output of outputs) {
          expect(isScientificNotation(output)).toBe(false)
        }
      })
    })

    it('generates outputs in scientific notation', async () => {
      const scoringData = {
        value: {
          ...scientificNotationScoringData.value,
          scientificNotation: true,
        },
      }
      render(<FormulaEdit {...defaultProps} scoringData={scoringData} />)

      const generateButton = screen.getByRole('button', {name: /generate/i})
      fireEvent.click(generateButton)

      await waitFor(() => {
        expect(changeItemStateStub).toHaveBeenCalled()
        const {scoringData} = changeItemStateStub.mock.lastCall[0]
        const {generatedSolutions} = scoringData.value
        expect(Array.isArray(generatedSolutions)).toBe(true)

        const outputs = generatedSolutions.map(solution => solution.output)
        for (const output of outputs) {
          expect(isScientificNotation(output)).toBe(true)
        }
      })
    })

    it('updates min and max when precision changed', async () => {
      const {rerender} = render(
        <FormulaEdit {...defaultProps} scoringData={scientificNotationScoringData} />,
      )

      const precisionInput = screen.getByLabelText(/precision for variable x/i)
      fireEvent.change(precisionInput, {target: {value: '2'}})

      await waitFor(() => expect(changeItemStateStub).toHaveBeenCalled())
      const updatedScoringData = changeItemStateStub.mock.lastCall[0].scoringData
      const scoringData = {
        value: {
          ...scientificNotationScoringData.value,
          ...updatedScoringData.value,
        },
      }

      // Pass the updated scoring data back into the component as props
      rerender(<FormulaEdit {...defaultProps} scoringData={scoringData} />)

      const minInput = screen.getByLabelText(/Minimum value for variable x/i)
      const maxInput = screen.getByLabelText(/Maximum value for variable x/i)
      expect(minInput.value).toBe('5.00*10^-2')
      expect(maxInput.value).toBe('2.50*10^-1')
    })

    it('handles very large numbers in scientific notation', async () => {
      const scoringData = {
        value: {
          ...scientificNotationScoringData.value,
          scientificNotation: true,
          variables: [
            {
              name: 'x',
              min: '5*10^400',
              max: '1*10^500',
              precision: 0,
            },
            {
              name: 'y',
              min: '-0.01',
              max: '0.01',
              precision: 2,
            },
          ],
        },
      }
      render(<FormulaEdit {...defaultProps} scoringData={scoringData} />)

      const generateButton = screen.getByRole('button', {name: /generate/i})
      fireEvent.click(generateButton)

      await waitFor(() => {
        expect(changeItemStateStub).toHaveBeenCalled()
        const {scoringData} = changeItemStateStub.mock.lastCall[0]
        const {generatedSolutions} = scoringData.value
        expect(Array.isArray(generatedSolutions)).toBe(true)

        const outputs = generatedSolutions.map(solution => solution.output)
        for (const output of outputs) {
          expect(isScientificNotation(output)).toBe(true)
          const [_, exponent] = parseScientificNotation(output) // eslint-disable-line no-unused-vars
          expect(Number(exponent)).toBeGreaterThanOrEqual(400)
          expect(Number(exponent)).toBeLessThanOrEqual(500)
        }
      })
    })
  })
})
