package vcs_test

import (
	"context"
	"fmt"
	"io"
	"net/http"
	"net/http/httptest"
	"net/url"
	"os"
	"strings"
	"testing"

	"github.com/drmaxgit/go-azuredevops/azuredevops"
	"github.com/runatlantis/atlantis/server/events/models"
	"github.com/runatlantis/atlantis/server/events/vcs"
	"github.com/runatlantis/atlantis/server/events/vcs/testdata"
	"github.com/runatlantis/atlantis/server/logging"
	. "github.com/runatlantis/atlantis/testing"
)

func TestAzureDevopsClient_MergePull(t *testing.T) {
	logger := logging.NewNoopLogger(t)
	cases := []struct {
		description string
		response    string
		code        int
		expErr      string
	}{
		{
			"success",
			adMergeSuccess,
			200,
			"",
		},
		{
			"405",
			`{"message":"405 Method Not Allowed"}`,
			405,
			"405 {message: 405 Method Not Allowed}",
		},
		{
			"406",
			`{"message":"406 Branch cannot be merged"}`,
			406,
			"406 {message: 406 Branch cannot be merged}",
		},
	}

	// Set default pull request completion options
	mcm := azuredevops.NoFastForward.String()
	twi := new(bool)
	*twi = true
	completionOptions := azuredevops.GitPullRequestCompletionOptions{
		BypassPolicy:            new(bool),
		BypassReason:            azuredevops.String(""),
		DeleteSourceBranch:      new(bool),
		MergeCommitMessage:      azuredevops.String("commit message"),
		MergeStrategy:           &mcm,
		SquashMerge:             new(bool),
		TransitionWorkItems:     twi,
		TriggeredByAutoComplete: new(bool),
	}

	id := azuredevops.IdentityRef{}
	pull := azuredevops.GitPullRequest{
		PullRequestID: azuredevops.Int(22),
	}

	userIDResponse := `{
		"members": [
			{
				"id": "6416203b-98bb-4910-8f8a-b12aa19a399f"
			}
		],
		"continuationToken": null,
		"totalCount": 0,
		"items": [
			{
				"id": "6416203b-98bb-4910-8f8a-b12aa19a399f"
			}
		]
	}`

	for _, c := range cases {
		t.Run(c.description, func(t *testing.T) {
			testServer := httptest.NewTLSServer(
				http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
					switch r.RequestURI {
					// The first request should hit this URL.
					case "/owner/project/_apis/git/repositories/repo/pullrequests/22?api-version=5.1-preview.1":
						w.WriteHeader(c.code)
						w.Write([]byte(c.response)) // nolint: errcheck
					case "/owner/_apis/userentitlements?$filter=name+eq+'user'&$api-version=6.0-preview.3":
						w.WriteHeader(c.code)
						w.Write([]byte(userIDResponse)) // nolint: errcheck
					default:
						t.Errorf("got unexpected request at %q", r.RequestURI)
						http.Error(w, "not found", http.StatusNotFound)
					}
				}))

			testServerURL, err := url.Parse(testServer.URL)
			Ok(t, err)
			client, err := vcs.NewAzureDevopsClient(testServerURL.Host, "user", "token")
			client.Client.VsaexBaseURL = *testServerURL
			Ok(t, err)
			defer disableSSLVerification()()

			merge, _, err := client.Client.PullRequests.Merge(context.Background(),
				"owner",
				"project",
				"repo",
				pull.GetPullRequestID(),
				&pull,
				completionOptions,
				id,
			)

			if err != nil {
				fmt.Printf("Merge failed: %+v\n", err)
				return
			}
			fmt.Printf("Successfully merged pull request: %+v\n", merge)

			err = client.MergePull(
				logger,
				models.PullRequest{
					Num: 22,
					BaseRepo: models.Repo{
						FullName: "owner/project/repo",
						Owner:    "owner",
						Name:     "repo",
					},
				}, models.PullRequestOptions{
					DeleteSourceBranchOnMerge: false,
				})
			if c.expErr == "" {
				Ok(t, err)
			} else {
				ErrContains(t, c.expErr, err)
				ErrContains(t, "unable to merge merge request, it may not be in a mergeable state", err)
			}
		})
	}
}

