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

goog.module('goog.ui.editor.AbstractDialogTest');
goog.setTestOnly();

const AbstractDialog = goog.require('goog.ui.editor.AbstractDialog');
const ArgumentMatcher = goog.require('goog.testing.mockmatchers.ArgumentMatcher');
const DomHelper = goog.require('goog.dom.DomHelper');
const EventHandler = goog.require('goog.events.EventHandler');
const GoogEvent = goog.require('goog.events.Event');
const KeyCodes = goog.require('goog.events.KeyCodes');
const MockControl = goog.require('goog.testing.MockControl');
const TagName = goog.require('goog.dom.TagName');
const classlist = goog.require('goog.dom.classlist');
const dom = goog.require('goog.dom');
const events = goog.require('goog.testing.events');
const testSuite = goog.require('goog.testing.testSuite');
const userAgent = goog.require('goog.userAgent');

function shouldRunTests() {
  // Test disabled in IE7 due to flakiness. See b/4269021.
  return !userAgent.IE;
}

let dialog;
let builder;

let mockCtrl;
let mockAfterHideHandler;
let mockOkHandler;
let mockCancelHandler;
let mockCustomButtonHandler;

const CUSTOM_EVENT = 'customEvent';
const CUSTOM_BUTTON_ID = 'customButton';

/**
 * Sets up the mock event handler to expect an AFTER_HIDE event.
 * @suppress {missingProperties} suppression added to enable type checking
 */
function expectAfterHide() {
  mockAfterHideHandler.handleEvent(new ArgumentMatcher(
      (arg) => arg.type == AbstractDialog.EventType.AFTER_HIDE));
}

/**
 * Sets up the mock event handler to expect an OK event.
 * @suppress {missingProperties} suppression added to enable type checking
 */
function expectOk() {
  mockOkHandler.handleEvent(
      new ArgumentMatcher((arg) => arg.type == AbstractDialog.EventType.OK));
}

/**
 * Sets up the mock event handler to expect an OK event and to call
 * preventDefault when handling it.
 */
function expectOkPreventDefault() {
  expectOk();
  mockOkHandler.$does((e) => {
    e.preventDefault();
  });
}

/**
 * Sets up the mock event handler to expect an OK event and to return false
 * when handling it.
 */
function expectOkReturnFalse() {
  expectOk();
  mockOkHandler.$returns(false);
}

/**
 * Sets up the mock event handler to expect a CANCEL event.
 * @suppress {missingProperties} suppression added to enable type checking
 */
function expectCancel() {
  mockCancelHandler.handleEvent(new ArgumentMatcher(
      (arg) => arg.type == AbstractDialog.EventType.CANCEL));
}

/**
 * Sets up the mock event handler to expect a custom button event.
 * @suppress {missingProperties} suppression added to enable type checking
 */
function expectCustomButton() {
  mockCustomButtonHandler.handleEvent(
      new ArgumentMatcher((arg) => arg.type == CUSTOM_EVENT));
}

/**
 * Helper to create the dialog being tested in each test. Since NewDialog is
 * abstract, needs to add a concrete version of any abstract methods. Also
 * creates up the global builder variable which should be set up after the call
 * to this method.
 * @return {!AbstractDialog} The dialog.
 * @suppress {checkTypes} suppression added to enable type checking
 */
function createTestDialog() {
  const dialog = new AbstractDialog(new DomHelper());
  builder = new AbstractDialog.Builder(dialog);
  /** @suppress {visibility} suppression added to enable type checking */
  dialog.createDialogControl = () => builder.build();
  /** @suppress {visibility} suppression added to enable type checking */
  dialog.createOkEvent = (e) => new GoogEvent(AbstractDialog.EventType.OK);
  dialog.addEventListener(
      AbstractDialog.EventType.AFTER_HIDE, mockAfterHideHandler);
  dialog.addEventListener(AbstractDialog.EventType.OK, mockOkHandler);
  dialog.addEventListener(AbstractDialog.EventType.CANCEL, mockCancelHandler);
  dialog.addEventListener(CUSTOM_EVENT, mockCustomButtonHandler);
  return dialog;
}

/**
 * Asserts that the given dialog is open.
 * @param {string} msg Message to be printed in case of failure.
 * @param {AbstractDialog} dialog Dialog to be tested.
 */
function assertOpen(msg, dialog) {
  assertTrue(msg + ' [AbstractDialog.isOpen()]', dialog && dialog.isOpen());
}

/**
 * Asserts that the given dialog is closed.
 * @param {string} msg Message to be printed in case of failure.
 * @param {AbstractDialog} dialog Dialog to be tested.
 */
function assertNotOpen(msg, dialog) {
  assertFalse(msg + ' [AbstractDialog.isOpen()]', dialog && dialog.isOpen());
}

