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

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

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

import java.util.Arrays;
import java.util.List;
import java.util.concurrent.atomic.AtomicReference;

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

import nz.org.riskscape.engine.ArgsProblems;
import nz.org.riskscape.engine.gt.GeometryHelper;
import nz.org.riskscape.engine.NoSuchMemberException;
import nz.org.riskscape.engine.Tuple;
import nz.org.riskscape.engine.coverage.TypedCoverage;
import nz.org.riskscape.engine.data.BookmarkProblems;
import nz.org.riskscape.engine.data.coverage.IndexedTypedCoverage;
import nz.org.riskscape.engine.data.coverage.NearestNeighbourCoverage;
import nz.org.riskscape.engine.function.ArgumentList;
import nz.org.riskscape.engine.function.IdentifiedFunction;
import nz.org.riskscape.engine.function.RiskscapeFunction;
import nz.org.riskscape.engine.function.UnaryFunction;
import nz.org.riskscape.engine.problem.GeneralProblems;
import nz.org.riskscape.engine.relation.ListRelation;
import nz.org.riskscape.engine.relation.Relation;
import nz.org.riskscape.engine.rl.BaseExpressionRealizerTest;
import nz.org.riskscape.engine.rl.RealizableFunction;
import nz.org.riskscape.engine.rl.RealizationContext;
import nz.org.riskscape.engine.types.CoercionException;
import nz.org.riskscape.engine.types.CoverageType;
import nz.org.riskscape.engine.types.Nullable;
import nz.org.riskscape.engine.types.RSList;
import nz.org.riskscape.engine.types.RelationType;
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.problem.Problem;
import nz.org.riskscape.problem.ResultOrProblems;
import nz.org.riskscape.rl.ast.FunctionCall;
/**
 * Tests of the `to_coverage` function when called as a standard RiskScape function. For testing as
 * an aggregating function see {@link ToTypedCoverageTest}.
 */
@SuppressWarnings("unchecked")
public class ToTypedCoverageFunctionTest extends BaseExpressionRealizerTest {

  GeometryHelper nzGeomHelper = GeometryHelper.nzTransverseMercatorWithOriginShift(project.getSridSet());

  Struct itemType = Struct.of(
      "geom", Nullable.of(Types.GEOMETRY),
      "name", Types.TEXT);

  Struct listInputType = Struct.of("values", RSList.create(itemType));
  RelationType relationType = new RelationType(itemType);
  Struct relationInputType = Struct.of("relation", relationType);

  final Tuple tupleOne = Tuple.ofValues(itemType, nzGeomHelper.box(0, 0, 10, 10), "one");
  final Tuple tupleTwo = Tuple.ofValues(itemType, nzGeomHelper.box(5, 5, 15, 15), "two");

  final Tuple pointTupleOne = Tuple.ofValues(itemType, nzGeomHelper.point(100, 100), "point one");
  final Tuple pointTupleTwo = Tuple.ofValues(itemType, nzGeomHelper.point(100, 200), "point two");

  Relation pointRelation = new ListRelation(itemType, pointTupleOne, pointTupleTwo);

  Tuple inputTuple = Tuple.EMPTY_TUPLE;
  TypedCoverage coverage;
  ToTypedCoverage toTypedCoverage = new ToTypedCoverage();

  @Before
  public void setup() {
    IdentifiedFunction toCoverage = toTypedCoverage.identified("to_coverage");

    project.getFunctionSet().add(toCoverage);

    // a function that will evaluate as a constant, and returns a relation.
    // this function mimics what happens with the `bookmark` function. In that the returned type will
    // be a different instance each time the function is realized.
    RiskscapeFunction relationFunction = RealizableFunction.asFunction((context, functionCall, argumentTypes) -> {
      Struct realizedItemType = Struct.of(
          "geom", Nullable.of(Types.GEOMETRY),
          "name", Types.TEXT);

      AtomicReference<Relation> relation = new AtomicReference<>(new ListRelation(realizedItemType,
          Tuple.ofValues(realizedItemType, nzGeomHelper.point(100, 100), "point one"),
          Tuple.ofValues(realizedItemType, nzGeomHelper.point(100, 200), "point two")
      ));

      return ResultOrProblems.of(RiskscapeFunction.create(BUILT_IN, argumentTypes,
          new RelationType(realizedItemType),
          // this is a one shot realized function in that it will return the relation on the first call
          // and null there after. This is to ensure the relation is only fetched once.
          args -> relation.getAndSet(null)
      ));
    }, ArgumentList.fromArray(), Types.ANYTHING);
    project.getFunctionSet().add(relationFunction.identified("static_relation"));

    RiskscapeFunction listFunction = RealizableFunction.asFunction((context, functionCall, argumentTypes) -> {
      // it's important here for the test that each realization of this function gives a new struct (even though
      // equal - this tests that we are realizing with the correct types to avoid a struct owner error, like with the
      // relation function above
      Struct realizedItemType = Struct.of(
          "geom", Nullable.of(Types.GEOMETRY),
          "name", Types.TEXT);

      AtomicReference<List<?>> list = new AtomicReference<>(Arrays.asList(
          Tuple.ofValues(realizedItemType, nzGeomHelper.point(100, 100), "point one"),
          Tuple.ofValues(realizedItemType, nzGeomHelper.point(100, 200), "point two")
      ));

      return ResultOrProblems.of(RiskscapeFunction.create(BUILT_IN, argumentTypes,
          RSList.create(realizedItemType),
          // this is a one shot realized function in that it will return the list on the first call
          // and null there after. This is to ensure the list is only fetched once - if it gets called more than once
          // it'll trigger an NPE and the test will fail
          args -> list.getAndSet(null)
      ));
    }, ArgumentList.fromArray(), Types.ANYTHING);
    project.getFunctionSet().add(listFunction.identified("static_list"));
  }

