/*
 * 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.jena.sparql.algebra.optimize;

import org.apache.jena.atlas.lib.StrUtils;
import org.junit.Assert;
import org.junit.Test;

import org.apache.jena.sparql.algebra.Op;
import org.apache.jena.sparql.sse.SSE;

/**
 * Tests for the {@link TransformEliminateAssignments}
 *
 */
public class TestTransformEliminateAssignments {

    private void test(String input, String... output) {
        test(input, false, output);
    }

    private void test(String input, boolean aggressive, String... output) {
        Op original = SSE.parseOp(input);
        test(original, aggressive, output);
    }

    private void test(Op original, boolean aggressive, String... output) {
        // Transform
        Op actual = TransformEliminateAssignments.eliminate(original, aggressive);

        // Check results
        if (output == null) {
            // No transformation.
            Assert.assertEquals(original, actual);
        } else {
            // Transformation expected
            Op expected = SSE.parseOp(StrUtils.strjoinNL(output));
            Assert.assertEquals(expected, actual);
        }
    }

    private void testNoChange(String... input) {
        testNoChange(false, input);
    }

    private void testNoChangeAggressive(String... input) {
        testNoChange(true, input);
    }

    private void testNoChange(boolean aggressive, String... input) {
        test(StrUtils.strjoinNL(input), aggressive, (String[]) null);
    }

    @Test
    public void unused_01() {
        // Assignments never used can be eliminated
        // However we must be inside a projection as otherwise the assigned
        // variable would be visible and we couldn't eliminate the assignment
        //@formatter:off
        test(StrUtils.strjoinNL("(project (?y)",
                                "  (extend (?x true)",
                                "    (table unit)))"),
             "(project (?y)",
             "  (table unit))");
        //@formatter:on
    }

    @Test
    public void unused_02() {
        // Assignments never used can be eliminated
        // However we must be inside a projection as otherwise the assigned
        // variable would be visible and we couldn't eliminate the assignment
        //@formatter:off
        test(StrUtils.strjoinNL("(project (?y)",
                                "  (extend ((?x true) (?y false))",
                                "    (table unit)))"),
             "(project (?y)",
             "  (extend (?y false)",
             "    (table unit)))");
        //@formatter:on
    }

    @Test
    public void filter_01() {
        // Assigned variable used only once can substitute expression for the
        // later usage of the variable
        // However we must be inside a projection as otherwise the assigned
        // variable would be visible and we couldn't eliminate the assignment
        //@formatter:off
        test(StrUtils.strjoinNL("(project (?y)",
                                "  (filter (exprlist ?x)",
                                "    (extend (?x true)",
                                "      (table unit))))"),
             "(project (?y)",
             "  (filter (exprlist true)",
             "    (table unit)))");
        //@formatter:on
    }

    @Test
    public void filter_02() {
        // Assignment for ?y can be removed because it is never used
        // Assignment for ?x can be in-lined
        // However we must be inside a projection as otherwise the assigned
        // variable would be visible and we couldn't eliminate the assignment
        //@formatter:off
        test(StrUtils.strjoinNL("(project (?z)",
                                "  (filter (exprlist ?x)",
                                "    (extend ((?x true) (?y false))",
                                "      (table unit))))"),
             "(project (?z)",
             "  (filter (exprlist true)",
             "    (table unit)))");
        //@formatter:on
    }

    @Test
    public void extend_01() {
        // Assigned variable used only once can substitute expression for the
        // later usage of the variable
        // However we must be inside a projection as otherwise the assigned
        // variable would be visible and we couldn't eliminate the assignment
        //@formatter:off
        test(StrUtils.strjoinNL("(project (?y)",
                                "  (extend ((?x true) (?y ?x))",
                                "    (table unit)))"),
             "(project (?y)",
             "  (extend (?y true)",
             "    (table unit)))");
        //@formatter:on
    }

