package com.onthegomap.flatmap.openmaptiles;

import static com.onthegomap.flatmap.TestUtils.assertContains;
import static com.onthegomap.flatmap.TestUtils.assertFeatureNear;
import static com.onthegomap.flatmap.TestUtils.gunzip;
import static org.junit.jupiter.api.Assertions.assertEquals;

import com.onthegomap.flatmap.TestUtils;
import com.onthegomap.flatmap.VectorTile;
import com.onthegomap.flatmap.config.Arguments;
import com.onthegomap.flatmap.mbiles.Mbtiles;
import java.io.IOException;
import java.nio.file.Path;
import java.util.Map;
import java.util.Set;
import org.junit.jupiter.api.AfterAll;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.io.TempDir;
import org.locationtech.jts.geom.Envelope;
import org.locationtech.jts.geom.Geometry;
import org.locationtech.jts.geom.LineString;
import org.locationtech.jts.geom.Point;
import org.locationtech.jts.geom.Polygon;

/**
 * End-to-end tests for OpenMapTiles map generation.
 * <p>
 * Generates an entire map for the smallest openstreetmap extract available (Monaco) and asserts that expected output
 * features exist
 */
public class OpenMapTilesTest {

  @TempDir
  static Path tmpDir;
  private static Mbtiles mbtiles;

  private static final Envelope monacoBounds = new Envelope(7.40921, 7.44864, 43.72335, 43.75169);

  @BeforeAll
  public static void runFlatmap() throws Exception {
    Path dbPath = tmpDir.resolve("output.mbtiles");
    OpenMapTilesMain.run(Arguments.of(
      // Override input source locations
      "osm_path", TestUtils.pathToResource("monaco-latest.osm.pbf"),
      "natural_earth_path", TestUtils.pathToResource("natural_earth_vector.sqlite"),
      "water_polygons_path", TestUtils.pathToResource("water-polygons-split-3857.zip"),
      // no centerlines in monaco - so fake it out with an empty source
      "lake_centerlines_path", TestUtils.pathToResource("water-polygons-split-3857.zip"),

      // Override temp dir location
      "tmp", tmpDir.toString(),

      // Override output location
      "mbtiles", dbPath.toString()
    ));
    mbtiles = Mbtiles.newReadOnlyDatabase(dbPath);
  }

  @AfterAll
  public static void close() throws IOException {
    mbtiles.close();
  }

  @Test
  public void testMetadata() {
    Map<String, String> metadata = mbtiles.metadata().getAll();
    assertEquals("OpenMapTiles", metadata.get("name"));
    assertEquals("0", metadata.get("minzoom"));
    assertEquals("14", metadata.get("maxzoom"));
    assertEquals("baselayer", metadata.get("type"));
    assertEquals("pbf", metadata.get("format"));
    assertEquals("7.40921,43.72335,7.44864,43.75169", metadata.get("bounds"));
    assertEquals("7.42892,43.73752,14", metadata.get("center"));
    assertContains("openmaptiles.org", metadata.get("description"));
    assertContains("openmaptiles.org", metadata.get("attribution"));
    assertContains("www.openstreetmap.org/copyright", metadata.get("attribution"));
  }

  @Test
  public void ensureValidGeometries() throws Exception {
    Set<Mbtiles.TileEntry> parsedTiles = TestUtils.getAllTiles(mbtiles);
    for (var tileEntry : parsedTiles) {
      var decoded = VectorTile.decode(gunzip(tileEntry.bytes()));
      for (VectorTile.Feature feature : decoded) {
        TestUtils.validateGeometry(feature.geometry().decode());
      }
    }
  }

  @Test
  public void testContainsOceanPolyons() {
    assertFeatureNear(mbtiles, "water", Map.of(
      "class", "ocean"
    ), 7.4484, 43.70783, 0, 14);
  }

  @Test
  public void testContainsCountryName() {
    assertFeatureNear(mbtiles, "place", Map.of(
      "class", "country",
      "iso_a2", "MC",
      "name", "Monaco"
    ), 7.42769, 43.73235, 2, 14);
  }

  @Test
  public void testContainsSuburb() {
    assertFeatureNear(mbtiles, "place", Map.of(
      "name", "Les Moneghetti",
      "class", "suburb"
    ), 7.41746, 43.73638, 11, 14);
  }

  @Test
  public void testContainsBuildings() {
    assertFeatureNear(mbtiles, "building", Map.of(), 7.41919, 43.73401, 13, 14);
    assertNumFeatures("building", Map.of(), 14, 1316, Polygon.class);
    assertNumFeatures("building", Map.of(), 13, 196, Polygon.class);
  }

  @Test
  public void testContainsHousenumber() {
    assertFeatureNear(mbtiles, "housenumber", Map.of(
      "housenumber", "27"
    ), 7.42117, 43.73652, 14, 14);
    assertNumFeatures("housenumber", Map.of(), 14, 274, Point.class);
  }

  @Test
  public void testBoundary() {
    assertFeatureNear(mbtiles, "boundary", Map.of(
      "admin_level", 2L,
      "maritime", 1L,
      "disputed", 0L
    ), 7.41884, 43.72396, 4, 14);
  }

  @Test
  public void testAeroway() {
    assertNumFeatures("aeroway", Map.of(
      "class", "heliport"
    ), 14, 1, Polygon.class);
    assertNumFeatures("aeroway", Map.of(
      "class", "helipad"
    ), 14, 11, Polygon.class);
  }

  @Test
  public void testLandcover() {
    assertNumFeatures("landcover", Map.of(
      "class", "grass",
      "subclass", "park"
    ), 14, 20, Polygon.class);
    assertNumFeatures("landcover", Map.of(
      "class", "grass",
      "subclass", "garden"
    ), 14, 33, Polygon.class);
  }

  @Test
  public void testPoi() {
    assertNumFeatures("poi", Map.of(
      "class", "restaurant",
      "subclass", "restaurant"
    ), 14, 217, Point.class);
    assertNumFeatures("poi", Map.of(
      "class", "art_gallery",
      "subclass", "artwork"
    ), 14, 132, Point.class);
  }

  @Test
  public void testLanduse() {
    assertNumFeatures("landuse", Map.of(
      "class", "residential"
    ), 14, 8, Polygon.class);
    assertNumFeatures("landuse", Map.of(
      "class", "hospital"
    ), 14, 4, Polygon.class);
  }

  @Test
  public void testTransportation() {
    assertNumFeatures("transportation", Map.of(
      "class", "path",
      "subclass", "footway"
    ), 14, 909, LineString.class);
    assertNumFeatures("transportation", Map.of(
      "class", "primary"
    ), 14, 170, LineString.class);
  }

  @Test
  public void testTransportationName() {
    assertNumFeatures("transportation_name", Map.of(
      "name", "Boulevard du Larvotto",
      "class", "primary"
    ), 14, 22, LineString.class);
  }

  @Test
  public void testWaterway() {
    assertNumFeatures("waterway", Map.of(
      "class", "stream"
    ), 14, 6, LineString.class);
  }

  private static void assertNumFeatures(String layer, Map<String, Object> attrs, int zoom,
    int expected, Class<? extends Geometry> clazz) {
    TestUtils.assertNumFeatures(mbtiles, layer, zoom, attrs, monacoBounds, expected, clazz);
  }
}
