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

import static nz.org.riskscape.engine.Matchers.*;
import static nz.org.riskscape.engine.bind.InputFieldProperty.*;
import static nz.org.riskscape.engine.bind.ParameterProperties.*;
import static nz.org.riskscape.engine.bind.TypedProperty.*;
import static nz.org.riskscape.engine.problem.ProblemMatchers.isProblem;
import static org.hamcrest.Matchers.*;
import static org.junit.Assert.*;

import java.util.Arrays;
import java.util.HashSet;
import java.util.List;
import java.util.Optional;

import org.junit.Test;

import nz.org.riskscape.config.ConfigProblems;
import nz.org.riskscape.engine.ProjectTest;
import nz.org.riskscape.engine.bind.ParamProblems;
import nz.org.riskscape.engine.bind.ParameterProperty;
import nz.org.riskscape.engine.bind.ParameterTemplate.Choice;
import nz.org.riskscape.engine.bind.UserDefinedParameter;
import nz.org.riskscape.engine.problem.GeneralProblems;
import nz.org.riskscape.problem.Problem;
import nz.org.riskscape.problem.Problems;
import nz.org.riskscape.problem.ResultOrProblems;

@SuppressWarnings("unchecked")
public class IniFileParameterBuilderTest extends ProjectTest {

  @Test
  public void canBuildASimpleTemplate() throws Exception {
    populate(
        "[parameter foo]",
        "label = pick a foo, any foo",
        "description = cool story, bro",
        "choices = foo, bar, baz",
        "properties = hidden, numeric",
        "default = foo");

    UserDefinedParameter built = build("foo").get();
    assertThat(built.getTemplate().getLabel().get(), is("pick a foo, any foo"));
    assertThat(built.getTemplate().getDescription().get(), is("cool story, bro"));
    assertThat(built.getTemplate().getChoices(), contains(
        choice("foo"), choice("bar"), choice("baz")
    ));
    // note that Hamcrest can sometimes struggle with the Matcher<ParameterProperty> generics,
    // so it's easiest just to match against items/objects here rather than use is() Matchers
    assertThat(built.getTemplate().getProperties(), contains(HIDDEN, NUMERIC));
    assertThat(built.getTemplate().getDefaultValue().get(), is("foo"));
  }

  @Test
  public void canBuildATemplateWithMultilineConfig() throws Exception {
    populate(
        "[parameter foo]",
        "label = pick a foo",
        "label = any foo",
        "description = cool story",
        "description = bro",
        "choices = foo",
        "choices = bar",
        "choices = baz",
        "properties = hidden",
        "# note that integer also implies numeric",
        "properties = integer");

    UserDefinedParameter built = build("foo").get();
    assertThat(built.getTemplate().getLabel().get(), is("pick a foo any foo"));
    assertThat(built.getTemplate().getDescription().get(), is("cool story bro"));
    assertThat(built.getTemplate().getChoices(), contains(
        choice("foo"), choice("bar"), choice("baz")
    ));
    assertThat(built.getTemplate().getProperties(), contains(HIDDEN, INTEGER, NUMERIC));
  }

  @Test
  public void canBuildATemplateWithKeyValueProperties() throws Exception {
    populate(
        "[parameter foo]",
        "properties = hidden  , min: 0, max:   100 , numeric ");

    UserDefinedParameter built = build("foo").get();
    assertThat(built.getTemplate().getLabel(), is(Optional.empty()));
    assertThat(built.getTemplate().getDescription(), is(Optional.empty()));
    assertThat(built.getTemplate().getChoices(), empty());
    assertThat(built.getTemplate().getProperties(), containsInAnyOrder(HIDDEN, MIN_VALUE, MAX_VALUE, NUMERIC));
    assertThat(getProperty(built, "min").get().getValueOr(), is(Optional.of(0D)));
    assertThat(getProperty(built, "max").get().getValueOr(), is(Optional.of(100D)));
  }

