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


import static org.hamcrest.Matchers.*;

import java.lang.reflect.Method;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.Objects;
import java.util.Optional;
import java.util.function.Function;
import java.util.stream.Collectors;

import org.hamcrest.BaseMatcher;
import org.hamcrest.Description;
import org.hamcrest.DiagnosingMatcher;
import org.hamcrest.Matcher;
import org.hamcrest.TypeSafeDiagnosingMatcher;
import org.hamcrest.TypeSafeMatcher;
import org.hamcrest.core.IsAnything;
import org.hamcrest.core.IsSame;
import org.locationtech.jts.geom.Geometry;

import com.google.common.collect.Lists;
import com.google.common.collect.Streams;

import nz.org.riskscape.dsl.Token;
import nz.org.riskscape.engine.bind.Parameter;
import nz.org.riskscape.engine.i18n.MessageKey;
import nz.org.riskscape.engine.pipeline.RealizedStep;
import nz.org.riskscape.engine.problem.ProblemFactory;
import nz.org.riskscape.engine.problem.ProblemFactoryProxy.JavaMethodCode;
import nz.org.riskscape.engine.projection.FlatProjector;
import nz.org.riskscape.engine.projection.Projector;
import nz.org.riskscape.engine.relation.Relation;
import nz.org.riskscape.engine.relation.TupleIterator;
import nz.org.riskscape.engine.types.Struct;
import nz.org.riskscape.engine.util.Pair;
import nz.org.riskscape.problem.Problem;
import nz.org.riskscape.problem.Problem.Severity;
import nz.org.riskscape.problem.ProblemCode;
import nz.org.riskscape.problem.ResultOrProblems;
import nz.org.riskscape.rl.ast.Expression;

@SuppressWarnings("unchecked")
// TODO move Problem matchers to their own type
public class Matchers {

  /**
   *
   * @deprecated use {@link GeometryMatchers} instead
   */
  @Deprecated
  public static Matcher<Geometry> isGeometry(Geometry toMatch) {
    return GeometryMatchers.isGeometry(toMatch);
  }

  public static <T, U> Matcher<Pair<T, U>> isPair(Matcher<? extends T> leftMatcher, Matcher<? extends U> rightMatcher) {
    return new TypeSafeMatcher<Pair<T, U>>() {
      @Override
      protected boolean matchesSafely(Pair<T, U> t) {
        return leftMatcher.matches(t.getLeft()) && rightMatcher.matches(t.getRight());
      }

      @Override
      public void describeTo(Description d) {
        d.appendText("Pair of left: ");
        leftMatcher.describeTo(d);
        d.appendText(" and right: ");
        rightMatcher.describeTo(d);
      }
    };
  }

  /**
   * Build a matcher that ensures the given object of is an instance of a particular type, and iff it is, apply a
   * second, type-specific matcher to it.  Useful for tests where you have a generic object and want to apply a type
   * specific matcher to it.  The alternative to this is to remove the generics instead, which gives
   * rise to a ton of compiler warnings.
   */
  public static <T, U> Matcher<U> instanceOfAnd(Class<T> t, Matcher<? extends T> matcher) {
    return new DiagnosingMatcher<U>() {

      @Override
      public void describeTo(Description description) {
        description.appendText("Object of type ").appendValue(t).appendText(" and matching ");
        matcher.describeTo(description);
      }

      @Override
      protected boolean matches(Object item, Description mismatchDescription) {

          if (item != null && !t.isInstance(item)) {
            mismatchDescription.appendText("item is not an instance of " + t + ", is a " + item.getClass());

            return false;
          }

        if (matcher.matches(item)) {
          return true;
        } else {
          matcher.describeMismatch(item, mismatchDescription);
          return false;
        }
      }

    };
  }

