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

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.Mockito.*;

import java.util.ArrayList;
import java.util.List;
import java.util.stream.Collectors;

import org.junit.Ignore;
import org.junit.Test;
import org.mockito.stubbing.OngoingStubbing;

import com.google.common.collect.Lists;

import nz.org.riskscape.engine.types.Struct;
import nz.org.riskscape.engine.types.Types;
import nz.org.riskscape.engine.types.WithinRange;
import nz.org.riskscape.engine.types.WithinSet;
import nz.org.riskscape.rl.ast.Expression;
import nz.org.riskscape.wizard.CliChoice;
import nz.org.riskscape.wizard.ExpressionHelper;
import nz.org.riskscape.wizard.ExpressionHelper.AttributeMetadata;
import nz.org.riskscape.wizard.ask.BooleanExpressionAsker.Comparison;
import nz.org.riskscape.wizard.survey2.Choices;

public class BooleanExpressionAskerTest extends BaseAskerTest<BooleanExpressionAsker> {

  @Override
  protected BooleanExpressionAsker createSubject() {
    return new BooleanExpressionAsker(translationContext, 2);
  }

  /**
   * Every time the user is presented with choices in {@link #primeChosenValueAnswer(java.lang.String...) }
   * those choices are added to this list for later verification.
   */
  List<List<String>> presentedValueChoices = new ArrayList<>();

  List<String> presentedAttributeChoices = new ArrayList<>();

  public BooleanExpressionAskerTest() {
    question = question.withType(Expression.class)
        .withAnnotations(
            ExpressionHelper.TAG_EXPRESSION_TYPE, ExpressionHelper.TAG_EXPRESSION_TYPE_BOOLEAN
        );

    setInputType(Struct.of("name", Types.TEXT,
        "value", new WithinRange(Types.INTEGER, 1, 10),
        "quality", new WithinSet(Types.TEXT, "poor", "okay", "good"))
        .add("geom", Types.GEOMETRY));
  }

  @Test
  public void canAskWhenMainTagIsPresent() {
    assertTrue(subject.canAsk(buildState, question));
  }

  @Test
  public void byAttributeSingleText() {
    primeAttributeAnswer("name");

    primeComparisonAnswer(Comparison.EQUAL, Lists.newArrayList(Comparison.EQUAL, Comparison.NOT_EQUAL));
    primeAnotherAnswer(false);
    primeValueAnswer("foo-bar");

    assertThat(ask().getBoundValue(),
        is(parser.parse("name = 'foo-bar'")));

    assertThat(presentedAttributeChoices, contains(
        "name", "value", "quality"
    ));
  }

  @Test
  public void byAttributeSingleTextWithEscapedValue() {
    primeAttributeAnswer("name");

    primeComparisonAnswer(Comparison.EQUAL, Lists.newArrayList(Comparison.EQUAL, Comparison.NOT_EQUAL));
    primeAnotherAnswer(false);
    primeValueAnswer("Donny don't!");

    assertThat(ask().getBoundValue(),
        is(parser.parse("name = 'Donny don\\'t!'")));
  }

  @Test
  public void byAttributeInteger() {
    primeAttributeAnswer("value");

    primeComparisonAnswer(Comparison.GREATER_THAN, Lists.newArrayList(Comparison.values()));
    // you can only provide a single value for number types. so we shouldn't get asks to provide another.
    nextAnswerNotAsked();
    primeValueAnswer("6");

    assertThat(ask().getBoundValue(),
        is(parser.parse("value > 6")));
  }

  @Test
  public void byAttributeTextEqualMany() {
    primeAttributeAnswer("name");

    primeComparisonAnswer(Comparison.EQUAL, Lists.newArrayList(Comparison.EQUAL, Comparison.NOT_EQUAL));
    primeAnotherAnswer(true, false);
    primeValueAnswer("foo", "bar");

    assertThat(ask().getBoundValue(),
        is(parser.parse("name = 'foo' or name = 'bar'")));
  }

  @Test
  public void byAttributeTextNotEqualMany() {
    primeAttributeAnswer("name");

    primeComparisonAnswer(Comparison.NOT_EQUAL, Lists.newArrayList(Comparison.EQUAL, Comparison.NOT_EQUAL));
    primeAnotherAnswer(true, true, false);
    primeValueAnswer("foo", "bar", "baz");

    assertThat(ask().getBoundValue(), is(parser.parse("name != 'foo' and name != 'bar' and name != 'baz'")));
  }

