// Copyright 2011 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.soyTest');
goog.setTestOnly('goog.soyTest');

goog.require('goog.dom');
goog.require('goog.dom.NodeType');
goog.require('goog.dom.TagName');
goog.require('goog.functions');
goog.require('goog.soy');
/** @suppress {extraRequire} */
goog.require('goog.soy.testHelper');
goog.require('goog.testing.PropertyReplacer');
goog.require('goog.testing.jsunit');

var stubs;

function setUp() {
  stubs = new goog.testing.PropertyReplacer();
}

function tearDown() {
  stubs.reset();
}

function testRenderHtml() {
  var testDiv = goog.dom.createElement(goog.dom.TagName.DIV);
  goog.soy.renderHtml(testDiv, example.sanitizedHtmlTemplate());
  assertEquals('hello <b>world</b>', testDiv.innerHTML.toLowerCase());
}


function testRenderElement() {
  var testDiv = goog.dom.createElement(goog.dom.TagName.DIV);
  goog.soy.renderElement(testDiv, example.multiRootTemplate, {name: 'Boo'});
  assertEquals('<div>Hello</div><div>Boo</div>', elementToInnerHtml(testDiv));
}


function testRenderElementWithNoTemplateData() {
  var testDiv = goog.dom.createElement(goog.dom.TagName.DIV);
  goog.soy.renderElement(testDiv, example.noDataTemplate);
  assertEquals('<div>Hello</div>', elementToInnerHtml(testDiv));
}


function testRenderAsFragmentTextNode() {
  var fragment =
      goog.soy.renderAsFragment(example.textNodeTemplate, {name: 'Boo'});
  assertEquals(goog.dom.NodeType.TEXT, fragment.nodeType);
  assertEquals('Boo', fragmentToHtml(fragment));
}


function testRenderAsFragmentInjectedData() {
  var fragment = goog.soy.renderAsFragment(
      example.injectedDataTemplate, {name: 'Boo'}, {name: 'ijBoo'});
  assertEquals(goog.dom.NodeType.TEXT, fragment.nodeType);
  assertEquals('BooijBoo', fragmentToHtml(fragment));
}


function testRenderAsFragmentSingleRoot() {
  var fragment =
      goog.soy.renderAsFragment(example.singleRootTemplate, {name: 'Boo'});
  assertEquals(goog.dom.NodeType.ELEMENT, fragment.nodeType);
  assertEquals(String(goog.dom.TagName.SPAN), fragment.tagName);
  assertEquals('Boo', fragment.innerHTML);
}


function testRenderAsFragmentMultiRoot() {
  var fragment =
      goog.soy.renderAsFragment(example.multiRootTemplate, {name: 'Boo'});
  assertEquals(goog.dom.NodeType.DOCUMENT_FRAGMENT, fragment.nodeType);
  assertEquals('<div>Hello</div><div>Boo</div>', fragmentToHtml(fragment));
}


function testRenderAsFragmentNoData() {
  var fragment = goog.soy.renderAsFragment(example.noDataTemplate);
  assertEquals(goog.dom.NodeType.ELEMENT, fragment.nodeType);
  assertEquals('<div>Hello</div>', fragmentToHtml(fragment));
}


function testRenderAsElementTextNode() {
  var elem = goog.soy.renderAsElement(example.textNodeTemplate, {name: 'Boo'});
  assertEquals(goog.dom.NodeType.ELEMENT, elem.nodeType);
  assertEquals(String(goog.dom.TagName.DIV), elem.tagName);
  assertEquals('Boo', elementToInnerHtml(elem));
}

function testRenderAsElementInjectedData() {
  var elem = goog.soy.renderAsElement(
      example.injectedDataTemplate, {name: 'Boo'}, {name: 'ijBoo'});
  assertEquals(goog.dom.NodeType.ELEMENT, elem.nodeType);
  assertEquals(String(goog.dom.TagName.DIV), elem.tagName);
  assertEquals('BooijBoo', elementToInnerHtml(elem));
}


function testRenderAsElementSingleRoot() {
  var elem =
      goog.soy.renderAsElement(example.singleRootTemplate, {name: 'Boo'});
  assertEquals(goog.dom.NodeType.ELEMENT, elem.nodeType);
  assertEquals(String(goog.dom.TagName.SPAN), elem.tagName);
  assertEquals('Boo', elementToInnerHtml(elem));
}


function testRenderAsElementMultiRoot() {
  var elem = goog.soy.renderAsElement(example.multiRootTemplate, {name: 'Boo'});
  assertEquals(goog.dom.NodeType.ELEMENT, elem.nodeType);
  assertEquals(String(goog.dom.TagName.DIV), elem.tagName);
  assertEquals('<div>Hello</div><div>Boo</div>', elementToInnerHtml(elem));
}

function testRenderAsElementWithNoData() {
  var elem = goog.soy.renderAsElement(example.noDataTemplate);
  assertEquals('Hello', elementToInnerHtml(elem));
}

function testConvertToElement() {
  var elem = goog.soy.convertToElement(example.sanitizedHtmlTemplate());
  assertEquals(goog.dom.NodeType.ELEMENT, elem.nodeType);
  assertEquals(String(goog.dom.TagName.DIV), elem.tagName);
  assertEquals('hello <b>world</b>', elem.innerHTML.toLowerCase());
}


/**
 * Asserts that the function throws an error for unsafe templates.
 * @param {Function} func Callback to test.
 */
