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

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

import java.util.Arrays;
import java.util.List;
import java.util.Optional;
import java.util.function.BiFunction;

import org.geotools.geometry.jts.ReferencedEnvelope;
import org.junit.Test;
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 org.locationtech.jts.geom.Polygon;

import nz.org.riskscape.defaults.function.MultiCoverage.RawValueSetter;
import nz.org.riskscape.engine.CrsHelper;
import nz.org.riskscape.engine.Matchers;
import nz.org.riskscape.engine.Tuple;
import nz.org.riskscape.engine.TupleMatchers;
import nz.org.riskscape.engine.coverage.TypedCoverage;
import nz.org.riskscape.engine.gt.GeometryHelper;
import nz.org.riskscape.engine.rl.BaseExpressionRealizerTest;
import nz.org.riskscape.engine.types.Struct;
import nz.org.riskscape.engine.types.Types;

public class MultiCoverageTest extends BaseExpressionRealizerTest implements CrsHelper {

  Struct type = Struct.of("foo", Types.ANYTHING, "bar", Types.ANYTHING);
  TypedCoverage fooCoverage = mock(TypedCoverage.class, "foo");
  TypedCoverage barCoverage = mock(TypedCoverage.class, "bar");

  TypedCoverage[] coverages = new TypedCoverage[] {
      fooCoverage, barCoverage
  };
  RawValueSetter[] valueSetters = new RawValueSetter[]{
    (value, values) -> values.set(0, value),
    (value, values) -> values.set(1, value)
  };

  @SuppressWarnings("unchecked")
  BiFunction<Geometry, Point, List<Geometry>> cutFunction = mock(BiFunction.class, "cut function");

  MultiCoverage coverage = MultiCoverage.create(type, coverages, valueSetters,
      project.getSridSet(), cutFunction);

  GeometryHelper nztmHelper = GeometryHelper.nzTransverseMercatorWithOriginShift(project.getSridSet());
  GeometryFactory gf = nztmHelper.emptyPoint().getFactory();
  Point point = nztmHelper.point(1, 1);

  // we return some combination of these polygons from the cut function - normally cutting would take a poly and return
  // smaller polygons, but this is a unit test and we don't care about the cutting, just what happens to the pieces
  Polygon poly1 = (Polygon) nztmHelper.point(2, 2).buffer(2);
  Polygon poly2 = (Polygon) nztmHelper.point(3, 3).buffer(2);
  Polygon poly3 = (Polygon) nztmHelper.point(4, 4).buffer(2);


  @Test
  public void evaluateSamplesEachCoverageIndividually() throws Exception {
    when(fooCoverage.evaluate(point)).thenReturn("foo value");
    when(barCoverage.evaluate(point)).thenReturn("bar value");

    assertThat(
      coverage.evaluate(point),
      Matchers.instanceOfAnd(
        Tuple.class,
        allOf(
          hasProperty("struct", equalTo(type)),
          TupleMatchers.tupleWithValue("foo", equalTo("foo value")),
          TupleMatchers.tupleWithValue("bar", equalTo("bar value"))
        )
      )
    );
  }

  @Test
  public void evaluateSetsNullForAResultWhenSomeChildCoveragesYieldNull() throws Exception {
    when(fooCoverage.evaluate(point)).thenReturn("foo value");

    assertThat(
      coverage.evaluate(point),
      Matchers.instanceOfAnd(
        Tuple.class,
        allOf(
          TupleMatchers.tupleWithValue("foo", equalTo("foo value")),
          TupleMatchers.tupleWithValue("bar", nullValue())
        )
      )
    );
  }

  @Test
  public void evaluateReturnsNullWhenAllChildCoveragesYieldNull() throws Exception {
    assertThat(
      coverage.evaluate(point),
      nullValue()
    );
  }

  @Test
  public void evaluateIntersectionsReturnsAnEmptyListIfNoResultsSampled() throws Exception {
    when(cutFunction.apply(point, point)).thenReturn(Arrays.asList(poly1, poly2));

    assertThat(
      coverage.evaluateIntersection(point),
      empty()
    );
  }