func TestAzureDevopsClient_UpdateStatus(t *testing.T) {
	logger := logging.NewNoopLogger(t)
	cases := []struct {
		status             models.CommitStatus
		expState           string
		supportsIterations bool
	}{
		{
			models.PendingCommitStatus,
			"pending",
			true,
		},
		{
			models.SuccessCommitStatus,
			"succeeded",
			true,
		},
		{
			models.FailedCommitStatus,
			"failed",
			true,
		},
		{
			models.PendingCommitStatus,
			"pending",
			false,
		},
		{
			models.SuccessCommitStatus,
			"succeeded",
			false,
		},
		{
			models.FailedCommitStatus,
			"failed",
			false,
		},
	}
	iterResponse := `{"count": 2, "value": [{"id": 1, "sourceRefCommit": { "commitId": "oldsha"}}, {"id": 2, "sourceRefCommit": { "commitId": "sha"}}]}`
	prResponse := `{"supportsIterations": %t}`
	partResponse := `{"context":{"genre":"Atlantis Bot","name":"src"},"description":"description","state":"%s","targetUrl":"https://google.com"`

	for _, c := range cases {
		prResponse := fmt.Sprintf(prResponse, c.supportsIterations)
		t.Run(c.expState, func(t *testing.T) {
			gotRequest := false
			testServer := httptest.NewTLSServer(
				http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
					switch r.RequestURI {
					case "/owner/project/_apis/git/repositories/repo/pullrequests/22/statuses?api-version=5.1-preview.1":
						gotRequest = true
						defer r.Body.Close() // nolint: errcheck
						body, err := io.ReadAll(r.Body)
						Ok(t, err)
						exp := fmt.Sprintf(partResponse, c.expState)
						if c.supportsIterations == true {
							exp = fmt.Sprintf("%s%s}\n", exp, `,"iterationId":2`)
						} else {
							exp = fmt.Sprintf("%s}\n", exp)
						}
						Equals(t, exp, string(body))
						w.Write([]byte(exp)) // nolint: errcheck
					case "/owner/project/_apis/git/repositories/repo/pullrequests/22/iterations?api-version=5.1":
						w.Write([]byte(iterResponse)) // nolint: errcheck
					case "/owner/project/_apis/git/pullrequests/22?api-version=5.1-preview.1":
						w.Write([]byte(prResponse)) // nolint: errcheck
					default:
						t.Errorf("got unexpected request at %q", r.RequestURI)
						http.Error(w, "not found", http.StatusNotFound)
					}
				}))

			testServerURL, err := url.Parse(testServer.URL)
			Ok(t, err)
			client, err := vcs.NewAzureDevopsClient(testServerURL.Host, "user", "token")
			Ok(t, err)
			defer disableSSLVerification()()

			repo := models.Repo{
				FullName: "owner/project/repo",
				Owner:    "owner",
				Name:     "repo",
			}
			err = client.UpdateStatus(
				logger,
				repo,
				models.PullRequest{
					Num:        22,
					BaseRepo:   repo,
					HeadCommit: "sha",
				}, c.status, "src", "description", "https://google.com")
			Ok(t, err)
			Assert(t, gotRequest, "expected to get the request")
		})
	}
}

