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

goog.module('goog.editor.nodeTest');
goog.setTestOnly();

const ExpectedFailures = goog.require('goog.testing.ExpectedFailures');
const NodeType = goog.require('goog.dom.NodeType');
const TagName = goog.require('goog.dom.TagName');
const editorNode = goog.require('goog.editor.node');
const googArray = goog.require('goog.array');
const googDom = goog.require('goog.dom');
const style = goog.require('goog.style');
const testSuite = goog.require('goog.testing.testSuite');
const testingDom = goog.require('goog.testing.dom');
const userAgent = goog.require('goog.userAgent');

let expectedFailures;
let parentNode;
let childNode1;
let childNode2;
let childNode3;

let gChildWsNode1 = null;
let gChildTextNode1 = null;
let gChildNbspNode1 = null;
let gChildMixedNode1 = null;
let gChildWsNode2a = null;
let gChildWsNode2b = null;
let gChildTextNode3a = null;
let gChildWsNode3 = null;
let gChildTextNode3b = null;

function setUpDomTree() {
  gChildWsNode1 = document.createTextNode(' \t\r\n');
  gChildTextNode1 = document.createTextNode('Child node');
  gChildNbspNode1 = document.createTextNode('\u00a0');
  gChildMixedNode1 = document.createTextNode('Text\n plus\u00a0');
  gChildWsNode2a = document.createTextNode('');
  gChildWsNode2b = document.createTextNode(' ');
  gChildTextNode3a = document.createTextNode('I am a grand child');
  gChildWsNode3 = document.createTextNode('   \t  \r   \n');
  gChildTextNode3b = document.createTextNode('I am also a grand child');

  childNode3.appendChild(gChildTextNode3a);
  childNode3.appendChild(gChildWsNode3);
  childNode3.appendChild(gChildTextNode3b);

  childNode1.appendChild(gChildMixedNode1);
  childNode1.appendChild(gChildWsNode1);
  childNode1.appendChild(gChildNbspNode1);
  childNode1.appendChild(gChildTextNode1);

  childNode2.appendChild(gChildWsNode2a);
  childNode2.appendChild(gChildWsNode2b);
  document.body.appendChild(parentNode);
}

function tearDownDomTree() {
  googDom.removeChildren(childNode1);
  googDom.removeChildren(childNode2);
  googDom.removeChildren(childNode3);
  gChildWsNode1 = null;
  gChildTextNode1 = null;
  gChildNbspNode1 = null;
  gChildMixedNode1 = null;
  gChildWsNode2a = null;
  gChildWsNode2b = null;
  gChildTextNode3a = null;
  gChildWsNode3 = null;
  gChildTextNode3b = null;
}

function createDivWithTextNodes(var_args) {
  const dom = googDom.createDom(TagName.DIV);
  for (let i = 0; i < arguments.length; i++) {
    googDom.appendChild(dom, googDom.createTextNode(arguments[i]));
  }
  return dom;
}

