/*
 * 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.junit.Assert.*;
import static org.mockito.Mockito.*;

import java.util.Arrays;
import java.util.List;
import java.util.Locale;
import java.util.Optional;
import java.util.concurrent.atomic.AtomicBoolean;

import org.junit.Test;

import nz.org.riskscape.engine.Assert;
import nz.org.riskscape.engine.i18n.MutableMessageSource;
import nz.org.riskscape.wizard.bld.IncrementalBuildState;
import nz.org.riskscape.wizard.bld.IncrementalBuildState.Context;
import nz.org.riskscape.wizard.bld.change.NoChange;

public class QuestionTest {

  TestQuestionSet set = new TestQuestionSet("test", Survey.EMPTY_SURVEY);
  IncrementalBuildState buildState = IncrementalBuildState.empty(mock(Context.class));

  @Test
  public void testQuestionArity() throws Exception {
    List<Object> oneList = Arrays.asList("one");
    List<Object> twoList = Arrays.asList("one", "two");

    // the default is a single-value required question
    Question defaultQuestion = new Question("default", String.class);
    assertTrue(defaultQuestion.isSingleValueQuestion());
    assertTrue(defaultQuestion.isRequired());
    defaultQuestion.checkValidity(oneList);
    Assert.assertThrows(RuntimeException.class, () -> defaultQuestion.checkValidity(twoList));

    // sanity-check a required single-value question behaves the same
    Question requiredOne = new Question("default", String.class).requiredOne();
    assertEquals(defaultQuestion, requiredOne);
    assertTrue(defaultQuestion.isSingleValueQuestion());
    assertTrue(defaultQuestion.isRequired());

    Question requiredMany = new Question("default", String.class).atLeastOne();
    assertFalse(requiredMany.isSingleValueQuestion());
    assertTrue(requiredMany.isRequired());
    requiredMany.checkValidity(oneList);
    requiredMany.checkValidity(twoList);

    // check optional questions too
    Question optionalOne = new Question("optional", String.class).optionalOne();
    assertTrue(optionalOne.isSingleValueQuestion());
    assertFalse(optionalOne.isRequired());
    optionalOne.checkValidity(oneList);
    Assert.assertThrows(RuntimeException.class, () -> optionalOne.checkValidity(twoList));

    Question optionalMany = new Question("optional", String.class).optionalMany();
    assertFalse(optionalMany.isSingleValueQuestion());
    assertFalse(optionalMany.isRequired());
    optionalMany.checkValidity(oneList);
    optionalMany.checkValidity(twoList);
  }

  @Test
  public void testQuestionCanBeConditionallyAsked() throws Exception {
    buildState = mock(IncrementalBuildState.class);

    // use a conditional predicate that's true the 2nd time we ask
    Question question = set.add(new Question("conditional", String.class))
        .askWhen(state -> state.isResponseGiven("test", "foo"));

    when(buildState.isResponseGiven("test", "foo")).thenReturn(false);
    // first mock class means predicate will be false
    assertFalse(question.readyToBeAsked(buildState));

    when(buildState.isResponseGiven("test", "foo")).thenReturn(true);
    // predicate becomes true, so question can be asked
    assertTrue(question.readyToBeAsked(buildState));

    // by default questions without a predicate can be asked at any time
    Question defaultQuestion = new Question("default", String.class);
    assertTrue(defaultQuestion.readyToBeAsked(buildState));
  }

  // follows
  @Test
  public void testQuestionCanFollowAnother() throws Exception {

    Question first = set.add(new Question("first", String.class));
    Question second = set.add(new Question("second", String.class).dependsOn(first));

    assertTrue(second.isFollowing(first));
    assertFalse(first.isFollowing(second));

    assertTrue(first.readyToBeAsked(buildState));
    // second question is not ready yet because the first one hasn't been answered
    // yet
    assertFalse(second.readyToBeAsked(buildState));

    // once the first question has been answered, the second is ready to be asked
    buildState = new NoChange(Answer.strings(first, "foo")).make(buildState).get();

    assertTrue(first.readyToBeAsked(buildState));
    assertTrue(second.readyToBeAsked(buildState));
  }

  @Test
  public void testQuestionCanHavePredicateAndDependency() throws Exception {
    AtomicBoolean predicateMet = new AtomicBoolean(false);
    Question first = set.add(new Question("first", String.class));
    // check a question with a dependency AND a predicate
    Question second = set.add(new Question("second", String.class).dependsOn(first))
        .askWhen(bs -> predicateMet.get());

    // dependency not satisfied
    assertFalse(second.readyToBeAsked(buildState));

    IncrementalBuildState afterFirst = new NoChange(Answer.strings(first, "foo")).make(buildState).get();

    // dependency met, but predicate false
    assertFalse(second.readyToBeAsked(afterFirst));

    // predicate true but dependency not met
    predicateMet.set(true);
    assertFalse(second.readyToBeAsked(buildState));
    // both predicate and dependency met
    assertTrue(second.readyToBeAsked(afterFirst));
  }

  @Test
  public void testQuestionParameterTypeValidity() throws Exception {
    Question question = new Question("valid?", String.class).optionalMany();

    // question expects string Answers, so this is valid...
    question.checkValidity(Arrays.asList("a", "b", "c"));

    // ...this is not
    Assert.assertThrows(RuntimeException.class, () -> question.checkValidity(Arrays.asList(1L, 2L)));
    Assert.assertThrows(RuntimeException.class, () -> question.checkValidity(Arrays.asList("one", 2L)));
    Assert.assertThrows(RuntimeException.class, () -> question.checkValidity(Arrays.asList(Boolean.TRUE, "two")));
  }

  @Test
  public void testQuestionAnnotations() throws Exception {
    Question question = new Question("life", String.class);

    assertTrue(question.getAnnotations().isEmpty());

    Question annotated = question.parseAnnotations("foo", "bar=baz");
    // double check immutability
    assertFalse(question.hasAnnotation("foo"));

    // even without a value, it's still present
    assertTrue(annotated.hasAnnotation("foo"));
    assertSame(Question.NO_VALUE, annotated.getAnnotation("foo").get());
    // this one has a value
    assertTrue(annotated.hasAnnotation("bar"));
    assertTrue(annotated.hasAnnotationWithValue("bar", "baz"));
    assertFalse(annotated.hasAnnotationWithValue("bar", "foo"));
    assertEquals("baz", annotated.getAnnotation("bar").get());
    // value, not a tag
    assertFalse(annotated.hasAnnotation("baz"));
  }

  @Test
  public void testTitleAndMessageLookup() throws Exception {
    // check that we get empty messages
    Question newStyleI18n = new Question("foo", String.class).inSet(set);
    assertEquals(Optional.empty(), newStyleI18n.getTitle(Locale.CANADA));
    assertEquals(Optional.empty(), newStyleI18n.getDescription(Locale.JAPAN));

    // throw some translations in the message source and assert they come back
    Locale locale = Locale.JAPAN;
    MutableMessageSource messageSource = (MutableMessageSource) set.getSurvey().getMessageSource();
    messageSource.addMessage(Locale.JAPAN, "question.test.foo.title", "cool title");
    messageSource.addMessage(Locale.JAPAN, "question.test.foo.description", "great message");

    assertEquals(Optional.of("cool title"), newStyleI18n.getTitle(locale));
    assertEquals(Optional.of("great message"), newStyleI18n.getDescription(locale));
  }
}
