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

import static nz.org.riskscape.engine.Matchers.*;
import static nz.org.riskscape.problem.Problem.Severity.*;
import static org.hamcrest.Matchers.*;
import static org.junit.Assert.*;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.function.BiFunction;
import java.util.function.Consumer;

import org.junit.Test;

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


public class ResultOrProblemsTest {

  Problem error = Problem.error("Terrible times");
  Problem warn = Problem.warning("Sad times");
  Problem info = Problems.foundWith(new Object()).withSeverity(Problem.Severity.INFO);
  Problem fatal = Problems.foundWith(new Object()).withSeverity(Problem.Severity.FATAL);

  final List<Problem> drainedProblems = new ArrayList<>();
  final Object thing = new Object();

  final Consumer<Problem> problemConsumer = p -> drainedProblems.add(p);
  final BiFunction<Severity, List<Problem>, Problem> parentSupplier = (severity, children)
      -> Problems.foundWith(thing).withSeverity(severity).withChildren(children);

  @Test
  public void getReturnsSetResult() throws Exception {
    assertEquals("foo", ResultOrProblems.of("foo").get());
  }

  @Test
  public void getAssertsIfThereAreWarnings() throws Exception {
    ResultOrProblems<?> resultWithErrors = ResultOrProblems.of("foo", Problem.warning("kind of worked"));

    assertFalse(resultWithErrors.hasErrors());
    assertTrue(resultWithErrors.isPresent());

    assertEquals("foo", resultWithErrors.getWithProblemsIgnored());

    // we throw an assertion here so that tests can detect ignored warnings.
    // In production, the get will work but log a warning/stacktrace
    Assert.assertThrows(AssertionError.class, () -> resultWithErrors.get());
  }

  @Test(expected = ResultComputationException.class)
  public void getFailsIfThereIsAResultButThereAreAlsoErrors() throws Exception {
    ResultOrProblems<?> resultWithErrors = ResultOrProblems.of("foo", Problem.error("went badly"));

    assertTrue(resultWithErrors.hasErrors());
    assertTrue(resultWithErrors.isPresent());

    resultWithErrors.get();
  }

  @Test
  public void getThrowsExceptionWhenNoResultGiven() throws Exception {
    ResultComputationException ex = Assert.assertThrows(ResultComputationException.class, () ->
      ResultOrProblems.failed(error).get());

    assertEquals(error, ex.getProblem());
  }

  @Test
  public void orElseReturnsOtherObjectWhenResultNotSet() throws Exception {
    assertEquals("foo", ResultOrProblems.failed(error).orElse("foo"));
    // allowed to return null
    assertNull(ResultOrProblems.failed(error).orElse(null));
  }

  @Test
  public void orElseReturnsResultObjectWhenResultSet() throws Exception {
    assertEquals("foo", ResultOrProblems.of("foo").orElse("bar"));
  }

  @Test
  public void orElseGetCallsFunctionWhenResultNotSet() throws Exception {
    assertEquals("foo", ResultOrProblems.failed(error).orElseGet(probs -> {
      assertThat(probs, contains(error));
      return "foo";
    }));

    // allowed to return null
    assertNull(ResultOrProblems.failed(error).orElseGet(probs -> {
      assertThat(probs, contains(error));
      return null;
    }));
  }

  @Test
  public void orElseGetResultObjectWhenResultSet() throws Exception {
    assertEquals("foo", ResultOrProblems.of("foo").orElseGet(probs -> "bar"));
  }


  @Test
  public void mapInvokesFunctionWhenResultSet() throws Exception {
    ResultOrProblems<String> original = ResultOrProblems.of("foo");

    assertEquals(Long.valueOf(1L), original.map((result) -> {
      assertEquals("foo", result);
      return 1L;
    }).get());
  }

  @Test
  public void mapDoesNotInvokeFunctionWhenResultNotSet() throws Exception {
    ResultOrProblems<String> original = ResultOrProblems.failed(error);

    original.map((result) -> {
      fail("Should not have been called");
      return null;
    });
  }

