/*
 * Copyright 2008 Google Inc.
 *
 * 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.
 */
package com.google.gwt.uibinder.rebind;

import com.google.gwt.core.ext.TreeLogger;
import com.google.gwt.core.ext.UnableToCompleteException;
import com.google.gwt.core.ext.typeinfo.TypeOracle;
import com.google.gwt.dev.CompilerContext;
import com.google.gwt.dev.javac.CompilationState;
import com.google.gwt.dev.javac.CompilationStateBuilder;
import com.google.gwt.dev.javac.testing.impl.MockResourceOracle;
import com.google.gwt.dev.util.log.PrintWriterTreeLogger;
import com.google.gwt.uibinder.attributeparsers.AttributeParsers;
import com.google.gwt.uibinder.test.UiJavaResources;

import junit.framework.TestCase;

import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.Text;
import org.xml.sax.SAXParseException;

import java.io.PrintWriter;
import java.util.Arrays;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;

/**
 * Tests XMLElement.
 */
public class XMLElementTest extends TestCase {
  private static final String STRING_WITH_DOUBLEQUOTE = "I have a \" quote in me";

  private static final W3cDomHelper docHelper = new W3cDomHelper(
      TreeLogger.NULL, new MockResourceOracle());

  private static TreeLogger createCompileLogger() {
    PrintWriterTreeLogger logger = new PrintWriterTreeLogger(new PrintWriter(
        System.err, true));
    logger.setMaxDetail(TreeLogger.ERROR);
    return logger;
  }

  private TypeOracle types;
  private MockMortalLogger logger;

  private Document doc;
  private XMLElementProvider elemProvider;
  private XMLElement elm;
  private Element item;

  @Override
  public void setUp() throws Exception {
    super.setUp();
    CompilationState state = CompilationStateBuilder.buildFrom(
        createCompileLogger(), new CompilerContext(), UiJavaResources.getUiResources());
    types = state.getTypeOracle();
    logger = new MockMortalLogger();
    init(DesignTimeUtilsStub.EMPTY,
        "<doc><elm attr1=\"attr1Value\" attr2=\"attr2Value\"/></doc>");
  }

  public void testAssertNoAttributes() throws SAXParseException {
    init("<doc>\n\n<elm yes='true' no='false'>Blah <blah/> blah</elm></doc>");
    assertNull(logger.died);
    try {
      elm.assertNoAttributes();
      fail();
    } catch (UnableToCompleteException e) {
      assertTrue("Expect extra attributes list",
          logger.died.contains("\"yes\""));
      assertTrue("Expect extra attributes list", logger.died.contains("\"no\""));
      assertTrue("Expect line number " + logger.died,
          logger.died.contains("Unknown:3"));
    }
  }

  public void testAssertNoBody() throws SAXParseException {
    init("<doc>\n\n<elm yes='true' no='false'>Blah <blah/> blah</elm></doc>");
    assertNull(logger.died);
    try {
      elm.assertNoBody();
      fail();
    } catch (UnableToCompleteException e) {
      assertTrue("Expect extra child " + logger.died,
          logger.died.contains("<blah>"));
      assertTrue("Expect line number", logger.died.contains("Unknown:3"));
    }
  }

  public void testAssertNoText() throws SAXParseException {
    init("<doc>\n\n<elm yes='true' no='false'>Blah <blah/> blah</elm></doc>");
    assertNull(logger.died);
    try {
      elm.assertNoText();
      fail();
    } catch (UnableToCompleteException e) {
      assertTrue("Expect extra text", logger.died.contains("Blah"));
      assertTrue("Expect extra text", logger.died.contains("blah"));
      assertTrue("Expect line number", logger.died.contains("Unknown:3"));
    }
  }

