package it.com.atlassian.stash.rest.webhook;

import com.atlassian.bitbucket.test.ProjectTestHelper;
import com.atlassian.bitbucket.test.RepositoryTestHelper;
import it.com.atlassian.stash.BaseRestTest;
import org.hamcrest.Matcher;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.TestName;
import org.springframework.core.io.ClassPathResource;

import java.util.function.Supplier;

import static com.atlassian.bitbucket.test.JsonMatcher.propEqualsIgnoreCase;
import static com.atlassian.bitbucket.test.JsonMatcher.propNotNull;
import static com.atlassian.bitbucket.test.rest.server.stub.mirror.StubMirror.DEFAULT_EXPECTED_TIMEOUT;
import static org.hamcrest.core.AllOf.allOf;

/**
 * A template class for testing web hook callbacks. Subclasses must perform the web hook registration and implement
 * {@link #assertWebHookCallbackArrived(String, Supplier)} to verify delivery of the web hook to the URL they register.
 *
 * Implementation note: subclasses will typically use a single {@link com.atlassian.bitbucket.test.rest.server.stub.mirror.StubMirror}
 * per test class suite which will accumulate requests as tests execute. Projects and repo slugs, keys etc should be unique
 * for each test so that {@code mirror.wait...(matcher)} calls match on requests created during the current test, not
 * the preceding tests.
 */
public abstract class BaseWebHookCallbackTest extends BaseRestTest {
    @Rule
    public TestName testName = new TestName();
    
    @Test
    public void testProjectCreatedCallback() throws Exception {
        testContext.project(prefixed("ABC"), prefixed("Abc"), "Abc project");

        assertWebHookCallbackArrived("project:created",
                () -> allOf(
                        propEqualsIgnoreCase("eventKey", "project:created"),
                        propNotNull("date"),
                        propEqualsIgnoreCase("actor.name", "admin"),
                        propEqualsIgnoreCase("project.key", prefixed("ABC")),
                        propEqualsIgnoreCase("project.name", prefixed("Abc")),
                        propNotNull("project.id")));
    }

    @Test
    public void testProjectDeletedCallback() throws Exception {
        testContext.project(prefixed("DEF"), prefixed("Def"), "Def project");

        ProjectTestHelper.deleteProject(prefixed("DEF"));

        assertWebHookCallbackArrived("project:deleted",
                () -> allOf(
                        propEqualsIgnoreCase("eventKey", "project:deleted"),
                        propNotNull("date"),
                        propEqualsIgnoreCase("actor.name", "admin"),
                        propEqualsIgnoreCase("project.key", prefixed("DEF")),
                        propEqualsIgnoreCase("project.name", prefixed("Def")),
                        propNotNull("project.id")));
    }

    @Test
    public void testProjectModifiedCallbackOnRename() throws Exception {
        testContext.project(prefixed("GHI"), prefixed("Ghi"), "Ghi project");
        ProjectTestHelper.updateProject(prefixed("GHI"), prefixed("GHI2"), null, null);

        try {
            assertWebHookCallbackArrived("project:modified",
                    () -> allOf(
                            propEqualsIgnoreCase("eventKey", "project:modified"),
                            propNotNull("date"),
                            propEqualsIgnoreCase("actor.name", "admin"),
                            propEqualsIgnoreCase("old.key", prefixed("GHI")),
                            propEqualsIgnoreCase("old.name", prefixed("Ghi")),
                            propNotNull("old.id"),
                            propEqualsIgnoreCase("new.key", prefixed("GHI2")),
                            propEqualsIgnoreCase("new.name", prefixed("Ghi")),
                            propNotNull("new.id")));
        } finally {
            ProjectTestHelper.updateProject(prefixed("GHI2"), prefixed("GHI"), null, null);
        }
    }

    @Test
    public void testRepositoryCreatedCallback() throws Exception {
        testContext.project(prefixed(projName()));
        testContext.repository(prefixed(projName()), prefixed("123"));

        assertWebHookCallbackArrived("repo:created",
                () -> allOf(
                        propEqualsIgnoreCase("eventKey", "repo:created"),
                        propNotNull("date"),
                        propEqualsIgnoreCase("actor.name", "admin"),
                        propEqualsIgnoreCase("repository.slug", prefixed("123")),
                        propEqualsIgnoreCase("repository.name", prefixed("123")),
                        propNotNull("repository.id"),
                        propEqualsIgnoreCase("repository.project.key", prefixed(projName())),
                        propEqualsIgnoreCase("repository.project.name", prefixed(projName())),
                        propNotNull("repository.project.id")));
    }