  /**
   * Build a matcher that ensures the given collection of objects are all of a particular type, and iff they are is,
   * apply a second, type-specific collection matcher to it.  Useful for tests where you have a generic collection and
   * want to apply a type specific collection matcher to it.  The alternative to this is to remove the generics instead,
   * which gives rise to a ton of compiler warnings.
   */
  public static <T, U> Matcher<Iterable<U>> isCollectionOf(Class<T> t, Matcher<Iterable<? extends T>> matcher) {
    return new DiagnosingMatcher<Iterable<U>>() {

      @Override
      public void describeTo(Description description) {
        description.appendText("Collection with elements of type ").appendValue(t).appendText(" and matching ");
        matcher.describeTo(description);
      }

      @Override
      protected boolean matches(Object item, Description mismatchDescription) {
        Iterable<?> items = (Iterable<?>) item;

        int index = 0;
        for (Object collectionItem : items) {
          if (collectionItem != null && !t.isInstance(collectionItem)) {
            mismatchDescription.appendText("item " + index + " is not an instance of " + t + ", is a "
                + collectionItem.getClass());
            return false;
          }
          index++;
        }

        if (matcher.matches(items)) {
          return true;
        } else {
          matcher.describeMismatch(items, mismatchDescription);
          return false;
        }
      }

    };
  }

  public static Matcher<Problem> isError(ProblemCode code) {
    return isProblem(Severity.ERROR, code);
  }

  public static Matcher<Problem> isError(String codeName) {
    return isProblem(Severity.ERROR, codeName);
  }

  public static Matcher<Problem> equalIgnoringChildren(Problem match) {
    Problem lhs = match.setChildren(Collections.emptyList());
    return new TypeSafeDiagnosingMatcher<Problem>(Problem.class) {

      @Override
      public void describeTo(Description description) {
        description.appendValue(lhs);
      }

      @Override
      protected boolean matchesSafely(Problem item, Description mismatchDescription) {
        Problem rhs = item.setChildren(Collections.emptyList());

        mismatchDescription.appendText("equals (ignoring children)").appendValue(rhs);

        return lhs.equals(rhs);
      }
    };
  }
  public static Matcher<Problem> hasAncestorProblem(Problem ancestor) {
    return hasAncestorProblem(equalTo(ancestor));
  }
  /**
   * Check that the given matcher occurs somewhere in a matched problem's children, children's children etc, or itself.
   * Useful for checking that a particular problem is in the tree of problems *somewhere*.
   *
   * Example: `assertThat(someProblem, hasAncestorProblem(isError(StandardCodes.BLEUGH)))`
   *
   * @param ancestor a matcher to use when looking through ancestors for a match
   *
   */
  public static Matcher<Problem> hasAncestorProblem(Matcher<Problem> ancestor) {
    return new TypeSafeDiagnosingMatcher<Problem>(Problem.class) {

      @Override
      public void describeTo(Description description) {
        description.appendDescriptionOf(ancestor);
        description.appendText("among ancestors");

      }

      @Override
      protected boolean matchesSafely(Problem item, Description mismatchDescription) {
        List<Problem> visitStack = Lists.newArrayList();
        List<Problem> seen = Lists.newArrayList();
        visitStack.add(item);

        while (!visitStack.isEmpty()) {
          Problem visit = visitStack.remove(0);
          seen.add(visit);
          if (ancestor.matches(visit)) {
            return true;
          } else {
            visitStack.addAll(visit.getChildren());
          }
        }

        mismatchDescription.appendText("Could not find");
        mismatchDescription.appendDescriptionOf(ancestor);
        mismatchDescription.appendText("among");
        mismatchDescription.appendValueList("[", "\n", "]", seen);


        return false;
      }
    };
  }

  @SafeVarargs
  public static Matcher<Problem> hasProblems(Matcher<Problem>... matchers) {
    Matcher<Iterable<? extends Problem>> childMatcher = org.hamcrest.Matchers.contains(matchers);
    return new TypeSafeMatcher<Problem>(Problem.class) {

      @Override
      public void describeTo(Description description) {
        description.appendText("Problem with children ").appendDescriptionOf(childMatcher);
      }

      @Override
      protected boolean matchesSafely(Problem item) {
        return childMatcher.matches(item.getChildren());
      }

      @Override
      protected void describeMismatchSafely(Problem item, Description mismatchDescription) {
        mismatchDescription.appendText("Problem with children ");
        childMatcher.describeMismatch(item.getChildren(), mismatchDescription);
      }

    };
  }