  public void testConsumeBoolean() throws SAXParseException,
      UnableToCompleteException {
    init("<doc>\n\n<elm yes='true' no='false' "
        + "fnord='fnord' ref='{foo.bar.baz}'/></doc>");

    assertNull(elm.consumeBooleanAttribute("foo"));

    assertEquals("true", elm.consumeBooleanAttribute("yes"));
    assertNull(elm.consumeBooleanAttribute("yes"));

    assertEquals("false", elm.consumeBooleanAttribute("no"));
    assertNull(elm.consumeBooleanAttribute("no"));

    assertEquals("foo.bar().baz()", elm.consumeBooleanAttribute("ref"));

    assertNull(logger.died);
    try {
      elm.consumeBooleanAttribute("fnord");
      fail("Should throw UnableToCompleteException on misparse");
    } catch (UnableToCompleteException c) {
      assertNotNull(logger.died);
      assertTrue("Expect line number", logger.died.contains("Unknown:3"));
    }
  }

  public void testConsumeBooleanConstant() throws SAXParseException,
      UnableToCompleteException {
    init("<doc>\n\n<elm yes='true' no='false' "
        + "fnord='fnord' ref='{foo.bar.baz}' empty=''/></doc>");

    assertNull(elm.consumeBooleanConstantAttribute("foo"));

    assertTrue(elm.consumeBooleanConstantAttribute("yes"));
    assertNull(elm.consumeBooleanConstantAttribute("yes"));

    assertFalse(elm.consumeBooleanConstantAttribute("no"));
    assertNull(elm.consumeBooleanConstantAttribute("no"));

    assertNull(logger.died);
    try {
      elm.consumeBooleanConstantAttribute("empty");
    } catch (UnableToCompleteException c) {
      assertNotNull(logger.died);
      assertTrue("Expect line number", logger.died.contains("Unknown:3"));
    }

    logger.died = null;
    try {
      elm.consumeBooleanConstantAttribute("ref");
      fail("Should throw UnableToCompleteException on field ref");
    } catch (UnableToCompleteException c) {
      assertNotNull(logger.died);
      assertTrue("Expect line number", logger.died.contains("Unknown:3"));
    }

    logger.died = null;
    try {
      elm.consumeBooleanConstantAttribute("fnord");
      fail("Should throw UnableToCompleteException on misparse");
    } catch (UnableToCompleteException c) {
      assertNotNull(logger.died);
      assertTrue("Expect line number", logger.died.contains("Unknown:3"));
    }
  }

  public void testConsumeBooleanDefault() throws SAXParseException,
      UnableToCompleteException {
    init("<doc>\n\n<elm yes='true' no='false' "
        + "fnord='fnord' ref='{foo.bar.baz}'/></doc>");

    assertEquals("false", elm.consumeBooleanAttribute("foo", false));
    assertEquals("true", elm.consumeBooleanAttribute("foo", true));

    assertEquals("true", elm.consumeBooleanAttribute("yes", false));
    assertEquals("false", elm.consumeBooleanAttribute("yes", false));

    assertEquals("false", elm.consumeBooleanAttribute("no", true));
    assertEquals("true", elm.consumeBooleanAttribute("no", true));

    assertEquals("foo.bar().baz()", elm.consumeBooleanAttribute("ref", true));
    assertEquals("true", elm.consumeBooleanAttribute("ref", true));

    assertNull(logger.died);
    try {
      elm.consumeBooleanAttribute("fnord");
      fail("Should throw UnableToCompleteException on misparse");
    } catch (UnableToCompleteException c) {
      assertNotNull(logger.died);
      assertTrue("Expect line number", logger.died.contains("Unknown:3"));
    }
  }

