/*
 * 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.function.lang;

import static org.hamcrest.Matchers.*;
import static org.junit.Assert.*;

import java.util.Arrays;

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

import nz.org.riskscape.engine.ArgsProblems;
import nz.org.riskscape.engine.Matchers;
import nz.org.riskscape.engine.Tuple;
import nz.org.riskscape.engine.function.StringFunctions;
import nz.org.riskscape.engine.rl.BaseExpressionRealizerTest;
import nz.org.riskscape.engine.rl.DefaultOperators;
import nz.org.riskscape.engine.rl.LanguageFunctions;
import nz.org.riskscape.engine.rl.LogicFunctions;
import nz.org.riskscape.engine.rl.MathsFunctions;
import nz.org.riskscape.engine.rl.Reduce;
import nz.org.riskscape.engine.types.Nullable;
import nz.org.riskscape.engine.types.RSList;
import nz.org.riskscape.engine.types.ScopedLambdaType;
import nz.org.riskscape.engine.types.Struct;
import nz.org.riskscape.engine.types.TypeProblems;
import nz.org.riskscape.engine.types.Types;
import nz.org.riskscape.rl.ast.ExpressionProblems;
import nz.org.riskscape.rl.ast.FunctionCall;

public class ReduceTest extends BaseExpressionRealizerTest {

  Reduce reduce = new Reduce();

  @Before
  public void addFunctions() {
    project.getFunctionSet().add(reduce.identified("reduce"));
    project.getFunctionSet().insertFirst(DefaultOperators.INSTANCE);
    project.getFunctionSet().addAll(MathsFunctions.FUNCTIONS);
    project.getFunctionSet().addAll(StringFunctions.FUNCTIONS);
    project.getFunctionSet().addAll(LogicFunctions.LOGIC_FUNCTIONS);
    project.getFunctionSet().add(LanguageFunctions.NULL_OF);
  }

  @Test
  public void canReduceListToSum() {
    evaluate("reduce([1, 2, 3, 4], (lhs, rhs) -> lhs + rhs)", Tuple.EMPTY_TUPLE);
    assertThat(realizationProblems, empty());
    assertThat(realized.getResultType(), equalTo(Nullable.INTEGER));
    assertEquals(evaluated, 10L);
  }

  @Test
  public void canReduceListToProduct() {
    evaluate("reduce([2, 3, 4], (lhs, rhs) -> lhs * rhs)", Tuple.EMPTY_TUPLE);
    assertThat(realizationProblems, empty());
    assertThat(realized.getResultType(), equalTo(Nullable.INTEGER));
    assertEquals(evaluated, 24L);
  }

  @Test
  public void canReduceListToMax() {
    evaluate("reduce([5, 2, 9, 1, 7], (lhs, rhs) -> if(lhs > rhs, lhs, rhs))", Tuple.EMPTY_TUPLE);
    assertThat(realizationProblems, empty());
    assertThat(realized.getResultType(), equalTo(Nullable.INTEGER));
    assertEquals(evaluated, 9L);
  }

  @Test
  public void canReduceListToMin() {
    evaluate("reduce([5, 2, 9, 1, 7], (lhs, rhs) -> if(lhs < rhs, lhs, rhs))", Tuple.EMPTY_TUPLE);
    assertThat(realizationProblems, empty());
    assertThat(realized.getResultType(), equalTo(Nullable.INTEGER));
    assertEquals(evaluated, 1L);
  }

  @Test
  public void canConcatenateStrings() {
    evaluate("reduce(['a', 'b', 'c'], (lhs, rhs) -> lhs + rhs)", Tuple.EMPTY_TUPLE);
    assertThat(realizationProblems, empty());
    assertThat(realized.getResultType(), equalTo(Nullable.TEXT));
    assertEquals(evaluated, "abc");
  }

  @Test
  public void canReduceListWithSeparator() {
    evaluate("reduce(['a', 'b', 'c'], (lhs, rhs) -> lhs + ', ' + rhs)", Tuple.EMPTY_TUPLE);
    assertThat(realizationProblems, empty());
    assertThat(realized.getResultType(), equalTo(Nullable.TEXT));
    assertEquals(evaluated, "a, b, c");
  }

  @Test
  public void reduceEmptyListReturnsNull() {
    evaluate("reduce([], (lhs, rhs) -> lhs + rhs)", Tuple.EMPTY_TUPLE);
    assertThat(realizationProblems, empty());
    assertThat(realized.getResultType(), equalTo(Types.NOTHING));
    assertNull(evaluated);
  }

  @Test
  public void reduceNullListReturnsNull() {
    evaluate("reduce(null_of('list(integer)'), (lhs, rhs) -> lhs + rhs)", Tuple.EMPTY_TUPLE);
    assertThat(realizationProblems, empty());
    assertThat(realized.getResultType(), equalTo(Nullable.INTEGER));
    assertNull(evaluated);
  }

  @Test
  public void reduceSingleElementListReturnsElement() {
    evaluate("reduce([42], (lhs, rhs) -> lhs + rhs)", Tuple.EMPTY_TUPLE);
    assertThat(realizationProblems, empty());
    assertThat(realized.getResultType(), equalTo(Nullable.INTEGER));
    assertEquals(evaluated, 42L);
  }

  @Test
  public void reduceSkipsNullElements() {
    // Reduce should skip null elements in the list
    evaluate("reduce([1, null, 2, 3], (lhs, rhs) -> lhs + rhs)", Tuple.EMPTY_TUPLE);
    assertThat(realizationProblems, empty());
    assertThat(realized.getResultType(), equalTo(Nullable.INTEGER));
    assertEquals(evaluated, 6L);
  }

  @Test
  public void reduceWithAllNullElementsReturnsNull() {
    // If all elements are null, reduce should return null
    evaluate("reduce([null_of('integer'), null_of('integer')], (lhs, rhs) -> lhs + rhs)", Tuple.EMPTY_TUPLE);
    assertThat(realizationProblems, empty());
    assertThat(realized.getResultType(), equalTo(Nullable.INTEGER));
    assertNull(evaluated);
  }

  @Test
  public void reduceWithNullElementAtStart() {
    // First element is null, should start with second element
    evaluate("reduce([null, 1, 2, 3], (lhs, rhs) -> lhs + rhs)", Tuple.EMPTY_TUPLE);
    assertThat(realizationProblems, empty());
    assertThat(realized.getResultType(), equalTo(Nullable.INTEGER));
    assertEquals(evaluated, 6L);
  }

  @Test
  public void reduceIgnoresNullIntermediateResults() {
    // If combine returns null, it should be ignored
    evaluate("reduce([1, 2, null, 3, 4], (lhs, rhs) -> if(rhs = 2, null, lhs + rhs))",
        Tuple.EMPTY_TUPLE);
    assertThat(realizationProblems, empty());
    assertThat(realized.getResultType(), equalTo(Nullable.INTEGER));
    // Should be 1 + skip(2) + 3 + 4 = 8
    assertEquals(evaluated, 8L);
  }

  @Test
  public void canReduceUsingOuterScope() {
    Struct inputType = Struct.of("multiplier", Types.INTEGER, "items", RSList.create(Types.INTEGER));
    Tuple input1 = Tuple.ofValues(inputType, 2L, Arrays.asList(1L, 2L, 3L));

    assertThat(
        evaluate("reduce(items, (lhs, rhs) -> lhs + (rhs * multiplier))", input1),
        is(11L)
    );
  }

  @Test
  public void lambdaParametersReplaceScopeVariables() {
    // lhs and rhs in the lambda should replace any lhs/rhs from the outer scope
    Struct inputType = Struct.of("lhs", Types.INTEGER, "rhs", Types.INTEGER, "items", RSList.create(Types.INTEGER));
    Tuple input = Tuple.ofValues(inputType, 100L, 200L, Arrays.asList(1L, 2L, 3L));

    // Should use lambda's lhs/rhs (1+2)+3=6, not the outer scope's 100, 200
    assertEquals(
        evaluate("reduce(items, (lhs, rhs) -> lhs + rhs)", input),
        6L
    );
  }

  @Test
  public void errorWrongNumberOfArgs() {
    // not enough args - missing lambda
    evaluate("reduce([1, 2, 3])", Tuple.EMPTY_TUPLE);

    assertThat(
        realizationProblems,
        contains(Matchers.hasAncestorProblem(
            equalTo(
                ArgsProblems.get().wrongNumber(2, 1))
        ))
    );

    // too many args
    evaluate("reduce([1, 2, 3], (lhs, rhs) -> lhs + rhs, 'extra')", Tuple.EMPTY_TUPLE);

    assertThat(
        realizationProblems,
        contains(Matchers.hasAncestorProblem(
            equalTo(
                ArgsProblems.get().wrongNumber(2, 3))
        ))
    );
  }

  @Test
  public void errorOnWrongArgType() {
    // values not a list
    evaluate("reduce('foo', (lhs, rhs) -> lhs + rhs)", Tuple.EMPTY_TUPLE);

    FunctionCall.Argument listArg = expr.isA(FunctionCall.class).get().getArguments().get(0);

    assertThat(
        realizationProblems,
        contains(Matchers.hasAncestorProblem(
            TypeProblems.get().mismatch(listArg, RSList.LIST_ANYTHING, Types.TEXT)
        ))
    );

    // combine not a lambda
    evaluate("reduce([1,2,3], 'foo')", Tuple.EMPTY_TUPLE);

    FunctionCall.Argument lambdaArg = expr.isA(FunctionCall.class).get().getArguments().get(1);


    assertThat(
        realizationProblems,
        contains(Matchers.hasAncestorProblem(
            equalTo(
                TypeProblems.get().mismatch(lambdaArg, ScopedLambdaType.class, Types.TEXT.getClass())
            )
        ))
    );
  }

  @Test
  public void givesAnErrorIfWrongNumberOfLambdaArgs() {
    // Lambda with only one arg
    evaluate("reduce([1, 2, 3], (x) -> x)", Tuple.EMPTY_TUPLE);

    assertThat(
        realizationProblems,
        contains(Matchers.hasAncestorProblem(
            equalTo(ExpressionProblems.get().lambdaArityError(parse("(x) -> x"), 1, 2))
        ))
    );

    // Lambda with three args
    evaluate("reduce([1, 2, 3], (x, y, z) -> x + y + z)", Tuple.EMPTY_TUPLE);

    assertThat(
        realizationProblems,
        contains(Matchers.hasAncestorProblem(
            equalTo(ExpressionProblems.get().lambdaArityError(parse("(x, y, z) -> x + y + z"), 3, 2))
        ))
    );
  }

  @Test
  public void givesAnErrorIfLambdaDoesNotReturnTheSameTypeAsElementType() throws Exception {
    // it would be nice if reduce handled this case, but for now it's a nice simple example of the function handling
    // a more general problem
    evaluate("reduce([1, 2, 3], (a, b) -> a + b * 1.0)", Tuple.EMPTY_TUPLE);

    Object reduceArg = expr.isA(FunctionCall.class).get().getArguments().get(1);
    assertThat(
        realizationProblems,
        contains(Matchers.hasAncestorProblem(
            equalTo(TypeProblems.get().mismatch(reduceArg, Types.INTEGER, Types.FLOATING))
        ))
    );
  }

  @Test
  public void returnTypeAndElementTypeCanBeDifferentAsLongAsAssignable() throws Exception {
    // the list is nullable integers, but the result is not null - this is different, but is assignable
    evaluate("reduce([1, 2, 3, null], (a, b) -> if_null(a + b, 0))", Tuple.EMPTY_TUPLE);

    assertThat(evaluated, is(6L));
  }

  @Test
  public void canReduceListOfStructs() {
    Struct itemType = Struct.of("value", Types.INTEGER);
    RSList listType = RSList.create(itemType);
    Struct inputType = Struct.of("items", listType);

    Tuple item1 = Tuple.ofValues(itemType, 5L);
    Tuple item2 = Tuple.ofValues(itemType, 3L);
    Tuple item3 = Tuple.ofValues(itemType, 8L);

    Tuple input = Tuple.ofValues(inputType, Arrays.asList(item1, item2, item3));

    evaluate("reduce(items, (lhs, rhs) -> if(lhs.value > rhs.value, lhs, rhs))", input);
    assertThat(realizationProblems, empty());
    assertThat(realized.getResultType(), equalTo(Nullable.of(itemType)));
    assertEquals(evaluated, item3);  // item3 has the largest value
  }

}
