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

import static org.hamcrest.Matchers.*;
import static org.junit.Assert.*;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.*;

import java.net.URI;
import java.util.Arrays;
import java.util.EnumSet;
import java.util.List;
import java.util.Locale;
import java.util.Optional;

import org.junit.Before;
import org.junit.Test;
import org.mockito.Mockito;

import com.google.common.collect.ImmutableMap;

import nz.org.riskscape.defaults.function.AALHazardBasedFunction;
import nz.org.riskscape.defaults.function.ToListFunction;
import nz.org.riskscape.engine.FunctionSet;
import nz.org.riskscape.engine.data.Bookmark;
import nz.org.riskscape.engine.function.IdentifiedFunction;
import nz.org.riskscape.engine.function.IdentifiedFunction.Category;
import nz.org.riskscape.engine.function.RiskscapeFunction;
import nz.org.riskscape.engine.function.maths.Max;
import nz.org.riskscape.engine.function.maths.Min;
import nz.org.riskscape.engine.i18n.MutableMessageSource;
import nz.org.riskscape.engine.i18n.TranslationContext;
import nz.org.riskscape.engine.output.Format;
import nz.org.riskscape.engine.resource.Resource;
import nz.org.riskscape.engine.rl.agg.AggregationFunction;
import nz.org.riskscape.engine.types.Struct;
import nz.org.riskscape.engine.types.Types;
import nz.org.riskscape.rl.ast.StructDeclaration;
import nz.org.riskscape.wizard.Choice;
import nz.org.riskscape.wizard.Question;
import nz.org.riskscape.wizard.bld.IncrementalBuildState;

public class ChoicesTest extends BaseSurvey2Test {

  DefaultQuestionSet2 setOne;
  DefaultQuestionSet2 setTwo;

  public static final Locale NZ = new Locale("en", "NZ");

  MutableMessageSource messageSource = new MutableMessageSource();
  TranslationContext translationContext = new TranslationContext(NZ, messageSource);

  @Before
  public void setEngineMessages() {
    engine.setMessages(messages);
  }

  enum TestEnum {
    ONE,
    TWO,
    THREE
  }

  Bookmark bookmark1 = new Bookmark("foo", "Foo bookmark", null, URI.create("foo.csv"), ImmutableMap.of());
  Bookmark bookmark2 = new Bookmark("bar", "Bar bookmark", null, URI.create("bar.asc"), ImmutableMap.of());

  @Override
  protected BaseSurvey createSurvey() {
    return new BaseSurvey("test", new MutableMessageSource(), self ->
      Arrays.asList(
        BasePhase.simple(survey, "one", false, qs -> {
          setOne = qs;
        }),
        BasePhase.simple(survey, "two", false, qs -> {
          setTwo = qs;
        })
      )

    );
  }

  @Test
  public void getChoicesWillReturnAListOfQuestionSetOptionsFromTheCurrentPhase() throws Exception {
    Question q1 = new Question("q1", String.class).inSet(setOne);
    // an empty build state should return a PickQuestionSet question

    Question question = survey.getNextQuestion(buildState);
    assertThat(question.getParameterType(), equalTo(PickQuestionSet.class));

    List<PickQuestionSet> choices = Choices.from(question, buildState).stream()
        .map(c -> c.getDerivedFrom(PickQuestionSet.class))
        .toList();

    assertThat(choices, contains(new PickQuestionSet("one")));

    answer(question, "one");
    answer(q1, "foo");

    // now check again - we should have moved on to the 2nd phase now
    question = survey.getNextQuestion(buildState);
    assertThat(question.getParameterType(), equalTo(PickQuestionSet.class));

    choices = Choices.from(question, buildState).stream()
        .map(c -> c.getDerivedFrom(PickQuestionSet.class))
        .toList();
    assertThat(choices, contains(new PickQuestionSet("two")));
  }

  @Test
  public void getChoicesCanReturnFormatChoices() {
    Question question = new Question("format", Format.class).inSet(setOne);
    List<String> choices = question.getChoices(buildState).stream().map(Choice::getValue).toList();

    assertThat(choices, hasItems(
       "csv",
       "shapefile",
       "geojson",
       "geopackage"
    ));
  }


