/*
 * Copyright (C) 2019 - present Instructure, Inc.
 *
 * This file is part of Canvas.
 *
 * Canvas is free software: you can redistribute it and/or modify it under
 * the terms of the GNU Affero General Public License as published by the Free
 * Software Foundation, version 3 of the License.
 *
 * Canvas is distributed in the hope that it will be useful, but WITHOUT ANY
 * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
 * A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
 * details.
 *
 * You should have received a copy of the GNU Affero General Public License along
 * with this program. If not, see <http://www.gnu.org/licenses/>.
 */

import React from 'react'
import ReactDOM from 'react-dom'
import {fireEvent} from '@testing-library/react'

import {DEFAULT_LATE_POLICY_DATA} from 'jsx/gradebook/default_gradebook/apis/GradebookSettingsModalApi'
import LatePoliciesTabPanel from 'jsx/gradebook/default_gradebook/components/LatePoliciesTabPanel'

const MAX = '100'
const MIN = '0'
const BORDER_VALUES = ['0', '0.01', '0.1', '99', '99.9', '99.99', '100']
const EXTRA_ZEROS_MAP = {
  '0': ['00', '000000000', '0.0', '00.', '0.', '.0', '.00', '.000'],
  '0.1': ['0.10', '0.100', '00.100', '000.10', '0000.1', '000.1000'],
  '1': [
    '01',
    '001',
    '0001',
    '0001.0',
    '0001.00',
    '0001.000',
    '001.000',
    '01.000',
    '1.000',
    '1.00',
    '1.0'
  ]
}
const MAX_OUTLIER_VALUES = [
  '100.001',
  '100.01',
  '100.1',
  '101',
  '500',
  '1000000',
  '1e10',
  Number.MAX_SAFE_INTEGER.toString()
]
const MIN_OUTLIER_VALUES = [
  '-0.001',
  '-0.01',
  '-0.1',
  '-1',
  '-500',
  '-1000000',
  '-1e10',
  Number.MIN_SAFE_INTEGER.toString()
]

/*
 * same as buildUpChangeEventsAndBlur but only call change once for use when
 * not testing normal onChange workflows (normal being when input is typed
 * one key at a time)
 */
function changeAndBlur({input, value}) {
  const events = [fireEvent.change(input, {target: {value: ''}})] // clear
  // update input with value in one event
  events.concat(fireEvent.change(input, {target: {value: input.value.concat(value)}}))
  events.concat(fireEvent.blur(input)) // blur to finalize
  return events // returns booleans from fireEvent
}

function buildUpChangeEventsAndBlur({input, value}) {
  // need to first clear inputs otherwise we'll have pollution
  const events = [fireEvent.change(input, {target: {value: ''}})]
  // build up change events via each character in value.split('')
  events.concat(
    value
      .split('')
      .map(character => fireEvent.change(input, {target: {value: input.value.concat(character)}}))
  )
  // blur to finalize
  events.concat(fireEvent.blur(input))
  // returns booleans from fireEvent
  return events
}

function percentInputValidations({inputFn}) {
  QUnit.module('Percentage Input Validations', () => {
    QUnit.module('Excess Zeros', () => {
      Object.keys(EXTRA_ZEROS_MAP).forEach(expectedValue => {
        EXTRA_ZEROS_MAP[expectedValue].forEach(value => {
          test(`accepts ${value} as ${expectedValue}`, () => {
            const input = inputFn()
            changeAndBlur({input, value})
            strictEqual(input.value, expectedValue)
          })
        })
      })
    })

    QUnit.module('Border Values', () => {
      BORDER_VALUES.forEach(value => {
        test(`accepts border value: ${value}`, () => {
          const input = inputFn()
          changeAndBlur({input, value})
          strictEqual(input.value, value)
        })
      })
    })

    QUnit.module('Maximum outlier values', () => {
      MAX_OUTLIER_VALUES.forEach(value => {
        test(`bound values greater than the maximum (100): ${value}`, () => {
          const input = inputFn()
          changeAndBlur({input, value})
          strictEqual(input.value, MAX)
        })
      })
    })

    QUnit.module('Minimum outlier values', () => {
      MIN_OUTLIER_VALUES.forEach(value => {
        test(`bounds values lesser than the minimum (0): ${value}`, () => {
          const input = inputFn()
          changeAndBlur({input, value})
          strictEqual(input.value, MIN)
        })
      })
    })

    QUnit.module('building up values one keystroke at a time', () => {
      test('accepts building up to 100', () => {
        const input = inputFn()
        buildUpChangeEventsAndBlur({input, value: MAX})
        strictEqual(input.value, MAX)
      })

      test('accepts building up to 99.99', () => {
        const input = inputFn()
        const value = '99.99'
        buildUpChangeEventsAndBlur({input, value})
        strictEqual(input.value, value)
      })

      test('accepts building up to "99.09"', () => {
        const input = inputFn()
        const value = '99.09'
        buildUpChangeEventsAndBlur({input, value})
        strictEqual(input.value, value)
      })

      test('accepts building up to 0.01', () => {
        const input = inputFn()
        const value = '0.01'
        buildUpChangeEventsAndBlur({input, value})
        strictEqual(input.value, value)
      })

      test('accepts building up to 0.01 without a preceeding "0"', () => {
        const input = inputFn()
        fireEvent.change(input, {target: {value: '.'}})
        fireEvent.change(input, {target: {value: '.0'}})
        fireEvent.change(input, {target: {value: '.01'}})
        fireEvent.blur(input)
        strictEqual(input.value, '0.01')
      })

      test('accepts building up to 0.01 in reverse', () => {
        const input = inputFn()
        fireEvent.change(input, {target: {value: '1'}})
        fireEvent.change(input, {target: {value: '01'}})
        fireEvent.change(input, {target: {value: '.01'}})
        fireEvent.change(input, {target: {value: '0.01'}})
        fireEvent.blur(input)
        strictEqual(input.value, '0.01')
      })
    })
  })
}

