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

import java.nio.file.Path;
import java.util.Arrays;

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

import com.google.common.collect.Lists;

import nz.org.riskscape.engine.Matchers;
import nz.org.riskscape.engine.OutputProblems;
import nz.org.riskscape.engine.cli.PipelineCommand;
import nz.org.riskscape.engine.output.GeoJSONFormat;
import nz.org.riskscape.engine.test.EngineTestSettings;
import nz.org.riskscape.problem.Problem.Severity;

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

  @Before
  public void evalOptions() {
    evalCommand.runnerOptions.format = "csv";
  }

  @Test
  public void readsFirstLayerByDefault() throws Exception {
    evalCommand.pipelineFile = "input('cities') as cities";
    evalCommand.run();

    assertThat(readCsvColumns("cities.csv", "geom", "name"), containsInAnyOrder(
        Arrays.asList("POINT (151 -33)", "Sydney"),
        Arrays.asList("POINT (174 -36)", "Auckland"),
        Arrays.asList("POINT (174 -41)", "Wellington")
    ));
  }

  @Test
  public void readUusesNamedLayer() throws Exception {
    evalCommand.pipelineFile = "input('nz-cities') as cities";
    evalCommand.run();

    assertThat(readCsvColumns("cities.csv", "geom", "name"), containsInAnyOrder(
        Arrays.asList("POINT (174 -36)", "Auckland"),
        Arrays.asList("POINT (174 -41)", "Wellington")
    ));
  }

  @Test
  public void outputCanHaveAwkwardNames() throws Exception {
    evalCommand.pipelineFile = "input('nz-cities') as \"Cities Are \\\"Great\\\" \"";
    evalCommand.runnerOptions.format = "geopackage";
    evalCommand.run();

    assertThat(outBytes.toString(), containsString("Cities%20Are%20%22Great%22%20.gpkg"));

    assertThat(
      getTempDirectory(),
      hasFile(fileWithName("Cities Are \"Great\" .gpkg"))
    );

    // here we extract the expected layer, we do this to highlight how the layer name has been tweaked
    // to fit into a GeoPackage.
    // notice that the filename does not match the layer name - that's because we can't use quote characters inside a
    // quoted identifier - doesn't seem like this is a supported thing in database-land
    fetchResultsAsCsv(getTempDirectory().resolve("Cities Are \"Great\" .gpkg"), "Cities%20Are%20_Great_%20");
    assertThat(
      getTempDirectory(),
      hasFile(fileWithName("Cities%20Are%20_Great_%20.csv"))
    );
  }

  @Test
  public void canBeFiltered() throws Exception {
    evalCommand.pipelineFile = "input('filtered-cities') as cities";
    evalCommand.run();

    assertThat(readCsvColumns("cities.csv", "geom", "name"), containsInAnyOrder(
        Arrays.asList("POINT (151 -33)", "Sydney")
    ));
  }

  @Test
  public void canWriteGeoPackageAndThenReadItBack() throws Exception {
    // read the countries csv with lat/long and save it to geopackage
    evalCommand.pipelineFile = "input('countries') as countries";
    evalCommand.runnerOptions.format = "geopackage";
    evalCommand.runnerOptions.checksum = true;  // generate the checksums. we do this to check that this doesn't cause a
                                  // failure. In the past the GeoPackage storeAt URI had a layer=xyz in the query
                                  // which prevented checksumming from working.
    evalCommand.run();

    reset();
    evalCommand.pipelineFile = String.format("input('%s') as countries1",
        getTempDirectory().resolve("countries.gpkg").toAbsolutePath());
    evalCommand.runnerOptions.format = "csv";
    evalCommand.run();

    assertThat(readCsvColumns("countries1.csv", "geom", "id", "name"), contains(
        // when we read this csv we expect the geoms to be in long/lat as that is the expected encoding
        // for geopackage geometries
        Arrays.asList("POLYGON ((166 -34, 166 -48, 179 -48, 179 -34, 166 -34))", "1", "New Zealand")
    ));
  }

  @Test
  public void geoPackageOutputWillWriteMultipleLayers() {
    // eval a pipeline that creates multiple result layers
    Path geoPackageResultFile = getTempDirectory().resolve("results.gpkg");
    evalCommand.pipelineFile = stdhome().resolve("pipeline-cities-countries.txt").toString();
    evalCommand.runnerOptions.output = geoPackageResultFile.toString();
    evalCommand.run();

    assertResults(geoPackageResultFile);
  }

  @Test
  public void geoPackageOutputBaseLocationWillWriteMultipleLayers() {
    // set up a the project that sets output-base-location
    populateProject(stdhome().resolve("project-with-geopackage-output.ini"));

    // we set up the eval command manually in this test because the super class always sets the
    // output in it
    evalCommand = this.setupCommand(new PipelineCommand.Eval());

    // eval a pipeline that creates multiple result layers
    evalCommand.pipelineFile = stdhome().resolve("pipeline-cities-countries.txt").toString();
    evalCommand.run();

    // the default-results.gpkg is set as the output-base-location on the project
    Path geoPackageResultFile = stdhome().resolve("output").resolve("default-results.gpkg").toAbsolutePath();
    assertResults(geoPackageResultFile);
  }

  @Test
  public void geoPackageOutputStressTest() {
    // a test that moves a larger amount of data around to stress the GeoPackage writing a little more.
    // in this test the same data is written to multiple outputs so we can check that each has the expected
    // size to ensure no tuples are getting lost.
    Path geoPackageResultFile = getTempDirectory().resolve("results.gpkg");
    evalCommand.pipelineFile = stdhome().resolve("pipeline-survey-marks.txt").toString();
    evalCommand.runnerOptions.output = geoPackageResultFile.toString();
    evalCommand.run();

    // now we fetch the results (from the GeoPackage) and write them to csv's for easier test assertions.
    fetchResultsAsCsv(geoPackageResultFile, "marks1", "marks2", "marks3", "marks_by_district");

    // we check for the expected number of marks
    assertThat(readColumnsFromCsv(getTempDirectory().resolve("marks1.csv"), "id"), hasSize(5768));
    assertThat(readColumnsFromCsv(getTempDirectory().resolve("marks2.csv"), "id"), hasSize(5768));
    assertThat(readColumnsFromCsv(getTempDirectory().resolve("marks3.csv"), "id"), hasSize(5768));

    assertThat(readColumnsFromCsv(getTempDirectory().resolve("marks_by_district.csv"), "name", "mark_count"),
        containsInAnyOrder(
            Lists.newArrayList("Hawkes Bay", "499"),
            Lists.newArrayList("Marlborough", "251"),
            Lists.newArrayList("Nelson", "394"),
            Lists.newArrayList("Wellington", "599"),
            Lists.newArrayList("North Auckland", "581"),
            Lists.newArrayList("Otago", "616"),
            Lists.newArrayList("Westland", "154"),
            Lists.newArrayList("Gisborne", "577"),
            Lists.newArrayList("Southland", "554"),
            Lists.newArrayList("Taranaki", "229"),
            Lists.newArrayList("South Auckland", "969"),
            Lists.newArrayList("Canterbury", "345")
    ));
  }

  private void assertResults(Path geoPackageResultFile) {
    assertThat(outBytes.toString(), allOf(
        containsString(String.format("%s?layer=cities", geoPackageResultFile.toString())),
        containsString(String.format("%s?layer=raw-results", geoPackageResultFile.toString())),
        containsString(String.format("%s?layer=countries", geoPackageResultFile.toString())),
        containsString(String.format("%s?layer=results", geoPackageResultFile.toString()))
    ));

    assertThat(collectedSinkProblems, allOf(
        // one of the save steps in the pipeline has specified geojson output format
        hasItem(
            renderedAs(
                // check we are given some context about which step is producing these warnings, note that we are still
                // missing a file path from the warning, which is desired
                containsString("Problems found with `save` step on line 16"),
                containsInAnyOrder(
                    is(OutputProblems.get().userSpecifiedFormatIgnored(
                      new GeoJSONFormat(),
                      engine.getPipelineOutputStores().get("geopackage", null))
                    ),
                    Matchers.isProblem(Severity.WARNING, OutputProblems.class, "outputTypeAsText")
                )
            )
        )
    ));

    // now we fetch the results (from the GeoPackage) and write them to csv's for easier test assertions.
    fetchResultsAsCsv(geoPackageResultFile, "cities", "raw-results", "countries", "results");

    assertThat(readColumnsFromCsv(getTempDirectory().resolve("cities.csv"), "geom", "name"), containsInAnyOrder(
        Arrays.asList("POINT (151 -33)", "Sydney"),
        Arrays.asList("POINT (174 -36)", "Auckland"),
        Arrays.asList("POINT (174 -41)", "Wellington")
    ));

    assertThat(readColumnsFromCsv(getTempDirectory().resolve("countries.csv"), "geom", "name"), containsInAnyOrder(
        Arrays.asList("POLYGON ((166 -34, 166 -48, 179 -48, 179 -34, 166 -34))", "New Zealand")
    ));

    assertThat(readColumnsFromCsv(getTempDirectory().resolve("cities.csv"), "geom", "name"), containsInAnyOrder(
        Arrays.asList("POINT (151 -33)", "Sydney"),
        Arrays.asList("POINT (174 -36)", "Auckland"),
        Arrays.asList("POINT (174 -41)", "Wellington")
    ));

    assertThat(readColumnsFromCsv(getTempDirectory().resolve("results.csv"), "geom", "name", "country"),
        containsInAnyOrder(
        Arrays.asList("POINT (151 -33)", "Sydney", ""),
        Arrays.asList("POINT (174 -36)", "Auckland", "New Zealand"),
        Arrays.asList("POINT (174 -41)", "Wellington", "New Zealand")
    ));

    assertThat(readColumnsFromCsv(getTempDirectory().resolve("raw-results.csv"), "geom", "name", "country"),
        containsInAnyOrder(
        Arrays.asList("POINT (151 -33)", "Sydney", ""),
        Arrays.asList("POINT (174 -36)", "Auckland", "New Zealand"),
        Arrays.asList("POINT (174 -41)", "Wellington", "New Zealand")
    ));
  }

  private void fetchResultsAsCsv(Path geoPackageResultFile, String... layers) {
    // fetches the requested layers from geoPackageResultFile and saves them as CSV files.
    for (String layer : layers) {
      runCommand.modelId = "to-csv";
      runCommand.parameters = Lists.newArrayList(
          String.format("input='%s?layer=%s'", geoPackageResultFile, layer),
          String.format("name='%s'", layer)
      );
      runCommand.runnerOptions.replace = true;
      runCommand.runnerOptions.output = getTempDirectory().toString();

      runCommand.run();
    }
  }

}
