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

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

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

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

import com.google.common.collect.Range;

import nz.org.riskscape.engine.Matchers;
import nz.org.riskscape.engine.ProjectTest;
import nz.org.riskscape.engine.bind.impl.NumberBinder;
import nz.org.riskscape.engine.data.Bookmark;
import nz.org.riskscape.engine.problem.GeneralProblems;
import nz.org.riskscape.engine.relation.ListRelation;
import nz.org.riskscape.engine.rl.MathsFunctions;
import nz.org.riskscape.engine.rl.RealizedExpression;
import nz.org.riskscape.engine.types.Struct;
import nz.org.riskscape.engine.types.Types;
import nz.org.riskscape.engine.types.Value;
import nz.org.riskscape.engine.typeset.IdentifiedType;
import nz.org.riskscape.problem.Problem;
import nz.org.riskscape.problem.Problems;
import nz.org.riskscape.rl.ast.Expression;
import nz.org.riskscape.rl.ast.ListDeclaration;

public class ParameterPropertiesTest extends ProjectTest {

  @Before
  public void setup() {
    project.getFunctionSet().addAll(MathsFunctions.FUNCTIONS);
  }

  @Test
  public void canLookupProperties() {
    assertThat(find("min"), is(Optional.of(MIN_VALUE)));
    assertThat(find("bookmark"), is(Optional.of(BOOKMARK)));
    assertThat(find("file"), is(Optional.of(FILE)));
    assertThat(find("hidden"), is(Optional.of(HIDDEN)));
    assertThat(find("unformatted"), is(Optional.of(UNFORMATTED)));
    assertThat(find("foo"), is(Optional.empty()));
  }

  @Test
  public void canImplyOtherProperties() {
    assertThat(NUMERIC.getImplied(), empty());
    assertThat(MIN_VALUE.getImplied(), contains(NUMERIC));
    assertThat(INTEGER.getImplied(), contains(NUMERIC));
    assertThat(CHECKBOX.getImplied(), contains(LIST));
    assertThat(BOOKMARK_TEMPLATE.getImplied(), contains(BOOKMARK));
    assertThat(FILE.getImplied(), contains(TEXT));
  }

  @Test
  public void canDetectIncompatibleProperties() {
    // typed properties are generally incompatible, unless related
    assertCompatible(NUMERIC, INTEGER, true);
    assertCompatible(NUMERIC, LIST, true);
    assertCompatible(NUMERIC, BOOKMARK, false);
    assertCompatible(NUMERIC, EXPRESSION, false);
    assertCompatible(BOOKMARK, FILE, false);

    // input fields are generally incompatible
    assertCompatible(TEXTBOX, MULTISELECT, false);
    assertCompatible(CHECKBOX, DROPDOWN, false);

    // apart from HIDDEN, READONLY, and UNFORMATTED which work with other input fields
    assertCompatible(HIDDEN, MULTISELECT, true);
    assertCompatible(READONLY, MULTISELECT, true);
    assertCompatible(UNFORMATTED, MULTISELECT, true);
    assertCompatible(UNFORMATTED, READONLY, true);

    // key-value properties might vary depending on the value
    assertCompatible(MIN_VALUE, INTEGER, true);
    assertCompatible(MAX_VALUE, NUMERIC, true);
    assertCompatible(MAX_VALUE, MIN_VALUE, true);
    assertCompatible(BOOKMARK_TEMPLATE, BOOKMARK, true);
    assertCompatible(TYPE, BOOKMARK, true);

    // can mix n match accordingly
    assertCompatible(TEXTBOX, MIN_VALUE, true);
    assertCompatible(MULTISELECT, MAX_VALUE, true);
    assertCompatible(TEXTBOX, INTEGER, true);
    assertCompatible(DROPDOWN, NUMERIC, true);

    // note the system isn't foolproof - properties shouldn't have to know about
    // every other property, so long as one knows its incompatible with the other.
    // That's probably 'good enough'
    assertFalse(MIN_VALUE.isCompatible(BOOKMARK));
    assertFalse(TYPE.isCompatible(MIN_VALUE));
    assertTrue(BOOKMARK.isCompatible(MIN_VALUE));
    assertFalse(BOOKMARK_TEMPLATE.isCompatible(NUMERIC));
    assertFalse(TYPE.isCompatible(NUMERIC));
  }

  @Test
  public void canReportIncompatibleProblems() {
    assertThat(checkCompatible(NUMERIC, Arrays.asList(INTEGER, TEXTBOX, LIST)), empty());
    assertThat(checkCompatible(EXPRESSION, Arrays.asList(TEXTBOX, MIN_VALUE)),
        contains(ParamProblems.get().mutuallyExclusive("expression", "min")));
    assertThat(checkCompatible(EXPRESSION, Arrays.asList(INTEGER)),
        contains(ParamProblems.get().mutuallyExclusive("expression", "integer")));
    assertThat(checkCompatible(BOOKMARK_TEMPLATE, Arrays.asList(MIN_VALUE)),
        contains(ParamProblems.get().mutuallyExclusive("bookmark-template", "min")));
  }

