/*
 * RiskScape™ Copyright New Zealand Institute for Earth Science Limited
 * (Earth Sciences New Zealand) is distributed for research purposes only
 * under the terms of AGPLv3.
 *
 * RiskScape™ Copyright 2026 New Zealand Institute for Earth Science
 * Limited (Earth Sciences New Zealand). All rights reserved. Source code
 * available under the AGPLv3.
 * 
 * This program is free software: you can redistribute it and/or modify it under
 *  the terms of the GNU Affero General Public License as published by the Free
 *  Software Foundation, either version 3 of the License, or (at your option) any
 *  later version.
 * 
 * This program is distributed for RESEARCH PURPOSES ONLY, in the hope that it will
 * be useful for research and education initiatives.
 * 
 * If you are not a researcher, or you are a researcher who wishes to use this
 * program on terms other than AGPLv3 (including those who wish to restrict the
 * distribution of any source code created using this program), please contact:
 * https://riskscape.org.nz
 * 
 * This program is distributed WITHOUT ANY WARRANTY; without even the implied
 * warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
 * Affero General Public License for more details.  You should have received a copy
 * of the GNU Affero General Public License along with this program.  If not, see
 * <http://www.gnu.org/licenses/>.
 * 
 * By way of summary only, under the AGPLv3:
 *     • Permissions of this strongest copyleft license are conditioned
 *       on making available complete source code of licensed works and
 *       modifications, which include larger works using a licensed work,
 *       under the same license.
 *     • Copyright and license notices must be preserved.
 *     • Contributors provide an express grant of patent rights.
 *     • When a modified version is used to provide a service over a
 *       network, the complete source code of the modified version must be made
 *       available.
 */
package nz.org.riskscape.engine.filter;

import static org.junit.Assert.*;

import java.util.Collections;

import org.geotools.filter.spatial.ContainsImpl;
import org.geotools.filter.text.cql2.CQLException;
import org.geotools.filter.text.ecql.ECQL;
import org.junit.Test;

import com.google.common.collect.ImmutableList;
import com.google.common.collect.Lists;
import java.util.ArrayList;
import java.util.List;
import static nz.org.riskscape.engine.Assert.assertThrows;
import nz.org.riskscape.engine.DummyFunction;
import nz.org.riskscape.engine.RiskscapeException;
import nz.org.riskscape.engine.Tuple;
import nz.org.riskscape.engine.UnknownFunctionException;
import nz.org.riskscape.engine.expr.FunctionEvaluationException;

import nz.org.riskscape.engine.expr.StructAccessExpression;
import nz.org.riskscape.engine.function.IdentifiedFunction;
import nz.org.riskscape.engine.types.Struct;
import nz.org.riskscape.engine.types.Types;
import org.junit.Before;
import org.geotools.api.filter.expression.Expression;

public class FilterFactoryTest implements ECQLHelper {

  List<IdentifiedFunction> functions;
  FilterFactory factory;

  Struct mathType = Struct.of("text", Types.TEXT)
      .and("integer", Types.INTEGER)
      .and("floating", Types.FLOATING).build();

  @Before
  public void setup() {
    functions = new ArrayList<>();
    functions.add(new DummyFunction("one_arg", Lists.newArrayList(Types.TEXT)));
    functions.add(new DummyFunction("two_args", Lists.newArrayList(Types.TEXT, Types.TEXT)));
    factory = new FilterFactory((id) -> functions.stream().filter(f -> f.getId().equals(id)).findFirst());
  }

  @Test
  public void testPropertyFiltersReturnStructAccessExpressions() throws CQLException {
    StructAccessExpression expr1 = (StructAccessExpression) factory.property("foo");
    assertEquals(Collections.singletonList("foo"), expr1.getSegments());
    assertEquals(expr1, ECQL.toExpression("foo", factory));

    StructAccessExpression expr2 = (StructAccessExpression) factory.property("foo.bar");
    assertEquals(ImmutableList.of("foo", "bar"), expr2.getSegments());
    assertEquals(expr2, ECQL.toExpression("foo.bar", factory));

    StructAccessExpression expr3 = (StructAccessExpression) factory.property("foo.bar.baz");
    assertEquals(ImmutableList.of("foo", "bar", "baz"), expr3.getSegments());
    assertEquals(expr3, ECQL.toExpression("foo.bar.baz", factory));
  }

  @Test
  public void canConstructAFilterUsingCQL() throws CQLException {
    ContainsImpl filter = (ContainsImpl) ECQL.toFilter("contains(foo, bar)", factory);
    assertNotNull((StructAccessExpression) filter.getExpression1());
    assertNotNull((StructAccessExpression) filter.getExpression2());
  }

  @Test
  public void willBuildRiskscapeFunctionFromLiteralArgs() throws CQLException {
    Expression ex = toExpression("rsfunc('one_arg', 'cat')");
    assertEquals("cat", ex.evaluate(null));

    ex = toExpression("rsfunc('two_args', 'cat', 'dog')");
    assertEquals("cat+dog", ex.evaluate(null));
  }