  @Test
  public void flatMapDoesNotInvokeFunctionWhenNoResultSet() throws Exception {
    ResultOrProblems<Object> original = ResultOrProblems.failed(error);

    assertSame(original, original.flatMap((result) -> {
      fail("Should not have been called");
      return null;
    }));
  }

  @Test
  public void flatMapInvokesFunctionWhenResultSet() throws Exception {
    ResultOrProblems<String> original = ResultOrProblems.of("foo");

    assertEquals("bar", original.flatMap((result) -> {
      assertEquals("foo", result);
      return ResultOrProblems.of("bar");
    }).get());
  }

  @Test
  public void flatMapInvokesFunctionWhenResultSetAndConcatenatesProblems() throws Exception {
    ResultOrProblems<String> original = ResultOrProblems.of("foo", warn);

    ResultOrProblems<Integer> mapped = original.flatMap((result) -> {
      assertEquals("foo", result);
      return ResultOrProblems.of(1, error);
    });

    assertEquals(Integer.valueOf(1), mapped.getWithProblemsIgnored());
    assertEquals(Arrays.asList(warn, error), mapped.getProblems());
  }

  @Test
  public void mapFunctionsCanNotReturnNull() throws Exception {
    ResultOrProblems<Object> rop = ResultOrProblems.of(new Object());

    assertThat(Assert.assertThrows(NullPointerException.class, () -> rop.map(o -> null)).getMessage(),
        containsString("mapper must return a result"));

    assertThat(Assert.assertThrows(NullPointerException.class, () -> rop.map(o -> null, p -> p)).getMessage(),
        containsString("mapper must return a result"));
  }

  @Test(expected=IllegalArgumentException.class)
  public void failedWithResultOrArgumentsFailsIfThereAreNoErrors() throws Exception {
    ResultOrProblems.failed(
        ResultOrProblems.of(new Object()),
          ResultOrProblems.of(new Object(), Problem.warning("foo")));
  }

  @Test
  public void failedWithResultOrArgumentsIncludesAllProblems() throws Exception {
    ResultOrProblems<?> failure = ResultOrProblems.failed(
        ResultOrProblems.of(new Object()),
        ResultOrProblems.error("all bad"),
        ResultOrProblems.of(new Object(), Problem.warning("foo")));

    assertThat(failure.getProblems(), contains(
        isProblem(Severity.ERROR, is("all bad")),
        isProblem(Severity.WARNING, is("foo"))
    ));
  }

  @Test
  public void mapOrCombineReturnsMappedResultIfDependenciesAreSuccessful() throws Exception {
    ResultOrProblems<?> success = ResultOrProblems.of(new Object());
    ResultOrProblems<?> warning = ResultOrProblems.of(new Object(), Problem.warning("foo"));

    assertEquals(
        ResultOrProblems.of(1),
        ResultOrProblems.of("1").mapOrCombine(str -> Integer.parseInt(str), success, warning));
  }

  @Test
  public void mapOrCombineReturnsADependencyProblemsIfTheyFailed() throws Exception {
    ResultOrProblems<?> err = ResultOrProblems.error("all bad");
    ResultOrProblems<?> warning = ResultOrProblems.of(new Object(), Problem.warning("foo"));

    assertEquals(
        Arrays.asList(err.getProblems().get(0), warning.getProblems().get(0)),
        ResultOrProblems.of("1").mapOrCombine(str -> Integer.parseInt(str), err, warning).getProblems());
  }

  @Test
  public void mapOrCombineReturnsAllProblemsIfEverythingFailed() throws Exception {
    ResultOrProblems<?> err = ResultOrProblems.error("all bad");
    ResultOrProblems<?> warning = ResultOrProblems.of(new Object(), Problem.warning("foo"));
    ResultOrProblems<String> ugh = ResultOrProblems.error("ugh");

    assertThat(ugh.mapOrCombine(str -> Integer.parseInt(str), err, warning).getProblems(), contains(
        isProblem(Severity.ERROR, is("ugh")),
        isProblem(Severity.ERROR, is("all bad")),
        isProblem(Severity.WARNING, is("foo"))
    ));
  }