  @Test
  public void canTreatKeyValuePropertiesAsEqual() {
    ParameterProperty minZero = MIN_VALUE.withValue(0D);
    ParameterProperty minOne = MIN_VALUE.withValue(1D);
    ParameterProperty max100 = MAX_VALUE.withValue(100D);
    ParameterProperty max200 = MAX_VALUE.withValue(200D);

    // a parameter can only have one min/max rule, so it makes sense to treat these
    // more like enum values in terms of equality
    assertEquals(minZero, MIN_VALUE);
    assertEquals(minZero, minOne);
    assertEquals(max100, MAX_VALUE);
    assertEquals(max100, max200);

    assertNotEquals(minZero, MAX_VALUE);
    assertNotEquals(MIN_VALUE, MAX_VALUE);
  }

  @Test
  public void canValidateMinValue() {
    ParameterProperty property = MIN_VALUE.withValue(0D);
    assertThat(property.validate(bindingContext, 1D), empty());
    assertThat(property.validate(bindingContext, 0D), empty());
    assertThat(property.validate(bindingContext, -1D), contains(
        GeneralProblems.get().badValue(-1D, property, ">= " + 0D)
    ));
  }

  @Test
  public void canValidateMaxValue() {
    ParameterProperty property = MAX_VALUE.withValue(100D);
    assertThat(property.validate(bindingContext, 1D), empty());
    assertThat(property.validate(bindingContext, 100D), empty());
    assertThat(property.validate(bindingContext, 100.1D), contains(
        GeneralProblems.get().badValue(100.1D, property, "<= " + 100D)
    ));
  }

  @Test
  public void canFilterPropertiesByClass() {
    List<ParameterProperty> properties = Arrays.asList(MIN_VALUE, MAX_VALUE, BOOKMARK);

    assertThat(ParameterProperty.filter(properties, KeyValueProperty.class),
        contains(MIN_VALUE, MAX_VALUE));
    assertThat(ParameterProperty.filter(properties, TypedProperty.class), contains(BOOKMARK));
    assertThat(ParameterProperty.filter(properties, InputFieldProperty.class), empty());
  }

  @Test
  public void canFindBestTypedPropertyForBinding() {
    // where types can overlap we want to find the more specific type to bind against
    assertThat(getBindType(NUMERIC, INTEGER), is(Long.class));
    assertThat(getBindType(NUMERIC), is(Double.class));
    assertNull(getBindType(TEXTBOX));
    // any lists should get bound as a list declaration expression
    assertThat(getBindType(NUMERIC, INTEGER, LIST), is(ListDeclaration.class));
    assertThat(getBindType(TEXT, LIST), is(ListDeclaration.class));
    // technically this is not a valid combo, but expression should always take precedence
    assertThat(getBindType(EXPRESSION, BOOKMARK), is(Expression.class));
  }

  @Test
  public void cannotSetKeyValuePairIfPropertyDoesNotSupportIt() {
    assertFalse(NUMERIC.hasKeyValuePair());
    assertThat(NUMERIC.getValueOr(), is(Optional.empty()));
    assertThrows(UnsupportedOperationException.class, () -> NUMERIC.withValue(0D));
    assertFalse(TEXTBOX.hasKeyValuePair());
    assertThat(TEXTBOX.getValueOr(), is(Optional.empty()));
    assertThrows(UnsupportedOperationException.class, () -> TEXTBOX.withValue("foo"));
  }

  @Test
  public void canConstructAParameterPropertyFromConfig() {
    // simple key-only properties
    assertThat(build(bindingContext, "textbox", Optional.empty()), result(TEXTBOX));
    assertThat(build(bindingContext, "numeric", Optional.empty()), result(NUMERIC));

    // key-value properties
    ParameterProperty minValue = build(bindingContext, "min", Optional.of("-100")).get();
    assertThat(minValue, is(MIN_VALUE));
    assertThat(minValue.getValueOr(), is(Optional.of(-100D)));
    ParameterProperty maxValue = build(bindingContext, "max", Optional.of("3.14")).get();
    assertThat(maxValue, is(MAX_VALUE));
    assertThat(maxValue.getValueOr(), is(Optional.of(3.14D)));
  }