// GetModifiedFiles should make multiple requests if more than one page
// and concat results.
func TestAzureDevopsClient_GetModifiedFiles(t *testing.T) {
	logger := logging.NewNoopLogger(t)
	itemRespTemplate := `{
		"changes": [
	{
		"item": {
			"gitObjectType": "blob",
			"path": "%s",
			"url": "https://dev.azure.com/fabrikam/_apis/git/repositories/278d5cd2-584d-4b63-824a-2ba458937249/items/MyWebSite/MyWebSite/%s?versionType=Commit"
		},
		"changeType": "add"
	},
	{
		"item": {
			"gitObjectType": "blob",
			"path": "%s",
			"url": "https://dev.azure.com/fabrikam/_apis/git/repositories/278d5cd2-584d-4b63-824a-2ba458937249/items/MyWebSite/MyWebSite/%s?versionType=Commit"
		},
		"changeType": "add"
	}
]}`
	resp := fmt.Sprintf(itemRespTemplate, "/file1.txt", "/file1.txt", "/file2.txt", "/file2.txt")
	testServer := httptest.NewTLSServer(
		http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
			switch r.RequestURI {
			// The first request should hit this URL.
			case "/owner/project/_apis/git/repositories/repo/pullrequests/1?api-version=5.1-preview.1&includeWorkItemRefs=true":
				w.Write([]byte(testdata.ADPullJSON)) // nolint: errcheck
			// The second should hit this URL.
			case "/owner/project/_apis/git/repositories/repo/diffs/commits?%24top=100&api-version=5.1&baseVersion=new_feature&targetVersion=npaulk%2Fmy_work":
				// We write a header that means there's an additional page.
				w.Write([]byte(resp)) // nolint: errcheck
				return
			default:
				t.Errorf("got unexpected request at %q", r.RequestURI)
				http.Error(w, "not found", http.StatusNotFound)
				return
			}
		}))

	testServerURL, err := url.Parse(testServer.URL)
	Ok(t, err)
	client, err := vcs.NewAzureDevopsClient(testServerURL.Host, "user", "token")
	Ok(t, err)
	defer disableSSLVerification()()

	files, err := client.GetModifiedFiles(
		logger,
		models.Repo{
			FullName:          "owner/project/repo",
			Owner:             "owner",
			Name:              "repo",
			CloneURL:          "",
			SanitizedCloneURL: "",
			VCSHost: models.VCSHost{
				Type:     models.AzureDevops,
				Hostname: "dev.azure.com",
			},
		}, models.PullRequest{
			Num: 1,
		})
	Ok(t, err)
	Equals(t, []string{"file1.txt", "file2.txt"}, files)
}

func TestAzureDevopsClient_PullIsMergeable(t *testing.T) {
	logger := logging.NewNoopLogger(t)
	type Policy struct {
		genre  string
		name   string
		status string
	}
	cases := []struct {
		testName     string
		mergeStatus  string
		policy       Policy
		expMergeable bool
	}{
		{
			"merge conflicts",
			azuredevops.MergeConflicts.String(),
			Policy{
				"Not Atlantis",
				"foo",
				"approved",
			},
			false,
		},
		{
			"rejected policy status",
			azuredevops.MergeSucceeded.String(),
			Policy{
				"Not Atlantis",
				"foo",
				"rejected",
			},
			false,
		},
		{
			"merge succeeded",
			azuredevops.MergeSucceeded.String(),
			Policy{
				"Not Atlantis",
				"foo",
				"approved",
			},
			true,
		},
		{
			"pending policy status",
			azuredevops.MergeSucceeded.String(),
			Policy{
				"Not Atlantis",
				"foo",
				"pending",
			},
			false,
		},
		{
			"atlantis apply status rejected",
			azuredevops.MergeSucceeded.String(),
			Policy{
				"Atlantis Bot/atlantis",
				"apply",
				"rejected",
			},
			true,
		},
	}

	jsonPullRequestBytes, err := os.ReadFile("testdata/azuredevops-pr.json")
	Ok(t, err)

	jsonPolicyEvaluationBytes, err := os.ReadFile("testdata/azuredevops-policyevaluations.json")
	Ok(t, err)

	pullRequestBody := string(jsonPullRequestBytes)
	policyEvaluationsBody := string(jsonPolicyEvaluationBytes)

	for _, c := range cases {
		t.Run(c.testName, func(t *testing.T) {
			pullRequestResponse := strings.Replace(pullRequestBody, `"mergeStatus": "notSet"`, fmt.Sprintf(`"mergeStatus": "%s"`, c.mergeStatus), 1)
			policyEvaluationsResponse := strings.Replace(policyEvaluationsBody, `"status": "approved"`, fmt.Sprintf(`"status": "%s"`, c.policy.status), 1)
			policyEvaluationsResponse = strings.Replace(policyEvaluationsResponse, `"statusGenre": "Atlantis Bot/atlantis"`, fmt.Sprintf(`"statusGenre": "%s"`, c.policy.genre), 1)
			policyEvaluationsResponse = strings.Replace(policyEvaluationsResponse, `"statusName": "plan"`, fmt.Sprintf(`"statusName": "%s"`, c.policy.name), 1)

			testServer := httptest.NewTLSServer(
				http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
					switch r.RequestURI {
					case "/owner/project/_apis/git/repositories/repo/pullrequests/1?api-version=5.1-preview.1&includeWorkItemRefs=true":
						w.Write([]byte(pullRequestResponse)) // nolint: errcheck
						return
					case "/owner/project/_apis/policy/evaluations?api-version=5.1-preview&artifactId=vstfs%3A%2F%2F%2FCodeReview%2FCodeReviewId%2F33333333-3333-3333-333333333333%2F1":
						w.Write([]byte(policyEvaluationsResponse)) // nolint: errcheck
						return
					default:
						t.Errorf("got unexpected request at %q", r.RequestURI)
						http.Error(w, "not found", http.StatusNotFound)
						return
					}
				}))

			testServerURL, err := url.Parse(testServer.URL)
			Ok(t, err)

			client, err := vcs.NewAzureDevopsClient(testServerURL.Host, "user", "token")
			Ok(t, err)

			defer disableSSLVerification()()

			actMergeable, err := client.PullIsMergeable(
				logger,
				models.Repo{
					FullName:          "owner/project/repo",
					Owner:             "owner",
					Name:              "repo",
					CloneURL:          "",
					SanitizedCloneURL: "",
					VCSHost: models.VCSHost{
						Type:     models.AzureDevops,
						Hostname: "dev.azure.com",
					},
				}, models.PullRequest{
					Num: 1,
				}, "atlantis-test", []string{})
			Ok(t, err)
			Equals(t, c.expMergeable, actMergeable)
		})
	}
}

