/*
 * 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 org.apache.felix.gogo.runtime;

import java.io.ByteArrayInputStream;
import java.io.File;
import java.io.PrintStream;
import java.net.URI;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;

import org.apache.felix.gogo.runtime.threadio.ThreadIOImpl;
import org.junit.Before;
import org.junit.Test;
import org.osgi.framework.Bundle;
import org.osgi.framework.BundleContext;

import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotSame;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertSame;
import static org.junit.Assert.fail;
import static org.mockito.Matchers.eq;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;

public class TestTokenizer
{
    private final Map<String, Object> vars = new HashMap<>();
    private final Evaluate evaluate;
    private Path currentDir = null;

    public TestTokenizer()
    {
        evaluate = new TestEvaluate()
        {
            public Object eval(Token t) {
                throw new UnsupportedOperationException("eval not implemented.");
            }

            public Object get(String key)
            {
                return vars.get(key);
            }

            public Object put(String key, Object value)
            {
                return vars.put(key, value);
            }

            public Object expr(Token t) {
                throw new UnsupportedOperationException("expr not implemented.");
            }

            public Path currentDir() {
                return currentDir;
            }
        };
    }

    @Before
    public void setUp() {
        currentDir = null;
        vars.clear();
    }

    @Test
    public void testHello() throws Exception
    {
        testHello("hello world\n");
        testHello("hello world\n\n"); // multiple \n reduced to single Token.NL
        testHello("hello world\r\n"); // \r\n -> \n

        // escapes

        testHello("hello \\\nworld\n");
        try
        {
            testHello("hello\\u20world\n");
            fail("bad unicode accepted");
        }
        catch (SyntaxError e)
        {
            // expected
        }

        // whitespace and comments

        testHello(" hello  world    \n ");
        testHello("hello world // comment\n\n");
        testHello("hello world #\\ comment\n\n");
        testHello("// comment\nhello world\n");
        testHello("// comment ?\\ \nhello world\n");
        testHello("hello /*\n * comment\n */ world\n");
    }

    // hello world
    private void testHello(CharSequence text) {
        Tokenizer t = new Tokenizer(text);
        assertEquals("hello", t.next().toString());
        assertEquals("world", t.next().toString());
        assertEquals("\n", t.next().toString());
        assertNull(t.next());
    }
    
    @Test
    public void testString() throws Exception
    {
        testString("'single $quote' \"double $quote\"\n");
    }

    // 'single quote' "double quote"
    private void testString(CharSequence text) {
        Tokenizer t = new Tokenizer(text);
        assertEquals("'single $quote'", t.next().toString());
        assertEquals("\"double $quote\"", t.next().toString());
        assertEquals("\n", t.next().toString());
        assertNull(t.next());
    }

    @Test
    public void testClosure() throws Exception
    {
        testClosure2("x = { echo '}' $args //comment's\n}\n");
        testClosure2("x={ echo '}' $args //comment's\n}\n");
        token1("{ echo \\{ $args \n}");
        token1("{ echo \\} $args \n}");
    }

    //
    // x = {echo $args};
    //
    private void testClosure2(CharSequence text) {
        Tokenizer t = new Tokenizer(text);
        assertEquals("x", t.next().toString());
        assertEquals("=", t.next().toString());
        assertEquals("{", t.next().toString());
        assertEquals("echo", t.next().toString());
        assertEquals("'}'", t.next().toString());
        assertEquals("$args", t.next().toString());
        assertEquals("\n", t.next().toString());
        assertEquals("}", t.next().toString());
        assertEquals("\n", t.next().toString());
        assertEquals(null, t.next());
    }

    private void token1(CharSequence text) {
        Tokenizer t = new Tokenizer(text);
        assertEquals("{", t.next().toString());
        assertEquals("echo", t.next().toString());
        t.next();
        assertEquals("$args", t.next().toString());
        assertEquals("\n", t.next().toString());
        assertEquals("}", t.next().toString());
        assertNull(t.next());
    }

    @Test
    public void testJavaArray() throws Exception
    {
        vars.put("a", new Object[] { "b", "c"});

        assertEquals(Arrays.asList("B", "C"), expand("${(U)a}"));
    }

    @Test
    public void testRawVariable() throws Exception
    {
        vars.put("a1", Arrays.asList("a", 1));

        assertSame(vars.get("a1"), expand("$a1"));
        assertNotSame(vars.get("a1"), expand("${a1}"));
        assertEquals(vars.get("a1"), expand("${a1}"));
    }

    @Test
    public void testSubscripts() throws Exception
    {
        Map<String, String> map = new LinkedHashMap<>();
        map.put("a1", "baz");
        map.put("a2", "bar");
        map.put("b1", "foo");

        vars.put("key", "a1");
        vars.put("map", map);

        assertEquals("baz", expand("${map[a1]}"));
        assertEquals("baz", expand("${map[$key]}"));
        assertEquals("az", expand("${map[a1][1,-0]}"));
        assertEquals("AZ", expand("${(U)map[a1][1,3]}"));
        assertEquals(map, expand("${map}"));
        assertEquals("baz bar foo", expand("\"${map}\""));
        assertEquals("baz bar foo", expand("\"${map[*]}\""));
        assertEquals(Arrays.asList("baz", "bar", "foo"), expand("\"${map[@]}\""));
        assertEquals(Arrays.asList("a1", "a2", "b1"), expand("\"${(k)map[@]}\""));
        assertEquals(Arrays.asList("a1", "baz", "a2", "bar", "b1", "foo"), expand("\"${(kv)map[@]}\""));
        assertEquals(Arrays.asList("a2", "bar"), expand("${${(kv)map[@]}[2,4]}"));
        assertEquals(Arrays.asList("a2", "bar"), expand("${${(kv)=map}[2,4]}"));

        // TODO: test subscripts on array resulting in a single element
    }

    @Test
    public void testBraces() throws Exception {
        assertEquals(Arrays.asList("1", "3"), expand("{1..3..2}"));
        assertEquals(Arrays.asList("3", "1"), expand("{1..3..-2}"));
        assertEquals(Arrays.asList("1", "3"), expand("{3..1..-2}"));
        assertEquals(Arrays.asList("3", "1"), expand("{3..1..2}"));

        assertEquals(Arrays.asList("1", "2", "3"), expand("{1..3}"));
        assertEquals(Arrays.asList("a1b", "a2b", "a3b"), expand("a{1..3}b"));

        assertEquals(Arrays.asList("a1b", "a2b", "a3b"), expand("a{1,2,3}b"));

        assertEquals(Arrays.asList("a1b", "a2b", "a3b"), expand("a{1,2,3}b"));
        assertEquals(Arrays.asList("a1b", "a,b", "a3b"), expand("a{1,',',3}b"));

        currentDir = Paths.get(".");
        try {
            expand("a{1,*,3}b");
            fail("Expected exception");
        } catch (Exception e) {
            assertEquals("no matches found: a*b", e.getMessage());
        } finally {
            currentDir = null;
        }

        vars.put("a", "1");
        assertEquals(Arrays.asList("a1b", "a\nb", "a3b"), expand("a{$a,$'\\n',3}b"));
        assertEquals(Arrays.asList("ab1*z", "ab2*z", "arz"), expand("a{b{1..2}'*',r}z"));

    }

    @Test
    public void testJoinSplit() throws Exception {
        vars.put("array", Arrays.asList("a", "b", "c"));
        vars.put("string", "a\n\nb\nc");
        vars.put("str", "such a bad bad trip");

        assertEquals("a:b:c", expand("${(j.:.)array}"));
        assertEquals("a\nb\nc", expand("${(pj.\\n.)array}"));
        assertEquals("a\nb\nc", expand("${(F)array}"));

        assertEquals(Arrays.asList("a", "b", "c"), expand("${(f)string}"));
        assertEquals(Arrays.asList("a\n\n", "\nc"), expand("${(s:b:)string}"));

        assertEquals("a:b:c", expand("${(Fj':')array}"));

        assertEquals("a bad such trip", expand("${(j' ')${(s' 'uo)str}}"));
    }

    @Test
    public void testParamFlag() throws Exception {
        vars.put("foo", "bar");
        vars.put("bar", "baz");

        assertEquals("bar", expand("${${foo}}"));
        assertEquals("baz", expand("${(P)foo}"));
        assertEquals("baz", expand("${(P)${foo}}"));
    }

    @Test
    public void testCaseFlags() throws Exception {
        vars.put("foo", "bAr");

        assertEquals("bAr", expand("${foo}"));
        assertEquals("bar", expand("${(L)foo}"));
        assertEquals("BAR", expand("${(U)${foo}}"));
        assertEquals("Bar", expand("${(C)${foo}}"));
    }

    @Test
    public void testQuotes() throws Exception {
        vars.put("foo", "\"{a'}\\b");
        vars.put("q1", "\"foo\"");

        assertEquals("\\\"\\{a\\'\\}\\\\b", expand("${(q)foo}"));
        assertEquals("'\"{a'\\''}\\b'", expand("${(qq)foo}"));
        assertEquals("\"\\\"{a'}\\\\b\"", expand("${(qqq)foo}"));
        assertEquals("$'\"{a\\'}\\b'", expand("${(qqqq)foo}"));
        assertEquals("'\"{a'\\''}\\b'", expand("${(q-)foo}"));

        assertEquals("foo", expand("${(Q)q1}"));
    }

    @Test
    public void testChars() throws Exception {
        List<Integer> array = new ArrayList<>();
        for (int i = 0; i < 64; i++) {
            array.add(i);
        }
        vars.put("array", array);

        assertEquals(Arrays.asList("^@", "^A", "^B", "^C", "^D", "^E", "^F", "^G",
                "^H", "^I", "^J", "^K", "^L", "^M", "^N", "^O",
                "^P", "^Q", "^R", "^S", "^T", "^U", "^V", "^W",
                "^X", "^Y", "^Z", "^\\[", "^\\\\", "^\\]", "^^", "^_",
                "\\ ", "\\!", "\\\"", "\\#", "\\$", "\\%", "\\&", "\\'",
                "\\(", "\\)", "\\*", "+", ",", "-", ".", "/",
                "0", "1", "2", "3", "4", "5", "6", "7",
                "8", "9", ":", "\\;", "\\<", "\\=", "\\>", "\\?"), expand("${(qV#)array}"));
    }

    @Test
    public void testSorting() throws Exception {
        vars.put("array", Arrays.asList("foo1", "foo02", "foo2", "fOo3", "Foo20", "foo23"));

        assertEquals(Arrays.asList("Foo20", "fOo3", "foo02", "foo1", "foo2", "foo23"), expand("${(o)array}"));
        assertEquals(Arrays.asList("foo23", "foo2", "foo1", "foo02", "fOo3", "Foo20"), expand("${(O)array}"));
        assertEquals(Arrays.asList("foo02", "foo1", "foo2", "Foo20", "foo23", "fOo3"), expand("${(oi)array}"));
        assertEquals(Arrays.asList("fOo3", "foo23", "Foo20", "foo2", "foo1", "foo02"), expand("${(Oi)array}"));
        assertEquals(Arrays.asList("foo1", "foo02", "foo2", "fOo3", "Foo20", "foo23"), expand("${(oa)array}"));
        assertEquals(Arrays.asList("foo23", "Foo20", "fOo3", "foo2", "foo02", "foo1"), expand("${(Oa)array}"));
        assertEquals(Arrays.asList("foo1", "foo02", "foo2", "fOo3", "Foo20", "foo23"), expand("${(oia)array}"));
        assertEquals(Arrays.asList("foo23", "Foo20", "fOo3", "foo2", "foo02", "foo1"), expand("${(Oia)array}"));
        assertEquals(Arrays.asList("Foo20", "fOo3", "foo1", "foo02", "foo2", "foo23"), expand("${(on)array}"));
        assertEquals(Arrays.asList("foo23", "foo2", "foo02", "foo1", "fOo3", "Foo20"), expand("${(On)array}"));
        assertEquals(Arrays.asList("foo1", "foo02", "foo2", "fOo3", "Foo20", "foo23"), expand("${(oin)array}"));
        assertEquals(Arrays.asList("foo23", "Foo20", "fOo3", "foo2", "foo02", "foo1"), expand("${(Oin)array}"));
    }

    @Test
    public void testPatterns() throws Exception {
        vars.put("foo", "twinkle twinkle little star");
        vars.put("sub", "t*e");
        vars.put("sb", "*e");
        vars.put("rep", "spy");

        assertEquals("spynkle spynkle little star", expand("${(G)foo/'twi'/$rep}"));

        assertEquals("twinkle twinkle little star", expand("${foo/${sub}/${rep}}"));
        assertEquals("spy twinkle little star", expand("${foo/${~sub}/${rep}}"));
        assertEquals("spy star", expand("${foo//${~sub}/${rep}}"));
        assertEquals("spy spy lispy star", expand("${(G)foo/${~sub}/${rep}}"));
        assertEquals("spy star", expand("${(G)foo//${~sub}/${rep}}"));
        assertEquals("spy twinkle little star", expand("${foo/t${~sb}/${rep}}"));
    }

    @Test
    public void testExpand() throws Exception
    {
        final URI home = new URI("/home/derek");
        final File pwd = new File("/tmp");
        final String user = "derek";

        vars.put("HOME", home);
        vars.put("PWD", pwd);
        vars.put("USER", user);
        vars.put(user, "Derek Baum");

        // quote removal
        assertEquals("hello", expand("hello").toString());
        assertEquals("hello", expand("'hello'"));
        assertEquals("\"hello\"", expand("'\"hello\"'"));
        assertEquals("hello", expand("\"hello\""));
        assertEquals("'hello'", expand("\"'hello'\""));

        // escapes
        assertEquals("hello\\w", expand("hello\\\\w"));
        assertEquals("hellow", expand("hello\\w"));
        assertEquals("hello\\w", expand("\"hello\\\\w\""));
        assertEquals("hello\\w", expand("\"hello\\w\""));
        assertEquals("hello\\\\w", expand("'hello\\\\w'"));
//CHANGE        assertEquals("hello", expand("he\\\nllo"));
        assertEquals("he\\llo", expand("'he\\llo'"));
        assertEquals("he'llo", expand("'he'\\''llo'"));
        assertEquals("he\"llo", expand("\"he\\\"llo\""));
        assertEquals("he'llo", expand("he\\'llo"));
        assertEquals("he$llo", expand("\"he\\$llo\""));
        assertEquals("he\\'llo", expand("\"he\\'llo\""));
        assertEquals("hello\\w", expand("\"hello\\w\""));

        // unicode

        // Note: we could use literal Unicode pound '£' instead of \u00a3 in next test.
        // if above is not UK currency symbol, then your locale is not configured for UTF-8.
        // Java on Macs cannot handle UTF-8 unless you explicitly set '-Dfile.encoding=UTF-8'.
        assertEquals("pound\u00a3cent\u00a2", expand("$'pound\\u00a3cent\\u00a2'"));
        assertEquals("euro\\u20ac", expand("$'euro\\\\u20ac'"));
        assertEquals("euro\u20ac", expand("$'euro\\u20ac'"));
        assertEquals("euro\u020a", expand("$'euro\\u20a'"));
        assertEquals("euro\u020ag", expand("$'euro\\u20ag'"));

        // simple variable expansion - quoting or concatenation converts result to String
        assertEquals(user, expand("$USER"));
        assertEquals(home, expand("$HOME"));
        assertEquals(home.toString(), expand("$HOME$W"));
        assertEquals(pwd, expand("$PWD"));
        assertEquals("$PWD", expand("'$PWD'"));
        assertEquals("$PWD", expand("\\$PWD"));
        assertEquals(pwd.toString(), expand("\"$PWD\""));
        assertEquals("W" + pwd, expand("W$PWD"));
        assertEquals(pwd + user, expand("$PWD$USER"));

        // variable substitution  ${NAME:-WORD} etc
        assertNull(expand("$JAVA_HOME"));
        assertEquals(user, expand("${USER}"));
        assertEquals(user + "W", expand("${USER}W"));
        assertEquals("java_home", expand("${JAVA_HOME:-java_home}"));
        assertEquals(pwd, expand("${NOTSET:-$PWD}"));
        assertNull(vars.get("JAVA_HOME"));
        assertEquals("java_home", expand("${JAVA_HOME:=java_home}"));
        assertEquals("java_home", vars.get("JAVA_HOME"));
        assertEquals("java_home", expand("$JAVA_HOME"));
        assertEquals("yes", expand("${JAVA_HOME:+yes}"));
        assertNull(expand("${NOTSET:+yes}"));
        assertEquals("", expand("\"${NOTSET:+yes}\""));
        try
        {
            expand("${NOTSET:?}");
            fail("expected 'not set' exception");
        }
        catch (IllegalArgumentException e)
        {
            // expected
        }

        // bad variable names
        assertEquals("$ W", expand("$ W"));
        assertEquals("$ {W}", expand("$ {W}"));
        try
        {
            expand("${W }");
            fail("expected syntax error");
        }
        catch (SyntaxError e)
        {
            // expected
        }

        try {
            expand("${USER\\\n:?}");
        }
        catch (SyntaxError e)
        {
            // expected
        }
//CHANGE        assertEquals(user, expand("${US\\u0045R:?}"));

        // bash doesn't supported nested expansions
        // gogo only supports them in the ${} syntax
        assertEquals("Derek Baum", expand("${(P)$USER}"));
        assertEquals("Derek Baum", expand("${${(P)USER}:-Derek Baum}"));
        assertEquals("Derek Baum", expand("${${(P)USR}:-$derek}"));
        assertEquals("derek", expand("${${USER}}"));
        assertEquals("derek", expand("${${USER:-d}}"));
        assertEquals("x", expand("${$USR:-x}"));
        assertEquals("$" + user, expand("$$USER"));
    }

    private Object expand(CharSequence word) throws Exception
    {
        return Expander.expand(word, evaluate);
    }

    @Test
    public void testParser() {
        new Parser("// comment\n" + "a=\"who's there?\"; ps -ef;\n" + "ls | \n grep y\n").program();
        String p1 = "a=1 \\$b=2 c={closure}\n";
        new Parser(p1).program();
        new Parser("[" + p1 + "]").program();
    }

    //
    // FELIX-4679 / FELIX-4671.
    //
    @Test
    public void testScriptFelix4679() throws Exception
    {
        String script = "addcommand system (((${.context} bundles) 0) loadclass java.lang.System)";

        PrintStream sout = new PrintStream(System.out) {
            @Override
            public void close() {
            }
        };
        PrintStream serr = new PrintStream(System.err) {
            @Override
            public void close() {
            }
        };
        ThreadIOImpl tio = new ThreadIOImpl();
        tio.start();

        try
        {
            BundleContext bc = createMockContext();

            CommandProcessorImpl processor = new CommandProcessorImpl(tio);
            processor.addCommand("gogo", processor, "addcommand");
            processor.addConstant(".context", bc);

            CommandSessionImpl session = new CommandSessionImpl(processor, new ByteArrayInputStream(script.getBytes()), sout, serr);

            Closure c = new Closure(session, null, script);
            assertNull(c.execute(session, null));
        }
        finally
        {
            tio.stop();
        }
    }

    @Test
    public void testFelix5541() {
        Tokenizer t = new Tokenizer("<");
        assertEquals("<", t.next().toString());
        assertNull(t.next());
    }

    private BundleContext createMockContext() throws ClassNotFoundException
    {
        Bundle systemBundle = mock(Bundle.class);
        when(systemBundle.loadClass(eq("java.lang.System"))).thenReturn((Class) System.class);

        BundleContext bc = mock(BundleContext.class);
        when(bc.getBundles()).thenReturn(new Bundle[] { systemBundle });
        return bc;
    }

    @Test
    public void testHereDocMissing() {
        try {
            new Parser("a <<").statement();
            fail("Expected exception");
        } catch (EOFError e) {
            assertEquals("foo\n", e.repair());
        }
        try {
            new Parser("a << foo\n").statement();
            fail("Expected exception");
        } catch (EOFError e) {
            assertEquals("\nfoo\n", e.repair());
        }
        new Parser("a << foo\n \nfoo\n").statement();
    }
}
