package com.atlassian.bitbucket.mesh.test;

import com.atlassian.bitbucket.mesh.rpc.v1.*;
import com.google.common.collect.ImmutableList;
import com.google.protobuf.ByteString;
import com.google.protobuf.Descriptors;
import org.hamcrest.Description;
import org.hamcrest.TypeSafeDiagnosingMatcher;

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

import static java.util.Objects.requireNonNull;
import static org.apache.commons.lang3.StringUtils.defaultString;

/**
 * A matcher that verifies a {@link Throwable} has an {@link RpcGitError} attached via its trailers, and
 * that that error is wrapping the expected {@code oneof} payload. Verifying the payload implicitly tests
 * the {@code oneof} case
 */
public class HasGitError extends TypeSafeDiagnosingMatcher<Throwable> {

    private static final ImmutableList<String> SUPPORTED_STRATEGIES =
            ImmutableList.of("ff", "ff-only", "no-ff", "rebase-ff-only", "rebase-no-ff", "squash", "squash-ff-only");

    private final Object expected;

    public HasGitError(@Nonnull Object expected) {
        this.expected = requireNonNull(expected, "expected");
    }

    @Nonnull
    public static HasGitError emptyRebase() {
        return new HasGitError(RpcEmptyRebase.newBuilder()
                .build());
    }

    @Nonnull
    public static HasGitError emptyRebaseMerge() {
        return new HasGitError(RpcEmptyRebaseMerge.newBuilder()
                .build());
    }

    @Nonnull
    public static HasGitError emptySquashMerge() {
        return new HasGitError(RpcEmptySquashMerge.newBuilder()
                .build());
    }

    @Nonnull
    public static HasGitError invalidBranchName(@Nonnull String branchName, @Nonnull RpcInvalidBranchName.Reason reason) {
        return new HasGitError(RpcInvalidBranchName.newBuilder()
                .setBranchName(ByteString.copyFromUtf8(branchName))
                .setReason(reason)
                .build());
    }

    @Nonnull
    public static HasGitError invalidCommitish(String commitish) {
        return new HasGitError(RpcInvalidCommitish.newBuilder()
                .setCommitish(ByteString.copyFromUtf8(commitish))
                .build());
    }

    @Nonnull
    public static HasGitError invalidEditFile(@Nonnull RpcInvalidEditFile.Reason reason) {
        return new HasGitError(RpcInvalidEditFile.newBuilder()
                .setReason(reason)
                .build());
    }

    @Nonnull
    public static HasGitError invalidMerge(@Nonnull RpcInvalidMerge.Reason reason) {
        return new HasGitError(RpcInvalidMerge.newBuilder()
                .setReason(reason)
                .build());
    }

    @Nonnull
    public static HasGitError invalidPack(@Nonnull RpcInvalidPack.Reason reason) {
        return invalidPack(reason, null);
    }

    @Nonnull
    public static HasGitError invalidPack(@Nonnull RpcInvalidPack.Reason reason, @Nullable String details) {
        return new HasGitError(RpcInvalidPack.newBuilder()
                .setDetails(defaultString(details))
                .setReason(reason)
                .build());
    }

    @Nonnull
    public static HasGitError invalidRebase(@Nonnull RpcInvalidRebase.Reason reason) {
        return new HasGitError(RpcInvalidRebase.newBuilder()
                .setReason(reason)
                .build());
    }

    @Nonnull
    public static HasGitError invalidRefName(int maxLength, @Nonnull RpcInvalidRefName.Reason reason,
                                             @Nonnull String refName) {
        return new HasGitError(RpcInvalidRefName.newBuilder()
                .setMaxLength(maxLength)
                .setReason(reason)
                .setRefName(ByteString.copyFromUtf8(refName))
                .build());
    }

    @Nonnull
    public static HasGitError invalidRefName(@Nonnull RpcInvalidRefName.Reason reason, @Nonnull String refName) {
        return new HasGitError(RpcInvalidRefName.newBuilder()
                .setReason(reason)
                .setRefName(ByteString.copyFromUtf8(refName))
                .build());
    }

    @Nonnull
    public static HasGitError invalidTagName(@Nonnull RpcInvalidTagName.Reason reason, @Nonnull String tagName) {
        return new HasGitError(RpcInvalidTagName.newBuilder()
                .setReason(reason)
                .setTagName(ByteString.copyFromUtf8(tagName))
                .build());
    }

    @Nonnull
    public static HasGitError invalidTreeish(String treeish) {
        return new HasGitError(RpcInvalidTreeish.newBuilder()
                .setTreeish(ByteString.copyFromUtf8(treeish))
                .build());
    }

