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

import static nz.org.riskscape.engine.RelationMatchers.*;
import static nz.org.riskscape.engine.StructMatchers.*;
import static nz.org.riskscape.engine.TypeMatchers.*;
import static nz.org.riskscape.engine.output.ShapeFileNullMapper.*;
import static org.hamcrest.Matchers.*;
import static org.junit.Assert.*;

import java.io.IOException;
import java.net.MalformedURLException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.Arrays;
import java.util.Map;

import org.geotools.data.shapefile.ShapefileDataStore;
import org.geotools.data.shapefile.ShapefileDataStoreFactory;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import org.locationtech.jts.geom.Geometry;
import org.locationtech.jts.geom.Point;

import com.google.common.collect.ImmutableMap;
import com.google.common.collect.Lists;

import nz.org.riskscape.engine.Engine;
import nz.org.riskscape.engine.ProjectTest;
import nz.org.riskscape.engine.TemporaryDirectoryTestHelper;
import nz.org.riskscape.engine.Tuple;
import nz.org.riskscape.engine.data.Bookmark;
import nz.org.riskscape.engine.data.relation.FeatureSourceBookmarkResolver;
import nz.org.riskscape.engine.data.relation.FeatureSourceBookmarkResolver.FeatureSourceBookmarkParams;
import nz.org.riskscape.engine.gt.GeometryHelper;
import nz.org.riskscape.engine.relation.Relation;
import nz.org.riskscape.engine.types.Geom;
import nz.org.riskscape.engine.types.Nullable;
import nz.org.riskscape.engine.types.Referenced;
import nz.org.riskscape.engine.types.Struct;
import nz.org.riskscape.engine.types.Types;
import nz.org.riskscape.problem.ProblemSink;

/**
 * Tests that {@link ShapefileWriter2} work with the geotools API's that it makes use of to produces
 * the expected shapefile. And that it can be read again.
 *
 * As opposed to {@link ShapefileWriter2Test} which is more of a unit test for the writer.
 */
@SuppressWarnings("unchecked")
public class ShapefileWriter2IntegrationTest extends ProjectTest implements TemporaryDirectoryTestHelper {

  /**
   * Shameless copy of ShapefileBookmarkResolver. Needed here as engine project can't access
   * defaults.
   */
  public static class TestShapefileBookmarkResolver
      extends FeatureSourceBookmarkResolver<ShapefileDataStore, FeatureSourceBookmarkParams> {

    public static final Map<String, String> EXTENSIONS_TO_FORMATS
        = ImmutableMap.of("shp", "shapefile");

    public TestShapefileBookmarkResolver(Engine engine) {
      super(engine);
    }

    private final ShapefileDataStoreFactory datastoreFactory = new ShapefileDataStoreFactory();

    @Override
    protected ShapefileDataStore createDataStore(FeatureSourceBookmarkParams bookmark)
        throws MalformedURLException, IOException {
      Path shapefile = getBookmarkedPath(bookmark);
      return (ShapefileDataStore) datastoreFactory.createDataStore(shapefile.toUri().toURL());
    }

    @Override
    protected Map<String, String> getExtensionsToFormats() {
      return EXTENSIONS_TO_FORMATS;
    }
  }

  GeometryHelper geomHelper = GeometryHelper.nzMapGridWithOriginShift(project.getSridSet());
  TestShapefileBookmarkResolver resolver = new TestShapefileBookmarkResolver(project.getEngine());

  Path tempDir;
  Path shapefile;
  RiskscapeWriter writer;
  boolean prjSingleLine = false;

  @Before
  public void setup() throws IOException {
    tempDir = createTempDirectory("ShapefileWriter2IntegrationTest");
  }

  @After
  public void breakDown() throws Exception {
    remove(tempDir);
  }

  void setupWriter(Struct toWrite) throws IOException {
    shapefile = tempDir.resolve("shapefile.shp");
    writer = new ShapefileWriter2(
            shapefile.toFile(),
            toWrite,
            project.getSridSet(),
            ProblemSink.DEVNULL,
            prjSingleLine);
  }

