const { _, $ } = Cypress

describe('src/cy/commands/actions/select', () => {
  before(() => {
    cy
    .visit('/fixtures/dom.html')
    .then(function (win) {
      this.body = win.document.body.outerHTML
    })
  })

  beforeEach(function () {
    const doc = cy.state('document')

    $(doc.body).empty().html(this.body)
  })

  context('#select', () => {
    it('does not change the subject', () => {
      const select = cy.$$('select[name=maps]')

      cy.get('select[name=maps]').select('train').then(($select) => {
        expect($select).to.match(select)
      })
    })

    it('selects by value', () => {
      cy.get('select[name=maps]').select('de_train').then(($select) => {
        expect($select).to.have.value('de_train')
      })
    })

    it('selects by text', () => {
      cy.get('select[name=maps]').select('train').then(($select) => {
        expect($select).to.have.value('de_train')
      })
    })

    it('selects by trimmed text with newlines stripped', () => {
      cy.get('select[name=maps]').select('italy').then(($select) => {
        expect($select).to.have.value('cs_italy')
      })
    })

    it('prioritizes value over text', () => {
      cy.get('select[name=foods]').select('Ramen').then(($select) => {
        expect($select).to.have.value('Ramen')
      })
    })

    it('can select an array of values', () => {
      cy.get('select[name=movies]').select(['apoc', 'br']).then(($select) => {
        expect($select.val()).to.deep.eq(['apoc', 'br'])
      })
    })

    it('can handle options nested in optgroups', () => {
      cy.get('select[name=starwars]').select('Jar Jar').then(($select) => {
        expect($select).to.have.value('jarjar')
      })
    })

    it('can handle options with same value selected by text', () => {
      cy.get('select[name=startrek-same]').select('Uhura').then(($select) => {
        expect($select.val()).to.equal('same')
        expect($select.find('option:selected')).to.have.text('Uhura')
        expect($select[0].selectedIndex).to.equal(2)
        expect($select[0].selectedOptions[0]).to.eql($select.find('option:selected')[0])
      })
    })

    it('can handle options with some same values selected by text', () => {
      cy.get('select[name=startrek-some-same]').select('Uhura').then(($select) => {
        expect($select.val()).to.equal('same')
        expect($select.find('option:selected')).to.have.text('Uhura')
        expect($select[0].selectedIndex).to.equal(2)
        expect($select[0].selectedOptions[0]).to.eql($select.find('option:selected')[0])
      })
    })

    it('can select an array of values', () => {
      cy.get('select[name=movies]').select(['apoc', 'br']).then(($select) => {
        expect($select.val()).to.deep.eq(['apoc', 'br'])
      })
    })

    it('can select an array of texts', () => {
      cy.get('select[name=movies]').select(['The Human Condition', 'There Will Be Blood']).then(($select) => {
        expect($select.val()).to.deep.eq(['thc', 'twbb'])
      })
    })

    // readonly should only be limited to inputs, not checkboxes
    it('can select a readonly select', () => {
      cy.get('select[name=hunter]').select('gon').then(($select) => {
        expect($select.val()).to.eq('gon-val')
      })
    })

    it('clears previous values when providing an array', () => {
      // make sure we have a previous value
      const select = cy.$$('select[name=movies]').val(['2001'])

      expect(select.val()).to.deep.eq(['2001'])

      cy.get('select[name=movies]').select(['apoc', 'br']).then(($select) => {
        expect($select.val()).to.deep.eq(['apoc', 'br'])
      })
    })

    it('lists the select as the focused element', () => {
      const select = cy.$$('#select-maps')

      cy.get('#select-maps').select('de_train').focused().then(($focused) => {
        expect($focused.get(0)).to.eq(select.get(0))
      })
    })

    it('causes previous input to receive blur', (done) => {
      cy.$$('input:text:first').blur(() => {
        done()
      })

      cy.get('input:text:first').type('foo')
      cy.get('#select-maps').select('de_train')
    })

    it('can forcibly click even when being covered by another element', (done) => {
      const select = $('<select><option>foo</option></select>').attr('id', 'select-covered-in-span').prependTo(cy.$$('body'))

      $('<span>span on select</span>').css({ position: 'absolute', left: select.offset().left, top: select.offset().top, padding: 5, display: 'inline-block', backgroundColor: 'yellow' }).prependTo(cy.$$('body'))

      select.on('click', () => {
        done()
      })

      cy.get('#select-covered-in-span').select('foo', { force: true })
    })

    it('passes timeout and interval down to click', (done) => {
      const select = $('<select />').attr('id', 'select-covered-in-span').prependTo(cy.$$('body'))

      $('<span>span on select</span>').css({ position: 'absolute', left: select.offset().left, top: select.offset().top, padding: 5, display: 'inline-block', backgroundColor: 'yellow' }).prependTo(cy.$$('body'))

      cy.on('command:retry', (options) => {
        expect(options.timeout).to.eq(1000)
        expect(options.interval).to.eq(60)

        done()
      })

      cy.get('#select-covered-in-span').select('foobar', { timeout: 1000, interval: 60 })
    })

    it('can forcibly click even when element is invisible', (done) => {
      const select = cy.$$('#select-maps').hide()

      select.click(() => {
        done()
      })

      cy.get('#select-maps').select('de_dust2', { force: true })
    })

    it('can forcibly click when select is disabled', () => {
      cy.get('select[name=disabled]')
      // default select value
      .invoke('val').should('eq', 'foo')

      cy.get('select[name=disabled]')
      .select('bar', { force: true })
      .invoke('val').should('eq', 'bar')
    })

    it('retries until <option> can be selected', () => {
      const option = cy.$$('<option>foo</option>')

      cy.on('command:retry', _.once(() => {
        cy.$$('#select-maps').append(option)
      }))

      cy.get('#select-maps').select('foo')
    })

    it('retries until <select> is no longer disabled', () => {
      const select = cy.$$('select[name=disabled]')

      cy.on('command:retry', _.once(() => {
        select.prop('disabled', false)
      }))

      cy.get('select[name=disabled]').select('foo')
      .invoke('val').should('eq', 'foo')
    })

    it('retries until <optgroup> is no longer disabled', () => {
      const select = cy.$$('select[name=optgroup-disabled]')

      cy.on('command:retry', _.once(() => {
        select.find('optgroup').prop('disabled', false)
      }))

      cy.get('select[name=optgroup-disabled]').select('bar')
      .invoke('val').should('eq', 'bar')
    })

    it('retries until <options> are no longer disabled', () => {
      const select = cy.$$('select[name=opt-disabled]')

      cy.on('command:retry', _.once(() => {
        select.find('option').prop('disabled', false)
      }))

      cy.get('select[name=opt-disabled]').select('bar')
      .invoke('val').should('eq', 'bar')
    })

    describe('assertion verification', () => {
      beforeEach(function () {
        cy.on('log:added', (attrs, log) => {
          if (log.get('name') === 'assert') {
            this.lastLog = log
          }
        })

        return null
      })

      it('eventually passes the assertion', () => {
        cy.$$('#select-maps').change(function () {
          _.delay(() => {
            $(this).addClass('selected')
          }, 100)
        })

        cy.get('#select-maps').select('de_nuke').should('have.class', 'selected').then(function () {
          const { lastLog } = this

          expect(lastLog.get('name')).to.eq('assert')
          expect(lastLog.get('state')).to.eq('passed')
          expect(lastLog.get('ended')).to.be.true
        })
      })
    })

    describe('events', () => {
      it('emits click event', (done) => {
        cy.$$('select[name=maps]').click(() => {
          done()
        })

        cy.get('select[name=maps]').select('train')
      })

      it('emits change event', (done) => {
        cy.$$('select[name=maps]').change(() => {
          done()
        })

        cy.get('select[name=maps]').select('train')
      })

      it('emits focus event', (done) => {
        cy.$$('select[name=maps]').one('focus', () => {
          done()
        })

        cy.get('select[name=maps]').select('train')
      })

      it('emits input event', (done) => {
        cy.$$('select[name=maps]').one('input', () => {
          done()
        })

        cy.get('select[name=maps]').select('train')
      })

      it('emits all events in the correct order', () => {
        const fired = []
        const events = ['mousedown', 'focus', 'mouseup', 'click', 'input', 'change']

        _.each(events, (event) => {
          cy.$$('select[name=maps]').one(event, () => {
            fired.push(event)
          })
        })

        cy.get('select[name=maps]').select('train').then(() => {
          expect(fired).to.deep.eq(events)
        })
      })
    })

    describe('errors', {
      defaultCommandTimeout: 100,
    }, () => {
      beforeEach(function () {
        this.logs = []

        cy.on('log:added', (attrs, log) => {
          this.lastLog = log

          return this.logs.push(log)
        })

        return null
      })

      it('throws when not a dom subject', (done) => {
        cy.on('fail', () => {
          done()
        })

        cy.noop({}).select('foo')
      })

      it('throws when subject is not in the document', (done) => {
        let selected = 0

        const $select = cy.$$('#select-maps').change((e) => {
          selected += 1
          $select.remove()
        })

        cy.on('fail', (err) => {
          expect(selected).to.eq(1)
          expect(err.message).to.include('`cy.select()` failed because this element')

          done()
        })

        cy.get('#select-maps').select('de_dust2').select('de_aztec')
      })

      it('throws when more than 1 element in the collection', (done) => {
        const num = cy.$$('select').length

        cy.on('fail', (err) => {
          expect(err.message).to.include(`\`cy.select()\` can only be called on a single \`<select>\`. Your subject contained ${num} elements.`)
          expect(err.docsUrl).to.eq('https://on.cypress.io/select')

          done()
        })

        cy.get('select').select('foo')
      })

      it('throws on anything other than a select', (done) => {
        cy.on('fail', (err) => {
          expect(err.message).to.include('`cy.select()` can only be called on a `<select>`. Your subject is a: `<input id="input">`')
          expect(err.docsUrl).to.eq('https://on.cypress.io/select')

          done()
        })

        cy.get('input:first').select('foo')
      })

      it('throws when finding duplicate values', (done) => {
        cy.on('fail', (err) => {
          expect(err.message).to.include('`cy.select()` matched more than one `option` by value or text: `bm`')
          expect(err.docsUrl).to.eq('https://on.cypress.io/select')

          done()
        })

        cy.get('select[name=names]').select('bm')
      })

      it('throws when passing an array to a non multiple select', (done) => {
        cy.on('fail', (err) => {
          expect(err.message).to.include('`cy.select()` was called with an array of arguments but does not have a `multiple` attribute set.')
          expect(err.docsUrl).to.eq('https://on.cypress.io/select')

          done()
        })

        cy.get('select[name=names]').select(['bm', 'ss'])
      })

      it('throws when the subject isnt visible', (done) => {
        cy.$$('#select-maps').show().hide()

        cy.on('fail', (err) => {
          expect(err.message).to.include('`cy.select()` failed because this element is not visible')

          done()
        })

        cy.get('#select-maps').select('de_dust2')
      })

      it('throws when value or text does not exist', (done) => {
        cy.on('fail', (err) => {
          expect(err.message).to.include('`cy.select()` failed because it could not find a single `<option>` with value or text matching: `foo`')
          expect(err.docsUrl).to.eq('https://on.cypress.io/select')

          done()
        })

        cy.get('select[name=foods]').select('foo')
      })

      it('throws when the <select> itself is disabled', (done) => {
        cy.on('fail', (err) => {
          expect(err.message).to.include('`cy.select()` failed because this element is currently disabled:')
          expect(err.docsUrl).to.eq('https://on.cypress.io/select')

          done()
        })

        cy.get('select[name=disabled]').select('foo')
      })

      it('throws when optgroup is disabled', (done) => {
        cy.on('fail', (err) => {
          expect(err.message).to.include('`cy.select()` failed because this `<option>` you are trying to select is within an `<optgroup>` that is currently disabled:')
          expect(err.docsUrl).to.eq('https://on.cypress.io/select')

          done()
        })

        cy.get('select[name=optgroup-disabled]').select('bar')
      })

      it('throws when options are disabled', (done) => {
        cy.on('fail', (err) => {
          expect(err.message).to.include('`cy.select()` failed because this `<option>` you are trying to select is currently disabled:')
          expect(err.docsUrl).to.eq('https://on.cypress.io/select')

          done()
        })

        cy.get('select[name=opt-disabled]').select('bar')
      })

      it('eventually fails the assertion', function (done) {
        cy.on('fail', (err) => {
          const { lastLog } = this

          expect(err.message).to.include(lastLog.get('error').message)
          expect(err.message).not.to.include('undefined')
          expect(lastLog.get('name')).to.eq('assert')
          expect(lastLog.get('state')).to.eq('failed')
          expect(lastLog.get('error')).to.be.an.instanceof(chai.AssertionError)

          done()
        })

        cy.get('#select-maps').select('de_nuke').should('have.class', 'selected')
      })

      it('does not log an additional log on failure', function (done) {
        cy.on('fail', () => {
          expect(this.logs.length).to.eq(3)

          done()
        })

        cy.get('#select-maps').select('de_nuke').should('have.class', 'selected')
      })

      it('only logs once on failure', function (done) {
        cy.on('fail', (err) => {
          // 2 logs, 1 for cy.get, 1 for cy.select
          expect(this.logs.length).to.eq(2)

          done()
        })

        cy.get('#select-maps').select('does_not_exist')
      })
    })

    describe('.log', () => {
      beforeEach(function () {
        this.logs = []

        cy.on('log:added', (attrs, log) => {
          this.lastLog = log

          return this.logs.push(log)
        })

        return null
      })

      it('logs out select', () => {
        cy.get('#select-maps').select('de_dust2').then(function () {
          const { lastLog } = this

          expect(lastLog.get('name')).to.eq('select')
        })
      })

      it('passes in $el', () => {
        cy.get('#select-maps').select('de_dust2').then(function ($select) {
          const { lastLog } = this

          expect(lastLog.get('$el')).to.eq($select)
        })
      })

      it('snapshots before clicking', function (done) {
        cy.$$('#select-maps').change(() => {
          const { lastLog } = this

          expect(lastLog.get('snapshots').length).to.eq(1)
          expect(lastLog.get('snapshots')[0].name).to.eq('before')
          expect(lastLog.get('snapshots')[0].body).to.be.an('object')

          done()
        })

        cy.get('#select-maps').select('de_dust2').then(($select) => {})
      })

      it('snapshots after clicking', () => {
        cy.get('#select-maps').select('de_dust2').then(function ($select) {
          const { lastLog } = this

          expect(lastLog.get('snapshots').length).to.eq(2)
          expect(lastLog.get('snapshots')[1].name).to.eq('after')
          expect(lastLog.get('snapshots')[1].body).to.be.an('object')
        })
      })

      it('is not immediately ended', function (done) {
        cy.$$('#select-maps').click(() => {
          const { lastLog } = this

          expect(lastLog.get('state')).to.eq('pending')

          done()
        })

        cy.get('#select-maps').select('de_dust2')
      })

      it('ends', () => {
        cy.get('#select-maps').select('de_dust2').then(function () {
          const { lastLog } = this

          expect(lastLog.get('state')).to.eq('passed')
        })
      })

      it('#consoleProps', () => {
        cy.get('#select-maps').select('de_dust2').then(function ($select) {
          const { fromElWindow } = Cypress.dom.getElementCoordinatesByPosition($select)
          const console = this.lastLog.invoke('consoleProps')

          expect(console.Command).to.eq('select')
          expect(console.Selected).to.deep.eq(['de_dust2'])
          expect(console['Applied To']).to.eq($select.get(0))
          expect(console.Coords.x).to.be.closeTo(fromElWindow.x, 10)
          expect(console.Coords.y).to.be.closeTo(fromElWindow.y, 10)
        })
      })

      it('logs only one select event', () => {
        const types = []

        cy.on('log:added', (attrs, log) => {
          if (log.get('name') === 'select') {
            return types.push(log)
          }
        })

        cy.get('#select-maps').select('de_dust2').then(function () {
          expect(this.logs.length).to.eq(2)
          expect(types.length).to.eq(1)
        })
      })

      it('logs deltaOptions', () => {
        cy.get('#select-maps').select('de_dust2', { force: true, timeout: 1000 }).then(function () {
          const { lastLog } = this

          expect(lastLog.get('message')).to.eq('{force: true, timeout: 1000}')
          expect(lastLog.invoke('consoleProps').Options).to.deep.eq({ force: true, timeout: 1000 })
        })
      })
    })
  })
})
