/*
 * 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.sis.filter;

import java.util.Set;
import java.util.List;
import java.util.Collection;
import java.util.function.Function;
import java.util.function.BiFunction;
import org.apache.sis.feature.builder.FeatureTypeBuilder;
import org.apache.sis.math.FunctionProperty;

// Test dependencies
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.*;
import org.apache.sis.test.TestCase;
import static org.apache.sis.test.Assertions.assertSerializedEquals;

// Specific to the main branch:
import org.apache.sis.feature.AbstractFeature;
import org.apache.sis.feature.DefaultFeatureType;
import org.apache.sis.pending.geoapi.filter.LogicalOperator;


/**
 * Tests {@link LogicalFilter} implementations.
 *
 * @author  Johann Sorel (Geomatys)
 * @author  Martin Desruisseaux (Geomatys)
 */
public final class LogicalFilterTest extends TestCase {
    /**
     * The factory to use for creating the objects to test.
     */
    private final DefaultFilterFactory<AbstractFeature,Object,?> factory;

    /**
     * Creates a new test case.
     */
    public LogicalFilterTest() {
        factory = DefaultFilterFactory.forFeatures();
    }

    /**
     * Tests creation of "And" expression from the factory.
     * Also tests an evaluation from literal values and serialization.
     */
    @Test
    public void testAnd() {
        create(factory::and, factory::and, false);
    }

    /**
     * Tests creation of "Or" expression from the factory.
     * Also tests an evaluation from literal values and serialization.
     */
    @Test
    public void testOr() {
        create(factory::or, factory::or, true);
    }

    /**
     * Tests creation of "Not" expression from the factory.
     * Also tests an evaluation from literal values and serialization.
     */
    @Test
    public void testNot() {
        final var literal = factory.literal("text");
        final var operand = factory.isNull(literal);
        final var filter  = (LogicalOperator<AbstractFeature>) factory.not(operand);
        assertArrayEquals(new Filter<?>[] {operand}, filter.getOperands().toArray());
        assertTrue(filter.test(null));
        assertSerializedEquals(filter);
        assertFalse(isVolatile(filter));
    }

    /**
     * Verifies that {@link Filter#negate()} is overridden.
     */
    @Test
    public void testNegate() {
        final var literal = factory.literal("text");
        final var operand = factory.isNull(literal);
        assertInstanceOf(LogicalFilter.Not.class, operand.negate());
    }

    /**
     * Tests a filter having a volatile expression.
     */
    @Test
    public void testVolatile() {
        final var literal = new LeafExpression.Literal<AbstractFeature,Object>("test") {
            @Override public Set<FunctionProperty> properties() {
                return Set.of(FunctionProperty.VOLATILE);
            }
        };
        final var operand = factory.isNull(literal);
        final var filter  = factory.not(operand);
        assertTrue(isVolatile(filter));
    }

    /**
     * Implementation of {@link #testAnd()} and {@link #testOr()}.
     *
     * @param  binary    the function creating a logical operator from two operands.
     * @param  anyArity  the function creating a logical operator from an arbitrary number of operands.
     * @param  expected  expected evaluation result.
     */
    private void create(final BiFunction<Filter<AbstractFeature>, Filter<AbstractFeature>, Filter<AbstractFeature>> binary,
                        final Function<Collection<Filter<AbstractFeature>>, Filter<AbstractFeature>> anyArity,
                        final boolean expected)
    {
        final var f1 = factory.isNull(factory.literal("text"));
        final var f2 = factory.isNull(factory.literal(null));
        RuntimeException e;

        e = assertThrows(NullPointerException.class, () -> binary.apply(null, null),
                         "Creation with a null operand shall raise an exception.");
        assertNotNull(e);

        e = assertThrows(NullPointerException.class, () -> binary.apply(f1, null),
                         "Creation with a null operand shall raise an exception.");
        assertNotNull(e);

        e = assertThrows(NullPointerException.class, () -> binary.apply(null, f2),
                         "Creation with a null operand shall raise an exception.");
        assertNotNull(e);

        e = assertThrows(IllegalArgumentException.class, () -> anyArity.apply(Set.of(f1)),
                         "Creation with less then two operands shall raise an exception.");
        assertNotNull(e);
        /*
         * Test construction, evaluation and serialization.
         */
        var filter = (LogicalOperator<AbstractFeature>) binary.apply(f1, f2);
        assertArrayEquals(new Filter<?>[] {f1, f2}, filter.getOperands().toArray());
        assertEquals(expected, filter.test(null));
        assertSerializedEquals(filter);
        assertFalse(isVolatile(filter));
        /*
         * Same test, using the constructor accepting any number of operands.
         */
        filter = (LogicalOperator<AbstractFeature>) anyArity.apply(List.of(f1, f2, f1));
        assertArrayEquals(new Filter<?>[] {f1, f2, f1}, filter.getOperands().toArray());
        assertEquals(expected, filter.test(null));
        assertSerializedEquals(filter);
        assertFalse(isVolatile(filter));
        /*
         * Test the `Predicate` methods, which should be overridden by `Optimization.OnFilter`.
         */
        assertInstanceOf(Optimization.OnFilter.class, f1.and(f2));
        assertInstanceOf(Optimization.OnFilter.class, f1.or(f2));
        assertInstanceOf(Optimization.OnFilter.class, f1.negate());
    }

