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


import static nz.org.riskscape.engine.Matchers.*;

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

import static nz.org.riskscape.engine.Assert.*;

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

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

import nz.org.riskscape.engine.ArgsProblems;
import nz.org.riskscape.engine.RiskscapeException;
import nz.org.riskscape.engine.Tuple;
import nz.org.riskscape.engine.function.lang.NullOf;
import nz.org.riskscape.engine.types.Nullable;
import nz.org.riskscape.engine.types.RSList;
import nz.org.riskscape.engine.types.Struct;
import nz.org.riskscape.engine.types.Type;
import nz.org.riskscape.engine.types.TypeProblems;
import nz.org.riskscape.engine.types.Types;
import nz.org.riskscape.engine.types.WithinRange;
import nz.org.riskscape.engine.typexp.UnknownTypeException;
import nz.org.riskscape.rl.ast.ExpressionProblems;

public class LanguageFunctionsTest extends BaseExpressionRealizerTest {

  @Before
  public void setup() {
    project.getFunctionSet().addAll(LanguageFunctions.FUNCTIONS);
    project.getFunctionSet().addAll(LogicFunctions.LOGIC_FUNCTIONS);
  }

  @Test
  public void testConcat() throws Exception {
    evaluate("concat([1, 2, 3], [4, 5, 6])", Tuple.EMPTY_TUPLE);
    assertThat(realizationProblems, empty());
    assertThat(realized.getResultType(), equalTo(RSList.create(Types.INTEGER)));
    assertEquals(evaluated, Arrays.asList(1L, 2L, 3L, 4L, 5L, 6L));

    // check we can concat lists of two different types
    evaluate("concat([1, 2, 3], ['a', 'b', 'c'])", Tuple.EMPTY_TUPLE);
    assertThat(realizationProblems, empty());
    assertThat(realized.getResultType(), equalTo(RSList.create(Types.ANYTHING)));
    assertEquals(evaluated, Arrays.asList(1L, 2L, 3L, "a", "b", "c"));

    // check we can concat lists with nullable elements
    evaluate("concat([1, null, 3], [4, 5, 6])", Tuple.EMPTY_TUPLE);
    assertThat(realizationProblems, empty());
    assertThat(realized.getResultType(), equalTo(RSList.create(Nullable.INTEGER)));
    assertEquals(evaluated, Arrays.asList(1L, null, 3L, 4L, 5L, 6L));

    evaluate("concat([1, null, 3], ['a', null, 'c'])", Tuple.EMPTY_TUPLE);
    assertThat(realizationProblems, empty());
    assertThat(realized.getResultType(), equalTo(RSList.create(Nullable.ANYTHING)));
    assertEquals(evaluated, Arrays.asList(1L, null, 3L, "a", null, "c"));

    // check we can concat an empty list
    evaluate("concat([1, 2, 3], [])", Tuple.EMPTY_TUPLE);
    assertThat(realizationProblems, empty());
    // empty list special case
    assertThat(realized.getResultType(), equalTo(RSList.create(Types.INTEGER)));
    assertEquals(evaluated, Arrays.asList(1L, 2L, 3L));

    // and vice versa
    evaluate("concat([], ['a', null, 'c'])", Tuple.EMPTY_TUPLE);
    assertThat(realizationProblems, empty());
    assertThat(realized.getResultType(), equalTo(RSList.create(Nullable.TEXT)));
    assertEquals(evaluated, Arrays.asList("a", null, "c"));

    // check the type of an empty list is preserved, if it's known
    Tuple emptyList = Tuple.ofValues(Struct.of("value", RSList.create(Types.INTEGER)), Arrays.asList());
    evaluate("concat(value, [1, 2, 3])", emptyList);
    assertThat(realizationProblems, empty());
    assertThat(realized.getResultType(), equalTo(RSList.create(Types.INTEGER)));
    assertEquals(evaluated, Arrays.asList(1L, 2L, 3L));
  }

  @Test
  public void testConcatCoercibleTypes() {
    evaluate("concat([1, 2, 3], [3.3])", Tuple.EMPTY_TUPLE);
    assertThat(realizationProblems, empty());
    assertThat(realized.getResultType(), equalTo(RSList.create(Types.FLOATING)));
    assertEquals(evaluated, Arrays.asList(1D, 2D, 3D, 3.3D));

    evaluate("concat([3.3], [1, 2, 3])", Tuple.EMPTY_TUPLE);
    assertThat(realizationProblems, empty());
    assertThat(realized.getResultType(), equalTo(RSList.create(Types.FLOATING)));
    assertEquals(evaluated, Arrays.asList(3.3D, 1D, 2D, 3D));
  }