  public static Matcher<Problem> isProblemAffecting(Severity severity, Class<?> affects) {
    return new TypeSafeMatcher<Problem>(Problem.class) {

      @Override
      public void describeTo(Description description) {
        description.appendText("Problem with severity")
          .appendValue(severity)
          .appendText(" and context class ")
          .appendValue(affects);
      }

      @Override
      protected void describeMismatchSafely(Problem item, Description mismatch) {
        mismatch.appendText("Problem with severity")
        .appendValue(item.getSeverity())
        .appendText(" and affecting ")
        .appendValue(item.getAffectedClass());
      }

      @Override
      protected boolean matchesSafely(Problem item) {
        return item.getSeverity() == severity
            && item.affects(affects);
      }

    };
  }

  public static Matcher<Problem> isProblemAffectingLine(int line, String statement) {
    return new TypeSafeMatcher<Problem>(Problem.class) {

      @Override
      public void describeTo(Description description) {
        description.appendText("Problem on line ").appendValue(line)
          .appendText(" for statement").appendValue(statement);
      }

      @Override
      protected void describeMismatchSafely(Problem item, Description mismatch) {
        mismatch.appendText("Problem affects ").appendValue(item.getAffectedClass())
          .appendText(", ").appendValue(item.getAffectedObject());
      }

      @Override
      protected boolean matchesSafely(Problem item) {
        Token token = item.getAffected(Token.class).orElse(null);
        if (token != null) {
          return token.getLocation().getLine() == line && token.getValue().equals(statement);
        }
        return false;
      }
    };
  }

  private static Method getMethodByName(Class<?> clazz, String methodName) {
    // we don't know the exact method args, so just string-match by name
    for (Method method : clazz.getMethods()) {
      if (method.getName().equals(methodName)) {
        return method;
      }
    }
    throw new RuntimeException(String.format("Class %s doesn't have method %s", clazz.getName(), methodName));
  }

  /**
   * Checks a given Problem was created by a ProblemFactory API
   */
  public static Matcher<Problem> isProblem(Severity severity, Class<? extends ProblemFactory> factory, String apiName) {
    Method method = getMethodByName(factory, apiName);
    ProblemCode code = new JavaMethodCode(method);
    return isProblem(severity, code);
  }

  /**
   * Checks a given Problem was created by a ProblemFactory API
   */
  public static Matcher<Problem> isError(Class<? extends ProblemFactory> factory, String apiName) {
    return isProblem(Severity.ERROR, factory, apiName);
  }

  public static Matcher<Problem> isProblem(Severity severity, ProblemCode code) {
    return isProblem(severity, code, anything());
  }

  public static Matcher<Problem> isProblem(Severity severity, String codeName) {
    ProblemCode code = () -> codeName;
    return isProblem(severity, code, anything());
  }

  public static Matcher<Problem> isProblem(Severity severity, ProblemCode code, Matcher<?> childMatchers) {
    return new TypeSafeMatcher<Problem>(Problem.class) {

      @Override
      public void describeTo(Description description) {
        description.appendText("Problem with severity")
          .appendValue(severity)
          .appendText(" and code ")
          .appendValue(code.toKey());
      }

      @Override
      protected void describeMismatchSafely(Problem item, Description mismatch) {
        if (item.getSeverity() != severity) {
          mismatch.appendText("Wrong severity: ")
              .appendValue(item.getSeverity());
        }
        if (!item.getCode().toKey().equals(code.toKey())) {
          mismatch.appendText(" Wrong code: ")
              .appendValue(item.getCode().toKey());
        }
        if (!childMatchers.matches(item.getChildren())) {
          mismatch.appendText(" Wrong children: ");
          childMatchers.describeMismatch(item.getChildren(), mismatch);
        }

      }

      @Override
      protected boolean matchesSafely(Problem item) {
        return item.getSeverity() == severity
            && item.getCode().toKey().equals(code.toKey())
            && childMatchers.matches(item.getChildren());
      }
    };
  }

