/*
 * 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 nz.org.riskscape.wizard.AnswerMatchers.*;
import static org.hamcrest.Matchers.*;
import static org.hamcrest.Matchers.contains;
import static org.junit.Assert.*;
import static org.mockito.ArgumentMatchers.*;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.*;

import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.Arrays;
import java.util.Collections;
import java.util.LinkedList;
import java.util.List;
import java.util.Locale;
import java.util.stream.Collectors;

import org.hamcrest.Matcher;
import org.hamcrest.collection.IsIterableContainingInOrder;
import org.junit.Before;
import org.junit.Test;
import org.mockito.Mockito;

import com.google.common.collect.Lists;

import nz.org.riskscape.cli.Terminal;
import nz.org.riskscape.engine.ProjectTest;
import nz.org.riskscape.engine.cli.TerminalTestHelper;
import nz.org.riskscape.engine.i18n.MutableMessageSource;
import nz.org.riskscape.engine.util.Pair;
import nz.org.riskscape.wizard.bind.PickQuestionSetBinder;
import nz.org.riskscape.wizard.bld.IncrementalBuildState;
import nz.org.riskscape.wizard.survey2.BasePhase;
import nz.org.riskscape.wizard.survey2.DefaultQuestionSet2;
import nz.org.riskscape.wizard.survey2.PickQuestionSet;

/**
 * Tests the behaviour of the {@link CliWizardProcessor2} against a BaseSurvey with various tree structures
 */
public class CliWizardProcessor2Test extends ProjectTest {

  Survey survey = new nz.org.riskscape.wizard.survey2.BaseSurvey(
      "test", new MutableMessageSource(), s -> Arrays.asList(newPhase(s))
  );
  CliPrompter prompter = Mockito.mock(CliPrompter.class);

  WizardActions actions = Mockito.mock(WizardActions.class);

  TerminalTestHelper terminalHelper = new TerminalTestHelper();
  Terminal terminal = terminalHelper.terminal;

  LinkedList<Pair<String, Object>> expectedResponsesInOrder = new LinkedList<>();
  LinkedList<Pair<String, List<String>>> expectedChoicesInOrder = new LinkedList<>();

  CliWizardProcessor2 processor;

  // bound values used for responding to PickQuestionSet questions
  PickQuestionSet pickAllOptional = new PickQuestionSet("allOptional");
  PickQuestionSet pickAllRequired = new PickQuestionSet("allRequired");
  PickQuestionSet pickOneRequiredWithHidden = new PickQuestionSet("oneRequiredWithHidden");
  PickQuestionSet pickOneOptionalOneRequired = new PickQuestionSet("oneOptionalOneRequired");
  PickQuestionSet pickANumber = new PickQuestionSet("aNumber");

  @Before
  public void setup() throws IOException {
    Path tmpFile = Files.createTempDirectory("rs");
    project.setOutputBaseLocation(tmpFile.toUri());

    engine.getBinders().add(new PickQuestionSetBinder());

    when(prompter.getTerminal()).thenReturn(terminal);
    when(prompter.getMessages()).thenReturn(messages);
    when(prompter.getLocale()).thenReturn(Locale.getDefault());

    setupPickAQuestionMocking();

    setupAskQuestionMocking();


    processor = new CliWizardProcessor2(project, prompter, actions);
  }

  //
  /**
   * pickle the question asking to return the response from the list, failing if it's not as expected, also handles
   * question set asking correctly
   */
  private void setupAskQuestionMocking() {
    when(prompter.askWithRepeat(any(IncrementalBuildState.class), any(Question.class))).then(input -> {
      try {
        Question question = input.getArgument(1);

        if (expectedResponsesInOrder.isEmpty()) {
          throw new AssertionError("Unexpected question being asked - " + question);
        }
        Pair<String, Object> expected = expectedResponsesInOrder.peek();
        String expectedQNamed = expected.getLeft();
        Object expectedAnswer = expected.getRight();

        String actualQName = question.getName();

        // not as expected, fail
        if (!actualQName.equals(expectedQNamed)) {
          // out of order - go bang
          throw new AssertionError("out of order question being asked, expected askWithRepeat with a question named "
          + expectedQNamed + " but got " + actualQName);
        }

        return expectedAnswer;
      } finally {
        if (!expectedResponsesInOrder.isEmpty()) {
          System.out.println("Clearing response expectation " + expectedResponsesInOrder.removeFirst());
        }
      }
    });
  }

