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

import static org.hamcrest.Matchers.*;
import static org.junit.Assert.*;

import java.util.concurrent.atomic.AtomicLong;

import org.hamcrest.Matcher;
import org.junit.Test;
import org.locationtech.jts.geom.Coordinate;
import org.locationtech.jts.geom.Point;
import org.geotools.api.referencing.crs.CoordinateReferenceSystem;

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

import nz.org.riskscape.engine.Matchers;
import nz.org.riskscape.engine.SRIDSet;
import nz.org.riskscape.engine.Tuple;
import nz.org.riskscape.engine.TupleMatchers;
import nz.org.riskscape.engine.coverage.TypedCoverage;
import nz.org.riskscape.engine.data.ResolvedBookmark;
import nz.org.riskscape.engine.data.coverage.NearestNeighbourCoverage;
import nz.org.riskscape.engine.geo.GeometryUtils;
import nz.org.riskscape.engine.problem.GeneralProblems;
import nz.org.riskscape.hdf5.H5Dataset;
import nz.org.riskscape.hdf5.H5File;
import nz.org.riskscape.oq.sitecol.SitecolRelation;


public class SiteColIntegrationTest extends BaseHdf5Test {

  SRIDSet sridSet = project.getSridSet();
  CoordinateReferenceSystem defaultCrs = SRIDSet.EPSG4326_LATLON;

  @Test
  public void allSitesInARelationCorrespondToAnEntryInTheCoverage() throws Exception {
    ResolvedBookmark resolved = resolve(bookmark("openquake-scenario-10-events.hdf5", ImmutableMap.of(
        "mode", "COVERAGE_SITE_IDS")));
    TypedCoverage coverage = resolved.getData(TypedCoverage.class).get();

    // make sure it gets a default distance of 10km but as a rough approximation in wgs84
    assertThat(coverage, instanceOf(NearestNeighbourCoverage.class));
    NearestNeighbourCoverage nnCoverage = (NearestNeighbourCoverage) coverage;
    assertThat(
      nnCoverage.getIndex().getMaxDistanceInCrsUnits(),
      equalTo(GeometryUtils.toCrsUnits(10000, defaultCrs))
    );

    H5File file = scenarioFile();

    // Create the relation
    H5Dataset sitecolDataset = file.openDataset("/", "sitecol");
    SitecolRelation relation = new SitecolRelation(sitecolDataset, defaultCrs, sridSet);

    AtomicLong counter = new AtomicLong();
    relation.iterator().forEachRemaining(tuple -> {
      Long siteid = tuple.fetch("siteid");
      Point point = tuple.fetch("location");

      counter.incrementAndGet();
      assertEquals(siteid, ((Tuple) coverage.evaluate(point)).fetch("sid"));
    });

    assertEquals(4818, counter.get());
  }

  @Test
  public void samplingFindsTheNearestPoint() throws Exception {
    TypedCoverage coverage = openWithMaxDistance(10000);
    // this point is between 2849 2932 2934 2851, 2849 is the closest
    assertThat(coverage.evaluate(point(-41.1340, 175.0962, defaultCrs)), siteWithId(2849));
    // now edge towards 2934
    assertThat(coverage.evaluate(point(-41.1512, 175.1205, defaultCrs)), siteWithId(2934));
  }

  @Test
  public void samplingFindsTheNearestPointInADifferentCRS() throws Exception {
    TypedCoverage coverage = openWithMaxDistance(10000);

    // between 215 282 216 and 283 - near nelson
    assertThat(coverage.evaluate(point(5430415, 1613021, SRIDSet.EPSG2193_NZTM)), siteWithId(282));
    assertThat(coverage.evaluate(point(5429762, 1611522, SRIDSet.EPSG2193_NZTM)), siteWithId(216));
  }

  @Test
  public void samplingFindsNothingIfTheNearestPointIsBeyondMaxDistanceMetres() throws Exception {
    TypedCoverage coverage = openWithMaxDistance(1000);

    // between 1309 1387 1311 1389 - near taranaki
    assertThat(coverage.evaluate(point(-39.51020, 173.98948, defaultCrs)), siteWithId(1309));
    // now edge a little further away and, plop, gone
    assertThat(coverage.evaluate(point(-39.5147, 174.9959, defaultCrs)), nullValue());
    // keep heading in the same direction and we eventually get to a different site
    assertThat(coverage.evaluate(point(-39.55001, 174.04067, defaultCrs)), siteWithId(1389));
    // now move out to the coast back the way we came
    assertThat(coverage.evaluate(point(-39.4981, 173.9714, defaultCrs)), nullValue());
  }

  @Test
  public void samplingFindsNothingIfTheNearestPointInADifferentCRSIsBeyondMaxDistanceMetres() throws Exception {
    TypedCoverage coverage = openWithMaxDistance(100);

    // between 320 319 388 389 - near hurunui
    // clicked exactly
    assertThat(coverage.evaluate(point(5242577, 1615059, SRIDSet.EPSG2193_NZTM)), siteWithId(320));
    // 50m NE - still found
    assertThat(coverage.evaluate(point(5242627, 1615109, SRIDSet.EPSG2193_NZTM)), siteWithId(320));
    // 100m NE - not found
    assertThat(coverage.evaluate(point(5242677, 1615159, SRIDSet.EPSG2193_NZTM)), nullValue());
  }


  @Test
  public void maxDistanceMustBeGreaterThanZero() throws Exception {
    ResolvedBookmark resolved = resolve(bookmark("openquake-scenario-10-events.hdf5", ImmutableMap.of(
        "mode", "COVERAGE_SITE_IDS", "max-site-distance-metres", "0")));

    assertThat(
      resolved.getData(TypedCoverage.class).getProblems(),
      contains(GeneralProblems.get().valueOutOfRange("max-site-distance-metres", 0D, Range.greaterThan(0D)))
    );

    // check a negative value for good measure
    resolved = resolve(bookmark("openquake-scenario-10-events.hdf5", ImmutableMap.of(
        "mode", "COVERAGE_SITE_IDS", "max-site-distance-metres", "-1")));

    assertThat(
      resolved.getData(TypedCoverage.class).getProblems(),
      contains(GeneralProblems.get().valueOutOfRange("max-site-distance-metres", -1D, Range.greaterThan(0D)))
    );
  }

  private TypedCoverage openWithMaxDistance(long maxDistanceMetres) {
    ResolvedBookmark resolved = resolve(bookmark("openquake-scenario-10-events.hdf5", ImmutableMap.of(
        "mode", "COVERAGE_SITE_IDS", "max-site-distance-metres", Long.toString(maxDistanceMetres))));

    return resolved.getData(TypedCoverage.class).get();
  }

  private Point point(double x, double y, CoordinateReferenceSystem inCrs) {
    return sridSet.getGeometryFactory(inCrs).createPoint(new Coordinate(x, y));
  }

  private Matcher<Object> siteWithId(long siteId) {
    return Matchers.instanceOfAnd(Tuple.class, TupleMatchers.tupleWithValue("sid", equalTo(siteId)));
  }
}