func TestAzureDevopsClient_PullIsApproved(t *testing.T) {
	logger := logging.NewNoopLogger(t)
	cases := []struct {
		testName           string
		reviewerUniqueName string
		reviewerVote       int
		expApproved        bool
	}{
		{
			"approved",
			"atlantis.reviewer@example.com",
			azuredevops.VoteApproved,
			true,
		},
		{
			"approved with suggestions",
			"atlantis.reviewer@example.com",
			azuredevops.VoteApprovedWithSuggestions,
			true,
		},
		{
			"no vote",
			"atlantis.reviewer@example.com",
			azuredevops.VoteNone,
			false,
		},
		{
			"vote waiting for author",
			"atlantis.reviewer@example.com",
			azuredevops.VoteWaitingForAuthor,
			false,
		},
		{
			"vote rejected",
			"atlantis.reviewer@example.com",
			azuredevops.VoteRejected,
			false,
		},
		{
			"approved only by author",
			"atlantis.author@example.com",
			azuredevops.VoteApproved,
			false,
		},
	}

	jsBytes, err := os.ReadFile("testdata/azuredevops-pr.json")
	Ok(t, err)

	json := string(jsBytes)
	for _, c := range cases {
		t.Run(c.testName, func(t *testing.T) {
			response := strings.Replace(json, `"vote": 0,`, fmt.Sprintf(`"vote": %d,`, c.reviewerVote), 1)
			response = strings.Replace(response, "atlantis.reviewer@example.com", c.reviewerUniqueName, 1)

			testServer := httptest.NewTLSServer(
				http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
					switch r.RequestURI {
					case "/owner/project/_apis/git/repositories/repo/pullrequests/1?api-version=5.1-preview.1&includeWorkItemRefs=true":
						w.Write([]byte(response)) // nolint: errcheck
						return
					default:
						t.Errorf("got unexpected request at %q", r.RequestURI)
						http.Error(w, "not found", http.StatusNotFound)
						return
					}
				}))

			testServerURL, err := url.Parse(testServer.URL)
			Ok(t, err)

			client, err := vcs.NewAzureDevopsClient(testServerURL.Host, "user", "token")
			Ok(t, err)

			defer disableSSLVerification()()

			approvalStatus, err := client.PullIsApproved(
				logger,
				models.Repo{
					FullName:          "owner/project/repo",
					Owner:             "owner",
					Name:              "repo",
					CloneURL:          "",
					SanitizedCloneURL: "",
					VCSHost: models.VCSHost{
						Type:     models.AzureDevops,
						Hostname: "dev.azure.com",
					},
				}, models.PullRequest{
					Num: 1,
				})
			Ok(t, err)
			Equals(t, c.expApproved, approvalStatus.IsApproved)
		})
	}
}