  @Test
  public void canCreateCoverageFromListOfTuples() {
    inputTuple = Tuple.ofValues(listInputType, Arrays.asList(tupleOne, tupleTwo));

    evaluate("to_coverage(values)");

    assertSuccessful(IndexedTypedCoverage.class);
    assertThat(realized.getResultType(), is(new CoverageType(itemType)));
    assertThat(coverage.evaluate(nzGeomHelper.point(2, 2)), is(tupleOne));
    assertThat(coverage.evaluate(nzGeomHelper.point(12, 12)), is(tupleTwo));

    // same result when are explicit with desired index
    evaluate("to_coverage(values, options: {index: 'intersection'})");

    assertSuccessful(IndexedTypedCoverage.class);
    assertThat(realized.getResultType(), is(new CoverageType(itemType)));
    assertThat(coverage.evaluate(nzGeomHelper.point(2, 2)), is(tupleOne));
    assertThat(coverage.evaluate(nzGeomHelper.point(12, 12)), is(tupleTwo));
  }

  @Test
  public void handlesNullableList() {
    Struct myListInputType = Struct.of("values", Nullable.of(RSList.create(itemType)));

    inputTuple = Tuple.ofValues(myListInputType, Arrays.asList(tupleOne, tupleTwo));
    evaluate("to_coverage(values)");

    assertSuccessful(IndexedTypedCoverage.class);
    assertThat(realized.getResultType(), is(Nullable.of(new CoverageType(itemType))));
    assertThat(coverage.evaluate(nzGeomHelper.point(2, 2)), is(tupleOne));

    inputTuple = Tuple.ofValues(myListInputType);
    evaluate("to_coverage(values)");
    assertThat(evaluated, nullValue());
  }

  @Test
  public void canCreateCoverageFromRelation() {
    inputTuple = Tuple.ofValues(relationInputType, pointRelation);

    evaluate("to_coverage(relation, options: {index: 'nearest_neighbour', nearest_neighbour_max_distance: 50})");
    assertSuccessful(NearestNeighbourCoverage.class);
    assertThat(realized.getResultType(), is(new CoverageType(itemType)));

    assertThat(coverage.evaluate(nzGeomHelper.point(100, 100)), is(pointTupleOne));
    assertThat(coverage.evaluate(nzGeomHelper.point(51, 100)), is(pointTupleOne));
    assertThat(coverage.evaluate(nzGeomHelper.point(100, 149)), is(pointTupleOne));

    assertThat(coverage.evaluate(nzGeomHelper.point(100, 150)), oneOf(pointTupleOne, pointTupleTwo));

    assertThat(coverage.evaluate(nzGeomHelper.point(100, 151)), is(pointTupleTwo));
  }

  @Test
  public void canCreateCoverageFromBookmarkIdToRelation() {
    // we need to mock the bookmark function because it lives in the defaults plugin
    RiskscapeFunction bookmarkFunction = new UnaryFunction<>(Types.ANYTHING, relationType, arg1 -> {
      if (! arg1.equals("foo")) {
        throw new RuntimeException("boom");
      }
      return pointRelation;
    });
    project.getFunctionSet().add(bookmarkFunction.identified("bookmark"));

    evaluate("to_coverage('foo', options: {index: 'nearest_neighbour', nearest_neighbour_max_distance: 50})");
    assertSuccessful(NearestNeighbourCoverage.class);
    assertThat(realized.getResultType(), is(new CoverageType(itemType)));

    assertThat(coverage.evaluate(nzGeomHelper.point(100, 100)), is(pointTupleOne));
    assertThat(coverage.evaluate(nzGeomHelper.point(51, 100)), is(pointTupleOne));
    assertThat(coverage.evaluate(nzGeomHelper.point(100, 149)), is(pointTupleOne));

    assertThat(coverage.evaluate(nzGeomHelper.point(100, 150)), oneOf(pointTupleOne, pointTupleTwo));

    assertThat(coverage.evaluate(nzGeomHelper.point(100, 151)), is(pointTupleTwo));
  }