  public static Matcher<Problem> isProblem(Severity severity, Matcher<String> messageMatcher) {
    return isProblem(
        new IsSame<Severity>(severity),
        messageMatcher,
        IsAnything.anything(),
        p -> getMessage(p));
  }

  public static Matcher<Problem> isProblem(
      Severity severity,
      Matcher<String> messageMatcher,
      Matcher<?> subMatchers
  ) {
    return isProblem(
        new IsSame<Severity>(severity),
        messageMatcher,
        subMatchers,
        p -> getMessage(p));
  }

  /**
   * Extracts the message text from a Problem. This is the deprecated/old approach
   * where we used a hard-coded, non-translatable string.
   */
  @Deprecated
  private static String getMessage(Problem item) {
    return item.getMessage();
  }

  public static Matcher<Problem> isProblem(
      Matcher<Severity> severity,
      Matcher<String> messageMatcher,
      Matcher<?> subMatchers,
      Function<Problem, String> renderMsg
  ) {
    return new TypeSafeMatcher<Problem>() {

      @Override
      public void describeTo(Description description) {
        description
            .appendText("Problem [")
            .appendDescriptionOf(severity)
            .appendDescriptionOf(messageMatcher)
            .appendDescriptionOf(subMatchers)
            .appendText("]");
      }

      @Override
      protected boolean matchesSafely(Problem item) {
        return severity.matches(item.getSeverity())
            && messageMatcher.matches(renderMsg.apply(item))
            && subMatchers.matches(item.getChildren());
      }

      @Override
      protected void describeMismatchSafely(Problem item, Description mismatchDescription) {
        if (!severity.matches(item.getSeverity())) {
          mismatchDescription.appendText("wrong severity ").appendValue(item.getSeverity());
        } else if (!messageMatcher.matches(renderMsg.apply(item))) {
          mismatchDescription.appendText("wrong message: ");
          messageMatcher.describeMismatch(renderMsg.apply(item), mismatchDescription);
        } else {
          mismatchDescription.appendText("wrong children: ");
          subMatchers.describeMismatch(item.getChildren(), mismatchDescription);
        }
      }
    };
  }

  public static Matcher<Problem> isProblem(Class<? extends Throwable> expectedError) {
    return new TypeSafeMatcher<Problem>() {

      @Override
      public void describeTo(Description description) {
        description.appendText("Problem with error of type " + expectedError);
      }

      @Override
      protected boolean matchesSafely(Problem item) {
        return expectedError.isInstance(item.getException());
      }
    };
  }

  public static Matcher<MessageKey> isMessage(Matcher<String> codeMatcher, Matcher<String> defaultMessageMatcher) {
    return isMessage(codeMatcher, defaultMessageMatcher, is(emptyArray()));
  }

  public static Matcher<MessageKey> isMessage(
      Matcher<String> codeMatcher,
      Matcher<String> defaultMessageMatcher,
      Matcher<Object[]> argumentsMatcher) {
    return new TypeSafeMatcher<MessageKey>() {

      @Override
      public void describeTo(Description description) {
        description
            .appendText("MessageKey [")
            .appendDescriptionOf(codeMatcher)
            .appendDescriptionOf(defaultMessageMatcher)
            .appendDescriptionOf(argumentsMatcher)
            .appendText("]");
      }

      @Override
      protected boolean matchesSafely(MessageKey item) {
        return codeMatcher.matches(item.getCode())
            && defaultMessageMatcher.matches(item.getDefaultMessage())
            && argumentsMatcher.matches(item.getMessageArguments());
      }

      @Override
      protected void describeMismatchSafely(MessageKey item, Description mismatchDescription) {
        if (!codeMatcher.matches(item.getCode())) {
          mismatchDescription.appendText("wrong code ").appendValue(item.getCode());
        } else if (!defaultMessageMatcher.matches(item.getDefaultMessage())) {
          mismatchDescription.appendText("wrong default message: ");
          defaultMessageMatcher.describeMismatch(item.getDefaultMessage(), mismatchDescription);
        } else {
          mismatchDescription.appendText("wrong arguments: ");
          argumentsMatcher.describeMismatch(item.getMessageArguments(), mismatchDescription);
        }
      }
    };
  }

