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

import static nz.org.riskscape.engine.Matchers.*;
import static nz.org.riskscape.engine.problem.ProblemMatchers.*;
import static nz.org.riskscape.engine.problem.ProblemMatchers.isProblem;
import static nz.org.riskscape.hdf5.Hdf5Resolver.*;

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

import java.net.URI;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;

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

import com.google.common.collect.ImmutableMap;

import nz.org.riskscape.engine.Tuple;
import nz.org.riskscape.engine.TupleMatchers;
import nz.org.riskscape.engine.bind.Parameter;
import nz.org.riskscape.engine.bind.ParameterSet;
import nz.org.riskscape.engine.data.Bookmark;
import nz.org.riskscape.engine.data.ResolvedBookmark;
import nz.org.riskscape.engine.defaults.resource.HttpResourceLoader;
import nz.org.riskscape.engine.problem.GeneralProblems;
import nz.org.riskscape.engine.problem.ProblemMatchers;
import nz.org.riskscape.engine.relation.Relation;
import nz.org.riskscape.engine.types.RSList;
import nz.org.riskscape.engine.types.Struct;
import nz.org.riskscape.engine.types.TypeProblems;
import nz.org.riskscape.engine.types.Types;

@SuppressWarnings("unchecked")
public class Hdf5ResolverTest extends Hdf5BaseTest {

  private final Hdf5Resolver subject = new Hdf5Resolver(engine);

  private Bookmark bookmark;

  @Before
  public void setup() throws Exception {
    engine.getResourceFactory().add(new HttpResourceLoader(engine));
  }

  @Test
  public void canReadRelationAndGetAllAttributes() {
    bookmark("h5ex_t_1d_dataset.h5", ImmutableMap.of(
        "mode", "relation",
        "dataset", "/DS1"));
    ResolvedBookmark resolved = subject.resolve(bookmark, executionContext).get();
    Relation relation = resolved.getData(Relation.class).get();

    Struct expected = Struct.of("id", Types.INTEGER, "rup_id", Types.INTEGER, "rlz_id", Types.INTEGER);
    assertThat(relation.getType(), is(expected));

    List<Tuple> items = relation.stream()
        .collect(Collectors.toList());

    // Check the size and first and last items
    assertThat(items, hasSize(34));
    assertThat(items.get(0), is(Tuple.ofValues(relation.getType(), 0L, 0L, 0L)));
    assertThat(items.get(33), is(Tuple.ofValues(relation.getType(), 33L, 30L, 0L)));
  }

  @Test
  public void canReadRelationContainingArrays() {
    // in this test we're checking that the array can be read and all items are returned.
    // the test data is a multi dimensional 3 * 5 array but they are properly supported so they
    // end up being flattened.
    // not sure how useful this flattening is. maybe the H5ArrayType should keep the multi-dimensional
    // structure to the data.
    bookmark("h5ex_t_array.h5", ImmutableMap.of(
        "mode", "relation",
        "dataset", "/DS1"));
    ResolvedBookmark resolved = subject.resolve(bookmark, executionContext).get();
    Relation relation = resolved.getData(Relation.class).get();

    Struct expected = Struct.of("DS1", RSList.create(Types.INTEGER));
    assertThat(relation.getType(), is(expected));

    List<Tuple> items = relation.stream()
        .collect(Collectors.toList());

    // Check the size and first and last items
    assertThat(items, hasSize(4));
    List expected0 = Arrays.asList(0L, 0L, 0L, 0L, 0L, 0L, -1L, -2L, -3L, -4L, 0L, -2L, -4L, -6L, -8L);
    assertEquals(3 * 5, expected0.size());
    List expected3 = Arrays.asList(0L, 3L, 6L, 9L, 12L, 3L, 5L, 7L, 9L, 11L, 6L, 7L, 8L, 9L, 10L);
    assertEquals(3 * 5, expected3.size());
    assertThat(items.get(0), is(Tuple.ofValues(relation.getType(), expected0)));
    assertThat(items.get(3), is(Tuple.ofValues(relation.getType(), expected3)));
  }

