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

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

import java.util.Arrays;
import java.util.List;

import nz.org.riskscape.problem.Problem;

import org.hamcrest.Matcher;
import org.junit.Before;
import org.junit.Test;

import nz.org.riskscape.engine.Assert;
import nz.org.riskscape.engine.cli.BookmarksCommand;
import nz.org.riskscape.engine.cli.ExitException;
import nz.org.riskscape.engine.cli.ExpressionCommand;
import nz.org.riskscape.engine.test.EngineTestSettings;

@EngineTestSettings("pipeline-threads=6")
public class CoverageSamplingTest extends BaseModelRunCommandTest {

  private static final String NO_DATA = "POINT (5475350 1580350)";
  private static final String DATA1 = "POINT (5475250 1580350)";
  private static final String DATA2 = "POINT (5475150 1580350)";
  private static final String DATA3 = "POINT (5475050 1580350)";

  @Before
  public void populateProject() {
    populateProject(stdhome());
  }

  @Test
  public void canSampleArcgrid() {
    assertSamplePoint("raster1", NO_DATA, is("<nothing>"));
    assertSamplePoint("raster1", DATA1, is("1.0"));
    assertSamplePoint("raster1", DATA2, is("2.0"));
    assertSamplePoint("raster1", DATA3, is("3.0"));  }

  @Test
  public void canSampleGeotiff() {
    assertSamplePoint("raster2", NO_DATA, is("<nothing>"));
    assertSamplePoint("raster2", DATA1, is("1.0"));
    assertSamplePoint("raster2", DATA2, is("2.0"));
    assertSamplePoint("raster2", DATA3, is("3.0"));
  }

  @Test
  public void canSampleGeotiffWithNanNoData() {
    assertSamplePoint("raster3", NO_DATA, is("<nothing>"));
    assertSamplePoint("raster3", DATA1, is("1.0"));
    assertSamplePoint("raster3", DATA2, is("2.0"));
    assertSamplePoint("raster3", DATA3, is("3.0"));
  }

  @Test
  public void canSampleGeotiffInUint16() {
    // GeoTIFF here is UInt16, which is a bit unusual, but check it doesn't explode.
    // In particular, we want to check sparse-mode enabled, which used to trigger an exception
    String uint16SparseRaster = "bookmark('kaiju-fire-uint16.tif', {sparse-tiff: true})";
    String uint16NonSparseRaster = "bookmark('kaiju-fire-uint16.tif', {sparse-tiff: false})";
    String noData = "POINT (988410 213767)";
    String data1 = "POINT (989134 213656)";
    String crs = "EPSG:2263"; // New York-based raster

    assertSamplePoint(uint16SparseRaster, noData, crs, is("<nothing>"));
    assertSamplePoint(uint16SparseRaster, data1, crs, is("3.0"));

    assertSamplePoint(uint16NonSparseRaster, noData, crs, is("<nothing>"));
    assertSamplePoint(uint16NonSparseRaster, data1, crs, is("3.0"));
  }

  @Test
  public void canSampleGeotiffAndMapValue() {
    assertSamplePoint("raster2-with-map", NO_DATA, is("<nothing>"));
    assertSamplePoint("raster2-with-map", DATA1, is("2.0"));
    assertSamplePoint("raster2-with-map", DATA2, is("4.0"));
    assertSamplePoint("raster2-with-map", DATA3, is("6.0"));
  }

  @Test
  public void canSampleGeotiffAndMapValueWithLambda() {
    assertSamplePoint("raster2-with-map-lambda", NO_DATA, is("<nothing>"));
    assertSamplePoint("raster2-with-map-lambda", DATA1, is("3.0"));
    assertSamplePoint("raster2-with-map-lambda", DATA2, is("6.0"));
    assertSamplePoint("raster2-with-map-lambda", DATA3, is("9.0"));
  }

