/*
 * 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.Assert.*;
import static nz.org.riskscape.engine.Matchers.*;
import static nz.org.riskscape.engine.geo.GeometryUtils.*;
import static org.hamcrest.Matchers.*;
import static org.junit.Assert.*;

import org.geotools.api.referencing.crs.CoordinateReferenceSystem;
import org.geotools.referencing.CRS;
import org.hamcrest.Description;
import org.hamcrest.Matcher;
import org.hamcrest.TypeSafeDiagnosingMatcher;
import org.junit.Before;
import org.junit.Test;
import org.locationtech.jts.geom.Geometry;
import org.locationtech.jts.geom.LineString;
import org.locationtech.jts.geom.Point;
import org.locationtech.jts.geom.Polygon;
import org.locationtech.jts.geom.TopologyException;

import nz.org.riskscape.engine.ArgsProblems;
import nz.org.riskscape.engine.CrsHelper;
import nz.org.riskscape.engine.GeometryProblems;
import nz.org.riskscape.engine.RiskscapeException;
import nz.org.riskscape.engine.Tuple;
import nz.org.riskscape.engine.function.FunctionArgument;
import nz.org.riskscape.engine.function.RiskscapeFunction;
import nz.org.riskscape.engine.gt.GeometryHelper;
import nz.org.riskscape.engine.types.CoercionException;
import nz.org.riskscape.engine.types.Nullable;
import nz.org.riskscape.engine.types.Referenced;
import nz.org.riskscape.engine.types.Struct;
import nz.org.riskscape.engine.types.Types;
import nz.org.riskscape.problem.Problem;
import nz.org.riskscape.problem.ProblemSink;

@SuppressWarnings("unchecked")
public class GeometryFunctionsTest extends BaseExpressionRealizerTest implements CrsHelper {

  GeometryHelper nzHelper = GeometryHelper.nzTransverseMercatorWithOriginShift(project.getSridSet());
  GeometryHelper latLongHelper = GeometryHelper.latLong(project.getSridSet());

  @Before
  public void setup() {
    GeometryFunctions geometryFunctions = new GeometryFunctions(engine);
    project.getFunctionSet().addAll(geometryFunctions.getPredicates());
    project.getFunctionSet().addAll(geometryFunctions.getFunctions());
    project.getFunctionSet().addAll(LanguageFunctions.FUNCTIONS);
  }

  Struct type = Struct.of("foo", Types.GEOMETRY, "bar", Types.GEOMETRY, "distance", Types.FLOATING);

  @Test
  public void predicateFunctionFailsWhenWrongArgs() {
    RiskscapeFunction intersectsFunction = project.getFunctionSet().get("intersects", ProblemSink.DEVNULL);

    // wrong number of arguments
    assertThat(realizeOnly("intersects(foo)", type), failedResult(
        hasAncestorProblem(is(ArgsProblems.get().wrongNumber(2, 1)))
    ));
    assertThat(realizeOnly("intersects(foo, bar, 'baz')", type), failedResult(
        hasAncestorProblem(is(ArgsProblems.get().wrongNumber(2, 3)))
    ));

    // Wrong argument types, e.g not a geometry
    assertThat(realizeOnly("intersects(foo, 'bar')", type), failedResult(
        hasAncestorProblem(is(ArgsProblems.mismatch(intersectsFunction.getArguments().get(1), Types.TEXT)))
    ));
    assertThat(realizeOnly("intersects('foo', bar)", type), failedResult(
        hasAncestorProblem(is(ArgsProblems.mismatch(intersectsFunction.getArguments().get(0), Types.TEXT)))
    ));
  }

  @Test
  public void predicateFunctionsCoerceToGeom() {
    Struct childType = Struct.of("geom", Types.GEOMETRY);
    Struct myType = Struct.of("c1", childType, "c2", Nullable.of(childType));

    realize(myType, parse("intersects(c1, c2)"));
    assertThat(realized.getResultType(), is(Nullable.BOOLEAN));

    assertFalse((boolean)realized.evaluate(Tuple.ofValues(myType,
        Tuple.ofValues(childType, latLongHelper.point(0, 0)),
        Tuple.ofValues(childType, latLongHelper.point(1, 1))
    )));
    assertTrue((boolean)realized.evaluate(Tuple.ofValues(myType,
        Tuple.ofValues(childType, latLongHelper.point(1, 1)),
        Tuple.ofValues(childType, latLongHelper.point(1, 1))
    )));

    assertNull(realized.evaluate(Tuple.ofValues(myType,
        Tuple.ofValues(childType, latLongHelper.point(1, 1))
    )));
  }

  @Test
  public void predicateFunctionWillWorkWithEquivalentCrss() {
    CoordinateReferenceSystem crs1 = crsFromWkt("EPSG32702.wkt");
    CoordinateReferenceSystem crs2 = crsFromWkt("EPSG32702-GCS_WGS.wkt");

    // let's sanity check the input crs. They should not be equal objects, but should be equal ignoring
    // metadata
    assertNotEquals(crs1, crs2);
    assertTrue(CRS.equalsIgnoreMetadata(crs1, crs2));

    Struct inputType = Struct.of(
        "lhs", Referenced.of(Types.GEOMETRY, crs1),
        "rhs", Referenced.of(Types.GEOMETRY, crs2)
    );

    GeometryHelper crs1Helper = GeometryHelper.create(project.getSridSet(), crs1);
    GeometryHelper crs2Helper = GeometryHelper.create(project.getSridSet(), crs2);

    realize(inputType, parse("intersects(lhs, rhs)"));
    assertThat(realized.getResultType(), is(Types.BOOLEAN));

    assertTrue((boolean)realized.evaluate(Tuple.ofValues(inputType,
        crs1Helper.box(10, 10, 100, 100),
        crs2Helper.point(50, 50)
    )));
  }

  @Test
  public void predicateFunctionFailsToRealizeIfCrsMismatch() {
    // if the CRS's from referenced types are mismatched then a predicate function will fail to realize
    Struct inputType = Struct.of(
        "lhs", Struct.of("geom", Referenced.of(Types.GEOMETRY, nzHelper.getCrs())),
        "rhs", Struct.of("geom", Referenced.of(Types.GEOMETRY, latLongHelper.getCrs()))
    );
    assertThat(realizeOnly("intersects(lhs.geom, rhs.geom)", inputType), failedResult(hasAncestorProblem(
        is(GeometryProblems.get().mismatchedCrs(nzHelper.getCrs(), latLongHelper.getCrs())))
    ));
    assertThat(realizeOnly("intersects(lhs, rhs)", inputType), failedResult(hasAncestorProblem(
        is(GeometryProblems.get().mismatchedCrs(nzHelper.getCrs(), latLongHelper.getCrs())))
    ));
  }

  @Test
  public void predicateFunctionWillFailOnEvalIfCrsMismatch() {
    Struct inputType = Struct.of("lhs", Types.GEOMETRY, "rhs", Types.GEOMETRY);

    RealizedExpression re = realizeOnly("intersects(lhs, rhs)", inputType).get();
    // sanity check that the realized expression is okay
    assertTrue((boolean)re.evaluate(
        Tuple.ofValues(inputType, latLongHelper.box(0, 0, 2, 2), latLongHelper.point(1, 1))));

    EvalException ex = assertThrows(EvalException.class,
        () -> re.evaluate(Tuple.ofValues(inputType, nzHelper.box(0, 0, 2, 2), latLongHelper.point(1, 1))));
    assertThat(ex.getProblem(), hasAncestorProblem(
        is(GeometryProblems.get().mismatchedCrs(nzHelper.getCrs(), latLongHelper.getCrs()))
    ));
  }

  @Test
  public void testIntersects() {
    assertPredicate(false, "intersects(foo, bar)", latLongHelper.point(0, 0), latLongHelper.point(1, 1));
    assertPredicate(false, "intersects(foo, bar)", latLongHelper.box(0, 0, 2, 2), latLongHelper.point(3, 3));
    assertPredicate(true, "intersects(foo, bar)", latLongHelper.point(1, 1), latLongHelper.point(1, 1));
    assertPredicate(true, "intersects(foo, bar)", latLongHelper.box(0, 0, 2, 2), latLongHelper.point(1, 1));
    assertThat(realized.getResultType(), is(Types.BOOLEAN));
  }

  @Test
  public void testDisjoint() throws Exception {
    assertPredicate(true, "disjoint(foo, bar)", latLongHelper.point(0, 0), latLongHelper.point(1, 1));
    assertPredicate(true, "disjoint(foo, bar)", latLongHelper.box(0, 0, 2, 2), latLongHelper.point(3, 3));
    assertPredicate(false, "disjoint(foo, bar)", latLongHelper.point(1, 1), latLongHelper.point(1, 1));
    assertPredicate(false, "disjoint(foo, bar)", latLongHelper.box(0, 0, 2, 2), latLongHelper.point(1, 1));
    assertThat(realized.getResultType(), is(Types.BOOLEAN));
  }

  @Test
  public void testContains() throws Exception {
    assertPredicate(true, "contains(foo, bar)", latLongHelper.point(0, 0), latLongHelper.point(0, 0));
    assertPredicate(false, "contains(foo, bar)", latLongHelper.point(0, 0), latLongHelper.point(1, 1));
    assertPredicate(true, "contains(foo, bar)", latLongHelper.box(0, 0, 2, 2), latLongHelper.point(1, 1));
    assertPredicate(false, "contains(foo, bar)", latLongHelper.box(0, 0, 2, 2), latLongHelper.box(1, 1, 3, 3));
    assertThat(realized.getResultType(), is(Types.BOOLEAN));
  }

  @Test
  public void textBbox() throws Exception {
    //bbox takes float args for x/y
    assertPredicate(false, "bbox(foo, 1.0, 1.0, 3.0, 3.0)", latLongHelper.point(0, 0), latLongHelper.point(0, 0));
    assertPredicate(true, "bbox(foo, 1.0, 1.0, 3.0, 3.0)", latLongHelper.point(2, 2), latLongHelper.point(0, 0));
    assertThat(realized.getResultType(), is(Types.BOOLEAN));
  }

  @Test
  public void textBbox_WithNumericAdaptation() throws Exception {
    //use ints to test number adaptation
    assertPredicate(false, "bbox(foo, 1, 1.0, 3, 3.0)", latLongHelper.point(0, 0), latLongHelper.point(0, 0));
    assertPredicate(true, "bbox(foo, 1, 1.0, 3, 3.0)", latLongHelper.point(2, 2), latLongHelper.point(0, 0));

    //ensure that numeric adaptation doesn't change missing function behaviour
    //this example is missing fourth numeric arg
    evaluate("bbox(foo, 1, 1.0, 3)", Tuple.ofValues(type, latLongHelper.point(0, 0), latLongHelper.point(0, 0)));
    assertThat(realizationProblems, contains(isProblem(MissingFunctionException.class)));
  }

  @Test
  public void testWithin() throws Exception {
    assertPredicate(true, "within(foo, bar)", latLongHelper.point(0, 0), latLongHelper.point(0, 0));
    assertPredicate(false, "within(foo, bar)", latLongHelper.point(0, 0), latLongHelper.point(1, 1));
    assertPredicate(false, "within(foo, bar)", latLongHelper.box(0, 0, 2, 2), latLongHelper.point(0, 0));
    assertPredicate(false, "within(foo, bar)", latLongHelper.box(0, 0, 2, 2), latLongHelper.box(1, 1, 3, 3));
    assertThat(realized.getResultType(), is(Types.BOOLEAN));
  }

  @Test
  public void testTouches() throws Exception {
    assertPredicate(true, "touches(foo, bar)", latLongHelper.box(0, 0, 2, 2), latLongHelper.box(2, 2, 3, 3));
    assertPredicate(false, "touches(foo, bar)", latLongHelper.box(0, 0, 2, 2), latLongHelper.box(1, 1, 3, 3));
    assertThat(realized.getResultType(), is(Types.BOOLEAN));
  }

  @Test
  public void testCrosses() throws Exception {
    assertPredicate(true, "crosses(foo, bar)", latLongHelper.line(1, 0, 1, 2), latLongHelper.line(0, 1, 2, 1));
    assertPredicate(false, "crosses(foo, bar)", latLongHelper.line(1, 0, 1, 2), latLongHelper.line(1, 0, 1, 4));
    assertThat(realized.getResultType(), is(Types.BOOLEAN));
  }

  @Test
  public void testOverlaps() throws Exception {
    assertPredicate(true, "overlaps(foo, bar)", latLongHelper.box(0, 0, 2, 2), latLongHelper.box(1, 1, 3, 3));
    assertPredicate(false, "overlaps(foo, bar)", latLongHelper.box(0, 0, 2, 2), latLongHelper.box(1, 1, 2, 2));
    assertThat(realized.getResultType(), is(Types.BOOLEAN));
  }

  @Test
  public void testEquals() throws Exception {
    assertPredicate(true, "equals(foo, bar)", latLongHelper.box(0, 0, 2, 2), latLongHelper.box(2, 2, 0, 0));
    assertPredicate(false, "equals(foo, bar)", latLongHelper.box(0, 0, 0, 0), latLongHelper.point(0, 0));
    assertThat(realized.getResultType(), is(Types.BOOLEAN));
  }

  @Test
  public void testDwithin() throws Exception {
    assertPredicate(true, "dwithin(foo, bar, distance)",
        latLongHelper.point(0, 0), latLongHelper.point(1, 1), METRES_PER_DEGREE * 2);
    assertPredicate(false, "dwithin(foo, bar, distance)",
        latLongHelper.point(0, 0), latLongHelper.point(1, 1), METRES_PER_DEGREE / 4);
    assertThat(realized.getResultType(), is(Types.BOOLEAN));
  }

  @Test
  public void testBeyond() throws Exception {
    assertPredicate(false, "beyond(foo, bar, distance)",
        latLongHelper.point(0, 0), latLongHelper.point(1, 1), METRES_PER_DEGREE * 2);
    assertPredicate(true, "beyond(foo, bar, distance)",
        latLongHelper.point(0, 0), latLongHelper.point(1, 1), METRES_PER_DEGREE / 4);
    assertThat(realized.getResultType(), is(Types.BOOLEAN));
  }

  @Test
  public void testBeyond2() {
    // testing the argument checking and coercion of beyond.
    Struct inputType = Struct.of("foo", Types.GEOMETRY, "bar", Nullable.GEOMETRY);
    Point p1 = nzHelper.point(100, 100);
    Point p2 = nzHelper.point(100, 200);

    // null distance
    assertThat(evaluate("beyond(foo, foo, null_of('integer'))", Tuple.ofValues(inputType, p1, p2)), nullValue());
    assertThat(realized.getResultType(), is(Nullable.BOOLEAN));
    assertThat(evaluate("beyond(foo, foo, null_of('floating'))", Tuple.ofValues(inputType, p1, p2)), nullValue());
    assertThat(realized.getResultType(), is(Nullable.BOOLEAN));

    assertThat(evaluate("beyond(foo, bar, 50.0)", Tuple.ofValues(inputType, p1, p2)), is(true));
    assertThat(evaluate("beyond(foo, bar, 150.0)", Tuple.ofValues(inputType, p1, p2)), is(false));

    // int -> float coerce
    assertThat(evaluate("beyond(foo, bar, 50)", Tuple.ofValues(inputType, p1, p2)), is(true));
    assertThat(evaluate("beyond(foo, bar, 150)", Tuple.ofValues(inputType, p1, p2)), is(false));
    assertThat(realized.getResultType(), is(Nullable.BOOLEAN));
    // rhs geom is null
    assertThat(evaluate("beyond(foo, bar, 50)", Tuple.ofValues(inputType, p1)), nullValue());

    FunctionArgument distanceArg = project.getFunctionSet().get("beyond", ProblemSink.DEVNULL).getArguments().get(2);
    assertThat(realizeOnly("beyond(foo, bar, 'ten')", inputType), failedResult(
      hasAncestorProblem(is(ArgsProblems.mismatch(distanceArg, Types.TEXT))
    )));

    assertThat(realizeOnly("beyond(foo, bar)", inputType), failedResult(
      hasAncestorProblem(is(ArgsProblems.get().wrongNumber(3, 2))
    )));

    assertThat(realizeOnly("beyond(foo, bar, 20, 'ten')", inputType), failedResult(
      hasAncestorProblem(is(ArgsProblems.get().wrongNumber(3, 4))
    )));
  }

  @Test
  public void testRelate() throws Exception {
    String disjointMatrix = "FF*FF****";
    assertPredicate(true, "relate(foo, bar, '" + disjointMatrix + "')",
        latLongHelper.point(0, 0), latLongHelper.point(1, 1));
    assertPredicate(false, "relate(foo, bar, '" + disjointMatrix + "')",
        latLongHelper.point(0, 0), latLongHelper.point(0, 0));
    assertThat(realized.getResultType(), is(Types.BOOLEAN));
  }

  @Test
  public void testRelate2() {
    // testing the argument checking and coercion of beyond.
    Struct inputType = Struct.of("foo", Types.GEOMETRY, "bar", Nullable.GEOMETRY);
    Point p1 = latLongHelper.point(0, 0);
    Point p2 = latLongHelper.point(1, 1);

    // null distance
    assertThat(evaluate("relate(foo, foo, null_of('text'))", Tuple.ofValues(inputType, p1, p2)), nullValue());
    assertThat(realized.getResultType(), is(Nullable.BOOLEAN));

    assertThat(evaluate("relate(foo, bar, 'FF*FF****')", Tuple.ofValues(inputType, p1, p2)), is(true));

    // rhs geom is null
    assertThat(evaluate("relate(foo, bar, 'FF*FF****')", Tuple.ofValues(inputType, p1)), nullValue());

    FunctionArgument distanceArg = project.getFunctionSet().get("relate", ProblemSink.DEVNULL).getArguments().get(2);
    assertThat(realizeOnly("relate(foo, bar, 20)", inputType), failedResult(
      hasAncestorProblem(is(ArgsProblems.mismatch(distanceArg, Types.INTEGER))
    )));

    assertThat(realizeOnly("relate(foo, bar)", inputType), failedResult(
      hasAncestorProblem(is(ArgsProblems.get().wrongNumber(3, 2))
    )));

    assertThat(realizeOnly("relate(foo, bar, 'FF*FF****', 'ten')", inputType), failedResult(
      hasAncestorProblem(is(ArgsProblems.get().wrongNumber(3, 4))
    )));
  }

  @Test
  public void testRelate_EvalException_Bad_DisjointMatrix() throws Exception {
    //pattern is too short
    EvalException ee = assertThrows(EvalException.class,
        () -> evaluate("relate(foo, bar, 'FF*FF***')", Tuple.ofValues(type,
            latLongHelper.point(0, 0), latLongHelper.point(1, 1))));
    Throwable rootCause = ee.getRootCause();
    assertThat(rootCause, instanceOf(IllegalArgumentException.class));

    //pattern is too short
    ee = assertThrows(EvalException.class,
        () -> evaluate("relate(foo, bar, 'FF*FF*****')", Tuple.ofValues(type,
            latLongHelper.point(0, 0), latLongHelper.point(1, 1))));
  }

  @Test
  public void testCentroid() throws Exception {
    Tuple input = Tuple.ofValues(type, latLongHelper.box(0, 0, 2, 2), latLongHelper.point(2, 2));

    assertThat(evaluate("centroid(foo)", input), is(latLongHelper.point(1, 1)));
    assertThat(evaluate("centroid(bar)", input), is(latLongHelper.point(2, 2)));
    assertThat(realized.getResultType(), is(Types.POINT));
  }

  @Test
  public void centroidRetainsReferencedInReturnType() throws Exception {
    type = Struct.of(
        "foo", Referenced.of(Types.GEOMETRY, latLongHelper.getCrs()),
        "bar", Referenced.of(Types.GEOMETRY, nzHelper.getCrs())
    );
    Tuple input = Tuple.ofValues(type, latLongHelper.box(0, 0, 2, 2), nzHelper.point(2, 2));

    assertThat(evaluate("centroid(foo)", input), is(latLongHelper.point(1, 1)));
    assertThat(realized.getResultType(), is(Referenced.of(Types.POINT, latLongHelper.getCrs())));
    assertThat(evaluate("centroid(bar)", input), is(nzHelper.point(2, 2)));
    assertThat(realized.getResultType(), is(Referenced.of(Types.POINT, nzHelper.getCrs())));
  }

  @Test
  public void testIntersection() throws Exception {
    //polygon/point -> latLongHelper.point
    assertThat((Geometry)evaluate("intersection(foo, bar)", Tuple.ofValues(type,
        latLongHelper.box(0, 0, 2, 2), latLongHelper.point(2, 2))),
        isGeometry(latLongHelper.point(2, 2)));

    //polygon/polygon -> polygon
    assertThat((Geometry)evaluate("intersection(foo, bar)",
        Tuple.ofValues(type, latLongHelper.box(0, 0, 2, 2), latLongHelper.box(1, 0, 3, 2))),
        isGeometry(latLongHelper.box(1, 0, 2, 2)));

    //line/polygon -> latLongHelper.line
    assertThat((Geometry)evaluate("intersection(foo, bar)",
        Tuple.ofValues(type, latLongHelper.line(0, 0, 3, 3), latLongHelper.box(1, 1, 2, 2))),
        isGeometry(latLongHelper.line(1, 1, 2, 2)));

    assertThat(evaluate("intersection(foo, bar)", Tuple.ofValues(type,
        latLongHelper.point(1, 1), latLongHelper.point(1, 1))), is(latLongHelper.point(1, 1)));
    assertThat(realized.getResultType(), is(Types.GEOMETRY));

    assertThat(
        evaluate("intersection(foo, bar)", Tuple.ofValues(type, latLongHelper.point(1, 1), latLongHelper.point(2, 2))),
        is(latLongHelper.emptyPoint())
    );
  }

  @Test
  public void intersection_Returns_EmptyGeometries_WhenGeometriesDoNotIntersect() {
    //Unlike JTS which returns an empty geometry for non intesections, we want null to be returned.
    assertThat(evaluate("intersection(foo, bar)", Tuple.ofValues(type,
        latLongHelper.point(1, 1), latLongHelper.point(2, 1))),
        is(latLongHelper.emptyPoint()));

    assertThat(evaluate("intersection(foo, bar)", Tuple.ofValues(type,
        latLongHelper.line(1, 1, 1, 3), latLongHelper.line(2, 1, 2, 3))),
        is(latLongHelper.emptyLine()));


    assertThat(evaluate("intersection(foo, bar)", Tuple.ofValues(type,
        latLongHelper.box(1, 1, 3, 3), latLongHelper.box(4, 4, 5, 5))),
        is(latLongHelper.emptyBox()));
  }

  @Test
  public void intersection_Can_Result_In_Multi_Geometries() throws Exception {
    //polygon/point -> latLongHelper.point
    assertThat((Geometry)evaluate("intersection(foo, bar)",
        Tuple.ofValues(type, latLongHelper.box(0, 0, 1, 4), latLongHelper.line(
            latLongHelper.toCoordinate(0, 0), latLongHelper.toCoordinate(2, 2), latLongHelper.toCoordinate(0, 4)))),
        isGeometry(latLongHelper.multiLine(latLongHelper.line(0, 0, 1, 1), latLongHelper.line(1, 3, 0, 4))));

    assertThat(realized.getResultType(), is(Types.GEOMETRY));
  }

  @Test
  public void testCreatePoint() throws Exception {
    assertThat(evaluate("create_point(1.0, 1.0)", null), is(latLongHelper.point(1,1)));

    //integers are adapted to floating automagically
    assertThat(evaluate("create_point(1, 1)", null), is(latLongHelper.point(1,1)));
    assertThat(evaluate("create_point(1, 1.0)", null), is(latLongHelper.point(1,1)));
    assertThat(evaluate("create_point(1.0, 1)", null), is(latLongHelper.point(1,1)));

    assertThat(evaluate("create_point(-100.0, -300.0)", null), is(latLongHelper.point(-100,-300)));
    assertThat(realized.getResultType(), is(Types.POINT));
  }

  @Test
  public void testCreatePoint_Text() throws Exception {
    assertThat(evaluate("create_point('1.0', '1.0')", null), is(latLongHelper.point(1,1)));
    assertThat(evaluate("create_point('1', '1')", null), is(latLongHelper.point(1,1)));
    assertThat(realized.getResultType(), is(Types.POINT));

    assertThat(assertThrows(EvalException.class, () -> evaluate("create_point('cat', '1')", null)).getCause(),
        instanceOf(CoercionException.class));

    assertThat(assertThrows(EvalException.class, () -> evaluate("create_point('1', '')", null)).getCause(),
        instanceOf(CoercionException.class));
  }

  @Test
  public void testGeomFromWKT() throws Exception {
    assertThat(evaluate("geom_from_wkt('POINT(1 1)')", null), is(latLongHelper.point(1, 1)));
    assertThat(evaluate("geom_from_wkt('LINESTRING(1 1, 1 3)')", null), instanceOf(LineString.class));
    assertThat(evaluate("geom_from_wkt('POLYGON((1 1, 1 2, 2 2, 2 1, 1 1))')", null), instanceOf(Polygon.class));
    assertThat(realized.getResultType(), is(Types.GEOMETRY));

    //Missing the ending )
    EvalException ex = assertThrows(EvalException.class, () -> evaluate("geom_from_wkt('POINT(1 1')", null));
    assertThat(ex.getCause(), instanceOf(RiskscapeException.class));
    RiskscapeException re = (RiskscapeException) ex.getCause();
    assertThat(re.getProblem(), equalIgnoringChildren(GeometryProblems.get().badWkt("POINT(1 1")));
  }

  @Test
  public void testGeomFromWKT_WithCrs() throws Exception {

    // If CRS is not specified then SRID should be 0 (unknown)
    Geometry geom = (Geometry) evaluate("geom_from_wkt('POINT(1 1)')", null);
    assertThat(geom.getSRID(), is(0));
    assertThat(geom, is(latLongHelper.point(1, 1)));

    CoordinateReferenceSystem epsg2193 = CRS.decode("epsg:2193");

    assertThat(
        (Geometry) evaluate("geom_from_wkt('POINT(1 1)', 'epsg:2193')", null),
        equalWithCrs(latLongHelper.point(1, 1), epsg2193)
    );

    // this comes out as a realization problem, not an eval exception
    evaluate("geom_from_wkt('POINT(1 1)', 'lat long')", null);
    assertNull(realized);

    Problem expectedProblem = GeometryProblems.get().unknownCrsCode("lat long");
    assertThat(realizationProblems, hasItem(hasAncestorProblem(equalTo(expectedProblem))));
  }

  @Test
  public void geomFromWktDoesNotProduceCurvedGeometries() throws Exception {
    // RiskScape does not support curved geometries. These are linearized by the LinearGeometryProjection
    // when they come from feature source relations.
    // This test is to ensure that they cannot be brought into RiskScape using WKT.
    // If the WKT reader in GeomFromWKT is changed to one that supports curved geometry then we may need
    // to a test for curved geometries and throw a not supported problem instead.
    assertThrows(EvalException.class, () -> evaluate(
        "geom_from_wkt('CURVEPOLYGON ((50 50,50 60,60 60,60 50,50 50),(55 55,56 55,56 56,55 56,55 55))')",
        null
    ));
    assertThrows(EvalException.class, () -> evaluate(
        "geom_from_wkt('COMPOUNDCURVE ((10 10,20 10))')",
        null
    ));
    assertThrows(EvalException.class, () -> evaluate(
        "geom_from_wkt('MULTICURVE ((10 10,20 10),(20 20,30 20))')",
        null
    ));
    assertThrows(EvalException.class, () -> evaluate(
        "geom_from_wkt('MULTISURFACE (((10 10,10 20,20 20,20 10,10 10)),((50 50,50 60,60 60,60 50,50 50),"
            + "(55 55,56 55,56 56,55 56,55 55)))')",
        null
    ));
  }


  private Matcher<Geometry> equalWithCrs(Geometry expected, CoordinateReferenceSystem withCrs) {

    return new TypeSafeDiagnosingMatcher<Geometry>(Geometry.class) {

      @Override
      public void describeTo(Description description) {
        description.appendText("geometry in crs ").appendValue(withCrs.getName().toString());
      }

      @Override
      protected boolean matchesSafely(Geometry item, Description mismatchDescription) {
        if (item.equals(expected)) {
          int expectedSrid =project.getSridSet().get(withCrs);
          if (item.getSRID() == expectedSrid) {
            return true;
          } else {
            mismatchDescription.appendText("srid mismatch - ").appendValue(expectedSrid).appendText(" != ")
              .appendValue(item.getSRID());
            return false;
          }
        } else {
          return false;
        }
      }
    };
  }

  @Test
  public void mapsOldStyleFunctionNamesToNew() {
    assertThat(evaluate("createPoint('1.0', '1.0')", null), is(latLongHelper.point(1,1)));
    assertThat(evaluate("createPoint('1', '1')", null), is(latLongHelper.point(1,1)));
    assertThat(realized.getResultType(), is(Types.POINT));

    assertThat(evaluate("geomFromWKT('POINT(1 1)')", null), is(latLongHelper.point(1, 1)));
    assertThat(evaluate("geomFromWKT('LINESTRING(1 1, 1 3)')", null), instanceOf(LineString.class));
    assertThat(evaluate("geomFromWKT('POLYGON((1 1, 1 2, 2 2, 2 1, 1 1))')", null), instanceOf(Polygon.class));
    assertThat(realized.getResultType(), is(Types.GEOMETRY));
  }

  @Test
  public void testTopologyExceptionGivesInformativeTip() {
    // an invalid polygon - an hourglass shape with intersecting latLongHelper.lines that are
    // not declared as such (i.e. it needs a latLongHelper.point(s) in the middle declared)
    String problematicShapeWkt = "POLYGON((0 100, 100 100, 0 0, 100 0, 0 100))";
    Geometry problematicShape = (Geometry) evaluate(String.format("geom_from_wkt('%s')", problematicShapeWkt), null);

    // a valid polygon that intersects (although it doesn't have to)
    String otherShapeWkt = "POLYGON((0 90, 100 90, 50 50, 0 90))";
    Geometry otherShape = (Geometry) evaluate(String.format("geom_from_wkt('%s')", otherShapeWkt), null);

    // using the library function directly will throw a 'non-noded intersection' exception
    assertThrows(TopologyException.class, () -> problematicShape.intersection(otherShape));

    // evaluating it as a riskscape expression will wrap it in a RiskscapeException
    EvalException ex = assertThrows(EvalException.class,
        () -> evaluate("intersection(foo, bar)", Tuple.ofValues(type, problematicShape, otherShape)));

    // check we include a more meaningful error message for the user
    assertThat(ex.getProblem(),
        hasAncestorProblem(equalTo(GeometryProblems.get().topologyExceptionTip())));
    assertThat(render(ex.getProblem()), containsString("geometry data is invalid"));
  }

  private void assertPredicate(boolean truth, String expression, Geometry foo, Geometry bar) {
    assertEquals(Boolean.valueOf(truth), evaluate(expression, Tuple.ofValues(type, foo, bar, 0D)));
  }

  private void assertPredicate(boolean truth, String expression, Geometry foo, Geometry bar, double distance) {
    assertEquals(Boolean.valueOf(truth), evaluate(expression, Tuple.ofValues(type, foo, bar, distance)));
  }
}
