/* eslint arrow-body-style:'off' */

/**
  * in all browsers...
  *
  * activeElement is always programmatically respected and behaves identical whether window is in or out of focus
  *
  * browser: chrome...
  *
  * scenario 1: given '#one' is activeElement call programmatic el.focus() on '#two'
  * - if window is in focus
  *   - blur will fire on '#one'
  *   - focus will fire on '#two'
  * - if window is out of focus (the event wil be primed until the window receives focus again)
  *   - by clicking anywhere on the <body> (not on the element)...
  *     - focus on '#two' will fire first
  *     - blur on '#two' will fire second
  *     - activeElement will now be <body>
  *   - by clicking on another element that is focusable
  *     - focus on '#two' is first sent
  *     - blur on '#two' is then sent
  *     - focus is finally sent on the new focusable element we clicked
  *   - if instead on clicking we programmatically call .focus() back to '#one'
  *     - focus is fired on '#one'
  *     - if we were to instead click directly on '#one' then no focus or blur events are fired
  *     - if when clicking directly back to '#one' we prevent the 'mousedown' event
  *       - the focus event will fire AND the element will still be activeElement
  *       - had we not programmatically call .focus() ahead of time, then the focus event would
  *         have been not fired, and our activeElement would not have changed
  *
  * scenario 2 : given '#one' is activeElement call programmatic el.blur() on '#one'
  * - if window is in focus
  *   - blur will fire on '#one'
  * - if window is out of focus
  *   - no events will ever fire even when regaining focus

  * browser: firefox...
  *   - no focus events are queued when programmatically calling element.focus() AND the window is out of focus. the events evaporate into the ether.
  *   - however, if calling window.focus() programmatically prior to programmatic element.focus() calls will fire all events as if the window is natively in focus
*/
const { _ } = Cypress

const chaiSubset = require('chai-subset')

chai.use(chaiSubset)

const windowHasFocus = function () {
  if (top.document.hasFocus()) return true

  let hasFocus = false

  window.addEventListener('focus', function () {
    hasFocus = true
  })

  window.focus()

  return hasFocus
}

const requireWindowInFocus = () => {
  let hasFocus = windowHasFocus()

  if (!hasFocus) {
    expect(hasFocus, 'this test requires the window to be in focus').ok
  }
}

it('can intercept blur/focus events', () => {
  // Browser must be in focus

  const focus = cy.spy(window.top.HTMLElement.prototype, 'focus')
  const blur = cy.spy(window.top.HTMLElement.prototype, 'blur')

  const handleFocus = cy.stub().as('handleFocus')
  const handleBlur = cy.stub().as('handleBlur')

  const resetStubs = () => {
    focus.resetHistory()
    blur.resetHistory()
    handleFocus.resetHistory()
    handleBlur.resetHistory()
  }

  cy
  .visit('http://localhost:3500/fixtures/active-elements.html')
  .then(() => {
    requireWindowInFocus()

    expect(cy.getFocused()).to.be.null

    // programmatically focus the first, then second input element

    const one = cy.$$('#one')[0]
    const two = cy.$$('#two')[0]

    one.addEventListener('focus', handleFocus)
    two.addEventListener('focus', handleFocus)
    one.addEventListener('blur', handleBlur)
    two.addEventListener('blur', handleBlur)

    one.focus()

    expect(focus).to.calledOnce
    expect(handleFocus).calledOnce
    expect(blur).not.called
    expect(handleBlur).not.called

    resetStubs()

    one.focus()

    expect(focus).to.calledOnce
    expect(handleFocus).not.called
    expect(blur).not.called
    expect(handleBlur).not.called

    resetStubs()

    one.blur()

    expect(blur).calledOnce
    expect(handleBlur).calledOnce

    resetStubs()

    one.blur()

    expect(blur).calledOnce
    expect(handleBlur).not.called
  })
})

