/*
 * 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.cassandra.cql3.validation.entities;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Set;

import com.google.common.base.Joiner;
import com.google.common.collect.Iterables;

import org.junit.Before;
import org.junit.Ignore;
import org.junit.Test;

import org.apache.cassandra.cql3.Attributes;
import org.apache.cassandra.cql3.CQLStatement;
import org.apache.cassandra.cql3.CQLTester;
import org.apache.cassandra.cql3.QueryProcessor;
import org.apache.cassandra.cql3.VariableSpecifications;
import org.apache.cassandra.cql3.functions.Function;
import org.apache.cassandra.cql3.statements.BatchStatement;
import org.apache.cassandra.cql3.statements.ModificationStatement;
import org.apache.cassandra.service.ClientState;

import static org.junit.Assert.assertTrue;

/**
 * Checks the collection of Function objects returned by CQLStatement.getFunction
 * matches expectations. This is intended to verify the various subcomponents of
 * the statement (Operations, Terms, Restrictions, RestrictionSet, Selection,
 * Selector, SelectorFactories etc) properly report any constituent functions.
 * Some purely terminal functions are resolved at preparation, so those are not
 * included in the reported list. They still need to be surveyed, to verify the
 * calling client has the necessary permissions. UFAuthTest includes tests which
 * verify this more thoroughly than we can here.
 */
public class UFIdentificationTest extends CQLTester
{
    private com.google.common.base.Function<Function, String> toFunctionNames = new com.google.common.base.Function<Function, String>()
    {
        public String apply(Function f)
        {
            return f.name().keyspace + "." + f.name().name;
        }
    };

    String tFunc;
    String iFunc;
    String lFunc;
    String sFunc;
    String mFunc;
    String uFunc;
    String udtFunc;

    String userType;

    @Before
    public void setup() throws Throwable
    {
        userType = KEYSPACE + "." + createType("CREATE TYPE %s (t text, i int)");

        createTable("CREATE TABLE %s (" +
                    "   key int, " +
                    "   t_sc text STATIC," +
                    "   i_cc int, " +
                    "   t_cc text, " +
                    "   i_val int," +
                    "   l_val list<int>," +
                    "   s_val set<int>," +
                    "   m_val map<int, int>," +
                    "   u_val timeuuid," +
                    "   udt_val frozen<" + userType + ">," +
                    "   PRIMARY KEY (key, i_cc, t_cc)" +
                    ")");

        tFunc = createEchoFunction("text");
        iFunc = createEchoFunction("int");
        lFunc = createEchoFunction("list<int>");
        sFunc = createEchoFunction("set<int>");
        mFunc = createEchoFunction("map<int, int>");
        uFunc = createEchoFunction("timeuuid");
        udtFunc = createEchoFunction(userType);
    }

    @Test
    public void testSimpleModificationStatement() throws Throwable
    {
        assertFunctions(cql("INSERT INTO %s (key, i_cc, t_cc, t_sc) VALUES (0, 0, 'A', %s)", functionCall(tFunc, "'foo'")), tFunc);
        assertFunctions(cql("INSERT INTO %s (key, i_cc, t_cc) VALUES (0, %s, 'A')", functionCall(iFunc, "1")), iFunc);
        assertFunctions(cql("INSERT INTO %s (key, t_cc, i_cc) VALUES (0, %s, 1)", functionCall(tFunc, "'foo'")), tFunc);
        assertFunctions(cql("INSERT INTO %s (key, i_cc, t_cc, i_val) VALUES (0, 0, 'A', %s)", functionCall(iFunc, "1")), iFunc);
        assertFunctions(cql("INSERT INTO %s (key, i_cc, t_cc, l_val) VALUES (0, 0, 'A', %s)", functionCall(lFunc, "[1]")), lFunc);
        assertFunctions(cql("INSERT INTO %s (key, i_cc, t_cc, s_val) VALUES (0, 0, 'A', %s)", functionCall(sFunc, "{1}")), sFunc);
        assertFunctions(cql("INSERT INTO %s (key, i_cc, t_cc, m_val) VALUES (0, 0, 'A', %s)", functionCall(mFunc, "{1:1}")), mFunc);
        assertFunctions(cql("INSERT INTO %s (key, i_cc, t_cc, udt_val) VALUES (0, 0, 'A', %s)", functionCall(udtFunc, "{i : 1, t : 'foo'}")), udtFunc);
        assertFunctions(cql("INSERT INTO %s (key, i_cc, t_cc, u_val) VALUES (0, 0, 'A', %s)", functionCall(uFunc, "now()")), uFunc, "system.now");
    }