  @Test
  public void byAttributeSelectFromPagedValues() {
    primeAttributeAnswer("quality");

    primeComparisonAnswer(Comparison.NOT_EQUAL, Lists.newArrayList(Comparison.EQUAL, Comparison.NOT_EQUAL));

    primeValueEntryMode(BooleanExpressionAsker.ValueEntryMode.PAGED_LIST, 3);
    primeAnotherAnswer(true, false);

    primeChosenValueAnswer("NEXT PAGE", "poor", "PREV PAGE", "good");

    assertThat(ask().getBoundValue(), is(parser.parse("quality != 'poor' and quality != 'good'")));

    assertThat(presentedValueChoices, contains(
        contains("good", "okay", "NEXT PAGE"),  // first page shown, we choose 'next'
        contains("poor", "PREV PAGE"),          // second page shown, we choose 'poor'
        contains("poor", "PREV PAGE"),          // second page redrawn, we choose 'prev'
        contains("good", "okay", "NEXT PAGE")   // first page, we choose 'good' then done
    ));

  }

  @Test
  public void byAttributeTooManyValuesEnterValue() {
    primeAttributeAnswer("quality");

    primeComparisonAnswer(Comparison.NOT_EQUAL, Lists.newArrayList(Comparison.EQUAL, Comparison.NOT_EQUAL));

    // user chooses to enter value, rather than page through choice list
    primeValueEntryMode(BooleanExpressionAsker.ValueEntryMode.ENTER_VALUE, 3);
    primeValueAnswer("okay", "poor");
    primeAnotherAnswer(true, false);

    assertThat(ask().getBoundValue(), is(parser.parse("quality != 'okay' and quality != 'poor'")));
  }

  // TODO GL1268: BooleanExpressionAsker should be querying Question.getChoices()
  // but this combination of boolean expression and bespoke scope isn't actually used in survey2
  @Test  @Ignore
  public void byAttributeSingleTextFromExpressionScope() {
    // if the expression scope is set then it is used in preference to the inputType
    question = question.withChoices(ibs ->
      Choices.forScope(Struct.of("foo", Types.TEXT, "bar", Types.INTEGER), question)
    );
    primeAttributeAnswer("foo");

    primeComparisonAnswer(Comparison.EQUAL, Lists.newArrayList(Comparison.EQUAL, Comparison.NOT_EQUAL));
    primeAnotherAnswer(false);
    primeValueAnswer("foo-bar");

    assertThat(ask().getBoundValue(),
        is(parser.parse("foo = 'foo-bar'")));

    assertThat(presentedAttributeChoices, contains(
        "foo", "bar"
    ));
  }

  private void primeAttributeAnswer(String path) {
    when(prompter.choose(eq("choose attribute"), anyList()))
        .thenAnswer(invocation -> {
      List<CliChoice<AttributeMetadata>> choices = invocation.getArgument(1);
      presentedAttributeChoices = choices.stream()
          .map(c -> c.data.getFullyQualifiedName())
          .collect(Collectors.toList());

      return choices.stream()
          .filter(choice -> choice.label.equals(path))
          .findFirst()
          .orElseThrow(() -> new RuntimeException("choice not found"));
    });
  }


  private <T> CliChoice<T> getChoice(List<CliChoice<T>> choices, T selected) {
    return choices.stream()
        .filter(choice -> choice.data.equals(selected))
        .findFirst()
        .orElseThrow(() -> new RuntimeException("choice not found"));
  }

  private void primeComparisonAnswer(Comparison comparison, List<Comparison> expectedChoices) {
    when(prompter.choose(eq("choose comparison"), anyList())).thenAnswer(invocation -> {
      List<CliChoice<Comparison>> choices = invocation.getArgument(1);
      assertThat(choices.stream().map(choice -> choice.data).collect(Collectors.toList()),
          is(expectedChoices));
      return getChoice(choices, comparison);
    });
  }

  private void primeAnotherAnswer(Boolean... booleanAnswers) {
    OngoingStubbing<CliChoice> stub
        = when(prompter.choose(eq("add another value"), anyList()));
    for (Boolean answer : booleanAnswers) {
      stub = stub.thenAnswer(invocation -> {
        return getChoice(invocation.getArgument(1), answer);
      });
    }
  }

  private void primeValueAnswer(String answer, String... valueAnswers) {
    when(prompter.readlineWithTitle("enter value")).thenReturn(answer, valueAnswers);
  }

  private void primeChosenValueAnswer(String... valueAnswers) {
    OngoingStubbing<CliChoice> stub = when(prompter.choose(eq("choose value"), anyList()));
    for (String answer : valueAnswers) {
      stub = stub.thenAnswer(invocation -> {
        List<CliChoice<String>> choices = invocation.getArgument(1);

        presentedValueChoices.add(choices.stream()
            .map(c -> c.data)
            .collect(Collectors.toList())
        );

        return choices.stream()
            .filter(choice -> choice.data.equals(answer))
            .findFirst()
            .orElseThrow(() -> new RuntimeException("choice not found"));

      });
    }
  }

  private void primeValueEntryMode(BooleanExpressionAsker.ValueEntryMode mode, int expected) {
    when(prompter.choose(
        eq("too many choices"),
        anyList())).thenAnswer(invocation -> {
          return getChoice(invocation.getArgument(1), mode);
        });
  }

  private void nextAnswerNotAsked() {
    verify(prompter, never()).readlineWithTitle("add another value");
  }

}