    @Nonnull
    public static HasGitError invalidWorkTreePath(String path, RpcInvalidWorkTreePath.Reason reason) {
        return new HasGitError(RpcInvalidWorkTreePath.newBuilder()
                .setPath(ByteString.copyFromUtf8(path))
                .setReason(reason)
                .build());
    }

    @Nonnull
    public static HasGitError lfsByteRangeNotSatisfiable(long start, long end) {
        return new HasGitError(RpcLfsByteRangeNotSatisfiable.newBuilder()
                .setStart(start)
                .setEnd(end)
                .build());
    }

    @Nonnull
    public static HasGitError lfsChecksumValidationFailed(@Nonnull String oid, @Nonnull String checksum) {
        return new HasGitError(RpcLfsChecksumValidationFailed.newBuilder()
                .setOid(oid)
                .setChecksum(checksum)
                .build());
    }

    @Nonnull
    public static HasGitError noSuchCommit(@Nonnull String commitish) {
        return new HasGitError(RpcNoSuchCommit.newBuilder()
                .setCommitish(ByteString.copyFromUtf8(commitish))
                .build());
    }

    @Nonnull
    public static HasGitError noSuchObject(@Nonnull String objectId) {
        return new HasGitError(RpcNoSuchObject.newBuilder()
                .setObjectId(ByteString.copyFromUtf8(objectId))
                .build());
    }

    @Nonnull
    public static HasGitError noSuchPath(@Nonnull String commitish, @Nonnull String path) {
        return new HasGitError(RpcNoSuchPath.newBuilder()
                .setCommitish(ByteString.copyFromUtf8(commitish))
                .setPath(ByteString.copyFromUtf8(path))
                .build());
    }

    @Nonnull
    public static HasGitError rebaseConflicted(@Nonnull String commitId) {
        return new HasGitError(RpcRebaseConflicted.newBuilder()
                .setFromCommitId(commitId)
                .build());
    }

    @Nonnull
    public static HasGitError nonFastForwardMerge(@Nonnull String fromCommitId) {
        return new HasGitError(RpcNonFastForwardMerge.newBuilder()
                .setFromCommitId(fromCommitId)
                .build());
    }

    @Nonnull
    public static HasGitError nonFastForwardUpdate(@Nonnull String rejectedRef) {
        return new HasGitError(RpcNonFastForwardUpdate.newBuilder()
                .setRejectedRef(ByteString.copyFromUtf8(rejectedRef))
                .build());
    }

    @Nonnull
    public static HasGitError refNameConflict(@Nonnull String conflictName, @Nonnull String refName) {
        return new HasGitError(RpcRefNameConflict.newBuilder()
                .setConflictName(ByteString.copyFromUtf8(conflictName))
                .setRefName(ByteString.copyFromUtf8(refName))
                .build());
    }

    @Nonnull
    public static HasGitError unsupportedCommand(@Nonnull String commandLine) {
        return new HasGitError(RpcUnsupportedCommand.newBuilder()
                .setCommandLine(ByteString.copyFromUtf8(commandLine))
                .build());
    }

    @Nonnull
    public static HasGitError unsupportedMergeStrategy(RpcMergeStrategy mergeStrategy) {
        return new HasGitError(RpcUnsupportedMergeStrategy.newBuilder()
                .addAllSupportedStrategies(SUPPORTED_STRATEGIES)
                .setStrategyId(mergeStrategy.name())
                .build());
    }

    @Override
    public void describeTo(Description description) {
        appendMessage(description, expected);
    }

    @Override
    protected boolean matchesSafely(Throwable throwable, Description description) {
        RpcGitError error = RpcGitErrorUtils.fromThrowable(throwable);
        if (error == null) {
            description.appendText("no RpcGitError was present");
            return false;
        }

        //Since the different error types are in a oneof, we don't need to validate the specific
        //oneof case. Instead, we use that case to dynamically retrieve whichever field has been
        //and compare the values. If it's the wrong oneof, we'll get a mismatch on the message.
        RpcGitError.ErrorOneofCase oneofCase = error.getErrorOneofCase();

        Descriptors.Descriptor typeDescriptor = error.getDescriptorForType();
        Descriptors.FieldDescriptor fieldDescriptor = typeDescriptor.findFieldByNumber(oneofCase.getNumber());

        Object actual = error.getField(fieldDescriptor);
        if (expected.equals(actual)) {
            return true;
        }

        description.appendText("was ");
        appendMessage(description, actual);

        return false;
    }

    private static void appendMessage(Description description, Object message) {
        //toString() for messages doesn't indicate the message type. We can _generally_ infer the
        //type from the fields it includes, but it's simpler to just include the name explicitly
        description.appendText(message.getClass().getSimpleName())
                .appendValue(message);
    }
}