  /**
   * pickle the prompter to expect certain choices and then respond with whatever was set up with respondToQuestionWith
   */
  private void setupPickAQuestionMocking() {
    when(prompter.choose(eq("Pick A Question"), any(), any())).then(input -> {
      List<Question> choices = input.getArgument(1);

      String choiceToMake = expectedChoicesInOrder.peek().getLeft();
      List<String> expectedChoices = expectedChoicesInOrder.peek().getRight();
      // build some matchers to check that the user is presented with the choices we expected next

      List<Matcher<? super Question>> questionMatchers = expectedChoices.stream()
        .map(ec ->
          hasProperty("name", equalTo(ec))
        )
        .collect(Collectors.toList());

      assertThat(
        choices,
        new IsIterableContainingInOrder<>(
            questionMatchers
        )
      );

      Question found = choices.stream().filter(q -> q.getName().equals(choiceToMake)).findAny().orElse(null);

      if (found == null) {
        throw new AssertionError("Question " + choiceToMake + " was not in the list of questions that was "
            + "presented:" + choices);
      }

      // expectation met, drop the choices now we've seen them
      System.out.println("Clearing choice expectation " + expectedChoicesInOrder.removeFirst());

      return found;
    });
  }

  /**
   * Construct the phase we will use for testing our cli
   * @param testPhase
   * @return
   */
  public BasePhase newPhase(Survey testPhase) {
    return new BasePhase(testPhase) {

      @Override
      public List<QuestionSet> getAvailableQuestionSets(IncrementalBuildState buildState) {
        if (buildState.isEmpty()) {
          return super.getAvailableQuestionSets(buildState);
        } else {
          // can pick only one question set
          return Collections.emptyList();
        }
      }

      @Override
      public List<QuestionSet> buildQuestionSets(IncrementalBuildState buildState) {
        return Arrays.asList(
          new DefaultQuestionSet2(pickAllOptional.questionSetId, this)
            .addQuestion("foo", String.class).customizeQuestion(Question::optionalOne).thenNoChange()
            .addQuestion("bar", String.class).customizeQuestion(Question::optionalOne).thenNoChange()
            .addQuestion("baz", String.class).customizeQuestion(Question::optionalOne).thenNoChange(),

          new DefaultQuestionSet2(pickAllRequired.questionSetId, this)
            .addQuestion("foo", String.class).thenNoChange()
            .addQuestion("bar", String.class).thenNoChange()
            .addQuestion("baz", String.class).thenNoChange(),

          new DefaultQuestionSet2(pickOneRequiredWithHidden.questionSetId, this)
            .addQuestion("foo", String.class).thenNoChange()
            .addHiddenQuestion("bar").thenNoChange(),

          new DefaultQuestionSet2(pickANumber.questionSetId, this)
            .addQuestion("foo", Long.class).thenNoChange(),

          new DefaultQuestionSet2(pickOneOptionalOneRequired.questionSetId, this)
            .addQuestion("optional", Long.class).customizeQuestion(q -> q.optionalOne()).thenNoChange()
            .addQuestion("required", String.class).thenNoChange()
        );
      }
    };
  }