testSuite({
  setUpPage() {
    expectedFailures = new ExpectedFailures();
    parentNode = document.getElementById('parentNode');
    childNode1 = parentNode.childNodes[0];
    childNode2 = parentNode.childNodes[1];
    childNode3 = parentNode.childNodes[2];
  },

  tearDown() {
    expectedFailures.handleTearDown();
  },

  testGetCompatModeQuirks() {
    const quirksIfr = googDom.createElement(TagName.IFRAME);
    document.body.appendChild(quirksIfr);
    // Webkit used to default to standards mode, but fixed this in
    // Safari 4/Chrome 2, aka, WebKit 530.
    // Also IE10 fails here.
    // TODO(johnlenz):  IE10+ inherit quirks mode from the owner document
    // according to:
    // http://msdn.microsoft.com/en-us/library/ff955402(v=vs.85).aspx
    // but this test shows different behavior for IE10 and 11. If we discover
    // that we care about quirks mode documents we should investigate
    // this failure.
    expectedFailures.run(() => {
      assertFalse(
          'Empty sourceless iframe is quirks mode, not standards mode',
          editorNode.isStandardsMode(
              googDom.getFrameContentDocument(quirksIfr)));
    });
    document.body.removeChild(quirksIfr);
  },

  testGetCompatModeStandards() {
    const standardsIfr = googDom.createElement(TagName.IFRAME);
    document.body.appendChild(standardsIfr);
    const doc = googDom.getFrameContentDocument(standardsIfr);
    doc.open();
    doc.write('<!DOCTYPE HTML><html><head></head><body>&nbsp;</body></html>');
    doc.close();
    assertTrue(
        'Iframe with DOCTYPE written in is standards mode',
        editorNode.isStandardsMode(doc));
    document.body.removeChild(standardsIfr);
  },

  /** Creates a DOM tree and tests that getLeftMostLeaf returns proper node */
  testGetLeftMostLeaf() {
    setUpDomTree();

    assertEquals(
        'Should skip ws node', gChildMixedNode1,
        editorNode.getLeftMostLeaf(parentNode));
    assertEquals(
        'Should skip ws node', gChildMixedNode1,
        editorNode.getLeftMostLeaf(childNode1));
    assertEquals(
        'Has no non ws leaves', childNode2,
        editorNode.getLeftMostLeaf(childNode2));
    assertEquals(
        'Should return first child', gChildTextNode3a,
        editorNode.getLeftMostLeaf(childNode3));
    assertEquals(
        'Has no children', gChildTextNode1,
        editorNode.getLeftMostLeaf(gChildTextNode1));

    tearDownDomTree();
  },

  /** Creates a DOM tree and tests that getRightMostLeaf returns proper node */
  testGetRightMostLeaf() {
    setUpDomTree();

    assertEquals(
        'Should return child3\'s rightmost child', gChildTextNode3b,
        editorNode.getRightMostLeaf(parentNode));
    assertEquals(
        'Should skip ws node', gChildTextNode1,
        editorNode.getRightMostLeaf(childNode1));
    assertEquals(
        'Has no non ws leaves', childNode2,
        editorNode.getRightMostLeaf(childNode2));
    assertEquals(
        'Should return last child', gChildTextNode3b,
        editorNode.getRightMostLeaf(childNode3));
    assertEquals(
        'Has no children', gChildTextNode1,
        editorNode.getRightMostLeaf(gChildTextNode1));

    tearDownDomTree();
  },

  /**
   * Creates a DOM tree and tests that getFirstChild properly ignores
   * ignorable nodes
   */
  testGetFirstChild() {
    setUpDomTree();

    assertNull('Has no none ws children', editorNode.getFirstChild(childNode2));
    assertEquals(
        'Should skip first child, as it is ws', gChildMixedNode1,
        editorNode.getFirstChild(childNode1));
    assertEquals(
        'Should just return first child', gChildTextNode3a,
        editorNode.getFirstChild(childNode3));
    assertEquals(
        'Should return first child', childNode1,
        editorNode.getFirstChild(parentNode));

    assertNull(
        'First child of a text node should return null',
        editorNode.getFirstChild(gChildTextNode1));
    assertNull(
        'First child of null should return null',
        editorNode.getFirstChild(null));

    tearDownDomTree();
  },

  /**
   * Create a DOM tree and test that getLastChild properly ignores
   * ignorable nodes
   */
  testGetLastChild() {
    setUpDomTree();

    assertNull('Has no none ws children', editorNode.getLastChild(childNode2));
    assertEquals(
        'Should skip last child, as it is ws', gChildTextNode1,
        editorNode.getLastChild(childNode1));
    assertEquals(
        'Should just return last child', gChildTextNode3b,
        editorNode.getLastChild(childNode3));
    assertEquals(
        'Should return last child', childNode3,
        editorNode.getLastChild(parentNode));

    assertNull(
        'Last child of a text node should return null',
        editorNode.getLastChild(gChildTextNode1));
    assertNull(
        'Last child of null should return null',
        editorNode.getLastChild(gChildTextNode1));

    tearDownDomTree();
  },

  /**
   * Test if nodes that should be ignorable return false and nodes that should
   * not be ignored return true.
   */
  testIsImportant() {
    const wsNode = document.createTextNode(' \t\r\n');
    assertFalse(
        'White space node is ignorable', editorNode.isImportant(wsNode));
    const textNode = document.createTextNode('Hello');
    assertTrue('Text node is important', editorNode.isImportant(textNode));
    const nbspNode = document.createTextNode('\u00a0');
    assertTrue('Node with nbsp is important', editorNode.isImportant(nbspNode));
    const imageNode = googDom.createElement(TagName.IMG);
    assertTrue('Image node is important', editorNode.isImportant(imageNode));
  },

  /**
   * Test that isAllNonNbspWhiteSpace returns true if node contains only
   * whitespace that is not nbsp and false otherwise
   */
  testIsAllNonNbspWhiteSpace() {
    const wsNode = document.createTextNode(' \t\r\n');
    assertTrue(
        'String is all non nbsp', editorNode.isAllNonNbspWhiteSpace(wsNode));
    const textNode = document.createTextNode('Hello');
    assertFalse(
        'String should not be whitespace',
        editorNode.isAllNonNbspWhiteSpace(textNode));
    const nbspNode = document.createTextNode('\u00a0');
    assertFalse('String has nbsp', editorNode.isAllNonNbspWhiteSpace(nbspNode));
  },

  /**
   * Creates a DOM tree and Test that getPreviousSibling properly ignores
   * ignorable nodes
   */
  testGetPreviousSibling() {
    setUpDomTree();

    assertNull(
        'No previous sibling', editorNode.getPreviousSibling(gChildTextNode3a));
    assertEquals(
        'Should have text sibling', gChildTextNode3a,
        editorNode.getPreviousSibling(gChildWsNode3));
    assertEquals(
        'Should skip over white space sibling', gChildTextNode3a,
        editorNode.getPreviousSibling(gChildTextNode3b));
    assertNull(
        'No previous sibling', editorNode.getPreviousSibling(gChildMixedNode1));
    assertEquals(
        'Should have mixed text sibling', gChildMixedNode1,
        editorNode.getPreviousSibling(gChildWsNode1));
    assertEquals(
        'Should skip over white space sibling', gChildMixedNode1,
        editorNode.getPreviousSibling(gChildNbspNode1));
    assertNotEquals(
        'Should not move past ws and nbsp', gChildMixedNode1,
        editorNode.getPreviousSibling(gChildTextNode1));
    assertEquals(
        'Should go to child 2', childNode2,
        editorNode.getPreviousSibling(childNode3));
    assertEquals(
        'Should go to child 1', childNode1,
        editorNode.getPreviousSibling(childNode2));
    assertNull(
        'Only has white space siblings',
        editorNode.getPreviousSibling(gChildWsNode2b));

    tearDownDomTree();
  },

  /**
   * Creates a DOM tree and tests that getNextSibling properly ignores
   * igrnorable nodes when determining the next sibling
   */
  testGetNextSibling() {
    setUpDomTree();

    assertEquals(
        'Child 1 should have Child 2', childNode2,
        editorNode.getNextSibling(childNode1));
    assertEquals(
        'Child 2 should have child 3', childNode3,
        editorNode.getNextSibling(childNode2));
    assertNull(
        'Child 3 has no next sibling', editorNode.getNextSibling(childNode3));
    assertNotEquals(
        'Should not skip ws and nbsp nodes', gChildTextNode1,
        editorNode.getNextSibling(gChildMixedNode1));
    assertNotEquals(
        'Should not skip nbsp node', gChildTextNode1,
        editorNode.getNextSibling(gChildWsNode1));
    assertEquals(
        'Should have sibling', gChildTextNode1,
        editorNode.getNextSibling(gChildNbspNode1));
    assertNull(
        'Should have no next sibling',
        editorNode.getNextSibling(gChildTextNode1));
    assertNull(
        'Only has ws sibling', editorNode.getNextSibling(gChildWsNode2a));
    assertNull(
        'Has no next sibling', editorNode.getNextSibling(gChildWsNode2b));
    assertEquals(
        'Should skip ws node', gChildTextNode3b,
        editorNode.getNextSibling(gChildTextNode3a));

    tearDownDomTree();
  },

  testIsEmpty() {
    const textNode = document.createTextNode('');
    assertTrue(
        'Text node with no content should be empty',
        editorNode.isEmpty(textNode));
    textNode.data = '\xa0';
    assertTrue(
        'Text node with nbsp should be empty', editorNode.isEmpty(textNode));
    assertFalse(
        'Text node with nbsp should not be empty when prohibited',
        editorNode.isEmpty(textNode, true));

    textNode.data = '     ';
    assertTrue(
        'Text node with whitespace should be empty',
        editorNode.isEmpty(textNode));
    textNode.data = 'notEmpty';
    assertFalse(
        'Text node with text should not be empty',
        editorNode.isEmpty(textNode));

    const div = googDom.createElement(TagName.DIV);
    assertTrue('Empty div should be empty', editorNode.isEmpty(div));
    div.innerHTML = '<iframe></iframe>';
    assertFalse(
        'Div containing an iframe is not empty', editorNode.isEmpty(div));
    div.innerHTML = '<img></img>';
    assertFalse(
        'Div containing an image is not empty', editorNode.isEmpty(div));
    div.innerHTML = '<embed></embed>';
    assertFalse(
        'Div containing an embed is not empty', editorNode.isEmpty(div));
    div.innerHTML = '<div><span></span></div>';
    assertTrue(
        'Div containing other empty tags is empty', editorNode.isEmpty(div));
    div.innerHTML = '<div><span>  </span></div>';
    assertTrue(
        'Div containing other empty tags and whitespace is empty',
        editorNode.isEmpty(div));
    div.innerHTML = '<div><span>Not empty</span></div>';
    assertFalse(
        'Div containing tags and text is not empty', editorNode.isEmpty(div));

    const img = googDom.createElement(TagName.IMG);
    assertFalse('Empty img should not be empty', editorNode.isEmpty(img));

    const iframe = googDom.createElement(TagName.IFRAME);
    assertFalse('Empty iframe should not be empty', editorNode.isEmpty(iframe));

    const embed = googDom.createElement(TagName.EMBED);
    assertFalse('Empty embed should not be empty', editorNode.isEmpty(embed));
  },

  /**
   * Test that getLength returns 0 if the node has no length and no children,
   * the # of children if the node has no length but does have children,
   * and the length of the node if the node does have length
   */
  testGetLength() {
    const parentNode = googDom.createElement(TagName.P);

    assertEquals(
        'Length 0 and no children', 0, editorNode.getLength(parentNode));

    const childNode1 = document.createTextNode('node 1');
    const childNode2 = document.createTextNode('node number 2');
    const childNode3 = document.createTextNode('');
    parentNode.appendChild(childNode1);
    parentNode.appendChild(childNode2);
    parentNode.appendChild(childNode3);
    assertEquals(
        'Length 0 and 3 children', 3, editorNode.getLength(parentNode));
    assertEquals('Text node, length 6', 6, editorNode.getLength(childNode1));
    assertEquals('Text node, length 0', 0, editorNode.getLength(childNode3));
  },

  testFindInChildrenSuccess() {
    const parentNode = googDom.createElement(TagName.DIV);
    parentNode.innerHTML = '<div>foo</div><b>foo2</b>';

    const index = editorNode.findInChildren(
        parentNode, /**
                       @suppress {strictMissingProperties} suppression added to
                       enable type checking
                     */
        (node) => node.tagName == TagName.B);
    assertEquals('Should find second child', index, 1);
  },

  testFindInChildrenFailure() {
    const parentNode = googDom.createElement(TagName.DIV);
    parentNode.innerHTML = '<div>foo</div><b>foo2</b>';

    const index = editorNode.findInChildren(parentNode, (node) => false);
    assertNull('Shouldn\'t find a child', index);
  },

  testFindHighestMatchingAncestor() {
    setUpDomTree();
    let predicateFunc = (node) => node.tagName == TagName.DIV;
    let node =
        editorNode.findHighestMatchingAncestor(gChildTextNode3a, predicateFunc);
    assertNotNull('Should return an ancestor', node);
    assertEquals(
        'Should have found "parentNode" as the last ' +
            'ancestor matching the predicate',
        parentNode, node);

    predicateFunc = (node) => node.childNodes.length == 1;
    node =
        editorNode.findHighestMatchingAncestor(gChildTextNode3a, predicateFunc);
    assertNull('Shouldn\'t return an ancestor', node);

    tearDownDomTree();
  },

  testIsBlock() {
    const blockDisplays = [
      'block',
      'list-item',
      'table',
      'table-caption',
      'table-cell',
      'table-column',
      'table-column-group',
      'table-footer',
      'table-footer-group',
      'table-header-group',
      'table-row',
      'table-row-group',
    ];

    const structuralTags = [
      TagName.BODY,
      TagName.FRAME,
      TagName.FRAMESET,
      TagName.HEAD,
      TagName.HTML,
    ];

    // The following tags are considered inline in IE, except LEGEND which is
    // only a block element in WEBKIT.
    const ambiguousTags = [
      TagName.DETAILS,
      TagName.HR,
      TagName.ISINDEX,
      TagName.LEGEND,
      TagName.MAIN,
      TagName.MAP,
      TagName.NOFRAMES,
      TagName.OPTGROUP,
      TagName.OPTION,
      TagName.SUMMARY,
    ];

    // Older versions of IE and Gecko consider the following elements to be
    // inline, but IE9+ and Gecko 2.0+ recognize the new elements.
    const legacyAmbiguousTags = [
      TagName.ARTICLE,
      TagName.ASIDE,
      TagName.FIGCAPTION,
      TagName.FIGURE,
      TagName.FOOTER,
      TagName.HEADER,
      TagName.HGROUP,
      TagName.NAV,
      TagName.SECTION,
    ];

    const tagsToIgnore = googArray.flatten(structuralTags, ambiguousTags);

    if (userAgent.IE && !userAgent.isDocumentModeOrHigher(9)) {
      googArray.extend(tagsToIgnore, legacyAmbiguousTags);
    }

    // Appending an applet tag can cause the test to hang if Java is blocked on
    // the system.
    tagsToIgnore.push(TagName.APPLET);

    // Appending an embed tag to the page in IE brings up a warning dialog about
    // loading Java content.
    if (userAgent.IE) {
      tagsToIgnore.push(TagName.EMBED);
    }

    const failures = [];
    for (let tag in TagName) {
      if (googArray.contains(tagsToIgnore, TagName[tag])) {
        continue;
      }

      const el = googDom.createElement(tag);
      document.body.appendChild(el);
      const display = style.getCascadedStyle(el, 'display') ||
          style.getComputedStyle(el, 'display');
      googDom.removeNode(el);

      if (editorNode.isBlockTag(el)) {
        if (!googArray.contains(blockDisplays, display)) {
          failures.push(`Display for ${tag} should be block-like`);
        }
      } else {
        if (googArray.contains(blockDisplays, display)) {
          failures.push(`Display for ${tag} should not be block-like`);
        }
      }
    }
    if (failures.length) {
      fail(failures.join('\n'));
    }
  },

  testSkipEmptyTextNodes() {
    assertNull(
        'skipEmptyTextNodes should gracefully handle null',
        editorNode.skipEmptyTextNodes(null));

    const dom1 = createDivWithTextNodes('abc', '', 'xyz', '', '');
    assertEquals(
        'expected not to skip first child', dom1.firstChild,
        editorNode.skipEmptyTextNodes(dom1.firstChild));
    assertEquals(
        'expected to skip second child', dom1.childNodes[2],
        editorNode.skipEmptyTextNodes(dom1.childNodes[1]));
    assertNull(
        'expected to skip all the rest of the children',
        editorNode.skipEmptyTextNodes(dom1.childNodes[3]));
  },

  testIsEditableContainer() {
    const editableContainerElement = document.getElementById('editableTest');
    assertTrue(
        'Container element should be considered editable container',
        editorNode.isEditableContainer(editableContainerElement));

    const nonEditableContainerElement = document.getElementById('parentNode');
    assertFalse(
        'Other element should not be considered editable container',
        editorNode.isEditableContainer(nonEditableContainerElement));
  },

  testIsEditable() {
    const editableContainerElement = document.getElementById('editableTest');
    const childNode = editableContainerElement.firstChild;
    const childElement =
        googDom.getElementsByTagName(TagName.SPAN, editableContainerElement)[0];

    assertFalse(
        'Container element should not be considered editable',
        editorNode.isEditable(editableContainerElement));
    assertTrue(
        'Child text node should be considered editable',
        editorNode.isEditable(childNode));
    assertTrue(
        'Child element should be considered editable',
        editorNode.isEditable(childElement));
    assertTrue(
        'Grandchild node should be considered editable',
        editorNode.isEditable(childElement.firstChild));
    assertFalse(
        'Other element should not be considered editable',
        editorNode.isEditable(document.getElementById('parentNode')));
  },

  testFindTopMostEditableAncestor() {
    const root = document.getElementById('editableTest');
    /** @suppress {checkTypes} suppression added to enable type checking */
    const span = googDom.getElementsByTagName(TagName.SPAN, root)[0];
    const textNode = span.firstChild;

    assertEquals(
        'Should return self if self is matched.', textNode,
        editorNode.findTopMostEditableAncestor(
            textNode, (node) => node.nodeType == NodeType.TEXT));
    assertEquals(
        'Should not walk out of editable node.', null,
        editorNode.findTopMostEditableAncestor(
            textNode, /**
                         @suppress {strictMissingProperties} suppression added
                         to enable type checking
                       */
            (node) => node.tagName == TagName.BODY));
    assertEquals(
        'Should not match editable container.', null,
        editorNode.findTopMostEditableAncestor(
            textNode, /**
                         @suppress {strictMissingProperties} suppression added
                         to enable type checking
                       */
            (node) => node.tagName == TagName.DIV));
    assertEquals(
        'Should find node in editable container.', span,
        editorNode.findTopMostEditableAncestor(
            textNode, /**
                         @suppress {strictMissingProperties} suppression added
                         to enable type checking
                       */
            (node) => node.tagName == TagName.SPAN));
  },

  /** @suppress {checkTypes} suppression added to enable type checking */
  testSplitDomTreeAt() {
    const innerHTML = '<p>1<b>2</b>3</p>';
    const root = googDom.createElement(TagName.DIV);

    root.innerHTML = innerHTML;
    let result = editorNode.splitDomTreeAt(
        googDom.getElementsByTagName(TagName.B, root)[0], null, root);
    testingDom.assertHtmlContentsMatch('<p>1<b>2</b></p>', root);
    testingDom.assertHtmlContentsMatch('<p>3</p>', result);

    root.innerHTML = innerHTML;
    result = editorNode.splitDomTreeAt(
        googDom.getElementsByTagName(TagName.B, root)[0],
        googDom.createTextNode('and'), root);
    testingDom.assertHtmlContentsMatch('<p>1<b>2</b></p>', root);
    testingDom.assertHtmlContentsMatch('<p>and3</p>', result);
  },

  testTransferChildren() {
    const prefix = '<b>Bold 1</b>';
    const innerHTML = '<b>Bold</b><ul><li>Item 1</li><li>Item 2</li></ul>';

    const root1 = googDom.createElement(TagName.DIV);
    root1.innerHTML = innerHTML;

    const root2 = googDom.createElement(TagName.P);
    root2.innerHTML = prefix;

    const b = googDom.getElementsByTagName(TagName.B, root1)[0];

    // Transfer the children.
    editorNode.transferChildren(root2, root1);
    assertEquals(0, root1.childNodes.length);
    testingDom.assertHtmlContentsMatch(prefix + innerHTML, root2);
    assertEquals(b, googDom.getElementsByTagName(TagName.B, root2)[1]);

    // Transfer them back.
    editorNode.transferChildren(root1, root2);
    assertEquals(0, root2.childNodes.length);
    testingDom.assertHtmlContentsMatch(prefix + innerHTML, root1);
    assertEquals(b, googDom.getElementsByTagName(TagName.B, root1)[1]);
  },
});