  /**
   * Test that {@link XMLElement#consumeBooleanAttribute(String, boolean)} calls
   * {@link DesignTimeUtils}.
   */
  public void testConsumeBooleanDefault_designTime() throws Exception {
    DesignTimeUtilsImpl designTime = new DesignTimeUtilsImpl();
    init(designTime,
        "<doc><elm yes='true' no='false' ref='{foo.bar.baz}'/></doc>");
    // consume attributes
    assertEquals("true", elm.consumeBooleanAttribute("foo", true));
    assertEquals("true", elm.consumeBooleanAttribute("yes"));
    assertEquals("false", elm.consumeBooleanAttribute("no"));
    assertEquals("foo.bar().baz()", elm.consumeBooleanAttribute("ref"));
    // validate
    Map<String, String> attributes = designTime.getAttributes();
    assertEquals(4, attributes.size());
    assertEquals("true", attributes.get("0/0 foo.default"));
    assertEquals("true", attributes.get("0/0 yes"));
    assertEquals("false", attributes.get("0/0 no"));
    assertEquals("foo.bar().baz()", attributes.get("0/0 ref"));
  }

  /**
   * Test for {@link XMLElement#consumeStringArrayAttribute(String)}.
   */
  public void testConsumeStringArray() throws Exception {
    init("<doc>\n\n<elm spaces='a b c' commas='a,b,c' mixed='a, b, c'/></doc>");

    String[] expected = new String[]{"\"a\"", "\"b\"", "\"c\""};
    assertTrue(Arrays.equals(new String[]{},
        elm.consumeStringArrayAttribute("no-such-attribute")));
    assertTrue(Arrays.equals(expected,
        elm.consumeStringArrayAttribute("spaces")));
    assertTrue(Arrays.equals(expected,
        elm.consumeStringArrayAttribute("commas")));
    assertTrue(Arrays.equals(expected, elm.consumeStringArrayAttribute("mixed")));
  }

  /**
   * Test that {@link XMLElement#consumeStringArrayAttribute(String)} calls
   * {@link DesignTimeUtils}.
   */
  public void testConsumeStringArray_designTime() throws Exception {
    DesignTimeUtilsImpl designTime = new DesignTimeUtilsImpl();
    init(designTime, "<doc>\n\n<elm attr='a b c'/></doc>");
    // consume attributes
    String[] expected = new String[]{"\"a\"", "\"b\"", "\"c\""};
    assertTrue(Arrays.equals(expected, elm.consumeStringArrayAttribute("attr")));
    // validate
    Map<String, String> attributes = designTime.getAttributes();
    assertEquals(1, attributes.size());
    assertEquals("new String[] {\"a\", \"b\", \"c\"}",
        attributes.get("0/0 attr"));
  }

  public void testConsumeChildrenNoTextAllowed() throws SAXParseException {
    init("<doc>\n\n<elm><child>Hi.</child> Stray text is bad</elm></doc>");
    assertNull(logger.died);
    try {
      elm.consumeChildElements();
      fail();
    } catch (UnableToCompleteException e) {
      assertNotNull(logger.died);
      assertTrue("Expect line number", logger.died.contains("Unknown:3"));
    }
  }

  public void testConsumeInnerTextEscapedAsHtmlStringLiteral()
      throws UnableToCompleteException {
    appendText(STRING_WITH_DOUBLEQUOTE);
    assertEquals(
        UiBinderWriter.escapeTextForJavaStringLiteral(STRING_WITH_DOUBLEQUOTE),
        elm.consumeInnerTextEscapedAsHtmlStringLiteral(new NullInterpreter<String>()));
  }

  public void testConsumeInnerTextEscapedAsHtmlStringLiteralEmpty()
      throws UnableToCompleteException {
    assertEquals(
        "",
        elm.consumeInnerTextEscapedAsHtmlStringLiteral(new NullInterpreter<String>()));
  }

  public void testConsumeRawAttribute() {
    assertEquals("attr1Value", elm.consumeRawAttribute("attr1"));
    assertNull(elm.consumeRawAttribute("attr1"));
  }

  public void testConsumeRawAttributeWithDefault() {
    assertEquals("attr1Value", elm.consumeRawAttribute("attr1", "default"));
    assertEquals("default", elm.consumeRawAttribute("attr1", "default"));
    assertEquals("otherDefault",
        elm.consumeRawAttribute("unsetthing", "otherDefault"));
  }