it('blur the activeElement when clicking the body', () => {
  cy
  .visit('http://localhost:3500/fixtures/active-elements.html')
  .then(() => {
    const events = []

    expect(cy.getFocused()).to.be.null

    const doc = cy.state('document')

    // programmatically focus the first, then second input element
    const $body = cy.$$('body')
    const $one = cy.$$('#one')
    const $two = cy.$$('#two');

    ['focus', 'blur'].forEach((evt) => {
      $one.on(evt, (e) => {
        events.push(e.originalEvent)
      })

      $two.on(evt, (e) => {
        events.push(e.originalEvent)
      })
    })

    $one.get(0).focus()
    $two.get(0).focus()

    cy.then(() => {
      // if we currently have focus it means
      // that the browser should fire the
      // native event immediately
      expect(events).to.have.length(3)

      expect(_.toPlainObject(events[0])).to.include({
        type: 'focus',
        isTrusted: true,
        target: $one.get(0),
      })

      expect(_.toPlainObject(events[1])).to.include({
        type: 'blur',
        isTrusted: true,
        target: $one.get(0),
      })

      expect(_.toPlainObject(events[2])).to.include({
        type: 'focus',
        isTrusted: true,
        target: $two.get(0),
      })
    })

    cy
    .get('body').click()
    .then(() => {
      expect(doc.activeElement).to.eq($body.get(0))
    })

    cy.then(() => {
      // if we had focus then no additional
      // focus event is necessary
      expect(events).to.have.length(4)

      expect(_.toPlainObject(events[3])).to.include({
        type: 'blur',
        isTrusted: true,
        target: $two.get(0),
      })
    })
  })
})

