/**
 * @license
 * Copyright The Closure Library Authors.
 * SPDX-License-Identifier: Apache-2.0
 */

goog.module('goog.ui.ModalPopupTest');
goog.setTestOnly();

const EventType = goog.require('goog.events.EventType');
const GoogEventTarget = goog.require('goog.events.EventTarget');
const MockClock = goog.require('goog.testing.MockClock');
const ModalPopup = goog.require('goog.ui.ModalPopup');
const PopupBase = goog.require('goog.ui.PopupBase');
const State = goog.require('goog.a11y.aria.State');
const TagName = goog.require('goog.dom.TagName');
const Transition = goog.require('goog.fx.Transition');
const aria = goog.require('goog.a11y.aria');
const css3 = goog.require('goog.fx.css3');
const dispose = goog.require('goog.dispose');
const dom = goog.require('goog.dom');
const events = goog.require('goog.events');
const googString = goog.require('goog.string');
const style = goog.require('goog.style');
const testSuite = goog.require('goog.testing.testSuite');
const testingEvents = goog.require('goog.testing.events');

let popup;
let main;
let mockClock;

/** @implements {Transition} */
class MockTransition extends GoogEventTarget {
  constructor() {
    super();
    this.wasPlayed = false;
  }

  play() {
    this.wasPlayed = true;
  }

  stop() {}
}

