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

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

import java.math.BigDecimal;
import java.util.Arrays;

import org.geotools.filter.FilterFactoryImpl;
import org.junit.Before;
import org.junit.Test;
import org.geotools.api.filter.spatial.Equals;

import org.locationtech.jts.geom.Coordinate;
import org.locationtech.jts.geom.Geometry;
import org.locationtech.jts.geom.GeometryFactory;
import org.locationtech.jts.geom.Point;

import nz.org.riskscape.engine.Assert;
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.Types;
import nz.org.riskscape.engine.typeset.TypeSet;
import nz.org.riskscape.rl.ExpressionParser;

public class StructMemberAccessExpressionUnitTest {

  Struct struct = Struct.of("text", Types.TEXT);
  Struct withGeom = Struct.of("point", Types.GEOMETRY);


  Struct childStruct1 = Struct.of("text", Types.TEXT, "number", Types.INTEGER);

  Struct childStruct2 = Struct.of("words", Types.TEXT, "decimals", Types.DECIMAL);

  Struct nullable = Struct.of("nullable-value", Nullable.TEXT);

  Struct parentStruct = Struct.of("child1", childStruct1, "child2", childStruct2);
  Struct nullableParentStruct = Struct.of("nullableChild", Nullable.of(nullable));
  Struct linkedParentStruct;

  TypeSet typeSet;

  @Before
  public void setup() {
    typeSet = new TypeSet();
    typeSet.add("linked-child1", childStruct1);
    typeSet.add("linked-child2", childStruct2);
    linkedParentStruct = Struct.of("child1", typeSet.getLinkedType("linked-child1"))
        .and("child2", typeSet.getLinkedType("linked-child2"));
  }

  @Test
  public void canAccessNullablePropertiesFromAStruct() {
    StructMemberAccessExpression expr = StructMemberAccessExpression.
        build(nullable, Arrays.asList("nullable-value")).get();

    assertEquals("foo", expr.evaluate(new Tuple(nullable).set("nullable-value", "foo")));
    assertNull(expr.evaluate(new Tuple(nullable).set("nullable-value", null)));

    assertEquals("foo", expr.evaluate(new Tuple(nullable).set("nullable-value", "foo"), String.class));
    assertNull(expr.evaluate(new Tuple(nullable).set("nullable-value", null), String.class));
  }

  @Test
  public void canAccessNestedNullablePropertiesFromAStruct() {
    StructMemberAccessExpression parentExpr =
        StructMemberAccessExpression.build(nullableParentStruct, Arrays.asList("nullableChild")).get();

    StructMemberAccessExpression childExpr = StructMemberAccessExpression.build(
        nullableParentStruct, Arrays.asList("nullableChild", "nullable-value")).get();

    Tuple value = new Tuple(nullableParentStruct).set("nullableChild", null);
    assertNull(parentExpr.evaluate(value));
    assertNull(childExpr.evaluate(value));

    Tuple child = new Tuple(nullable).set("nullable-value", null);
    value = new Tuple(nullableParentStruct).set("nullableChild", child);
    assertEquals(child, parentExpr.evaluate(value));
    assertNull(childExpr.evaluate(value));

    child = new Tuple(nullable).set("nullable-value", "value1");
    value = new Tuple(nullableParentStruct).set("nullableChild", child);
    assertEquals(child, parentExpr.evaluate(value));
    assertEquals("value1", childExpr.evaluate(value));
  }

  @Test
  public void canAccessPropertiesFromAStruct() {
    StructMemberAccessExpression expr = StructMemberAccessExpression.build(struct, Arrays.asList("text")).get();
    assertEquals("foo", expr.evaluate(new Tuple(struct).set("text", "foo")));
    assertEquals("sweet dealio", expr.evaluate(new Tuple(struct).set("text", "sweet dealio")));
  }

  @Test
  public void canQueryTypesFromAStruct() {
    StructMemberAccessExpression expr = StructMemberAccessExpression.build(struct, Arrays.asList("text")).get();
    assertEquals(Types.TEXT, expr.evaluateType(struct).get());
  }

  @Test
  public void canAccessPropertiesFromAStructOfStructs() {
    Tuple value = new Tuple(parentStruct)
        .set("child1", new Tuple(childStruct1).set("text", "text 1").set("number", "4"))
        .set("child2", new Tuple(childStruct2).set("words", "these are my words").set("decimals",
            new BigDecimal("1.054")));

    StructMemberAccessExpression expr1 = StructMemberAccessExpression.
        build(parentStruct, Arrays.asList("child1", "text")).get();

    assertEquals("text 1", expr1.evaluate(value));

    StructMemberAccessExpression expr2 = StructMemberAccessExpression
        .build(parentStruct, Arrays.asList("child2", "decimals")).get();

    assertEquals(new BigDecimal("1.054"), expr2.evaluate(value));
  }

  @Test
  public void canQueryTypeFromAStructOfLinkedStructs() {
    StructMemberAccessExpression expr1 = StructMemberAccessExpression
        .build(linkedParentStruct, Arrays.asList("child1", "text")).get();

    assertEquals(Types.TEXT, expr1.evaluateType(linkedParentStruct).get());

    StructMemberAccessExpression expr2 = StructMemberAccessExpression
        .build(linkedParentStruct, Arrays.asList("child2", "decimals")).get();

    assertEquals(Types.DECIMAL, expr2.evaluateType(linkedParentStruct).get());
  }