    @Test
    public void testRepositoryDeletedCallback() throws Exception {
        testContext.project(prefixed(projName()));
        RepositoryTestHelper.createRepository(prefixed(projName()), prefixed("456"));
        RepositoryTestHelper.deleteRepositoryAndWait(prefixed(projName()), prefixed("456"), DEFAULT_EXPECTED_TIMEOUT);

        assertWebHookCallbackArrived("repo:deleted",
                () -> allOf(
                        propEqualsIgnoreCase("eventKey", "repo:deleted"),
                        propNotNull("date"),
                        propEqualsIgnoreCase("actor.name", "admin"),
                        propEqualsIgnoreCase("repository.slug", prefixed("456")),
                        propEqualsIgnoreCase("repository.name", prefixed("456")),
                        propNotNull("repository.id"),
                        propEqualsIgnoreCase("repository.project.key", prefixed(projName())),
                        propEqualsIgnoreCase("repository.project.name", prefixed(projName())),
                        propNotNull("repository.project.id")));
    }

    @Test
    public void testRepositoryModifiedCallbackOnRename() throws Exception {
        testContext.project(prefixed(projName()), prefixed(projName()), prefixed(projName()));
        testContext.repository(prefixed(projName()), prefixed("789"));

        RepositoryTestHelper.updateRepositoryName(prefixed(projName()), prefixed("789"), prefixed("000"));
        try {
            assertWebHookCallbackArrived("repo:modified",
                    () -> allOf(
                            propEqualsIgnoreCase("eventKey", "repo:modified"),
                            propNotNull("date"),
                            propEqualsIgnoreCase("actor.name", "admin"),
                            propEqualsIgnoreCase("old.slug", prefixed("789")),
                            propEqualsIgnoreCase("old.name", prefixed("789")),
                            propNotNull("old.id"),
                            propEqualsIgnoreCase("old.project.key", prefixed(projName())),
                            propEqualsIgnoreCase("old.project.name", prefixed(projName())),
                            propNotNull("new.project.id"),
                            propEqualsIgnoreCase("new.slug", prefixed("000")),
                            propEqualsIgnoreCase("new.name", prefixed("000")),
                            propNotNull("new.id"),
                            propEqualsIgnoreCase("new.project.key", prefixed(projName())),
                            propEqualsIgnoreCase("new.project.name", prefixed(projName())),
                            propNotNull("new.project.id")));
        } finally {
            RepositoryTestHelper.updateRepositoryName(prefixed(projName()), prefixed("000"), prefixed("789"));
        }
    }

    @Test
    public void testRepositoryRefsUpdatedOnPushCallback() throws Exception {
        testContext.project(prefixed(projName()));
        testContext.repository(prefixed(projName()), prefixed("123"), new ClassPathResource(RepositoryTestHelper.QA_RESOURCES_REPOSITORY));

        assertWebHookCallbackArrived("repo:refs_changed",
                () -> allOf(
                        propEqualsIgnoreCase("eventKey", "repo:refs_changed"),
                        propNotNull("date"),
                        propEqualsIgnoreCase("actor.name", "admin"),
                        propEqualsIgnoreCase("repository.slug", prefixed("123")),
                        propEqualsIgnoreCase("repository.name", prefixed("123")),
                        propNotNull("repository.id"),
                        propEqualsIgnoreCase("repository.project.key", prefixed(projName())),
                        propEqualsIgnoreCase("repository.project.name", prefixed(projName())),
                        propNotNull("repository.project.id"),
                        // spot checks, not comprehensive tests
                        propEqualsIgnoreCase("changes[0].refId", "refs/tags/tag#1"),
                        propEqualsIgnoreCase("changes[0].fromHash", "0000000000000000000000000000000000000000"),
                        propEqualsIgnoreCase("changes[0].toHash", "f2175c116ac675b72bf00cdcaa26f50b8b13ff7e"),
                        propEqualsIgnoreCase("changes[0].type", "ADD"),
                        propEqualsIgnoreCase("changes[201].refId", "refs/tags/tag_99"),
                        propEqualsIgnoreCase("changes[201].fromHash", "0000000000000000000000000000000000000000"),
                        propEqualsIgnoreCase("changes[201].toHash", "ef7233f739420cb62647a6d042c62f0bd49611b0"),
                        propEqualsIgnoreCase("changes[201].type", "ADD")));
    }

    protected abstract void assertWebHookCallbackArrived(String eventId, Supplier<Matcher<String>> matcherSupplier);

    protected String prefixed(String name) {
        return getClass().getSimpleName() + "-" + name;
    }

    private String projName() {
        return testName.getMethodName();
    }
}
