// Copyright 2008 The Closure Library Authors. All Rights Reserved.
//
// Licensed 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 CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

goog.provide('goog.editor.plugins.TagOnEnterHandlerTest');
goog.setTestOnly('goog.editor.plugins.TagOnEnterHandlerTest');

goog.require('goog.dom');
goog.require('goog.dom.NodeType');
goog.require('goog.dom.Range');
goog.require('goog.dom.TagName');
goog.require('goog.editor.BrowserFeature');
goog.require('goog.editor.Field');
goog.require('goog.editor.Plugin');
goog.require('goog.editor.plugins.TagOnEnterHandler');
goog.require('goog.events.KeyCodes');
goog.require('goog.html.SafeHtml');
goog.require('goog.string.Unicode');
goog.require('goog.testing.dom');
goog.require('goog.testing.editor.TestHelper');
goog.require('goog.testing.events');
goog.require('goog.testing.jsunit');
goog.require('goog.userAgent');

var savedHtml;

var editor;
var field1;

function setUp() {
  field1 = makeField('field1');
  field1.makeEditable();
}


/**
 * Tests that deleting a BR that comes right before a block element works.
 * @bug 1471096
 */
function testDeleteBrBeforeBlock() {
  // This test only works on Gecko, because it's testing for manual deletion of
  // BR tags, which is done only for Gecko. For other browsers we fall through
  // and let the browser do the delete, which can only be tested with a robot
  // test (see javascript/apps/editor/tests/delete_br_robot.html).
  if (goog.userAgent.GECKO) {
    field1.setSafeHtml(
        false,
        goog.html.SafeHtml.concat(
            'one', goog.html.SafeHtml.BR, goog.html.SafeHtml.BR,
            goog.html.SafeHtml.create('div', {}, 'two')));
    var helper = new goog.testing.editor.TestHelper(field1.getElement());
    helper.select(field1.getElement(), 2);  // Between the two BR's.
    goog.testing.events.fireKeySequence(
        field1.getElement(), goog.events.KeyCodes.DELETE);
    assertEquals(
        'Should have deleted exactly one <br>', 'one<br><div>two</div>',
        field1.getElement().innerHTML);

  }  // End if GECKO
}


/**
 * Tests that deleting a BR is working normally (that the workaround for the
 * bug is not causing double deletes).
 * @bug 1471096
 */
function testDeleteBrNormal() {
  // This test only works on Gecko, because it's testing for manual deletion of
  // BR tags, which is done only for Gecko. For other browsers we fall through
  // and let the browser do the delete, which can only be tested with a robot
  // test (see javascript/apps/editor/tests/delete_br_robot.html).
  if (goog.userAgent.GECKO) {
    field1.setSafeHtml(
        false,
        goog.html.SafeHtml.concat(
            'one', goog.html.SafeHtml.BR, goog.html.SafeHtml.BR,
            goog.html.SafeHtml.BR, 'two'));
    var helper = new goog.testing.editor.TestHelper(field1.getElement());
    helper.select(
        field1.getElement(), 2);  // Between the first and second BR's.
    field1.getElement().focus();
    goog.testing.events.fireKeySequence(
        field1.getElement(), goog.events.KeyCodes.DELETE);
    assertEquals(
        'Should have deleted exactly one <br>', 'one<br><br>two',
        field1.getElement().innerHTML);

  }  // End if GECKO
}


/**
 * Regression test for http://b/1991234 . Tests that when you hit enter and it
 * creates a blank line with whitespace and a BR, the cursor is placed in the
 * whitespace text node instead of the BR, otherwise continuing to type will
 * create adjacent text nodes, which causes browsers to mess up some
 * execcommands. Fix is in a Gecko-only codepath, thus test runs only for Gecko.
 * A full test for the entire sequence that reproed the bug is in
 * javascript/apps/editor/tests/ponenter_robot.html .
 */