  @Test
  public void canBeUsedWithOtherSpatialFunctionsFromOpenGis() {
    withGeom = Struct.of("geom", Types.GEOMETRY);
    Struct alsoGeom = Struct.of("geometry", Types.GEOMETRY);
    Struct parent = withGeom.parent("lhs").add("rhs", alsoGeom);

    GeometryFactory factory = new GeometryFactory();
    Point pointy = factory.createPoint(new Coordinate(1, 2));
    Point pointed = factory.createPoint(new Coordinate(1, 2));

    Tuple value = new Tuple(parent)
        .set("lhs", new Tuple(withGeom).set("geom", pointy))
        .set("rhs", new Tuple(alsoGeom).set("geometry", pointed));

    StructMemberAccessExpression expr1 = StructMemberAccessExpression.
        build(parent, Arrays.asList("lhs", "geom")).get();
    StructMemberAccessExpression expr2 = StructMemberAccessExpression.
        build(parent, Arrays.asList("rhs", "geometry")).get();
    FilterFactoryImpl filterFactory = new FilterFactoryImpl();
    Equals equals = filterFactory.equal(expr1, expr2);

    assertTrue(equals.evaluate(value));

    Tuple nonMatching = new Tuple(parent)
        .set("lhs", new Tuple(withGeom).set("geom", pointy))
        .set("rhs", new Tuple(alsoGeom).set("geometry", factory.createPoint(new Coordinate(2, 1))));

    assertFalse(equals.evaluate(nonMatching));
  }

  @Test
  public void structAccessExpressionEqualsIfMembersAreSame() {
    StructMemberAccessExpression e1 = StructMemberAccessExpression.build(
        parentStruct, Arrays.asList("child1", "text")).get();
    StructMemberAccessExpression e2 = StructMemberAccessExpression.build(
        parentStruct, Arrays.asList("child1", "text")).get();
    assertEquals(e1, e2);
    assertFalse(e1 == e2);
  }

  @Test
  public void setValueWillSetAnObjectToATuple() throws Exception {
    struct = Struct.of("foo", Types.TEXT, "bar", Types.INTEGER);
    Tuple tuple = Tuple.ofValues(struct, "text", 1L);
    new StructAccessExpression("bar").getExpressionFor(struct).ifPresent(sa -> sa.setValue(tuple, 2L));

    assertThat(tuple.fetch("bar"), equalTo(2L));
  }

  @Test
  public void setWillFailIfTupleIsOfTheWrongType() throws Exception {
    struct = Struct.of("foo", Types.TEXT, "bar", Types.INTEGER);
    Tuple tuple = Tuple.ofValues(struct.replace("foo", Types.TEXT), "text", 1L);

    Assert.assertThrows(IllegalArgumentException.class, () -> {
      new StructAccessExpression("bar").getExpressionFor(struct).ifPresent(sa -> sa.setValue(tuple, 2L));
    });
  }

  @Test
  public void setValueCanSetAnObjectToANestedTuple() throws Exception {
    struct = Struct.of("foo", Types.TEXT, "bar", Types.INTEGER);
    Struct parent = struct.parent("child");
    Tuple tuple = Tuple.ofValues(parent, Tuple.ofValues(struct, "text", 1L));

    new StructAccessExpression("child.foo").getExpressionFor(parent).ifPresent(sa -> sa.setValue(tuple, "hi mom"));
    assertThat(tuple.fetchChild("child").fetch("foo"), equalTo("hi mom"));
  }

  @Test
  public void setValueCanSetAnObjectToANestedTupleWithNullsInTheWay() throws Exception {
    struct = Struct.of("foo", Types.TEXT, "bar", Types.INTEGER);
    Struct parent = struct.parent("child");
    Tuple tuple = Tuple.ofValues(parent);

    new StructAccessExpression("child.foo").getExpressionFor(parent).ifPresent(sa -> sa.setValue(tuple, "hi mom"));
    assertThat(tuple.fetchChild("child").fetch("foo"), equalTo("hi mom"));
  }

  @Test
  public void canEvaluateGeometryFromATupleWithSingleGeometryMember() throws Exception {
    struct = Struct.of("foo", Types.TEXT, "bar", withGeom);
    StructMemberAccessExpression expr = new StructAccessExpression("bar").getExpressionFor(struct).get();
    GeometryFactory gf = new GeometryFactory();
    Point point = gf.createPoint(new Coordinate(0, 1));
    Tuple toEvaluate = Tuple.ofValues(struct, "bar", Tuple.ofValues(withGeom, point));

    assertSame(toEvaluate.fetchChild("bar"), expr.evaluate(toEvaluate));
    assertSame(point, expr.evaluate(toEvaluate, Geometry.class));
  }

  @Test
  public void canConvertToRiskscapeELExpression() {
    StructMemberAccessExpression expr = StructMemberAccessExpression.build(struct, Arrays.asList("text")).get();
    assertThat(expr.toExpression(), is(ExpressionParser.parseString("text")));

    expr = StructMemberAccessExpression.build(parentStruct, Arrays.asList("child1", "number")).get();
    assertThat(expr.toExpression(), is(ExpressionParser.parseString("child1.number")));
  }

  @Test(expected = ClassCastException.class)
  public void failsToEvaluateGeometryFromATupleWithNoGeometryMember() throws Exception {
    StructMemberAccessExpression expr = new StructAccessExpression("text").getExpressionFor(struct).get();
    expr.evaluate(Tuple.of(struct), Geometry.class);
  }

  @Test(expected = ClassCastException.class)
  public void failsToEvaluateGeometryFromATupleWithManyGeometryMember() throws Exception {
    withGeom = Struct.of("point1", Types.GEOMETRY, "point2", Types.GEOMETRY);
    struct = Struct.of("foo", Types.TEXT, "bar", withGeom);
    StructMemberAccessExpression expr = new StructAccessExpression("bar").getExpressionFor(struct).get();
    expr.evaluate(Tuple.of(struct), Geometry.class);
  }


}