  @Test
  public void canWriteASimpleStruct() throws Exception {
    Struct type = Struct.of("the_geom", Types.GEOMETRY, "name", Types.TEXT, "value", Types.INTEGER);

    Tuple t1 = Tuple.ofValues(type, geomHelper.point(10, 10), "foo", 10L);
    Tuple t2 = Tuple.ofValues(type, geomHelper.point(20, 10), "bar", 20L);

    setupWriter(type);
    write(t1, t2);
    writer.close();

    Relation written = getWrittenAsRelation(false);

    assertThat(written.getType(), isStruct(
        isStructMember("the_geom", isTypeWrapping(Geom.class)),
        isStructMember("name", Types.TEXT),
        isStructMember("value", Types.INTEGER)
    ));
    assertThat(written, withTuples(Lists.newArrayList(
        Tuple.ofValues(written.getType(), t1.toArray()),
        Tuple.ofValues(written.getType(), t2.toArray())
    )));
  }

  @Test
  public void canWriteASimpleStructWithReferencedGeom() throws Exception {
    Struct type = Struct.of("the_geom", Referenced.of(Types.GEOMETRY, geomHelper.getCrs()),
        "name", Types.TEXT, "value", Types.INTEGER);

    Tuple t1 = Tuple.ofValues(type, geomHelper.point(10, 10), "foo", 10L);
    Tuple t2 = Tuple.ofValues(type, geomHelper.point(20, 10), "bar", 20L);

    setupWriter(type);
    write(t1, t2);
    writer.close();

    Relation written = getWrittenAsRelation(false);

    assertThat(written.getType(), isStruct(
        isStructMember("the_geom", isTypeWrapping(Geom.class)),
        isStructMember("name", Types.TEXT),
        isStructMember("value", Types.INTEGER)
    ));
    assertThat(written, withTuples(Lists.newArrayList(
        Tuple.ofValues(written.getType(), t1.toArray()),
        Tuple.ofValues(written.getType(), t2.toArray())
    )));
  }

  @Test
  public void usesFirstGeometryItFinds() throws Exception {
    Struct nested = Struct.of("geom", Types.GEOMETRY, "name", Types.TEXT);
    Struct type = Struct.of("a", nested, "b", nested);

    Geometry point1 = geomHelper.point(10, 10);
    Geometry point2 = geomHelper.point(20, 10);
    Tuple t1 = Tuple.ofValues(nested, point1, "foo");
    Tuple t2 = Tuple.ofValues(nested, point2, "bar");

    setupWriter(type);
    write(Tuple.ofValues(type, t1, t2));
    writer.close();

    Relation written = getWrittenAsRelation(false);

    assertThat(written.getType(), isStruct(
        isStructMember("the_geom", isTypeWrapping(Geom.class)),
        isStructMember("name", Types.TEXT),
        isStructMember("geom", Types.TEXT),
        isStructMember("name_1", Types.TEXT)
    ));
    // first geom found is written as is, second one is written as WKT
    assertThat(written, withTuples(Lists.newArrayList(
        Tuple.ofValues(written.getType(), point1, "foo", point2.toText(), "bar")
    )));
  }

  @Test
  public void mapsNullValuesInSimpleTypes() throws Exception {
    Struct type = Struct.of("geom", Types.GEOMETRY, "foo", Nullable.TEXT, "bar", Types.TEXT);

    Point point1 = geomHelper.point(10, 10);
    Point point2 = geomHelper.point(20, 10);
    Tuple t1 = Tuple.ofValues(type, point1, "foo", "bar");
    Tuple t2 = Tuple.ofValues(type, point2, null, "bar");

    setupWriter(type);
    write(t1, t2);
    writer.close();

    Relation written = getWrittenAsRelation(false);

    assertThat(written.getType(), isStruct(
        isStructMember("the_geom", isTypeWrapping(Geom.class)),
        isStructMember("foo", Types.TEXT),
        isStructMember("bar", Types.TEXT)
    ));
    assertThat(written, withTuples(Lists.newArrayList(
        Tuple.ofValues(written.getType(), point1, "foo", "bar"),
        Tuple.ofValues(written.getType(), point2, NULL_VALUE_TEXT, "bar")
    )));
  }