  @Test
  public void testConcatNonCoercibleTypes() {
    evaluate("concat([1, 2, 3], ['foo'])", Tuple.EMPTY_TUPLE);
    assertThat(realizationProblems, empty());
    assertThat(realized.getResultType(), equalTo(RSList.create(Types.ANYTHING)));
    assertEquals(evaluated, Arrays.asList(1L, 2L, 3L, "foo"));
  }

  @Test
  public void testConcatNullLists() throws Exception {
    // check concating a null list doesn't lose list elements
    evaluate("concat(null_of('list(text)'), ['a', 'b', 'c'])", Tuple.EMPTY_TUPLE);
    assertThat(realizationProblems, empty());
    assertThat(realized.getResultType(), equalTo(RSList.create(Types.TEXT)));
    assertEquals(evaluated, Arrays.asList("a", "b", "c"));

    evaluate("concat([1, 2, 3], null_of('list(nullable(integer))'))", Tuple.EMPTY_TUPLE);
    assertThat(realizationProblems, empty());
    assertThat(realized.getResultType(), equalTo(RSList.create(Nullable.INTEGER)));
    assertEquals(evaluated, Arrays.asList(1L, 2L, 3L));

    // 2 null lists just gives you an empty list (rather than null)
    evaluate("concat(null_of('list(text)'), null_of('list(integer)'))", Tuple.EMPTY_TUPLE);
    assertThat(realizationProblems, empty());
    assertThat(realized.getResultType(), equalTo(RSList.create(Types.ANYTHING)));
    assertEquals(evaluated, Collections.emptyList());
  }

  @Test
  public void testConcatNullElements() throws Exception {
    // check nullable type is preserved
    evaluate("concat([null_of('text')], ['a', 'b', 'c'])", Tuple.EMPTY_TUPLE);
    assertThat(realizationProblems, empty());
    assertThat(realized.getResultType(), equalTo(RSList.create(Nullable.TEXT)));
    assertEquals(evaluated, Arrays.asList(null, "a", "b", "c"));

    evaluate("concat([1, 2, 3], [null_of('integer')])", Tuple.EMPTY_TUPLE);
    assertThat(realizationProblems, empty());
    assertThat(realized.getResultType(), equalTo(RSList.create(Nullable.INTEGER)));
    assertEquals(evaluated, Arrays.asList(1L, 2L, 3L, null));

    // nullable and mixed types should result in nullable anything
    evaluate("concat(['a', 'b', 'c'], [null_of('integer')])", Tuple.EMPTY_TUPLE);
    assertThat(realizationProblems, empty());
    assertThat(realized.getResultType(), equalTo(RSList.create(Nullable.ANYTHING)));
    assertEquals(evaluated, Arrays.asList("a", "b", "c", null));

    evaluate("concat([null_of('integer')], [1.0, 2.0, 3.0])", Tuple.EMPTY_TUPLE);
    assertThat(realizationProblems, empty());
    assertThat(realized.getResultType(), equalTo(RSList.create(Nullable.FLOATING)));
    assertEquals(evaluated, Arrays.asList(null, 1D, 2D, 3D));
  }

  @Test
  public void testConcatErrorCases() throws Exception {
    // wrong number of args
    evaluate("concat([1, 2, 3])", Tuple.EMPTY_TUPLE);
    assertThat(realizationProblems, contains(hasAncestorProblem(
        is(ArgsProblems.get().wrongNumber(2, 1)))));

    // wrong type of args
    evaluate("concat(0, [1, 2, 3])", Tuple.EMPTY_TUPLE);
    assertThat(realizationProblems, contains(hasAncestorProblem(
        is(TypeProblems.get().mismatch(getFunctionCallArg(0), RSList.create(Nullable.ANYTHING), Types.INTEGER))
            )));
    evaluate("concat([1, 2, 3], 4)", Tuple.EMPTY_TUPLE);
    assertThat(realizationProblems, contains(hasAncestorProblem(
        is(TypeProblems.get().mismatch(getFunctionCallArg(1), RSList.create(Nullable.ANYTHING), Types.INTEGER))
            )));

    evaluate("concat([1, 2, 3], null_of('integer'))", Tuple.EMPTY_TUPLE);
    assertThat(realizationProblems, contains(hasAncestorProblem(
        is(TypeProblems.get().mismatch(getFunctionCallArg(1), RSList.create(Nullable.ANYTHING), Types.INTEGER)))));
  }

