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

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

import java.util.Arrays;
import java.util.List;
import java.util.Locale;
import java.util.stream.Collectors;

import org.junit.Test;

import com.google.common.collect.Lists;

import nz.org.riskscape.engine.RiskscapeException;
import nz.org.riskscape.engine.problem.GeneralProblems;
import nz.org.riskscape.problem.Problem;
import nz.org.riskscape.problem.Problem.Severity;
import nz.org.riskscape.problem.Problems;
import nz.org.riskscape.problem.StandardCodes;


public class ProblemRendererTest extends TerminalTestHelper {

  private Problem makeProblem(String argName, String value, String mustBe) {
    String displayAs = String.format("argument '%s'", argName);
    return GeneralProblems.get().badValue(value, displayAs, mustBe).withSeverity(Severity.WARNING);
  }

  private Problem groupProblems(List<Problem> children, String displayAs) {
    return new Problem(children, StandardCodes.PROBLEMS_FOUND, displayAs);
  }

  @Test
  public void rendersSingleProblem() {
    Problem p = makeProblem("arg1", "-1", "> 0");

    String expected = joinLines(
        "Value '-1' is invalid for argument 'arg1', it must be > 0",
        ""
    );

    // may as well test some rendering options
    assertThat(messages.renderProblem(p).toString(o -> o.terminateWith = "\n"), is(expected));
  }

  @Test
  public void rendersCompositeProblem() {
    Problem c1 = makeProblem("one", "0", ">= 1");
    Problem c2 = makeProblem("two", "200", "<= 100");
    // group the child problems together
    Problem p = groupProblems(Lists.newArrayList(c1, c2), "argument 'arg1'");

    String expected = joinLines(
        "[cool] Problems found with argument 'arg1'",
        "[cool]   - Value '0' is invalid for argument 'one', it must be >= 1",
        "[cool]   - Value '200' is invalid for argument 'two', it must be <= 100",
        ""
    );
    assertThat(messages.renderProblem(p).toString(o -> {
      o.prefixAll = "[cool] ";
      o.terminateWith = "\n";
    }), is(expected));
  }

  @Test
  public void rendersNestedCompositeProblem() {
    Problem cc1 = makeProblem("one-one", "-5", ">= 1");
    Problem cc2 = makeProblem("one-two", "1024", "<= 100");
    Problem c1 = groupProblems(Lists.newArrayList(cc1, cc2), "child problem 'one'");
    Problem c2 = makeProblem("two", "bar", "foo");
    Problem p = groupProblems(Lists.newArrayList(c1, c2), "argument 'arg1'");

    String expected = joinLines(
        "Problems found with argument 'arg1'",
        "  - Problems found with child problem 'one'",
        "    - Value '-5' is invalid for argument 'one-one', it must be >= 1",
        "    - Value '1024' is invalid for argument 'one-two', it must be <= 100",
        "  - Value 'bar' is invalid for argument 'two', it must be foo"
    );
    assertThat(render(p), is(expected));
  }

  @Test
  public void canOutputMessageForNestedProblem() {
    Problem problem = Problem.composite(Lists.newArrayList(
        Problem.warning("just a warning"),
        Problem.error("just an error")
    ),
        "problem is: %s", "mock-problem");
    String expected = joinLines(
        "problem is: mock-problem",
        "  - just a warning",
        "  - just an error"
    );
    assertThat(render(problem), is(expected));
  }

  @Test
  public void canOutputMessageForDeeplyNestedProblem() {
    Problem problem = Problem.composite(Lists.newArrayList(
        Problem.composite(Lists.newArrayList(Problem.warning("with a child")), "just a warning"),
        Problem.error("just an error")
    ),
        "problem is: %s", "mock-problem");
    String expected = joinLines(
        "problem is: mock-problem",
        "  - just a warning",
        "    - with a child",
        "  - just an error"
    );
    assertThat(render(problem), is(expected));
  }

  @Test
  public void aCaughtExceptionRendersTheExceptionToString() throws Exception {
    Problem noMessage = Problems.caught(new NullPointerException());
    Problem withAMessage = Problems.caught(new RuntimeException("did you get the message?"));
    Problem withNoProblem = Problems.caught(new RiskscapeException("I can't see any problem?"));
    Problem withAProblem = Problems.caught(new RiskscapeException(Problem.error("default message")));

    assertThat(render(noMessage), equalTo("java.lang.NullPointerException"));
    assertThat(render(withAMessage), equalTo("java.lang.RuntimeException: did you get the message?"));

    // riskscape exceptions can accept a problem, or just a message - in the case of a message, the user see's the
    // standard exception error message - this is an 'internal' error that has been surfaced to the user
    assertThat(
        render(withNoProblem),
        equalTo("I can't see any problem?")
    );
    // but if a problem is attached to the exception, the message won't include any internal details, as it's assumed
    // the problem describes any information the user needs to rectify the issue
    assertThat(
        render(withAProblem),
        equalTo("default message")
    );
  }

  @Test
  public void canUseGenericRendererForNestedProblem() {
    Problem cc1 = makeProblem("one-one", "-5", ">= 1");
    Problem c1 = groupProblems(Lists.newArrayList(cc1), "child problem 'one'");
    Problem c2 = makeProblem("two", "bar", "foo");
    Problem p = groupProblems(Lists.newArrayList(c1, c2), "argument 'arg1'");

    // CliProblemRenderer is specific to the cli package, whereas the more
    // generic parent ProblemRenderer can be used in any ProjectTest

    String expected = joinLines(
        "Problems found with argument 'arg1'",
        "  Problems found with child problem 'one'",
        "    Value '-5' is invalid for argument 'one-one', it must be >= 1",
        "  Value 'bar' is invalid for argument 'two', it must be foo"
    );
    assertThat(messages.renderProblem(p, Locale.getDefault()).toString(o -> o.prefixChildren = ""), is(expected));
   }

  public enum MyFavouriteVariables {
    FOO, BAR, BAZ
  }

  @Test
  public void canConstructAndRenderProblemFromAnEnum() {
    // when user doesn't supply correct enum value, we should list the options
    Problem p = GeneralProblems.notAnOption("qux", MyFavouriteVariables.class);
    assertThat(render(p), containsString("'qux' is not a valid option for my favourite variables."));
    assertThat(render(p), containsString(" Available options are: [FOO, BAR, BAZ]"));
  }

  private String joinLines(String... lines) {
    return Arrays.asList(lines).stream().collect(Collectors.joining(String.format("%n")));
  }

}
