/*
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements.  See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership.  The ASF licenses this file
 * to you 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 freemarker.template.utility;

import static org.junit.Assert.*;

import java.io.IOException;
import java.io.StringWriter;
import java.util.regex.Pattern;

import org.hamcrest.Matchers;
import org.junit.Test;

public class StringUtilTest {

    @Test
    public void testV2319() {
        assertEquals("\\n\\r\\f\\b\\t\\x00\\x19", StringUtil.javaScriptStringEnc("\n\r\f\b\t\u0000\u0019"));
    }

    @Test
    public void testControlChars() {
        assertEsc(
                "\n\r\f\b\t \u0000\u0019\u001F \u007F\u0080\u009F \u2028\u2029",
                "\\n\\r\\f\\b\\t \\x00\\x19\\x1F \\x7F\\x80\\x9F \\u2028\\u2029",
                "\\n\\r\\f\\b\\t \\u0000\\u0019\\u001F \\u007F\\u0080\\u009F \\u2028\\u2029");
    }

    @Test
    public void testHtmlChars() {
        assertEsc(
                "<safe>/>->]> </foo> <!-- --> <![CDATA[ ]]> <?php?>",
                "<safe>/>->]> <\\/foo> \\x3C!-- --\\> \\x3C![CDATA[ ]]\\> \\x3C?php?>",
                "<safe>/>->]> <\\/foo> \\u003C!-- --\\u003E \\u003C![CDATA[ ]]\\u003E \\u003C?php?>");
        assertEsc("<!c", "\\x3C!c", "\\u003C!c");
        assertEsc("c<!", "c\\x3C!", "c\\u003C!");
        assertEsc("c<", "c\\x3C", "c\\u003C");
        assertEsc("c<c", "c<c", "c<c");
        assertEsc("<c", "<c", "<c");
        assertEsc(">", "\\>", "\\u003E");
        assertEsc("->", "-\\>", "-\\u003E");
        assertEsc("-->", "--\\>", "--\\u003E");
        assertEsc("c-->", "c--\\>", "c--\\u003E");
        assertEsc("-->c", "--\\>c", "--\\u003Ec");
        assertEsc("]>", "]\\>", "]\\u003E");
        assertEsc("]]>", "]]\\>", "]]\\u003E");
        assertEsc("c]]>", "c]]\\>", "c]]\\u003E");
        assertEsc("]]>c", "]]\\>c", "]]\\u003Ec");
        assertEsc("c->", "c->", "c->");
        assertEsc("c>", "c>", "c>");
        assertEsc("-->", "--\\>", "--\\u003E");
        assertEsc("/", "\\/", "\\/");
        assertEsc("/c", "\\/c", "\\/c");
        assertEsc("</", "<\\/", "<\\/");
        assertEsc("</c", "<\\/c", "<\\/c");
        assertEsc("c/", "c/", "c/");
    }

    @Test
    public void testJSChars() {
        assertEsc("\"", "\\\"", "\\\"");
        assertEsc("'", "\\'", "'");
        assertEsc("\\", "\\\\", "\\\\");
    }

    @Test
    public void testSameStringsReturned() {
        String s = "==> I/m <safe>!";
        assertTrue(s == StringUtil.jsStringEnc(s, false));  // "==" because is must return the same object
        assertTrue(s == StringUtil.jsStringEnc(s, true));

        s = "";
        assertTrue(s == StringUtil.jsStringEnc(s, false));
        assertTrue(s == StringUtil.jsStringEnc(s, true));

        s = "\u00E1rv\u00EDzt\u0171r\u0151 \u3020";
        assertEquals(s, StringUtil.jsStringEnc(s, false));
        assertTrue(s == StringUtil.jsStringEnc(s, false));
        assertTrue(s == StringUtil.jsStringEnc(s, true));
    }

    @Test
    public void testOneOffs() {
        assertEsc("c\"c\"cc\"\"c", "c\\\"c\\\"cc\\\"\\\"c", "c\\\"c\\\"cc\\\"\\\"c");
        assertEsc("\"c\"cc\"", "\\\"c\\\"cc\\\"", "\\\"c\\\"cc\\\"");
        assertEsc("c/c/cc//c", "c/c/cc//c", "c/c/cc//c");
        assertEsc("c<c<cc<<c", "c<c<cc<<c", "c<c<cc<<c");
        assertEsc("/<", "\\/\\x3C", "\\/\\u003C");
        assertEsc(">", "\\>", "\\u003E");
        assertEsc("]>", "]\\>", "]\\u003E");
        assertEsc("->", "-\\>", "-\\u003E");
    }

    @Test
    public void testFTLEscaping() {
        assertFTLEsc("", "", "", "", "\"\"");
        assertFTLEsc("\'", "\\'", "'", "\\'", "\"'\"");
        assertFTLEsc("\"", "\\\"", "\\\"", "\"", "'\"'");
        assertFTLEsc("\"", "\\\"", "\\\"", "\"", "'\"'");
        assertFTLEsc("foo", "foo", "foo", "foo", "\"foo\"");
        assertFTLEsc("foo's", "foo\\'s", "foo's", "foo\\'s", "\"foo's\"");
        assertFTLEsc("foo \"", "foo \\\"", "foo \\\"", "foo \"", "'foo \"'");
        assertFTLEsc("foo's \"", "foo\\'s \\\"", "foo's \\\"", "foo\\'s \"", "\"foo's \\\"\"");
        assertFTLEsc("foo\nb\u0000c", "foo\\nb\\x0000c", "foo\\nb\\x0000c", "foo\\nb\\x0000c", "\"foo\\nb\\x0000c\"");
    }
    
    private void assertEsc(String s, String javaScript, String json) {
        assertEquals(javaScript, StringUtil.jsStringEnc(s, false));
        assertEquals(json, StringUtil.jsStringEnc(s, true));
    }

    private void assertFTLEsc(String s, String partAny, String partQuot, String partApos, String quoted) {
        assertEquals(partAny, StringUtil.FTLStringLiteralEnc(s));
        assertEquals(partQuot, StringUtil.FTLStringLiteralEnc(s, '\"'));
        assertEquals(partApos, StringUtil.FTLStringLiteralEnc(s, '\''));
        assertEquals(quoted, StringUtil.ftlQuote(s));
    }
    
    @Test
    public void testTrim() {
        assertSame(CollectionUtils.EMPTY_CHAR_ARRAY, StringUtil.trim(CollectionUtils.EMPTY_CHAR_ARRAY));
        assertSame(CollectionUtils.EMPTY_CHAR_ARRAY, StringUtil.trim(" \t\u0001 ".toCharArray()));
        {
            char[] cs = "foo".toCharArray();
            assertSame(cs, cs);
        }
        assertArrayEquals("foo".toCharArray(), StringUtil.trim("foo ".toCharArray()));
        assertArrayEquals("foo".toCharArray(), StringUtil.trim(" foo".toCharArray()));
        assertArrayEquals("foo".toCharArray(), StringUtil.trim(" foo ".toCharArray()));
        assertArrayEquals("foo".toCharArray(), StringUtil.trim("\t\tfoo \r\n".toCharArray()));
        assertArrayEquals("x".toCharArray(), StringUtil.trim(" x ".toCharArray()));
        assertArrayEquals("x y z".toCharArray(), StringUtil.trim(" x y z ".toCharArray()));
    }

    @Test
    public void testIsTrimmedToEmpty() {
        assertTrue(StringUtil.isTrimmableToEmpty("".toCharArray()));
        assertTrue(StringUtil.isTrimmableToEmpty("\r\r\n\u0001".toCharArray()));
        assertFalse(StringUtil.isTrimmableToEmpty("x".toCharArray()));
        assertFalse(StringUtil.isTrimmableToEmpty("  x  ".toCharArray()));
    }
    
    @Test
    public void testJQuote() {
        assertEquals("null", StringUtil.jQuote(null));
        assertEquals("\"foo\"", StringUtil.jQuote("foo"));
        assertEquals("\"123\"", StringUtil.jQuote(Integer.valueOf(123)));
        assertEquals("\"foo's \\\"bar\\\"\"",
                StringUtil.jQuote("foo's \"bar\""));
        assertEquals("\"\\n\\r\\t\\u0001\"",
                StringUtil.jQuote("\n\r\t\u0001"));
        assertEquals("\"<\\nb\\rc\\td\\u0001>\"",
                StringUtil.jQuote("<\nb\rc\td\u0001>"));
    }

    @Test
    public void testJQuoteNoXSS() {
        assertEquals("null", StringUtil.jQuoteNoXSS(null));
        assertEquals("\"foo\"", StringUtil.jQuoteNoXSS("foo"));
        assertEquals("\"123\"", StringUtil.jQuoteNoXSS(Integer.valueOf(123)));
        assertEquals("\"foo's \\\"bar\\\"\"",
                StringUtil.jQuoteNoXSS("foo's \"bar\""));
        assertEquals("\"\\n\\r\\t\\u0001\"",
                StringUtil.jQuoteNoXSS("\n\r\t\u0001"));
        assertEquals("\"\\u003C\\nb\\rc\\td\\u0001>\"",
                StringUtil.jQuoteNoXSS("<\nb\rc\td\u0001>"));
        assertEquals("\"\\u003C\\nb\\rc\\td\\u0001>\"",
                StringUtil.jQuoteNoXSS((Object) "<\nb\rc\td\u0001>"));
    }
    
    @Test
    public void testFTLStringLiteralEnc() {
        assertEquals("", StringUtil.FTLStringLiteralEnc(""));
        assertEquals("abc", StringUtil.FTLStringLiteralEnc("abc"));
        assertEquals("{", StringUtil.FTLStringLiteralEnc("{"));
        assertEquals("a{b}c", StringUtil.FTLStringLiteralEnc("a{b}c"));
        assertEquals("a#b", StringUtil.FTLStringLiteralEnc("a#b"));
        assertEquals("a$b", StringUtil.FTLStringLiteralEnc("a$b"));
        assertEquals("a#\\{b}c", StringUtil.FTLStringLiteralEnc("a#{b}c"));
        assertEquals("a$\\{b}c", StringUtil.FTLStringLiteralEnc("a${b}c"));
        assertEquals("a'c\\\"d", StringUtil.FTLStringLiteralEnc("a'c\"d", '"'));
        assertEquals("a\\'c\"d", StringUtil.FTLStringLiteralEnc("a'c\"d", '\''));
        assertEquals("a\\'c\"d", StringUtil.FTLStringLiteralEnc("a'c\"d", '\''));
        assertEquals("\\n\\r\\t\\f\\x0002\\\\", StringUtil.FTLStringLiteralEnc("\n\r\t\f\u0002\\"));
        assertEquals("\\l\\g\\a", StringUtil.FTLStringLiteralEnc("<>&"));
    }
    
    @Test
    public void testGlobToRegularExpression() {
        assertGlobMatches("a/b/c.ftl", "a/b/c.ftl");
        assertGlobDoesNotMatch("/a/b/cxftl", "/a/b/c.ftl", "a/b/C.ftl");
        
        assertGlobMatches("a/b/*.ftl", "a/b/.ftl", "a/b/x.ftl", "a/b/xx.ftl");
        assertGlobDoesNotMatch("a/b/*.ftl", "a/c/x.ftl", "a/b/c/x.ftl", "/a/b/x.ftl", "a/b/xxftl");
        
        assertGlobMatches("a/b/?.ftl", "a/b/x.ftl");
        assertGlobDoesNotMatch("a/b/?.ftl", "a/c/x.ftl", "a/b/.ftl", "a/b/xx.ftl", "a/b/xxftl");
        
        assertGlobMatches("a/**/c.ftl", "a/b/c.ftl", "a/c.ftl", "a/b/b2/b3/c.ftl", "a//c.ftl");
        assertGlobDoesNotMatch("a/**/c.ftl", "x/b/c.ftl", "a/b/x.ftl");
        
        assertGlobMatches("**/c.ftl", "a/b/c.ftl", "c.ftl", "/c.ftl", "///c.ftl");
        assertGlobDoesNotMatch("**/c.ftl", "a/b/x.ftl");

        assertGlobMatches("a/b/**", "a/b/c.ftl", "a/b/c2/c.ftl", "a/b/", "a/b/c/");
        assertGlobDoesNotMatch("a/b.ftl");

        assertGlobMatches("**", "a/b/c.ftl", "");

        assertGlobMatches("\\[\\{\\*\\?\\}\\]\\\\", "[{*?}]\\");
        assertGlobDoesNotMatch("\\[\\{\\*\\?\\}\\]\\\\", "[{xx}]\\");

        assertGlobMatches("a/b/\\?.ftl", "a/b/?.ftl");
        assertGlobDoesNotMatch("a/b/\\?.ftl", "a/b/x.ftl");

        assertGlobMatches("\\?\\?.ftl", "??.ftl");
        assertGlobMatches("\\\\\\\\", "\\\\");
        assertGlobMatches("\\\\\\\\?", "\\\\x");
        assertGlobMatches("x\\", "x");

        assertGlobMatches("???*", "123", "1234", "12345");
        assertGlobDoesNotMatch("???*", "12", "1", "");

        assertGlobMatches("**/a??/b*.ftl", "a11/b1.ftl", "x/a11/b123.ftl", "x/y/a11/b.ftl");
        assertGlobDoesNotMatch("**/a??/b*.ftl", "a1/b1.ftl", "x/a11/c123.ftl");
        
        assertFalse(StringUtil.globToRegularExpression("ab*").matcher("aBc").matches());
        assertTrue(StringUtil.globToRegularExpression("ab*", true).matcher("aBc").matches());
        assertTrue(StringUtil.globToRegularExpression("ab", true).matcher("aB").matches());
        assertTrue(StringUtil.globToRegularExpression("\u00E1b*", true).matcher("\u00C1bc").matches());
        
        try {
            StringUtil.globToRegularExpression("x**/y");
            fail();
        } catch (IllegalArgumentException e) {
            assertThat(e.getMessage(), Matchers.containsString("**"));
        }
        
        try {
            StringUtil.globToRegularExpression("**y");
            fail();
        } catch (IllegalArgumentException e) {
            assertThat(e.getMessage(), Matchers.containsString("**"));
        }
        
        try {
            StringUtil.globToRegularExpression("[ab]c");
            fail();
        } catch (IllegalArgumentException e) {
            assertThat(e.getMessage(), Matchers.containsString("unsupported"));
        }
        
        try {
            StringUtil.globToRegularExpression("{aa,bb}c");
            fail();
        } catch (IllegalArgumentException e) {
            assertThat(e.getMessage(), Matchers.containsString("unsupported"));
        }
    }
    
    private void assertGlobMatches(String glob, String... ss) {
        Pattern pattern = StringUtil.globToRegularExpression(glob);
        for (String s : ss) {
            if (!pattern.matcher(s).matches()) {
                fail("Glob " + glob + " (regexp: " + pattern + ") doesn't match " + s);
            }
        }
    }

    private void assertGlobDoesNotMatch(String glob, String... ss) {
        Pattern pattern = StringUtil.globToRegularExpression(glob);
        for (String s : ss) {
            if (pattern.matcher(s).matches()) {
                fail("Glob " + glob + " (regexp: " + pattern + ") matches " + s);
            }
        }
    }
    
    @Test
    public void testHTMLEnc() {
        String s = "";
        assertSame(s, StringUtil.HTMLEnc(s));
        
        s = "asd";
        assertSame(s, StringUtil.HTMLEnc(s));
        
        assertEquals("a&amp;b&lt;c&gt;d&quot;e'f", StringUtil.HTMLEnc("a&b<c>d\"e'f"));
        assertEquals("&lt;", StringUtil.HTMLEnc("<"));
        assertEquals("&lt;a", StringUtil.HTMLEnc("<a"));
        assertEquals("&lt;a&gt;", StringUtil.HTMLEnc("<a>"));
        assertEquals("a&gt;", StringUtil.HTMLEnc("a>"));
        assertEquals("&lt;&gt;", StringUtil.HTMLEnc("<>"));
        assertEquals("a&lt;&gt;b", StringUtil.HTMLEnc("a<>b"));
    }

    @Test
    public void testXHTMLEnc() throws IOException {
        String s = "";
        assertSame(s, StringUtil.XHTMLEnc(s));
        
        s = "asd";
        assertSame(s, StringUtil.XHTMLEnc(s));
        
        testXHTMLEnc("a&amp;b&lt;c&gt;d&quot;e&#39;f", "a&b<c>d\"e'f");
        testXHTMLEnc("&lt;", "<");
        testXHTMLEnc("&lt;a", "<a");
        testXHTMLEnc("&lt;a&gt;", "<a>");
        testXHTMLEnc("a&gt;", "a>");
        testXHTMLEnc("&lt;&gt;", "<>");
        testXHTMLEnc("a&lt;&gt;b", "a<>b");
    }
    
    private void testXHTMLEnc(String expected, String in) throws IOException {
        assertEquals(expected, StringUtil.XHTMLEnc(in));
        
        StringWriter sw = new StringWriter();
        StringUtil.XHTMLEnc(in, sw);
        assertEquals(expected, sw.toString());
    }

    @Test
    public void testXMLEnc() throws IOException {
        String s = "";
        assertSame(s, StringUtil.XMLEnc(s));
        
        s = "asd";
        assertSame(s, StringUtil.XMLEnc(s));
        
        testXMLEnc("a&amp;b&lt;c&gt;d&quot;e&apos;f", "a&b<c>d\"e'f");
        testXMLEnc("&lt;", "<");
        testXMLEnc("&lt;a", "<a");
        testXMLEnc("&lt;a&gt;", "<a>");
        testXMLEnc("a&gt;", "a>");
        testXMLEnc("&lt;&gt;", "<>");
        testXMLEnc("a&lt;&gt;b", "a<>b");
    }
    
    private void testXMLEnc(String expected, String in) throws IOException {
        assertEquals(expected, StringUtil.XMLEnc(in));
        
        StringWriter sw = new StringWriter();
        StringUtil.XMLEnc(in, sw);
        assertEquals(expected, sw.toString());
    }

    @Test
    public void testXMLEncQAttr() throws IOException {
        String s = "";
        assertSame(s, StringUtil.XMLEncQAttr(s));
        
        s = "asd";
        assertSame(s, StringUtil.XMLEncQAttr(s));
        
        assertEquals("a&amp;b&lt;c>d&quot;e'f", StringUtil.XMLEncQAttr("a&b<c>d\"e'f"));
        assertEquals("&lt;", StringUtil.XMLEncQAttr("<"));
        assertEquals("&lt;a", StringUtil.XMLEncQAttr("<a"));
        assertEquals("&lt;a>", StringUtil.XMLEncQAttr("<a>"));
        assertEquals("a>", StringUtil.XMLEncQAttr("a>"));
        assertEquals("&lt;>", StringUtil.XMLEncQAttr("<>"));
        assertEquals("a&lt;>b", StringUtil.XMLEncQAttr("a<>b"));
    }
    
    @Test
    public void testXMLEncNQG() throws IOException {
        String s = "";
        assertSame(s, StringUtil.XMLEncNQG(s));
        
        s = "asd";
        assertSame(s, StringUtil.XMLEncNQG(s));
        
        assertEquals("a&amp;b&lt;c>d\"e'f", StringUtil.XMLEncNQG("a&b<c>d\"e'f"));
        assertEquals("&lt;", StringUtil.XMLEncNQG("<"));
        assertEquals("&lt;a", StringUtil.XMLEncNQG("<a"));
        assertEquals("&lt;a>", StringUtil.XMLEncNQG("<a>"));
        assertEquals("a>", StringUtil.XMLEncNQG("a>"));
        assertEquals("&lt;>", StringUtil.XMLEncNQG("<>"));
        assertEquals("a&lt;>b", StringUtil.XMLEncNQG("a<>b"));
        
        assertEquals("&gt;", StringUtil.XMLEncNQG(">"));
        assertEquals("]&gt;", StringUtil.XMLEncNQG("]>"));
        assertEquals("]]&gt;", StringUtil.XMLEncNQG("]]>"));
        assertEquals("x]]&gt;", StringUtil.XMLEncNQG("x]]>"));
        assertEquals("x]>", StringUtil.XMLEncNQG("x]>"));
        assertEquals("]x>", StringUtil.XMLEncNQG("]x>"));
    }

    @Test
    public void testRTFEnc() throws IOException {
        String s = "";
        assertSame(s, StringUtil.RTFEnc(s));
        
        s = "asd";
        assertSame(s, StringUtil.RTFEnc(s));
        
        testRTFEnc("a\\{b\\}c\\\\d", "a{b}c\\d");
        testRTFEnc("\\{", "{");
        testRTFEnc("\\{a", "{a");
        testRTFEnc("\\{a\\}", "{a}");
        testRTFEnc("a\\}", "a}");
        testRTFEnc("\\{\\}", "{}");
        testRTFEnc("a\\{\\}b", "a{}b");
    }

    private void testRTFEnc(String expected, String in) throws IOException {
        assertEquals(expected, StringUtil.RTFEnc(in));
        
        StringWriter sw = new StringWriter();
        StringUtil.RTFEnc(in, sw);
        assertEquals(expected, sw.toString());
    }
    
}