  @Test
  public void testAppend() throws Exception {
    evaluate("append([1, 2, 3], 4)", Tuple.EMPTY_TUPLE);
    assertThat(realizationProblems, empty());
    assertThat(realized.getResultType(), equalTo(RSList.create(Types.INTEGER)));
    assertEquals(evaluated, Arrays.asList(1L, 2L, 3L, 4L));

    // check we can append an item of any type
    evaluate("append([1, 2, 3], 'a')", Tuple.EMPTY_TUPLE);
    assertThat(realizationProblems, empty());
    assertThat(realized.getResultType(), equalTo(RSList.create(Types.ANYTHING)));
    assertEquals(evaluated, Arrays.asList(1L, 2L, 3L, "a"));

    // check appending an empty list
    evaluate("append([], 'a')", Tuple.EMPTY_TUPLE);
    assertThat(realizationProblems, empty());
    // special case for empty list - we know it's empty so we can use the element type
    assertThat(realized.getResultType(), equalTo(RSList.create(Types.TEXT)));
    assertEquals(evaluated, Arrays.asList("a"));

    evaluate("append(null_of('list(integer)'), 1)", Tuple.EMPTY_TUPLE);
    assertThat(realizationProblems, empty());
    assertThat(realized.getResultType(), equalTo(RSList.create(Types.INTEGER)));
    assertEquals(evaluated, Arrays.asList(1L));
  }

  @Test
  public void testAppendCoercibleTypes() {
    evaluate("append([1, 2, 3], 3.3)", Tuple.EMPTY_TUPLE);
    assertThat(realizationProblems, empty());
    assertThat(realized.getResultType(), equalTo(RSList.create(Types.FLOATING)));
    assertEquals(evaluated, Arrays.asList(1D, 2D, 3D, 3.3D));

    evaluate("append([3.3], 3)", Tuple.EMPTY_TUPLE);
    assertThat(realizationProblems, empty());
    assertThat(realized.getResultType(), equalTo(RSList.create(Types.FLOATING)));
    assertEquals(evaluated, Arrays.asList(3.3D, 3D));
  }

  @Test
  public void testAppendNonCoercibleTypes() {
    evaluate("append([3.3], 'foo')", Tuple.EMPTY_TUPLE);
    assertThat(realizationProblems, empty());
    assertThat(realized.getResultType(), equalTo(RSList.create(Types.ANYTHING)));
    assertEquals(evaluated, Arrays.asList(3.3D, "foo"));

    evaluate("append([{value: 'bar'}], 'foo')", Tuple.EMPTY_TUPLE);
    assertThat(realizationProblems, empty());
    assertThat(realized.getResultType(), equalTo(RSList.create(Types.ANYTHING)));
    assertEquals(evaluated, Arrays.asList(tuple("{value: 'bar'}"), "foo"));
  }

  @Test
  public void testAppendNullableElements() throws Exception {
    // check we can append null
    evaluate("append([1, 2, 3], null_of('integer'))", Tuple.EMPTY_TUPLE);
    assertThat(realizationProblems, empty());
    assertThat(realized.getResultType(), equalTo(RSList.create(Nullable.INTEGER)));
    assertEquals(evaluated, Arrays.asList(1L, 2L, 3L, null));

    // check we don't lose nullable from the type
    evaluate("append([null_of('text')], 'foo')", Tuple.EMPTY_TUPLE);
    assertThat(realizationProblems, empty());
    assertThat(realized.getResultType(), equalTo(RSList.create(Nullable.TEXT)));
    assertEquals(evaluated, Arrays.asList(null, "foo"));

    // check combining list of nullable text + nullable text
    evaluate("append([null_of('text')], null_of('text'))", Tuple.EMPTY_TUPLE);
    assertThat(realizationProblems, empty());
    assertThat(realized.getResultType(), equalTo(RSList.create(Nullable.TEXT)));
    assertEquals(evaluated, Arrays.asList(null, null));

    // check combining nullable list of text + nullable text
    List<Object> listWithOneNull = new ArrayList<>(); // can't use Arrays.asList(null);
    listWithOneNull.add(null);
    evaluate("append(null_of('list(text)'), null_of('text'))", Tuple.EMPTY_TUPLE);
    assertThat(realizationProblems, empty());
    assertThat(realized.getResultType(), equalTo(RSList.create(Nullable.TEXT)));
    assertEquals(evaluated, listWithOneNull);

    evaluate("append(null_of('list(text)'), 'foo')", Tuple.EMPTY_TUPLE);
    assertThat(realizationProblems, empty());
    assertThat(realized.getResultType(), equalTo(RSList.create(Types.TEXT)));
    assertEquals(evaluated, Arrays.asList("foo"));
  }