  public static Matcher<Parameter> isRequiredParameter(Matcher<?> nameMatcher,
      Matcher<Class<?>> typeMatcher) {
    return isParameter(nameMatcher, typeMatcher, is(1), is(1));
  }

  public static Matcher<Parameter> isParameter(Matcher<?> nameMatcher,
      Matcher<Class<?>> typeMatcher, Matcher<?> minMatcher, Matcher<?> maxMatcher) {
    return new TypeSafeMatcher<Parameter>() {
      @Override
      protected boolean matchesSafely(Parameter t) {
        return nameMatcher.matches(t.getName())
            && typeMatcher.matches(t.getType())
            && minMatcher.matches(t.getMinRequired())
            && maxMatcher.matches(t.getMaxRequired());
      }

      @Override
      public void describeTo(Description d) {
        d.appendText("Parameter [");
        d.appendDescriptionOf(nameMatcher);
        d.appendDescriptionOf(typeMatcher);
        d.appendDescriptionOf(minMatcher);
        d.appendDescriptionOf(maxMatcher);
        d.appendText("]");
      }

      @Override
      protected void describeMismatchSafely(Parameter item, Description mismatchDescription) {
        if (!nameMatcher.matches(item.getName())) {
          mismatchDescription.appendText("wrong name ").appendValue(item.getName());
        } else if (!typeMatcher.matches(item.getType())) {
          mismatchDescription.appendText("wrong type ").appendValue(item.getType());
        } else if (!minMatcher.matches(item.getMinRequired())) {
          mismatchDescription.appendText("wrong min ").appendValue(item.getMinRequired());
        } else if (!maxMatcher.matches(item.getMaxRequired())) {
          mismatchDescription.appendText("wrong max ").appendValue(item.getMaxRequired());
        }
      }

    };
  }

  /**
   * A matcher that checks the a result is present and is equal to the given object
   */
  public static <T> Matcher<ResultOrProblems<? extends T>> result(T object) {
    return result(org.hamcrest.core.Is.is(object));
  }

  public static Matcher<ResultOrProblems<?>> failedResult(Matcher<Problem> first, Matcher<Problem>... matchers) {

    List<Matcher<Problem>> allMatchers = Lists.newArrayList();
    allMatchers.add(first);
    allMatchers.addAll(Arrays.asList(matchers));

    Matcher<Iterable<Problem>> containsResults = hasItems(allMatchers.toArray(new Matcher[0]));

    return new TypeSafeMatcher<ResultOrProblems<?>>() {

      @Override
      public void describeTo(Description description) {
        description.appendText("Failed result with problems ").appendDescriptionOf(containsResults);
      }

      @Override
      protected void describeMismatchSafely(ResultOrProblems<?> item, Description mismatchDescription) {
        if (item.isPresent()) {
          mismatchDescription.appendText("was not failed - ").appendValue(Problem.debugString(item.getProblems()));
        } else {
          containsResults.describeMismatch(item.getProblems(), mismatchDescription);
        }
      }

      @Override
      protected boolean matchesSafely(ResultOrProblems<?> item) {
        if (item.isPresent()) {
          return false;
        } else {
          return containsResults.matches(item.getProblems());
        }
      }
    };
  }

  public static <T, X> Matcher<ResultOrProblems<T>> result(Class<X> expectedType, Matcher<X> matcher) {
    return new TypeSafeMatcher<ResultOrProblems<T>>(ResultOrProblems.class) {
      @Override
      public void describeTo(Description description) {
        description
            .appendValue(expectedType)
            .appendText("Of(")
            .appendDescriptionOf(matcher)
            .appendText(")");

      }

      @Override
      protected void describeMismatchSafely(ResultOrProblems<T> item, Description mismatchDescription) {
        if (item.isPresent()) {
          T result = item.get();
          if (expectedType.isInstance(result)) {
            matcher.describeMismatch(result, mismatchDescription);
          } else {
            mismatchDescription.appendText("Result of type ").appendValue(result.getClass());
          }
        } else {
          mismatchDescription.appendText("Failed result with problems ").appendValue(item.getProblems());
        }
      }

      @Override
      protected boolean matchesSafely(ResultOrProblems<T> item) {
        return item.isPresent() && matcher.matches(item.get());
      }
    };
  }