func TestAzureDevopsClient_GetPullRequest(t *testing.T) {
	logger := logging.NewNoopLogger(t)
	// Use a real Azure DevOps json response and edit the mergeable_state field.
	jsBytes, err := os.ReadFile("testdata/azuredevops-pr.json")
	Ok(t, err)
	response := string(jsBytes)

	t.Run("get pull request", func(t *testing.T) {
		testServer := httptest.NewTLSServer(
			http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
				switch r.RequestURI {
				case "/owner/project/_apis/git/repositories/repo/pullrequests/1?api-version=5.1-preview.1&includeWorkItemRefs=true":
					w.Write([]byte(response)) // nolint: errcheck
					return
				default:
					t.Errorf("got unexpected request at %q", r.RequestURI)
					http.Error(w, "not found", http.StatusNotFound)
					return
				}
			}))
		testServerURL, err := url.Parse(testServer.URL)
		Ok(t, err)
		client, err := vcs.NewAzureDevopsClient(testServerURL.Host, "user", "token")
		Ok(t, err)
		defer disableSSLVerification()()

		_, err = client.GetPullRequest(
			logger,
			models.Repo{
				FullName:          "owner/project/repo",
				Owner:             "owner",
				Name:              "repo",
				CloneURL:          "",
				SanitizedCloneURL: "",
				VCSHost: models.VCSHost{
					Type:     models.AzureDevops,
					Hostname: "dev.azure.com",
				},
			}, 1)
		Ok(t, err)
	})
}

func TestAzureDevopsClient_MarkdownPullLink(t *testing.T) {
	client, err := vcs.NewAzureDevopsClient("hostname", "user", "token")
	Ok(t, err)
	pull := models.PullRequest{Num: 1}
	s, _ := client.MarkdownPullLink(pull)
	exp := "!1"
	Equals(t, exp, s)
}

var adMergeSuccess = `{
	"status": "completed",
	"mergeStatus": "succeeded",
	"autoCompleteSetBy": {
					"id": "54d125f7-69f7-4191-904f-c5b96b6261c8",
					"displayName": "Jamal Hartnett",
					"uniqueName": "fabrikamfiber4@hotmail.com",
					"url": "https://vssps.dev.azure.com/fabrikam/_apis/Identities/54d125f7-69f7-4191-904f-c5b96b6261c8",
					"imageUrl": "https://dev.azure.com/fabrikam/DefaultCollection/_api/_common/identityImage?id=54d125f7-69f7-4191-904f-c5b96b6261c8"
	},
	"pullRequestId": 22,
	"completionOptions": {
					"bypassPolicy":false,
					"bypassReason":"",
					"deleteSourceBranch":false,
					"mergeCommitMessage":"TEST MERGE COMMIT MESSAGE",
					"mergeStrategy":"noFastForward",
					"squashMerge":false,
					"transitionWorkItems":true,
					"triggeredByAutoComplete":false
	}
}
`

func TestAzureDevopsClient_GitStatusContextFromSrc(t *testing.T) {
	cases := []struct {
		src      string
		expGenre string
		expName  string
	}{
		{
			"atlantis/plan",
			"Atlantis Bot/atlantis",
			"plan",
		},
		{
			"atlantis/foo/bar/biz/baz",
			"Atlantis Bot/atlantis/foo/bar/biz",
			"baz",
		},
		{
			"foo",
			"Atlantis Bot",
			"foo",
		},
		{
			"",
			"Atlantis Bot",
			"",
		},
	}

	for _, c := range cases {
		result := vcs.GitStatusContextFromSrc(c.src)
		expName := c.expName
		expGenre := c.expGenre
		Equals(t, &expName, result.Name)
		Equals(t, &expGenre, result.Genre)
	}
}