  @Test
  public void willBuildRiskscapeFunctionWithStructAccessExpressions() throws CQLException {
    Struct type = Struct.of("a", Types.TEXT).and("b", Types.TEXT).build();
    Tuple tuple = new Tuple(type).set("a", "mouse").set("b", "rat");

    Expression ex = toExpression("rsfunc('one_arg', a)");
    assertEquals("mouse", ex.evaluate(tuple));

    ex = toExpression("rsfunc('two_args', a, b)");
    assertEquals("mouse+rat", ex.evaluate(tuple));
  }

  @Test
  public void rsfuncFunctionArgumentMustBeLiteral() {
    CQLException ex = assertThrows(CQLException.class, () -> toExpression("rsfunc(property.value, a, b)"));
  }

  @Test
  public void anExceptionIsThrownForWhenToManyArgsAreProvided() throws CQLException {
    Expression ex = toExpression("rsfunc('one_arg', 'a', 'b')");
    //Riskscape function validation is done on first use.
    RiskscapeException e = assertThrows(RiskscapeException.class, () -> ex.evaluate(null));
    assertEquals("Function one_arg expects 1 arguments but 2 were given", e.getMessage());
  }

  @Test
  public void anExceptionIsThrownForWhenNotEnoughArgsAreProvided() throws CQLException {
    Expression ex = toExpression("rsfunc('two_args', 'a')");
    RiskscapeException e = assertThrows(RiskscapeException.class, () -> ex.evaluate(null));
    assertEquals("Function two_args expects 2 arguments but 1 were given", e.getMessage());
  }

  @Test
  public void anExceptionIsThrownForUnknownFunction() throws CQLException {
    Expression ex = toExpression("rsfunc('unknown_func', 'a')");
    UnknownFunctionException e = assertThrows(UnknownFunctionException.class, () -> ex.evaluate(null));
    assertEquals("Unknown function 'unknown_func'. Refer to 'riskscape function list' for available functions",
        e.getMessage());
  }

  @Test
  public void canDetectStaticExpressionsCorrectly() throws CQLException {
    assertTrue(FilterFactory.isStatic(toExpression("'dog'")));
    assertTrue(FilterFactory.isStatic(toExpression("10")));
    assertTrue(FilterFactory.isStatic(toExpression("10 + 10")));
    assertTrue(FilterFactory.isStatic(toExpression("10 * 10")));
    assertTrue(FilterFactory.isStatic(toExpression("10 / 5")));

    assertFalse(FilterFactory.isStatic(toExpression("asset.thing")));
    assertFalse(FilterFactory.isStatic(toExpression("10 + asset.value")));
    assertFalse(FilterFactory.isStatic(toExpression("10 * asset.value")));
    assertFalse(FilterFactory.isStatic(toExpression("10 / asset.value")));
  }

  @Test
  public void canCreateAPolynomialFunction() throws Exception {
    Tuple data = Tuple.of(mathType, "floating", 1D, "text", "one");

    Expression function = toExpression("polynomial(floating, 2, 1)");
    assertEquals(3D, function.evaluate(data));
  }

  @Test
  public void cannotRunAMathFunctionIfInputsAreNotNumbers() throws Exception {
    Tuple data = Tuple.of(mathType, "floating", 1D, "text", "one");

    Expression function = toExpression("polynomial(text, 2, 1)");
    FunctionEvaluationException ex = assertThrows(FunctionEvaluationException.class, () -> function.evaluate(data));
    assertEquals("Failed to coerce argument 1", ex.getMessage());
  }

  @Test
  public void cannotRunAMathFunctionIfInputsAreNotNumbers2() throws Exception {
    Tuple data = Tuple.of(mathType, "floating", 1D, "text", "one");

    Expression function = toExpression("polynomial(floating, 'two', 1)");
    FunctionEvaluationException ex = assertThrows(FunctionEvaluationException.class, () -> function.evaluate(data));
    assertEquals("Failed to coerce argument 2", ex.getMessage());
  }

  @Test
  public void canCreateAPowerFunction() throws Exception {
    Tuple data = Tuple.of(mathType, "floating", 2D);
    Expression function = toExpression("power(floating, 2)");
    assertEquals(4D, function.evaluate(data));
  }

  @Test
  public void canCreateANormalDistribution() throws Exception {
    Tuple data = Tuple.of(mathType, "floating", 2D);
    Expression function = toExpression("cumulative_normal_distribution(floating, 2, 1)");

    assertEquals(0.5D, function.evaluate(data));
  }

  @Test
  public void canCreateALogNormalDistribution() throws Exception {
    Tuple data = Tuple.of(mathType, "floating", 1D);
    Expression function = toExpression("cumulative_log_normal_distribution(floating, 0, 10)");

    assertEquals(0.5D, function.evaluate(data));
  }

  @Test
  public void canCreateAMathFunctionWithDynamicAttributes() throws CQLException {
    Expression function = toExpression("power(floating, integer)");
    Tuple data = Tuple.of(mathType, "floating", 3f, "integer", 2L);
    assertEquals(9d, function.evaluate(data));
    data.set("integer", 3L);
    assertEquals(27d, function.evaluate(data));
  }

  @Override
  public FilterFactory getFilterFactory() {
    return factory;
  }

}