  @Test
  public void testAppendErrorCases() throws Exception {
    // wrong number of args
    evaluate("append([1, 2, 3])", Tuple.EMPTY_TUPLE);
    assertThat(realizationProblems, contains(hasAncestorProblem(is(ArgsProblems.get().wrongNumber(2, 1)))));

    // wrong type of args
    evaluate("append(0, [1, 2, 3])", Tuple.EMPTY_TUPLE);
    assertThat(realizationProblems, contains(hasAncestorProblem(
        is(TypeProblems.get().mismatch(getFunctionCallArg(0), RSList.create(Nullable.ANYTHING), Types.INTEGER)))));
  }

  @Test
  public void test_null_of() throws Exception {
    assertNull(evaluate("null_of('text')", Tuple.EMPTY_TUPLE));
    assertEquals(Nullable.TEXT, realized.getResultType());
    // double positive still OK
    assertNull(evaluate("null_of('nullable(text)')", Tuple.EMPTY_TUPLE));
    assertEquals(Nullable.TEXT, realized.getResultType());

    assertNull(evaluate("null_of('list(integer)')", Tuple.EMPTY_TUPLE));
    assertEquals(Nullable.of(RSList.create(Types.INTEGER)), realized.getResultType());

    // finds identified types
    Type myType = new WithinRange(Types.FLOATING, 5.0D, 10.D);
    project.getTypeSet().add("my-type", myType);
    assertNull(evaluate("null_of('my-type')", Tuple.EMPTY_TUPLE));
    assertEquals(Nullable.of(myType), realized.getResultType());

    // not a string
    assertNull(evaluate("null_of(1)", Tuple.EMPTY_TUPLE));
    assertThat(
        realizationProblems,
        contains(hasAncestorProblem(equalTo(ArgsProblems.mismatch(NullOf.ARGUMENTS.get(0), Types.INTEGER))))
    );

    // not a constant
    assertNull(evaluate("null_of(element)", tuple("{element: 'foo'}")));
    assertThat(
        realizationProblems,
        contains(hasAncestorProblem(
            equalTo(ExpressionProblems.get().constantRequired(getFunctionCallArg(0).getExpression()))
            ))
    );

    // bad type expression
    evaluate("null_of('textual')", Tuple.EMPTY_TUPLE);
    assertThat(
        realizationProblems,
        contains(hasAncestorProblem(isProblem(UnknownTypeException.class)))
    );
  }

  @Test
  public void testNullable() throws Exception {
    assertThat(evaluateConstant("nullable(1)"), is(1L));
    assertThat(realized.getResultType(), is(Nullable.INTEGER));

    assertThat(evaluateConstant("nullable(nullable(1))"), is(1L));
    assertThat(realized.getResultType(), is(Nullable.INTEGER));

    assertThat(evaluateConstant("nullable(null_of('integer'))"), nullValue());
    assertThat(realized.getResultType(), is(Nullable.INTEGER));
  }

  @Test
  public void testLength() {
    evaluate("length([])", Tuple.EMPTY_TUPLE);
    assertThat(realizationProblems, empty());
    assertThat(realized.getResultType(), is(Types.INTEGER));
    assertEquals(evaluated, 0L);

    evaluate("length([1,2,3])", Tuple.EMPTY_TUPLE);
    assertThat(realizationProblems, empty());
    assertEquals(evaluated, 3L);
  }

  @Test
  public void testLengthOfNullableList() {
    evaluate("length(null_of('list(integer)'))", Tuple.EMPTY_TUPLE);
    assertThat(realizationProblems, empty());
    assertThat(realized.getResultType(), is(Nullable.INTEGER));
    assertNull(evaluated);

    evaluate("length(nullable([1, 3]))", Tuple.EMPTY_TUPLE);
    assertThat(realizationProblems, empty());
    assertThat(realized.getResultType(), is(Nullable.INTEGER));
    assertEquals(evaluated, 2L);
  }

