/*
 * 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 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.Tuple;
import nz.org.riskscape.engine.types.Nullable;
import nz.org.riskscape.engine.types.Struct;
import nz.org.riskscape.engine.types.Type;
import nz.org.riskscape.engine.types.Types;
import nz.org.riskscape.engine.types.WithinRange;
import nz.org.riskscape.rl.ast.ExpressionProblems;

public class DefaultOperatorsTest extends BaseExpressionRealizerTest {

  @Before
  public void before() {
    project.getFunctionSet().insertFirst(DefaultOperators.INSTANCE);
    project.getFunctionSet().addAll(LanguageFunctions.FUNCTIONS);
  }

  @Test
  public void equality() throws Exception {
    assertThat(evaluate("3 = 3", null), is(true));
    assertThat(evaluate("3.0 = 3.0", null), is(true));
    assertThat(evaluate("3 = 3.0", null), is(true));
    assertThat(evaluate("'foo' = 'foo'", null), is(true));

    assertThat(evaluate("3 = 4", null), is(false));
    assertThat(evaluate("3.0 = 4.0", null), is(false));
    assertThat(evaluate("'foo' = 'bar'", null), is(false));

    // negated
    assertThat(evaluate("3 != 3", null), is(false));
    assertThat(evaluate("3.0 != 3.0", null), is(false));
    assertThat(evaluate("3 != 3.0", null), is(false));
    assertThat(evaluate("'foo' != 'foo'", null), is(false));

    assertThat(evaluate("3 != 4", null), is(true));
    assertThat(evaluate("3.0 != 4.0", null), is(true));
    assertThat(evaluate("'foo' != 'bar'", null), is(true));

    // with expected nulls (the nullable wrapper catches it)
    assertThat(evaluate("null_of('text') != 'foo'", null), nullValue());
    assertThat(evaluate("'foo' != null_of('text')", null), nullValue());

    // the null keyword is a nothing type
    // something / nothing
    assertThat(evaluate("'foo' = null", null), is(false));
    assertThat(evaluate("'foo' != null", null), is(true));
    assertThat(evaluate("'foo' <> null", null), is(true));
    // nullalbe / nothing, comparing nothing to a nullable will return true/false (rather than null)
    assertThat(evaluate("null_of('text') = null", null), is(true));
    assertThat(evaluate("null_of('text') != null", null), is(false));
    assertThat(evaluate("null_of('text') <> null", null), is(false));

    // our input formats like gpkg/shapefile don't report the nullability of their attributes
    // so we want to ensure the the logic functions can handle unexpected null values so we could
    // have a bookmark filter to remove null values
    Struct inputType = Struct.of("a", Types.TEXT, "b", Types.TEXT);
    realize(inputType, parse("a = b"));
    assertThat(realized.evaluate(Tuple.ofValues(inputType, "foo", "foo")), is(true));
    assertThat(realized.evaluate(Tuple.ofValues(inputType, "foo", null)), is(false));
    assertThat(realized.evaluate(Tuple.ofValues(inputType, null, "foo")), is(false));

    realize(inputType, parse("a != b"));
    assertThat(realized.evaluate(Tuple.ofValues(inputType, "foo", "foo")), is(false));
    assertThat(realized.evaluate(Tuple.ofValues(inputType, "foo", null)), is(true));
    assertThat(realized.evaluate(Tuple.ofValues(inputType, null, "foo")), is(true));
  }

  @Test
  public void powerOf() throws Exception {
    assertEquals(4D, evaluate("2 ** 2", null));
    assertEquals(58.617180389370496D, evaluate("3.2 ** 3.5", null));
    assertEquals(6.25D, evaluate("2.5 ** 2", null));

    //with a negative exponent
    assertEquals(1.0842021724855044E-19D, evaluate("2 ** -63", null));
  }

  @Test
  public void division() throws Exception {
    assertEquals(2D, evaluate("10.0 / 5.0", null));
    assertEquals(2D, evaluate("10.0 / 5", null));
    assertEquals(2D, evaluate("10 / 5", null));
  }

  @Test
  public void structOperations() throws Exception {
    // addition for some integers
    Struct equalToOperatedType = Struct.of("a", Types.INTEGER);
    assertEquals(tuple("{a: 2}"), evaluate("{a: 1} + {a: 1}", null));

    // make sure we normalized the struct
    Struct normalizedOperatedType = realizationContext.normalizeStruct(equalToOperatedType);
    assertEquals(equalToOperatedType, normalizedOperatedType);
    assertNotSame(equalToOperatedType, normalizedOperatedType);
    assertSame(normalizedOperatedType, realized.getResultType());

    // feed our result back in as input - make sure struct owner errors aren't all upons
    evaluate("tuple + {a: 3}", Tuple.ofValues(Struct.of("tuple", ((Tuple) evaluated).getStruct()), evaluated));
    assertEquals(tuple("{a: 5}"), evaluated);

    assertEquals(tuple("{a: 2, b: 4}"), evaluate("{a: 1, b: 3} + {a: 1, b: 1}", null));
    // can mix integers and floats
    assertEquals(tuple("{a: 2, b: 4.0}"), evaluate("{a: 1, b: 3.0} + {a: 1, b: 1}", null));

    // rhs can have extra members - these get dropped
    assertEquals(tuple("{b: 4}"), evaluate("{b: 3} + {a: 1, b: 1}", null));

    // nullable members are fine, so are wrapped numbers
    Struct typed = Struct.of("a", Nullable.INTEGER, "b", new WithinRange(Types.INTEGER, 0, 10));
    Struct scoped = typed.parent("typed");
    Tuple scope = Tuple.ofValues(scoped, Tuple.ofValues(typed, 1L, 2L));

    // gnarly - we can still get a result.  The result is nullable, but the within range wrapping is dropped
    assertEquals(
        Tuple.ofValues(Struct.of("foo", Nullable.INTEGER), 3L),
        evaluate("{foo: typed.a} + {foo: typed.b}", scope)
    );

    // we can do things other than addition...
    //  - multiplication
    assertEquals(tuple("{a: 6, b: 10.0}"), evaluate("{a: 2, b: 1} * {a: 3, b: 10.0}", null));
    //  - division
    assertEquals(tuple("{a: 10.0}"), evaluate("{a: 100} / {a: 10}", null));
    //  - equality won't match - equality applies to the whole thing
    assertEquals(Boolean.FALSE, evaluate("{a: 'cat', b: 'dog'} =  {a: 'cat', b: 'cat'}", null));
    //  - but some others will...
    assertEquals(tuple("{a: true, b: false}"), evaluate("{a: 100, b: 10} > {a: 10, b: 100}", null));

    // failure cases
    //   - incompatible types
    evaluate("{a: 12} + {a: 'foo'}", null);
    assertThat(this.realizationProblems, contains(
        ExpressionProblems.get().noSuchOperatorFunction(
            "+",
            Arrays.asList(Struct.of("a", Types.INTEGER), Struct.of("a", Types.TEXT))
        )
    ));
    //   - lhs has extra members
    evaluate("{a: 12, b: 10} + {b: 1}", null);
    assertThat(this.realizationProblems, contains(
        ExpressionProblems.get().noSuchOperatorFunction(
            "+",
            Arrays.asList(Struct.of("a", Types.INTEGER, "b", Types.INTEGER), Struct.of("b", Types.INTEGER))
        )
    ));
  }

  @Test
  public void structOpScalarOperations() throws Exception {
    // simplest case - add one to a single element
    assertEquals(tuple("{a: 2}"), evaluate("{a: 1} + 1", null));
    assertEquals(tuple("{a: 2}"), evaluate("1 + {a: 1}", null));
    assertEquals(Struct.of("a", Types.INTEGER), realized.getResultType());

    // add a single value to all elements
    assertEquals(tuple("{a: 2, b: 3, c: 4.5}"), evaluate("{a: 1, b: 2, c: 3.5} + 1", null));
    assertEquals(Struct.of("a", Types.INTEGER, "b", Types.INTEGER, "c", Types.FLOATING), realized.getResultType());

    assertEquals(tuple("{a: 1.5, b: 2.5, c: 3.5}"), evaluate("{a: 1, b: 2, c: 3} + 0.5", null));
    assertEquals(Struct.of("a", Types.FLOATING, "b", Types.FLOATING, "c", Types.FLOATING), realized.getResultType());

    // handles nullables
    Struct type = Struct.of("a", Types.INTEGER, "b", Nullable.INTEGER, "c", Nullable.FLOATING);
    Struct inputType = Struct.of("value", type);
    Struct expectedResultType = Struct.of("a", Types.FLOATING, "b", Nullable.FLOATING, "c", Nullable.FLOATING);
    assertEquals(Tuple.ofValues(expectedResultType, 1.5D, 2.5D, 4.0D), evaluate("value + 0.5",
        Tuple.ofValues(inputType, Tuple.ofValues(type, 1L, 2L, 3.5D))));
    assertEquals(expectedResultType, realized.getResultType());

    // test associativity
    assertEquals(tuple("{a: 1}"), evaluate("{a: 2} - 1", null));
    assertEquals(tuple("{a: -1}"), evaluate("2 - {a: 3}", null));

    //   - incompatible types
    evaluate("{a: 12} + 'foo'", null);
    assertThat(this.realizationProblems, contains(
        ExpressionProblems.get().noSuchOperatorFunction(
            "+",
            Arrays.asList(Struct.of("a", Types.INTEGER), Types.TEXT)
        )
    ));

    evaluate("{a: 12, b: 'foo'} + 3", null);
    assertThat(this.realizationProblems, contains(
        ExpressionProblems.get().noSuchOperatorFunction(
            "+",
            Arrays.asList(Struct.of("a", Types.INTEGER, "b", Types.TEXT), Types.INTEGER)
        )
    ));
  }

  @Test
  public void checkNumericCasting() throws Exception {
    Struct struct = Struct.of(
        "floating", Types.FLOATING,
        "smallfloat", Types.SMALLFLOAT,
        "integer", Types.INTEGER
    );
    Tuple input = Tuple.ofValues(struct, 2D, 4F, 8L);

    // int operations stay int apart from division
    assertMathsExpression("integer + integer", input, 16L, Types.INTEGER);
    assertMathsExpression("integer - integer", input, 0L, Types.INTEGER);
    assertMathsExpression("integer * integer", input, 64L, Types.INTEGER);
    assertMathsExpression("integer / integer", input, 1D, Types.FLOATING);

    // floating stays floating
    assertMathsExpression("floating + floating", input, 4D, Types.FLOATING);
    assertMathsExpression("floating - floating", input, 0D, Types.FLOATING);
    assertMathsExpression("floating * floating", input, 4D, Types.FLOATING);
    assertMathsExpression("floating / floating", input, 1D, Types.FLOATING);

    // smallfloat doesn't survive maths - changes back to floating (this is something we could come back and address)
    assertMathsExpression("smallfloat + smallfloat", input, 8D, Types.FLOATING);
    assertMathsExpression("smallfloat - smallfloat", input, 0D, Types.FLOATING);
    assertMathsExpression("smallfloat * smallfloat", input, 16D, Types.FLOATING);
    assertMathsExpression("smallfloat / smallfloat", input, 1D, Types.FLOATING);

    // mixing int and floating casts to floating (regardless of operand order)
    assertMathsExpression("integer + floating", input, 10D, Types.FLOATING);
    assertMathsExpression("integer - floating", input, 6D, Types.FLOATING);
    assertMathsExpression("integer * floating", input, 16D, Types.FLOATING);
    assertMathsExpression("integer / floating", input, 4D, Types.FLOATING);
    assertMathsExpression("floating + integer", input, 10D, Types.FLOATING);
    assertMathsExpression("floating - integer", input, -6D, Types.FLOATING);
    assertMathsExpression("floating * integer", input, 16D, Types.FLOATING);
    assertMathsExpression("floating / integer", input, 0.25D, Types.FLOATING);

    // do some null tests for good measure
    struct = struct.and(Struct.of(
        "nullint", Nullable.INTEGER,
        "nullfloat", Nullable.FLOATING,
        "nullsmallfloat", Nullable.of(Types.SMALLFLOAT)));

    // throw null in, check it doesn't blow up
    Tuple withNulls = Tuple.ofValues(struct, 2D, 4F, 8L, null, null, null);
    assertMathsExpression("integer + nullfloat", withNulls, null, Nullable.FLOATING);
    assertMathsExpression("integer + nullsmallfloat", withNulls, null, Nullable.FLOATING);
    assertMathsExpression("nullint + floating", withNulls, null, Nullable.FLOATING);

    // now with real values
    Tuple noNulls = Tuple.ofValues(struct, 2D, 4F, 8L, 10D, 12F, 14D);
    assertMathsExpression("integer + nullfloat", noNulls, 20D, Nullable.FLOATING);
    assertMathsExpression("integer + nullsmallfloat", noNulls, 22D, Nullable.FLOATING);
    assertMathsExpression("nullint + floating", noNulls, 12D, Nullable.FLOATING);
  }

  @Test
  public void canCompareSmallFloats() throws Exception {
    Struct struct = Struct.of(
      "smallfloat", Types.SMALLFLOAT
    );
    Tuple input = Tuple.ofValues(struct, 4F);
    // same value
    assertThat(evaluate("smallfloat > smallfloat", input), equalTo(false));
    assertThat(evaluate("smallfloat >= smallfloat", input), equalTo(true));

    // mixed types
    assertThat(evaluate("smallfloat > 0", input), equalTo(true));
    assertThat(evaluate("smallfloat = 4", input), equalTo(true));
    assertThat(evaluate("smallfloat < 4", input), equalTo(false));

    assertThat(evaluate("smallfloat > 4.0", input), equalTo(false));
    assertThat(evaluate("smallfloat > 3.999", input), equalTo(true));
  }

  private void assertMathsExpression(String expression, Tuple input, Number expected, Type resultType) {
    assertThat(evaluate(expression, input), equalTo(expected));
    assertThat(realized.getResultType(), equalTo(resultType));
  }
}