testSuite({
  setUp() {
    mockCtrl = new MockControl();
    mockAfterHideHandler = mockCtrl.createLooseMock(EventHandler);
    mockOkHandler = mockCtrl.createLooseMock(EventHandler);
    mockCancelHandler = mockCtrl.createLooseMock(EventHandler);
    mockCustomButtonHandler = mockCtrl.createLooseMock(EventHandler);
  },

  tearDown() {
    if (dialog) {
      mockAfterHideHandler.$setIgnoreUnexpectedCalls(true);
      dialog.dispose();
    }
  },

  /**
   * Tests that if you create a dialog and hide it without having shown it, no
   * errors occur.
   */
  testCreateAndHide() {
    dialog = createTestDialog();
    mockCtrl.$replayAll();

    assertNotOpen('Dialog should not be open after creation', dialog);
    dialog.hide();
    assertNotOpen('Dialog should not be open after hide()', dialog);

    mockCtrl.$verifyAll();  // Verifies AFTER_HIDE was not dispatched.
  },

  /**
   * Tests that when you show and hide a dialog the flags indicating open are
   * correct and the AFTER_HIDE event is dispatched (and no errors happen).
   * @suppress {checkTypes} suppression added to enable type checking
   */
  testShowAndHide() {
    dialog = createTestDialog();
    expectAfterHide(dialog);
    mockCtrl.$replayAll();

    assertNotOpen('Dialog should not be open before show()', dialog);
    dialog.show();
    assertOpen('Dialog should be open after show()', dialog);
    dialog.hide();
    assertNotOpen('Dialog should not be open after hide()', dialog);

    mockCtrl.$verifyAll();  // Verifies AFTER_HIDE was dispatched.
  },

  /**
   * Tests that when you show and dispose a dialog (without hiding it first) the
   * flags indicating open are correct and the AFTER_HIDE event is dispatched
   * (and no errors happen).
   * @suppress {checkTypes} suppression added to enable type checking
   */
  testShowAndDispose() {
    dialog = createTestDialog();
    expectAfterHide(dialog);
    mockCtrl.$replayAll();

    assertNotOpen('Dialog should not be open before show()', dialog);
    dialog.show();
    assertOpen('Dialog should be open after show()', dialog);
    dialog.dispose();
    assertNotOpen('Dialog should not be open after dispose()', dialog);

    mockCtrl.$verifyAll();  // Verifies AFTER_HIDE was dispatched.
  },

  /**
   * Tests that when you dispose a dialog (without ever showing it first) the
   * flags indicating open are correct and the AFTER_HIDE event is never
   * dispatched (and no errors happen).
   */
  testDisposeWithoutShow() {
    dialog = createTestDialog();
    mockCtrl.$replayAll();

    assertNotOpen('Dialog should not be open before dispose()', dialog);
    dialog.dispose();
    assertNotOpen('Dialog should not be open after dispose()', dialog);

    mockCtrl.$verifyAll();  // Verifies AFTER_HIDE was NOT dispatched.
  },

  /**
   * Tests that labels set in the builder can be found in the resulting dialog's
   * HTML.
   */
  testBasicLayout() {
    dialog = createTestDialog();
    mockCtrl.$replayAll();

    // create some dialog content
    const content = dom.createDom(TagName.DIV, null, 'The Content');
    builder.setTitle('The Title')
        .setContent(content)
        .addOkButton('The OK Button')
        .addCancelButton()
        .addButton('The Apply Button', () => {})
        .addClassName('myClassName');
    dialog.show();

    /** @suppress {visibility} suppression added to enable type checking */
    const dialogElem = dialog.dialogInternal_.getElement();
    const html = dialogElem.innerHTML;
    // TODO(user): This is really insufficient. If the title and
    // content were swapped this test would still pass!
    assertContains('Dialog html should contain title', '>The Title<', html);
    assertContains('Dialog html should contain content', '>The Content<', html);
    assertContains(
        'Dialog html should contain custom OK button label', '>The OK Button<',
        html);
    assertContains(
        'Dialog html should contain default Cancel button label', '>Cancel<',
        html);
    assertContains(
        'Dialog html should contain custom button label', '>The Apply Button<',
        html);
    assertTrue(
        'Dialog should have default Closure class',
        classlist.contains(dialogElem, 'modal-dialog'));
    assertTrue(
        'Dialog should have our custom class',
        classlist.contains(dialogElem, 'myClassName'));

    mockCtrl.$verifyAll();
  },

  /**
   * Tests that clicking the OK button dispatches the OK event and closes the
   * dialog (dispatching the AFTER_HIDE event too).
   * @suppress {checkTypes,visibility} suppression added to enable type checking
   */
  testOk() {
    dialog = createTestDialog();
    expectOk(dialog);
    expectAfterHide(dialog);
    mockCtrl.$replayAll();

    dialog.show();
    events.fireClickSequence(dialog.getOkButtonElement());
    assertNotOpen('Dialog should not be open after clicking OK', dialog);

    mockCtrl.$verifyAll();
  },

  /**
   * Tests that hitting the enter key dispatches the OK event and closes the
   * dialog (dispatching the AFTER_HIDE event too).
   * @suppress {checkTypes,visibility} suppression added to enable type checking
   */
  testEnter() {
    dialog = createTestDialog();
    expectOk(dialog);
    expectAfterHide(dialog);
    mockCtrl.$replayAll();

    dialog.show();
    events.fireKeySequence(dialog.dialogInternal_.getElement(), KeyCodes.ENTER);
    assertNotOpen('Dialog should not be open after hitting enter', dialog);

    mockCtrl.$verifyAll();
  },

  /**
   * Tests that clicking the Cancel button dispatches the CANCEL event and
   * closes the dialog (dispatching the AFTER_HIDE event too).
   * @suppress {checkTypes,visibility} suppression added to enable type checking
   */
  testCancel() {
    dialog = createTestDialog();
    expectCancel(dialog);
    expectAfterHide(dialog);
    mockCtrl.$replayAll();

    builder.addCancelButton('My Cancel Button');

    dialog.show();
    events.fireClickSequence(dialog.getCancelButtonElement());
    assertNotOpen('Dialog should not be open after clicking Cancel', dialog);

    mockCtrl.$verifyAll();
  },

  /**
   * Tests that hitting the escape key dispatches the CANCEL event and closes
   * the dialog (dispatching the AFTER_HIDE event too).
   * @suppress {checkTypes,visibility} suppression added to enable type checking
   */
  testEscape() {
    dialog = createTestDialog();
    expectCancel(dialog);
    expectAfterHide(dialog);
    mockCtrl.$replayAll();

    dialog.show();
    events.fireKeySequence(dialog.dialogInternal_.getElement(), KeyCodes.ESC);
    assertNotOpen('Dialog should not be open after hitting escape', dialog);

    mockCtrl.$verifyAll();
  },

  /**
   * Tests that clicking the custom button dispatches the custom event and
   * closes the dialog (dispatching the AFTER_HIDE event too).
   * @suppress {checkTypes,visibility} suppression added to enable type checking
   */
  testCustomButton() {
    dialog = createTestDialog();
    expectCustomButton(dialog);
    expectAfterHide(dialog);
    mockCtrl.$replayAll();

    builder.addButton('My Custom Button', () => {
      dialog.dispatchEvent(CUSTOM_EVENT);
    }, CUSTOM_BUTTON_ID);

    dialog.show();
    events.fireClickSequence(dialog.getButtonElement(CUSTOM_BUTTON_ID));
    assertNotOpen(
        'Dialog should not be open after clicking custom button', dialog);

    mockCtrl.$verifyAll();
  },

  /**
   * Tests that if the OK handler calls preventDefault, the dialog doesn't
   *      close.
   * @suppress {checkTypes,visibility} suppression added to enable type checking
   */
  testOkPreventDefault() {
    dialog = createTestDialog();
    expectOkPreventDefault(dialog);
    mockCtrl.$replayAll();

    dialog.show();
    events.fireClickSequence(dialog.getOkButtonElement());
    assertOpen(
        'Dialog should not be closed because preventDefault was called',
        dialog);

    mockCtrl.$verifyAll();
  },

  /**
     Tests that if the OK handler returns false, the dialog doesn't close.
     @suppress {checkTypes,visibility} suppression added to enable type checking
   */
  testOkReturnFalse() {
    dialog = createTestDialog();
    expectOkReturnFalse(dialog);
    mockCtrl.$replayAll();

    dialog.show();
    events.fireClickSequence(dialog.getOkButtonElement());
    assertOpen(
        'Dialog should not be closed because handler returned false', dialog);

    mockCtrl.$verifyAll();
  },

  /**
   * Tests that if creating the OK event fails, no event is dispatched and the
   * dialog doesn't close.
   * @suppress {visibility} suppression added to enable type checking
   */
  testCreateOkEventFail() {
    dialog = createTestDialog();
    /** @suppress {visibility} suppression added to enable type checking */
    dialog.createOkEvent = () => {  // Override our mock createOkEvent.
      return null;
    };
    mockCtrl.$replayAll();

    dialog.show();
    events.fireClickSequence(dialog.getOkButtonElement());
    assertOpen(
        'Dialog should not be closed because OK event creation failed', dialog);

    mockCtrl.$verifyAll();  // Verifies that no event was dispatched.
  },

  /**
   * Tests that processOkAndClose() dispatches the OK event and closes the
   * dialog (dispatching the AFTER_HIDE event too).
   * @suppress {checkTypes,visibility} suppression added to enable type checking
   */
  testProcessOkAndClose() {
    dialog = createTestDialog();
    expectOk(dialog);
    expectAfterHide(dialog);
    mockCtrl.$replayAll();

    dialog.show();
    dialog.processOkAndClose();
    assertNotOpen(
        'Dialog should not be open after processOkAndClose()', dialog);

    mockCtrl.$verifyAll();
  },

  /**
   * Tests that if the OK handler triggered by processOkAndClose calls
   * preventDefault, the dialog doesn't close (in the old implementation this
   * failed due to not great design, so this is sort of a regression test).
   * @suppress {checkTypes,visibility} suppression added to enable type checking
   */
  testProcessOkAndClosePreventDefault() {
    dialog = createTestDialog();
    expectOkPreventDefault(dialog);
    mockCtrl.$replayAll();

    dialog.show();
    dialog.processOkAndClose();
    assertOpen(
        'Dialog should not be closed because preventDefault was called',
        dialog);

    mockCtrl.$verifyAll();
  },
});