  @Test
  public void canReadRelationAndGetAllAttributesFromRemoteFile() {
    // We don't really want users to be downloading potentially large HDF5 files every time a bookmark
    // accesses them. But this is necessary to allow the platform resouce loader to do its work.
    // And it makes for a convenient way to test it from risksape too.
    bookmark(URI.create("https://" + HTTPS_TEST_URI + "/test/hdf5/h5ex_t_1d_dataset.h5"), ImmutableMap.of(
        "format", "hdf5",
        "mode", "relation",
        "dataset", "/DS1"));
    ResolvedBookmark resolved = subject.resolve(bookmark, executionContext).get();
    Relation relation = resolved.getData(Relation.class).get();

    Struct expected = Struct.of("id", Types.INTEGER, "rup_id", Types.INTEGER, "rlz_id", Types.INTEGER);
    assertThat(relation.getType(), is(expected));

    List<Tuple> items = relation.stream()
        .collect(Collectors.toList());

    // Check the size and first and last items
    assertThat(items, hasSize(34));
    assertThat(items.get(0), is(Tuple.ofValues(relation.getType(), 0L, 0L, 0L)));
    assertThat(items.get(33), is(Tuple.ofValues(relation.getType(), 33L, 30L, 0L)));
  }

  @Test
  public void canReadRelationAndGetIncludeAttributesOnly() {
    bookmark("h5ex_t_1d_dataset.h5", ImmutableMap.of(
        "mode", "relation",
        "dataset", "/DS1",
        "include-attributes", "  rup_id ,rlz_id"));
    ResolvedBookmark resolved = subject.resolve(bookmark, executionContext).get();
    Relation relation = resolved.getData(Relation.class).get();

    Struct expected = Struct.of("rup_id", Types.INTEGER, "rlz_id", Types.INTEGER);
    assertThat(relation.getType(), is(expected));

    List<Tuple> items = relation.stream()
        .collect(Collectors.toList());

    // Check the size and first and last items
    assertThat(items, hasSize(34));
    assertThat(items.get(0), is(Tuple.ofValues(relation.getType(), 0L, 0L)));
    assertThat(items.get(33), is(Tuple.ofValues(relation.getType(), 30L, 0L)));
  }

  @Test
  public void givesAnErrorIfFileNotHDF5() {
    bookmark("README.md", ImmutableMap.of(
        "mode", "relation",
        "dataset", "/DS1"));

    ResolvedBookmark resolved = subject.resolve(bookmark, executionContext).get();
    assertThat(resolved.getData(Relation.class), failedResult(hasAncestorProblem(
        is(PROBLEMS.hdf5FileLoadError("Not an HDF5 file"))
    )));
  }

  @Test
  public void givesAnErrorIfDatasetDoesNoExist() {
    bookmark("h5ex_t_1d_dataset.h5", ImmutableMap.of(
        "mode", "relation",
        "dataset", "/bogus",
        "include-attributes", "  rup_id"));

    ResolvedBookmark resolved = subject.resolve(bookmark, executionContext).get();
    assertThat(resolved.getData(Relation.class), failedResult(hasAncestorProblem(
        is(PROBLEMS.hdf5DatasetDoesNotExist("/bogus"))
    )));
  }

  @Test
  public void givesAnErrorIfIncludeAttrDoNoExist() {
    bookmark("h5ex_t_1d_dataset.h5", ImmutableMap.of(
        "mode", "relation",
        "dataset", "/DS1",
        "include-attributes", "  rup_id ,bogus"));

    ResolvedBookmark resolved = subject.resolve(bookmark, executionContext).get();
    assertThat(resolved.getData(Relation.class), failedResult(hasAncestorProblem(
        equalTo(GeneralProblems.get().notAnOption(
            "bogus",
            "include-attributes",
            Arrays.asList("id", "rup_id", "rlz_id")
        ))
    )));
  }

  @Test
  public void givesAnErrorIfNoDatasetSpecified() {
    bookmark("h5ex_t_1d_dataset.h5", ImmutableMap.of(
        "mode", "relation"));

    ResolvedBookmark resolved = subject.resolve(bookmark, executionContext).get();
    assertThat(resolved.validate(), contains(
        isProblem(GeneralProblems.class, (r, gp) ->
          gp.required(r.match(namedArg(Parameter.class, "dataset")))
    )));
  }

  @Test
  public void canReadDatasetWithSimpleType() {
    bookmark("h5ex_t_string.h5", ImmutableMap.of(
        "mode", "relation",
        "dataset", "/DS1"));
    ResolvedBookmark resolved = subject.resolve(bookmark, executionContext).get();
    Relation relation = resolved.getData(Relation.class).get();

    Struct expected = Struct.of("DS1", Types.TEXT);
    assertThat(relation.getType(), is(expected));

    List<Tuple> items = relation.stream()
        .collect(Collectors.toList());

    // Check the size and first and last items
    assertThat(items, contains(
        TupleMatchers.tupleWithValue("DS1", is("Parting")),
        TupleMatchers.tupleWithValue("DS1", is("is such")),
        TupleMatchers.tupleWithValue("DS1", is("sweet")),
        TupleMatchers.tupleWithValue("DS1", is("sorrow."))
    ));
  }