  public void testConsumeRequiredRaw() throws UnableToCompleteException {
    assertEquals("attr1Value", elm.consumeRequiredRawAttribute("attr1"));
    assertNull(logger.died);
    try {
      elm.consumeRequiredRawAttribute("unsetthing");
      fail("Should have thrown UnableToCompleteException");
    } catch (UnableToCompleteException e) {
      assertNotNull(logger.died);
    }
  }

  public void testConsumeRequired() throws UnableToCompleteException {
    assertEquals(
        "\"attr1Value\"",
        elm.consumeRequiredAttribute("attr1",
            types.findType("java.lang.String")));
  }

  /**
   * Test that {@link XMLElement#consumeRequiredAttribute(String)} calls
   * {@link DesignTimeUtils}.
   */
  public void testConsumeRequired_designTime() throws Exception {
    DesignTimeUtilsImpl designTime = new DesignTimeUtilsImpl();
    init(designTime, "<doc><elm attr='val'/></doc>");
    // consume attributes
    assertEquals(
        "\"val\"",
        elm.consumeRequiredAttribute("attr", types.findType("java.lang.String")));
    // validate
    Map<String, String> attributes = designTime.getAttributes();
    assertEquals(1, attributes.size());
    assertEquals("\"val\"", attributes.get("0/0 attr"));
  }

  public void testConsumeRequiredDouble() throws UnableToCompleteException,
      SAXParseException {
    init("<doc>\n\n<elm minus='-123.45' plus='123.45' minus-one='-1' "
        + "plus-one='1' fnord='fnord' ref='{foo.bar.baz}'/></doc>");
    assertEquals("1", elm.consumeRequiredDoubleAttribute("plus-one"));
    assertEquals("-1", elm.consumeRequiredDoubleAttribute("minus-one"));
    assertEquals("123.45", elm.consumeRequiredDoubleAttribute("plus"));
    assertEquals("-123.45", elm.consumeRequiredDoubleAttribute("minus"));
    assertEquals("(double)foo.bar().baz()",
        elm.consumeRequiredDoubleAttribute("ref"));

    assertNull(logger.died);
    try {
      elm.consumeRequiredDoubleAttribute("fnord");
      fail("Should throw UnableToCompleteException on misparse");
    } catch (UnableToCompleteException c) {
      assertNotNull(logger.died);
      assertTrue("Expect line number", logger.died.contains("Unknown:3"));
    }

    logger.died = null;
    try {
      elm.consumeRequiredDoubleAttribute("plus-one");
      fail("Should throw UnableToCompleteException consumed attribute");
    } catch (UnableToCompleteException c) {
      assertNotNull(logger.died);
      assertTrue("Expect line number", logger.died.contains("Unknown:3"));
    }

    logger.died = null;
    try {
      elm.consumeRequiredDoubleAttribute("empty");
      fail("Should throw UnableToCompleteException on no such attribute");
    } catch (UnableToCompleteException c) {
      assertNotNull(logger.died);
      assertTrue("Expect line number", logger.died.contains("Unknown:3"));
    }
  }

  /**
   * Test for {@link XMLElement#consumeRequiredIntAttribute(String)}.
   */
  public void testConsumeRequiredInt() throws UnableToCompleteException,
      SAXParseException {
    init("<doc>\n\n<elm plus='123' minus='-123'"
        + " fnord='fnord' ref='{foo.bar.baz}'/></doc>");
    assertEquals("123", elm.consumeRequiredIntAttribute("plus"));
    assertEquals("-123", elm.consumeRequiredIntAttribute("minus"));
    assertEquals("(int)foo.bar().baz()", elm.consumeRequiredIntAttribute("ref"));

    assertNull(logger.died);
    try {
      elm.consumeRequiredDoubleAttribute("fnord");
      fail("Should throw UnableToCompleteException on misparse");
    } catch (UnableToCompleteException c) {
      assertNotNull(logger.died);
      assertTrue("Expect line number", logger.died.contains("Unknown:3"));
    }

    logger.died = null;
    try {
      elm.consumeRequiredDoubleAttribute("empty");
      fail("Should throw UnableToCompleteException on no such attribute");
    } catch (UnableToCompleteException c) {
      assertNotNull(logger.died);
      assertTrue("Expect line number", logger.died.contains("Unknown:3"));
    }
  }