describe('polyfill programmatic blur events', () => {
  // restore these props for the rest of the tests
  let stubElementFocus
  let stubElementBlur
  let stubSVGFocus
  let stubSVGBlur
  let stubHasFocus
  let oldActiveElement = null

  const setActiveElement = (el) => {
    Object.defineProperty(cy.state('document'), 'activeElement', {
      get () {
        return el
      },
      configurable: true,
    })
  }

  beforeEach(() => {
    oldActiveElement = Object.getOwnPropertyDescriptor(window.Document.prototype, 'activeElement')

    // simulate window being out of focus by overwriting
    // the focus/blur methods on HTMLElement
    stubHasFocus = cy.stub(window.top.document, 'hasFocus').returns(false)

    stubElementFocus = cy.stub(window.top.HTMLElement.prototype, 'focus')
    stubElementBlur = cy.stub(window.top.HTMLElement.prototype, 'blur')
    stubSVGFocus = cy.stub(window.top.SVGElement.prototype, 'focus')
    stubSVGBlur = cy.stub(window.top.SVGElement.prototype, 'blur')
  })

  afterEach(() => {
    Object.defineProperty(window.Document.prototype, 'activeElement', oldActiveElement)
    stubHasFocus.restore()
    stubElementFocus.restore()
    stubElementBlur.restore()
    stubSVGFocus.restore()
    stubSVGBlur.restore()
  })

  // https://github.com/cypress-io/cypress/issues/1486
  it('simulated events when window is out of focus when .focus called', () => {
    cy
    .visit('http://localhost:3500/fixtures/active-elements.html')
    .then(() => {
      // programmatically focus the first, then second input element
      const $one = cy.$$('#one')
      const $two = cy.$$('#two')

      const stub = cy.stub().as('focus/blur event').callsFake(() => {
        Cypress.log({})
      });

      ['focus', 'blur'].forEach((evt) => {
        $one.on(evt, stub)

        return $two.on(evt, stub)
      })

      $one.get(0).focus()
      // a hack here because we nuked the real .focus
      setActiveElement($one.get(0))

      $two.get(0).focus()
      // cy.get('#two').click()

      const getEvent = (n) => {
        return stub.getCall(n).args[0].originalEvent
      }

      cy.wrap(null).then(() => {
        expect(stub).to.be.calledThrice

        expect(getEvent(0)).to.containSubset({
          type: 'focus',
          target: $one.get(0),
          isTrusted: false,
        })

        expect(getEvent(1)).to.containSubset({
          type: 'blur',
          target: $one.get(0),
          isTrusted: false,
        })

        expect(getEvent(2)).to.containSubset({
          type: 'focus',
          target: $two.get(0),
          isTrusted: false,
        })
      })
      .then(() => {
        stub.resetHistory()

        setActiveElement($two.get(0))

        $two.get(0).focus()

        expect(stub, 'should not send focus if already focused el').not.called
      })
    })
  })

  // https://github.com/cypress-io/cypress/issues/1176
  it('simulated events when window is out of focus when .blur called', () => {
    cy
    .visit('http://localhost:3500/fixtures/active-elements.html')
    .then(() => {
      // programmatically focus the first, then second input element
      const $one = cy.$$('#one')
      const $two = cy.$$('#two')

      const stub = cy.stub().as('focus/blur event');

      ['focus', 'blur'].forEach((evt) => {
        $one.on(evt, stub)

        $two.on(evt, stub)
      })

      $one.get(0).focus()

      // a hack here because we nuked the real .focus
      setActiveElement($one.get(0))

      $one.get(0).blur()

      cy.then(() => {
        expect(stub).calledTwice

        expect(_.toPlainObject(stub.getCall(0).args[0].originalEvent)).to.containSubset({
          type: 'focus',
          target: $one.get(0),
          isTrusted: false,
        })

        expect(_.toPlainObject(stub.getCall(1).args[0].originalEvent)).to.containSubset({
          type: 'blur',
          target: $one.get(0),
          isTrusted: false,
        })
      })

      .then(() => {
        stub.resetHistory()

        setActiveElement(cy.$$('body').get(0))

        $one.get(0).blur()

        expect(stub, 'should not send blur if not focused el').not.called
      })
    })
  })

  // https://github.com/cypress-io/cypress/issues/1486
  it('SVGElement simulated events when window is out of focus when .focus called', () => {
    cy
    .visit('http://localhost:3500/fixtures/active-elements.html')
    .then(() => {
      // programmatically focus the first, then second input element

      const $one = cy.$$(`<svg id="svg-one" tabindex width="100" height="100">
      <circle cx="50" cy="50" r="40" stroke="green" stroke-width="4" fill="yellow" />
    </svg>`).appendTo(cy.$$('body'))
      const $two = cy.$$(`<svg id="svg-two" tabindex width="100" height="100">
      <circle cx="50" cy="50" r="40" stroke="green" stroke-width="4" fill="yellow" />
    </svg>`).appendTo(cy.$$('body'))

      const stub = cy.stub().as('focus/blur event').callsFake(() => {
        Cypress.log({})
      });

      ['focus', 'blur'].forEach((evt) => {
        $one.on(evt, stub)

        return $two.on(evt, stub)
      })

      $one.get(0).focus()
      // a hack here because we nuked the real .focus
      setActiveElement($one.get(0))

      $two.get(0).focus()
      // cy.get('#two').click()

      const getEvent = (n) => {
        return stub.getCall(n).args[0].originalEvent
      }

      cy.wrap(null).then(() => {
        expect(stub).to.be.calledThrice

        expect(getEvent(0)).to.containSubset({
          type: 'focus',
          target: $one.get(0),
          isTrusted: false,
        })

        expect(getEvent(1)).to.containSubset({
          type: 'blur',
          target: $one.get(0),
          isTrusted: false,
        })

        expect(getEvent(2)).to.containSubset({
          type: 'focus',
          target: $two.get(0),
          isTrusted: false,
        })
      })
      .then(() => {
        stub.resetHistory()

        setActiveElement($two.get(0))

        $two.get(0).focus()

        expect(stub, 'should not send focus if already focused el').not.called
      })
    })
  })

  // https://github.com/cypress-io/cypress/issues/1176
  it('SVGElement simulated events when window is out of focus when .blur called', () => {
    cy
    .visit('http://localhost:3500/fixtures/active-elements.html')
    .then(() => {
      // programmatically focus the first, then second input element

      const $one = cy.$$(`<svg id="svg-one" tabindex width="100" height="100">
      <circle cx="50" cy="50" r="40" stroke="green" stroke-width="4" fill="yellow" />
    </svg>`).appendTo(cy.$$('body'))
      const $two = cy.$$(`<svg id="svg-two" tabindex width="100" height="100">
      <circle cx="50" cy="50" r="40" stroke="green" stroke-width="4" fill="yellow" />
    </svg>`).appendTo(cy.$$('body'))
      const stub = cy.stub().as('focus/blur event');

      ['focus', 'blur'].forEach((evt) => {
        $one.on(evt, stub)

        $two.on(evt, stub)
      })

      $one.get(0).focus()

      // a hack here because we nuked the real .focus
      setActiveElement($one.get(0))

      $one.get(0).blur()

      cy.then(() => {
        expect(stub).calledTwice

        expect(_.toPlainObject(stub.getCall(0).args[0].originalEvent)).to.containSubset({
          type: 'focus',
          target: $one.get(0),
          isTrusted: false,
        })

        expect(_.toPlainObject(stub.getCall(1).args[0].originalEvent)).to.containSubset({
          type: 'blur',
          target: $one.get(0),
          isTrusted: false,
        })
      })

      .then(() => {
        stub.resetHistory()

        setActiveElement(cy.$$('body').get(0))

        $one.get(0).blur()

        expect(stub, 'should not send blur if not focused el').not.called
      })
    })
  })

  it('document.hasFocus() always returns true', () => {
    cy.visit('http://localhost:3500/fixtures/active-elements.html')
    cy.document().then((doc) => {
      expect(doc.hasFocus(), 'hasFocus returns true').eq(true)
    })
  })

  it('does not send focus events for non-focusable elements', () => {
    cy.visit('http://localhost:3500/fixtures/active-elements.html')
    .then(() => {
      cy.$$('<div id="no-focus">clearly not a focusable element</div>')
      .appendTo(cy.$$('body'))

      const stub = cy.stub()
      const el1 = cy.$$('#no-focus')
      const win = cy.$$(cy.state('window'))

      win.on('focus', stub)
      el1.on('focus', stub)
      el1[0].focus()

      expect(stub).not.called
    })
  })
})

