package com.atlassian.bitbucket.hamcrest;

import com.atlassian.bitbucket.repository.*;
import org.apache.commons.lang3.StringUtils;
import org.hamcrest.*;

import javax.annotation.Nonnull;
import javax.annotation.Nullable;

import java.util.Objects;

import static org.hamcrest.Matchers.allOf;
import static org.hamcrest.Matchers.equalTo;
import static org.mockito.internal.util.Checks.checkNotNull;

public class RepositoryMatchers {

    private RepositoryMatchers() {
        throw new UnsupportedOperationException(RepositoryMatchers.class.getName() + " should not be instantiated");
    }

    public static Matcher<Repository> withId(int expectedId) {
        return new FeatureMatcher<Repository, Integer>(equalTo(expectedId), "repository ID equal to", "id") {
            @Override
            protected Integer featureValueOf(@Nonnull Repository actual) {
                return actual.getId();
            }
        };
    }

    public static Matcher<Repository> withName(@Nonnull String expectedName) {
        checkNotNull(expectedName, "expectedName");
        return withNameThat(equalTo(expectedName));
    }

    public static Matcher<Repository> withNameAndId(@Nonnull String expectedName, int expectedId) {
        checkNotNull(expectedName, "expectedName");
        return allOf(withName(expectedName), withId(expectedId));
    }

    public static Matcher<Repository> withNameThat(@Nonnull Matcher<String> nameMatcher) {
        checkNotNull(nameMatcher, "nameMatcher");
        return new FeatureMatcher<Repository, String>(nameMatcher, "repository name that", "name") {
            @Override
            protected String featureValueOf(@Nonnull Repository actual) {
                return actual.getName();
            }
        };
    }

    public static Matcher<Repository> withProjectName(@Nonnull String expectedProjectName) {
        checkNotNull(expectedProjectName, "expectedProjectName");
        return new FeatureMatcher<Repository, String>(equalTo(expectedProjectName), "project name that", "project name") {
            @Override
            protected String featureValueOf(@Nonnull Repository actual) {
                return actual.getProject().getName();
            }
        };
    }

    /**
     * Creates a branch matcher which verifies that the branch has the specified {@code displayId} and
     * {@code latestCommit} <i>and is not the default branch</i>.
     *
     * @param displayId    the expected display ID
     * @param latestCommit the expected latestCommit
     * @return {@code branch(displayId, latestCommit, false)}
     * @since 4.6
     */
    @Factory
    public static Matcher<Branch> branch(String displayId, String latestCommit) {
        return branch(displayId, latestCommit, false);
    }

    @Factory
    public static Matcher<Branch> branch(String displayId, String latestCommit, boolean isDefault) {
        return new BranchMatcher(displayId, latestCommit, isDefault);
    }

    @Factory
    public static Matcher<Branch> isBranchWithDisplayId(String displayId) {
        return new FeatureMatcher<Branch, String>(equalTo(displayId), "display ID", "displayId") {
            @Override
            protected String featureValueOf(Branch actual) {
                return actual.getDisplayId();
            }
        };
    }

    @Factory
    public static Matcher<Ref> ref(String displayId, String commit) {
        return new RefMatcher(displayId, commit);
    }

    /**
     * Creates a request matcher which verifies that the {@link ResolveRefRequest} has the specified
     * {@code repository} with {@code branchId} for its {@link ResolveRefRequest#getRefId refId}, and
     * {@link StandardRefType#BRANCH} for its {@link ResolveRefRequest#getType type}.
     *
     * @param repository the expected repository
     * @param branchId   the expected branch ID
     * @return a matcher for the specified expected values
     * @since 4.6
     */
    @Factory
    public static Matcher<ResolveRefRequest> resolveBranchRequest(@Nonnull Repository repository,
                                                                  @Nullable String branchId) {
        return resolveRefRequest(repository, branchId, StandardRefType.BRANCH);
    }

    /**
     * Creates a request matcher which verifies that the {@link ResolveRefRequest} has the specified
     * {@code repository} and {@code refId} <i>and has no explicit {@link ResolveRefRequest#getType() type}</i>.
     *
     * @param repository the expected repository
     * @param refId      the expected ref ID
     * @return {@code resolveRefRequest(repository, refId, null)}
     * @since 4.6
     */
    @Factory
    public static Matcher<ResolveRefRequest> resolveRefRequest(@Nonnull Repository repository, @Nullable String refId) {
        return resolveRefRequest(repository, refId, null);
    }