  public void testConsumeSingleChildElementEmpty() throws SAXParseException,
      UnableToCompleteException {
    assertNull(logger.died);
    try {
      elm.consumeSingleChildElement();
      fail("Should throw on single child element");
    } catch (UnableToCompleteException e) {
      assertNotNull(logger.died);
      assertTrue("Expect line number", logger.died.contains("Unknown:1"));
    }

    init("<doc><elm><child>Hi.</child></elm></doc>");
    assertEquals("Hi.",
        elm.consumeSingleChildElement().consumeUnescapedInnerText());

    logger.died = null;
    init("<doc>\n\n<elm id='elm'><child>Hi.</child><child>Ho.</child></elm></doc>");
    assertNull(logger.died);
    try {
      elm.consumeSingleChildElement();
      fail("Should throw on too many children");
    } catch (UnableToCompleteException e) {
      assertNotNull(logger.died);
      assertTrue("Expect line number", logger.died.contains("Unknown:3"));
    }
  }

  public void testConsumeUnescapedInnerText() throws UnableToCompleteException {
    appendText(STRING_WITH_DOUBLEQUOTE);
    assertEquals(STRING_WITH_DOUBLEQUOTE, elm.consumeUnescapedInnerText());
  }

  public void testConsumeUnescapedInnerTextEmpty()
      throws UnableToCompleteException {
    assertEquals("", elm.consumeUnescapedInnerText());
  }

  public void testNullOnMissingAttribute() {
    assertNull(elm.consumeRawAttribute("fnord"));
  }

  public void testIterator() {
    String[] expecteds = {"attr1", "attr2"};
    Set<String> seen = new HashSet<String>();
    for (int i = elm.getAttributeCount() - 1; i >= 0; i--) {
      XMLAttribute attr = elm.getAttribute(i);
      String expected = expecteds[i];
      assertEquals(expected, attr.getLocalName());
      assertFalse(attr.isConsumed());
      assertEquals(expected + "Value", attr.consumeRawValue());
      assertTrue(attr.isConsumed());
      seen.add(expected);
    }
    assertEquals(2, seen.size());
  }

  public void testNoEndTags() throws SAXParseException {
    doc = docHelper.documentFor("<doc><br/></doc>", null);
    Element documentElement = doc.getDocumentElement();
    Element item = (Element) documentElement.getElementsByTagName("br").item(0);
    XMLElement elm = elemProvider.get(item);
    assertEquals("br", item.getTagName());
    assertEquals("", elm.getClosingTag());
  }

  private void appendText(final String text) {
    Text t = doc.createTextNode(text);
    item.appendChild(t);
  }

  /**
   * Initializes {@link #elemProvider} and parses DOM string.
   */
  private void init(DesignTimeUtils designTime, String domString)
      throws SAXParseException {
    elemProvider = new XMLElementProviderImpl(new AttributeParsers(types, null,
        logger), types, logger, designTime);
    init(domString);
    designTime.rememberPathForElements(doc);
  }

  /**
   * Parses specified DOM string into {@link #doc}, extract first element into
   * {@link #item} and prepares its {@link XMLElement} into {@link #elm}.
   */
  private void init(String domString) throws SAXParseException {
    doc = docHelper.documentFor(domString, null);
    item = (Element) doc.getDocumentElement().getElementsByTagName("elm").item(
        0);
    elm = elemProvider.get(item);
  }
}