  @Test
  public void canBuildATemplateWithStackableProperties() throws Exception {
    populate(
        "[parameter foo]",
        "properties = multiselect, numeric, unformatted");

    UserDefinedParameter built = build("foo").get();
    assertThat(built.getTemplate().getLabel(), is(Optional.empty()));
    assertThat(built.getTemplate().getDescription(), is(Optional.empty()));
    assertThat(built.getTemplate().getChoices(), empty());
    assertThat(built.getTemplate().getProperties(), containsInAnyOrder(MULTISELECT, NUMERIC, UNFORMATTED, LIST));
  }

  @Test
  public void canBaseATemplateOffACommonTemplate() throws Exception {
    populate(
        "[parameter base]",
        "label = pick a foo",
        "properties = min:0, max:100, numeric",
        "choices = bar, baz",
        "description = cool story, bro",
        "",
        "[parameter foo]",
        "template = base",
        "description = cool foo, bro");

    UserDefinedParameter built = build("foo").get();
    // can override a field in the base template with a more specific value
    assertThat(built.getTemplate().getDescription().get(), is("cool foo, bro"));
    // all other fields get inherited from the parent/base template
    assertThat(built.getTemplate().getLabel().get(), is("pick a foo"));
    assertThat(built.getTemplate().getChoices(), contains(choice("bar"), choice("baz")));
    assertThat(built.getTemplate().getProperties(), containsInAnyOrder(MIN_VALUE, MAX_VALUE, NUMERIC));
  }

  @Test
  public void canSpecifyLabelsForTheChoices() throws Exception {
    populate(
        "[parameter foo]",
        "choices = \"A selection of Bar\": bar, \"The finest Baz\": baz");

    UserDefinedParameter built = build("foo").get();
    assertThat(built.getTemplate().getChoices(), contains(
        choice("bar", "A selection of Bar"), choice("baz", "The finest Baz")
    ));
  }

  @Test
  public void canGetSensibleErrorIfUsingNonExistentTemplate() throws Exception {
    populate(
        "[parameter foo]",
        "template = ruh-roh");

    assertThat(build("foo"), failedResult(hasAncestorProblem(
        is(Problems.foundWith(config.getSection("parameter foo").get(),
            GeneralProblems.get().noSuchObjectExists("ruh-roh", UserDefinedParameter.class)))
    )));
  }

  @Test
  public void canGetSensibleErrorForBadChoicesExpression() throws Exception {
    populate(
        "[parameter foo]",
        // the choice values need to be valid expressions, otherwise they won't slot into the pipeline nicely
        "choices = {not valid",
        "choices = 'neither is this");

    assertThat(build("foo"), failedResult(hasAncestorProblem(
        equalIgnoringChildren(IniFileParameterBuilder.PROBLEMS.couldNotParseOption("{not valid"))
    )));
  }

  @Test
  public void canGetSensibleErrorForBadChoicesLabelExpression() throws Exception {
    populate("[parameter foo]",
        // the label part of the choice needs to be valid too
        "choices = bad label: valid.choice",
        "choices = also bad: 'but this is fine'");

    assertThat(build("foo"), failedResult(hasAncestorProblem(
        equalIgnoringChildren(IniFileParameterBuilder.PROBLEMS.couldNotParseOption("bad label: valid.choice")))));
  }

  @Test
  public void canGetSensibleErrorForIncompatibleProperties() throws Exception {
    populate("[parameter foo]",
        // incompatible - can't be both a textbox and a drop-down/select input
        "properties = textbox, dropdown");

    assertThat(build("foo"), failedResult(hasAncestorProblem(
        is(ParamProblems.get().mutuallyExclusive("dropdown", "textbox"))
    )));
  }

  @Test
  public void canGetSensibleErrorForInvalidBaseTemplate() throws Exception {
    // base template exists, but it has an error ('minimum' instead of 'min')
    populate(
        "[parameter bad-base]",
        "properties = minimum: 0",
        "",
        "[parameter foo]",
        "template = bad-base");

    assertThat(build("foo"), failedResult(hasAncestorProblem(
        isProblem(GeneralProblems.class, (r, gp) ->
        gp.failedResourceLookedUp(r.eq("bad-base"), r.any(), r.eq(UserDefinedParameter.class))
        )
    )));
  }