function assertUnsafeTemplateOutputErrorThrown(func) {
  stubs.set(goog.asserts, 'ENABLE_ASSERTS', true);
  assertContains(
      'Soy template output is unsafe for use as HTML',
      assertThrows(func).message);
  stubs.set(goog.asserts, 'ENABLE_ASSERTS', false);
  assertEquals('zSoyz', func());
}

function testAllowButEscapeUnsanitizedText() {
  var div = goog.dom.createElement(goog.dom.TagName.DIV);
  goog.soy.renderElement(div, example.unsanitizedTextTemplate);
  assertEquals('I &lt;3 Puppies &amp; Kittens', div.innerHTML);
  var fragment = goog.soy.renderAsFragment(example.unsanitizedTextTemplate);
  assertEquals('I <3 Puppies & Kittens', fragment.nodeValue);
  assertEquals(
      'I &lt;3 Puppies &amp; Kittens',
      goog.soy.renderAsElement(example.unsanitizedTextTemplate).innerHTML);
}

function testRejectSanitizedCss() {
  assertUnsafeTemplateOutputErrorThrown(function() {
    goog.soy.renderAsElement(example.sanitizedCssTemplate);
  });
}

function testRejectSanitizedCss() {
  assertUnsafeTemplateOutputErrorThrown(function() {
    return goog.soy
        .renderAsElement(example.templateSpoofingSanitizedContentString)
        .innerHTML;
  });
}

function testRejectStringTemplatesWhenModeIsSet() {
  stubs.set(goog.soy, 'REQUIRE_STRICT_AUTOESCAPE', true);
  assertUnsafeTemplateOutputErrorThrown(function() {
    return goog.soy.renderAsElement(example.noDataTemplate).innerHTML;
  });
}

function testAcceptSanitizedHtml() {
  assertEquals(
      'Hello World',
      goog.dom.getTextContent(
          goog.soy.renderAsElement(example.sanitizedHtmlTemplate)));
}

function testRejectSanitizedHtmlAttributes() {
  // Attributes context has nothing to do with html.
  assertUnsafeTemplateOutputErrorThrown(function() {
    return goog.dom.getTextContent(
        goog.soy.renderAsElement(example.sanitizedHtmlAttributesTemplate));
  });
}

function testAcceptNonObject() {
  // Some templates, or things that spoof templates in unit tests, might return
  // non-strings in unusual cases.
  assertEquals(
      'null', goog.dom.getTextContent(
                  goog.soy.renderAsElement(goog.functions.constant(null))));
}

function testDebugAssertionWithBadFirstTag() {
  try {
    goog.soy.renderAsElement(example.tableRowTemplate);
    // Expect no exception in production code.
    assert(!goog.DEBUG);
  } catch (e) {
    // Expect exception in debug code.
    assert(goog.DEBUG);
    // Make sure to let the developer know which tag caused the problem.
    assertContains('<tr>', e.message);
  }

  try {
    goog.soy.renderAsFragment(example.tableRowTemplate);
    // Expect no exception in production code.
    assert(!goog.DEBUG);
  } catch (e) {
    // Expect exception in debug code.
    assert(goog.DEBUG);
    // Make sure to let the developer know which tag caused the problem.
    assertContains('<tr>', e.message);
  }

  try {
    goog.soy.renderAsElement(example.colGroupTemplateCaps);
    // Expect no exception in production code.
    assert(!goog.DEBUG);
  } catch (e) {
    // Expect exception in debug code.
    assert(goog.DEBUG);
    // Make sure to let the developer know which tag caused the problem.
    assertContains('<COLGROUP>', e.message);
  }
}

/**
 * When innerHTML is assigned on an element in IE, IE recursively severs all
 * parent-children links in the removed content. This test ensures that that
 * doesn't happen when re-rendering an element with soy.
 */
function testRerenderLeavesChildrenInIE() {
  // Given a div with existing content.
  var grandchildDiv = goog.dom.createElement(goog.dom.TagName.DIV);
  var childDiv =
      goog.dom.createDom(goog.dom.TagName.DIV, null, [grandchildDiv]);
  var testDiv = goog.dom.createDom(goog.dom.TagName.DIV, null, [childDiv]);
  // Expect parent/children links.
  assertArrayEquals(
      'Expect testDiv to contain childDiv.', [childDiv],
      Array.from(testDiv.children));
  assertEquals(
      'Expect childDiv to be contained in testDiv.', testDiv,
      childDiv.parentElement);
  assertArrayEquals(
      'Expect childDiv to contain grandchildDiv.', [grandchildDiv],
      Array.from(childDiv.children));
  assertEquals(
      'Expect grandchildDiv to be contained in childDiv.', childDiv,
      grandchildDiv.parentElement);

  // When the div's content is re-rendered.
  goog.soy.renderHtml(testDiv, example.sanitizedHtmlTemplate());
  assertEquals(
      `Expect testDiv's contents to complete change`, 'hello <b>world</b>',
      testDiv.innerHTML.toLowerCase());
  // Expect the previous childDiv tree to retain its parent-child connections.
  assertArrayEquals(
      'Expect childDiv to still contain grandchildDiv.', [grandchildDiv],
      Array.from(childDiv.children));
  assertEquals(
      'Expect grandchildDiv to still be contained in childDiv.', childDiv,
      grandchildDiv.parentElement);
}