  @Test
  public void canReadDatasetWithSimpleTypeAndSetAttributes() {
    // we're testing that the common relation bookmark functionality is working here. We'll assume if
    // set-attributes is doind the right thing then all the other common stuff should work fine as well.
    bookmark("h5ex_t_string.h5", ImmutableMap.of(
        "mode", "relation",
        "dataset", "/DS1",
        "set-attribute.value2", "DS1"));
    ResolvedBookmark resolved = subject.resolve(bookmark, executionContext).get();
    Relation relation = resolved.getData(Relation.class).get();

    Struct expected = Struct.of("DS1", Types.TEXT, "value2", Types.TEXT);
    assertThat(relation.getType(), is(expected));

    List<Tuple> items = relation.stream()
        .collect(Collectors.toList());

    // Check the size and first and last items
    assertThat(items, contains(
        TupleMatchers.tupleWithValue("value2", is("Parting")),
        TupleMatchers.tupleWithValue("value2", is("is such")),
        TupleMatchers.tupleWithValue("value2", is("sweet")),
        TupleMatchers.tupleWithValue("value2", is("sorrow."))
    ));
  }

  @Test
  public void canReadDatasetWithSimpleTypeUsingIncludeAttributeName() {
    bookmark("h5ex_t_string.h5", ImmutableMap.of(
        "mode", "relation",
        "dataset", "/DS1",
        "include-attributes", "words"));
    ResolvedBookmark resolved = subject.resolve(bookmark, executionContext).get();
    Relation relation = resolved.getData(Relation.class).get();

    Struct expected = Struct.of("words", Types.TEXT);
    assertThat(relation.getType(), is(expected));

    List<Tuple> items = relation.stream()
        .collect(Collectors.toList());

    // Check the size and first and last items
    assertThat(items, contains(
        TupleMatchers.tupleWithValue("words", is("Parting")),
        TupleMatchers.tupleWithValue("words", is("is such")),
        TupleMatchers.tupleWithValue("words", is("sweet")),
        TupleMatchers.tupleWithValue("words", is("sorrow."))
    ));
  }

  @Test
  public void canReadMultipleDatasetsAsSingleRelation() {
    bookmark("h5ex_d_combined.h5", ImmutableMap.of(
        "mode", "relation",
        "dataset", Arrays.asList("/DS1", "/DS2")
    ));
    ResolvedBookmark resolved = subject.resolve(bookmark, executionContext).get();
    Relation relation = resolved.getData(Relation.class).get();

    Struct expected = Struct.of("DS1", Types.INTEGER, "DS2", Types.INTEGER);
    assertThat(relation.getType(), is(expected));

    List<Tuple> items = relation.stream()
        .collect(Collectors.toList());

    // Check the size and first and last items
    assertThat(items, hasSize(28));
    assertThat(items.get(0), is(Tuple.ofValues(relation.getType(), 0L, 1L)));
    assertThat(items.get(27), is(Tuple.ofValues(relation.getType(), 12L, 13L)));
  }

  @Test
  public void cannotReadMultipleDatasetsWithDifferentSizes() {
    bookmark("h5ex_d_diffsizes.h5", ImmutableMap.of(
        "mode", "relation",
        "dataset", Arrays.asList("/DS1", "/DS2")
    ));
    ResolvedBookmark resolved = subject.resolve(bookmark, executionContext).get();
    assertThat(resolved.getData(Relation.class), failedResult(hasAncestorProblem(
        is(Hdf5Resolver.PROBLEMS.differentDatasetSizes())
    )));
  }

  @Test
  public void cannotReadDatasetsWithDuplicateAttributes() {
    bookmark("h5ex_d_combined.h5", ImmutableMap.of(
        "mode", "relation",
        "dataset", Arrays.asList("/DS1", "/dups/DS1")
    ));
    ResolvedBookmark resolved = subject.resolve(bookmark, executionContext).get();
    assertThat(resolved.getData(Relation.class), failedResult(hasAncestorProblem(
          ProblemMatchers.isProblem(TypeProblems.class,
              (r, p) -> p.duplicateKeys(r.any())
        ))));
  }

  private void bookmark(String fileName, Map<String, ?> unparsed) {
    bookmark(examplesDir.resolve(fileName).toUri(), unparsed);
  }

  private void bookmark(URI location, Map<String, ?> unparsed) {
    this.bookmark = new Bookmark("id", "desc", "hdf5", location, Collections.emptyMap())
        .addUnparsed(ParameterSet.normaliseParameterMap(unparsed));
  }

}
