package common_test

import (
	"errors"
	"fmt"
	"os"
	"os/exec"
	"path"
	"strings"
	"testing"

	"go.uber.org/mock/gomock"
	"src.opensuse.org/autogits/common"
	"src.opensuse.org/autogits/common/gitea-generated/models"
	mock_common "src.opensuse.org/autogits/common/mock"
)

func TestPR(t *testing.T) {
	baseConfig := common.AutogitConfig{
		Reviewers:      []string{"+super1", "*super2", "m1", "-m2"},
		Branch:         "branch",
		Organization:   "foo",
		GitProjectName: "barPrj",
	}

	type prdata struct {
		pr           *models.PullRequest
		pr_err       error
		reviews      []*models.PullReview
		review_error error
	}

	tests := []struct {
		name      string
		data      []prdata
		api_error string

		resLen        int
		reviewed      bool
		consistentSet bool
		prjGitPRIndex int

		reviewSetFetcher func(*mock_common.MockGiteaPRFetcher) (*common.PRSet, error)
	}{
		{
			name: "Error fetching PullRequest",
			data: []prdata{
				{pr: &models.PullRequest{Body: "", Index: 42, Base: &models.PRBranchInfo{Repo: &models.Repository{Name: "repo", Owner: &models.User{UserName: "test"}}}}, pr_err: errors.New("Missing PR")},
			},
			prjGitPRIndex: -1,
		},
		{
			name: "Error fetching PullRequest in PrjGit",
			data: []prdata{
				{pr: &models.PullRequest{Body: "PR: foo/barPrj#22", Index: 42, Base: &models.PRBranchInfo{Repo: &models.Repository{Name: "repo", Owner: &models.User{UserName: "test"}}}}, pr_err: errors.New("missing PR")},
				{pr: &models.PullRequest{Body: "", Index: 22, Base: &models.PRBranchInfo{Repo: &models.Repository{Name: "barPrj", Owner: &models.User{UserName: "foo"}}}}},
			},
		},
		{
			name: "Error fetching prjgit",
			data: []prdata{
				{pr: &models.PullRequest{Body: "", Index: 42, Base: &models.PRBranchInfo{Repo: &models.Repository{Name: "repo", Owner: &models.User{UserName: "test"}}}}},
			},
			resLen:        1,
			prjGitPRIndex: -1,
		},
		{
			name: "Review set is consistent",
			data: []prdata{
				{pr: &models.PullRequest{Body: "", Index: 42, Base: &models.PRBranchInfo{Repo: &models.Repository{Name: "repo", Owner: &models.User{UserName: "test"}}}}},
				{pr: &models.PullRequest{Body: "PR: test/repo#42", Index: 22, Base: &models.PRBranchInfo{Repo: &models.Repository{Name: "barPrj", Owner: &models.User{UserName: "foo"}}}}},
			},
			resLen:        2,
			prjGitPRIndex: 1,
			consistentSet: true,
		},

		{
			name: "Review set is consistent: 1pkg",
			data: []prdata{
				{pr: &models.PullRequest{Body: "PR: foo/barPrj#22", Index: 42, Base: &models.PRBranchInfo{Repo: &models.Repository{Name: "repo", Owner: &models.User{UserName: "test"}}}}},
				{pr: &models.PullRequest{Body: "PR: test/repo#42", Index: 22, Base: &models.PRBranchInfo{Repo: &models.Repository{Name: "barPrj", Owner: &models.User{UserName: "foo"}}}}},
			},
			resLen:        2,
			prjGitPRIndex: 1,
			consistentSet: true,
		},
		{
			name: "Review set is consistent: 2pkg",
			data: []prdata{
				{pr: &models.PullRequest{Body: "some desc", Index: 42, Base: &models.PRBranchInfo{Repo: &models.Repository{Name: "repo", Owner: &models.User{UserName: "test"}}}}},
				{pr: &models.PullRequest{Body: "PR: test/repo#42\nPR: test/repo2#41", Index: 22, Base: &models.PRBranchInfo{Repo: &models.Repository{Name: "barPrj", Owner: &models.User{UserName: "foo"}}}}},
				{pr: &models.PullRequest{Body: "some other desc\nPR: foo/fer#33", Index: 41, Base: &models.PRBranchInfo{Repo: &models.Repository{Name: "repo2", Owner: &models.User{UserName: "test"}}}}},
			},
			resLen:        3,
			prjGitPRIndex: 1,
			consistentSet: true,
		},
		{
			name: "Review set of prjgit PR is consistent",
			data: []prdata{
				{
					pr: &models.PullRequest{Body: "", Index: 42, Base: &models.PRBranchInfo{Repo: &models.Repository{Name: "barPrj", Owner: &models.User{UserName: "foo"}}}},
					reviews: []*models.PullReview{
						{Body: "LGTM", User: &models.User{UserName: "m2"}, State: common.ReviewStateApproved},
						{Body: "LGTM", User: &models.User{UserName: "super2"}, State: common.ReviewStateApproved},
						{Body: "LGTM", User: &models.User{UserName: common.Bot_BuildReview}, State: common.ReviewStateApproved},
					},
				},
			},
			resLen:        1,
			prjGitPRIndex: 0,
			consistentSet: true,
			reviewed:      true,
			reviewSetFetcher: func(mock *mock_common.MockGiteaPRFetcher) (*common.PRSet, error) {
				return common.FetchPRSet(mock, "foo", "barPrj", 42, &baseConfig)
			},
		},
		{
			name: "Review set is consistent: 2pkg",
			data: []prdata{
				{pr: &models.PullRequest{Body: "PR: foo/barPrj#222", Index: 42, Base: &models.PRBranchInfo{Repo: &models.Repository{Name: "repo", Owner: &models.User{UserName: "test"}}}}},
				{pr: &models.PullRequest{Body: "PR: test/repo2#41", Index: 20, Base: &models.PRBranchInfo{Repo: &models.Repository{Name: "barPrj", Owner: &models.User{UserName: "foo"}}}}},
				{pr: &models.PullRequest{Body: "PR: test/repo#42\nPR: test/repo2#41", Index: 22, Base: &models.PRBranchInfo{Repo: &models.Repository{Name: "barPrj", Owner: &models.User{UserName: "foo"}}}}},
				{pr: &models.PullRequest{Body: "PR: foo/barPrj#20", Index: 41, Base: &models.PRBranchInfo{Repo: &models.Repository{Name: "repo2", Owner: &models.User{UserName: "test"}}}}},
			},
			resLen:        3,
			prjGitPRIndex: 2,
			consistentSet: true,
		},
	}

	for _, test := range tests {
		t.Run(test.name, func(t *testing.T) {
			ctl := gomock.NewController(t)
			pr_mock := mock_common.NewMockGiteaPRFetcher(ctl)
			review_mock := mock_common.NewMockGiteaPRChecker(ctl)
			//				reviewer_mock := mock_common.NewMockGiteaReviewRequester(ctl)

			if test.reviewSetFetcher == nil { // if we are fetching the prjgit directly, the these mocks are not called
				if test.prjGitPRIndex >= 0 {
					pr_mock.EXPECT().GetAssociatedPrjGitPR(baseConfig.Organization, baseConfig.GitProjectName, test.data[0].pr.Base.Repo.Owner.UserName, test.data[0].pr.Base.Repo.Name, test.data[0].pr.Index).
						Return(test.data[test.prjGitPRIndex].pr, test.data[test.prjGitPRIndex].pr_err)
				} else if test.prjGitPRIndex < 0 {
					// no prjgit PR
					pr_mock.EXPECT().GetAssociatedPrjGitPR(baseConfig.Organization, baseConfig.GitProjectName, test.data[0].pr.Base.Repo.Owner.UserName, test.data[0].pr.Base.Repo.Name, test.data[0].pr.Index).
						Return(nil, nil)
				}
			}

			var test_err error
			for _, data := range test.data {
				pr_mock.EXPECT().GetPullRequest(data.pr.Base.Repo.Owner.UserName, data.pr.Base.Repo.Name, data.pr.Index).Return(data.pr, data.pr_err).AnyTimes()
				if data.pr_err != nil {
					test_err = data.pr_err
				}
				review_mock.EXPECT().GetPullRequestReviews(data.pr.Base.Repo.Owner.UserName, data.pr.Base.Repo.Name, data.pr.Index).Return(data.reviews, data.review_error).AnyTimes()
			}

			var res *common.PRSet
			var err error

			if test.reviewSetFetcher != nil {
				res, err = test.reviewSetFetcher(pr_mock)
			} else {
				res, err = common.FetchPRSet(pr_mock, "test", "repo", 42, &baseConfig)
			}

			if err == nil {
				if test_err != nil {
					t.Fatal("Expected", test_err, "but got", err)
				}
			} else {
				if res != nil {
					t.Fatal("error but got ReviewSet?")
				}

				if test.api_error != "" {
					if err.Error() != test.api_error {
						t.Fatal("expected", test.api_error, "but got", err)
					}
				} else if test_err != err {
					t.Fatal("expected", test_err, "but got", err)
				}
				return
			}

			if test.resLen != len(res.PRs) {
				t.Error("expected result len", test.resLen, "but got", len(res.PRs))
			}

			PrjGitPR, err := res.GetPrjGitPR()
			if test.prjGitPRIndex < 0 {
				if err == nil {
					t.Error("expected error, but nothing")
				}
			}
			pr_found := false
			if test.prjGitPRIndex >= 0 {
				for i := range test.data {
					if PrjGitPR == test.data[i].pr && i == test.prjGitPRIndex {
						t.Log("found at index", i)
						pr_found = true
					}
				}
				if !pr_found {
					t.Error("Cannot find expected PrjGit location in PR set", PrjGitPR)
				}
			} else {
				if PrjGitPR != nil {
					t.Log("Expected prjgit not found, but found?", PrjGitPR)
				}
			}

			if isConsistent := res.IsConsistent(); isConsistent != test.consistentSet {
				t.Error("IsConsistent() returned unexpected:", isConsistent)
			}
			/*
				if err := res.AssignReviewers(reviewer_mock); err != nil {
					t.Error("expected no errors assigning reviewers:", err)
				}
			*/

			maintainers := mock_common.NewMockMaintainershipData(ctl)
			maintainers.EXPECT().IsApproved(gomock.Any(), gomock.Any()).Return(true).AnyTimes()

			if isApproved := res.IsApproved(review_mock, maintainers); isApproved != test.reviewed {
				t.Error("expected reviewed to be NOT", isApproved)
			}
		})
	}
}

