/**
 * Copyright 2019 Adobe. All rights reserved.
 * This file is licensed to you under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License. You may obtain a copy
 * of the License at http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software distributed under
 * the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS
 * OF ANY KIND, either express or implied. See the License for the specific language
 * governing permissions and limitations under the License.
 */

import {helpers} from '../../../coral-utils/src/tests/helpers';
import {BaseComponent} from '../../../coral-base-component';
import {BaseOverlay} from '../../../coral-base-overlay';
import {commons} from '../../../coral-utils';

/**
 Get the zIndex of an element

 @param {HTMLElement} element  The element to get the Zindex of

 @returns {Number} The element's zIndex
 */
const zIndex = (element) => {
  const elZIndex = element.style.zIndex;
  if (elZIndex === '') {
    return -1;
  } else {
    return parseFloat(elZIndex);
  }
};

describe('BaseOverlay', function () {
  var transitionEnd;

  // Non Modal overlay
  class OverlayDummy1 extends BaseOverlay(BaseComponent(HTMLElement)) {
    constructor() {
      super();

      this._trapFocus = 'on';
      this._returnFocus = 'on';
      this._focusOnShow = 'on';

      this._elements.content = this.querySelector('coral-overlay-dummy-content') || document.createElement('coral-overlay-dummy-content');
    }

    get content() {
      return this._getContentZone(this._elements.content);
    }

    set content(value) {
      this._setContentZone('content', value, {
        handle: 'content',
        tagName: 'coral-overlay-dummy-content',
        insert: function (content) {
          this.appendChild(content);
        }
      });
    }

    connectedCallback() {
      super.connectedCallback();

      this.content = this._elements.content;
    }
  }

  class OverlayDummy2 extends OverlayDummy1 {
    constructor() {
      super();
    }

    get open() {
      return super.open;
    }

    set open(value) {
      super.open = value;

      if (this.open) {
        // Show the backdrop when we're shown
        // This makes the overlay "modal"
        this._showBackdrop();
      }
    }
  }

  window.customElements.define('coral-overlay-dummy1', OverlayDummy1);
  window.customElements.define('coral-overlay-dummy2', OverlayDummy2);

  before(function () {
    transitionEnd = commons.transitionEnd;

    // mock transitionEnd for the tests (simulate transition ended after 100 ms)
    commons.transitionEnd = function (el, cb) {
      window.setTimeout(function () {
        cb();
      }, 100);
    }
  });

  after(function () {
    // reset to original transitionEnd impl.
    commons.transitionEnd = transitionEnd;
  });

  // Close and remove the instance
  function cleanUpInstance(instance) {
    if (!instance) {
      return;
    }

    instance.remove();
  }

  /**
   Test if the provided backdrop element is hidden or hiding
   */
  function backdropOpen(element) {
    return element.style.display !== 'none' && element._isOpen;
  }

  // Hold the instances
  var overlay;
  var overlay1;
  var overlay2;
  var overlay3;

  beforeEach(function () {
    overlay = helpers.build(new OverlayDummy2());
  });

  // Clean up
  afterEach(function () {
    cleanUpInstance(overlay);
    cleanUpInstance(overlay1);
    cleanUpInstance(overlay2);
    cleanUpInstance(overlay3);
  });

  describe('Namespace', function () {
    it('should define the trapFocus in an enum', function () {
      expect(OverlayDummy1.trapFocus).to.exist;
      expect(OverlayDummy1.trapFocus.ON).to.equal('on');
      expect(OverlayDummy1.trapFocus.OFF).to.equal('off');
      expect(Object.keys(OverlayDummy1.trapFocus).length).to.equal(2);
    });

    it('should define the returnFocus in an enum', function () {
      expect(OverlayDummy1.returnFocus).to.exist;
      expect(OverlayDummy1.returnFocus.ON).to.equal('on');
      expect(OverlayDummy1.returnFocus.OFF).to.equal('off');
      expect(Object.keys(OverlayDummy1.returnFocus).length).to.equal(2);
    });

    it('should define the focusOnShow in an enum', function () {
      expect(OverlayDummy1.focusOnShow).to.exist;
      expect(OverlayDummy1.focusOnShow.ON).to.equal('on');
      expect(OverlayDummy1.focusOnShow.OFF).to.equal('off');
      expect(Object.keys(OverlayDummy1.focusOnShow).length).to.equal(2);
    });
  });

  describe('Instantiation', function () {
    it('should have a classname', function () {
      overlay1 = new OverlayDummy1();
      helpers.target.appendChild(overlay1);

      overlay2 = new OverlayDummy2();
      helpers.target.appendChild(overlay2);

      expect(overlay1.classList.contains('_coral-BaseOverlay'));
      expect(overlay2.classList.contains('_coral-BaseOverlay'));
    });
  });

  describe('API', function () {
    describe('#_isTopMost()', function () {
      it('should know if it is top most', function (done) {
        overlay1 = new OverlayDummy2();
        helpers.target.appendChild(overlay1);
        overlay1.open = true;

        overlay2 = new OverlayDummy2();
        helpers.target.appendChild(overlay2);
        overlay2.open = true;

        helpers.next(function () {
          expect(overlay1._isTopOverlay()).to.be.false;
          expect(overlay2._isTopOverlay()).to.be.true;

          // Clean up
          overlay1.open = false;
          overlay2.open = false;

          done();
        });
      });
    });

    describe('#open/show()/hide()', function () {
      it('should default to false', function () {
        expect(overlay.open).to.equal(false, 'Overlays initialize closed by default');
      });

      it('should be set to display:none after closing the overlay silently', function (done) {
        overlay.content.textContent = 'Overlay 1';

        overlay.on('coral-overlay:open', function () {
          // close silently
          overlay.set('open', false, true);
          expect(overlay.open).to.equal(false, 'overlay should be closed now');

          // we use transitionEnd instead of coral-overlay:close since the silent setter was used
          commons.transitionEnd(overlay, function () {
            expect(overlay.style.display).to.equal('none', 'overlay should be set to "display:none" now');
            expect(overlay.style.visibility).to.equal('visible', 'overlay should be set to "visibility:visible" now');

            done();
          });
        });

        overlay.open = true;
        expect(overlay.open).to.equal(true, 'overlay should be open now');
      });

      it('should not change hidden when show()/hide() called', function () {
        expect(overlay.hidden).to.equal(false);
        overlay.show();
        expect(overlay.hidden).to.equal(false);
        overlay.hide();
        expect(overlay.hidden).to.equal(false);
      });
    });

    describe('#focusOnShow', function () {
      it('should default to ON', function () {
        expect(overlay.focusOnShow).to.equal(overlay.constructor.focusOnShow.ON);
      });

      it('should focus the overlay when no content is focusable', function (done) {
        overlay.on('coral-overlay:open', function () {
          expect(document.activeElement).to.equal(overlay, 'Focus should fallback to the overlay itself');

          done();
        });

        overlay.show();
      });

      it('should focus the first tababble descendent when available', function (done) {
        overlay.content.insertAdjacentHTML('afterbegin', window.__html__['BaseOverlay.someButtons.html']);

        overlay.on('coral-overlay:open', function () {
          expect(document.activeElement.id).to.equal('button1', 'The first focusable element should get the focus');

          done();
        });

        overlay.show();
      });

      it('should accept an HTMLElement to focus', function (done) {
        overlay.content.insertAdjacentHTML('afterbegin', window.__html__['BaseOverlay.threeButtons.html']);

        var button2 = overlay.content.querySelector('#button2');

        overlay.focusOnShow = button2;

        overlay.on('coral-overlay:open', function () {
          expect(document.activeElement.id).to.equal(button2.id, 'The provided button should be focused');

          done();
        });

        overlay.show();
      });

      it('should fallback to the document body when the provided element is not focusable', function (done) {
        overlay.content.insertAdjacentHTML('afterbegin', window.__html__['BaseOverlay.threeButtons.html']);

        var div = overlay.content.querySelector('div');

        overlay.focusOnShow = div;

        overlay.on('coral-overlay:open', function () {
          expect(document.activeElement).to.equal(document.body, 'Focus should fallback to body');

          done();
        });

        overlay.show();
      });

      it('should focus the first item that matches a selector', function (done) {
        overlay.content.insertAdjacentHTML('afterbegin', window.__html__['BaseOverlay.threeButtons.html']);

        overlay.focusOnShow = '.demo-content button';

        overlay.on('coral-overlay:open', function () {
          expect(document.activeElement.id).to.equal('button1', 'Should focus the first item that matches the selctor');

          done();
        });

        overlay.show();
      });

      it('should default to the first tababble descendent when the selector is invalid', function (done) {
        overlay.content.insertAdjacentHTML('afterbegin', window.__html__['BaseOverlay.threeButtons.html']);

        overlay.focusOnShow = '#input';

        overlay.on('coral-overlay:open', function () {
          expect(document.activeElement.id).to.equal('button1', 'Should fallback to the first tabbable element');

          done();
        });

        overlay.show();
      });

      it('should not focus the contents if the selector matches a non focusable item', function (done) {
        overlay.content.insertAdjacentHTML('afterbegin', window.__html__['BaseOverlay.threeButtons.html']);
        overlay.focusOnShow = 'div';

        overlay.on('coral-overlay:open', function () {
          expect(document.activeElement).to.equal(document.body, 'Should focus the body since the div is not focusable');

          done();
        });

        overlay.show();
      });

      it('should default to the overlay when the selector is invalid (and no tabbable element is available)', function (done) {
        overlay.content.insertAdjacentHTML('afterbegin', window.__html__['BaseOverlay.nonTabbableContent.html']);

        overlay.focusOnShow = '#input';

        overlay.on('coral-overlay:open', function () {
          expect(document.activeElement).to.equal(overlay, 'Should fallback to the overlay element');

          done();
        });

        overlay.show();
      });

      it('should accept :first-child as a selector', function (done) {
        overlay.content.insertAdjacentHTML('afterbegin', window.__html__['BaseOverlay.someButtons.html']);
        overlay.focusOnShow = ':first-child';

        overlay.on('coral-overlay:open', function () {
          expect(document.activeElement).to.equal(overlay.content.firstChild, 'Should focus the first child');

          done();
        });

        overlay.show();
      });

      it('should focus on the last-of-type item when shown', function (done) {
        overlay.content.insertAdjacentHTML('afterbegin', window.__html__['BaseOverlay.someButtons.html']);
        overlay.focusOnShow = 'button:last-of-type';

        overlay.on('coral-overlay:open', function () {
          expect(document.activeElement.id).to.equal('button2', 'Should focus the last button');

          done();
        });

        overlay.show();
      });

      it('should not move focus when OFF', function (done) {
        overlay.content.insertAdjacentHTML('afterbegin', window.__html__['BaseOverlay.someButtons.html']);
        overlay.focusOnShow = overlay.constructor.focusOnShow.OFF;

        overlay.on('coral-overlay:open', function () {
          expect(document.activeElement).to.equal(document.body, 'Focus should remain on the body');

          done();
        });

        overlay.show();
      });
    });

    describe('#returnFocus', function () {
      it('should focus on previously focused element when hidden', function (done) {
        var button = document.createElement('button');
        helpers.target.appendChild(button);

        // Focus on the button; this becomes the focused item that should receive focus after the overlay is closed
        button.focus();

        // Spy on the focus method
        // We can't actually check if the button is focused because the browser window needs to be focused for that to happen
        var focusSpy = sinon.spy(button, 'focus');

        // we close it immediately after it opens
        overlay.on('coral-overlay:open', function () {
          // we wait for the component to deal with the focus before hiding it; returnFocusTo assumes that the component
          // was focused or its internals, otherwise it would decide not to focus the target
          expect(overlay).to.equal(document.activeElement, 'Overlay should be in focus');
          overlay.hide();
        });

        overlay.on('coral-overlay:beforeclose', function () {
          expect(overlay).to.equal(document.activeElement, 'Overlay should be in focus');
        });

        overlay.on('coral-overlay:close', function () {
          // See if our spy was called
          expect(focusSpy.callCount).to.equal(1, 'Focus should have been called once');
          expect(button).to.equal(document.activeElement, 'Focus should be restored to the button');

          done();
        });

        overlay.show();
      });
    });

    describe('#returnFocusTo()', function () {
      it('should return focus to the passed element', function (done) {
        var button1 = document.createElement('button');
        var button2 = document.createElement('button');
        helpers.target.appendChild(button1);
        helpers.target.appendChild(button2);

        // we move the focus to the 2nd button in order to test that returnFocusTo returns the focus to the provided
        // elements, instead of giving it back to the element that had focus before opening the overlay
        button2.focus();

        // Spy on the focus method; We can't actually check if the button is focused because the browser window needs to
        // be focused for that to happen
        var button1focusSpy = sinon.spy(button1, 'focus');
        var button2focusSpy = sinon.spy(button2, 'focus');

        // Tell the overlay to return focus to the button when hidden
        overlay.returnFocusTo(button1);

        // we close it immediately after it opens
        overlay.on('coral-overlay:open', function () {
          expect(overlay).to.equal(document.activeElement, 'Overlay should be in focus');
          overlay.hide();
        });

        overlay.on('coral-overlay:close', function () {
          expect(button1focusSpy.callCount).to.equal(1, 'focus() should have been called');
          expect(button2focusSpy.callCount).to.equal(0, 'focus() should not have been called');
          expect(document.activeElement).to.equal(button1, 'Focus returned to the button');

          done();
        });

        overlay.show();
      });

      it('should focus on the element passed to returnFocusTo() when hidden, even when element is not interactive', function (done) {
        var div = document.createElement('div');
        helpers.target.appendChild(div);

        // Spy on the focus method
        // We can't actually check if the div is focused because the browser window needs to be focused for that to happen
        var focusSpy = sinon.spy(div, 'focus');

        // Tell the overlay to return focus to the div when hidden
        overlay.returnFocusTo(div);

        overlay.on('coral-overlay:open', function () {
          expect(div.getAttribute('tabindex')).to.equal('-1', 'returnFocusTo element is focusable');
          expect(overlay).to.equal(document.activeElement, 'Overlay should be in focus');
          overlay.hide();
        });

        overlay.on('coral-overlay:close', function () {
          // See if our spy was called
          expect(focusSpy.callCount).to.equal(1, 'focus() should have been called once');

          // Dispatch a blur event from returnFocusTo element
          helpers.event('blur', div);

          helpers.next(function () {
            expect(div.getAttribute('tabindex')).to.equal(null, 'tabindex removed from non-interactive returnFocusTo on blur');

            done();
          });
        });

        overlay.show();
      });
    });

    describe('Testing getters', function () {
      it('should return element to focus when accessed using getters', function () {
        var button1 = document.createElement('button');
        var button2 = document.createElement('button');
        helpers.target.appendChild(button1);
        helpers.target.appendChild(button2);
        overlay.returnFocusTo(button1);
        overlay._elementToFocusWhenHidden = button2;
        expect(overlay.elementToFocusWhenHidden).to.equal(button2);
        expect(overlay.returnFocusToElement).to.equal(button1);
      });
    });
  });

  describe('Markup', function () {
  });

  describe('Events', function () {
    describe('#coral-overlay:_animate', () => {
      it('should trigger before any animation', (done) => {
        const animateOpenSpy = sinon.spy();

        overlay1 = new OverlayDummy2();
        overlay1.content.textContent = 'Overlay 1';
        overlay1._overlayAnimationTime = 100; // This test will use a commons.transitionEnd mock!
        helpers.target.appendChild(overlay1);

        overlay1.on('coral-overlay:_animate', animateOpenSpy);

        overlay1.open = true;
        overlay1.open = false;

        window.setTimeout(function () {
          expect(animateOpenSpy.callCount).to.equal(2);
          done();
        }, 200);
      });
    });

    describe('#coral-overlay:open', function () {
      it('should trigger "coral-overlay:open" event only after the transition is finished', function (done) {
        overlay1 = new OverlayDummy2();
        overlay1.content.textContent = 'Overlay 1';
        //overlay1._overlayAnimationTime = 100;
        helpers.target.appendChild(overlay1);

        var openHasBeenCalled = false;
        overlay1.on('coral-overlay:open', function () {
          openHasBeenCalled = true;
        });

        overlay1.open = true;
        expect(openHasBeenCalled).to.equal(false, '"coral-overlay:open" should only be fired after the transition is finished');

        // "coral-overlay:open" should only be called after animation is over
        window.setTimeout(function () {
          expect(openHasBeenCalled).to.equal(true, '"coral-overlay:open" should now have been called');
          done();
        }, 101);
      });

      it('should make sure that only one "coral-overlay:open" event is thrown at a time', function (done) {
        var beforeOpenSpy = sinon.spy();
        var beforeCloseSpy = sinon.spy();

        var openSpy = sinon.spy();
        var closeSpy = sinon.spy();

        overlay1 = new OverlayDummy2();
        overlay1.content.textContent = 'Overlay 1';
        overlay1._overlayAnimationTime = 100; // This test will use a commons.transitionEnd mock!
        helpers.target.appendChild(overlay1);

        overlay1.on('coral-overlay:beforeopen', beforeOpenSpy);
        overlay1.on('coral-overlay:beforeclose', beforeCloseSpy);
        overlay1.on('coral-overlay:open', openSpy);
        overlay1.on('coral-overlay:close', closeSpy);

        overlay1.open = true;
        overlay1.open = false;
        overlay1.open = true;

        window.setTimeout(function () {
          expect(beforeOpenSpy.callCount).to.equal(2, '"coral-overlay:beforeopen" should have been called three times!');
          expect(beforeCloseSpy.callCount).to.equal(1, '"coral-overlay:beforeclose" should have been called twice');
          expect(openSpy.callCount).to.equal(1, '"coral-overlay:open" should only be called "once"!');
          expect(closeSpy.callCount).to.equal(0, '"coral-overlay:close" should never be called as it is canceled by open before animation is done');
          done();
        }, 300);
      });
    });

    describe('#coral-overlay:close', function () {
      it('should trigger "coral-overlay:close" event only after the transition is finished', function (done) {
        overlay1 = new OverlayDummy2();
        overlay1.content.textContent = 'Overlay 1';
        //overlay1._overlayAnimationTime = 100;
        helpers.target.appendChild(overlay1);

        overlay1.open = true;

        var closeHasBeenCalled = false;
        overlay1.on('coral-overlay:close', function () {
          closeHasBeenCalled = true;
        });

        helpers.next(function () {
          overlay1.open = false;
          expect(closeHasBeenCalled).to.equal(false, '"coral-overlay:close" should only be called after transition is over');

          // "coral-overlay:close" should only be called after animation is over
          window.setTimeout(function () {
            expect(closeHasBeenCalled).to.equal(true, '"coral-overlay:close" should now have been called');
            done();
          }, 200);
        });
      });

      it('should make sure that only one "coral-overlay:close" event is thrown at a time', function (done) {
        var beforeOpenSpy = sinon.spy();
        var beforeCloseSpy = sinon.spy();
        var openSpy = sinon.spy();
        var closeSpy = sinon.spy();

        overlay1 = new OverlayDummy2();
        overlay1.content.textContent = 'Overlay 1';
        overlay1._overlayAnimationTime = 100; // This test will use a commons.transitionEnd mock!
        helpers.target.appendChild(overlay1);

        overlay1.on('coral-overlay:beforeopen', beforeOpenSpy);
        overlay1.on('coral-overlay:beforeclose', beforeCloseSpy);
        overlay1.on('coral-overlay:open', openSpy);
        overlay1.on('coral-overlay:close', closeSpy);

        overlay1.open = true;
        overlay1.open = false;

        window.setTimeout(function () {
          expect(beforeOpenSpy.callCount).to.equal(1, '"coral-overlay:beforeopen" should have been called twice!');
          expect(beforeCloseSpy.callCount).to.equal(1, '"coral-overlay:beforeclose" should have been called twice');
          expect(openSpy.callCount).to.equal(0, '"coral-overlay:open" should never be called as it is canceled by close before animation is done');
          expect(closeSpy.callCount).to.equal(1, '"coral-overlay:close" should only be called "once"!');
          done();
        }, 200);
      });
    });

    it('should be possible to toggle the overlay while it is still in the transition', function (done) {
      var openSpy = sinon.spy();
      var closeSpy = sinon.spy();

      overlay1 = new OverlayDummy2();
      overlay1.content.textContent = 'Overlay 1';
      overlay1._overlayAnimationTime = 100; // This test will use a commons.transitionEnd mock!
      helpers.target.appendChild(overlay1);

      helpers.next(function () {
        overlay1.on('coral-overlay:open', openSpy);
        overlay1.on('coral-overlay:close', closeSpy);

        overlay1.open = true;

        //make sure sync of "open" attribute has been called by waiting 2 frames
        helpers.next(function () {
          helpers.next(function () {
            overlay1.open = false;
            expect(openSpy.called).to.equal(false, '"coral-overlay:open" should never be called as it is canceled by close before animation is done');
            expect(closeSpy.called).to.equal(false, '"coral-overlay:close" should only be called once animation is over');

            // "coral-overlay:close" should only be called after animation is over
            window.setTimeout(function () {
              expect(openSpy.called).to.equal(false, '"coral-overlay:open" should still not be called as it was canceled by close before animation was done');
              expect(closeSpy.called).to.equal(true, '"coral-overlay:close" now should have been called');
              done();
            }, 300);
          });
        });
      });
    });

    it('should be possible to open/close overlay silently', function (done) {
      overlay1 = new OverlayDummy2();
      overlay1.content.textContent = 'Overlay 1';
      //overlay1._overlayAnimationTime = 100;
      helpers.target.appendChild(overlay1);

      overlay1.open = true;
      expect(overlay1.open).to.equal(true, 'overlay should be open now');

      var beforeCloseSpy = sinon.spy();
      overlay1.on('coral-overlay:beforeclose', beforeCloseSpy);

      var closeSpy = sinon.spy();
      overlay1.on('coral-overlay:close', closeSpy);

      helpers.next(function () {
        overlay1.set('open', false, true); // close silently
        expect(overlay1.open).to.equal(false, 'overlay should be closed now');

        // "coral-overlay:close" should only be called after animation is over
        window.setTimeout(function () {
          expect(closeSpy.callCount).to.equal(0, 'no "coral-overlay:close" event should have been fired');
          expect(beforeCloseSpy.callCount).to.equal(0, 'no "coral-overlay:beforeclose" event should have been fired');

          done();
        }, 101);
      });
    });
  });

  describe('User Interaction', function () {
    it('should call backdropClickedCallback on all overlays when backdrop clicked', function (done) {
      overlay1 = new OverlayDummy2();
      helpers.target.appendChild(overlay1);
      overlay1.backdropClickedCallback = sinon.spy();
      overlay1.open = true;

      overlay2 = new OverlayDummy2();
      helpers.target.appendChild(overlay2);
      overlay2.backdropClickedCallback = sinon.spy();
      overlay2.open = true;

      helpers.next(function () {
        document.querySelector('._coral-Underlay').click();

        expect(overlay1.backdropClickedCallback.callCount).to.equal(1);
        expect(overlay2.backdropClickedCallback.callCount).to.equal(1);

        // Clean up
        overlay1.open = false;
        overlay2.open = false;

        helpers.next(function () {
          // Wait for clean up to be complete
          done();
        });
      });
    });
  });

  describe('Implementation Details', function () {
    describe('focus()', function () {
      it('should keep focus on the container when focused', function (done) {
        overlay.content.insertAdjacentHTML('afterbegin', window.__html__['BaseOverlay.someButtons.html']);

        overlay.on('coral-overlay:open', function () {
          overlay.focus();
          expect(document.activeElement).to.equal(overlay);

          done();
        });

        overlay.show();
      });
    });

    describe('tabcapture', function () {
      it('should focus on the last focusable element when top tab capture focused', function (done) {
        overlay.insertAdjacentHTML('afterbegin', window.__html__['BaseOverlay.someButtons.html']);

        var button2 = overlay.querySelector('#button2');

        overlay.open = true;

        helpers.next(function () {
          overlay.querySelector('[coral-tabcapture]').focus();

          helpers.event('focus', overlay.querySelector('[coral-tabcapture]'));
          expect(document.activeElement).to.equal(button2);
          done();
        });
      });

      it('should focus on the first focusable element when intermediate tab capture focused', function (done) {
        overlay.insertAdjacentHTML('afterbegin', window.__html__['BaseOverlay.someButtons.html']);

        var button1 = overlay.querySelector('#button1');

        overlay.open = true;

        helpers.next(function () {
          overlay.querySelectorAll('[coral-tabcapture]')[1].focus();
          // for FF
          helpers.event('focus', overlay.querySelectorAll('[coral-tabcapture]')[1]);
          expect(document.activeElement).to.equal(button1);
          done();
        });
      });

      it('should focus on the last focusable element when last tab capture focused', function (done) {
        overlay = new OverlayDummy2();
        helpers.target.appendChild(overlay);

        overlay.insertAdjacentHTML('afterbegin', window.__html__['BaseOverlay.someButtons.html']);

        var button2 = overlay.querySelector('#button2');

        overlay.open = true;

        helpers.next(function () {
          overlay.querySelectorAll('[coral-tabcapture]')[2].focus();
          // for FF
          helpers.event('focus', overlay.querySelectorAll('[coral-tabcapture]')[2]);
          expect(document.activeElement).to.equal(button2);
          done();
        });
      });

      it('should position tabcapture elements correctly on show', function (done) {
        overlay = new OverlayDummy2();
        helpers.target.appendChild(overlay);

        overlay.appendChild(document.createElement('div'));

        overlay.open = true;
        helpers.next(function () {
          expect(overlay.lastElementChild).to.equal(overlay._elements.bottomTabCapture);
          expect(overlay.lastElementChild.previousElementSibling).to.equal(overlay._elements.intermediateTabCapture);

          // Clean up
          overlay.open = false;

          helpers.next(function () {
            // Wait for clean up to be complete
            done();
          });
        });
      });

      it('should position tabcapture elements correctly on show if their order is changed', function (done) {
        overlay = new OverlayDummy2();
        helpers.target.appendChild(overlay);

        overlay.insertBefore(document.createElement('div'), overlay._elements.bottomTabCapture);

        overlay.open = true;
        helpers.next(function () {
          expect(overlay.lastElementChild).to.equal(overlay._elements.bottomTabCapture);
          expect(overlay.lastElementChild.previousElementSibling).to.equal(overlay._elements.intermediateTabCapture);

          // Clean up
          overlay.open = false;

          helpers.next(function () {
            // Wait for clean up to be complete
            done();
          });
        });
      });
    });

    describe('Backdrop', function () {
      it('should appear above other overlays with a correctly positioned backdrop', function (done) {
        overlay1 = new OverlayDummy2();
        overlay1.content.textContent = 'Overlay 1';
        helpers.target.appendChild(overlay1);

        overlay2 = new OverlayDummy2();
        overlay2.content.textContent = 'Overlay 2';
        helpers.target.appendChild(overlay2);

        overlay1.open = true;
        overlay2.open = true;

        helpers.next(function () {
          var backdrop = document.querySelector('._coral-Underlay');
          expect(backdrop).to.not.be.null;

          // Make sure the top overlay is above the bottom overlay
          expect(zIndex(overlay2)).to.be.greaterThan(zIndex(overlay1));

          // Make sure the backdrop is positioned under the top overlay
          expect(zIndex(backdrop)).to.be.lessThan(zIndex(overlay2), 'backdrop zIndex as compared to top overlay when both visible');

          // Make sure the backdrop is positioned above the bottom overlay
          expect(zIndex(backdrop)).to.be.greaterThan(zIndex(overlay1), 'backdrop zIndex as compared to bottom overlay when both visible');

          // Clean up
          overlay1.open = false;
          overlay2.open = false;

          helpers.next(function () {
            // Wait for clean up to be complete
            done();
          });
        });
      });

      it('should hide backdrop when overlay is removed from DOM', function (done) {
        overlay = new OverlayDummy2();
        helpers.target.appendChild(overlay);

        overlay.open = true;

        helpers.next(function () {
          var backdrop = document.querySelector('._coral-Underlay');
          expect(backdrop).to.not.be.null;

          // Make sure the backdrop is visible
          expect(backdropOpen(backdrop)).to.equal(true, 'backdrop visibility when overlay shown');

          // Remove from the DOM
          helpers.target.removeChild(overlay);

          helpers.next(function () {
            // Make sure the backdrop is hidden
            expect(backdropOpen(backdrop)).to.equal(false, 'backdrop visibility when overlay removed from DOM');

            done();
          });
        });
      });

      it('should hide backdrop when hiding overlay (even if overlay is directly detached afterwards)', function (done) {
        overlay = new OverlayDummy2();
        helpers.target.appendChild(overlay);

        overlay.open = true;

        helpers.next(function () {
          var backdrop = document.querySelector('._coral-Underlay');
          expect(backdrop).to.not.be.null;

          // Make sure the backdrop is visible
          expect(backdropOpen(backdrop)).to.equal(true, 'backdrop visibility when overlay shown');

          // hide the overlay
          overlay.open = false;

          // Remove from the DOM
          helpers.target.removeChild(overlay);

          helpers.next(function () {
            // Make sure the backdrop is hidden
            expect(backdropOpen(backdrop)).to.equal(false, 'backdrop visibility when overlay removed from DOM');

            done();
          });
        });
      });

      it('should not hide backdrop when multiple modal overlays are open and one is closed', function (done) {
        overlay1 = new OverlayDummy2();
        helpers.target.appendChild(overlay1);

        overlay2 = new OverlayDummy2();
        helpers.target.appendChild(overlay2);

        overlay1.open = true;
        overlay2.open = true;

        helpers.next(function () {
          var backdrop = document.querySelector('._coral-Underlay');
          expect(backdrop).to.not.be.null;

          // Make sure the backdrop is visible
          expect(backdropOpen(backdrop)).to.equal(true, 'backdrop visibility when both overlays visible');

          // Make sure the backdrop positioned under the top overlay
          expect(zIndex(backdrop)).to.be.lessThan(zIndex(overlay2), 'backdrop zIndex as compared to top overlay when both visible');

          // Make sure the backdrop positioned above the bottom overlay
          expect(zIndex(backdrop)).to.be.greaterThan(zIndex(overlay1), 'backdrop zIndex as compared to bottom overlay when both visible');

          // Hide top overlay
          overlay2.open = false;

          helpers.next(function () {
            // Make sure the backdrop is visible
            expect(backdropOpen(backdrop)).to.equal(true, 'backdrop visibility when top overlay hidden');

            // Make sure it's positioned under the bottom overlay
            expect(zIndex(backdrop)).to.be.lessThan(zIndex(overlay1), 'backdrop zIndex as compared to bottom overlay when top overlay closed');

            // Hide the bottom overlay
            overlay1.open = false;

            helpers.next(function () {
              // Make sure the backdrop is hidden
              expect(backdropOpen(backdrop)).to.equal(false, 'backdrop visibility when both overlays hidden');

              // Make sure it's positioned under the bottom overlay
              expect(zIndex(backdrop)).to.be.lessThan(zIndex(overlay1), 'backdrop zIndex as compared to bottom overlay when both overlays closed');

              done();
            });
          });
        });
      });

      it('should not hide backdrop when a non-modal overlay is closed before a modal overlay is opened', function (done) {
        overlay1 = new OverlayDummy1();
        helpers.target.appendChild(overlay1);

        overlay2 = new OverlayDummy2();
        helpers.target.appendChild(overlay2);

        overlay2.open = true;

        helpers.next(function () {
          var backdrop = document.querySelector('._coral-Underlay');
          expect(backdrop).to.not.be.null;

          // Make sure the backdrop is open
          expect(backdropOpen(backdrop)).to.equal(true, 'backdrop visibility after modal overlay shown');

          // Hide modal overlay
          overlay2.open = false;

          helpers.next(function () {
            // Make sure the backdrop is not open
            expect(backdropOpen(backdrop)).to.equal(false, 'backdrop visibility before modal overlay shown again');

            // Show non-modal overlay
            overlay1.open = true;

            helpers.next(function () {
              // Make sure the backdrop is not open
              expect(backdropOpen(backdrop)).to.equal(false, 'backdrop visibility before modal overlay shown again');

              // Show modal overlay
              overlay2.open = true;

              // Hide non-modal overlay
              overlay1.open = false;

              setTimeout(function () {
                // Make sure the backdrop is visible
                expect(backdropOpen(backdrop)).to.equal(true, 'backdrop visibility when non-modal overlay hidden');

                done();
              }, BaseOverlay.FADETIME + 50);
            });
          });
        });
      });

      it('should correctly position the backdrop when a middle overlay is closed', function (done) {
        overlay1 = new OverlayDummy2();
        helpers.target.appendChild(overlay1);
        overlay1.open = true;

        overlay2 = new OverlayDummy2();
        helpers.target.appendChild(overlay2);
        overlay2.open = true;

        overlay3 = new OverlayDummy2();
        helpers.target.appendChild(overlay3);
        overlay3.open = true;

        helpers.next(function () {
          var backdrop = document.querySelector('._coral-Underlay');
          expect(backdrop).to.not.be.null;

          // Make sure the backdrop is visible
          expect(backdropOpen(backdrop)).to.equal(true, 'backdrop visibility initially');

          // Make sure it's positioned under the top overlay
          expect(zIndex(backdrop)).to.be.lessThan(zIndex(overlay3), 'backdrop zIndex as compared to top overlay');

          // Make sure it's positioned above the middle overlay
          expect(zIndex(backdrop)).to.be.greaterThan(zIndex(overlay2), 'backdrop zIndex as compared to middle overlay');

          // Make sure it's positioned above the bottom overlay
          expect(zIndex(backdrop)).to.be.greaterThan(zIndex(overlay1), 'backdrop zIndex as compared to bottom overlay');

          // Hide middle overlay
          overlay2.open = false;

          helpers.next(function () {
            // Make sure the backdrop is visible
            expect(backdropOpen(backdrop)).to.equal(true, 'backdrop visibility when middle overlay hidden');

            // Make sure it's positioned under the top overlay
            expect(zIndex(backdrop)).to.be.lessThan(zIndex(overlay3), 'backdrop zIndex as compared to top overlay');

            // Make sure it's positioned above the bottom overlay
            expect(zIndex(backdrop)).to.be.greaterThan(zIndex(overlay1), 'backdrop zIndex as compared to bottom overlay');

            // Hide the bottom overlay
            overlay1.open = false;

            helpers.next(function () {
              // Make sure the backdrop is visible
              expect(backdropOpen(backdrop)).to.equal(true, 'backdrop visibility when bottom overlay hidden');

              // Make sure it's positioned under the top overlay
              expect(zIndex(backdrop)).to.be.lessThan(zIndex(overlay3), 'backdrop zIndex as compared to top overlay');

              // Hide the top overlay
              overlay3.open = false;

              helpers.next(function () {
                // Make sure the backdrop is hidden
                expect(backdropOpen(backdrop)).to.equal(false, 'backdrop visibility when all overlays hidden');

                done();
              });
            });
          });
        });
      });

      it('should position the backdrop under the topmost overlay that does have a backdrop', function (done) {
        overlay1 = new OverlayDummy2();
        overlay1.open = true;
        helpers.target.appendChild(overlay1);

        // make two non modal overlays
        overlay2 = new OverlayDummy1();
        overlay2.open = true;
        helpers.target.appendChild(overlay2);

        overlay3 = new OverlayDummy1();
        overlay3.open = true;
        helpers.target.appendChild(overlay3);

        helpers.next(function () {
          var backdrop = document.querySelector('._coral-Underlay');
          expect(backdrop).to.not.be.null;

          // Make sure the backdrop is visible
          expect(backdropOpen(backdrop)).to.equal(true, 'backdrop visibility initially');

          expect(zIndex(backdrop)).to.equal(zIndex(overlay1) - 1, 'backdrop should be behind the modal dialog 1');

          overlay3.open = false;

          helpers.next(function () {
            expect(zIndex(backdrop)).to.equal(zIndex(overlay1) - 1, 'backdrop shouldstill be behind the modal dialog 1');
            done();
          });

        });
      });

      it('should hide backdrop when removed from the DOM while visible, show it again when reattached', function (done) {
        overlay = new OverlayDummy2();
        helpers.target.appendChild(overlay);

        overlay.open = true;

        helpers.next(function () {
          var backdrop = document.querySelector('._coral-Underlay');
          expect(backdrop).to.not.be.null;

          // Make sure the backdrop is visible
          expect(backdropOpen(backdrop)).to.equal(true, 'backdrop visibility when overlay shown');

          // Remove from the DOM
          helpers.target.removeChild(overlay);

          helpers.next(function () {
            // Make sure the backdrop is hidden
            expect(backdropOpen(backdrop)).to.equal(false, 'backdrop visibility when overlay removed from DOM');

            // Add back to the DOM
            helpers.target.appendChild(overlay);

            helpers.next(function () {
              // Make sure the backdrop is visible
              expect(backdropOpen(backdrop)).to.equal(true, 'backdrop visibility when overlay appended to DOM');

              done();
            });
          });
        });
      });

      it('should be considered top most when attached as visible', function (done) {
        // Add a overlay to the DOM, then make it visible
        overlay1 = new OverlayDummy2();
        helpers.target.appendChild(overlay1);
        overlay1.open = true;

        // Create a overlay, make it visible, but don't add it to the DOM
        overlay2 = new OverlayDummy2();
        overlay2.open = true;

        helpers.next(function () {
          // Add the visible overlay
          helpers.target.appendChild(overlay2);

          helpers.next(function () {
            // It should now be on top
            expect(zIndex(overlay2)).to.be.greaterThan(zIndex(overlay1));

            overlay2.open = false;

            helpers.next(function () {
              var backdrop = document.querySelector('._coral-Underlay');
              expect(backdrop).to.not.be.null;

              expect(zIndex(overlay1)).to.be.greaterThan(zIndex(backdrop));

              // Clean up
              overlay1.open = false;
              overlay2.open = false;

              helpers.next(function () {
                // Wait for clean up to be complete
                done();
              });
            });
          });
        });
      });

      it('should hide when done closing', function (done) {
        // Temporarily change the fade time to 1ms
        var FADETIME = BaseOverlay.FADETIME;
        BaseOverlay.FADETIME = 0;

        overlay = new OverlayDummy2();
        helpers.target.appendChild(overlay);

        helpers.next(function () {
          overlay1.open = true;
          helpers.next(function () {
            overlay.open = false;

            // Test if hidden after 10ms
            setTimeout(function () {
              expect(overlay.open).to.be.false;
              expect(overlay.style.display).to.equal('none');
              expect(overlay.style.visibility).to.equal('visible');

              // Restore fade time
              BaseOverlay.FADETIME = FADETIME;
              helpers.next(function () {
                // Wait for clean up to be complete
                done();
              });
            }, 101);
          });
        });
      });
    });
  });
});