  /**
   * A matcher that checks a ResultOrProblems has a result and contains a value that matches the given matcher
   */
  public static <T> Matcher<ResultOrProblems<? extends T>> result(Matcher<T> matcher) {

    return new TypeSafeMatcher<ResultOrProblems<? extends T>>() {

      @Override
      public void describeTo(Description description) {
        description.appendText("Of(");
        matcher.describeTo(description);
        description.appendText(")");
      }

      @Override
      protected void describeMismatchSafely(ResultOrProblems<? extends T> item, Description mismatchDescription) {
        if (item.isPresent()) {
          matcher.describeMismatch(item.get(), mismatchDescription);
        } else {
          mismatchDescription.appendText("failed result - " + item.getProblems());
        }
      }

      @Override
      protected boolean matchesSafely(ResultOrProblems<? extends T> item) {
        return item.isPresent() && matcher.matches(item.get());
      }
    };
  }

  /**
   * Matcher for when both an item *and* Problems are returned. Often the problems
   * will be warnings, but that's up to the problem Matcher to confirm.
   */
  public static <T> Matcher<ResultOrProblems<? extends T>> resultWithProblems(Matcher<T> matcher,
      Matcher<Iterable<? extends Problem>> problemsMatcher) {

    return new TypeSafeMatcher<ResultOrProblems<? extends T>>() {

      @Override
      public void describeTo(Description description) {
        description.appendText("Of(");
        matcher.describeTo(description);
        description.appendText(")");
      }

      @Override
      protected void describeMismatchSafely(ResultOrProblems<? extends T> item, Description mismatchDescription) {
        if (item.isPresent()) {
          matcher.describeMismatch(item.getWithProblemsIgnored(), mismatchDescription);
        }
        if (!item.getProblems().isEmpty()) {
          mismatchDescription.appendText("failed result - " + item.getProblems());
        }
      }

      @Override
      protected boolean matchesSafely(ResultOrProblems<? extends T> item) {
        return item.isPresent() && matcher.matches(item.getWithProblemsIgnored())
            && problemsMatcher.matches(item.getProblems());
      }
    };
  }

  public static Matcher<Projector> projecting(Struct expectedType) {
    return new TypeSafeDiagnosingMatcher<Projector>(Projector.class) {

      @Override
      public void describeTo(Description description) {
        description.appendText("projector of ").appendValue(expectedType);
      }

      @Override
      protected boolean matchesSafely(Projector item, Description mismatchDescription) {
        if (!item.getProjectedType().equals(expectedType)) {
          mismatchDescription.appendText("projected type ").appendValue(item.getProjectedType());
          return false;
        }
        return true;
      }
    };
  }

  public static Matcher<FlatProjector> flatProjecting(Struct expectedType) {
    return new TypeSafeDiagnosingMatcher<FlatProjector>(FlatProjector.class) {

      @Override
      public void describeTo(Description description) {
        description.appendText("projector of ").appendValue(expectedType);
      }

      @Override
      protected boolean matchesSafely(FlatProjector item, Description mismatchDescription) {
        if (!item.getProjectedType().equals(expectedType)) {
          mismatchDescription.appendText("projected type ").appendValue(item.getProjectedType());
          return false;
        }
        return true;
      }
    };
  }

  /**
   * @deprecated use {@link RelationMatchers} instead
   */
  @Deprecated
  public static Matcher<Relation> relationWithType(Struct expectedType) {
    return RelationMatchers.relationWithType(expectedType);
  }

  /**
   * @deprecated use {@link RelationMatchers} instead
   */
  @Deprecated
  public static <T extends Object> Matcher<T> relationWithValues(Object... values) {
    return RelationMatchers.relationWithValues(values);
  }

  /**
   * @deprecated use {@link RelationMatchers} instead
   */
  @Deprecated
  public static Matcher<? super Relation> withValues(Object... values) {
    return RelationMatchers.withValues(values);
  }