    /**
     * Tests evaluation using a feature instance.
     */
    @Test
    public void testEvaluateOnFeature() {
        final var builder = new FeatureTypeBuilder();
        builder.addAttribute(String.class).setName("attNull");
        builder.addAttribute(String.class).setName("attNotNull");
        final var feature = builder.setName("Test").build().newInstance();
        feature.setPropertyValue("attNotNull", "a value");

        final var filterTrue  = factory.isNull(factory.property("attNull",    String.class));
        final var filterFalse = factory.isNull(factory.property("attNotNull", String.class));

        assertTrue (factory.and(filterTrue,  filterTrue ).test(feature));
        assertFalse(factory.and(filterFalse, filterTrue ).test(feature));
        assertFalse(factory.and(filterTrue,  filterFalse).test(feature));
        assertFalse(factory.and(filterFalse, filterFalse).test(feature));

        assertTrue (factory.or (filterTrue,  filterTrue ).test(feature));
        assertTrue (factory.or (filterFalse, filterTrue ).test(feature));
        assertTrue (factory.or (filterTrue,  filterFalse).test(feature));
        assertFalse(factory.or (filterFalse, filterFalse).test(feature));

        assertFalse(factory.not(filterTrue ).test(feature));
        assertTrue (factory.not(filterFalse).test(feature));
        /*
         * Test the `Predicate` methods, which should be overridden by `Optimization.OnFilter`.
         */
        var predicate = filterTrue.and(filterFalse);
        assertInstanceOf(Optimization.OnFilter.class, predicate);
        assertFalse(predicate.test(feature));

        predicate = filterTrue.or(filterFalse);
        assertInstanceOf(Optimization.OnFilter.class, predicate);
        assertTrue(predicate.test(feature));
    }

    /**
     * Tests {@link Optimization} applied on logical filters.
     */
    @Test
    public void testOptimization() {
        final var f1 = factory.isNull(factory.literal("text"));     // False
        final var f2 = factory.isNull(factory.literal(null));       // True
        final var f3 = factory.isNull(factory.property("*"));       // Indeterminate
        optimize(factory.and(f1, f2), Filter.exclude());
        optimize(factory.or (f1, f2), Filter.include());
        optimize(factory.and(f3, factory.not(f3)), Filter.exclude());
        optimize(factory.or (f3, factory.not(f3)), Filter.include());
    }

    /**
     * Verifies an optimization which is expected to evaluate immediately.
     */
    private static void optimize(final Filter<AbstractFeature> original, final Filter<AbstractFeature> expected) {
        final Filter<?> optimized = new Optimization().apply(original);
        assertNotSame(original, optimized, "Expected a new optimized filter.");
        assertSame(optimized, new Optimization().apply(optimized), "Second optimization should have no effect.");
        assertSame(expected, optimized, "Expression should have been evaluated now.");
    }

    /**
     * Tests {@link Optimization} applied on logical filters when the {@code FeatureType} is known.
     */
    @Test
    public void testFeatureOptimization() {
        final String attribute = "population";
        final var builder = new FeatureTypeBuilder();
        builder.addAttribute(String.class).setName(attribute);
        final var feature  = builder.setName("Test").build();
        final var instance = feature.newInstance();
        instance.setPropertyValue("population", "1000");
        /*
         * Prepare an expression which divide the population value by 5.
         */
        final var expression = factory.divide(factory.property(attribute, Integer.class), factory.literal(5));
        final Optimization optimization = new Optimization();
        assertSame(expression, optimization.apply(expression));                       // No optimization.
        assertEquals(200, expression.apply(instance).intValue());
        /*
         * Notify the optimizer that property values will be of `String` type.
         * The optimizer should compute an `ObjectConverter` in advance.
         */
        optimization.setFeatureType(feature);
        final var optimized = optimization.apply(expression);
        assertEquals(200, expression.apply(instance).intValue());
        assertNotSame(expression, optimized);

        final var property = assertInstanceOf(PropertyValue.class, optimized.getParameters().get(0));
        assertEquals(String.class, property.getSourceClass());
        assertEquals(Number.class, property.getValueClass());
    }

    /**
     * Returns {@code true} if the given filter is declared volatile.
     */
    private static boolean isVolatile(final Filter<?> filter) {
        return Optimization.properties(filter).equals(Set.of(FunctionProperty.VOLATILE));
    }
}