  @Test
  public void failedFromExceptionTakesMessageFromException() throws Exception {
    RiskscapeException horribleFailure = new RiskscapeException("this was bad, really bad : %s <- do not blow up");

    ResultOrProblems<?> errored = ResultOrProblems.error(horribleFailure);

    assertTrue(errored.hasErrors());
    assertThat(errored.getProblems(), contains(
        isProblem(Severity.ERROR, containsString("this was bad, really bad : "))
    ));
  }

  @Test
  public void testComposeFailures() throws Exception {
    ResultOrProblems<?> failed = ResultOrProblems.error("uh oh");
    ResultOrProblems<?> composed = failed.composeFailure(Problem.error("it went wrong"));

    assertEquals(1, composed.getProblems().size());
    assertEquals("it went wrong", composed.getProblems().get(0).getMessage());
    assertEquals(failed.getProblems(), composed.getProblems().get(0).getChildren());
  }


  @Test
  public void getOrThrowThrowsProblemExceptionIfNoResult() throws ProblemException {
    ResultOrProblems<?> resultWithErrors = ResultOrProblems.failed(Problem.error("went badly"));

    assertTrue(resultWithErrors.hasErrors());
    assertFalse(resultWithErrors.isPresent());

    ProblemException ex = Assert.assertThrows(ProblemException.class, () -> resultWithErrors.getOrThrow());
    assertEquals(resultWithErrors.getProblems(), ex.getProblems());
  }

  @Test
  public void getOrThrowThrowsProblemWithWrappingIfArgGiven() throws ProblemException {
    ResultOrProblems<?> resultWithErrors = ResultOrProblems.failed(Problem.error("went badly"));

    ProblemException ex = Assert.assertThrows(ProblemException.class, () ->
      resultWithErrors.getOrThrow(Problem.error("nah mang")));

    assertEquals(Arrays.asList(Problem.error("nah mang").withChildren(Problem.error("went badly"))), ex.getProblems());
  }

  @Test
  public void getOrThrowAlsoTakesAFunction() throws ProblemException {
    Problem rootCause = Problem.error("went badly");
    ResultOrProblems<?> resultWithErrors = ResultOrProblems.failed(rootCause);

    ProblemException ex = Assert.assertThrows(ProblemException.class, () ->
      resultWithErrors.getOrThrow(ps -> Problems.foundWith("it", ps)));

    // flip it and reverse it
    assertThat(ex.getProblems(), contains(Problems.foundWith("it", rootCause)));
  }

  @Test
  public void getFailedReturnsFailedResultIfThereIsAResultAndAlsoErrors() throws Exception {
    ResultOrProblems<?> resultWithErrors = ResultOrProblems.of("yay", Problem.error("went badly"));

    assertTrue(resultWithErrors.hasErrors());
    assertTrue(resultWithErrors.isPresent());

    assertEquals("yay", resultWithErrors.getWithProblemsIgnored());
  }

  @Test
  public void drainWarningsReturnsANewResultIfThereWereAnyWarnings() throws Exception {
    List<Problem> list = new ArrayList<>();
    Consumer<Problem> c = p -> list.add(p);

    assertEquals(
        Arrays.asList(),
        ResultOrProblems.of("yay", Problem.warning("went badly")).drainWarnings(c).getProblems()
    );

    assertThat(list, contains(Problem.warning("went badly")));
    list.clear();

    assertEquals(
        Arrays.asList(),
        ResultOrProblems.of("yay", Problem.warning("went badly"), Problem.warning("this and that")).
          drainWarnings(c).getProblems()
    );

    assertThat(list, contains(Problem.warning("went badly"), Problem.warning("this and that")));
  }