  /**
   * @deprecated use {@link RelationMatchers} instead
   */
  @Deprecated
  public static Matcher<? super Relation> withTuples(List<Tuple> values) {
    return RelationMatchers.withTuples(values);
  }


  /**
   * @deprecated use {@link RelationMatchers} instead
   */
  @Deprecated
  public static <T extends Relation> Matcher<T> relationWithTuples(Tuple... values) {
    return RelationMatchers.relationWithTuples(values);
  }

  public static <T extends Object> Matcher<T> failedStep(String expectedName) {
    return failedStep(expectedName, anything());
  }

  public static <T extends Object> Matcher<T> failedStep(String expectedName, Matcher<?> problemMatchers) {
    return new BaseMatcher<T>() {

      @Override
      public boolean matches(Object item) {
        if (item instanceof RealizedStep) {
          RealizedStep step = (RealizedStep) item;
          return step.getStepName().equals(expectedName)
              && step.isFailed()
              && problemMatchers.matches(step.getProblems());
        } else {
          return false;
        }
      }

      @Override
      public void describeMismatch(Object item, Description description) {
        if (item instanceof RealizedStep) {
          RealizedStep step = (RealizedStep) item;
          if (step.getStepName().equals(expectedName)) {
            description.appendText("expected a failed step");
          } else {
            description.appendText("wrong name ").appendText(step.getStepName());
          }
          if (! problemMatchers.matches(step.getProblems())) {
            description.appendText(" wrong problems: ");
            problemMatchers.describeMismatch(step.getProblems(), description);
          }
        } else {
          description.appendText("wrong class");
        }
      }

      @Override
      public void describeTo(Description description) {
        description.appendText("Failed step " + expectedName);
      }
    };
  }

  public static <T extends Object> Matcher<T> realizedStepWith(
      String expectedName,
      Matcher<?> resultMatcher) {

    Matcher<?> problemMatcher = result(resultMatcher);
    return new BaseMatcher<T>() {

      @Override
      public boolean matches(Object item) {
        if (item instanceof RealizedStep) {
          RealizedStep step = (RealizedStep) item;

          return matchesNonRecursive(step);
        } else {
          return false;
        }
      }

      protected boolean matchesNonRecursive(RealizedStep step) {
        return step.getStepName().equals(expectedName) && problemMatcher.matches(step.getResult());
      }

      @Override
      public void describeMismatch(Object item, Description description) {
        if (item instanceof RealizedStep) {
          RealizedStep step = (RealizedStep) item;
          if (matchesNonRecursive(step)) {
            description.appendText("Dependency of ").appendValue(step.getStepName()).appendText(" - ");
          } else {
            description
                .appendValue(item)
                .appendText(" does not match ")
                .appendValue(expectedName)
                .appendText(":")
                .appendDescriptionOf(problemMatcher);
          }
        } else {
          description.appendValue(item).appendText(" is not a RealizedStep");
        }
      }

      @Override
      public void describeTo(Description description) {
        description.appendText("RealizedStep with result ");

      }
    };
  }

  /**
   * Matches that a row (List<String>) contains the expected items, or at least those that need to
   * be tested.
   *
   * The LHS of {@link Pair} identifies the item to test, the RHS how to test it.
   *
   */
  @SafeVarargs
  public static Matcher<List<String>> rowMatches(Pair<Integer, Matcher<String>>... matchers) {
    return new TypeSafeDiagnosingMatcher<List<String>>() {
      @Override
      protected boolean matchesSafely(List<String> t, Description d) {
        for (Pair<Integer, Matcher<String>> matcher: matchers) {
          if (t.size() < matcher.getLeft()) {
            d.appendText("Not enough items");
            return false;
          }
          Object value = t.get(matcher.getLeft());
          if (!matcher.getRight().matches(value)) {
            matcher.getRight().describeMismatch(value, d);
            return false;
          }
        }
        return true;
      }

      @Override
      public void describeTo(Description d) {
        d.appendText("row with items");
        for (Pair<Integer, Matcher<String>> matcher: matchers) {
          d.appendValue(matcher.getLeft()).appendText(": ");
          matcher.getRight().describeTo(d);
          d.appendText(" ");
        }
      }
    };
  }