  @Test
  public void failsWhenBookmarkIdNotValid() {
    // bookmark('foo') will fail to realize if foo is not a known bookmark id (or file path)
    // so we emulate that with our dummy bookmark function
    RiskscapeFunction bookmarkFunction = RealizableFunction.asFunction(
        new RealizableFunction() {
      @Override
      public ResultOrProblems<RiskscapeFunction> realize(RealizationContext context,
          FunctionCall functionCall, List<Type> argumentTypes) {
        return ResultOrProblems.failed(BookmarkProblems.get().notBookmarkOrFile("foo"));
      }
    },
        ArgumentList.create("id", Types.TEXT), Types.ANYTHING
    );
    project.getFunctionSet().add(bookmarkFunction.identified("bookmark"));

    realize(Struct.EMPTY_STRUCT, parse("to_coverage('foo')"));
    assertThat(realizationProblems, contains(
        hasAncestorProblem(is(BookmarkProblems.get().notBookmarkOrFile("foo")))
    ));
  }

  @Test
  public void failsWhenBookmarkIdNotConstant() {
    Struct type = Struct.of("text", Types.TEXT);
    assertThat(realizeOnly("to_coverage(text)", type), failedResult(
        hasAncestorProblem(is(ArgsProblems.mismatch(toTypedCoverage.getArguments().get(0), Types.TEXT,
            Arrays.asList(RSList.create(Struct.EMPTY_STRUCT), RelationType.WILD))))
    ));
  }


  @Test
  public void returnsTheSameCoverageWithConstantRelationInputGL581() {
    inputTuple = Tuple.EMPTY_TUPLE;

    evaluate("to_coverage(static_relation(), "
        + "options: {index: 'nearest_neighbour', nearest_neighbour_max_distance: 50})");
    assertSuccessful(NearestNeighbourCoverage.class);
    assertThat(realized.getResultType(), is(new CoverageType(itemType)));

    assertThat(coverage.evaluate(nzGeomHelper.point(100, 100)), is(pointTupleOne));

    // we should get the same coverage instance returned every time this function is evaluated
    for (int i = 0; i < 10; i++) {
      assertThat(realized.evaluate(inputTuple), sameInstance(coverage));
    }
    coverage = (NearestNeighbourCoverage)realized.evaluate(inputTuple);
    assertThat(coverage.evaluate(nzGeomHelper.point(100, 100)), is(pointTupleOne));
  }

  @Test
  public void returnsTheSameCoverageWithConstantListInputGL581() {
    inputTuple = Tuple.EMPTY_TUPLE;

    evaluate("to_coverage(static_list(), options: {index: 'nearest_neighbour', nearest_neighbour_max_distance: 50})");
    assertSuccessful(NearestNeighbourCoverage.class);
    assertThat(realized.getResultType(), is(new CoverageType(itemType)));

    assertThat(coverage.evaluate(nzGeomHelper.point(100, 100)), is(pointTupleOne));

    // we should get the same coverage instance returned every time this function is evaluated
    for (int i = 0; i < 10; i++) {
      assertThat(realized.evaluate(inputTuple), sameInstance(coverage));
    }
    coverage = (NearestNeighbourCoverage)realized.evaluate(inputTuple);
    assertThat(coverage.evaluate(nzGeomHelper.point(100, 100)), is(pointTupleOne));
  }

  @Test
  public void handlesNullableRelation() {
    Struct myRelationInputType = Struct.of("relation", Nullable.of(new RelationType(itemType)));

    inputTuple = Tuple.ofValues(myRelationInputType, pointRelation);
    evaluate("to_coverage(relation, options: {index: 'nearest_neighbour', nearest_neighbour_max_distance: 50})");

    assertSuccessful(NearestNeighbourCoverage.class);
    assertThat(realized.getResultType(), is(Nullable.of(new CoverageType(itemType))));
    assertThat(coverage.evaluate(nzGeomHelper.point(100, 100)), is(pointTupleOne));
    assertThat(coverage.evaluate(nzGeomHelper.point(51, 100)), is(pointTupleOne));

    inputTuple = Tuple.ofValues(myRelationInputType);
    evaluate("to_coverage(relation, options: {index: 'nearest_neighbour', nearest_neighbour_max_distance: 50})");
    assertThat(evaluated, nullValue());
  }