  @Test
  public void drainWarningsReturnsSelfIfNone() throws Exception {
    ResultOrProblems<?> failed = ResultOrProblems.of("hi");
    List<Problem> list = new ArrayList<>();
    assertSame(failed, failed.drainWarnings(c -> list.add(c)));
    assertEquals(Collections.EMPTY_LIST, list);

    failed = failed.flatMap(f -> ResultOrProblems.of("there"));
    assertSame(failed, failed.drainWarnings(c -> list.add(c)));
    assertEquals(Collections.EMPTY_LIST, list);
  }

  @Test
  public void drainWarningsReturnsSelfIfFailed() throws Exception {
    ResultOrProblems<?> failed = ResultOrProblems.failed(Problem.error("went badly"), Problem.error("went badly"));
    List<Problem> list = new ArrayList<>();
    assertSame(failed, failed.drainWarnings(c -> list.add(c)));
    assertEquals(Collections.EMPTY_LIST, list);
  }

  @Test
  public void composeProblemsDoesNothingIfNoProblems() throws Exception {
    ResultOrProblems<String> startFrom = ResultOrProblems.of("foo");

    ResultOrProblems<String> composed =
        startFrom.composeProblems(GeneralProblems.get().noSuchObjectExists("bar", String.class));

    assertTrue(composed.isPresent());
    assertEquals("foo", composed.get());
    assertFalse(composed.hasProblems());
  }

  @Test
  public void composeProblemsDoesNothingIfNoProblems_LambdaVersion() throws Exception {
    ResultOrProblems<String> startFrom = ResultOrProblems.of("foo");

    ResultOrProblems<String> composed =
        startFrom.composeProblems((s, p) -> {
          throw new AssertionError("should not have been called");
        });

    assertTrue(composed.isPresent());
    assertEquals("foo", composed.get());
    assertFalse(composed.hasProblems());
  }

  @Test
  public void composeProblemsWillWrapAFailedResultsProblems() throws Exception {
    List<Problem> problems = Arrays.asList(
        GeneralProblems.get().noSuchObjectExists("foo", String.class),
        GeneralProblems.get().noSuchObjectExists("bar", String.class)
    );

    Problem composeWith = GeneralProblems.get().noSuchObjectExists("baz", String.class);

    ResultOrProblems<String> startFrom = ResultOrProblems.failed(problems);

    ResultOrProblems<String> composed = startFrom.composeProblems(composeWith);

    assertFalse(composed.isPresent());
    assertTrue(composed.hasProblems());
    assertEquals(1, composed.getProblems().size());
    assertNotSame(composed.getProblems().get(0), composeWith);
    assertEquals(composed.getProblems().get(0).getChildren(), problems);
  }

  @Test
  public void composeProblemsWillWrapAFailedResultsProblems_LambdaVersion() throws Exception {
    List<Problem> problems = Arrays.asList(
        GeneralProblems.get().noSuchObjectExists("foo", String.class),
        GeneralProblems.get().noSuchObjectExists("bar", String.class)
    );

    Problem composeWith = GeneralProblems.get().noSuchObjectExists("baz", String.class);

    ResultOrProblems<String> startFrom = ResultOrProblems.failed(problems);

    ResultOrProblems<String> composed = startFrom.composeProblems((s, v) -> {
      // mess with the result in some custom way
      assertSame(s, Severity.ERROR);
      return composeWith.withChildren(problems.get(0)).withSeverity(Severity.FATAL);
    });

    assertFalse(composed.isPresent());
    assertTrue(composed.hasProblems());
    assertEquals(1, composed.getProblems().size());
    assertEquals(Severity.FATAL, composed.getAsSingleProblem().severity);
    assertNotSame(composed.getProblems().get(0), composeWith);
    assertEquals(composed.getProblems().get(0).getChildren(), problems.subList(0, 1));
  }