  /**
   * A matcher that checks a relation contains single-value tuples that have the given values
   */
  public static Matcher<? super TupleIterator> iteratorWithValues(Object... values) {
    List<Object> valuesList = Arrays.asList(values);
    return new TypeSafeMatcher<TupleIterator>() {

      @Override
      public void describeTo(Description description) {
        description.appendText("Relation with values " + valuesList);
      }

      @Override
      protected boolean matchesSafely(TupleIterator item) {
        return Streams.stream(item)
            .map(t -> t.fetch("value"))
            .collect(Collectors.toList()).equals(valuesList);
      }

    };
  }

  /**
   * A matcher that checks a tuple iterator contains tuples that have the given values
   */
  public static Matcher<? super TupleIterator> iteratorWithTuples(Tuple... values) {
    List<Tuple> valuesList = Arrays.asList(values);
    return new TypeSafeDiagnosingMatcher<TupleIterator>() {

      @Override
      public void describeTo(Description description) {
        description.appendText("TupleIterator with values " + valuesList);
      }

      @Override
      protected boolean matchesSafely(TupleIterator item, Description descripiton) {
        boolean matched = true;
        List<Tuple> found = Streams.stream(item).collect(Collectors.toList());
        if (found.size() != valuesList.size()) {
          matched = false;
        } else {
          for (int i = 0; i < found.size(); i++) {
            if (! Objects.equals(found.get(i), valuesList.get(i))) {
              matched = false;
            }
          }
        }
        descripiton.appendText("values " + found);
        return matched;
      }

    };
  }

  /**
   * @return a {@link Matcher} that confirms an optional is empty (not present). Name isEmptyOptional to avoid confusion
   * or collisions with isEmpty, which might conflict or be easily confused with other collection matchers/mock methods
   */
  public static <T> Matcher<Optional<T>> isEmptyOptional() {
    return new DiagnosingMatcher<Optional<T>>() {

      @Override
      public void describeTo(Description description) {
        description.appendText("empty Optional");
      }

      @Override
      protected boolean matches(Object item, Description mismatchDescription) {
        if (item instanceof Optional) {
          Optional<?> opt = (Optional<?>) item;
          if (opt.isPresent()) {
            mismatchDescription.appendText("Optional not empty, is ").appendValue(opt.get());
            return false;
          } else {
            return true;
          }
        } else {
          mismatchDescription.appendText("not an Optional - ").appendValue(item);
          return false;
        }
      }
    };
  }

  /**
   * @return a {@link Matcher} that confirms an optional value is present and equals to the given value
   */
  public static <T> Matcher<Optional<T>> isPresent(T valueMatcher) {
    return isPresent(org.hamcrest.Matchers.equalTo(valueMatcher));
  }

  /**
   * @return a {@link Matcher} that confirms an optional value is present and matches the given value matcher.
   */
  public static <T> Matcher<Optional<T>> isPresent(Matcher<T> valueMatcher) {
    return new DiagnosingMatcher<Optional<T>>() {

      @Override
      public void describeTo(Description description) {
        description.appendText("Optional with value ").appendDescriptionOf(valueMatcher);
      }

      @Override
      protected boolean matches(Object item, Description mismatchDescription) {
        if (item instanceof Optional) {
          Optional<?> opt = (Optional<?>) item;
          if (opt.isPresent()) {
            return valueMatcher.matches(opt.get());
          } else {
            mismatchDescription.appendText("Optional is empty");
            return false;
          }
        } else {
          mismatchDescription.appendText("not an Optional - ").appendValue(item);
          return false;
        }
      }
    };
  }

  public static Matcher<? super Expression> isExpression(String source) {
    return new TypeSafeDiagnosingMatcher<Expression>() {

      @Override
      public void describeTo(Description description) {
        description.appendText("Expression: " + source);
      }

      @Override
      protected boolean matchesSafely(Expression item, Description desc) {
        if (! source.equals(item.toSource())) {
          desc.appendText("wrong source, found: " + item.toSource());
          return false;
        }
        return true;
      }
    };
  }
}