  @Test
  public void mapsNullValuesInStructTypes() throws Exception {
    Struct nested = Struct.of("a", Types.TEXT, "b", Types.INTEGER);
    Struct type = Struct.of("geom", Types.GEOMETRY, "foo", Nullable.of(nested), "bar", Types.TEXT);

    Tuple t1 = Tuple.ofValues(type, geomHelper.point(10, 10), Tuple.ofValues(nested, "foo-a", 30L), "bar");
    Tuple t2 = Tuple.ofValues(type, geomHelper.point(20, 10), null, "bar");

    setupWriter(type);
    write(t1, t2);
    writer.close();

    Relation written = getWrittenAsRelation(false);

    assertThat(written.getType(), isStruct(
        isStructMember("the_geom", isTypeWrapping(Geom.class)),
        isStructMember("a", Types.TEXT),
        isStructMember("b", Types.INTEGER),
        isStructMember("bar", Types.TEXT)
    ));
    assertThat(written, withTuples(Lists.newArrayList(
        Tuple.ofValues(written.getType(), t1.toArray()[0], "foo-a", 30L, "bar"),
        Tuple.ofValues(written.getType(), t2.toArray()[0], NULL_VALUE_TEXT, NULL_VALUE_NUMBER, "bar")
    )));
  }

  @Test
  public void canWriteRelationWithNullGeometries() throws Exception {
    Struct type = Struct.of("geom", Nullable.GEOMETRY, "foo", Types.TEXT);

    Tuple hasGeom = Tuple.ofValues(type, geomHelper.point(20, 20), "bar");
    Tuple noGeom = Tuple.ofValues(type, null, "null-bar");

    setupWriter(type);
    write(hasGeom, noGeom);
    writer.close();

    // we read with skip-invalid = true, because null geoms aren't valid
    Relation written = getWrittenAsRelation(true);

    assertThat(written.getType(), isStruct(
        isStructMember("the_geom", isTypeWrapping(Geom.class)),
        isStructMember("foo", Types.TEXT)
    ));
    assertThat(written, withTuples(Lists.newArrayList(
        Tuple.ofValues(written.getType(), hasGeom.toArray())
    // noGeom is skipped because it isn't valid
    )));
  }

  /**
   * Same as {@link #canWriteRelationWithNullGeometries() } except that the tuple with null geometry
   * is the first to be written. Sadly this upsets the writer because the shapeType has not been set,
   * but we can't do that until we know what the geometry is going to be.
   *
   */
  @Test
  public void canWriteRelationWithNullGeometryFirst() throws Exception {
    Struct type = Struct.of("geom", Nullable.of(Types.POINT), "foo", Types.TEXT);

    Tuple hasGeom = Tuple.ofValues(type, geomHelper.point(20, 20), "bar");
    Tuple noGeom = Tuple.ofValues(type, null, "null-bar");

    setupWriter(type);
    write(
        noGeom,
        hasGeom
    );
    writer.close();

    // we read with skip-invalid = true, because null geoms aren't valid
    Relation written = getWrittenAsRelation(true);

    assertThat(written.getType(), isStruct(
        isStructMember("the_geom", isTypeWrapping(Geom.class)),
        isStructMember("foo", Types.TEXT)
    ));
    assertThat(written, withTuples(Lists.newArrayList(
        Tuple.ofValues(written.getType(), hasGeom.toArray())
    // noGeom is skipped because it isn't valid
    )));
  }

  @Test
  public void canWriteAnEmptyRelation() throws Exception {
    Struct type = Struct.of("the_geom", Types.GEOMETRY, "name", Types.TEXT, "value", Types.INTEGER);

    setupWriter(type);
    writer.close();
    assertTrue(shapefile.toFile().exists());
  }

  @Test
  public void canWriteAndReadbackWithSingleLinePrj() throws Exception{
    prjSingleLine = true;
    Struct type = Struct.of("the_geom", Types.GEOMETRY, "name", Types.TEXT, "value", Types.INTEGER);

    // Write the shapefile out
    setupWriter(type);
    writer.close();

    // Make sure that the prj has actually been written in a single line
    assertThat(Files.readString(shapefile.resolveSibling("shapefile.prj")), not(containsString("\n")));

    // Load the shapefile back in to make sure there's no problems with it
    getWrittenAsRelation(false);
  }


  void write(Tuple... values) {
    for (Tuple value : values) {
      writer.write(value);
    }
  }

  Relation getWrittenAsRelation(boolean skipInvalid) {
    Bookmark bookmark = new Bookmark("test", "", "", shapefile.toUri(),
        ImmutableMap.of("skip-invalid", Arrays.asList(String.valueOf(skipInvalid))
    ));
    return resolver.resolve(bookmark, executionContext)
        .map(resolved -> resolved.getData(Relation.class).get())
        .orElse(null);
  }

}