    @Test
    public void testNonTerminalCollectionLiterals() throws Throwable
    {
        String iFunc2 = createEchoFunction("int");
        String mapValue = String.format("{%s:%s}", functionCall(iFunc, "1"), functionCall(iFunc2, "1"));
        assertFunctions(cql("INSERT INTO %s (key, i_cc, t_cc, m_val) VALUES (0, 0, 'A', %s)", mapValue), iFunc, iFunc2);

        String listValue = String.format("[%s]", functionCall(iFunc, "1"));
        assertFunctions(cql("INSERT INTO %s (key, i_cc, t_cc, l_val) VALUES (0, 0, 'A',  %s)", listValue), iFunc);

        String setValue = String.format("{%s}", functionCall(iFunc, "1"));
        assertFunctions(cql("INSERT INTO %s (key, i_cc, t_cc, s_val) VALUES (0, 0, 'A', %s)", setValue), iFunc);
    }

    @Test
    public void testNonTerminalUDTLiterals() throws Throwable
    {
        String udtValue = String.format("{ i: %s, t : %s } ", functionCall(iFunc, "1"), functionCall(tFunc, "'foo'"));
        assertFunctions(cql("INSERT INTO %s (key, i_cc, t_cc, udt_val) VALUES (0, 0, 'A', %s)", udtValue), iFunc, tFunc);
    }

    @Test
    public void testModificationStatementWithConditions() throws Throwable
    {
        assertFunctions(cql("UPDATE %s SET i_val=0 WHERE key=0 AND i_cc = 0 AND t_cc = 'A' IF t_sc=%s", functionCall(tFunc, "'foo'")), tFunc);
        assertFunctions(cql("UPDATE %s SET i_val=0 WHERE key=0 AND i_cc = 0 AND t_cc = 'A' IF i_val=%s", functionCall(iFunc, "1")), iFunc);
        assertFunctions(cql("UPDATE %s SET i_val=0 WHERE key=0 AND i_cc = 0 AND t_cc = 'A' IF l_val=%s", functionCall(lFunc, "[1]")), lFunc);
        assertFunctions(cql("UPDATE %s SET i_val=0 WHERE key=0 AND i_cc = 0 AND t_cc = 'A' IF s_val=%s", functionCall(sFunc, "{1}")), sFunc);
        assertFunctions(cql("UPDATE %s SET i_val=0 WHERE key=0 AND i_cc = 0 AND t_cc = 'A' IF m_val=%s", functionCall(mFunc, "{1:1}")), mFunc);


        String iFunc2 = createEchoFunction("int");
        assertFunctions(cql("UPDATE %s SET i_val=0 WHERE key=0 AND i_cc = 0 AND t_cc = 'A' IF i_val IN (%s, %S)",
                            functionCall(iFunc, "1"),
                            functionCall(iFunc2, "2")),
                        iFunc, iFunc2);

        assertFunctions(cql("UPDATE %s SET i_val=0 WHERE key=0 AND i_cc = 0 AND t_cc = 'A' IF u_val=%s",
                            functionCall(uFunc, "now()")),
                        uFunc, "system.now");

        // conditions on collection elements
        assertFunctions(cql("UPDATE %s SET i_val=0 WHERE key=0 AND i_cc = 0 AND t_cc = 'A' IF l_val[%s] = %s",
                            functionCall(iFunc, "1"),
                            functionCall(iFunc2, "1")),
                        iFunc, iFunc2);
        assertFunctions(cql("UPDATE %s SET i_val=0 WHERE key=0 AND i_cc = 0 AND t_cc = 'A' IF m_val[%s] = %s",
                            functionCall(iFunc, "1"),
                            functionCall(iFunc2, "1")),
                        iFunc, iFunc2);
    }