    @Test
    public void extend_02() {
        // Assigned variable used only once can substitute expression for the
        // later usage of the variable
        // However we must be inside a projection as otherwise the assigned
        // variable would be visible and we couldn't eliminate the assignment
        //@formatter:off
        test(StrUtils.strjoinNL("(project (?z)",
                                "  (extend ((?x true) (?y ?x) (?z ?y))",
                                "    (table unit)))"),
             "(project (?z)",
             "  (extend (?z true)",
             "    (table unit)))");
        //@formatter:on
    }

    @Test
    public void extend_03() {
        // Assigned variable used only once can substitute expression for the
        // later usage of the variable
        // However we must be inside a projection as otherwise the assigned
        // variable would be visible and we couldn't eliminate the assignment
        //@formatter:off
        test(StrUtils.strjoinNL("(project (?z)",
                                "  (extend ((?a true) (?b ?a) (?c false) (?d ?c) (?z (|| ?b ?d)))",
                                "    (table unit)))"),
             "(project (?z)",
             "  (extend (?z (|| true false))",
             "    (table unit)))");
        //@formatter:on
    }

    @Test
    public void orderby_01() {
        // Assigned variable used only once can substitute expression for the
        // later usage of the variable
        // However we must be inside a projection as otherwise the assigned
        // variable would be visible and we couldn't eliminate the assignment
        //@formatter:off
        test(StrUtils.strjoinNL("(project (?y)",
                                "  (order (?x)",
                                "    (extend (?x true)",
                                "      (table unit))))"),
             "(project (?y)",
             "  (order (true)",
             "    (table unit)))");
        //@formatter:on
    }

    @Test
    public void orderby_02() {
        // Assigned variable used only once can substitute expression for the
        // later usage of the variable
        // BUT we won't do this by default for complex expressions where they
        // are used in a place where they could be evaluated multiple times
        //@formatter:off
        testNoChange(StrUtils.strjoinNL("(project (?y)",
                                        "  (order (?x)",
                                        "    (extend (?x (contains 'foo' 'bar'))",
                                        "      (table unit))))"));
        //@formatter:on
    }

    @Test
    public void orderby_03() {
        // Assigned variable used only once can substitute expression for the
        // later usage of the variable
        // BUT we won't do this by default for complex expressions where they
        // are used in a place where they could be evaluated multiple times
        // EXCEPT if we are doing aggressive in-lining
        //@formatter:off
        test(StrUtils.strjoinNL("(project (?y)",
                                "  (order (?x)",
                                "    (extend (?x (contains 'foo' 'bar'))",
                                "      (table unit))))"),
             true,
             "(project (?y)",
             "  (order ((contains 'foo' 'bar'))",
             "    (table unit)))");
        //@formatter:on
    }

    @Test
    public void filter_unstable_01() {
        // Assigned variable used only once can substitute expression for the
        // later usage of the variable
        // EXCEPT if the expression is unstable in which case we leave it alone
        //@formatter:off
        testNoChange(StrUtils.strjoinNL("(project (?y)",
                                        "  (filter (exprlist ?x)",
                                        "    (extend (?x (rand))",
                                        "      (table unit))))"));
        //@formatter:on
    }

    @Test
    public void filter_unstable_02() {
        // Assigned variable used only once can substitute expression for the
        // later usage of the variable
        // EXCEPT if the expression is unstable in which case we leave it alone
        //@formatter:off
        testNoChange(StrUtils.strjoinNL("(project (?y)",
                                        "  (filter (exprlist ?x)",
                                        "    (extend (?x (uuid))",
                                        "      (table unit))))"));
        //@formatter:on
    }

    @Test
    public void filter_unstable_03() {
        // Assigned variable used only once can substitute expression for the
        // later usage of the variable
        // EXCEPT if the expression is unstable in which case we leave it alone
        //@formatter:off
        testNoChange(StrUtils.strjoinNL("(project (?y)",
                                        "  (filter (exprlist ?x)",
                                        "    (extend (?x (struuid))",
                                        "      (table unit))))"));
        //@formatter:on
    }

