/*
 * Copyright 2012 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.common.css.compiler.ast;

import static java.nio.charset.StandardCharsets.UTF_8;

import com.google.common.base.Functions;

import junit.framework.TestCase;

/**
 * Unit tests for {@code CssStringNode}
 *
 */
public class CssStringNodeTest extends TestCase {
  public void testCssValueNodeRoundtrip() throws Exception {
    String v = "ordinary";
    for (CssStringNode.Type t : CssStringNode.Type.values()) {
      CssStringNode n = new CssStringNode(t, v);
      n.setValue(v);
      assertEquals(v, n.getValue());
    }
  }

  public void testCssValueNodeFixedPoint() throws Exception {
    // This test doesn't care if setValue/getValue work in terms of
    // CSS or abstract values, but we just want to make sure that
    // eventually what we set is what we get.
    // Here's a value that must be escaped in CSS. It ensures that
    // we exercise whatever codec is in operation behind these
    // accessors.
    String v = "\\\\\"\"";
    CssStringNode n = new CssStringNode(
        CssStringNode.Type.DOUBLE_QUOTED_STRING, v);
    String v2 = n.getValue();
    n.setValue(v2);
    assertEquals(v2, n.getValue());
  }

  public void testConcreteRoundtrip() throws Exception {
    // This value is safe for verbatim inclusion in either kind of
    // string literal, and for each kind it includes:
    //   (a) an escape sequence that denotes a character that must
    // be escaped in literals of that kind.
    //   (b) a character (possibly as part of an escape sequence)
    // that must be escaped in literals of that kind
    // Also, it includes weird escape sequences that are unlikely
    // to occur together in sane usage, which helps demonstrate
    // normalization.
    String v = "\\'\\\"\\041zA\\0000411\\41 1";
    //          ^single quote
    //             ^double quote
    //                 ^weird A
    //                      ^z
    //                       ^A
    //                        ^weird A, second variation
    //                                ^1
    //                                 ^A with escape code delimiter
    //                                      ^1
    for (CssStringNode.Type t : CssStringNode.Type.values()) {
      CssStringNode n = new CssStringNode(t, v);
      n.setConcreteValue(v);
      assertEquals(v, n.getConcreteValue());
    }
  }

  public void testShortEscaper() throws Exception {
    for (String[] io : new String[][] {
        {"", ""},
        {"a", "a"},
        {"¤", "\\a4"},
        {"ξ", "\\3be"},
        {"ξe", "\\3be e"},
        {"ξx", "\\3bex"},
        {"唐", "\\5510"},
        {"𠍱", "\\20371"},
        {new String(
            new byte[] {(byte) 0xf0, (byte) 0xa0, (byte) 0x8d, (byte) 0xb1},
            UTF_8),
         "\\20371"}}) {
      assertEquals(io[1], CssStringNode.SHORT_ESCAPER.apply(io[0]));
    }
    // Six-hexadecimal-digit codepoints aren't allowed
    // leading-zeros-padding. This is also an interesting case because
    // Java chars and Strings are defined in terms of UTF-16, which
    // represents codepoints in this range as surrogate pairs.  Let's
    // use UTF-8 to specify the input because it's simpler:
    byte[] puabUtf8 = {(byte) 0xf4, (byte) 0x80, (byte) 0x80, (byte) 0x80};
    assertEquals("\\100000",
                 CssStringNode.SHORT_ESCAPER.apply(
                     new String(puabUtf8, UTF_8)));
    assertEquals("\\100000a",
                 CssStringNode.SHORT_ESCAPER.apply(
                     String.format("%sa",
                                   new String(puabUtf8, UTF_8))));
  }

  public void testInsertsIgnoredWhitespaceAfterEscape() throws Exception {
    // When parsing, we always discard zero or one whitespace after an
    // escape sequence.
    // See http://www.w3.org/TR/CSS2/syndata.html#characters
    // and http://www.w3.org/TR/css3-syntax/#consume-an-escaped-code-point
    String stringTemplate = "%s following (%s)";
    String cssTemplate = "%s  following (%s)";

    for (CssStringNode.Type type : CssStringNode.Type.values()) {
      // We produce escape sequences in three cases:
      // (1) newline
      assertEquals(
          String.format(cssTemplate, "\\00000a", type.getClass().getName()),
          CssStringNode.escape(
              type,
              CssStringNode.HTML_ESCAPER,
              String.format(stringTemplate, "\n", type.getClass().getName())));

      // (2) no CSS literal representation exists
      assertEquals(
          String.format(cssTemplate, "\\a4", type.getClass().getName()),
          CssStringNode.escape(
              type,
              CssStringNode.SHORT_ESCAPER,
              String.format(stringTemplate, "¤", type.getClass().getName())));

      // (3) HTML/SGML special character when using the HTML_ESCAPER
      assertEquals(
          String.format(cssTemplate, "\\00003c", type.getClass().getName()),
          CssStringNode.escape(
              type,
              CssStringNode.HTML_ESCAPER,
              String.format(stringTemplate, "<", type.getClass().getName())));
    }
  }