  @Test
  public void forEnumsCanReturnSubsetOfEnumValues() {
    List<Choice> result = Choices.forEnums(List.of(TestEnum.ONE, TestEnum.TWO));
    assertThat(result, contains(
      hasProperty("value", equalTo(TestEnum.ONE.toString())),
      hasProperty("value", equalTo(TestEnum.TWO.toString()))
    ));
    assertThat(result, not(contains(hasProperty("value", equalTo(TestEnum.THREE.toString())))));
  }

  @Test
  public void forEnumsHasI18n() {
    String label1 = "One";
    String description1 = "This is the first entry";
    String description2 = "This is the second entry";
    messageSource.addMessage(NZ, "nz.org.riskscape.wizard.survey2.ChoicesTest.TestEnum.ONE", label1);
    messageSource.addMessage(NZ, "nz.org.riskscape.wizard.survey2.ChoicesTest.TestEnum.ONE.description", description1);
    messageSource.addMessage(NZ, "nz.org.riskscape.wizard.survey2.ChoicesTest.TestEnum.TWO.description", description2);
    List<Choice> result = Choices.forEnums(List.of(TestEnum.ONE, TestEnum.TWO));
    assertThat(result.get(0).getLabel(translationContext), equalTo(label1));
    assertThat(result.get(0).getDescription(translationContext), equalTo(description1));
    assertThat(result.get(1).getDescription(translationContext), equalTo(description2));
  }

  @Test
  public void forEnumsI18nSupportsMultipleLanguages() {
    TranslationContext frenchContext = new TranslationContext(Locale.FRENCH, messageSource);

    String english = "The first entry";
    String french = "Le premier article";
    messageSource.addMessage(NZ, "nz.org.riskscape.wizard.survey2.ChoicesTest.TestEnum.ONE.description", english);
    messageSource.addMessage(
      Locale.FRENCH,
      "nz.org.riskscape.wizard.survey2.ChoicesTest.TestEnum.ONE.description",
      french
    );
    Choice result = Choices.forEnums(List.of(TestEnum.ONE)).get(0);

    assertThat(result.getDescription(translationContext), equalTo(english));
    assertThat(result.getDescription(frenchContext), equalTo(french));
  }

  @Test
  public void forEnumReturnsChoicesForAllValuesOfThatEnum() {
    List<Choice> result = Choices.forEnum(TestEnum.class);
    assertThat(result, contains(
      hasProperty("value", equalTo(TestEnum.ONE.toString())),
      hasProperty("value", equalTo(TestEnum.TWO.toString())),
      hasProperty("value", equalTo(TestEnum.THREE.toString()))
    ));
  }

  @Test
  public void forBookmarksReturnsChoiceWithBookmarkInfo() {

    project.add(bookmark1);

    Choice bookmarkChoice = Choices.forBookmarks(buildState).get(0);

    assertThat(bookmarkChoice.getDerivedFrom(Bookmark.class), is(bookmark1));
    assertThat(bookmarkChoice.getDescription(translationContext), is(bookmark1.getDescription()));
    assertThat(bookmarkChoice.getLabel(translationContext), is(bookmark1.getId()));
  }

  @Test
  public void forBookmarksReturnsChoices() {

    project.add(bookmark1);
    project.add(bookmark2);

    List<Choice> result = Choices.forBookmarks(buildState);
    List<Bookmark> derivedFrom = result.stream().map(c -> c.getDerivedFrom(Bookmark.class)).toList();

    assertThat(derivedFrom, containsInAnyOrder(is(bookmark1), is(bookmark2)));
  }