func TestPRAssignReviewers(t *testing.T) {

	tests := []struct {
		name      string
		config    common.AutogitConfig
		reviewers []struct {
			org, repo string
			num       int64
			reviewer  string
		}

		pkgReviews []*models.PullReview
		prjReviews []*models.PullReview

		expectedReviewerCall [2][]string
	}{
		{
			name: "No reviewers",
			config: common.AutogitConfig{
				GitProjectName: "repo",
				Organization:   "org",
				Branch:         "main",
				Reviewers:      []string{},
			},
			expectedReviewerCall: [2][]string{{"autogits_obs_staging_bot"}, {"prjmaintainer", "pkgmaintainer"}},
		},
		{
			name: "One project reviewer only",
			config: common.AutogitConfig{
				GitProjectName: "repo",
				Organization:   "org",
				Branch:         "main",
				Reviewers:      []string{"-user1"},
			},
			expectedReviewerCall: [2][]string{{"user1", "autogits_obs_staging_bot"}, {"prjmaintainer", "pkgmaintainer"}},
		},
		{
			name: "One project reviewer and one pkg reviewer only",
			config: common.AutogitConfig{
				GitProjectName: "repo",
				Organization:   "org",
				Branch:         "main",
				Reviewers:      []string{"-user1", "user2"},
			},
			expectedReviewerCall: [2][]string{{"user1", "autogits_obs_staging_bot"}, {"user2", "prjmaintainer", "pkgmaintainer"}},
		},
		{
			name: "No need to get reviews of submitter",
			config: common.AutogitConfig{
				GitProjectName: "repo",
				Organization:   "org",
				Branch:         "main",
				Reviewers:      []string{"-user1", "submitter"},
			},
			expectedReviewerCall: [2][]string{{"user1", "autogits_obs_staging_bot"}, {"prjmaintainer", "pkgmaintainer"}},
		},
		{
			name: "Reviews are done",
			config: common.AutogitConfig{
				GitProjectName: "repo",
				Organization:   "org",
				Branch:         "main",
				Reviewers:      []string{"-user1", "user2"},
			},
			pkgReviews: []*models.PullReview{
				{
					State: common.ReviewStateApproved,
					User:  &models.User{UserName: "user2"},
				},
				{
					State: common.ReviewStateApproved,
					User:  &models.User{UserName: "pkgmaintainer"},
				},
				{
					State: common.ReviewStatePending,
					User:  &models.User{UserName: "prjmaintainer"},
				},
			},
			prjReviews: []*models.PullReview{
				{
					State: common.ReviewStateRequestChanges,
					User:  &models.User{UserName: "user1"},
				},
				{
					State: common.ReviewStateRequestReview,
					User:  &models.User{UserName: "autogits_obs_staging_bot"},
				},
			},
			expectedReviewerCall: [2][]string{},
		},
		{
			name: "Stale review is not done, re-request it",
			config: common.AutogitConfig{
				GitProjectName: "repo",
				Organization:   "org",
				Branch:         "main",
				Reviewers:      []string{"-user1", "user2"},
			},
			pkgReviews: []*models.PullReview{
				{
					State: common.ReviewStateApproved, 
					User: &models.User{UserName: "user2"},
				},
				{
					State: common.ReviewStatePending,
					User:  &models.User{UserName: "prjmaintainer"},
				},
			},
			prjReviews: []*models.PullReview{
				{
					State: common.ReviewStateRequestChanges,
					User:  &models.User{UserName: "user1"},
					Stale: true,
				},
				{
					State: common.ReviewStateRequestReview,
					Stale: true,
					User:  &models.User{UserName: "autogits_obs_staging_bot"},
				},
			},
			expectedReviewerCall: [2][]string{{"user1", "autogits_obs_staging_bot"}, {"pkgmaintainer"}},
		},
	}

	for _, test := range tests {
		t.Run(test.name, func(t *testing.T) {
			ctl := gomock.NewController(t)
			pr_mock := mock_common.NewMockGiteaPRFetcher(ctl)
			review_mock := mock_common.NewMockGiteaReviewFetcherAndRequester(ctl)
			maintainership_mock := mock_common.NewMockMaintainershipData(ctl)

			pr_mock.EXPECT().GetPullRequest("other", "pkgrepo", int64(1)).Return(&models.PullRequest{
				Body:               "Some description is here",
				User:               &models.User{UserName: "submitter"},
				RequestedReviewers: []*models.User{},
				Base:               &models.PRBranchInfo{Repo: &models.Repository{Name: "pkgrepo", Owner: &models.User{UserName: "other"}}},
				Head:               &models.PRBranchInfo{},
				Index:              1,
			}, nil)
			review_mock.EXPECT().GetPullRequestReviews("other", "pkgrepo", int64(1)).Return(test.pkgReviews, nil)
			pr_mock.EXPECT().GetAssociatedPrjGitPR("org", "repo", "other", "pkgrepo", int64(1)).Return(&models.PullRequest{
				Body:               fmt.Sprintf(common.PrPattern, "other", "pkgrepo", 1),
				User:               &models.User{UserName: "bot1"},
				RequestedReviewers: []*models.User{{UserName: "main_reviewer"}},
				Base:               &models.PRBranchInfo{Repo: &models.Repository{Name: "repo", Owner: &models.User{UserName: "org"}}},
				Head:               &models.PRBranchInfo{},
				Index:              42,
			}, nil)
			review_mock.EXPECT().GetPullRequestReviews("org", "repo", int64(42)).Return(test.prjReviews, nil)

			maintainership_mock.EXPECT().ListProjectMaintainers().Return([]string{"prjmaintainer"}).AnyTimes()
			maintainership_mock.EXPECT().ListPackageMaintainers("pkgrepo").Return([]string{"pkgmaintainer"}).AnyTimes()

			prs, _ := common.FetchPRSet(pr_mock, "other", "pkgrepo", int64(1), &test.config)
			if len(prs.PRs) != 2 {
				t.Fatal("PRs not fetched")
			}
			for _, pr := range prs.PRs {
				r := test.expectedReviewerCall[0]
				if !prs.IsPrjGitPR(pr.PR) {
					r = test.expectedReviewerCall[1]
				}
				if len(r) > 0 {
					review_mock.EXPECT().RequestReviews(pr.PR, r).Return(nil, nil)
				}
			}
			prs.AssignReviewers(review_mock, maintainership_mock)
		})
	}

	prjgit_tests := []struct {
		name      string
		config    common.AutogitConfig
		reviewers []struct {
			org, repo string
			num       int64
			reviewer  string
		}

		prjReviews []*models.PullReview

		expectedReviewerCall [2][]string
	}{
		{
			name: "PrjMaintainers in prjgit review when not part of pkg set",
			config: common.AutogitConfig{
				GitProjectName: "repo",
				Organization:   "org",
				Branch:         "main",
				Reviewers:      []string{},
			},
			expectedReviewerCall: [2][]string{{"autogits_obs_staging_bot", "prjmaintainer"}},
		},
	}
	for _, test := range prjgit_tests {
		t.Run(test.name, func(t *testing.T) {
			ctl := gomock.NewController(t)
			pr_mock := mock_common.NewMockGiteaPRFetcher(ctl)
			review_mock := mock_common.NewMockGiteaReviewFetcherAndRequester(ctl)
			maintainership_mock := mock_common.NewMockMaintainershipData(ctl)

			pr_mock.EXPECT().GetPullRequest("org", "repo", int64(1)).Return(&models.PullRequest{
				Body:               "Some description is here",
				User:               &models.User{UserName: "submitter"},
				RequestedReviewers: []*models.User{},
				Base:               &models.PRBranchInfo{Repo: &models.Repository{Name: "repo", Owner: &models.User{UserName: "org"}}},
				Head:               &models.PRBranchInfo{},
				Index:              1,
			}, nil)
			review_mock.EXPECT().GetPullRequestReviews("org", "repo", int64(1)).Return(test.prjReviews, nil)

			maintainership_mock.EXPECT().ListProjectMaintainers().Return([]string{"prjmaintainer"}).AnyTimes()

			prs, _ := common.FetchPRSet(pr_mock, "org", "repo", int64(1), &test.config)
			if len(prs.PRs) != 1 {
				t.Fatal("PRs not fetched")
			}
			for _, pr := range prs.PRs {
				r := test.expectedReviewerCall[0]
				if !prs.IsPrjGitPR(pr.PR) {
					t.Fatal("only prjgit pr here")
				}
				if len(r) > 0 {
					review_mock.EXPECT().RequestReviews(pr.PR, r).Return(nil, nil)
				}
			}
			prs.AssignReviewers(review_mock, maintainership_mock)
		})
	}
}

