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

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.HashSet;
import java.util.List;
import java.util.Map;

import org.junit.Test;

import com.google.common.collect.ImmutableMap;

import nz.org.riskscape.engine.ProjectTest;
import nz.org.riskscape.engine.bind.BoundParameters;
import nz.org.riskscape.engine.bind.Parameter;
import nz.org.riskscape.engine.bind.ParameterProperty;
import nz.org.riskscape.engine.bind.ParameterSet;
import nz.org.riskscape.engine.bind.ParameterTemplate;
import nz.org.riskscape.engine.problem.GeneralProblems;
import nz.org.riskscape.engine.relation.ListRelation;
import nz.org.riskscape.rl.ast.Expression;
import nz.org.riskscape.rl.ast.ListDeclaration;

public class PipelineModelParameterTest extends ProjectTest {

  @Test
  public void canBindParametersToCorrectType() {
    assertThat(create(NUMERIC).getParameter().getType(), is(Double.class));
    // bookmarks go to strings - the getExpression function converts it to something that should yield a bookmark
    // when it's evaluated
    assertThat(create(BOOKMARK).getParameter().getType(), is(String.class));
    assertThat(create(NUMERIC, INTEGER).getParameter().getType(), is(Long.class));
    // binding should factor in implied type too (i.e. min implies numeric)
    assertThat(create(MIN_VALUE).getParameter().getType(), is(Double.class));

    // should default to expression if type isn't known
    assertThat(create().getParameter().getType(), is(Expression.class));
    assertThat(create(TEXTBOX).getParameter().getType(), is(Expression.class));

    // we don't support binding to Java Lists so they need to be an expression,
    // but using ListDeclaration means we don't accept any old expression
    assertThat(create(LIST, TEXT).getParameter().getType(), is(ListDeclaration.class));
    // this includes where list is implied
    assertThat(create(NUMERIC, MULTISELECT).getParameter().getType(), is(ListDeclaration.class));
  }

  @Test
  public void canInferPropertiesIfNoneSet() {
    assertThat(createWithDefault("123").getProperties(), contains(NUMERIC));
    assertThat(createWithDefault("[ 1, 2, 3 ]").getProperties(), containsInAnyOrder(NUMERIC, MULTISELECT, LIST));
    // we don't infer text for now because it might get annoying
    assertThat(createWithDefault("'foo'").getProperties(), empty());
    assertThat(createWithDefault("[ 'foo' ]").getProperties(), containsInAnyOrder(MULTISELECT, LIST));
    assertThat(createWithDefault("property.access").getProperties(), empty());
    assertThat(createWithDefault(" ' bad expression ]").getProperties(), empty());
  }

  @Test
  public void doNotInferIfPropertiesAlreadySet() {
    assertThat(createWithDefault("123", TEXT).getProperties(), contains(TEXT));
    assertThat(createWithDefault("[ 1, 2, 3 ]", TEXTBOX).getProperties(), contains(TEXTBOX));
    assertThat(createWithDefault("[ 'foo' ]", DROPDOWN).getProperties(), contains(DROPDOWN));
  }

  @Test
  public void canSetParameterDefault() {
    assertThat(createWithDefault("123").getParameter().getDefaultValues(bindingContext),
        contains(expressionParser.parse("123")));
    // typed properties should change how the binding is done
    assertThat(createWithDefault("123", INTEGER).getParameter().getDefaultValues(bindingContext),
        contains(123L));
    assertThat(createWithDefault("123", TEXT).getParameter().getDefaultValues(bindingContext),
        contains("123"));
  }