function testEnterCreatesBlankLine() {
  if (goog.userAgent.GECKO) {
    field1.setSafeHtml(
        false,
        goog.html.SafeHtml.create('p', {}, ['one ', goog.html.SafeHtml.BR]));
    var helper = new goog.testing.editor.TestHelper(field1.getElement());
    // Place caret after 'one' but keeping a space and a BR as FF does.
    helper.select('one ', 3);
    field1.getElement().focus();
    goog.testing.events.fireKeySequence(
        field1.getElement(), goog.events.KeyCodes.ENTER);
    var range = field1.getRange();
    assertFalse(
        'Selection should not be in BR tag',
        range.getStartNode().nodeType == goog.dom.NodeType.ELEMENT &&
            range.getStartNode().tagName == goog.dom.TagName.BR);
    assertEquals(
        'Selection should be in text node to avoid creating adjacent' +
            ' text nodes',
        goog.dom.NodeType.TEXT, range.getStartNode().nodeType);
    var rangeStartNode =
        goog.dom.Range.createFromNodeContents(range.getStartNode());
    assertHTMLEquals(
        'The value of selected text node should be replaced with' +
            '&nbsp;',
        '&nbsp;', rangeStartNode.getHtmlFragment());
  }
}


/**
 * Regression test for http://b/3051179 . Tests that when you hit enter and it
 * creates a blank line with a BR and the cursor is placed in P.
 * Splitting DOM causes to make an empty text node. Then if the cursor is placed
 * at the text node the cursor is shown at wrong location.
 * Therefore this test checks that the cursor is not placed at an empty node.
 * Fix is in a Gecko-only codepath, thus test runs only for Gecko.
 */
function testEnterNormalizeNodes() {
  if (goog.userAgent.GECKO) {
    field1.setSafeHtml(
        false,
        goog.html.SafeHtml.create('p', {}, ['one', goog.html.SafeHtml.BR]));
    var helper = new goog.testing.editor.TestHelper(field1.getElement());
    // Place caret after 'one' but keeping a BR as FF does.
    helper.select('one', 3);
    field1.getElement().focus();
    goog.testing.events.fireKeySequence(
        field1.getElement(), goog.events.KeyCodes.ENTER);
    var range = field1.getRange();
    assertTrue(
        'Selection should be in P tag',
        range.getStartNode().nodeType == goog.dom.NodeType.ELEMENT &&
            range.getStartNode().tagName == goog.dom.TagName.P);
    assertTrue(
        'Selection should be at the head and collapsed',
        range.getStartOffset() == 0 && range.isCollapsed());
  }
}


/**
 * Verifies
 * goog.editor.plugins.TagOnEnterHandler.prototype.handleRegularEnterGecko_
 * when we explicitly split anchor elements. This test runs only for Gecko
 * since this is a Gecko-only codepath.
 */
function testEnterAtBeginningOfLink() {
  if (goog.userAgent.GECKO) {
    field1.setSafeHtml(false, goog.html.SafeHtml.create('a', {'href': '/'}, [
      'b', goog.html.SafeHtml.BR
    ]));
    var helper = new goog.testing.editor.TestHelper(field1.getElement());
    field1.focusAndPlaceCursorAtStart();
    goog.testing.events.fireKeySequence(
        field1.getElement(), goog.events.KeyCodes.ENTER);
    helper.assertHtmlMatches('<p>&nbsp;</p><p><a href="/">b<br></a></p>');
  }
}


/**
 * Verifies correct handling of pressing enter in an empty list item.
 */
function testEnterInEmptyListItemInEmptyList() {
  if (goog.userAgent.GECKO) {
    field1.setSafeHtml(
        false,
        goog.html.SafeHtml.create(
            'ul', {},
            goog.html.SafeHtml.create('li', {}, goog.string.Unicode.NBSP)));
    var helper = new goog.testing.editor.TestHelper(field1.getElement());
    var li = goog.dom.getElementsByTagName(
        goog.dom.TagName.LI, field1.getElement())[0];
    helper.select(li.firstChild, 0);
    goog.testing.events.fireKeySequence(
        field1.getElement(), goog.events.KeyCodes.ENTER);
    helper.assertHtmlMatches('<p>&nbsp;</p>');
  }
}


