/*
 * 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 java.util.UUID;

import org.junit.Test;

import nz.org.riskscape.engine.test.EngineTestPlugins;

@EngineTestPlugins({ "defaults", "beta" })
public class GeoTiffOutputTest extends BaseModelRunCommandTest {

  @Test
  public void canDuplicateAGeoTiff() throws Exception {
    // read a GeoTIFF, save the values, and check that each pixel has the same value as the original
    assertThat(
        duplicateThenCompare("tsunami_10m", "canDuplicateAGeoTIFF",
                " { bounds: bounds('tsunami_10m'), grid-resolution: 5 }"),
        is("0.0")
    );

    // now that we have produced a duplicate coverage we can check it returns the same values for all points
    // in the buildings dataset. checking the two coverages against building could uncover problems if the
    // coverages did not have grid lines at the same positions. hopefully there are some buildings that are
    // on (or at least very near) to the gird lines.
    assertThat(
        compare("buildings", "tsunami_10m",
                getOutputPath("canDuplicateAGeoTIFF.tif").toString()),
        is("0.0")
    );
  }

  @Test
  public void canUseTemplateToDuplicateGeoTiff() throws Exception {
    // use the 'template' save() option this time, instead of specifying bounds/grid-resolution
    assertThat(
            duplicateThenCompare("tsunami_10m", "canUseTemplateToDuplicateCoverage",
                    "{ template: 'tsunami_10m' }"),
            is("0.0")
    );
  }

  @Test
  public void mungingPixelValuesMakesADifferentCoverage() throws Exception {
    // this test is a sanity check that the compare model will pick up on different values in the coverages
    assertThat(
        // add 0.2 to each pixel with a value
        duplicateThenCompare("tsunami_10m", "mungingPixelValuesMakesADifferentCoverage",
                "{ value: value + 0.2, template: 'tsunami_10m' }"),
        // we don't really care what the delta is, just that is't not zero
        not("0.0")
    );
  }

  @Test
  public void canSaveOverlappingFeaturesToGeoTiff() throws Exception {
    // this basically just samples the buildings against the hazard, writes the result to tiff, and checks it
    // against the original tiff. Where buildings overlap the same pixel, we'll just write the same value
    // multiple times, which makes it easy to check we're getting the expected result
    assertThat(compareOverlappingBuildingsWith50mGrid("mean"), is("0.0"));
    assertThat(compareOverlappingBuildingsWith50mGrid("max"), is("0.0"));
    assertThat(compareOverlappingBuildingsWith50mGrid("min"), is("0.0"));

    // sum will be way different though
    assertThat(compareOverlappingBuildingsWith50mGrid("sum"), not("0.0"));
  }

  @Test
  public void canSaveFeaturesInDifferentCrs() throws Exception {
    // write geometry in lat,long (so we have to flip x,y axis for the GeoTIFF).
    // Use the pixel centroid so that reprojection shouldn't result in > 2.5m skew
    assertThat(
            duplicateThenCompare("tsunami_10m", "canSaveFeaturesInDifferentCrs_latlong",
                    " { bounds: bounds('tsunami_10m'), grid-resolution: 5, "
                           + "geometry: reproject(centroid(geom), 'EPSG:4326') }"),
            is("0.0")
    );

    // repeat the test, but read from the long,lat GeoTIFF we just created this time
    String longLatTiff = getOutputPath("canSaveFeaturesInDifferentCrs_latlong.tif").toString();
    assertThat(
            duplicateThenCompare(longLatTiff, "canSaveFeaturesInDifferentCrs_latlong2",
                    " { bounds: bounds('" + longLatTiff + "'), grid-resolution: 5, "
                            + "geometry: reproject(centroid(geom), 'EPSG:4326') }"),
            is("0.0")
    );

    // repeat, but reproject to long,lat this time, so the axis order of the geometry we're writing is flipped
    assertThat(
            duplicateThenCompare(longLatTiff, "canSaveFeaturesInDifferentCrs_longlat",
                    " { bounds: bounds('" + longLatTiff + "'), grid-resolution: 5, "
                            + "geometry: reproject(centroid(geom), 'CRS:84') }"),
            is("0.0")
    );

    // using a template should reproject to the template's CRS, so we go from lat,long to long,lat
    assertThat(
            duplicateThenCompare(longLatTiff, "canSaveFeaturesInDifferentCrs_latlong_template",
                    " { template: '" + longLatTiff + "', "
                            + "geometry: reproject(centroid(geom), 'EPSG:4326') }"),
            is("0.0")
    );

    // this time the template will reproject from lat,long back to EPSG:32702
    assertThat(
            duplicateThenCompare("tsunami_10m", "canSaveFeaturesInDifferentCrs_10m_template",
                    " { template: 'tsunami_10m', "
                            + "geometry: reproject(centroid(geom), 'EPSG:4326') }"),
            is("0.0")
    );
  }

  @Test
  public void templateCanBeAnExpression_GL1554() throws Exception {

    // NB: the underlying GeoTIFF has 5m grid but only represents the hazards to a 50m resolution.
    // So each 10x10 block of pixels will have the same hazard value. We then write the sampled result to
    // a 50m resolution, so we have buildings that overlap within the same pixel. The overall result should contain
    // the same hazard values as the original GeoTIFF
    runCommand.modelId = "duplicate";
    runCommand.parameters = Arrays.asList(
            "coverage='buildings'",
            "options={ "
            + " template: '../../../../../../examples/getting-started/' + 'MaxEnv_All_Scenarios_50m.tif'"
            + " }"
    );
    runCommand.run();

    // this is enough to assert that the thing ran - it won't have if the template couldn't have been formed from the
    // expression
    getOutputPath("coverage.tif");
  }

  private String compareOverlappingBuildingsWith50mGrid(String pixelStat) throws Exception {
    String outputTiff = "compareOverlappingBuildingsWith50mGrid_" + pixelStat + ".tif";
    // NB: the underlying GeoTIFF has 5m grid but only represents the hazards to a 50m resolution.
    // So each 10x10 block of pixels will have the same hazard value. We then write the sampled result to
    // a 50m resolution, so we have buildings that overlap within the same pixel. The overall result should contain
    // the same hazard values as the original GeoTIFF
    runCommand.modelId = "duplicate";
    runCommand.parameters = Arrays.asList(
            "coverage='buildings'",
            String.format("output='%s'", outputTiff),
            "options={ "
            + " bounds: bounds('50m_bounds'),"
            + " grid-resolution: 50,"
            + " geometry: centroid(the_geom),"
            + " value: sample_centroid(the_geom, bookmark('tsunami_50m')),"
            + String.format(" pixel-statistic: '%s'", pixelStat)
            + " }"
    );
    runCommand.run();
    String savedTiff = getOutputPath(outputTiff).toString();
    return compare("buildings", "tsunami_50m", savedTiff);
  }

  String duplicateThenCompare(String coverage, String outputName, String options) throws Exception {
    runCommand.modelId = "duplicate";
    runCommand.parameters = List.of(
        String.format("coverage='%s'", coverage),
        String.format("output='%s'", outputName),
        String.format("options=%s", options)
    );
    runCommand.run();

    return compare(coverage, coverage, getOutputPath(outputName + ".tif").toString());
  }

  String compare(String exposures, String coverage1, String coverage2) throws Exception {
    // we just use a random outputName, this should be enough to ensure we don't have any
    // duplicate filenames
    String outputName = "compare-" + UUID.randomUUID().toString();
    runCommand.modelId = "compare";
    runCommand.parameters = List.of(
        String.format("exposures='%s'", exposures),
        String.format("coverage1='%s'", coverage1),
        String.format("coverage2='%s'", coverage2),
        String.format("output='%s'", outputName)
    );
    runCommand.run();

    List<List<String>> delta = openCsv(outputName + ".csv", "delta");
    return delta.get(0).get(0);
  }

}