  @Test
  public void canTurnBoundValueIntoExpression() {
    PipelineModelParameter parameter = create();
    BoundParameters bound = createBound(parameter, "123");
    assertThat(parameter.getExpression(bound), is(expressionParser.parse("123")));
    assertSame(parameter.getExpression(bound), bound.getValue(parameter.getName()));

    parameter = create(INTEGER, MIN_VALUE.withValue(0D));
    bound = createBound(parameter, "123");
    assertThat(parameter.getExpression(bound), is(expressionParser.parse("123")));

    parameter = create(NUMERIC);
    bound = createBound(parameter, "123");
    // note that the numeric property treats binds values as doubles
    assertThat(parameter.getExpression(bound), is(expressionParser.parse("123.0")));

    addPickledData("bar", ListRelation.ofValues("baz"));
    parameter = create(BOOKMARK);
    bound = createBound(parameter, "bar");
    // NB: bookmarks should always come out as bookmark() expressions
    assertThat(parameter.getExpression(bound), is(expressionParser.parse("bookmark('bar')")));

    parameter = create(TEXT);
    bound = createBound(parameter, "123");
    assertThat(parameter.getExpression(bound), is(expressionParser.parse("'123'")));
    // don't double-quote if it's already a valid expression - the assumption here
    // is the user just got confused about what format the parameter was in, rather
    // than they really wanted '\\'123\\''
    bound = createBound(parameter, "'123'");
    assertThat(parameter.getExpression(bound), is(expressionParser.parse("'123'")));

    // we need to escape single quotes or we'll end up with a dud expression
    bound = createBound(parameter, "don't mangle this!");
    assertThat(parameter.getExpression(bound), is(expressionParser.parse("'don\\'t mangle this!'")));

    // assume any whitespace is there by accident rather than the user desperately
    // wanted a leading/trailing space in their text string
    bound = createBound(parameter, " 123 ");
    assertThat(parameter.getExpression(bound), is(expressionParser.parse("'123'")));
    bound = createBound(parameter, " '123' ");
    assertThat(parameter.getExpression(bound), is(expressionParser.parse("'123'")));
    // and if they *really* did, then there's a work-around for this
    bound = createBound(parameter, "' 123 '");
    assertThat(parameter.getExpression(bound), is(expressionParser.parse("' 123 '")));
  }

  @Test
  public void canValidateParameterAsPartOfBinding() {
    ParameterProperty min = MIN_VALUE.withValue(1D);
    ParameterProperty max = MAX_VALUE.withValue(99D);
    Parameter parameter = create(INTEGER, min, max).getParameter();

    assertThat(bindingContext.bind("1", parameter), result(1L));
    assertThat(bindingContext.bind("99", parameter), result(99L));

    assertTrue(bindingContext.bind("0", parameter).hasErrors());
    assertThat(bindingContext.bind("0", parameter), resultWithProblems(is(0L),
        contains(hasAncestorProblem(
            is(GeneralProblems.get().badValue(0L, min, ">= 1.0"))
        ))
    ));
    assertThat(bindingContext.bind("100", parameter), resultWithProblems(is(100L),
        contains(hasAncestorProblem(
            is(GeneralProblems.get().badValue(100L, max, "<= 99.0"))
        ))
    ));
  }

  @Test
  public void canPreserveOriginalBookmarkExpression() {
    addPickledData("bar", ListRelation.ofValues("baz"));
    PipelineModelParameter parameter = create(BOOKMARK);
    BoundParameters bound = createBound(parameter, "bookmark( 'bar', {} )");
    // note we get the same expression out as to what went in
    assertThat(parameter.getExpression(bound), is(expressionParser.parse("bookmark( 'bar', {} )")));
  }

  @Test
  public void canInheritFromBookmarkTemplate() {
    // we're just going to pretend here that bar.shp is a file rather than a bookmark
    // In practice, either files or bookmark IDs could be resolved and turned into a bookmark,
    // but only files will work when substituted as the bookmark location
    addPickledData("bar.shp", ListRelation.ofValues("baz"));

    PipelineModelParameter parameter = create(BOOKMARK_TEMPLATE.withValue("foo"));
    BoundParameters bound = createBound(parameter, "bar.shp");

    // the template (foo) should be used as the main bookmark, but the location gets swapped out
    // with what the user has specified (bar.shp)
    assertThat(parameter.getExpression(bound), is(expressionParser.parse("bookmark('foo', { location: 'bar.shp' })")));
  }

  private PipelineModelParameter create(ParameterProperty... properties) {
    return PipelineModelParameter.create("foo", template(properties));
  }

  private PipelineModelParameter createWithDefault(String expression, ParameterProperty... properties) {
    return PipelineModelParameter.create("foo", template(properties))
        .withDefaultValue(bindingContext, expression);
  }

  private ParameterTemplate template(ParameterProperty... properties) {
    return ParameterTemplate.EMPTY.withProperties(new HashSet<>(Arrays.asList(properties)));
  }

  private BoundParameters createBound(PipelineModelParameter param, String value) {
    ParameterSet parameterSet = new ParameterSet(Arrays.asList(param.getParameter()));
    Map<String, List<?>> unbound = ImmutableMap.of(param.getName(), Arrays.asList(value));
    return parameterSet.bind(bindingContext, unbound);
  }
}