function testEnterInEmptyListItemAtBeginningOfList() {
  if (goog.userAgent.GECKO) {
    field1.setSafeHtml(
        false,
        goog.html.SafeHtml.create('ul', {'style': {'font-weight': 'bold'}}, [
          goog.html.SafeHtml.create('li', {}, goog.string.Unicode.NBSP),
          goog.html.SafeHtml.create('li', {}, '1'),
          goog.html.SafeHtml.create('li', {}, '2')
        ]));
    var helper = new goog.testing.editor.TestHelper(field1.getElement());
    var li = goog.dom.getElementsByTagName(
        goog.dom.TagName.LI, field1.getElement())[0];
    helper.select(li.firstChild, 0);
    goog.testing.events.fireKeySequence(
        field1.getElement(), goog.events.KeyCodes.ENTER);
    helper.assertHtmlMatches(
        '<p>&nbsp;</p><ul style="font-weight: bold"><li>1</li><li>2</li></ul>');
  }
}


function testEnterInEmptyListItemAtEndOfList() {
  if (goog.userAgent.GECKO) {
    field1.setSafeHtml(
        false,
        goog.html.SafeHtml.create('ul', {'style': {'font-weight': 'bold'}}, [
          goog.html.SafeHtml.create('li', {}, '1'),
          goog.html.SafeHtml.create('li', {}, '2'),
          goog.html.SafeHtml.create('li', {}, goog.string.Unicode.NBSP)
        ]));
    var helper = new goog.testing.editor.TestHelper(field1.getElement());
    var li = goog.dom.getElementsByTagName(
        goog.dom.TagName.LI, field1.getElement())[2];
    helper.select(li.firstChild, 0);
    goog.testing.events.fireKeySequence(
        field1.getElement(), goog.events.KeyCodes.ENTER);
    helper.assertHtmlMatches(
        '<ul style="font-weight: bold"><li>1</li><li>2</li></ul><p>&nbsp;</p>');
  }
}


function testEnterInEmptyListItemInMiddleOfList() {
  if (goog.userAgent.GECKO) {
    field1.setSafeHtml(
        false,
        goog.html.SafeHtml.create('ul', {'style': {'font-weight': 'bold'}}, [
          goog.html.SafeHtml.create('li', {}, '1'),
          goog.html.SafeHtml.create('li', {}, goog.string.Unicode.NBSP),
          goog.html.SafeHtml.create('li', {}, '2')
        ]));
    var helper = new goog.testing.editor.TestHelper(field1.getElement());
    var li = goog.dom.getElementsByTagName(
        goog.dom.TagName.LI, field1.getElement())[1];
    helper.select(li.firstChild, 0);
    goog.testing.events.fireKeySequence(
        field1.getElement(), goog.events.KeyCodes.ENTER);
    helper.assertHtmlMatches(
        '<ul style="font-weight: bold"><li>1</li></ul>' +
        '<p>&nbsp;</p>' +
        '<ul style="font-weight: bold"><li>2</li></ul>');
  }
}