testSuite({
  setUp() {
    main = /** @type {!Element} */ (dom.getElement('main'));
    mockClock = new MockClock(true);
  },

  tearDown() {
    dispose(popup);
    mockClock.dispose();
    aria.removeState(main, State.HIDDEN);
  },

  /** @suppress {checkTypes} suppression added to enable type checking */
  testOrientationChange() {
    let i = 0;
    popup = new ModalPopup();
    /** @suppress {visibility} suppression added to enable type checking */
    popup.resizeBackgroundTask_ = () => {
      i++;
    };
    popup.render();
    popup.setVisible(true);
    const event = new events.Event(
        EventType.ORIENTATIONCHANGE, popup.getDomHelper().getWindow());

    testingEvents.fireBrowserEvent(event);
    assertEquals(1, i);

    testingEvents.fireBrowserEvent(event);
    assertEquals(2, i);

    popup.setVisible(false);
    testingEvents.fireBrowserEvent(event);
    assertEquals(2, i);
  },

  testDispose() {
    popup = new ModalPopup();
    popup.render();

    dispose(popup);
    assertNull(dom.getElementByClass('goog-modalpopup-bg'));
    assertNull(dom.getElementByClass('goog-modalpopup'));
    assertEquals(0, dom.getElementsByTagNameAndClass(TagName.SPAN).length);
  },

  testRenderWithoutIframeMask() {
    popup = new ModalPopup();
    popup.render();

    assertEquals(
        0,
        dom.getElementsByTagNameAndClass(TagName.IFRAME, 'goog-modalpopup-bg')
            .length);

    const bg =
        dom.getElementsByTagNameAndClass(TagName.DIV, 'goog-modalpopup-bg');
    assertEquals(1, bg.length);
    const content = dom.getElementByClass('goog-modalpopup');
    assertNotNull(content);
    const tabCatcher = dom.getElementsByTagNameAndClass(TagName.SPAN);
    assertEquals(1, tabCatcher.length);

    assertTrue(dom.compareNodeOrder(bg[0], content) < 0);
    assertTrue(dom.compareNodeOrder(content, tabCatcher[0]) < 0);
    assertTrue(googString.isEmptyOrWhitespace(
        googString.makeSafe(aria.getState(main, State.HIDDEN))));
    popup.setVisible(true);
    assertTrue(googString.isEmptyOrWhitespace(googString.makeSafe(
        aria.getState(popup.getElementStrict(), State.HIDDEN))));
    assertEquals('true', aria.getState(main, State.HIDDEN));
    popup.setVisible(false);
    assertTrue(googString.isEmptyOrWhitespace(
        googString.makeSafe(aria.getState(main, State.HIDDEN))));
  },

  testRenderWithIframeMask() {
    popup = new ModalPopup(true);
    popup.render();

    const iframe =
        dom.getElementsByTagNameAndClass(TagName.IFRAME, 'goog-modalpopup-bg');
    assertEquals(1, iframe.length);
    const bg =
        dom.getElementsByTagNameAndClass(TagName.DIV, 'goog-modalpopup-bg');
    assertEquals(1, bg.length);
    const content = dom.getElementByClass('goog-modalpopup');
    assertNotNull(content);
    const tabCatcher = dom.getElementsByTagNameAndClass(TagName.SPAN);
    assertEquals(1, tabCatcher.length);

    assertTrue(dom.compareNodeOrder(iframe[0], bg[0]) < 0);
    assertTrue(dom.compareNodeOrder(bg[0], content) < 0);
    assertTrue(dom.compareNodeOrder(content, tabCatcher[0]) < 0);
    assertTrue(googString.isEmptyOrWhitespace(
        googString.makeSafe(aria.getState(main, State.HIDDEN))));
    popup.setVisible(true);
    assertTrue(googString.isEmptyOrWhitespace(googString.makeSafe(
        aria.getState(popup.getElementStrict(), State.HIDDEN))));
    assertEquals('true', aria.getState(main, State.HIDDEN));
    popup.setVisible(false);
    assertTrue(googString.isEmptyOrWhitespace(
        googString.makeSafe(aria.getState(main, State.HIDDEN))));
  },

  testRenderWithAriaState() {
    popup = new ModalPopup();
    popup.render();

    aria.setState(main, State.HIDDEN, true);
    popup.setVisible(true);
    assertEquals('true', aria.getState(main, State.HIDDEN));
    popup.setVisible(false);
    assertEquals('true', aria.getState(main, State.HIDDEN));

    aria.setState(main, State.HIDDEN, false);
    popup.setVisible(true);
    assertEquals('false', aria.getState(main, State.HIDDEN));
    popup.setVisible(false);
    assertEquals('false', aria.getState(main, State.HIDDEN));
  },

  testRenderDoesNotShowAnyElement() {
    popup = new ModalPopup(true);
    popup.render();

    const iframe =
        dom.getElementsByTagNameAndClass(TagName.IFRAME, 'goog-modalpopup-bg');
    assertFalse(style.isElementShown(iframe[0]));
    const bg =
        dom.getElementsByTagNameAndClass(TagName.DIV, 'goog-modalpopup-bg');
    assertFalse(style.isElementShown(bg[0]));
    assertFalse(style.isElementShown(dom.getElementByClass('goog-modalpopup')));
    const tabCatcher = dom.getElementsByTagNameAndClass(TagName.SPAN);
    assertFalse(style.isElementShown(tabCatcher[0]));
  },

  testIframeOpacityIsSetToZero() {
    popup = new ModalPopup(true);
    popup.render();

    const iframe = dom.getElementsByTagNameAndClass(
        TagName.IFRAME, 'goog-modalpopup-bg')[0];
    assertEquals(0, style.getOpacity(iframe));
  },

  testEventFiredOnShow() {
    popup = new ModalPopup(true);
    popup.render();

    let beforeShowCallCount = 0;
    const beforeShowHandler = () => {
      beforeShowCallCount++;
    };
    let showCallCount = false;
    /**
     * @suppress {strictPrimitiveOperators} suppression added to enable type
     * checking
     */
    const showHandler = () => {
      assertEquals(
          'BEFORE_SHOW is not dispatched before SHOW', 1, beforeShowCallCount);
      showCallCount++;
    };

    events.listen(popup, PopupBase.EventType.BEFORE_SHOW, beforeShowHandler);
    events.listen(popup, PopupBase.EventType.SHOW, showHandler);

    popup.setVisible(true);

    assertEquals(1, beforeShowCallCount);
    assertEquals(1, showCallCount);
  },

  testEventFiredOnHide() {
    popup = new ModalPopup(true);
    popup.render();
    popup.setVisible(true);

    let beforeHideCallCount = 0;
    const beforeHideHandler = () => {
      beforeHideCallCount++;
    };
    let hideCallCount = false;
    /**
     * @suppress {strictPrimitiveOperators} suppression added to enable type
     * checking
     */
    const hideHandler = () => {
      assertEquals(
          'BEFORE_HIDE is not dispatched before HIDE', 1, beforeHideCallCount);
      hideCallCount++;
    };

    events.listen(popup, PopupBase.EventType.BEFORE_HIDE, beforeHideHandler);
    events.listen(popup, PopupBase.EventType.HIDE, hideHandler);

    popup.setVisible(false);

    assertEquals(1, beforeHideCallCount);
    assertEquals(1, hideCallCount);
  },

  testShowEventFiredWithNoTransition() {
    popup = new ModalPopup();
    popup.render();

    let showHandlerCalled = false;
    events.listen(popup, PopupBase.EventType.SHOW, () => {
      showHandlerCalled = true;
    });

    popup.setVisible(true);
    assertTrue(showHandlerCalled);
  },

  testHideEventFiredWithNoTransition() {
    popup = new ModalPopup();
    popup.render();

    let hideHandlerCalled = false;
    events.listen(popup, PopupBase.EventType.HIDE, () => {
      hideHandlerCalled = true;
    });

    popup.setVisible(true);
    popup.setVisible(false);
    assertTrue(hideHandlerCalled);
  },

  testTransitionsPlayedOnShow() {
    popup = new ModalPopup();
    popup.render();

    const mockPopupShowTransition = new MockTransition();
    const mockPopupHideTransition = new MockTransition();
    const mockBgShowTransition = new MockTransition();
    const mockBgHideTransition = new MockTransition();

    let showHandlerCalled = false;
    events.listen(popup, PopupBase.EventType.SHOW, () => {
      showHandlerCalled = true;
    });

    popup.setTransition(
        mockPopupShowTransition, mockPopupHideTransition, mockBgShowTransition,
        mockBgHideTransition);
    assertFalse(mockPopupShowTransition.wasPlayed);
    assertFalse(mockBgShowTransition.wasPlayed);

    popup.setVisible(true);
    assertTrue(mockPopupShowTransition.wasPlayed);
    assertTrue(mockBgShowTransition.wasPlayed);

    assertFalse(showHandlerCalled);
    mockPopupShowTransition.dispatchEvent(Transition.EventType.END);
    assertTrue(showHandlerCalled);
  },

  testTransitionsPlayedOnHide() {
    popup = new ModalPopup();
    popup.render();

    const mockPopupShowTransition = new MockTransition();
    const mockPopupHideTransition = new MockTransition();
    const mockBgShowTransition = new MockTransition();
    const mockBgHideTransition = new MockTransition();

    let hideHandlerCalled = false;
    events.listen(popup, PopupBase.EventType.HIDE, () => {
      hideHandlerCalled = true;
    });

    popup.setTransition(
        mockPopupShowTransition, mockPopupHideTransition, mockBgShowTransition,
        mockBgHideTransition);
    popup.setVisible(true);
    assertFalse(mockPopupHideTransition.wasPlayed);
    assertFalse(mockBgHideTransition.wasPlayed);

    popup.setVisible(false);
    assertTrue(mockPopupHideTransition.wasPlayed);
    assertTrue(mockBgHideTransition.wasPlayed);

    assertFalse(hideHandlerCalled);
    mockPopupHideTransition.dispatchEvent(Transition.EventType.END);
    assertTrue(hideHandlerCalled);
  },

  testTransitionsAndDisposingOnHideWorks() {
    popup = new ModalPopup();
    popup.render();

    events.listen(popup, PopupBase.EventType.HIDE, () => {
      popup.dispose();
    });

    const popupShowTransition =
        css3.fadeIn(popup.getElement(), 0.1 /* duration */);
    const popupHideTransition =
        css3.fadeOut(popup.getElement(), 0.1 /* duration */);
    const bgShowTransition =
        css3.fadeIn(popup.getElement(), 0.1 /* duration */);
    const bgHideTransition =
        css3.fadeOut(popup.getElement(), 0.1 /* duration */);

    popup.setTransition(
        popupShowTransition, popupHideTransition, bgShowTransition,
        bgHideTransition);
    popup.setVisible(true);
    popup.setVisible(false);
    // Nothing to assert. We only want to ensure that there is no error.
  },

  testSetVisibleWorksCorrectlyWithTransitions() {
    popup = new ModalPopup();
    popup.render();
    popup.setTransition(
        css3.fadeIn(popup.getElement(), 1),
        css3.fadeIn(popup.getBackgroundElement(), 1),
        css3.fadeOut(popup.getElement(), 1),
        css3.fadeOut(popup.getBackgroundElement(), 1));

    // Consecutive calls to setVisible works without needing to wait for
    // transition to finish.
    popup.setVisible(true);
    assertTrue(popup.isVisible());
    popup.setVisible(false);
    assertFalse(popup.isVisible());
    mockClock.tick(1100);

    // Calling setVisible(true) immediately changed the state to visible.
    popup.setVisible(true);
    assertTrue(popup.isVisible());
    mockClock.tick(1100);

    // Consecutive calls to setVisible, in opposite order.
    popup.setVisible(false);
    popup.setVisible(true);
    assertTrue(popup.isVisible());
    mockClock.tick(1100);

    // Calling setVisible(false) immediately changed the state to not visible.
    popup.setVisible(false);
    assertFalse(popup.isVisible());
    mockClock.tick(1100);
  },

  testTransitionsDisposed() {
    popup = new ModalPopup();
    popup.render();

    const transition = css3.fadeIn(popup.getElement(), 0.1 /* duration */);

    let hideHandlerCalled = false;
    events.listen(popup, PopupBase.EventType.HIDE, () => {
      hideHandlerCalled = true;
    });

    popup.setTransition(transition, transition, transition, transition);
    popup.dispose();

    transition.dispatchEvent(Transition.EventType.END);
    assertFalse(hideHandlerCalled);
  },

  testBackgroundHeight() {
    // Insert an absolutely-positioned element larger than the viewport.
    const viewportSize = dom.getViewportSize();
    const w = viewportSize.width * 2;
    const h = viewportSize.height * 2;
    const dummy = dom.createElement(TagName.DIV);
    dummy.style.position = 'absolute';
    style.setSize(dummy, w, h);
    document.body.appendChild(dummy);

    try {
      popup = new ModalPopup();
      popup.render();
      popup.setVisible(true);

      const size = style.getSize(popup.getBackgroundElement());
      assertTrue(
          'Background element must cover the size of the content',
          size.width >= w && size.height >= h);
    } finally {
      dom.removeNode(dummy);
    }
  },

  /** @suppress {visibility} suppression added to enable type checking */
  testSetupBackwardTabWrapResetsFlagAfterTimeout() {
    popup.setupBackwardTabWrap();
    assertTrue(
        'Backward tab wrap should be in progress',
        popup.backwardTabWrapInProgress_);
    mockClock.tick(1);
    assertFalse(
        'Backward tab wrap flag should be reset after delay',
        popup.backwardTabWrapInProgress_);
  },

  testPopupGetsFocus() {
    popup = new ModalPopup();
    popup.render();
    popup.setVisible(true);
    assertTrue(
        'Dialog must receive initial focus',
        dom.getActiveElement(document) == popup.getElement());
  },

  testPopupGetsFocus_withOptionalParent() {
    const parentEl = dom.createElement(TagName.DIV);
    document.body.appendChild(parentEl);
    popup = new ModalPopup();
    popup.setCenterInsideParentElement(true);
    popup.render(parentEl);

    popup.setVisible(true);

    assertTrue(
        'Dialog must receive initial focus',
        dom.getActiveElement(document) == popup.getElement());
  },

  testDecoratedPopupGetsFocus() {
    const dialogElem = dom.createElement(TagName.DIV);
    document.body.appendChild(dialogElem);
    popup = new ModalPopup();
    popup.decorate(dialogElem);
    popup.setVisible(true);
    assertTrue(
        'Dialog must receive initial focus',
        dom.getActiveElement(document) == popup.getElement());
    dom.removeNode(dialogElem);
  },

  testBackgroundSize_withoutOptionalParent_inheritBodySize() {
    popup = new ModalPopup();
    popup.render();
    // Because the test does not add css, the size of body element changes
    // after showing the background.
    const documentHeight = document.documentElement.scrollHeight;
    const documentWidth = document.documentElement.scrollWidth;

    popup.setVisible(true);

    const backgroundSize = style.getSize(popup.getBackgroundElement());
    assertTrue(backgroundSize.width === documentWidth);
    assertTrue(backgroundSize.height === documentHeight);
  },

  testBackgroundSize_withOptionalParent_backgroundInheritParentSize() {
    const parentEl = dom.createElement(TagName.DIV);
    document.body.appendChild(parentEl);
    style.setSize(parentEl, 99, 88);
    // Reinforce the test by changing the parent element size and assert the
    // modal popup receives the right parent dimensions.
    parentEl.style.transform = 'scale(0.5)';
    parentEl.style.zoom = '50%';
    parentEl.style.border = '20px solid';
    popup = new ModalPopup();
    popup.setCenterInsideParentElement(true);
    popup.render(parentEl);

    popup.setVisible(true);

    const backgroundSize = style.getSize(popup.getBackgroundElement());
    assertRoughlyEquals('backgroundSize.width', 99, backgroundSize.width, 1);
    assertTrue(backgroundSize.height === 88);
    dom.removeNode(parentEl);
  },

  testPopupPosition_withoutOptionalParent_relativeToBody() {
    popup = new ModalPopup();
    popup.render();
    style.setSize(popup.getElement(), 20, 20);

    popup.setVisible(true);

    const viewportSize = dom.getViewportSize();
    const modalEl = popup.getElement();
    const modalSize = style.getSize(modalEl);
    const top = style.getComputedStyle(modalEl, 'top');
    const expectedTop = viewportSize.height / 2 - modalSize.height / 2;
    assertTrue(top === expectedTop + 'px');
    const left = style.getComputedStyle(modalEl, 'left');
    const expectedLeft = viewportSize.width / 2 - modalSize.width / 2;
    assertTrue(left === expectedLeft + 'px');
  },

  testPopupPosition_withOptionalParent_relativeToBody() {
    const parentEl = dom.createElement(TagName.DIV);
    document.body.appendChild(parentEl);
    popup = new ModalPopup();
    popup.setCenterInsideParentElement(true);
    popup.render(parentEl);
    style.setSize(popup.getElement(), 20, 20);
    style.setSize(parentEl, 99, 88);
    // Reinforce the test by changing the parent element size and assert the
    // modal popup receives the right parent dimensions.
    parentEl.style.transform = 'scale(0.5)';
    parentEl.style.zoom = '50%';
    const borderThickness = 20;
    parentEl.style.border = `${borderThickness}px solid`;

    popup.setVisible(true);

    const modalEl = popup.getElement();
    const modalSize = style.getSize(modalEl);
    const parentSize = style.getSize(parentEl);
    const parentInnerHeight =
        parentSize.height - borderThickness - borderThickness;
    const expectedTop = parentInnerHeight / 2 - modalSize.height / 2;
    const top = style.getComputedStyle(modalEl, 'top');
    assertTrue(top === expectedTop + 'px');
    const left = style.getComputedStyle(modalEl, 'left');
    const parentInnerWidth =
        parentSize.width - borderThickness - borderThickness;
    const expectedLeft = parentInnerWidth / 2 - modalSize.width / 2;
    assertTrue(left === expectedLeft + 'px');
    dom.removeNode(parentEl);
  },

  testSetCenterInsideParentElementAfterRender_throws() {
    const parentEl = dom.createElement(TagName.DIV);
    document.body.appendChild(parentEl);
    popup = new ModalPopup();
    popup.render();

    assertThrows(() => popup.setCenterInsideParentElement(true));
  }
});