// TODO(webkit): fix+unskip for webkit release
describe('intercept blur methods correctly', { browser: '!webkit' }, () => {
  beforeEach(() => {
    cy.visit('http://localhost:3500/fixtures/active-elements.html').then(() => {
      top.focus()

      cy.$$('input:first')[0].focus()
      cy.document().then((doc) => {
        // we need to wait for initial selectionchange event on input:first
        // which sets up the state for the tests
        // NOTE: initial selectionchange event does not fire in firefox
        if (!Cypress.isBrowser({ family: 'firefox' })) {
          return new Promise((resolve) => doc.onselectionchange = resolve)
        }
      })

      cy.document().then((doc) => {
        doc.onselectionchange = cy.stub()
        .as('selectionchange')
      })
    })
  })

  it('focus  <a>', () => {
    const $el = cy.$$('<a href="#">foo</a>')

    $el.appendTo(cy.$$('body'))

    cy.wrap($el[0]).focus()
    .should('have.focus')

    if (Cypress.isBrowser('firefox')) {
      cy.wait(0).get('@selectionchange').should('be.called')

      return
    }

    cy.wait(10).get('@selectionchange').should('not.be.called')
  })

  it('focus <select>', () => {
    const $el = cy.$$('<select>')

    $el.appendTo(cy.$$('body'))
    $el[0].focus()
    cy.wrap($el[0]).focus()
    .should('have.focus')

    if (Cypress.isBrowser('firefox')) {
      cy.wait(0).get('@selectionchange').should('be.called')

      return
    }

    cy.wait(10).get('@selectionchange').should('not.be.called')
  })

  it('focus <button>', () => {
    const $el = cy.$$('<button/>')

    $el.appendTo(cy.$$('body'))
    $el[0].focus()
    cy.wrap($el[0]).focus()
    .should('have.focus')

    if (Cypress.isBrowser('firefox')) {
      cy.wait(0).get('@selectionchange').should('be.called')

      return
    }

    cy.wait(10).get('@selectionchange').should('not.be.called')
  })

  it('focus <iframe>', () => {
    const $el = cy.$$('<iframe src="" />')

    $el.appendTo(cy.$$('body'))
    $el[0].focus()
    cy.wrap($el[0]).focus()
    .should('have.focus')

    cy.wait(0).get('@selectionchange').should('not.be.called')
  })

  it('focus [tabindex]', () => {
    const $el = cy.$$('<div tabindex="1">tabindex</div>')

    $el.appendTo(cy.$$('body'))
    $el[0].focus()

    if (Cypress.isBrowser('firefox')) {
      cy.wait(0).get('@selectionchange').should('be.called')

      return
    }

    cy.wait(0).get('@selectionchange').should('not.be.called')
  })

  it('focus <textarea>', () => {
    const $el = cy.$$('<textarea/>')

    $el.appendTo(cy.$$('body'))
    $el[0].focus()
    cy.wrap($el[0]).focus()
    .should('have.focus')

    cy.get('@selectionchange').should('be.called')
  })

  it('focus [contenteditable]', () => {
    const $el = cy.$$('<div contenteditable>contenteditable</div>')

    $el.appendTo(cy.$$('body'))
    $el[0].focus()

    cy.get('@selectionchange').should('be.called')
  })

  it('cannot focus a [contenteditable] child', () => {
    const outer = cy.$$('<div contenteditable>contenteditable</div>').appendTo(cy.$$('body'))
    const inner = cy.$$('<div>first inner contenteditable</div>').appendTo(outer)

    cy.$$('<div>second inner contenteditable</div>').appendTo(outer)

    cy.get('input:first').focus()
    .wait(0)
    .get('@selectionchange').then((stub) => stub.resetHistory())

    cy.wrap(inner).should(($el) => $el.focus)
    .wait(0)

    cy.get('input:first').should('have.focus')

    cy.get('@selectionchange').should('not.be.called')
  })

  it('focus svg', () => {
    const $svg = cy.$$(`<svg tabindex="1" width="900px" height="500px" viewBox="0 0 95 50" style="border: solid red 1px;"
      xmlns="http://www.w3.org/2000/svg">
     <g data-Name="group" stroke="green" fill="white" stroke-width="5" data-tabindex="0" >
       <a xlink:href="#">
         <circle cx="20" cy="25" r="5" data-Name="shape 1"  data-tabindex="0" />
       </a>
       <a xlink:href="#">
         <circle cx="40" cy="25" r="5" data-Name="shape 2"  data-tabindex="0" />
       </a>
       <a xlink:href="#">
         <circle cx="60" cy="25" r="5" data-Name="shape 3" data-tabindex="0" />
       </a>
       <a xlink:href="#">
         <circle cx="80" cy="25" r="5" data-Name="shape 4" data-tabindex="0" />
       </a>
     </g>
  </svg>`).appendTo(cy.$$('body'))

    cy.wrap($svg).focus().should('have.focus')
  })

  it('focus area', () => {
    cy.visit('http://localhost:3500/fixtures/active-elements.html').then(() => {
      cy.$$(`
      <map name="map">
      <area shape="circle" coords="0,0,100"
      href="#"
      target="_blank" alt="area" />
      </map>
      <img usemap="#map" src="/fixtures/media/cypress.png" alt="image" />
      `).appendTo(cy.$$('body'))

      cy.get('area')

      // make sure the element can receive focus then reset activeElement with blur
      // without this firefox can fail due to <area> not being ready to receive focus
      // seems unrelated to 'load' state
      .should(($el) => {
        $el.focus()
        expect($el).be.focused
        $el.blur()
      })

      // do the actual test now
      .focus()
      .should('have.focus')
    })
  })

  // W3C Hidden @see html.spec.whatwg.org/multipage/interaction.html#focusable-area
  // fix https://github.com/cypress-io/cypress/issues/4898
  it('does not send focus events for focusable elements that are w3c hidden', () => {
    cy.visit('http://localhost:3500/fixtures/active-elements.html')
    .then(() => {
      cy.$$('<input style="visibility:hidden" id="no-focus-1"/>')
      .appendTo(cy.$$('body'))

      cy.$$('<input style="display:none" id="no-focus-2"/>')
      .appendTo(cy.$$('body'))

      cy.$$('<div style="visibility:hidden"><input id="no-focus-3"/></div>')
      .appendTo(cy.$$('body'))

      cy.$$('<div style="display:none"><input id="no-focus-4"/></div>')
      .appendTo(cy.$$('body'))

      const stub = cy.stub().as('focus')

      cy.$$('#no-focus-1').on('focus', stub).get(0).focus()
      cy.$$('#no-focus-2').on('focus', stub).get(0).focus()
      cy.$$('#no-focus-3').on('focus', stub).get(0).focus()
      cy.$$('#no-focus-4').on('focus', stub).get(0).focus()

      expect(stub).not.called

      cy.get('#no-focus-1').should('not.be.visible')
      cy.get('#no-focus-2').should('not.be.visible')
      cy.get('#no-focus-3').should('not.be.visible')
      cy.get('#no-focus-4').should('not.be.visible')
    })
  })

  // W3C Hidden @see html.spec.whatwg.org/multipage/interaction.html#focusable-area
  // fix https://github.com/cypress-io/cypress/issues/4898
  it('does send focus events for focusable elements that are 0x0 size', () => {
    cy.visit('http://localhost:3500/fixtures/active-elements.html')
    .then(() => {
      cy.$$('<input style="width:0;height:0;padding:0;margin:0;border:0;outline:0" id="focus-1"/>')
      .appendTo(cy.$$('body'))

      const stub = cy.stub()

      cy.$$('#focus-1')
      .on('focus', stub)
      .get(0).focus()

      expect(stub).calledOnce

      cy.get('#focus-1').should('not.be.visible')
    })
  })
})