  @Test
  public void evaluateIntersectionsReturnsAListWithAllNonNullResultsFromChildCoverages() throws Exception {
    when(cutFunction.apply(point, point)).thenReturn(Arrays.asList(poly1, poly2, poly3));

    when(fooCoverage.evaluate(centrePoint(poly1))).thenReturn("foo 1");
    when(fooCoverage.evaluate(centrePoint(poly3))).thenReturn("foo 2");
    when(barCoverage.evaluate(centrePoint(poly3))).thenReturn("bar 2");

    assertThat(
      coverage.evaluateIntersection(point),
      contains(
        Matchers.isPair(
          equalTo(poly1),
          allOf(
            TupleMatchers.tupleWithValue("foo", equalTo("foo 1")),
            TupleMatchers.tupleWithValue("bar", nullValue())
          )
        ),
        //notice that poly2 is skipped - no results returned for it
        Matchers.isPair(
          equalTo(poly3),
          allOf(
            TupleMatchers.tupleWithValue("foo", equalTo("foo 2")),
            TupleMatchers.tupleWithValue("bar", equalTo("bar 2"))
          )
        )
      )
    );

    // intersecting values returns the same values (but without the geometries)
    List<Tuple> intersectingValues = coverage.getEvaluateIntersectingValuesOp().get().apply(point)
        .stream()
        .map(i -> (Tuple)i)
        .toList();
    assertThat(intersectingValues, contains(
        allOf(
            TupleMatchers.tupleWithValue("foo", equalTo("foo 1")),
            TupleMatchers.tupleWithValue("bar", nullValue())
        ),
        allOf(
            TupleMatchers.tupleWithValue("foo", equalTo("foo 2")),
            TupleMatchers.tupleWithValue("bar", equalTo("bar 2"))
        )
    ));
  }

  @Test
  public void boundsDefinedByAllCoveragesInTheSameCRSAsTheFirst() throws Exception {
    ReferencedEnvelope fooBounds = new ReferencedEnvelope(nzTransverseMercator());
    fooBounds.expandToInclude(new Coordinate(-10, -10));
    ReferencedEnvelope barBounds = new ReferencedEnvelope(nzTransverseMercator());
    barBounds.expandToInclude(new Coordinate(20, 20));
    when(fooCoverage.getEnvelope()).thenReturn(Optional.of(fooBounds));
    when(fooCoverage.getCoordinateReferenceSystem()).thenReturn(nzTransverseMercator());
    when(barCoverage.getEnvelope()).thenReturn(Optional.of(barBounds));
    when(barCoverage.getCoordinateReferenceSystem()).thenReturn(nzTransverseMercator());

    ReferencedEnvelope expectedBounds = new ReferencedEnvelope(nzTransverseMercator());
    expectedBounds.expandToInclude(fooBounds);
    expectedBounds.expandToInclude(barBounds);

    assertThat(coverage.getCoordinateReferenceSystem(), equalTo(nzTransverseMercator()));
    assertThat(coverage.getEnvelope(), equalTo(Optional.of(expectedBounds)));
  }

  @Test
  public void evaluateIntersectionAlignsSegmentingToTheFirstCoveragesBoundsIfDefined() throws Exception {
    ReferencedEnvelope fooBounds = new ReferencedEnvelope(nzTransverseMercator());
    fooBounds.expandToInclude(new Coordinate(-10, -10));
    fooBounds.expandToInclude(new Coordinate(20, 20));
    ReferencedEnvelope barBounds = new ReferencedEnvelope(longLat());
    barBounds.expandToInclude(new Coordinate(-20, 30)); // not included
    when(fooCoverage.getEnvelope()).thenReturn(Optional.of(fooBounds));
    when(fooCoverage.getCoordinateReferenceSystem()).thenReturn(nzTransverseMercator());
    when(barCoverage.getEnvelope()).thenReturn(Optional.of(barBounds));
    when(barCoverage.getCoordinateReferenceSystem()).thenReturn(longLat());

    // expect that the cut function is given the tl of foo's bounds, not the point
    Point expectedOrigin = project.getSridSet().getGeometryFactory(nzTransverseMercator())
        .createPoint(new Coordinate(-10, -10));
    when(cutFunction.apply(point, expectedOrigin)).thenReturn(Arrays.asList(poly1));

    when(fooCoverage.evaluate(centrePoint(poly1))).thenReturn("foo 1");

    assertThat(
      coverage.evaluateIntersection(point),
      contains(
        Matchers.isPair(
          equalTo(poly1),
          allOf(
            TupleMatchers.tupleWithValue("foo", equalTo("foo 1")),
            TupleMatchers.tupleWithValue("bar", nullValue())
          )
        )
      )
    );

    // additionally, expect that the crs and bounds are from foo coverage
    assertThat(coverage.getCoordinateReferenceSystem(), equalTo(nzTransverseMercator()));
    assertThat(coverage.getEnvelope(), equalTo(Optional.of(fooBounds)));
  }

