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

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

import java.util.Arrays;
import java.util.LinkedHashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.function.Function;
import java.util.stream.Collectors;

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

import com.google.common.collect.ImmutableMap;

import nz.org.riskscape.engine.Assert;
import nz.org.riskscape.engine.DefaultEngine;
import nz.org.riskscape.engine.ProjectTest;
import nz.org.riskscape.engine.i18n.MutableMessageSource;
import nz.org.riskscape.engine.util.Pair;
import nz.org.riskscape.problem.ProblemException;
import nz.org.riskscape.wizard.bind.CompositeBinder;
import nz.org.riskscape.wizard.bld.IncrementalBuildState;
import nz.org.riskscape.wizard.bld.change.NoChange;
import nz.org.riskscape.wizard.model2.ModelSurvey2;

public class ConfigParserTest extends ProjectTest {

  Survey survey = new EmptySurvey("foo", new MutableMessageSource());
  IncrementalBuildState buildState = IncrementalBuildState.empty(
      new IncrementalBuildState.Context(survey, executionContext));

  EmptyQuestionSet qs1 = new EmptyQuestionSet("qs1", survey);
  Question q1 = new Question("q1", String.class).inSet(qs1).optionalMany();

  EmptyQuestionSet qs2 = new EmptyQuestionSet("qs2", survey);
  Question q2 = new Question("q2", String.class).inSet(qs2).optionalMany()
      .withType(ConfigParserTestComposite.class);

  Function<String, Survey> surveyConstructor = Mockito.mock(Function.class);

  ConfigParser parser = new ConfigParser(surveyConstructor);

  @Before
  public void setup() {
    Mockito.when(surveyConstructor.apply(Mockito.anyString())).thenAnswer(inv -> survey);
  }

  @Override
  public DefaultEngine createEngine() {
    DefaultEngine engine = super.createEngine();
    engine.getBinders().add(new CompositeBinder());

    return engine;
  }

  @Test
  public void canWriteOutAnswersWithCompositeResponses() {
    answer(Answer.strings(q1, "simple"));
    bindAnswer(q2, ImmutableMap.of("foo", "1", "bar", "yay"));
    assertThat(
      getResponses(),
      contains(
        //Pair.of("survey", survey.getId()),
        Pair.of("version", parser.getVersion(buildState)),
        Pair.of("qs1.q1[0]", "simple"),
        Pair.of("qs2.q2[0][foo]", "1"),
        Pair.of("qs2.q2[0][bar]", "yay")
      )
    );
  }

  @Test
  public void canWriteOutAnswersWithMultipleCompositeResponses() {
    answer(Answer.strings(q1, "lots", "of", "these"));

    bindAnswer(q2,
        ImmutableMap.of("foo", "1", "bar", "1"),
        ImmutableMap.of("foo", "2", "bar", "2")
    );

    assertThat(
      getResponses(),
      contains(
        //Pair.of("survey", survey.getId()),
        Pair.of("version", parser.getVersion(buildState)),
        Pair.of("qs1.q1[0]", "lots"),
        Pair.of("qs1.q1[1]", "of"),
        Pair.of("qs1.q1[2]", "these"),
        Pair.of("qs2.q2[0][foo]", "1"),
        Pair.of("qs2.q2[0][bar]", "1"),
        Pair.of("qs2.q2[1][foo]", "2"),
        Pair.of("qs2.q2[1][bar]", "2")
      )
    );
  }

  @Test
  public void canConvertAnswersWithCompositeResponsesToModelParameters() {
    answer(Answer.strings(q1, "simple"));
    bindAnswer(q2, ImmutableMap.of("foo", "1", "bar", "yay"));

    List<WizardModelParameter> parameters = parser.getModelParameters(buildState);

    assertThat(parameters, contains(
      hasProperty("question", hasProperty("name", is("q1"))),
      hasProperty("question", hasProperty("name", is("q2.foo"))),
      hasProperty("question", hasProperty("name", is("q2.bar")))
    ));
  }

  @Test
  public void canConvertAnswersWithMultipleCompositeResponsesToModelParameters() {
    answer(Answer.strings(q1, "lots", "of", "these"));

    bindAnswer(q2,
        ImmutableMap.of("foo", "1", "bar", "1"),
        ImmutableMap.of("foo", "2", "bar", "2")
    );

    List<WizardModelParameter> parameters = parser.getModelParameters(buildState);


    assertThat(
      parameters,
      contains(
        hasProperty("parameter", hasProperty("name", is("qs1.q1[0]"))),
        hasProperty("parameter", hasProperty("name", is("qs1.q1[1]"))),
        hasProperty("parameter", hasProperty("name", is("qs1.q1[2]"))),
        hasProperty("parameter", hasProperty("name", is("qs2.q2[0][foo]"))),
        hasProperty("parameter", hasProperty("name", is("qs2.q2[0][bar]"))),
        hasProperty("parameter", hasProperty("name", is("qs2.q2[1][foo]"))),
        hasProperty("parameter", hasProperty("name", is("qs2.q2[1][bar]")))
      )
    );
  }

