/*
 * 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 java.util.List;

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

import nz.org.riskscape.engine.Tuple;
import nz.org.riskscape.engine.function.lang.IfNull;
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.TypeProblems;
import nz.org.riskscape.engine.types.Types;
import nz.org.riskscape.engine.types.WithinRange;

public class LogicFunctionsTest extends BaseExpressionRealizerTest {

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

  @Test
  public void testIsNotNull() throws Exception {
    Struct inputType = Struct.of("foo", Nullable.TEXT, "bar", Nullable.BOOLEAN);

    assertThat(evaluate("is_not_null(foo)", Tuple.ofValues(inputType, "foo1", true)), is(true));
    assertThat(evaluate("is_not_null(bar)", Tuple.ofValues(inputType, "foo2", true)), is(true));
    assertThat(evaluate("is_not_null(bar)", Tuple.ofValues(inputType, "foo2", false)), is(true));

    assertThat(evaluate("is_not_null(nullable(null_of('text')))", Tuple.EMPTY_TUPLE), is(false));
    assertThat(evaluate("is_not_null(nullable(null_of('boolean')))", Tuple.EMPTY_TUPLE), is(false));
    assertThat(realized.getResultType(), is(Types.BOOLEAN));
  }

  @Test
  public void testNot() throws Exception {
    assertThat(evaluate("not(true)", null), is(false));
    assertThat(evaluate("not(false)", null), is(true));
    assertThat(realized.getResultType(), is(Types.BOOLEAN));

    assertThat(evaluate("not(nullable(true))", Tuple.EMPTY_TUPLE), is(false));
    assertThat(evaluate("not(nullable(false))", Tuple.EMPTY_TUPLE), is(true));

    assertThat(evaluate("not(nullable(null_of('boolean')))", Tuple.EMPTY_TUPLE), is(nullValue()));
    assertThat(realized.getResultType(), is(Nullable.BOOLEAN));
  }

  @Test
  public void mapsOldStyleFunctionNamesToNew() {
    Struct inputType = Struct.of("foo", Nullable.TEXT, "bar", Nullable.BOOLEAN);
    Tuple input = Tuple.ofValues(inputType, "foo-value");

    assertThat(evaluate("isNull(foo)", input), is(false));
    assertThat(evaluate("isNull(bar)", input), is(true));

    assertThat(evaluate("isNotNull(foo)", input), is(true));
    assertThat(evaluate("isNotNull(bar)", input), is(false));
  }

  @Test
  public void test_if_null() throws Exception {
    assertEquals("foo", evaluate("if_null('foo', 'bar')", Tuple.EMPTY_TUPLE));
    assertEquals("bar", evaluate("if_null(null_of('text'), 'bar')", Tuple.EMPTY_TUPLE));

    // args need to be of the same type
    evaluate("if_null(1, 'bar')", Tuple.EMPTY_TUPLE);
    assertThat(
      realizationProblems,
      contains(hasAncestorProblem(equalTo(TypeProblems.get().
            mismatch(getFunctionCallArg(0), Types.TEXT, Types.INTEGER)
      )))
    );

    // tested arg can be more specific
    assertEquals(1L, evaluate("if_null(nullable(1), 2)", Tuple.EMPTY_TUPLE));

    // else type can be more specific - this used to not work, but now we check the variance from both sides
    // and declare the return type to be whichever was covariant to the other
    assertEquals(
        1L,
        evaluate(
          "if_null(null_of('integer'), value)",
          Tuple.ofValues(Struct.of("value", new WithinRange(Types.INTEGER, 0, 100)), 1L)
        )
      );
    assertEquals(Types.INTEGER, realized.getResultType());

    // else type cannot be null (it's just confusing to get null back from call if_null())
    evaluate("if_null(nullable(null_of('integer')), null_of('integer'))", Tuple.EMPTY_TUPLE);
    assertThat(
        realizationProblems,
        contains(hasAncestorProblem(equalTo(TypeProblems.get().
            cannotBeNull(new IfNull().getArguments().get(1), Types.INTEGER, Nullable.INTEGER)
        )))
      );

    // check that this also works with the empty list case, which is why we did this in the first place
    assertEquals(List.of(1L), evaluate("if_null(nullable([1]), [])", Tuple.EMPTY_TUPLE));
    // notice how this returns a list of integers, not the empty list - that's because an empty list is covariant to any
    // other list type
    assertEquals(RSList.create(Types.INTEGER), realized.getResultType());
  }

  @Test
  public void ifNullAcceptsNothingAsTestArg() {
    Struct inputType = Struct.of("foo", Types.TEXT, "bar", Types.NOTHING);
    assertThat(
        evaluate("if_null(bar, foo)", Tuple.ofValues(inputType, "foo")),
        is("foo")
    );
    // Types.NOTHING is always null so in this case if_null should just return the else's type
    assertThat(realized.getResultType(), is(Types.TEXT));

    // this is why we'd want if_null to accept nothing, it allows if_null to wrap a warning/assert
    // function call so pointless always null attributes can be avoided
    assertThat(
        evaluate("if_null(warning(false, 'Fee-fi-fo-fum'), 'foo')", Tuple.ofValues(inputType, "foo")),
        is("foo")
    );
  }

}
