package commit

import (
	"context"
	"testing"

	"github.com/golang/protobuf/ptypes/timestamp"
	"github.com/stretchr/testify/require"
	"gitlab.com/gitlab-org/gitaly/v14/internal/git"
	"gitlab.com/gitlab-org/gitaly/v14/internal/gitaly/service/ref"
	"gitlab.com/gitlab-org/gitaly/v14/internal/testhelper"
	"gitlab.com/gitlab-org/gitaly/v14/proto/go/gitalypb"
	"google.golang.org/grpc/codes"
)

func TestSuccessfulFindAllCommitsRequest(t *testing.T) {
	defer func() {
		_findBranchNamesFunc = ref.FindBranchNames
	}()

	_findBranchNamesFunc = func(context.Context, git.RepositoryExecutor) ([][]byte, error) {
		return [][]byte{
			[]byte("few-commits"),
			[]byte("two-commits"),
		}, nil
	}

	_, repo, _, client := setupCommitServiceWithRepo(t, true)

	// Commits made on another branch in parallel to the normal commits below.
	// Will be used to test topology ordering.
	alternateCommits := []*gitalypb.GitCommit{
		{
			Id:        "0031876facac3f2b2702a0e53a26e89939a42209",
			Subject:   []byte("Merge branch 'few-commits-4' into few-commits-2"),
			Body:      []byte("Merge branch 'few-commits-4' into few-commits-2\n"),
			Author:    dummyCommitAuthor(1500320762),
			Committer: dummyCommitAuthor(1500320762),
			ParentIds: []string{
				"bf6e164cac2dc32b1f391ca4290badcbe4ffc5fb",
				"48ca272b947f49eee601639d743784a176574a09",
			},
			BodySize: 48,
			TreeId:   "91639b9835ff541f312fd2735f639a50bf35d472",
		},
		{
			Id:        "48ca272b947f49eee601639d743784a176574a09",
			Subject:   []byte("Commit #9 alternate"),
			Body:      []byte("Commit #9 alternate\n"),
			Author:    dummyCommitAuthor(1500320271),
			Committer: dummyCommitAuthor(1500320271),
			ParentIds: []string{"335bc94d5b7369b10251e612158da2e4a4aaa2a5"},
			BodySize:  20,
			TreeId:    "91639b9835ff541f312fd2735f639a50bf35d472",
		},
		{
			Id:        "335bc94d5b7369b10251e612158da2e4a4aaa2a5",
			Subject:   []byte("Commit #8 alternate"),
			Body:      []byte("Commit #8 alternate\n"),
			Author:    dummyCommitAuthor(1500320269),
			Committer: dummyCommitAuthor(1500320269),
			ParentIds: []string{"1039376155a0d507eba0ea95c29f8f5b983ea34b"},
			BodySize:  20,
			TreeId:    "91639b9835ff541f312fd2735f639a50bf35d472",
		},
	}

	// Nothing special about these commits.
	normalCommits := []*gitalypb.GitCommit{
		{
			Id:        "bf6e164cac2dc32b1f391ca4290badcbe4ffc5fb",
			Subject:   []byte("Commit #10"),
			Body:      []byte("Commit #10\n"),
			Author:    dummyCommitAuthor(1500320272),
			Committer: dummyCommitAuthor(1500320272),
			ParentIds: []string{"9d526f87b82e2b2fd231ca44c95508e5e85624ca"},
			BodySize:  11,
			TreeId:    "91639b9835ff541f312fd2735f639a50bf35d472",
		},
		{
			Id:        "9d526f87b82e2b2fd231ca44c95508e5e85624ca",
			Subject:   []byte("Commit #9"),
			Body:      []byte("Commit #9\n"),
			Author:    dummyCommitAuthor(1500320270),
			Committer: dummyCommitAuthor(1500320270),
			ParentIds: []string{"1039376155a0d507eba0ea95c29f8f5b983ea34b"},
			BodySize:  10,
			TreeId:    "91639b9835ff541f312fd2735f639a50bf35d472",
		},
		{
			Id:        "1039376155a0d507eba0ea95c29f8f5b983ea34b",
			Subject:   []byte("Commit #8"),
			Body:      []byte("Commit #8\n"),
			Author:    dummyCommitAuthor(1500320268),
			Committer: dummyCommitAuthor(1500320268),
			ParentIds: []string{"54188278422b1fa877c2e71c4e37fc6640a58ad1"},
			BodySize:  10,
			TreeId:    "91639b9835ff541f312fd2735f639a50bf35d472",
		}, {
			Id:        "54188278422b1fa877c2e71c4e37fc6640a58ad1",
			Subject:   []byte("Commit #7"),
			Body:      []byte("Commit #7\n"),
			Author:    dummyCommitAuthor(1500320266),
			Committer: dummyCommitAuthor(1500320266),
			ParentIds: []string{"8b9270332688d58e25206601900ee5618fab2390"},
			BodySize:  10,
			TreeId:    "91639b9835ff541f312fd2735f639a50bf35d472",
		}, {
			Id:        "8b9270332688d58e25206601900ee5618fab2390",
			Subject:   []byte("Commit #6"),
			Body:      []byte("Commit #6\n"),
			Author:    dummyCommitAuthor(1500320264),
			Committer: dummyCommitAuthor(1500320264),
			ParentIds: []string{"f9220df47bce1530e90c189064d301bfc8ceb5ab"},
			BodySize:  10,
			TreeId:    "91639b9835ff541f312fd2735f639a50bf35d472",
		}, {
			Id:        "f9220df47bce1530e90c189064d301bfc8ceb5ab",
			Subject:   []byte("Commit #5"),
			Body:      []byte("Commit #5\n"),
			Author:    dummyCommitAuthor(1500320262),
			Committer: dummyCommitAuthor(1500320262),
			ParentIds: []string{"40d408f89c1fd26b7d02e891568f880afe06a9f8"},
			BodySize:  10,
			TreeId:    "91639b9835ff541f312fd2735f639a50bf35d472",
		}, {
			Id:        "40d408f89c1fd26b7d02e891568f880afe06a9f8",
			Subject:   []byte("Commit #4"),
			Body:      []byte("Commit #4\n"),
			Author:    dummyCommitAuthor(1500320260),
			Committer: dummyCommitAuthor(1500320260),
			ParentIds: []string{"df914c609a1e16d7d68e4a61777ff5d6f6b6fde3"},
			BodySize:  10,
			TreeId:    "91639b9835ff541f312fd2735f639a50bf35d472",
		}, {
			Id:        "df914c609a1e16d7d68e4a61777ff5d6f6b6fde3",
			Subject:   []byte("Commit #3"),
			Body:      []byte("Commit #3\n"),
			Author:    dummyCommitAuthor(1500320258),
			Committer: dummyCommitAuthor(1500320258),
			ParentIds: []string{"6762605237fc246ae146ac64ecb467f71d609120"},
			BodySize:  10,
			TreeId:    "91639b9835ff541f312fd2735f639a50bf35d472",
		}, {
			Id:        "6762605237fc246ae146ac64ecb467f71d609120",
			Subject:   []byte("Commit #2"),
			Body:      []byte("Commit #2\n"),
			Author:    dummyCommitAuthor(1500320256),
			Committer: dummyCommitAuthor(1500320256),
			ParentIds: []string{"79b06233d3dc769921576771a4e8bee4b439595d"},
			BodySize:  10,
			TreeId:    "91639b9835ff541f312fd2735f639a50bf35d472",
		}, {
			Id:        "79b06233d3dc769921576771a4e8bee4b439595d",
			Subject:   []byte("Commit #1"),
			Body:      []byte("Commit #1\n"),
			Author:    dummyCommitAuthor(1500320254),
			Committer: dummyCommitAuthor(1500320254),
			ParentIds: []string{"1a0b36b3cdad1d2ee32457c102a8c0b7056fa863"},
			BodySize:  10,
			TreeId:    "91639b9835ff541f312fd2735f639a50bf35d472",
		},
		{
			Id:      "1a0b36b3cdad1d2ee32457c102a8c0b7056fa863",
			Subject: []byte("Initial commit"),
			Body:    []byte("Initial commit\n"),
			Author: &gitalypb.CommitAuthor{
				Name:     []byte("Dmitriy Zaporozhets"),
				Email:    []byte("dmitriy.zaporozhets@gmail.com"),
				Date:     &timestamp.Timestamp{Seconds: 1393488198},
				Timezone: []byte("-0800"),
			},
			Committer: &gitalypb.CommitAuthor{
				Name:     []byte("Dmitriy Zaporozhets"),
				Email:    []byte("dmitriy.zaporozhets@gmail.com"),
				Date:     &timestamp.Timestamp{Seconds: 1393488198},
				Timezone: []byte("-0800"),
			},
			ParentIds: nil,
			BodySize:  15,
			TreeId:    "91639b9835ff541f312fd2735f639a50bf35d472",
		},
	}

	// A commit that exists on "two-commits" branch.
	singleCommit := []*gitalypb.GitCommit{
		{
			Id:        "304d257dcb821665ab5110318fc58a007bd104ed",
			Subject:   []byte("Commit #11"),
			Body:      []byte("Commit #11\n"),
			Author:    dummyCommitAuthor(1500322381),
			Committer: dummyCommitAuthor(1500322381),
			ParentIds: []string{"1a0b36b3cdad1d2ee32457c102a8c0b7056fa863"},
			BodySize:  11,
			TreeId:    "91639b9835ff541f312fd2735f639a50bf35d472",
		},
	}

	timeOrderedCommits := []*gitalypb.GitCommit{
		alternateCommits[0], normalCommits[0],
		alternateCommits[1], normalCommits[1],
		alternateCommits[2],
	}
	timeOrderedCommits = append(timeOrderedCommits, normalCommits[2:]...)
	topoOrderedCommits := append(alternateCommits, normalCommits...)

	testCases := []struct {
		desc            string
		request         *gitalypb.FindAllCommitsRequest
		expectedCommits []*gitalypb.GitCommit
	}{
		{
			desc: "all commits of a revision",
			request: &gitalypb.FindAllCommitsRequest{
				Revision: []byte("few-commits"),
			},
			expectedCommits: timeOrderedCommits,
		},
		{
			desc: "maximum number of commits of a revision",
			request: &gitalypb.FindAllCommitsRequest{
				MaxCount: 5,
				Revision: []byte("few-commits"),
			},
			expectedCommits: timeOrderedCommits[:5],
		},
		{
			desc: "skipping number of commits of a revision",
			request: &gitalypb.FindAllCommitsRequest{
				Skip:     5,
				Revision: []byte("few-commits"),
			},
			expectedCommits: timeOrderedCommits[5:],
		},
		{
			desc: "maximum number of commits of a revision plus skipping",
			request: &gitalypb.FindAllCommitsRequest{
				Skip:     5,
				MaxCount: 2,
				Revision: []byte("few-commits"),
			},
			expectedCommits: timeOrderedCommits[5:7],
		},
		{
			desc: "all commits of a revision ordered by date",
			request: &gitalypb.FindAllCommitsRequest{
				Revision: []byte("few-commits"),
				Order:    gitalypb.FindAllCommitsRequest_DATE,
			},
			expectedCommits: timeOrderedCommits,
		},
		{
			desc: "all commits of a revision ordered by topology",
			request: &gitalypb.FindAllCommitsRequest{
				Revision: []byte("few-commits"),
				Order:    gitalypb.FindAllCommitsRequest_TOPO,
			},
			expectedCommits: topoOrderedCommits,
		},
		{
			desc:            "all commits of all branches",
			request:         &gitalypb.FindAllCommitsRequest{},
			expectedCommits: append(singleCommit, timeOrderedCommits...),
		},
		{
			desc:            "non-existing revision",
			request:         &gitalypb.FindAllCommitsRequest{Revision: []byte("i-do-not-exist")},
			expectedCommits: []*gitalypb.GitCommit{},
		},
	}

	for _, testCase := range testCases {
		t.Run(testCase.desc, func(t *testing.T) {
			request := testCase.request
			request.Repository = repo

			ctx, cancel := testhelper.Context()
			defer cancel()
			c, err := client.FindAllCommits(ctx, request)
			require.NoError(t, err)

			receivedCommits := getAllCommits(t, func() (gitCommitsGetter, error) { return c.Recv() })

			require.Equal(t, len(testCase.expectedCommits), len(receivedCommits), "number of commits received")

			for i, receivedCommit := range receivedCommits {
				testhelper.ProtoEqual(t, testCase.expectedCommits[i], receivedCommit)
			}
		})
	}
}