function testEnterInEmptyListItemInSublist() {
  if (goog.userAgent.GECKO) {
    field1.setSafeHtml(false, goog.html.SafeHtml.create('ul', {}, [
      goog.html.SafeHtml.create('li', {}, 'A'),
      goog.html.SafeHtml.create(
          'ul', {'style': {'font-weight': 'bold'}},
          [
            goog.html.SafeHtml.create('li', {}, '1'),
            goog.html.SafeHtml.create('li', {}, goog.string.Unicode.NBSP),
            goog.html.SafeHtml.create('li', {}, '2')
          ]),
      goog.html.SafeHtml.create('li', {}, 'B')
    ]));
    var helper = new goog.testing.editor.TestHelper(field1.getElement());
    var li = goog.dom.getElementsByTagName(
        goog.dom.TagName.LI, field1.getElement())[2];
    helper.select(li.firstChild, 0);
    goog.testing.events.fireKeySequence(
        field1.getElement(), goog.events.KeyCodes.ENTER);
    helper.assertHtmlMatches(
        '<ul>' +
        '<li>A</li>' +
        '<ul style="font-weight: bold"><li>1</li></ul>' +
        '<li>&nbsp;</li>' +
        '<ul style="font-weight: bold"><li>2</li></ul>' +
        '<li>B</li>' +
        '</ul>');
  }
}


function testEnterInEmptyListItemAtBeginningOfSublist() {
  if (goog.userAgent.GECKO) {
    field1.setSafeHtml(false, goog.html.SafeHtml.create('ul', {}, [
      goog.html.SafeHtml.create('li', {}, 'A'),
      goog.html.SafeHtml.create(
          'ul', {'style': {'font-weight': 'bold'}},
          [
            goog.html.SafeHtml.create('li', {}, goog.string.Unicode.NBSP),
            goog.html.SafeHtml.create('li', {}, '1'),
            goog.html.SafeHtml.create('li', {}, '2')
          ]),
      goog.html.SafeHtml.create('li', {}, 'B')
    ]));
    var helper = new goog.testing.editor.TestHelper(field1.getElement());
    var li = goog.dom.getElementsByTagName(
        goog.dom.TagName.LI, field1.getElement())[1];
    helper.select(li.firstChild, 0);
    goog.testing.events.fireKeySequence(
        field1.getElement(), goog.events.KeyCodes.ENTER);
    helper.assertHtmlMatches(
        '<ul>' +
        '<li>A</li>' +
        '<li>&nbsp;</li>' +
        '<ul style="font-weight: bold"><li>1</li><li>2</li></ul>' +
        '<li>B</li>' +
        '</ul>');
  }
}


function testEnterInEmptyListItemAtEndOfSublist() {
  if (goog.userAgent.GECKO) {
    field1.setSafeHtml(false, goog.html.SafeHtml.create('ul', {}, [
      goog.html.SafeHtml.create('li', {}, 'A'),
      goog.html.SafeHtml.create(
          'ul', {'style': {'font-weight': 'bold'}},
          [
            goog.html.SafeHtml.create('li', {}, '1'),
            goog.html.SafeHtml.create('li', {}, '2'),
            goog.html.SafeHtml.create('li', {}, goog.string.Unicode.NBSP)
          ]),
      goog.html.SafeHtml.create('li', {}, 'B')
    ]));
    var helper = new goog.testing.editor.TestHelper(field1.getElement());
    var li = goog.dom.getElementsByTagName(
        goog.dom.TagName.LI, field1.getElement())[3];
    helper.select(li.firstChild, 0);
    goog.testing.events.fireKeySequence(
        field1.getElement(), goog.events.KeyCodes.ENTER);
    helper.assertHtmlMatches(
        '<ul>' +
        '<li>A</li>' +
        '<ul style="font-weight: bold"><li>1</li><li>2</li></ul>' +
        '<li>&nbsp;</li>' +
        '<li>B</li>' +
        '</ul>');
  }
}


function testPrepareContentForPOnEnter() {
  assertPreparedContents('hi', 'hi');
  assertPreparedContents(
      goog.editor.BrowserFeature.COLLAPSES_EMPTY_NODES ? '<p>&nbsp;</p>' : '',
      '   ');
}


function testPrepareContentForDivOnEnter() {
  assertPreparedContents('hi', 'hi', goog.dom.TagName.DIV);
  assertPreparedContents(
      goog.editor.BrowserFeature.COLLAPSES_EMPTY_NODES ? '<div><br></div>' : '',
      '   ', goog.dom.TagName.DIV);
}


