package com.atlassian.stash.internal.spring;

import com.google.common.collect.ArrayListMultimap;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.Multimap;
import org.hamcrest.Description;
import org.hamcrest.Matcher;
import org.hamcrest.TypeSafeDiagnosingMatcher;
import org.springframework.web.servlet.ModelAndView;

import java.util.Collection;
import java.util.Map;
import java.util.Objects;
import java.util.TreeMap;

import static org.hamcrest.Matchers.equalTo;

/**
 * A template for building a {@link ModelAndView} matchers. This can be extended to add type safe methods
 * for your controller. The {@link ModelAndViewMatcher} provides a generic implementation that can be used on any
 * {@code ModelAndView}
 *
 * @param <T> the type of the subclass.
 */
public abstract class AbstractModelAndViewMatcher<T extends AbstractModelAndViewMatcher<T>> extends TypeSafeDiagnosingMatcher<ModelAndView> {
    private final String viewName;
    private final Map<String, Matcher<?>> matchers = new TreeMap<>();
    private ErrorFieldsMatcher errorFieldsMatcher = null;

    protected AbstractModelAndViewMatcher(String name) {
        this.viewName = name;
    }

    public T withEntry(String name, Matcher<?> matcher) {
        matchers.put(name, new ModelMatcher<>(Object.class, matcher));
        return self();
    }

    public T withEntry(String name, Object value) {
        return withEntry(name, equalTo(value));
    }

    public <S> T withEntry(String name, Matcher<? super S> matcher, Class<S> type) {
        matchers.put(name, new ModelMatcher<>(type, matcher));
        return self();
    }

    public T withFormError(String error) {
        withEntry("formErrors", ImmutableList.of(error));
        return self();
    }

    public T withFieldError(String field, String name) {
        if (errorFieldsMatcher == null) {
            errorFieldsMatcher = new ErrorFieldsMatcher();
            matchers.put("fieldErrors", errorFieldsMatcher);
        }
        errorFieldsMatcher.addField(field, name);
        return self();
    }

    @Override
    public void describeTo(Description description) {
        description.appendText(viewName).appendText(" [");
        boolean first = true;
        for (Map.Entry<String, Matcher<?>> entry : matchers.entrySet()) {
            if (first) {
                first = false;
            } else {
                description.appendText(", ");
            }
            description.appendText(entry.getKey()).appendText(": ").appendDescriptionOf(entry.getValue());
        }
        description.appendText("]");
    }

    @Override
    protected boolean matchesSafely(ModelAndView item, Description mismatchDescription) {
        if (!Objects.equals(item.getViewName(), viewName)) {
            mismatchDescription.appendText(String.format("View %s != %s", viewName, item.getViewName()));
            return false;
        } else {
            Map<String, Object> model = item.getModel();
            for (Map.Entry<String, Matcher<?>> matcherEntry : matchers.entrySet()) {
                String key = matcherEntry.getKey();
                Object actualValue = model.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;
        }
    }

    protected abstract T self();

    private static class ModelMatcher<T> extends TypeSafeDiagnosingMatcher<T> {
        private final Class<T> type;
        private final Matcher<?> delegate;

        private ModelMatcher(Class<T> type, Matcher<?> delegate) {
            this.type = type;
            this.delegate = delegate;
        }

        @Override
        protected boolean matchesSafely(T item, Description mismatchDescription) {
            if (type.isInstance(item)) {
                if (delegate.matches(item)) {
                    return true;
                } else {
                    delegate.describeMismatch(item, mismatchDescription);
                    return false;
                }
            } else {
                mismatchDescription.appendValue(String.format("Expected %s but got %s.", type.getSimpleName(), item.getClass().getSimpleName()));
                return false;
            }
        }

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

    private static class ErrorFieldsMatcher extends TypeSafeDiagnosingMatcher<Map<String, Collection<String>>> {
        private final Multimap<String, String> errors = ArrayListMultimap.create();

        private ErrorFieldsMatcher addField(String field, String error) {
            errors.put(field, error);
            return this;
        }

        @Override
        protected boolean matchesSafely(Map<String, Collection<String>> item, Description mismatchDescription) {
            if (item == null) {
                if (errors.isEmpty()) {
                    return true;
                } else {
                    mismatchDescription.appendText(toString(ImmutableMap.of()));
                    return false;
                }
            } else {
                Map<String, Collection<String>> expectedErrors = errors.asMap();
                if (item.equals(expectedErrors)) {
                    return true;
                } else {
                    mismatchDescription.appendText(toString(item));
                    return false;
                }
            }
        }

        @Override
        public void describeTo(Description description) {
            description.appendText(toString(errors));
        }

        private static String toString(Multimap<String, String> errors) {
            return toString(errors.asMap());
        }

        private static String toString(Map<String, Collection<String>> errors) {
            Map<String, Collection<String>> display = new TreeMap<>();
            display.putAll(errors);
            return display.toString();
        }
    }
}