  @Test
  public void composeProblemsWillWrapAResultWithWarningsProblems() throws Exception {
    List<Problem> problems = Arrays.asList(
        GeneralProblems.get().noSuchObjectExists("foo", String.class).withSeverity(Severity.WARNING),
        GeneralProblems.get().noSuchObjectExists("bar", String.class).withSeverity(Severity.INFO)
    );

    Problem composeWith = GeneralProblems.get().noSuchObjectExists("baz", String.class);

    ResultOrProblems<String> startFrom = ResultOrProblems.of("cool", problems);

    ResultOrProblems<String> composed = startFrom.composeProblems(composeWith);

    assertTrue(composed.isPresent());
    assertTrue(composed.hasProblems());
    assertFalse(composed.hasErrors());

    assertEquals(Severity.WARNING, composed.getAsSingleProblem().severity);
  }

  @Test
  public void composeProblemsWillWrapAResultWithWarningsProblems_LambdaVersion() throws Exception {
    List<Problem> problems = Arrays.asList(
        GeneralProblems.get().noSuchObjectExists("foo", String.class).withSeverity(Severity.WARNING),
        GeneralProblems.get().noSuchObjectExists("bar", String.class).withSeverity(Severity.INFO)
    );

    Problem composeWith = GeneralProblems.get().noSuchObjectExists("baz", String.class);

    ResultOrProblems<String> startFrom = ResultOrProblems.of("cool", problems);

    ResultOrProblems<String> composed =
        startFrom.composeProblems((s, p) -> composeWith.withSeverity(Severity.INFO));

    assertTrue(composed.isPresent());
    assertTrue(composed.hasProblems());
    assertFalse(composed.hasErrors());

    assertEquals(Severity.INFO, composed.getAsSingleProblem().severity);
    assertTrue(composed.getAsSingleProblem().getChildren().isEmpty());
  }

  @Test
  public void canDrainProblemsWhereThereAreNone() {
    ResultOrProblems<Object> result = ResultOrProblems.of(thing);

    ResultOrProblems<Object> drained = result.drainWarnings(problemConsumer);
    assertSame(result, drained);
    assertThat(drainedProblems, hasSize(0));

    // should be the same we flat map to something else
    result = result.flatMap(o -> ResultOrProblems.of(o));
    drained = result.drainWarnings(problemConsumer);
    assertSame(result, drained);
    assertThat(drainedProblems, hasSize(0));
  }

  @Test
  public void canDrainInfoAndWarnings() {
    ResultOrProblems<Object> result = ResultOrProblems.of(thing).withMoreProblems(info);

    ResultOrProblems<Object> drained = result.drainWarnings(problemConsumer);
    assertNotSame(result, drained);
    assertThat(drained, is(ResultOrProblems.of(thing)));
    assertThat(drainedProblems, contains(info));

    drainedProblems.clear();
    result = ResultOrProblems.of(thing).withMoreProblems(warn);
    drained = result.drainWarnings(problemConsumer);
    assertNotSame(result, drained);
    assertThat(drained, is(ResultOrProblems.of(thing)));
    assertThat(drainedProblems, contains(warn));

    drainedProblems.clear();
    result = ResultOrProblems.of(thing).withMoreProblems(warn, info);
    drained = result.drainWarnings(problemConsumer);
    assertNotSame(result, drained);
    assertThat(drained, is(ResultOrProblems.of(thing)));
    assertThat(drainedProblems, contains(warn, info));
  }

  @Test
  public void doesNotDrainInfoOrWarningsWhenErrorsExist() {
    ResultOrProblems<Object> result = ResultOrProblems.of(thing).withMoreProblems(info, error);
    ResultOrProblems<Object> drained = result.drainWarnings(problemConsumer);
    assertSame(result, drained);
    assertThat(drainedProblems, hasSize(0));

    result = ResultOrProblems.of(thing).withMoreProblems(warn, error);
    drained = result.drainWarnings(problemConsumer);
    assertSame(result, drained);
    assertThat(drainedProblems, hasSize(0));

    result = ResultOrProblems.of(thing).withMoreProblems(warn, fatal, info);
    drained = result.drainWarnings(problemConsumer);
    assertSame(result, drained);
    assertThat(drainedProblems, hasSize(0));
  }