func TestPRMerge(t *testing.T) {
	cwd, _ := os.Getwd()
	cmd := exec.Command("/usr/bin/bash", path.Join(cwd, "test_repo_setup.sh"))
	cmd.Dir = t.TempDir()
	if out, err := cmd.CombinedOutput(); err != nil {
		t.Fatal(string(out))
	}

	common.ExtraGitParams = []string{
		"GIT_CONFIG_COUNT=1",
		"GIT_CONFIG_KEY_0=protocol.file.allow",
		"GIT_CONFIG_VALUE_0=always",

		"GIT_AUTHOR_NAME=testname",
		"GIT_AUTHOR_EMAIL=test@suse.com",
		"GIT_AUTHOR_DATE='2005-04-07T22:13:13'",
		"GIT_COMMITTER_NAME=testname",
		"GIT_COMMITTER_EMAIL=test@suse.com",
		"GIT_COMMITTER_DATE='2005-04-07T22:13:13'",
	}

	config := &common.AutogitConfig{
		Organization:   "org",
		GitProjectName: "prj",
	}

	tests := []struct {
		name       string
		pr         *models.PullRequest
		mergeError string
	}{
		{
			name: "Merge base not merged in main",

			pr: &models.PullRequest{
				Base: &models.PRBranchInfo{
					Sha: "e8b0de43d757c96a9d2c7101f4bff404e322f53a1fa4041fb85d646110c38ad4", // "base_add_b1"
					Repo: &models.Repository{
						Name: "prj",
						Owner: &models.User{
							UserName: "org",
						},
						SSHURL: path.Join(cmd.Dir, "prjgit"),
					},
				},
				Head: &models.PRBranchInfo{
					Sha: "88584433de1c917c1d773f62b82381848d882491940b5e9b427a540aa9057d9a", // "base_add_b2"
				},
			},
			mergeError: "Aborting merge",
		},
		{
			name: "Merge conflict in modules",

			pr: &models.PullRequest{
				Base: &models.PRBranchInfo{
					Sha: "4fbd1026b2d7462ebe9229a49100c11f1ad6555520a21ba515122d8bc41328a8",
					Repo: &models.Repository{
						Name: "prj",
						Owner: &models.User{
							UserName: "org",
						},
						SSHURL: path.Join(cmd.Dir, "prjgit"),
					},
				},
				Head: &models.PRBranchInfo{
					Sha: "88584433de1c917c1d773f62b82381848d882491940b5e9b427a540aa9057d9a", // "base_add_b2"
				},
			},
		},
	}

	for _, test := range tests {
		t.Run(test.name, func(t *testing.T) {
			ctl := gomock.NewController(t)
			mock := mock_common.NewMockGiteaPRFetcher(ctl)

			mock.EXPECT().GetPullRequest("org", "prj", int64(1)).Return(test.pr, nil)

			set, err := common.FetchPRSet(mock, "org", "prj", 1, config)
			if err != nil {
				t.Fatal(err)
			}

			if err = set.Merge("test", "test@example.com"); err != nil && (test.mergeError == "" || (len(test.mergeError) > 0 && !strings.Contains(err.Error(), test.mergeError))) {
				t.Fatal(err)
			}
		})
	}
}
