package com.atlassian.bitbucket.hamcrest;

import org.hamcrest.Description;
import org.hamcrest.Matcher;
import org.hamcrest.Matchers;
import org.hamcrest.TypeSafeDiagnosingMatcher;

import java.util.Optional;

public abstract class OptionalMatcher<T> extends TypeSafeDiagnosingMatcher<Optional<T>> {
    public static <T> OptionalMatcher<T> empty() {
        return new EmptyMatcher<>();
    }

    public static <T> OptionalMatcher<T> empty(final Class<? extends T> ignored) {
        return new EmptyMatcher<>();
    }

    public static <T> OptionalMatcher<T> some(Matcher<? super T> matcher) {
        return new SomeMatcher<>(matcher);
    }

    public static <T> OptionalMatcher<T> some(T value) {
        return new SomeMatcher<>(Matchers.equalTo(value));
    }

    public static <T> OptionalMatcher<T> fromOptional(Optional<T> option) {
        return option.map(OptionalMatcher::some).orElseGet(OptionalMatcher::empty);
    }

    public static <T> OptionalMatcher<T> fromNullable(T value) {
        return fromOptional(Optional.ofNullable(value));
    }

    private static class EmptyMatcher<T> extends OptionalMatcher<T> {
        @Override
        protected boolean matchesSafely(final Optional<T> item, final Description mismatchDescription) {
            if (item.isPresent()) {
                mismatchDescription.appendValue(item);
                return false;
            } else {
                return true;
            }
        }

        @Override
        public void describeTo(final Description description) {
            description.appendValue(Optional.empty());
        }
    }

    private static class SomeMatcher<T> extends OptionalMatcher<T> {
        private final Matcher<? super T> matcher;

        private SomeMatcher(final Matcher<? super T> matcher) {
            this.matcher = matcher;
        }

        @Override
        protected boolean matchesSafely(final Optional<T> item, final Description mismatchDescription) {
            if (item.isPresent()) {
                if (matcher.matches(item.get())) {
                    return true;
                } else {
                    mismatchDescription.appendText("some(");
                    matcher.describeMismatch(item.get(), mismatchDescription);
                    mismatchDescription.appendText(")");
                    return false;
                }
            } else {
                mismatchDescription.appendValue(item);
                return false;
            }
        }

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