/**
 * Assert that the prepared contents matches the expected.
 */
function assertPreparedContents(expected, original, opt_tag) {
  var field = makeField('field1', opt_tag);
  field.makeEditable();
  assertEquals(
      expected,
      field.reduceOp_(goog.editor.Plugin.Op.PREPARE_CONTENTS_HTML, original));
}


/**
 * Selects the node at the given id, and simulates an ENTER keypress.
 * @param {googe.editor.Field} field The field with the node.
 * @param {string} id A DOM id.
 * @return {boolean} Whether preventDefault was called on the event.
 */
function selectNodeAndHitEnter(field, id) {
  var cursor = field.getEditableDomHelper().getElement(id);
  goog.dom.Range.createFromNodeContents(cursor).select();
  return goog.testing.events.fireKeySequence(
      cursor, goog.events.KeyCodes.ENTER);
}


/**
 * Creates a field with only the enter handler plugged in, for testing.
 * @param {string} id A DOM id.
 * @param {!goog.dom.TagName=} opt_tag The block tag to use.  Defaults to P.
 * @return {goog.editor.Field} A field.
 */
function makeField(id, opt_tag) {
  var field = new goog.editor.Field(id);
  field.registerPlugin(
      new goog.editor.plugins.TagOnEnterHandler(opt_tag || goog.dom.TagName.P));
  return field;
}


/**
 * Runs a test for splitting the dom.
 * @param {number} offset Index into the text node to split.
 * @param {string} firstHalfString What the html of the first half of the DOM
 *     should be.
 * @param {string} secondHalfString What the html of the 2nd half of the DOM
 *     should be.
 * @param {boolean} isAppend True if the second half should be appended to the
 *     DOM.
 * @param {boolean=} opt_goToRoot True if the root argument for splitDom should
 *     be excluded.
 */
function helpTestSplit_(
    offset, firstHalfString, secondHalfString, isAppend, opt_goToBody) {
  var node = goog.dom.createElement(goog.dom.TagName.DIV);
  node.innerHTML = '<b>begin bold<i>italic</i>end bold</b>';
  document.body.appendChild(node);

  var italic =
      goog.dom.getElementsByTagName(goog.dom.TagName.I, node)[0].firstChild;

  var splitFn = isAppend ?
      goog.editor.plugins.TagOnEnterHandler.splitDomAndAppend_ :
      goog.editor.plugins.TagOnEnterHandler.splitDom_;
  var secondHalf = splitFn(italic, offset, opt_goToBody ? undefined : node);

  if (opt_goToBody) {
    secondHalfString = '<div>' + secondHalfString + '</div>';
  }

  assertEquals(
      'original node should have first half of the html', firstHalfString,
      node.innerHTML.toLowerCase().replace(goog.string.Unicode.NBSP, '&nbsp;'));
  assertEquals(
      'new node should have second half of the html', secondHalfString,
      secondHalf.innerHTML.toLowerCase().replace(
          goog.string.Unicode.NBSP, '&nbsp;'));

  if (isAppend) {
    assertTrue(
        'second half of dom should be the original node\'s next' +
            'sibling',
        node.nextSibling == secondHalf);
    goog.dom.removeNode(secondHalf);
  }

  goog.dom.removeNode(node);
}


/**
 * Runs different cases of splitting the DOM.
 * @param {function(number, string, string)} testFn Function that takes an
 *     offset, firstHalfString and secondHalfString as parameters.
 */
function splitDomCases_(testFn) {
  testFn(3, '<b>begin bold<i>ita</i></b>', '<b><i>lic</i>end bold</b>');
  testFn(0, '<b>begin bold<i>&nbsp;</i></b>', '<b><i>italic</i>end bold</b>');
  testFn(6, '<b>begin bold<i>italic</i></b>', '<b><i>&nbsp;</i>end bold</b>');
}