  @Test
  public void sampleAllFindsExpectedIntersections() throws Exception {
    runCommand.modelId = "sample_intersections";
    runCommand.run();

    assertThat(openCsv("results.csv", "name", "samples.geometry", "samples.sampled"), containsInAnyOrder(
        List.of(
            "x1y2",
            "POLYGON ((1580200 5475200, 1580300 5475200, 1580300 5475100, 1580200 5475100, 1580200 5475200))",
            "2.0"
        ),
        // x1y1_only goes right through this cell, but does not extend into the neighbours
        List.of("x1y1_only", "LINESTRING (1580250 5475100, 1580250 5475200)", "2.0"),
        // x1y0-2a goes right though cell x1y1 and extends just a little into the neighbours above and below
        // so we expect to find them as well
        List.of("x1y0-2a", "LINESTRING (1580250 5475200, 1580250 5475200.1)", "1.0"),
        List.of("x1y0-2a", "LINESTRING (1580250 5475100, 1580250 5475200)", "2.0"),
        List.of("x1y0-2a", "LINESTRING (1580250 5475099.9, 1580250 5475100)", "3.0"),
        List.of("x1y0-2", "LINESTRING (1580250 5475200, 1580250 5475299.9)", "1.0"),
        List.of("x1y0-2", "LINESTRING (1580250 5475100, 1580250 5475200)", "2.0"),
        List.of("x1y0-2", "LINESTRING (1580250 5475000.1, 1580250 5475100)", "3.0"),
        List.of("hx2y1_only", "LINESTRING (1580400 5475100, 1580300 5475100)", "3.0"),
        // similar to x1y0-2a but line is horizontal (not vertical)
        List.of("hx1y1-3", "LINESTRING (1580300 5475100, 1580299.9 5475100)", "3.0"),
        List.of("hx1y1-3", "LINESTRING (1580400 5475100, 1580300 5475100)", "3.0"),
        List.of("hx1y1-3", "LINESTRING (1580400.1 5475100, 1580400 5475100)", "3.0")
    ));
  }

  @Test
  public void sampleValuesFindsExpected() throws Exception {
    runCommand.modelId = "sample_values";
    runCommand.run();

    // same results as sampleAllFindsExpectedIntersections() except we're outputting the count and sum of values
    assertThat(openCsv("results.csv", "name", "num", "sampled"), containsInAnyOrder(
            List.of("x1y2", "1", "2.0"),
            List.of("x1y1_only", "1", "2.0"),
            List.of("x1y0-2a", "3", "6.0"),
            List.of("x1y0-2", "3", "6.0"),
            List.of("hx2y1_only", "1", "3.0"),
            List.of("hx1y1-3", "3", "9.0")
    ));
  }

  @Test
  public void canSampleGeotiffAndMapValueinPipeline() throws Exception {
    runCommand.modelId = "sample-raster2-with-map";
    runCommand.run();

    // the model filters out all sampled data that matches what was expected,
    // so getting anything more than the 'Everything OK' line indicates a problem
    assertThat(openCsv("results.csv", "sampled", "expected"), contains(
        Arrays.asList("Everything", "OK")));
  }

  @Test
  public void getSensibleErrrorForBadMapValue() {
    BookmarksCommand.Info cmd = setupCommand(new BookmarksCommand.Info());
    cmd.bookmarkId = "raster2-with-bad-map";

    ExitException ex = Assert.assertThrows(ExitException.class, () -> cmd.run());
    assertThat(render(ex.getProblem()), allOf(
        containsString("Problems found with 'map-value' parameter"),
        containsString("Could not find 'foo' among [value]")
    ));
  }

  String runEvalExprCommand(String cmd) {
    ExpressionCommand.Eval eval = setupCommand(new ExpressionCommand.Eval());
    eval.expression = cmd;
    Object result = eval.run();
    // report a decent test failure if we hit an unexpected error
    if (result instanceof Problem problem) {
      fail(cmd + " failed with error: " + render(problem));
    }
    return (String) result;
  }

  String sampleExpression(String function, String bookmarkExpr, String geomWkt, String crs) {
    return String.format("%s(geom_from_wkt('%s', '%s'), %s)", function, geomWkt, crs, bookmarkExpr);
  }

  String runSampleCommand(String function, String bookmarkExpr, String geomWkt, String crs) {
    return runEvalExprCommand(sampleExpression(function, bookmarkExpr, geomWkt, crs));
  }

  <T> void assertSamplePoint(String bookmarkId, String geomWkt, Matcher<String> matcher) {
    assertSamplePoint(String.format("bookmark('%s')", bookmarkId), geomWkt, "EPSG:2193", matcher);
  }

  <T> void assertSamplePoint(String bookmarkExpr, String geomWkt, String crs, Matcher<String> matcher) {
    // we're just sampling a point, so all the sampling functions should return the same result
    assertThat(runSampleCommand("sample_centroid", bookmarkExpr, geomWkt, crs), matcher);
    assertThat(runSampleCommand("sample_closest", bookmarkExpr, geomWkt, crs), matcher);
    // deprecated, but check it still works
    assertThat(runSampleCommand("sample_one", bookmarkExpr, geomWkt, crs), matcher);

    assertThat(runEvalExprCommand(
            String.format("max(%s)", sampleExpression("sample_values", bookmarkExpr, geomWkt, crs))
          ),
          matcher
    );

    assertThat(runEvalExprCommand(
                    String.format("max(map(%s, h -> h.sampled))",
                            sampleExpression("sample_intersections", bookmarkExpr, geomWkt, crs))
            ),
            matcher
    );
  }
}