    @Test @Ignore
    // Technically, attributes like timestamp and ttl are Terms so could potentially
    // resolve to function calls (& so you can call getFunctions on them)
    // However, this is currently disallowed by CQL syntax
    public void testModificationStatementWithAttributesFromFunction() throws Throwable
    {
        String longFunc = createEchoFunction("bigint");
        assertFunctions(cql("INSERT INTO %s (key, i_cc, t_cc, i_val) VALUES (0, 0, 'foo', 0) USING TIMESTAMP %s",
                            functionCall(longFunc, "9999")),
                        longFunc);

        assertFunctions(cql("INSERT INTO %s (key, i_cc, t_cc, i_val) VALUES (0, 0, 'foo', 0) USING TTL %s",
                            functionCall(iFunc, "8888")),
                        iFunc);

        assertFunctions(cql("INSERT INTO %s (key, i_cc, t_cc, i_val) VALUES (0, 0, 'foo', 0) USING TIMESTAMP %s AND TTL %s",
                            functionCall(longFunc, "9999"), functionCall(iFunc, "8888")),
                        longFunc, iFunc);
    }

    @Test
    public void testModificationStatementWithNestedFunctions() throws Throwable
    {
        String iFunc2 = createEchoFunction("int");
        String iFunc3 = createEchoFunction("int");
        String iFunc4 = createEchoFunction("int");
        String iFunc5 = createEchoFunction("int");
        String iFunc6 = createEchoFunction("int");
        String nestedFunctionCall = nestedFunctionCall(iFunc6, iFunc5,
                                                       nestedFunctionCall(iFunc4, iFunc3,
                                                                          nestedFunctionCall(iFunc2, iFunc, "1")));

        assertFunctions(cql("DELETE FROM %s WHERE key=%s", nestedFunctionCall),
                        iFunc, iFunc2, iFunc3, iFunc4, iFunc5, iFunc6);
    }

    @Test
    public void testSelectStatementSimpleRestrictions() throws Throwable
    {
        assertFunctions(cql("SELECT i_val FROM %s WHERE key=%s", functionCall(iFunc, "1")), iFunc);
        assertFunctions(cql("SELECT i_val FROM %s WHERE key=0 AND t_sc=%s ALLOW FILTERING", functionCall(tFunc, "'foo'")), tFunc);
        assertFunctions(cql("SELECT i_val FROM %s WHERE key=0 AND i_cc=%s AND t_cc='foo' ALLOW FILTERING", functionCall(iFunc, "1")), iFunc);
        assertFunctions(cql("SELECT i_val FROM %s WHERE key=0 AND i_cc=0 AND t_cc=%s ALLOW FILTERING", functionCall(tFunc, "'foo'")), tFunc);

        String iFunc2 = createEchoFunction("int");
        String tFunc2 = createEchoFunction("text");
        assertFunctions(cql("SELECT i_val FROM %s WHERE key=%s AND t_sc=%s AND i_cc=%s AND t_cc=%s ALLOW FILTERING",
                            functionCall(iFunc, "1"),
                            functionCall(tFunc, "'foo'"),
                            functionCall(iFunc2, "1"),
                            functionCall(tFunc2, "'foo'")),
                        iFunc, tFunc, iFunc2, tFunc2);
    }