    @Test
    public void filter_unstable_04() {
        // Assigned variable used only once can substitute expression for the
        // later usage of the variable
        // EXCEPT if the expression is unstable in which case we leave it alone
        //@formatter:off
        testNoChange(StrUtils.strjoinNL("(project (?y)",
                                        "  (filter (exprlist ?x)",
                                        "    (extend (?x (bnode))",
                                        "      (table unit))))"));
        //@formatter:on
    }

    @Test
    public void orderby_unstable_01() {
        // Assigned variable used only once can substitute expression for the
        // later usage of the variable
        // EXCEPT if the expression is unstable in which case we leave it alone
        //@formatter:off
        testNoChangeAggressive(StrUtils.strjoinNL("(project (?y)",
                                                  "  (order (?x)",
                                                  "    (extend (?x (rand))",
                                                  "      (table unit))))"));
        //@formatter:on
    }

    @Test
    public void orderby_unstable_02() {
        // Assigned variable used only once can substitute expression for the
        // later usage of the variable
        // EXCEPT if the expression is unstable in which case we leave it alone
        //@formatter:off
        testNoChangeAggressive(StrUtils.strjoinNL("(project (?y)",
                                                  "  (order (?x)",
                                                  "    (extend (?x (uuid))",
                                            "      (table unit))))"));
        //@formatter:on
    }

    @Test
    public void orderby_unstable_03() {
        // Assigned variable used only once can substitute expression for the
        // later usage of the variable
        // EXCEPT if the expression is unstable in which case we leave it alone
        //@formatter:off
        testNoChangeAggressive(StrUtils.strjoinNL("(project (?y)",
                                                  "  (order (?x)",
                                                  "    (extend (?x (struuid))",
                                                  "      (table unit))))"));
        //@formatter:on
    }

    @Test
    public void orderby_unstable_04() {
        // Assigned variable used only once can substitute expression for the
        // later usage of the variable
        // EXCEPT if the expression is unstable in which case we leave it alone
        //@formatter:off
        testNoChangeAggressive(StrUtils.strjoinNL("(project (?y)",
                                                  "  (order (?x)",
                                                  "    (extend (?x (bnode))",
                                                  "      (table unit))))"));
        //@formatter:on
    }

    @Test
    public void ineligible_01() {
        // Cannot eliminate as there is no projection so the assigned variable
        // is visible even though in the algebra given it is used only once
        //@formatter:off
        testNoChange("(filter (exprlist ?x)",
                     "  (extend (?x true)",
                     "    (table unit)))");
        //@formatter:on
    }

    @Test
    public void ineligible_02() {
        // Cannot eliminate as there is no projection so the assigned variable
        // is visible even though in the algebra given it is used only once
        //@formatter:off
        testNoChange("(filter (exprlist ?x)",
                     "  (extend ((?x true) (?y false))",
                     "    (table unit)))");
        //@formatter:on
    }

    @Test
    public void ineligible_03() {
        // As the assigned variable is used multiple times we leave the
        // assignment alone
        //@formatter:off
        testNoChange("(project (?y)",
                     "  (filter (> (* ?x ?x) 16)",
                     "    (extend (?x 3)",
                     "      (table unit))))");
        //@formatter:on
    }

    @Test
    public void ineligible_04() {
        // Because the value of the assignment is used in multiple places we
        // leave the assignment alone
        //@formatter:off
        testNoChange("(project (?y)",
                     "  (filter (exprlist ?x)",
                     "    (join",
                     "      (extend (?x true)",
                     "        (table unit))",
                     "      (bgp (triple ?x ?y ?z)))))");
        //@formatter:on
    }

    @Test
    public void ineligible_05() {
        // Don't inline if we'd move it from within a n-ary operator as doing so
        // may change the queries semantics
        //@formatter:off
        testNoChange("(project (?s)",
                    "  (filter (exprlist ?x)",
                    "    (union",
                    "      (bgp (triple ?s ?p ?o))",
                    "      (extend (?x true)",
                    "        (table unit)))))");
        //@formatter:on
    }