  @Test
  public void canAnswerASetOfRequiredQuestions() {
    respondToQuestionWith("question-choice-1", "allRequired");
    respondToQuestion("foo");
    respondToQuestion("bar");
    respondToQuestion("baz");

    assertThat(
      runSurvey(),
      contains(
        answerHasBoundValue("question-choice-1", equalTo(pickAllRequired)),
        answerHasOriginalResponse("foo", equalTo("foo")),
        answerHasOriginalResponse("bar", equalTo("bar")),
        answerHasOriginalResponse("baz", equalTo("baz"))
      )
    );
  }

  @Test
  public void canAnswerASetOfOptionalQuestions() {
    respondToQuestionWith("question-choice-1", "allOptional");
    chooseFrom("foo", "foo", "bar", "baz", "skip");
    respondToQuestion("foo");
    chooseFrom("bar", "bar", "baz", "skip");
    respondToQuestion("bar");
    chooseFrom("baz", "baz", "skip");
    respondToQuestion("baz");

    assertThat(
      runSurvey(),
      contains(
        answerHasBoundValue("question-choice-1", equalTo(pickAllOptional)),
        answerHasOriginalResponse("foo", equalTo("foo")),
        answerHasOriginalResponse("bar", equalTo("bar")),
        answerHasOriginalResponse("baz", equalTo("baz"))
      )
    );
  }

  @Test
  public void canSkipOverSomeOptionalQuestions() {

    respondToQuestionWith("question-choice-1", "allOptional");
    chooseFrom("bar", "foo", "bar", "baz", "skip");
    respondToQuestion("bar");
    chooseFrom("skip", "baz", "skip");

    assertThat(
      runSurvey(),
      contains(
        answerHasBoundValue("question-choice-1", equalTo(pickAllOptional)),
        skippedQuestion("foo"),
        answerHasOriginalResponse("bar", equalTo("bar")),
        skippedQuestion("baz")
      )
    );
  }

  @Test
  public void canNotSkipARequiredQuestionWhenPreceededByAnOptionalQuestion() {

    respondToQuestionWith("question-choice-1", "oneOptionalOneRequired");
    chooseFrom("required", "optional", "required");
    respondToQuestion("required");


    assertThat(
      runSurvey(),
      contains(
        answerHasBoundValue("question-choice-1", equalTo(pickOneOptionalOneRequired)),
        skippedQuestion("optional"),
        answerHasOriginalResponse("required", equalTo("required"))
      )
    );
  }

  @Test
  public void hiddenQuestionNotShown() {
    respondToQuestionWith("question-choice-1", "oneRequiredWithHidden");
    respondToQuestion("foo");

    assertThat(
      runSurvey(),
      contains(
        answerHasBoundValue("question-choice-1", equalTo(pickOneRequiredWithHidden)),
        answerHasOriginalResponse("foo", equalTo("foo")),
        answerHasBoundValue("bar", equalTo(Question.HIDDEN_VALUE))
      )
    );
  }

  @Test
  public void aBindingErrorWillAskTheQuestionAgain() throws Exception {
    respondToQuestionWith("question-choice-1", "aNumber");
    // first with a bad number
    respondToQuestionWith("foo", "one");
    // and now with a good one
    respondToQuestionWith("foo", "1");

    assertThat(
      runSurvey(),
      contains(
        answerHasBoundValue("question-choice-1", equalTo(pickANumber)),
        answerHasOriginalResponse("foo", equalTo("1"))
      )
    );
  }

  private List<Answer> runSurvey() {
    processor.newProcessor(survey);
    return Lists.reverse(processor.runInsideInterrupt().getAllAnswers());
  }

  private void chooseFrom(String choose, String... questionNames) {
    expectedChoicesInOrder.add(Pair.of(choose, Arrays.asList(questionNames)));
  }

  private void respondToQuestion(String expectedQNamed) {
    respondToQuestionWith(expectedQNamed, expectedQNamed);
  }

  private void respondToQuestionWith(String expectedQNamed, Object respondWith) {
    expectedResponsesInOrder.add(Pair.of(expectedQNamed, respondWith));
  }

}