  @Test
  public void testRange() throws Exception {
    evaluate("range(1, 7)", Tuple.EMPTY_TUPLE);
    assertThat(realizationProblems, empty());
    assertThat(realized.getResultType(), equalTo(RSList.create(Types.INTEGER)));
    assertEquals(evaluated, Arrays.asList(1L, 2L, 3L, 4L, 5L, 6L));

    // check the range() result can be used in other list functions
    evaluate("concat(range(0, 3), [3, 4, 5])", Tuple.EMPTY_TUPLE);
    assertThat(realizationProblems, empty());
    assertEquals(evaluated, Arrays.asList(0L, 1L, 2L, 3L, 4L, 5L));

    evaluate("length(range(0, 100))", Tuple.EMPTY_TUPLE);
    assertThat(realizationProblems, empty());
    assertEquals(evaluated, 100L);

    // try a bad range it should just give us an empty list
    evaluate("range(1, 0)", Tuple.EMPTY_TUPLE);
    assertThat(realizationProblems, empty());
    assertThat(realized.getResultType(), equalTo(RSList.create(Types.INTEGER)));
    assertEquals(evaluated, Collections.emptyList());
  }

  @Test
  public void testAssert() throws Exception {
    Struct type = Struct.of("condition", Types.BOOLEAN, "message", Types.TEXT);
    realize(type, parse("assert(condition, message)"));
    assertThat(realizationProblems, empty());
    assertThat(realized.getResultType(), equalTo(Types.NOTHING));

    assertThat(realized.evaluate(Tuple.ofValues(type, true, "foo bar")), nullValue());
    // no problems got recorded
    assertThat(sunkProblems, empty());

    RiskscapeException ex = assertThrows(RiskscapeException.class,
        () -> realized.evaluate(Tuple.ofValues(type, false, "foo bar")));
    assertThat(ex.getProblem(), hasAncestorProblem(is(LanguageFunctions.PROBLEMS.userAssert("foo bar"))));

    // ensure named arg handling it fine
    assertThat(evaluate("assert(message: 'boo', condition: true)", Tuple.EMPTY_TUPLE), nullValue());
  }

  @Test
  public void testAssertWithBadArgs() {
    // not enough
    assertThat(
        realizeOnly("assert(true)", Struct.EMPTY_STRUCT),
        failedResult(isError(ArgsProblems.class, "realizableDidNotMatch"))
    );

    // too many
    assertThat(
        realizeOnly("assert(true, 'foo', 'bar')", Struct.EMPTY_STRUCT),
        failedResult(isError(ArgsProblems.class, "realizableDidNotMatch"))
    );

    // wrong type arg1
    assertThat(
        realizeOnly("assert('true', 'foo')", Struct.EMPTY_STRUCT),
        failedResult(isError(ArgsProblems.class, "realizableDidNotMatch"))
    );

    // wrong type arg2
    assertThat(
        realizeOnly("assert(true, 33)", Struct.EMPTY_STRUCT),
        failedResult(isError(ArgsProblems.class, "realizableDidNotMatch"))
    );
  }

  @Test
  public void testWarning() throws Exception {
    Struct type = Struct.of("when", Types.BOOLEAN, "message", Types.TEXT);
    realize(type, parse("warning(when, message)"));
    assertThat(realizationProblems, empty());
    assertThat(realized.getResultType(), equalTo(Nullable.TEXT));

    assertThat(realized.evaluate(Tuple.ofValues(type, false, "foo bar")), nullValue());
    // no problems got recorded
    assertThat(sunkProblems, empty());

    // when=true executes okay and returns the expected message
    assertThat(realized.evaluate(Tuple.ofValues(type, true, "foo bar")), is("foo bar"));
    // but problems are sunk
    assertThat(sunkProblems, contains(
        is(LanguageFunctions.PROBLEMS.userWarning("foo bar"))
    ));
  }

  @Test
  public void testWarningWithBadArgs() {
    // not enough
    assertThat(
        realizeOnly("warning(true)", Struct.EMPTY_STRUCT),
        failedResult(isError(ArgsProblems.class, "realizableDidNotMatch"))
    );

    // too many
    assertThat(
        realizeOnly("warning(true, 'foo', 'bar')", Struct.EMPTY_STRUCT),
        failedResult(isError(ArgsProblems.class, "realizableDidNotMatch"))
    );

    // wrong type arg1
    assertThat(
        realizeOnly("warning('true', 'foo')", Struct.EMPTY_STRUCT),
        failedResult(isError(ArgsProblems.class, "realizableDidNotMatch"))
    );

    // wrong type arg2
    assertThat(
        realizeOnly("warning(true, 33)", Struct.EMPTY_STRUCT),
        failedResult(isError(ArgsProblems.class, "realizableDidNotMatch"))
    );
  }
}