    @Test
    public void ineligible_06() {
        // Don't inline if we'd move it from within a n-ary operator as doing so
        // may change the queries semantics
        //@formatter:off
        testNoChange("(project (?s)",
                    "  (filter (exprlist ?x)",
                    "    (leftjoin",
                    "      (bgp (triple ?s ?p ?o))",
                    "      (extend (?x true)",
                    "        (table unit)))))");
        //@formatter:on
    }

    @Test
    public void ineligible_07() {
        // Don't inline if we'd move it from within a n-ary operator as doing so
        // may change the queries semantics
        //@formatter:off
        testNoChange("(project (?s)",
                    "  (filter (exprlist ?x)",
                    "    (minus",
                    "      (bgp (triple ?s ?p ?o))",
                    "      (extend (?x true)",
                    "        (table unit)))))");
        //@formatter:on
    }

    @Test
    public void ineligible_08() {
        // Don't inline if we'd move it from within a n-ary operator as doing so
        // may change the queries semantics
        // Technically a trivial case like this is probably safe to inline but
        // the reality is that the assignment expression may depend on the
        // behaviour of one branch and so it is safer to not inline
        //@formatter:off
        testNoChange("(project (?s)",
                    "  (filter (exprlist ?x)",
                    "    (join",
                    "      (bgp (triple ?s ?p ?o))",
                    "      (extend (?x true)",
                    "        (table unit)))))");
        //@formatter:on
    }

    @Test
    public void ineligible_09() {
        // We can't inline if the assignment will be projected out
        //@formatter:off
        testNoChange("(project (?x ?y)",
                     "  (filter (exprlist ?x)",
                     "    (extend (?x true)",
                     "      (bgp (triple ?y <urn:ex:pred> <urn:ex:obj>)))))");
        //@formatter:on
    }

    // Test not correct.
    // The "(extend (?x ...))" is not used - it does not cause ?x to come out of (exists).
    // This would be true regardless of the (project) but the current code is only
    // triggered by (project)

    // OLD - wrong (new walker correctly waks into the exist).
//    @Test
//    public void ineligible_10() {
//        // We can't inline out of an EXISTS since the assignment isn't projected
//        // out anyway
//        //@formatter:off
//        testNoChange("(project (?y)",
//                     "  (filter (exprlist ?x (exists",
//                     "                         (extend (?x true)",
//                     "                           (table unit))))",
//                     "    (table unit)))");
//        //@formatter:on
//    }

    // Checks the right output though for the wrong reason.
//    @Test
//    public void eligible_10() {
//        // The "(extend (?x ...)" is not used - it does not com eout of the (exists).
//        // This would be true reagrdless of the (project) but the current code is only
//        // triggered by (project)
//        //@formatter:off
//        test(StrUtils.strjoinNL("(project (?y)",
//                                "  (filter (exprlist ?x (exists",
//                                "                         (extend (?x true)",
//                                "                           (table unit))))",
//                                "    (table unit)))"),
//             "(project (?y)",
//             "  (filter (exprlist ?x (exists",
//             "                         (extend (?x true)",
//             "                           (table unit))))",
//             "    (table unit)))");
//             ;
//        //@formatter:on
//    }
//
//    @Test
//    public void ineligible_11() {
//        // We can't inline out of an EXISTS since the assignment isn't projected
//        // out anyway
//        //@formatter:off
//        testNoChange("(project (?y)",
//                     "  (filter (exprlist ?x)",
//                     "    (filter (exprlist (exists",
//                     "                         (extend (?x true)",
//                     "                           (table unit))))",
//                     "      (table unit))))");
//        //@formatter:on
//    }

    @Test
    public void ineligible_12() {
        // Can't inline through a distinct
        //@formatter:off
        testNoChange("(project (?y)",
                     "  (filter (exprlist ?x)",
                     "    (distinct",
                     "      (extend (?x true)",
                     "        (bgp (triple ?s ?p ?o))))))");
        //@formatter:on
    }