    /**
     * Creates a request matcher which verifies that the {@link ResolveRefRequest} has the specified
     * {@code repository}, {@code refId} and {@code type}.
     *
     * @param repository the expected repository
     * @param refId      the expected ref ID
     * @param type       the expected type
     * @return a matcher for the specified expected values
     * @since 4.6
     */
    @Factory
    public static Matcher<ResolveRefRequest> resolveRefRequest(@Nonnull Repository repository, @Nullable String refId,
                                                               @Nullable RefType type) {
        return new ResolveRefRequestMatcher(repository, refId, type);
    }

    /**
     * Creates a request matcher which verifies that the {@link ResolveRefRequest} has the specified
     * {@code repository} with {@code tagId} for its {@link ResolveRefRequest#getRefId refId}, and
     * {@link StandardRefType#TAG} for its {@link ResolveRefRequest#getType type}.
     *
     * @param repository the expected repository
     * @param tagId      the expected tag ID
     * @return a matcher for the specified expected values
     * @since 4.6
     */
    @Factory
    public static Matcher<ResolveRefRequest> resolveTagRequest(@Nonnull Repository repository, @Nullable String tagId) {
        return resolveRefRequest(repository, tagId, StandardRefType.TAG);
    }

    @Factory
    public static Matcher<Tag> tag(String displayId, String latestCommit) {
        return tag(displayId, latestCommit, null);
    }

    @Factory
    public static Matcher<Tag> tag(String displayId, String latestCommit, String hash) {
        return new TagMatcher(displayId, latestCommit, hash);
    }

    private static class RefMatcher extends TypeSafeMatcher<Ref> {

        private final String displayId;
        private final String latestCommit;

        private RefMatcher(String displayId, String latestCommit) {
            this.displayId = displayId;
            this.latestCommit = latestCommit;
        }

        @Override
        public boolean matchesSafely(Ref ref) {
            return ref.getDisplayId().equals(displayId)
                    && ref.getLatestCommit().equals(latestCommit);
        }

        @Override
        public void describeTo(Description description) {
            description.appendText("a ref '").appendText(displayId)
                    .appendText("' at ").appendText(latestCommit);
        }
    }

    /**
     * @since 4.6
     */
    private static class ResolveRefRequestMatcher extends TypeSafeMatcher<ResolveRefRequest> {

        private final String refId;
        private final Repository repository;
        private final RefType type;

        public ResolveRefRequestMatcher(Repository repository, String refId, RefType type) {
            super(ResolveRefRequest.class);

            this.refId = refId;
            this.repository = repository;
            this.type = type;
        }

        @Override
        public void describeTo(Description description) {
            description.appendText("a request to resolve ");
            if (refId == null) {
                description.appendText("the default branch");
            } else {
                description.appendText("a ")
                        .appendText(type == null ? "REF" : type.toString())
                        .appendText(" called ")
                        .appendText(refId);
            }
            description.appendText(" in ")
                    .appendValue(repository);
        }

        @Override
        protected boolean matchesSafely(ResolveRefRequest request) {
            return Objects.equals(repository, request.getRepository()) &&
                    Objects.equals(refId, request.getRefId().orElse(null)) &&
                    Objects.equals(type, request.getType().orElse(null));
        }
    }

    private static class BranchMatcher extends TypeSafeMatcher<Branch> {

        private final String displayId;
        private final String latestCommit;
        private final boolean isDefault;

        private BranchMatcher(String displayId, String latestCommit, boolean isDefault) {
            this.displayId = displayId;
            this.latestCommit = latestCommit;
            this.isDefault = isDefault;
        }

        @Override
        public boolean matchesSafely(Branch branch) {
            return branch.getDisplayId().equals(displayId)
                    && branch.getLatestCommit().equals(latestCommit)
                    && branch.getIsDefault() == isDefault;
        }

        @Override
        public void describeTo(Description description) {
            description.appendText("a branch '").appendText(displayId)
                    .appendText("' at ").appendText(latestCommit);
            if (isDefault) {
                description.appendText(" (default branch)");
            }
        }
    }

    private static class TagMatcher extends TypeSafeMatcher<Tag> {

        private final String displayId;
        private final String latestCommit;
        private final String hash;

        private TagMatcher(String displayId, String latestCommit, String hash) {
            this.displayId = displayId;
            this.latestCommit = latestCommit;
            this.hash = hash;
        }

        @Override
        public boolean matchesSafely(Tag tag) {
            return StringUtils.equals(tag.getHash(), hash)
                    && tag.getDisplayId().equals(displayId)
                    && tag.getLatestCommit().equals(latestCommit);
        }

        @Override
        public void describeTo(Description description) {
            description.appendText("a tag '").appendText(displayId)
                    .appendText("' with hash ").appendText(hash)
                    .appendText(" tagging ").appendText(latestCommit);
        }
    }
}