  public void testHtmlEscaper() throws Exception {
    for (String[] io : new String[][] {
        {"", ""},
        {"a", "a"},
        {"¤", "\\0000a4"},
        {"ξ", "\\0003be"},
        {"ξe", "\\0003bee"},
        {"ξx", "\\0003bex"},
        {"唐", "\\005510"},
        {"𠍱", "\\020371"},
        {new String(
            new byte[] {(byte) 0xf0, (byte) 0xa0, (byte) 0x8d, (byte) 0xb1},
            UTF_8),
         "\\020371"}}) {
      assertEquals(io[1], CssStringNode.HTML_ESCAPER.apply(io[0]));
    }
    assertEquals("\\000026", CssStringNode.HTML_ESCAPER.apply("&"));
    assertEquals("\\00003c", CssStringNode.HTML_ESCAPER.apply("<"));
    assertEquals("\\00003e", CssStringNode.HTML_ESCAPER.apply(">"));
    assertEquals("\\000022", CssStringNode.HTML_ESCAPER.apply("\""));
    assertEquals("\\000027", CssStringNode.HTML_ESCAPER.apply("'"));
  }

  public void testEscape() throws Exception {
    for (String[] io : new String[][] {
        {"", ""},
        {"a", "a"},
        {"\\", "\\\\"},
        {"19\\3=6", "19\\\\3=6"},
        {"say \"hello\"", "say \\\"hello\\\""},
        {"say 'goodbye'", "say 'goodbye'"}}) {
      assertEquals(
          io[1],
          CssStringNode.escape(
              CssStringNode.Type.DOUBLE_QUOTED_STRING,
              Functions.<String>identity(), io[0]));
    }
    assertEquals(
        "say \\'goodbye\\'",
        CssStringNode.escape(
            CssStringNode.Type.SINGLE_QUOTED_STRING,
            Functions.<String>identity(), "say 'goodbye'"));
  }

  public void testUnescape() throws Exception {
    for (String[] io : new String[][] {
        {"", ""},
        {"a", "a"},
        {"\\\\", "\\"},
        {"\\\"", "\""},
        {"\\41", "A"},
        {"\\41 ", "A"},
        {"\\41  ", "A "},
        // The spec requires us to discard a whitespace following an
        // escape sequence, even when the escape sequence is as long
        // as possible and hence there is no ambiguity about where it
        // ends.
        {"abc\\000041 ", "abcA"},
        {"abc\\000041  ", "abcA "},
        {"\\41x", "Ax"},
        {"\\0000a4", "¤"},
        {"\\0003be", "ξ"},
        {"\\0003bee", "ξe"},
        {"\\3be e", "ξe"},
        {"\\0003be", "ξ"},
        {"\\3be z", "ξz"},
        {"\\005510", "唐"},
        {"\\020371", "𠍱"},
        {"\\020371", new String(
            new byte[] {(byte) 0xf0, (byte) 0xa0, (byte) 0x8d, (byte) 0xb1},
            UTF_8)}}) {
      assertEquals(io[1], CssStringNode.unescape(io[0]));
    }

    // Now let's look at a character that requires a max-length escape
    // code in CSS and use of surrogate pairs in the JVM
    byte[] puabUtf8 = {(byte) 0xf4, (byte) 0x80, (byte) 0x80, (byte) 0x80};
    assertEquals(new String(puabUtf8, UTF_8),
                 CssStringNode.unescape("\\100000"));
    assertEquals(new String(puabUtf8, UTF_8) + "a",
                 CssStringNode.unescape("\\100000a"));

    // Here's an escape sequence denoting a code point beyond the Java
    // char/String repertoire. According to CSS 2.1, we can replace the
    // escape.
    int bigCodePoint = Character.MAX_CODE_POINT + 1;
    assertEquals(
        // replacement character
        "\ufffd",
        CssStringNode.unescape(
            String.format("\\%x", bigCodePoint)));
  }

  public void testCopyCtor() {
    CssStringNode a = new CssStringNode(
        CssStringNode.Type.DOUBLE_QUOTED_STRING, "foo");
    a.setConcreteValue("\\0066oobar");
    CssStringNode b = new CssStringNode(a);
    assertEquals("\\0066oobar", a.getConcreteValue());
    assertEquals(a.getConcreteValue(), b.getConcreteValue());
    assertEquals(a.getValue(), b.getValue());
  }

  public void testStringCannotDirectlyContainNewline() {
    // See http://www.w3.org/TR/CSS2/syndata.html#strings
    CssStringNode a = new CssStringNode(
        CssStringNode.Type.SINGLE_QUOTED_STRING, "line1\nline2");
    assertTrue(
        "We should support the Java String representation of newlines.",
        a.getValue().contains("\n"));
    assertFalse(
        "If we set a Java newline, it should be escaped in the"
        + " generated concrete value.",
        a.getConcreteValue().contains("\n"));
    assertFalse(
        "If we ask for CSS markup, we should escape newlines per the"
        + " CSS spec.",
        a.toString(CssStringNode.HTML_ESCAPER).contains("\n"));
    assertTrue(
        "Escaping a new line shouldn't affect the left hand side",
        a.toString(CssStringNode.HTML_ESCAPER).startsWith("'line1"));
    assertTrue(
        "Escaping a new line shouldn't affect the right-hand side",
        a.toString(CssStringNode.HTML_ESCAPER).endsWith("line2'"));
  }
}