  @Test
  public void evaluateIntersectionAlignsSegmentingToGeometryIfBoundsUndefined() throws Exception {
    when(fooCoverage.getEnvelope()).thenReturn(Optional.empty());
    when(barCoverage.getEnvelope()).thenReturn(Optional.empty());
    assertThat(coverage.getEnvelope(), equalTo(Optional.empty()));

    // because the coverage has no bounds, the grid origin should be the lower-left corner of
    // the geometry being sampled. This will minimize cutting the geometry (whereas using the
    // centre point would *always* end up cutting up the input geometry)
    Geometry toSample = nztmHelper.line(0, 0, 100, 100);
    when(cutFunction.apply(toSample, nztmHelper.point(0, 0))).thenReturn(Arrays.asList(poly1));

    when(fooCoverage.evaluate(centrePoint(poly1))).thenReturn("foo 1");

    assertThat(coverage.evaluateIntersection(toSample),
        contains(Matchers.isPair(equalTo(poly1), allOf(TupleMatchers.tupleWithValue("foo", equalTo("foo 1")),
            TupleMatchers.tupleWithValue("bar", nullValue())))));
  }

  @Test
  public void coverageCanSetManyAttributesInResult() {
    Struct myType = Struct.of("foo-1", Types.ANYTHING, "foo-2", Types.ANYTHING, "bar", Types.ANYTHING);

    RawValueSetter[] myCoverageEvaluators = new RawValueSetter[]{
      (value, values) -> {
        // pretty trivial implementation, we just set the same thing to both attributes
        values.set(0, value);
        values.set(1, value);
      },
      (value, values) -> values.set(2, value)
    };

    MultiCoverage myCoverage = MultiCoverage.create(myType, coverages, myCoverageEvaluators,
        project.getSridSet(), cutFunction);

    when(fooCoverage.evaluate(point)).thenReturn("foo value");
    when(barCoverage.evaluate(point)).thenReturn("bar value");

    assertThat(
      myCoverage.evaluate(point),
      Matchers.instanceOfAnd(
        Tuple.class,
        allOf(
          hasProperty("struct", equalTo(myType)),
          TupleMatchers.tupleWithValue("foo-1", equalTo("foo value")),
          TupleMatchers.tupleWithValue("foo-2", equalTo("foo value")),
          TupleMatchers.tupleWithValue("bar", equalTo("bar value"))
        )
      )
    );
  }

  @Test
  public void evaluateIntersectionWithFlattenedAttributes() throws Exception {
    // same as evaluateIntersectionAlignsSegmentingToTheFirstCoveragesBoundsIfDefined()
    // but with raw value setters setting multiple values per coverage
    Struct myType = Struct.of("foo-1", Types.ANYTHING, "foo-2", Types.ANYTHING, "bar", Types.ANYTHING);

    RawValueSetter[] myCoverageEvaluators = new RawValueSetter[]{
      (value, values) -> {
        // pretty trivial implementation, we just set the same thing to both attributes
        values.set(0, value);
        values.set(1, value);
      },
      (value, values) -> values.set(2, value)
    };

    MultiCoverage myCoverage = MultiCoverage.create(myType, coverages, myCoverageEvaluators,
        project.getSridSet(), cutFunction);

    ReferencedEnvelope fooBounds = new ReferencedEnvelope(nzTransverseMercator());
    fooBounds.expandToInclude(new Coordinate(-10, -10));
    fooBounds.expandToInclude(new Coordinate(20, 20));
    ReferencedEnvelope barBounds = new ReferencedEnvelope(longLat());
    barBounds.expandToInclude(new Coordinate(-20, 30)); // not included
    when(fooCoverage.getEnvelope()).thenReturn(Optional.of(fooBounds));
    when(fooCoverage.getCoordinateReferenceSystem()).thenReturn(nzTransverseMercator());
    when(barCoverage.getEnvelope()).thenReturn(Optional.of(barBounds));
    when(barCoverage.getCoordinateReferenceSystem()).thenReturn(longLat());

    // expect that the cut function is given the tl of foo's bounds, not the point
    Point expectedOrigin = project.getSridSet().getGeometryFactory(nzTransverseMercator())
        .createPoint(new Coordinate(-10, -10));
    when(cutFunction.apply(point, expectedOrigin)).thenReturn(Arrays.asList(poly1));

    when(fooCoverage.evaluate(centrePoint(poly1))).thenReturn("foo 1");

    assertThat(
      myCoverage.evaluateIntersection(point),
      contains(
        Matchers.isPair(
          equalTo(poly1),
          allOf(
            TupleMatchers.tupleWithValue("foo-1", equalTo("foo 1")),
            TupleMatchers.tupleWithValue("foo-2", equalTo("foo 1")),
            TupleMatchers.tupleWithValue("bar", nullValue())
          )
        )
      )
    );

    // additionally, expect that the crs and bounds are from foo coverage
    assertThat(coverage.getCoordinateReferenceSystem(), equalTo(nzTransverseMercator()));
    assertThat(coverage.getEnvelope(), equalTo(Optional.of(fooBounds)));
  }

  private Point centrePoint(Geometry geom) {
    return gf.createPoint(geom.getEnvelopeInternal().centre());
  }

}