  @Test
  public void canDrainProblemsWithParentWhereThereAreNone() {
    ResultOrProblems<Object> result = ResultOrProblems.of(thing);

    ResultOrProblems<Object> drained = result.drainWarnings(problemConsumer, parentSupplier);
    assertSame(result, drained);
    assertThat(drainedProblems, hasSize(0));

    // should be the same we flat map to something else
    result = result.flatMap(o -> ResultOrProblems.of(o));
    drained = result.drainWarnings(problemConsumer, parentSupplier);
    assertSame(result, drained);
    assertThat(drainedProblems, hasSize(0));
  }

  @Test
  public void canDrainInfoAndWarningsWithParent() {
    ResultOrProblems<Object> result = ResultOrProblems.of(thing).withMoreProblems(info);

    ResultOrProblems<Object> drained = result.drainWarnings(problemConsumer, parentSupplier);
    assertNotSame(result, drained);
    assertThat(drained, is(ResultOrProblems.of(thing)));
    assertThat(drainedProblems, contains(withParent(INFO, info)));

    drainedProblems.clear();
    result = ResultOrProblems.of(thing).withMoreProblems(warn);
    drained = result.drainWarnings(problemConsumer, parentSupplier);
    assertNotSame(result, drained);
    assertThat(drained, is(ResultOrProblems.of(thing)));
    assertThat(drainedProblems, contains(withParent(WARNING, warn)));

    drainedProblems.clear();
    result = ResultOrProblems.of(thing).withMoreProblems(warn, info);
    drained = result.drainWarnings(problemConsumer, parentSupplier);
    assertNotSame(result, drained);
    assertThat(drained, is(ResultOrProblems.of(thing)));
    assertThat(drainedProblems, contains(withParent(WARNING, warn, info)));
  }

  @Test
  public void doesNotDrainInfoOrWarningsWithParentWhenErrorsExist() {
    ResultOrProblems<Object> result = ResultOrProblems.of(thing).withMoreProblems(info, error);
    ResultOrProblems<Object> drained = result.drainWarnings(problemConsumer, parentSupplier);
    assertSame(result, drained);
    assertThat(drainedProblems, hasSize(0));

    result = ResultOrProblems.of(thing).withMoreProblems(warn, error);
    drained = result.drainWarnings(problemConsumer, parentSupplier);
    assertSame(result, drained);
    assertThat(drainedProblems, hasSize(0));

    result = ResultOrProblems.of(thing).withMoreProblems(warn, fatal, info);
    drained = result.drainWarnings(problemConsumer, parentSupplier);
    assertSame(result, drained);
    assertThat(drainedProblems, hasSize(0));
  }

  @Test
  public void canAppendAdditionalProblemsEasily() {
    ResultOrProblems<Integer> base = ResultOrProblems.of(0, warn);
    ResultOrProblems<Integer> validated = base
        .withMoreProblems(x -> x < 0 ? Arrays.asList(error) : Collections.emptyList());
    ResultOrProblems<Integer> validatedWithError = base
        .withMoreProblems(x -> x <= 0 ? Arrays.asList(error) : Collections.emptyList());

    assertEquals(base, validated);
    assertNotEquals(base, validatedWithError);
    assertThat(validatedWithError.getProblems(), contains(warn, error));
  }

  @Test
  public void toStringSummarizesProblems() throws Exception {
    assertThat(
        ResultOrProblems.of("foo", List.of(Problems.foundWith("bar"))).toString(),
        equalTo("Of(foo, 1 problems)")
    );

    // doesn't show 0 problems when none
    assertThat(
        ResultOrProblems.of("foo", List.of()).toString(),
        equalTo("Of(foo)")
    );
  }

  private Problem withParent(Severity severity, Problem... children) {
    return Problems.foundWith(thing).withSeverity(severity).withChildren(children);
  }

}