  @Test
  public void spuriousKeysGetFlaggedAsWarnings() throws Exception {
    populate(
        "[parameter foo]",
        "description = cool",
        "story = bro");

    Problem expectedWarning = ConfigProblems.get().spuriousKeys(config.getSection("parameter foo").get(),
        new HashSet<String>(Arrays.asList("story")));

    ResultOrProblems<UserDefinedParameter> result = build("foo");
    assertTrue(result.hasProblems());
    assertThat(result.getProblems(), contains(hasAncestorProblem(
        equalTo(expectedWarning)
    )));
  }

  @Test
  public void ignoresDefaultIfNotSpecified() {
    populate("""
            [parameter foo]
            properties = file
            default =
            """);
    assertThat(build("foo").get().getTemplate().getDefaultValue(), isEmptyOptional());

  }

  @Test
  public void filePathDefaultsAreMadeAbsolute() {
    // If we define a value in a different file, we want the file to be resolved relative to that, rather than relative
    // to the main project.ini file
    String location = "/some/file/path";
    populateWithLocation(
      location + "/params.ini", """
      [parameter foo]
      default = ./folder/file.gpkg
      properties = file
      [parameter bar]
      default = ./folder/bar.gpkg
      properties = bookmark
      [parameter baz]
      default = ./folder/bar.gpkg
      [parameter named_bookmark]
      default = a_bookmark_id
      properties = bookmark
      """);

    assertThat(getDefaultValue("foo"), is("test:" + location + "/folder/file.gpkg"));
    assertThat(getDefaultValue("bar"), is("test:" + location + "/folder/bar.gpkg"));
    // We only want this to apply to parameters marked "file" or "bookmark" - shouldn't mess with other strings
    assertThat(getDefaultValue("baz"), is("./folder/bar.gpkg"));
    // Also only apply it if it has a leading './', so we don't mangle bookmarks that aren't filepaths
    assertThat(getDefaultValue("named_bookmark"), is("a_bookmark_id"));
  }

  @Test
  public void filePathsWithQuotesAreMadeAbsolute() {
    String location = "/some/file/path";
    populateWithLocation(
      location + "/params.ini", """
      [parameter foo]
      default = './folder/file.gpkg'
      properties = file
      [parameter bar]
      choices = "Dummy/test data (quick)": './folder/bar.gpkg'
      properties = file
      [parameter baz]
      choices = "Choice with space": './this file/sucks massively.txt'
      properties = file
      """);
    assertThat(getDefaultValue("foo"), is("test:" + location + "/folder/file.gpkg"));

    assertThat(getChoices("bar"), contains(hasProperty("value", is("test:" + location + "/folder/bar.gpkg"))));

    // Spaces aren't supported in URIs, so make sure that they're being escaped properly
    assertThat(getChoices("baz"),
            contains(hasProperty("value",
                    is("test:" + location + "/this%20file/sucks%20massively.txt"))));

  }

  private ResultOrProblems<UserDefinedParameter> build(String name) {
    return project.getParameterTemplates().getOr(name);
  }

  private Choice choice(String value, String label) {
    return new Choice(value, Optional.of(label));
  }

  private Choice choice(String value) {
    return new Choice(value, Optional.empty());
  }

  private Optional<ParameterProperty> getProperty(UserDefinedParameter param, String propertyName) {
    return param.getTemplate().getProperties().stream()
    .filter(p -> p.getKeyword().equals(propertyName))
    .findFirst();
  }

  private String getDefaultValue(String paramName) {
    return build(paramName).get().getTemplate().getDefaultValue().get();
  }

  private List<Choice> getChoices(String paramName) {
    return build(paramName).get().getTemplate().getChoices();
  }
}