  @Test
  public void canBuildABasicMapFromKeysWithIndices() throws Exception {
    // simplest case
    assertThat(
      parser.deserializeResponse(
        configMap(
          "foo[bar]", "great"
        ),
        "foo"
      ),
      equalTo(
        ImmutableMap.of("bar", "great")
      )
    );

    // many keys case
    assertThat(
      parser.deserializeResponse(
        configMap(
          "foo[bar]", "great",
          "foo[baz]", "awesome"
        ),
        "foo"
      ),
      equalTo(
        ImmutableMap.of("bar", "great", "baz", "awesome")
      )
    );

    // many values case
    assertThat(
      parser.deserializeResponse(
        configMap(
          "foo[0][bar]", "great",
          "foo[0][baz]", "awesome",
          "foo[1][bar]", "ace",
          "foo[1][baz]", "radical"
        ),
        "foo"
      ),
      equalTo(
        Arrays.asList(
          ImmutableMap.of("bar", "great", "baz", "awesome"),
          ImmutableMap.of("bar", "ace", "baz", "radical")
        )
      )
    );

    // inverted many values case
    assertThat(
      parser.deserializeResponse(
        configMap(
          "foo[bar][0]", "great",
          "foo[baz][0]", "awesome",
          "foo[bar][1]", "ace",
          "foo[baz][1]", "radical"
        ),
        "foo"
      ),
      equalTo(
        ImmutableMap.of(
          "bar", Arrays.asList("great", "ace"),
          "baz", Arrays.asList("awesome", "radical")
        )
      )
    );

    // nested maps
    assertThat(
      parser.deserializeResponse(
        configMap(
          "foo[bar][baz]", "great"
        ),
        "foo"
      ),
      equalTo(
        ImmutableMap.of("bar", ImmutableMap.of("baz", "great"))
      )
    );

    // duplicate keys
    ProblemException ex = Assert.assertThrows(ProblemException.class, () -> {
      parser.deserializeResponse(
          configMap(
            "foo[bar]", "great",
            "foo[bar]", "awesome"
          ),
          "foo"
        );
    });

    assertThat(
      ex.getProblems(),
      contains(
        WizardProblems.get().configError().withChildren(
          ConfigParser.PROBLEMS.multipleKeysGiven("foo[bar]")
        )
      )
    );

    // array indices must be 0..n
    ex = Assert.assertThrows(ProblemException.class, () -> {
      parser.deserializeResponse(
          configMap(
            "foo[0]", "great",
            "foo[2]", "awesome"
          ),
          "foo"
        );
    });

    assertThat(
      ex.getProblems(),
      contains(
        WizardProblems.get().configError().withChildren(
          ConfigParser.PROBLEMS.indicesNotContiguous("foo", 1)
        )
      )
    );
  }

  @Test
  public void noKeyDefaultsToOlderModelSurvey() throws Exception {
    Mockito.when(surveyConstructor.apply(Mockito.anyString())).thenAnswer(inv -> survey);

    // no survey key defaults to survey
    parser.getSurvey(configMap());
    Mockito.verify(surveyConstructor).apply(ModelSurvey2.ID);

    // otherwise constructor is given the specified config value
    parser.getSurvey(configMap("survey", "latest cool modes"));
    Mockito.verify(surveyConstructor).apply("latest cool modes");
  }

  @SuppressWarnings({ "rawtypes", "unchecked" })
  private LinkedHashMap<String, List<?>> configMap(String... keyPairs) {
    LinkedHashMap<String, List> building = new LinkedHashMap<>();
    for (int i = 0; i < keyPairs.length; i+=2) {
      String key = keyPairs[i];
      String value = keyPairs[i + 1];

      building.compute(key, (k, existing) -> {
        if (existing == null) {
          existing = new LinkedList<>();
        }
        existing.add(value);
        return existing;
      });
    }

    return (LinkedHashMap) building;
  }

  private void answer(Question q, List<Answer.Response> responses) {
    answer(new Answer(q, responses));
  }

  private void bindAnswer(Question q, Object... values) {
    answer(q, Arrays.asList(values).stream()
        .map(val -> Answer.bind(buildState.getBindingContext(), q, val).get())
        .collect(Collectors.toList()));
  }

  private void answer(Answer answer) {
    buildState =
        new NoChange(answer).make(buildState).get();
  }

  private List<Pair<String, String>> getResponses() {
    List<Pair<String, String>> responses = parser.getConfigToWrite(buildState).collect(Collectors.toList());
    return responses;
  }

}