    @Test
    public void testSelectStatementRestrictionsWithNestedFunctions() throws Throwable
    {
        String iFunc2 = createEchoFunction("int");
        String iFunc3 = createEchoFunction("int");
        String iFunc4 = createEchoFunction("int");
        String iFunc5 = createEchoFunction("int");
        String iFunc6 = createEchoFunction("int");
        String nestedFunctionCall = nestedFunctionCall(iFunc6, iFunc5,
                                                       nestedFunctionCall(iFunc3, iFunc4,
                                                                          nestedFunctionCall(iFunc, iFunc2, "1")));

        assertFunctions(cql("SELECT i_val FROM %s WHERE key=%s", nestedFunctionCall),
                        iFunc, iFunc2, iFunc3, iFunc4, iFunc5, iFunc6);
    }

    @Test
    public void testNonTerminalTupleInSelectRestrictions() throws Throwable
    {
        assertFunctions(cql("SELECT i_val FROM %s WHERE key=0 AND (i_cc, t_cc) IN ((%s, %s))",
                            functionCall(iFunc, "1"),
                            functionCall(tFunc, "'foo'")),
                        iFunc, tFunc);

        assertFunctions(cql("SELECT i_val FROM %s WHERE key=0 AND (i_cc, t_cc) = (%s, %s)",
                            functionCall(iFunc, "1"),
                            functionCall(tFunc, "'foo'")),
                        iFunc, tFunc);

        assertFunctions(cql("SELECT i_val FROM %s WHERE key=0 AND (i_cc, t_cc) > (%s, %s)",
                            functionCall(iFunc, "1"),
                            functionCall(tFunc, "'foo'")),
                        iFunc, tFunc);

        assertFunctions(cql("SELECT i_val FROM %s WHERE key=0 AND (i_cc, t_cc) < (%s, %s)",
                            functionCall(iFunc, "1"),
                            functionCall(tFunc, "'foo'")),
                        iFunc, tFunc);

         assertFunctions(cql("SELECT i_val FROM %s WHERE key=0 AND (i_cc, t_cc) > (%s, %s) AND (i_cc, t_cc) < (%s, %s)",
                            functionCall(iFunc, "1"),
                            functionCall(tFunc, "'foo'"),
                            functionCall(iFunc, "1"),
                            functionCall(tFunc, "'foo'")),
                         iFunc, tFunc);
    }

    @Test
    public void testNestedFunctionInTokenRestriction() throws Throwable
    {
        String iFunc2 = createEchoFunction("int");
        assertFunctions(cql("SELECT i_val FROM %s WHERE token(key) = token(%s)", functionCall(iFunc, "1")),
                        "system.token", iFunc);
        assertFunctions(cql("SELECT i_val FROM %s WHERE token(key) > token(%s)", functionCall(iFunc, "1")),
                        "system.token", iFunc);
        assertFunctions(cql("SELECT i_val FROM %s WHERE token(key) < token(%s)", functionCall(iFunc, "1")),
                        "system.token", iFunc);
        assertFunctions(cql("SELECT i_val FROM %s WHERE token(key) > token(%s) AND token(key) < token(%s)",
                            functionCall(iFunc, "1"),
                            functionCall(iFunc2, "1")),
                        "system.token", iFunc, iFunc2);
    }

    @Test
    public void testSelectStatementSimpleSelections() throws Throwable
    {
        String iFunc2 = createEchoFunction("int");
        execute("INSERT INTO %s (key, i_cc, t_cc, i_val) VALUES (0, 0, 'foo', 0)");
        assertFunctions(cql2("SELECT i_val, %s FROM %s WHERE key=0", functionCall(iFunc, "i_val")), iFunc);
        assertFunctions(cql2("SELECT i_val, %s FROM %s WHERE key=0", nestedFunctionCall(iFunc, iFunc2, "i_val")), iFunc, iFunc2);
    }

    @Test
    public void testSelectStatementNestedSelections() throws Throwable
    {
        String iFunc2 = createEchoFunction("int");
        execute("INSERT INTO %s (key, i_cc, t_cc, i_val) VALUES (0, 0, 'foo', 0)");
        assertFunctions(cql2("SELECT i_val, %s FROM %s WHERE key=0", functionCall(iFunc, "i_val")), iFunc);
        assertFunctions(cql2("SELECT i_val, %s FROM %s WHERE key=0", nestedFunctionCall(iFunc, iFunc2, "i_val")), iFunc, iFunc2);
    }