  @SuppressWarnings("unchecked")
  @Test
  public void canGetSensibleErrorWhenBuildingBadProperty() {
    assertThat(build(bindingContext, "foo", Optional.empty()), failedResult(
        is(GeneralProblems.get().notAnOption("foo", ParameterProperty.class, AVAILABLE_KEYWORDS))
    ));
    assertThat(build(bindingContext, "textbox", Optional.of("foo")), failedResult(
        is(GeneralProblems.get().badArity(Value.class, TEXTBOX, Range.singleton(0), 1))
    ));
    assertThat(build(bindingContext, "min", Optional.empty()), failedResult(
        is(GeneralProblems.get().badArity(Value.class, MIN_VALUE, Range.singleton(1), 0))
    ));
    assertThat(build(bindingContext, "min", Optional.of("zero")), failedResult(
        is(Problems.foundWith(MIN_VALUE,
            NumberBinder.PROBLEMS.numberFormatException("zero", Double.class)))
    ));
  }

  @Test
  public void canCheckBookmarkTemplateIsValid() {
    assertThat(build(bindingContext, "bookmark-template", Optional.of("foo")), failedResult(
        hasAncestorProblem(
            is(GeneralProblems.get().noSuchObjectExists("foo", Bookmark.class))
    )));
    addPickledData("foo", ListRelation.ofValues("bar"));
    ParameterProperty useBookmarkFoo = build(bindingContext, "bookmark-template", Optional.of("foo")).get();
    assertThat(useBookmarkFoo, is(BOOKMARK_TEMPLATE.withValue("foo")));
  }

  @Test
  public void canInferPropertiesFromDefaultValue() {
    assertThat(infer(realize("123")), contains(NUMERIC));
    assertThat(infer(realize("12.3")), contains(NUMERIC));
    assertThat(infer(realize("[1, 2, 3]")), containsInAnyOrder(MULTISELECT, NUMERIC));
    assertThat(infer(realize("[1.0, 2.3]")), containsInAnyOrder(MULTISELECT, NUMERIC));
    assertThat(infer(realize("['foo', 'bar']")), contains(MULTISELECT));

    // don't infer types for more complicated expressions, even if they result in a number.
    // The UI can't really do anything to help users enter more complicated expressions like this
    assertThat(infer(realize("random_choice([1, 2])")), empty());
    assertThat(infer(realize("maxint()")), empty());

    // we probably still want somthing like this to work
    assertThat(infer(realize("[ 0, 1, maxint() ]")), containsInAnyOrder(MULTISELECT, NUMERIC));

    // NB: I don't think the engine should automatically infer something is text - that could get annoying.
    // If we infer something is a number and get that wrong, then the user will get an obvious error when
    // they enter `exposure.xyz`. However, if we infer text, then the engine would automatically turn
    // the expression into `'exposure.xyz'`, giving the user no errors, but strange behaviour
    assertThat(infer(realize("'foo'")), empty());
  }

  @Test
  public void canCreateAKeyValuePropertyWithNoDefaultValue() throws Exception {
    KeyValueProperty<String> noDefault = new KeyValueProperty<>("foo", String.class);

    // it's a key value pair even if no value is set yet.
    assertTrue(noDefault.hasKeyValuePair());
    assertNull(noDefault.getValueOr().orElse(null));
    assertThat(noDefault.toString(), not(emptyString()));

    ParameterProperty withValue = noDefault.withValue("bar");
    // it still is
    assertTrue(withValue.hasKeyValuePair());
    // and now it's got a value, yay
    assertThat(withValue.getValueOr().orElse(null), equalTo("bar"));
  }

  @Test
  public void bookmarkTypePropertiesWorkForIdentifiedTypes() throws Exception {
    KeyValueProperty<IdentifiedType> property = ParameterProperties.TYPE;
    // for now, I've limited this to identified types, but I think the only reason for doing this is
    // to limit the scope of error messages.  There's no good reason I can think of why we should have
    // to limit it like this in the long run.
    project.getTypeSet().add("foo-type", Types.TEXT);

    assertThat(
      property.withValue(bindingContext, "foo-type"),
      Matchers.result(hasProperty("value", is(project.getTypeSet().get("foo-type"))))
    );
    // this type doesn't exist (NB not bothering with asserting messages, we do that elsewhere)
    assertThat(
      property.withValue(bindingContext, "no-type"),
      Matchers.failedResult(any(Problem.class))
    );
    // simple types don't work either
    assertThat(
      property.withValue(bindingContext, "text"),
      Matchers.failedResult(any(Problem.class))
    );

  }

  private RealizedExpression realize(String expression) {
    return expressionRealizer.realize(Struct.EMPTY_STRUCT, expression).get();
  }

  private Class<?> getBindType(ParameterProperty... properties) {
    return getBindingClass(Arrays.asList(properties)).orElse(null);
  }

  private void assertCompatible(ParameterProperty a, ParameterProperty b, boolean expected) {
    // check compatibility both ways, to avoid repeating ourselves
    assertThat(a.isCompatible(b), is(expected));
    assertThat(b.isCompatible(a), is(expected));
  }
}