  @Test
  public void forFunctionsReturnsCorrectChoices() {

    RiskscapeFunction func = Mockito.mock(RiskscapeFunction.class);
    IdentifiedFunction wrapper = new IdentifiedFunction.Wrapping(
      func, "foo", "Description", Resource.UNKNOWN_URI, Category.MISC
    );

    project.add(wrapper);

    List<Choice> result = Choices.forFunctions(buildState, EnumSet.of(Category.MISC));
    assertThat(result, hasItem(is(new Choice("foo", "foo", Optional.of("Description"), wrapper))));

    // Check that filtering works as expected
    result = Choices.forFunctions(buildState, EnumSet.of(Category.UNASSIGNED));
    assertThat(result, is(empty()));
  }

  @Test
  public void forScopeReturnsAttributeChoicesWithinInputScope() {
    Struct scope = Struct.of(
        "exposure", Struct.of("value", Types.INTEGER),
        "hazard", Struct.of("geom", Types.GEOMETRY, "pga", Types.FLOATING)
    );

    Question q1 = new Question("q1", StructDeclaration.class).inSet(setOne);
    IncrementalBuildState fauxState = mock(IncrementalBuildState.class);
    when(fauxState.getInputStruct(eq(q1))).thenReturn(scope);
    when(fauxState.getQuestionSet()).thenReturn(setOne);

    List<Choice> result = Choices.forScope(fauxState, q1);

    List<String> labels = result.stream().map(c -> c.getLabel(translationContext)).toList();

    assertThat(labels, contains(
        "exposure.value", "exposure",
        "hazard.geom", "hazard.pga", "hazard"
    ));
  }

  @Test
  public void canReturnAttributeChoicesForSpecificScope() {
    Struct scope = Struct.of(
        "exposure", Struct.of("value", Types.INTEGER),
        "hazard", Struct.of("geom", Types.GEOMETRY, "pga", Types.FLOATING)
    );

    Question q1 = new Question("q1", StructDeclaration.class).inSet(setOne);

    // just pass in the scope directly instead of a build state
    List<Choice> result = Choices.forScope(scope, q1);

    List<String> labels = result.stream().map(c -> c.getLabel(translationContext)).toList();

    assertThat(labels, contains(
        "exposure.value", "exposure",
        "hazard.geom", "hazard.pga", "hazard"
    ));
  }

  @Test
  public void forAggregationReturnsMathsAggregationFunctions() {

    FunctionSet functions = buildState.getProject().getFunctionSet();
    functions.clear();

    functions.addAll(List.of(
      new Max(),
      new Min(),
      new ToListFunction().builtin("to_list", Category.LANGUAGE),
      AggregationFunction.asFunction(new AALHazardBasedFunction()).builtin("aal_trapz", Category.MATHS)));


    List<Choice> result = Choices.forAggregation(buildState);
    List<String> functionNames = result.stream().map(c -> c.getValue()).toList();

    assertThat(functionNames, hasItems("max","min"));


    assertThat(functionNames, not(hasItems(
      "to_list", // We don't want non-maths functions coming in
      "aal_trapz"))); // Nor functions that are overly complex for wizard users
  }

  @Test
  public void forAggregationNonInteractiveReturnsExtraPercentileFunctions() {


    List<Choice> result = Choices.forAggregationNonInteractive(buildState);
    List<String> functionNames = result.stream().map(c -> c.getValue()).toList();

    assertThat(functionNames, hasItems(
      "percentile(x, 75)",
      "percentile(x, 90)",
      "percentile(x, 95)",
      "percentile(x, 99)"));
  }

  @Test
  public void forAggregationNonInteractiveReturnsMaxMin() {
    // See #1315

    List<Choice> result = Choices.forAggregationNonInteractive(buildState);
    List<String> functionNames = result.stream().map(Choice::getValue).toList();

    assertThat(functionNames, hasItems("max", "min"));
  }

  @Test
  public void forAggregationNonInteractiveDoesntReturnPercentiles() {
    // See platform#376
    List<Choice> result = Choices.forAggregationNonInteractive(buildState);
    List<String> functionNames = result.stream().map(Choice::getValue).toList();

    // These two functions have multiple arguments, and so should not show up
    assertThat(functionNames, both(not(hasItem("percentile"))).and(not(hasItem("percentiles"))));

  }
}