QUnit.module(
  'Gradebook > Default Gradebook > Components > LatePoliciesTabPanelPercentageNumberInput',
  withoutEnzymeHooks => {
    let $container
    let props

    withoutEnzymeHooks.beforeEach(() => {
      $container = document.getElementById('fixtures').appendChild(document.createElement('div'))

      props = {
        latePolicy: {
          changes: {},
          validationErrors: {}
          // by default there is no data key, it gets added after first render
        },
        changeLatePolicy,
        locale: 'en',
        showAlert: false
      }
    })

    withoutEnzymeHooks.afterEach(() => {
      ReactDOM.unmountComponentAtNode($container)
    })

    // this is to fake out how GradebookSettingsModal updates the state of props.latePolicy.changes
    function changeLatePolicy({changes}) {
      props.latePolicy.changes = changes
      mountComponent()
    }

    function mountComponent(componentProps = props) {
      ReactDOM.render(<LatePoliciesTabPanel {...componentProps} />, $container)
    }

    function setupComponent(componentProps = props) {
      mountComponent(componentProps)

      // yes actually update props at this point
      props = {
        ...componentProps,
        latePolicy: {
          ...componentProps.latePolicy,
          data: DEFAULT_LATE_POLICY_DATA
        }
      }
      mountComponent(props)
    }

    function findLabel(label) {
      return [...$container.querySelectorAll('label')].find(el => el.innerText.trim() === label)
    }

    function findInput(label) {
      const labelEl = findLabel(label)
      return $container.querySelector(`input#${labelEl.getAttribute('for')}`)
    }

    function getLatePoliciesTabPanelContainer() {
      return document.getElementById('LatePoliciesTabPanel__Container')
    }

    function findCheckbox(label) {
      const $modal = getLatePoliciesTabPanelContainer()
      const $label = [...$modal.querySelectorAll('label')].find($el =>
        $el.textContent.includes(label)
      )
      return $modal.querySelector(`#${$label.getAttribute('for')}`)
    }

    function getAutomaticallyApplyDeductionToLateSubmissionsCheckbox() {
      return findCheckbox('Automatically apply deduction to late submissions')
    }

    function getGradePercentageForMissingSubmissionsInput() {
      return findInput('Grade percentage for missing submissions')
    }

    function getLateSubmissionDeductionPercentInput() {
      return findInput('Late submission deduction percent')
    }

    function getLowestPossibleGradePercentInput() {
      return findInput('Lowest possible grade percent')
    }

    QUnit.module('Late Policies:', contextHooks => {
      contextHooks.beforeEach(() => {
        setupComponent(props)
      })

      QUnit.module('Missing Submissions', () => {
        QUnit.module(
          'when the "Automatically apply grade for missing submissions" checkbox is toggled on and the "Grade percentage for missing submissions" is changed',
          lateSubmissionDeductionPercentHooks => {
            lateSubmissionDeductionPercentHooks.beforeEach(() => {
              getAutomaticallyApplyDeductionToLateSubmissionsCheckbox().click()
            })

            percentInputValidations({inputFn: getGradePercentageForMissingSubmissionsInput})
          }
        )
      })

      QUnit.module('Late Submissions', () => {
        QUnit.module(
          'when the "Automatically apply deduction to late submissions" checkbox is toggled on and the "Late submission deduction percent" is changed',
          lateSubmissionDeductionPercentHooks => {
            lateSubmissionDeductionPercentHooks.beforeEach(() => {
              getAutomaticallyApplyDeductionToLateSubmissionsCheckbox().click()
            })

            percentInputValidations({inputFn: getLateSubmissionDeductionPercentInput})
          }
        )

        QUnit.module(
          'when the "Automatically apply deduction to late submissions" checkbox is toggled on and the "Lowest possible grade percent" is changed',
          lowestPossibleGradePercentHooks => {
            lowestPossibleGradePercentHooks.beforeEach(() => {
              getAutomaticallyApplyDeductionToLateSubmissionsCheckbox().click()
            })

            percentInputValidations({inputFn: getLowestPossibleGradePercentInput})
          }
        )
      })
    })
  }
)