    @Test
    public void ineligible_13() {
        // Can't inline through a reduced
        //@formatter:off
        testNoChange("(project (?y)",
                     "  (filter (exprlist ?x)",
                     "    (reduced",
                     "      (extend (?x true)",
                     "        (bgp (triple ?s ?p ?o))))))");
        //@formatter:on
    }

    @Test
    public void exists_01() {
        // (extend) used in the filter-exists
        //@formatter:off
        testNoChange("(project (?y)",
                     "  (filter (exists",
                     "            (filter  ?x (table unit)))",
                     "    (extend (?x true)",
                     "      (table unit))))") ;
        //@formatter:on
    }

    @Test
    public void exists_02() {
        // We can't inline into an EXISTS since the assignment isn't projected
        // out anyway and its an n-ary operator so would change semantics
        // However this makes the assignment unused so can still remove it
        //@formatter:off
        test(StrUtils.strjoinNL("(project (?y)",
                               "  (filter (exists",
                               "            (filter ?z",
                               "              (table unit)))",
                               "    (extend (?x true)",
                               "      (table unit))))"),
            "(project (?y)",
            "  (filter (exprlist (exists",
            "                      (filter (exprlist ?z)",
            "                        (table unit))))",
            "    (table unit)))");
        //@formatter:on
    }


    @Test
    public void exists_03() {
        // "?x" not used elsewhere.
        //@formatter:off
        test(StrUtils.strjoinNL("(project (?y)",
                                "  (filter (exists ",
                                "            (filter ?x",
                                "              (extend (?x true)",
                                "                 (table unit))))",
                                "    (table unit)))"),
            "(project (?y)",
            "  (filter (exists (filter true (table unit)))",
            "     (table unit)",
            "))") ;

        //@formatter:on
    }

    @Test
    public void exists_04() {
        // Can inline within an exists provided it meets normal conditions of
        // being inside a projection
        //@formatter:off
        test(StrUtils.strjoinNL("(project (?y)",
                               "  (filter (exprlist (exists",
                               "                      (project (?y)",
                               "                        (filter (exprlist ?x)",
                               "                          (extend (?x true)",
                               "                            (table unit))))))",
                               "    (table unit)))"),
            "(project (?y)",
            "  (filter (exprlist (exists",
            "                      (project (?y)",
            "                        (filter (exprlist true)",
            "                          (table unit)))))",
            "    (table unit)))");
        //@formatter:on
    }

    @Test
    public void through_project_01() {
        // We can inline out through a project by also eliminating the variable
        // from the project
        //@formatter:off
        test(StrUtils.strjoinNL("(project (?y)",
                                "  (filter (exprlist ?x)",
                                "    (project (?x)",
                                "      (extend (?x true)",
                                "        (table unit)))))"),
            "(project (?y)",
            "  (filter true",
            "    (project ()",
            "      (table unit))))");
        //@formatter:on
    }

    @Test
    public void through_project_02() {
        // We can inline out through a project by also eliminating the variable
        // from the project
        //@formatter:off
        test(StrUtils.strjoinNL("(project (?y)",
                                "  (filter (exprlist ?x)",
                                "    (project (?x ?y)",
                                "      (extend (?x true)",
                                "        (bgp (triple ?y <urn:pred> <urn:obj>))))))"),
            "(project (?y)",
            "  (filter true",
            "    (project (?y)",
            "      (bgp (triple ?y <urn:pred> <urn:obj>)))))");
        //@formatter:on
    }

    @Test
    public void through_project_03() {
        // We can't inline out through a project if the assignment is not
        // projected
        // However we can still eliminate it from the inner projection if that
        // would render it unused
        //@formatter:off
        test(StrUtils.strjoinNL("(project (?y)",
                                "  (filter (exprlist ?x)",
                                "    (project (?y)",
                                "      (extend (?x true)",
                                "        (table unit)))))"),
             "(project (?y)",
             "  (filter (exprlist ?x)",
             "    (project (?y)",
             "      (table unit))))");
        //@formatter:on
    }