function testSplitDom() {
  splitDomCases_(function(offset, firstHalfString, secondHalfString) {
    helpTestSplit_(offset, firstHalfString, secondHalfString, false, true);
    helpTestSplit_(offset, firstHalfString, secondHalfString, false, false);
  });
}


function testSplitDomAndAppend() {
  splitDomCases_(function(offset, firstHalfString, secondHalfString) {
    helpTestSplit_(offset, firstHalfString, secondHalfString, true, false);
  });
}


function testSplitDomAtElement() {
  var node = goog.dom.createElement(goog.dom.TagName.DIV);
  node.innerHTML = '<div>abc<br>def</div>';
  document.body.appendChild(node);

  goog.editor.plugins.TagOnEnterHandler.splitDomAndAppend_(
      node.firstChild, 1, node.firstChild);

  goog.testing.dom.assertHtmlContentsMatch(
      '<div>abc</div><div><br>def</div>', node);

  goog.dom.removeNode(node);
}


function testSplitDomAtElementStart() {
  var node = goog.dom.createElement(goog.dom.TagName.DIV);
  node.innerHTML = '<div>abc<br>def</div>';
  document.body.appendChild(node);

  goog.editor.plugins.TagOnEnterHandler.splitDomAndAppend_(
      node.firstChild, 0, node.firstChild);

  goog.testing.dom.assertHtmlContentsMatch(
      '<div></div><div>abc<br>def</div>', node);

  goog.dom.removeNode(node);
}


function testSplitDomAtChildlessElement() {
  var node = goog.dom.createElement(goog.dom.TagName.DIV);
  node.innerHTML = '<div>abc<br>def</div>';
  document.body.appendChild(node);

  var br = goog.dom.getElementsByTagName(goog.dom.TagName.BR, node)[0];
  goog.editor.plugins.TagOnEnterHandler.splitDomAndAppend_(
      br, 0, node.firstChild);

  goog.testing.dom.assertHtmlContentsMatch(
      '<div>abc</div><div><br>def</div>', node);

  goog.dom.removeNode(node);
}

function testReplaceWhiteSpaceWithNbsp() {
  var node = goog.dom.createElement(goog.dom.TagName.DIV);
  var textNode = document.createTextNode('');
  node.appendChild(textNode);

  textNode.nodeValue = ' test ';
  goog.editor.plugins.TagOnEnterHandler.replaceWhiteSpaceWithNbsp_(
      node.firstChild, true, false);
  assertHTMLEquals('&nbsp;test ', node.innerHTML);

  textNode.nodeValue = '  test ';
  goog.editor.plugins.TagOnEnterHandler.replaceWhiteSpaceWithNbsp_(
      node.firstChild, true, false);
  assertHTMLEquals('&nbsp;test ', node.innerHTML);

  textNode.nodeValue = ' test ';
  goog.editor.plugins.TagOnEnterHandler.replaceWhiteSpaceWithNbsp_(
      node.firstChild, false, false);
  assertHTMLEquals(' test&nbsp;', node.innerHTML);

  textNode.nodeValue = ' test  ';
  goog.editor.plugins.TagOnEnterHandler.replaceWhiteSpaceWithNbsp_(
      node.firstChild, false, false);
  assertHTMLEquals(' test&nbsp;', node.innerHTML);

  textNode.nodeValue = '';
  goog.editor.plugins.TagOnEnterHandler.replaceWhiteSpaceWithNbsp_(
      node.firstChild, false, false);
  assertHTMLEquals('&nbsp;', node.innerHTML);

  textNode.nodeValue = '';
  goog.editor.plugins.TagOnEnterHandler.replaceWhiteSpaceWithNbsp_(
      node.firstChild, false, true);
  assertHTMLEquals('', node.innerHTML);
}