    @Test
    public void testBatchStatement() throws Throwable
    {
        String iFunc2 = createEchoFunction("int");
        List<ModificationStatement> statements = new ArrayList<>();
        statements.add(modificationStatement(cql("INSERT INTO %s (key, i_cc, t_cc) VALUES (%s, 0, 'foo')",
                                                 functionCall(iFunc, "0"))));
        statements.add(modificationStatement(cql("INSERT INTO %s (key, i_cc, t_cc) VALUES (1, %s, 'foo')",
                                                 functionCall(iFunc2, "1"))));
        statements.add(modificationStatement(cql("INSERT INTO %s (key, i_cc, t_cc) VALUES (2, 2, %s)",
                                                 functionCall(tFunc, "'foo'"))));

        BatchStatement batch = new BatchStatement(BatchStatement.Type.LOGGED, VariableSpecifications.empty(), statements, Attributes.none());
        assertFunctions(batch, iFunc, iFunc2, tFunc);
    }

    @Test
    public void testBatchStatementWithConditions() throws Throwable
    {
        List<ModificationStatement> statements = new ArrayList<>();
        statements.add(modificationStatement(cql("UPDATE %s SET i_val = %s WHERE key=0 AND i_cc=0 and t_cc='foo' IF l_val = %s",
                                                 functionCall(iFunc, "0"), functionCall(lFunc, "[1]"))));
        statements.add(modificationStatement(cql("UPDATE %s SET i_val = %s WHERE key=0 AND i_cc=1 and t_cc='foo' IF s_val = %s",
                                                 functionCall(iFunc, "0"), functionCall(sFunc, "{1}"))));

        BatchStatement batch = new BatchStatement(BatchStatement.Type.LOGGED, VariableSpecifications.empty(), statements, Attributes.none());
        assertFunctions(batch, iFunc, lFunc, sFunc);
    }

    private ModificationStatement modificationStatement(String cql)
    {
        return (ModificationStatement) QueryProcessor.getStatement(cql, ClientState.forInternalCalls());
    }

    private void assertFunctions(String cql, String... function)
    {
        CQLStatement stmt = QueryProcessor.getStatement(cql, ClientState.forInternalCalls());
        assertFunctions(stmt, function);
    }

    private void assertFunctions(CQLStatement stmt, String... function)
    {
        Set<String> expected = com.google.common.collect.Sets.newHashSet(function);
        Set<String> actual = com.google.common.collect.Sets.newHashSet(Iterables.transform(stmt.getFunctions(),
                                                                                           toFunctionNames));
        assertTrue(com.google.common.collect.Sets.symmetricDifference(expected, actual).isEmpty());
    }

    private String cql(String template, String... params)
    {
        String tableName = KEYSPACE + "." + currentTable();
        return String.format(template, com.google.common.collect.Lists.asList(tableName, params).toArray());
    }

    // Alternative query builder - appends the table name to the supplied params,
    // for stmts of the form "SELECT x, %s FROM %s WHERE y=0"
    private String cql2(String template, String... params)
    {
        Object[] args = Arrays.copyOf(params, params.length + 1);
        args[params.length] = KEYSPACE + "." + currentTable();
        return String.format(template, args);
    }

    private String functionCall(String fName, String... args)
    {
        return String.format("%s(%s)", fName, Joiner.on(",").join(args));
    }

    private String nestedFunctionCall(String outer, String inner, String innerArgs)
    {
        return functionCall(outer, functionCall(inner, innerArgs));
    }

    private String createEchoFunction(String type) throws Throwable
    {
        return createFunction(KEYSPACE, type,
           "CREATE FUNCTION %s(input " + type + ")" +
           " CALLED ON NULL INPUT" +
           " RETURNS " + type +
           " LANGUAGE java" +
           " AS ' return input;'");
    }
}