  @Test
  public void canBePassedACoverageInsteadOfARelation() {
    // this flexibility lets us use *either* a shapefile or geotiff in the same pipeline
    TypedCoverage coverageArg = TypedCoverage.empty(Types.FLOATING);
    Struct inputType = Struct.of("coverage", new CoverageType(Types.FLOATING));
    inputTuple = Tuple.ofValues(inputType, coverageArg);
    evaluate("to_coverage(coverage)");
    assertSuccessful(TypedCoverage.class);
    assertThat(evaluated, is(coverageArg));

    Struct nullableInputType = Struct.of("coverage", Nullable.of(coverageArg.getScalarDataType()));
    inputTuple = Tuple.ofValues(nullableInputType, coverageArg);
    evaluate("to_coverage(coverage)");
    assertSuccessful(TypedCoverage.class);
    assertTrue(realized.getResultType().isNullable());
    assertThat(evaluated, is(coverageArg));

    inputTuple = Tuple.ofValues(nullableInputType, (Object) null);
    evaluate("to_coverage(coverage)");
    assertThat(evaluated, nullValue());
  }

  public void failsIfRelationHasNoGeometry() {
    Struct myItemType = Struct.of("name", Types.TEXT, "value", Types.INTEGER);
    Struct myInputType = Struct.of("relation", new RelationType(myItemType));
    assertThat(realizeOnly("to_coverage(relation)", myInputType), failedResult(
        hasAncestorProblem(is(TypeProblems.get().structMustHaveMemberType(Types.GEOMETRY, myItemType)))
    ));
  }

  @Test
  public void failsIfListItemHasNoGeometry() {
    Struct myItemType = Struct.of("name", Types.TEXT, "value", Types.INTEGER);
    Struct myInputType = Struct.of("values", RSList.create(myItemType));

    assertThat(realizeOnly("to_coverage(values)", myInputType), failedResult(
        hasAncestorProblem(is(TypeProblems.get().structMustHaveMemberType(Types.GEOMETRY, myItemType)))
    ));
  }

  @Test
  public void failsIfNotListOfTuples() {
    assertThat(realizeOnly("to_coverage([1, 2, 3])", Struct.EMPTY_STRUCT), failedResult(
        hasAncestorProblem(is(ArgsProblems.mismatch(toTypedCoverage.getArguments().get(0),
            RSList.create(Types.INTEGER), Arrays.asList(RSList.create(Struct.EMPTY_STRUCT), RelationType.WILD))
        ))
    ));
  }

  @Test
  public void failsIfNotList() {
    assertThat(realizeOnly("to_coverage(33)", Struct.EMPTY_STRUCT), failedResult(
        hasAncestorProblem(is(ArgsProblems.mismatch(toTypedCoverage.getArguments().get(0), Types.INTEGER,
            Arrays.asList(RSList.create(Struct.EMPTY_STRUCT), RelationType.WILD))))
    ));
  }

  @Test
  public void failsIfOptionsNotRecognised() {
    // there is no index called 'fast'
    assertThat(
        realizeOnly("to_coverage(values, options: {index: 'fast'})", listInputType),
        failedResult(hasAncestorProblem(isProblem(CoercionException.class)))
    );

    // missing distance
    assertThat(
        realizeOnly("to_coverage(values, options: {index: 'nearest_neighbour'})", listInputType),
        failedResult(hasAncestorProblem(is(GeneralProblems.get().required("nearest_neighbour_max_distance"))))
    );

    // max distance should be a number
    assertThat(
        realizeOnly(
            "to_coverage(values, options: {index: 'nearest_neighbour', nearest_neighbour_max_distance: 'medium'})",
            listInputType),
        failedResult(hasAncestorProblem(isProblem(CoercionException.class)))
    );

    // unknown parameter
    assertThat(
        realizeOnly("to_coverage(values, options: {index: 'intersection', bogus: 'medium'})", listInputType),
        failedResult(hasAncestorProblem(isProblem(NoSuchMemberException.class)))
    );
  }

  public void failsIfWrongNumberOfArgs() {
    assertThat(realizeOnly("to_coverage()", listInputType), failedResult(
        hasAncestorProblem(is(ArgsProblems.get().wrongNumberRange(1, 2, 0)))
    ));

    assertThat(realizeOnly("to_coverage(values, {index: 'intersection'}, 'foo')", listInputType), failedResult(
        hasAncestorProblem(is(ArgsProblems.get().wrongNumberRange(1, 2, 3)))
    ));
  }

  private void evaluate(String expr) {
    evaluate(expr, inputTuple);
  }

  private void assertSuccessful(Class<? extends TypedCoverage> expected) {
    if (!realizationProblems.isEmpty()) {
      fail(Problem.debugString(realizationProblems));
    }
    assertThat(evaluated, instanceOf(expected));
    coverage = expected.cast(evaluated);
  }

}
