/*
 * 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.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.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;

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

import com.google.common.collect.Lists;

import nz.org.riskscape.engine.bind.BindingContext;
import nz.org.riskscape.engine.i18n.MessageSource;
import nz.org.riskscape.engine.i18n.MutableMessageSource;
import nz.org.riskscape.engine.pipeline.ExecutionContext;
import nz.org.riskscape.problem.ResultOrProblems;
import nz.org.riskscape.wizard.bld.IncrementalBuildState;
import nz.org.riskscape.wizard.bld.PipelineChange;
import nz.org.riskscape.wizard.bld.change.NoChange;
import nz.org.riskscape.wizard.survey2.QuestionTree;

public class WizardProcessorTest {

  public TestQuestionSet questionSet;

  Survey survey = new Survey() {

    {
      questionSet = new TestQuestionSet("test", this);
    }

    @Override
    public boolean isFinished(IncrementalBuildState buildState) {
      return getQuestionTree(buildState).isComplete(buildState);
    }

    @Override
    public QuestionTree getQuestionTree(IncrementalBuildState buildState) {
      return QuestionTree.fromList(questionSet.getQuestions());
    }

    @Override
    public PipelineChange getPipelineChange(IncrementalBuildState buildState, Answer answer) {
      return questionSet.getPipelineChange(buildState, answer);
    }

    @Override
    public MessageSource getMessageSource() {
      return new MutableMessageSource();
    }

    @Override
    public String getId() {
      return "test";
    }

    @Override
    public List<QuestionSet> getApplicableQuestionSets(IncrementalBuildState buildState) {
      return Collections.singletonList(questionSet);
    }
  };

  ExecutionContext executionContext = Mockito.mock(ExecutionContext.class);
  BindingContext bindingContext = Mockito.mock(BindingContext.class);
  WizardProcessor processor;

  Question required = new Question("foo", String.class).requiredOne();
  Question optional = new Question("foo", String.class).optionalOne();

  @Before
  public void setup() {
    when(executionContext.getBindingContext()).thenReturn(bindingContext);
    when(bindingContext.bind(any(), eq(String.class))).thenAnswer(inv -> ResultOrProblems.of(inv.getArgument(0)));
    processor = new WizardProcessor(executionContext, survey);
  }

  @Test
  public void anEmptySurveyIsComplete() {
    assertThat(processor.isDone(), is(true));
  }

  @Test
  public void canAnswerASingleQuestionSurvey() throws Exception {
    questionSet.add(required);

    // pre answer
    assertThat(processor.isDone(), is(false));
    assertThat(processor.getNextQuestions(), contains(questionNamed("foo")));

    apply("foo", "foo-answer");

    assertAnswers(answered("foo", "foo-answer"));
    assertGoodAndDone();
  }

  @Test
  public void canSkipOptionalQuestions() throws Exception {
    questionSet.add(optional);

    // apply the answer
    skip("foo");

    assertAnswers(skipped("foo"));
    assertGoodAndDone();
  }

  @Test(expected = IllegalArgumentException.class)
  public void canNotSkipRequiredQuestions() throws Exception {
    questionSet.add(required);

    // this is a coding error, rather than a user error
    skip("foo");
  }

  @Test
  public void automaticallySkipsOptionalQuestions() throws Exception {
    questionSet.add(optional);
    questionSet.add(optional.withName("bar"));
    questionSet.add(optional.withName("baz"));

    assertNextQuestions("foo", "bar", "baz");

    apply("baz", "baz answer");

    assertAnswers(skipped("foo"), skipped("bar"), answered("baz"));
    assertGoodAndDone();
  }

  @Test
  public void requiredQuestionsMustBeAnsweredFirst() throws Exception {
    questionSet.add(required);
    questionSet.add(optional.withName("bar"));
    questionSet.add(optional.withName("baz"));

    assertNextQuestions("foo");

    apply("foo", "foo answer");
    assertAnswers(answered("foo"));

    assertNextQuestions("bar", "baz");
  }

  @Test
  public void canAnswerASeriesOfQuestions() throws Exception {
    questionSet.add(optional);
    questionSet.add(optional.withName("bar"));
    questionSet.add(optional.withName("baz"));

    assertNextQuestions("foo", "bar", "baz");

    apply("foo", "foo answer");
    assertNextQuestions("bar", "baz");
    apply("bar", "bar answer");
    assertNextQuestions("baz");
    apply("baz", "baz answer");

    assertAnswers(answered("foo"), answered("bar"), answered("baz"));
    assertGoodAndDone();
  }

  @Test
  public void canUndoAQuestion() throws Exception {

    questionSet.add(optional);

    // TODO come back at the end and see if we can drop this?
    processor.getNextQuestions();
    apply("foo", "foo-1");

    assertAnswers(answered("foo", "foo-1"));
    assertGoodAndDone();

    processor.undo();

    assertNextQuestions("foo");
    assertThat(processor.getBuildState().getAllAnswers(), empty());
    assertFalse(processor.isDone());

    // answer it differently this time
    apply("foo", "foo-2");

    assertAnswers(answered("foo", "foo-2"));
    assertGoodAndDone();
  }

  @Test
  public void canUndoPastHiddenTrailingQuestions() {
    questionSet.add(optional.withName("first"));
    questionSet.add(optional.withName("second"));
    questionSet.add(required.hidden().withName("hidden"));

    processor.getNextQuestions();
    apply("first", "required");
    apply("second", "answer");

    // hidden should be skipped
    assertAnswers(answered("first"), answered("second"), hidden("hidden"));
    assertGoodAndDone();

    processor.undo();

    // undo took us past the trailing hidden question and past the last recorded response (second)
    assertAnswers(answered("first"));
    assertNextQuestions("second");
    assertGood();
  }

  @Test
  public void undoDoesntRemoveHiddenQuestions() {
    questionSet.add(optional.withName("first"));
    questionSet.add(required.hidden().withName("hidden"));
    questionSet.add(required.withName("second"));

    processor.getNextQuestions();
    apply("first", "required");

    // hidden should be skipped
    assertAnswers(answered("first"), hidden("hidden"));

    apply("second", "answer");

    assertAnswers(answered("first"), hidden("hidden"), answered("second"));

    // This removes the response to second, but appears to leave the trailing hidden question alone
    processor.undo();

    assertAnswers(answered("first"), hidden("hidden"));
    assertGood();
  }

  @Test
  public void undoAllowsChoseOtherOptional() {
    questionSet.add(optional.withName("first"));
    questionSet.add(optional.withName("second"));

    assertNextQuestions("first", "second");

    // this skips the first
    apply("second", "answer");

    assertAnswers(skipped("first"), answered("second"));

    processor.undo();

    // we are offered the first question again, i.e. we undid the skip that was done when we answered the second
    // question
    assertNextQuestions("first", "second");
    assertGood();
  }

  @Test
  public void automaticallySkipsTrailingHiddenQuestionsAfterAnswering() throws Exception {
    questionSet.add(optional);
    questionSet.add(required.hidden().withName("bar"));
    questionSet.add(required.hidden().withName("baz"));

    assertNextQuestions("foo");

    apply("foo", "foo answer");

    assertAnswers(answered("foo"), hidden("bar"), hidden("baz"));
    assertGoodAndDone();
  }

  @Test
  public void automaticallySkipsTrailingHiddenQuestionsAfterSkipping() throws Exception {
    questionSet.add(optional);
    questionSet.add(required.hidden().withName("bar"));

    assertNextQuestions("foo");

    skip("foo");

    assertAnswers(skipped("foo"), hidden("bar"));
    assertGoodAndDone();
  }

  @Test
  public void automaticallySkipsTrailingAndNestedHiddenQuestionsAfterAnswering() throws Exception {
    Question foo = questionSet.add(optional);
    questionSet.add(required.withName("after-foo").hidden());

    Question bar = questionSet.add(required.withName("bar").dependsOn(foo));
    questionSet.add(required.withName("after-bar").hidden().dependsOn(foo));

    questionSet.add(required.withName("baz").dependsOn(bar));
    questionSet.add(required.withName("after-baz").hidden().dependsOn(bar));


    assertNextQuestions("foo");
    apply("foo", "foo answer");

    assertNextQuestions("bar");
    apply("bar", "bar answer");

    assertNextQuestions("baz");
    apply("baz", "baz answer");

    assertAnswers(
      answered("foo"),
      answered("bar"),
      answered("baz"),
      // note these get answered in sort of reverse order
      hidden("after-baz"),
      hidden("after-bar"),
      hidden("after-foo")
    );
    assertGoodAndDone();
  }

  @Test
  public void trailingHiddenQuestionsAreSkippedIfDependencyWasSkipped() throws Exception {
    Question foo = questionSet.add(optional);
    questionSet.add(required.withName("after-foo").hidden());

    questionSet.add(required.withName("bar").dependsOn(foo));
    questionSet.add(required.withName("after-bar").hidden().dependsOn(foo));


    assertNextQuestions("foo");
    skip("foo");

    assertGoodAndDone();

    assertAnswers(
      skipped("foo"),
      hidden("after-foo")
    );
  }

  @Test
  public void questionSetsGetReusedUntilTheyAreExhausted() throws Exception {
    Survey mockSurvey = mock(Survey.class);

    QuestionTree qt1 = QuestionTree.fromList(Arrays.asList(questionSet.add(optional)));
    QuestionTree qt2 = QuestionTree.fromList(Arrays.asList(questionSet.add(optional.withName("bar"))));

    when(mockSurvey.getQuestionTree(any())).thenReturn(qt1, qt2);
    when(mockSurvey.skip(any(), any())).thenAnswer(inv -> {
      return new NoChange(Answer.skip(inv.getArgument(1)));
    });
    processor = new WizardProcessor(executionContext, mockSurvey);

    // multiple times doesn't matter, keeps using the same question tree
    assertNextQuestions("foo");
    assertNextQuestions("foo");

    skip("foo");

    // same here
    assertNextQuestions("bar");
    assertNextQuestions("bar");
  }

  @Test
  public void hiddenQuestionsAtStartOfNextQuestionSetAreSkipped() {
    Survey mockSurvey = mock(Survey.class);

    QuestionTree qt1 = QuestionTree.fromList(List.of(questionSet.add(required)));
    QuestionTree qt2 = QuestionTree.fromList(List.of(
        questionSet.add(required.hidden().withName("hidden")),
        questionSet.add(required.withName("after")))
        // Suppose that "after" relies on pipeline changes from "hidden" happening before the question makes sense,
        // like questions in the report phase
    );

    when(mockSurvey.getQuestionTree(any())).thenReturn(qt1, qt2);
    when(mockSurvey.skip(any(), any())).thenAnswer(inv ->
       new NoChange(Answer.hidden(inv.getArgument(1)))
    );
    when(mockSurvey.getPipelineChange(any(), any())).thenAnswer(inv ->
      new NoChange(inv.getArgument(1))
    );

    processor = new WizardProcessor(executionContext, mockSurvey);

    assertNextQuestions("foo");

    // Answer the last question in the QuestionSet
    apply("foo", "foo answer");

    // Hidden question in next stage is answered immediately
    assertAnswers(answered("foo"), hidden("hidden"));

    // Which means "after" is happy
    assertNextQuestions("after");
    apply("after", "after answer");
    assertAnswers(answered("foo"), hidden("hidden"), answered("after"));
  }

  @Test
  public void notLeftWithOnlyHiddenInQuestionTreeAfterUndo() {

    // WizardProcessor::getNextQuestions should not be called if there are only hidden questions left - we should skip
    // the trailing hidden questions before trying to get new ones. This test makes sure that if we undo an answer,
    // we aren't left in a situation where the only questions left are hidden ones.
    // See #1325

    Survey mockSurvey = mock(Survey.class);


    when(mockSurvey.getQuestionTree(any())).thenAnswer(inv -> {
      IncrementalBuildState bs = inv.getArgument(0);

      String lastAnsweredQuestion = bs.getQuestion().getName();

      QuestionTree first = QuestionTree.fromList(List.of(questionSet.add(required.withName("first")),
              questionSet.add(required.hidden().withName("hidden"))));

      QuestionTree second = QuestionTree.fromList(List.of(questionSet.add(required.withName("second"))));

      // Base survey returns the new QuestionTree when we've answered the last question of the previous one.
      // Simulate this behaviour for the test
      return switch(lastAnsweredQuestion) {
        default -> first;
        case "hidden" -> second;
        case "second" -> QuestionTree.empty();
      };
    });


    when(mockSurvey.skip(any(), any())).thenAnswer(inv ->
            new NoChange(Answer.hidden(inv.getArgument(1)))
    );
    when(mockSurvey.getPipelineChange(any(), any())).thenAnswer(inv ->
            new NoChange(inv.getArgument(1))
    );

    processor = new WizardProcessor(executionContext, mockSurvey);

    assertNextQuestions("first");
    apply("first", "answer");

    // Hidden question should be skipped
    assertAnswers(answered("first"), hidden("hidden"));

    assertNextQuestions("second");
    apply("second", "answer");

    assertAnswers(answered("first"), hidden("hidden"), answered("second"));
    assertGoodAndDone();

    processor.undo();

    assertNextQuestions("second"); // This would throw the error
    assertGood();
  }

  @SafeVarargs
  private void assertAnswers(Matcher<Answer>... matchers) {
    List<Answer> allAnswers = processor.getBuildState().getAllAnswers();
    allAnswers = Lists.reverse(allAnswers);


    assertThat(allAnswers, contains(matchers));
  }

  private void skip(String questionName) {
    processor.skip(questionSet.get(questionName));
  }

  private void apply(String questionName, String response) {
    processor.applyAnswer(Answer.single(questionSet.get(questionName), response, response));
  }

  private void assertNextQuestions(String... names) {
    // NB java compiler didn't like the stream version of this (I think because of generic inference?)
    List<Matcher<? super Question>> matchers = new ArrayList<>();
    for (String name : names) {
      matchers.add(questionNamed(name));
    }

    assertThat(processor.getNextQuestions(), contains(matchers));
  }

  private void assertGood() {
    assertThat(processor, hasProperty("failed", is(false)));
  }

  private void assertGoodAndDone() {
    assertThat(processor, allOf(
        hasProperty("failed", is(false)),
        hasProperty("done", is(true)),
        hasProperty("nextQuestions", empty())
   ));
  }

  public Matcher<Answer> answered(String questionName, String response) {
    return allOf(
      hasProperty("question", questionNamed(questionName)),
      hasProperty("response", hasProperty("originalInput", equalTo(response)))
    );
  }

  public Matcher<Answer> answered(String questionName) {
    return allOf(
      hasProperty("question", questionNamed(questionName)),
      hasProperty("response", hasProperty("originalInput", not(nullValue())))
    );
  }

  public Matcher<Answer> skipped(String questionName) {
    return allOf(
      hasProperty("question", questionNamed(questionName)),
      hasProperty("values", empty())
    );
  }


  public Matcher<Answer> hidden(String questionName) {
    return allOf(
      hasProperty("question", questionNamed(questionName)),
      hasProperty("values", contains(Question.HIDDEN_VALUE))
    );
  }


  public  Matcher<Question> questionNamed(String name) {
    return hasProperty("name", equalTo(name));
  }

}