    @Test
    public void through_project_04() {
        // Can't inline because the value is projected out
        //@formatter:off
        testNoChange("(project (?y)",
                     "  (project (?x ?y)",
                     "    (filter (exprlist ?x)",
                     "      (extend (?x true)",
                     "        (bgp (triple ?y <urn:pred> <urn:obj>))))))");
        //@formatter:on
    }

    @Test
    public void no_merge_01() {
        // We should not merge extends
        //@formatter:off
        testNoChange("(project (?x ?y)",
                     "  (filter (exprlist ?x)",
                     "    (extend (?x true)",
                     "      (extend (?y false)",
                     "        (table unit)))))");
        //@formatter:on
    }

    @Test
    public void scope_01() {
        // If the assignment is out of scope by the time it is used in the outer
        // scope then we can't substitute it out there
        // In this test the outer ?x is technically different from the inner ?x
        // anyway because of the projection
        //@formatter:off
        testNoChange(StrUtils.strjoinNL("(filter (exprlist ?x)",
                                        "  (project (?x ?y)",
                                        "    (extend (?x true)",
                                        "      (table unit))))"));
        //@formatter:on
    }

    @Test
    public void scope_02() {
        // If the assignment is out of scope by the time it is used in the outer
        // scope then we can't substitute it out there
        // However if the scoping means the value is never used we can instead
        // eliminate it entirely
        //@formatter:off
        test(StrUtils.strjoinNL("(filter (exprlist ?x)",
                                "  (project (?y)",
                                "    (extend (?x true)",
                                "      (table unit))))"),
            "(filter (exprlist ?x)",
            "  (project (?y)",
            "    (table unit)))");
        //@formatter:on
    }

    @Test
    public void scope_03() {
        // If the assignment is out of scope by the time it is used in the outer
        // scope then we can't substitute it out there
        // However in this case we can substitute it in the inner scope
        //@formatter:off
        test(StrUtils.strjoinNL("(filter (exprlist ?x)",
                                "  (project (?y)",
                                "    (filter (exprlist ?x)",
                                "      (extend (?x true)",
                                "        (table unit)))))"),
            "(filter (exprlist ?x)",
            "  (project (?y)",
            "    (filter (exprlist true)",
            "      (table unit))))");
        //@formatter:on
    }

    @Test
    public void scope_04() {
        // Only the topmost assignment should be inlined since any deeper
        // assignments are technically different variables
        //@formatter:off
        test(StrUtils.strjoinNL("(project (?y)",
                                "  (filter (exprlist ?x)",
                                "    (extend (?x true)",
                                "      (project (?z)",
                                "         (filter (exprlist (|| ?x (! ?x)))",
                                "           (extend (?x false)",
                                "             (table unit)))))))"),
             "(project (?y)",
             "  (filter (exprlist true)",
             "    (project (?z)",
             "      (filter (exprlist (|| ?x (! ?x)))",
             "        (extend (?x false)",
             "          (table unit))))))");
       //@formatter:on
    }

    @Test
    public void scope_05() {
        // Technically invalid query but validates that the inner projection of
        // ?x blocks later in-lining
        //@formatter:off
        testNoChange("(project (?y)",
                     "  (filter (exprlist ?x)",
                     "    (extend (?x true)",
                     "      (project (?x)",
                     "         (filter (exprlist (|| ?x (! ?x)))",
                     "           (extend (?x false)",
                     "             (table unit)))))))");
       //@formatter:off
    }

    @Test
    public void scope_06() {
        // In-lining in the outer scope should not change the inner scope
        //@formatter:off
        test(StrUtils.strjoinNL("(project (?y)",
                                "  (filter (exprlist ?x)",
                                "    (extend (?x true)",
                                "      (project (?z)",
                                "         (project (?x)",
                                "           (filter (exprlist (|| ?x (! ?x)))",
                                "             (extend (?x false)",
                                "               (table unit))))))))"),
            "(project (?y)",
            "  (filter (exprlist true)",
            "    (project (?z)",
            "       (project (?x)",
            "         (filter (exprlist (|| ?x (! ?x)))",
            "           (extend (?x false)",
            "             (table unit)))))))");
       //@formatter:off
    }
}
