package com.atlassian.bitbucket.hamcrest;

import com.google.common.collect.Sets;
import org.hamcrest.Description;
import org.hamcrest.Matcher;
import org.hamcrest.Matchers;
import org.hamcrest.TypeSafeDiagnosingMatcher;

import java.util.HashMap;
import java.util.Map;
import java.util.Set;

/**
 * Matcher for a map to ensures that the value of each entry matches a {@link Matcher}.
 * Every entry must have a match or a failure will result.
 *
 * @since 4.6
 */
public class MapMatcher<K, V> extends TypeSafeDiagnosingMatcher<Map<K, V>> {
    private Map<K, Matcher<?>> matchers = new HashMap<>();

    public MapMatcher<K, V> hasEntry(K key, Matcher<?> valueMatcher) {
        matchers.put(key, valueMatcher);
        return this;
    }

    public <T extends V> MapMatcher<K, V> hasEntry(K key, T value) {
        return hasEntry(key, Matchers.equalTo(value));
    }

    public MapMatcher<K, V> hasKey(K key) {
        return hasEntry(key, Matchers.anything());
    }

    @Override
    protected boolean matchesSafely(Map<K, V> item, Description mismatchDescription) {
        if (!item.keySet().equals(matchers.keySet())) {
            Set<?> extraItems = Sets.difference(item.keySet(), matchers.keySet());
            Set<?> notFound = Sets.difference(matchers.keySet(), item.keySet());
            if (extraItems.isEmpty()) {
                mismatchDescription.appendText(String.format("Expected keys %s not found.", notFound));
            } else {
                mismatchDescription.appendText(String.format("Extra keys %s found.", extraItems));
            }
            return false;
        } else {
            for (Map.Entry<K, Matcher<?>> matcherEntry : matchers.entrySet()) {
                K key = matcherEntry.getKey();
                V actualValue = item.get(key);
                Matcher<?> expectedMatcher = matcherEntry.getValue();
                if (!expectedMatcher.matches(actualValue)) {
                    mismatchDescription.appendText(String.format("Entry key %s expected ", key))
                            .appendDescriptionOf(expectedMatcher)
                            .appendText(" but was ");
                    expectedMatcher.describeMismatch(actualValue, mismatchDescription);
                    return false;
                }
            }
            return true;
        }
    }

    @Override
    public void describeTo(Description description) {
        description.appendText("{");
        boolean first = true;
        for (Map.Entry<K, Matcher<?>> entry : matchers.entrySet()) {
            if (!first) {
                description.appendText(", ");
            }
            description.appendText(entry.getKey().toString());
            description.appendText(": ");
            description.appendDescriptionOf(entry.getValue());

            first = false;
        }
        description.appendText("}");
    }
}