func TestFailedFindAllCommitsRequest(t *testing.T) {
	_, repo, _, client := setupCommitServiceWithRepo(t, true)

	invalidRepo := &gitalypb.Repository{StorageName: "fake", RelativePath: "path"}

	testCases := []struct {
		desc    string
		request *gitalypb.FindAllCommitsRequest
		code    codes.Code
	}{
		{
			desc:    "Invalid repository",
			request: &gitalypb.FindAllCommitsRequest{Repository: invalidRepo},
			code:    codes.InvalidArgument,
		},
		{
			desc:    "Repository is nil",
			request: &gitalypb.FindAllCommitsRequest{},
			code:    codes.InvalidArgument,
		},
		{
			desc: "Revision is invalid",
			request: &gitalypb.FindAllCommitsRequest{
				Repository: repo,
				Revision:   []byte("--output=/meow"),
			},
			code: codes.InvalidArgument,
		},
	}

	for _, testCase := range testCases {
		t.Run(testCase.desc, func(t *testing.T) {
			ctx, cancel := testhelper.Context()
			defer cancel()
			c, err := client.FindAllCommits(ctx, testCase.request)
			require.NoError(t, err)

			err = drainFindAllCommitsResponse(c)
			testhelper.RequireGrpcError(t, err, testCase.code)
		})
	}
}

func drainFindAllCommitsResponse(c gitalypb.CommitService_FindAllCommitsClient) error {
	var err error
	for err == nil {
		_, err = c.Recv()
	}
	return err
}
