/*
 * 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 org.hamcrest.Matchers.*;
import static org.junit.Assert.*;

import java.io.IOException;
import java.util.Date;

import org.junit.Test;
import org.locationtech.jts.geom.LinearRing;

import nz.org.riskscape.engine.ProjectTest;
import nz.org.riskscape.engine.SRIDSet;
import nz.org.riskscape.engine.Tuple;
import nz.org.riskscape.engine.gt.GeometryHelper;
import nz.org.riskscape.engine.resource.ByteArrayCreateHandle;
import nz.org.riskscape.engine.types.Nullable;
import nz.org.riskscape.engine.types.OfUnit;
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.engine.types.WithinRange;
import nz.org.riskscape.engine.types.WithinSet;

public class KmlWriterTest extends ProjectTest {

  GeometryHelper latLongHelper = GeometryHelper.latLong(project.getSridSet());

  @Test
  public void canWritePoints() {
    Struct type = Struct.of("geom", Types.POINT, "foo", Types.TEXT);
    String written = write(Tuple.ofValues(type, latLongHelper.point(10, 20), "bar"));
    String expected = "<?xml version=\"1.0\" encoding=\"UTF-8\"?>"
        + "<kml xmlns=\"http://www.opengis.net/kml/2.2\">"
        + "<Document id=\"root_doc\">"
        + "<Schema name=\"riskscape\" id=\"riskscape\">"
        + "<SimpleField name=\"foo\" type=\"string\"></SimpleField>"
        + "</Schema><Folder>"
        + "<Placemark><ExtendedData><SchemaData schemaUrl=\"#riskscape\">"
        + "<SimpleData name=\"foo\">bar</SimpleData>"
        + "</SchemaData></ExtendedData>"
        + "<Point><coordinates>20.000000,10.000000</coordinates></Point>"
        + "</Placemark>"
        + "</Folder></Document></kml>";

    assertEquals(expected, written);
  }

  @Test
  public void canWriteFileWithNoFeatures() {
    Struct type = Struct.of("geom", Types.POINT, "foo", Types.TEXT);
    String written = write(type);
    String expected = "<?xml version=\"1.0\" encoding=\"UTF-8\"?>"
        + "<kml xmlns=\"http://www.opengis.net/kml/2.2\">"
        + "<Document id=\"root_doc\">"
        + "<Schema name=\"riskscape\" id=\"riskscape\">"
        + "<SimpleField name=\"foo\" type=\"string\"></SimpleField>"
        + "</Schema><Folder>"
        + "</Folder></Document></kml>";

    assertEquals(expected, written);
  }

  @Test
  public void canWriteLines() {
    Struct type = Struct.of("geom", Types.LINE, "foo", Types.TEXT);
    String written = write(Tuple.ofValues(type, latLongHelper.line(10, 20, 30, 20), "bar"));
    String expected = "<LineString><coordinates>20.000000,10.000000 20.000000,30.000000"
        + "</coordinates></LineString>";

    assertThat(written, containsString(expected));
  }

  @Test
  public void canWritePolygon() {
    Struct type = Struct.of("geom", Types.POLYGON, "foo", Types.TEXT);
    String written = write(Tuple.ofValues(type, latLongHelper.box(10, 20, 30, 20), "bar")
    );
    String expected = "<Polygon><outerBoundaryIs><LinearRing>"
        + "<coordinates>20.000000,10.000000 20.000000,30.000000 20.000000,30.000000 20.000000,10.000000 "
        + "20.000000,10.000000</coordinates>"
        + "</LinearRing></outerBoundaryIs></Polygon>";

    assertThat(written, containsString(expected));
  }

  @Test
  public void canWritePolygonWithHole() {
    Struct type = Struct.of("geom", Types.POLYGON, "foo", Types.TEXT);
    LinearRing shell = latLongHelper.box(10, 20, 30, 30).getExteriorRing();
    LinearRing hole = latLongHelper.box(12, 22, 28, 28).getExteriorRing();
    String written = write(
        Tuple.ofValues(type, latLongHelper.box(shell, hole), "bar")
    );
    String expected = "<Polygon><outerBoundaryIs><LinearRing>"
        + "<coordinates>20.000000,10.000000 20.000000,30.000000 30.000000,30.000000 30.000000,10.000000 "
        + "20.000000,10.000000</coordinates>"
        + "</LinearRing></outerBoundaryIs><innerBoundaryIs><LinearRing>"
        + "<coordinates>22.000000,12.000000 22.000000,28.000000 28.000000,28.000000 28.000000,12.000000 "
        + "22.000000,12.000000</coordinates>"
        + "</LinearRing></innerBoundaryIs></Polygon>";

    assertThat(written, containsString(expected));
  }

  @Test
  public void canWriteNullableGeometries() {
    Struct type = Struct.of("geom", Nullable.GEOMETRY, "foo", Types.TEXT);
    String written = write(
        Tuple.ofValues(type, null, "baz"),
        Tuple.ofValues(type, latLongHelper.point(10, 20), "bar"));
    String expected = "<?xml version=\"1.0\" encoding=\"UTF-8\"?>"
        + "<kml xmlns=\"http://www.opengis.net/kml/2.2\">"
        + "<Document id=\"root_doc\">"
        + "<Schema name=\"riskscape\" id=\"riskscape\">"
        + "<SimpleField name=\"foo\" type=\"string\"></SimpleField>"
        + "</Schema><Folder>"
        + "<Placemark><ExtendedData><SchemaData schemaUrl=\"#riskscape\">"
        + "<SimpleData name=\"foo\">baz</SimpleData>"
        + "</SchemaData></ExtendedData>"  // note no geometry elememt in this placemark
        + "</Placemark>"
        + "<Placemark><ExtendedData><SchemaData schemaUrl=\"#riskscape\">"
        + "<SimpleData name=\"foo\">bar</SimpleData>"
        + "</SchemaData></ExtendedData>"
        + "<Point><coordinates>20.000000,10.000000</coordinates></Point>"
        + "</Placemark>"
        + "</Folder></Document></kml>";

    assertEquals(expected, written);
  }

  @Test
  public void canWriteMultiGeometry() {
    Struct type = Struct.of("geom", Types.GEOMETRY, "foo", Types.TEXT);
    String written = write(
        Tuple.ofValues(type, latLongHelper.multiPoint(
            latLongHelper.point(10, 15),
            latLongHelper.point(20, 30)
        ), "bar")
    );

    String expected = "<MultiGeometry>"
        + "<Point><coordinates>15.000000,10.000000</coordinates></Point>"
        + "<Point><coordinates>30.000000,20.000000</coordinates></Point>"
        + "</MultiGeometry>";

    assertThat(written, containsString(expected));
  }

  @Test
  public void canWriteStandardTypes() {
    Struct type = Struct.of("geom", Types.GEOMETRY, "integer", Types.INTEGER, "float", Types.FLOATING,
        "bool", Types.BOOLEAN);
    String written = write(Tuple.ofValues(type, latLongHelper.point(10, 15), 15L, 5.5D, false));
    assertThat(written, allOf(
        containsString("<SimpleField name=\"integer\" type=\"int\"></SimpleField>"),
        containsString("<SimpleField name=\"float\" type=\"double\"></SimpleField>"),
        containsString("<SimpleField name=\"bool\" type=\"bool\"></SimpleField>"),
        containsString("<SimpleData name=\"integer\">15</SimpleData>"),
        containsString("<SimpleData name=\"float\">5.5</SimpleData>"),
        containsString("<SimpleData name=\"bool\">false</SimpleData>")
    ));
  }

  @Test
  public void canWriteWrappingTypes() {
    Struct type = Struct.of("geom", Referenced.of(Types.GEOMETRY, SRIDSet.EPSG4326_LONLAT),
        "integer", new WithinRange(Types.INTEGER, 0, 20),
        "text", new WithinSet(Types.TEXT, "foo", "bar"),
        "length", new OfUnit("metre", Types.FLOATING));
    String written = write(Tuple.ofValues(type, latLongHelper.point(10, 15), 15L, "bar", 25.3));
    assertThat(written, allOf(
        containsString("<SimpleField name=\"integer\" type=\"int\"></SimpleField>"),
        containsString("<SimpleField name=\"text\" type=\"string\"></SimpleField>"),
        containsString("<SimpleField name=\"length\" type=\"double\"></SimpleField>"),
        containsString("<SimpleData name=\"integer\">15</SimpleData>"),
        containsString("<SimpleData name=\"text\">bar</SimpleData>"),
        containsString("<SimpleData name=\"length\">25.3</SimpleData>")
    ));
  }

  @Test
  public void someTypesMapToKmlString() {
    Struct type = Struct.of("geom", Referenced.of(Types.GEOMETRY, SRIDSet.EPSG4326_LONLAT),
        "date", Types.DATE,
        "geom1", Types.GEOMETRY);
    Date date = new Date();
    String written = write(Tuple.ofValues(type, latLongHelper.point(10, 15), date,
        latLongHelper.point(10, 15)));
    assertThat(written, allOf(
        containsString("<SimpleField name=\"date\" type=\"string\"></SimpleField>"),
        containsString("<SimpleField name=\"geom1\" type=\"string\"></SimpleField>"),
        containsString("<SimpleData name=\"date\">" + date.toString() + "</SimpleData>"),
        containsString("<SimpleData name=\"geom1\">POINT (10 15)</SimpleData>")
    ));
  }

  @Test
  public void nullValuesAreNotWritten() {
    Struct type = Struct.of("geom", Types.POINT, "foo", Nullable.INTEGER, "bar", Types.TEXT);
    String written = write(Tuple.ofValues(type, latLongHelper.point(10, 20), null, "bar"));
    String expected = "<?xml version=\"1.0\" encoding=\"UTF-8\"?>"
        + "<kml xmlns=\"http://www.opengis.net/kml/2.2\">"
        + "<Document id=\"root_doc\">"
        + "<Schema name=\"riskscape\" id=\"riskscape\">"
        + "<SimpleField name=\"foo\" type=\"int\"></SimpleField>"
        + "<SimpleField name=\"bar\" type=\"string\"></SimpleField>"
        + "</Schema><Folder>"
        + "<Placemark><ExtendedData><SchemaData schemaUrl=\"#riskscape\">"
        // note no foo here
        + "<SimpleData name=\"bar\">bar</SimpleData>"
        + "</SchemaData></ExtendedData>"
        + "<Point><coordinates>20.000000,10.000000</coordinates></Point>"
        + "</Placemark>"
        + "</Folder></Document></kml>";

    assertEquals(expected, written);
  }

  @Test
  public void flattensOutNestedData() {
    Struct childType = Struct.of("geom", Types.POINT, "foo", Types.TEXT);
    Struct type = Struct.of("child", childType, "bar", Types.TEXT);
    String written = write(Tuple.ofValues(type,
        Tuple.ofValues(childType, latLongHelper.point(10, 20), "foo"),
        "bar")
    );
    String expected = "<?xml version=\"1.0\" encoding=\"UTF-8\"?>"
        + "<kml xmlns=\"http://www.opengis.net/kml/2.2\">"
        + "<Document id=\"root_doc\">"
        + "<Schema name=\"riskscape\" id=\"riskscape\">"
        + "<SimpleField name=\"child.foo\" type=\"string\"></SimpleField>"
        + "<SimpleField name=\"bar\" type=\"string\"></SimpleField>"
        + "</Schema><Folder>"
        + "<Placemark><ExtendedData><SchemaData schemaUrl=\"#riskscape\">"
        + "<SimpleData name=\"child.foo\">foo</SimpleData>"
        + "<SimpleData name=\"bar\">bar</SimpleData>"
        + "</SchemaData></ExtendedData>"
        + "<Point><coordinates>20.000000,10.000000</coordinates></Point>"
        + "</Placemark>"
        + "</Folder></Document></kml>";

    assertEquals(expected, written);
  }

  @Test
  public void flattensOutNestedDataWillDuplicateAttr() {
    Struct childType = Struct.of("geom", Types.POINT, "foo", Types.TEXT);
    Struct type = Struct.of("child", childType, "child.foo", Types.TEXT);
    String written = write(Tuple.ofValues(type,
        Tuple.ofValues(childType, latLongHelper.point(10, 20), "foo"),
        "bar")
    );
    String expected = "<?xml version=\"1.0\" encoding=\"UTF-8\"?>"
        + "<kml xmlns=\"http://www.opengis.net/kml/2.2\">"
        + "<Document id=\"root_doc\">"
        + "<Schema name=\"riskscape\" id=\"riskscape\">"
        + "<SimpleField name=\"child.foo\" type=\"string\"></SimpleField>"
        + "<SimpleField name=\"child.foo.1\" type=\"string\"></SimpleField>"
        + "</Schema><Folder>"
        + "<Placemark><ExtendedData><SchemaData schemaUrl=\"#riskscape\">"
        + "<SimpleData name=\"child.foo\">foo</SimpleData>"
        + "<SimpleData name=\"child.foo.1\">bar</SimpleData>"
        + "</SchemaData></ExtendedData>"
        + "<Point><coordinates>20.000000,10.000000</coordinates></Point>"
        + "</Placemark>"
        + "</Folder></Document></kml>";

    assertEquals(expected, written);
  }

  @Test
  public void willUseKmlSchemaFields() {
    Struct type = Struct.of("Geometry", Referenced.of(Types.GEOMETRY, latLongHelper.getCrs()))
        .and("name", Nullable.TEXT)
        .and("visibility", Types.BOOLEAN)
        .and("open", Types.BOOLEAN)
        .and("address", Nullable.TEXT)
        .and("phoneNumber", Nullable.TEXT)
        .and("description", Nullable.TEXT);

    String written = write(Tuple.ofValues(type, latLongHelper.point(20, 30), "foo", false, true, "foo address",
        "0800-for-foo", "so much foo"));

    // the attributes in type match the standard KML data properties, so we use those.
    // note also that there is no schema because we don't need one
    String expected = "<?xml version=\"1.0\" encoding=\"UTF-8\"?>"
        + "<kml xmlns=\"http://www.opengis.net/kml/2.2\">"
        + "<Document id=\"root_doc\"><Folder>"
        + "<Placemark><name>foo</name><visibility>false</visibility>"
        + "<open>true</open><address>foo address</address>"
        + "<phoneNumber>0800-for-foo</phoneNumber><description>so much foo</description>"
        + "<Point><coordinates>30.000000,20.000000</coordinates></Point>"
        + "</Placemark></Folder></Document></kml>";

    assertEquals(expected, written);
  }

  @Test
  public void willUseKmlSchemaFieldsIfTypesMatchUp() {
    Struct type = Struct.of("Geometry", Referenced.of(Types.GEOMETRY, latLongHelper.getCrs()))
        .and("name", Nullable.TEXT)
        .and("visibility", Types.TEXT)   // should be bool
        .and("open", Types.TEXT)         // should be bool
        .and("address", Nullable.TEXT)
        .and("phoneNumber", Nullable.TEXT)
        .and("description", Nullable.TEXT);

    String written = write(Tuple.ofValues(type, latLongHelper.point(20, 30), "foo", "foo-visibility", "foo-open",
        "foo address", "0800-for-foo", "so much foo"));

    // because visibility and open are not boolean, they get pushed to ExtendedData and renamed with -1 suffix
    String expected = "<?xml version=\"1.0\" encoding=\"UTF-8\"?>"
        + "<kml xmlns=\"http://www.opengis.net/kml/2.2\">"
        + "<Document id=\"root_doc\">"
        + "<Schema name=\"riskscape\" id=\"riskscape\">"
        + "<SimpleField name=\"visibility-1\" type=\"string\"></SimpleField>"
        + "<SimpleField name=\"open-1\" type=\"string\"></SimpleField>"
        + "</Schema><Folder>"
        + "<Placemark><name>foo</name><address>foo address</address>"
        + "<phoneNumber>0800-for-foo</phoneNumber><description>so much foo</description>"
        + "<ExtendedData><SchemaData schemaUrl=\"#riskscape\">"
        + "<SimpleData name=\"visibility-1\">foo-visibility</SimpleData>"
        + "<SimpleData name=\"open-1\">foo-open</SimpleData>"
        + "</SchemaData></ExtendedData>"
        + "<Point><coordinates>30.000000,20.000000</coordinates></Point>"
        + "</Placemark>"
        + "</Folder></Document></kml>";

    assertEquals(expected, written);
  }

  @Test
  public void subsequentGeometriesAreStrings() {
    Struct type = Struct.of("geom", Referenced.of(Types.GEOMETRY, latLongHelper.getCrs()),
        "the_geom", Referenced.of(Types.GEOMETRY, latLongHelper.getCrs()),
        "Geometry", Referenced.of(Types.GEOMETRY, latLongHelper.getCrs()));

    String written = write(Tuple.ofValues(type,
        latLongHelper.point(20, 30),
        latLongHelper.point(40, 30),
        latLongHelper.point(60, 30)));

    String expected = "<?xml version=\"1.0\" encoding=\"UTF-8\"?>"
        + "<kml xmlns=\"http://www.opengis.net/kml/2.2\">"
        + "<Document id=\"root_doc\">"
        + "<Schema name=\"riskscape\" id=\"riskscape\">"
        + "<SimpleField name=\"the_geom\" type=\"string\"></SimpleField>"
        + "<SimpleField name=\"Geometry-1\" type=\"string\"></SimpleField>"
        + "</Schema><Folder>"
        + "<Placemark><ExtendedData><SchemaData schemaUrl=\"#riskscape\">"
        + "<SimpleData name=\"the_geom\">POINT (40 30)</SimpleData>"
        + "<SimpleData name=\"Geometry-1\">POINT (60 30)</SimpleData>"
        + "</SchemaData></ExtendedData>"
        + "<Point><coordinates>30.000000,20.000000</coordinates></Point>"
        + "</Placemark>"
        + "</Folder></Document></kml>";

    assertEquals(expected, written);
  }

  private String write(Tuple... values) {
    return write(values[0].getStruct(), values);
  }

  private String write(Struct type, Tuple... values) {
    ByteArrayCreateHandle handle = new ByteArrayCreateHandle();
    RiskscapeWriter writer = KmlWriter.of(type, handle, project).get();
    for (Tuple value : values) {
      writer.write(value);
    }
    // sanity check that the writer doesn't know where the files is stored before close is called
    assertNull(writer.getStoredAt());
    try {
      writer.close();
    } catch (IOException e) {
      throw new RuntimeException(e);
    }
    // and now the writer does know where the file is stored.
    assertNotNull(writer.getStoredAt());
    return handle.getWritten();
  }

}
