package com.atlassian.bitbucket.mesh.test;

import com.atlassian.bitbucket.mesh.event.EventListener;
import org.hamcrest.BaseMatcher;
import org.hamcrest.Matcher;
import org.hamcrest.core.IsIterableContaining;
import org.junit.rules.TestRule;
import org.junit.runner.Description;
import org.junit.runners.model.Statement;

import javax.annotation.Nonnull;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.CopyOnWriteArrayList;

import static java.util.Objects.requireNonNull;
import static org.hamcrest.CoreMatchers.allOf;
import static org.hamcrest.CoreMatchers.instanceOf;
import static org.hamcrest.MatcherAssert.assertThat;

public class ExpectedEvents implements TestRule {

    private final List<Object> events;
    private final List<Matcher<? super Iterable<Object>>> eventMatchers;

    public ExpectedEvents() {
        events = new CopyOnWriteArrayList<>();
        eventMatchers = new ArrayList<>();
    }

    @Override
    public Statement apply(Statement base, Description description) {
        return new Statement() {

            @Override
            public void evaluate() throws Throwable {
                try {
                    events.clear();
                    eventMatchers.clear();

                    base.evaluate();
                } finally {
                    assertThat(events, allOf(eventMatchers));
                    eventMatchers.clear();
                    events.clear();
                }
            }
        };
    }

    public void expectAtLeastOne(@Nonnull Class<?> eventClass) {
        requireNonNull(eventClass, "eventClass");

        eventMatchers.add(new IsIterableContaining<>(instanceOf(eventClass)));
    }

    public void expect(@Nonnull Class<?> eventClass) {
        expect(eventClass, 1);
    }

    public void expect(@Nonnull Class<?> eventClass, int count) {
        requireNonNull(eventClass);

        eventMatchers.add(new IsCollectionContainingExactly(count, instanceOf(eventClass)));
    }

    public void expectNone(@Nonnull Class<?> eventClass) {
        expect(eventClass, 0);
    }

    @EventListener
    public void onEvent(Object event) {
        events.add(event);
    }

    static class IsCollectionContainingExactly extends BaseMatcher<Iterable<Object>> {

        private final Matcher<Object> delegate;
        private final int wantedCount;

        IsCollectionContainingExactly(int wantedCount, Matcher<Object> delegate) {
            this.wantedCount = wantedCount;
            this.delegate = delegate;
        }

        @Override
        public void describeTo(org.hamcrest.Description description) {
            description.appendText("a collection containing " + wantedCount + " times ")
                    .appendDescriptionOf(delegate);
        }

        @Override
        public boolean matches(Object item) {
            int count = 0;

            if (item instanceof Iterable) {
                for (Object o : (Iterable<?>) item) {
                    if (delegate.matches(o)) {
                        ++count;
                    }
                }
                return count == wantedCount;
            }
            return false;
        }
    }
}
