package vcs_test

import (
	"crypto/tls"
	"encoding/json"
	"fmt"
	"io"
	"net/http"
	"net/http/httptest"
	"net/url"
	"os"
	"strings"
	"testing"

	"github.com/runatlantis/atlantis/server/events/command"
	"github.com/runatlantis/atlantis/server/events/models"
	"github.com/runatlantis/atlantis/server/events/vcs"
	"github.com/runatlantis/atlantis/server/logging"
	. "github.com/runatlantis/atlantis/testing"

	"github.com/shurcooL/githubv4"
)

// GetModifiedFiles should make multiple requests if more than one page
// and concat results.
func TestGithubClient_GetModifiedFiles(t *testing.T) {
	logger := logging.NewNoopLogger(t)
	respTemplate := `[
  {
    "sha": "bbcd538c8e72b8c175046e27cc8f907076331401",
    "filename": "%s",
    "status": "added",
    "additions": 103,
    "deletions": 21,
    "changes": 124,
    "blob_url": "https://github.com/octocat/Hello-World/blob/6dcb09b5b57875f334f61aebed695e2e4193db5e/file1.txt",
    "raw_url": "https://github.com/octocat/Hello-World/raw/6dcb09b5b57875f334f61aebed695e2e4193db5e/file1.txt",
    "contents_url": "https://api.github.com/repos/octocat/Hello-World/contents/file1.txt?ref=6dcb09b5b57875f334f61aebed695e2e4193db5e",
    "patch": "@@ -132,7 +132,7 @@ module Test @@ -1000,7 +1000,7 @@ module Test"
  }
]`
	firstResp := fmt.Sprintf(respTemplate, "file1.txt")
	secondResp := fmt.Sprintf(respTemplate, "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 "/api/v3/repos/owner/repo/pulls/1/files?per_page=300":
				// We write a header that means there's an additional page.
				w.Header().Add("Link", `<https://api.github.com/resource?page=2>; rel="next",
      <https://api.github.com/resource?page=2>; rel="last"`)
				w.Write([]byte(firstResp)) // nolint: errcheck
				return
				// The second should hit this URL.
			case "/api/v3/repos/owner/repo/pulls/1/files?page=2&per_page=300":
				w.Write([]byte(secondResp)) // nolint: errcheck
			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.NewGithubClient(testServerURL.Host, &vcs.GithubUserCredentials{"user", "pass"}, logger)
	Ok(t, err)
	defer disableSSLVerification()()

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

// GetModifiedFiles should include the source and destination of a moved
// file.
func TestGithubClient_GetModifiedFilesMovedFile(t *testing.T) {
	resp := `[
  {
    "sha": "bbcd538c8e72b8c175046e27cc8f907076331401",
    "filename": "new/filename.txt",
    "previous_filename": "previous/filename.txt",
    "status": "renamed",
    "additions": 103,
    "deletions": 21,
    "changes": 124,
    "blob_url": "https://github.com/octocat/Hello-World/blob/6dcb09b5b57875f334f61aebed695e2e4193db5e/file1.txt",
    "raw_url": "https://github.com/octocat/Hello-World/raw/6dcb09b5b57875f334f61aebed695e2e4193db5e/file1.txt",
    "contents_url": "https://api.github.com/repos/octocat/Hello-World/contents/file1.txt?ref=6dcb09b5b57875f334f61aebed695e2e4193db5e",
    "patch": "@@ -132,7 +132,7 @@ module Test @@ -1000,7 +1000,7 @@ module Test"
  }
]`
	testServer := httptest.NewTLSServer(
		http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
			switch r.RequestURI {
			// The first request should hit this URL.
			case "/api/v3/repos/owner/repo/pulls/1/files?per_page=300":
				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.NewGithubClient(testServerURL.Host, &vcs.GithubUserCredentials{"user", "pass"}, logging.NewNoopLogger(t))
	Ok(t, err)
	defer disableSSLVerification()()

	files, err := client.GetModifiedFiles(models.Repo{
		FullName:          "owner/repo",
		Owner:             "owner",
		Name:              "repo",
		CloneURL:          "",
		SanitizedCloneURL: "",
		VCSHost: models.VCSHost{
			Type:     models.Github,
			Hostname: "github.com",
		},
	}, models.PullRequest{
		Num: 1,
	})
	Ok(t, err)
	Equals(t, []string{"new/filename.txt", "previous/filename.txt"}, files)
}

func TestGithubClient_PaginatesComments(t *testing.T) {
	calls := 0
	issueResps := []string{
		`[
	{"node_id": "1", "body": "asd\nplan\nasd", "user": {"login": "someone-else"}},
	{"node_id": "2", "body": "asd plan\nasd", "user": {"login": "user"}}
]`,
		`[
	{"node_id": "3", "body": "asd", "user": {"login": "someone-else"}},
	{"node_id": "4", "body": "asdasd", "user": {"login": "someone-else"}}
]`,
		`[
	{"node_id": "5", "body": "asd plan", "user": {"login": "someone-else"}},
	{"node_id": "6", "body": "asd\nplan", "user": {"login": "user"}}
]`,
		`[
	{"node_id": "7", "body": "asd", "user": {"login": "user"}},
	{"node_id": "8", "body": "asd plan \n asd", "user": {"login": "user"}}
]`,
	}
	minimizeResp := "{}"
	type graphQLCall struct {
		Variables struct {
			Input githubv4.MinimizeCommentInput `json:"input"`
		} `json:"variables"`
	}
	gotMinimizeCalls := make([]graphQLCall, 0, 2)
	testServer := httptest.NewTLSServer(
		http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
			switch r.Method + " " + r.RequestURI {
			case "POST /api/graphql":
				defer r.Body.Close() // nolint: errcheck
				body, err := io.ReadAll(r.Body)
				if err != nil {
					t.Errorf("read body error: %v", err)
					http.Error(w, "server error", http.StatusInternalServerError)
					return
				}
				call := graphQLCall{}
				err = json.Unmarshal(body, &call)
				if err != nil {
					t.Errorf("parse body error: %v", err)
					http.Error(w, "server error", http.StatusInternalServerError)
					return
				}
				gotMinimizeCalls = append(gotMinimizeCalls, call)
				w.Write([]byte(minimizeResp)) // nolint: errcheck
				return
			default:
				if r.Method != "GET" || !strings.HasPrefix(r.RequestURI, "/api/v3/repos/owner/repo/issues/123/comments") {
					t.Errorf("got unexpected request at %q", r.RequestURI)
					http.Error(w, "not found", http.StatusNotFound)
					return
				}
				if (calls + 1) < len(issueResps) {
					w.Header().Add(
						"Link",
						fmt.Sprintf(
							`<http://%s/api/v3/repos/owner/repo/issues/123/comments?page=%d&per_page=100>; rel="next"`,
							r.Host,
							calls+1,
						),
					)
				}
				w.Write([]byte(issueResps[calls])) // nolint: errcheck
				calls++
			}
		}),
	)

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

	client, err := vcs.NewGithubClient(testServerURL.Host, &vcs.GithubUserCredentials{"user", "pass"}, logging.NewNoopLogger(t))
	Ok(t, err)
	defer disableSSLVerification()()

	err = client.HidePrevCommandComments(
		models.Repo{
			FullName:          "owner/repo",
			Owner:             "owner",
			Name:              "repo",
			CloneURL:          "",
			SanitizedCloneURL: "",
			VCSHost: models.VCSHost{
				Hostname: "github.com",
				Type:     models.Github,
			},
		},
		123,
		command.Plan.TitleString(),
	)
	Ok(t, err)
	Equals(t, 2, len(gotMinimizeCalls))
	Equals(t, "2", gotMinimizeCalls[0].Variables.Input.SubjectID)
	Equals(t, "8", gotMinimizeCalls[1].Variables.Input.SubjectID)
	Equals(t, githubv4.ReportedContentClassifiersOutdated, gotMinimizeCalls[0].Variables.Input.Classifier)
	Equals(t, githubv4.ReportedContentClassifiersOutdated, gotMinimizeCalls[1].Variables.Input.Classifier)
}

func TestGithubClient_HideOldComments(t *testing.T) {
	// Only comment 6 should be minimized, because it's by the same Atlantis bot user
	// and it has "plan" in the first line of the comment body.
	issueResp := `[
	{"node_id": "1", "body": "asd\nplan\nasd", "user": {"login": "someone-else"}},
	{"node_id": "2", "body": "asd plan\nasd", "user": {"login": "someone-else"}},
	{"node_id": "3", "body": "asdasdasd\nasdasdasd", "user": {"login": "someone-else"}},
	{"node_id": "4", "body": "asdasdasd\nasdasdasd", "user": {"login": "user"}},
	{"node_id": "5", "body": "asd\nplan\nasd", "user": {"login": "user"}},
	{"node_id": "6", "body": "asd plan\nasd", "user": {"login": "user"}},
	{"node_id": "7", "body": "asdasdasd", "user": {"login": "user"}},
	{"node_id": "8", "body": "asd plan\nasd", "user": {"login": "user"}},
	{"node_id": "9", "body": "Continued Plan from previous comment\nasd", "user": {"login": "user"}}
]`
	minimizeResp := "{}"
	type graphQLCall struct {
		Variables struct {
			Input githubv4.MinimizeCommentInput `json:"input"`
		} `json:"variables"`
	}
	gotMinimizeCalls := make([]graphQLCall, 0, 1)
	testServer := httptest.NewTLSServer(
		http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
			switch r.Method + " " + r.RequestURI {
			// This gets the pull request's comments.
			case "GET /api/v3/repos/owner/repo/issues/123/comments?direction=asc&sort=created":
				w.Write([]byte(issueResp)) // nolint: errcheck
				return
			case "POST /api/graphql":
				if accept, has := r.Header["Accept"]; !has || accept[0] != "application/vnd.github.queen-beryl-preview+json" {
					t.Error("missing preview header")
					http.Error(w, "bad request", http.StatusBadRequest)
					return
				}
				defer r.Body.Close() // nolint: errcheck
				body, err := io.ReadAll(r.Body)
				if err != nil {
					t.Errorf("read body error: %v", err)
					http.Error(w, "server error", http.StatusInternalServerError)
					return
				}
				call := graphQLCall{}
				err = json.Unmarshal(body, &call)
				if err != nil {
					t.Errorf("parse body error: %v", err)
					http.Error(w, "server error", http.StatusInternalServerError)
					return
				}
				gotMinimizeCalls = append(gotMinimizeCalls, call)
				w.Write([]byte(minimizeResp)) // 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.NewGithubClient(testServerURL.Host, &vcs.GithubUserCredentials{"user", "pass"}, logging.NewNoopLogger(t))
	Ok(t, err)
	defer disableSSLVerification()()

	err = client.HidePrevCommandComments(
		models.Repo{
			FullName:          "owner/repo",
			Owner:             "owner",
			Name:              "repo",
			CloneURL:          "",
			SanitizedCloneURL: "",
			VCSHost: models.VCSHost{
				Hostname: "github.com",
				Type:     models.Github,
			},
		},
		123,
		command.Plan.TitleString(),
	)
	Ok(t, err)
	Equals(t, 3, len(gotMinimizeCalls))
	Equals(t, "6", gotMinimizeCalls[0].Variables.Input.SubjectID)
	Equals(t, "9", gotMinimizeCalls[2].Variables.Input.SubjectID)
	Equals(t, githubv4.ReportedContentClassifiersOutdated, gotMinimizeCalls[0].Variables.Input.Classifier)
}

func TestGithubClient_UpdateStatus(t *testing.T) {
	cases := []struct {
		status   models.CommitStatus
		expState string
	}{
		{
			models.PendingCommitStatus,
			"pending",
		},
		{
			models.SuccessCommitStatus,
			"success",
		},
		{
			models.FailedCommitStatus,
			"failure",
		},
	}

	for _, c := range cases {
		t.Run(c.status.String(), func(t *testing.T) {
			testServer := httptest.NewTLSServer(
				http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
					switch r.RequestURI {
					case "/api/v3/repos/owner/repo/statuses/":
						body, err := io.ReadAll(r.Body)
						Ok(t, err)
						exp := fmt.Sprintf(`{"state":"%s","target_url":"https://google.com","description":"description","context":"src"}%s`, c.expState, "\n")
						Equals(t, exp, string(body))
						defer r.Body.Close() // nolint: errcheck
						w.WriteHeader(http.StatusOK)
					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.NewGithubClient(testServerURL.Host, &vcs.GithubUserCredentials{"user", "pass"}, logging.NewNoopLogger(t))
			Ok(t, err)
			defer disableSSLVerification()()

			err = client.UpdateStatus(models.Repo{
				FullName:          "owner/repo",
				Owner:             "owner",
				Name:              "repo",
				CloneURL:          "",
				SanitizedCloneURL: "",
				VCSHost: models.VCSHost{
					Type:     models.Github,
					Hostname: "github.com",
				},
			}, models.PullRequest{
				Num: 1,
			}, c.status, "src", "description", "https://google.com")
			Ok(t, err)
		})
	}
}

func TestGithubClient_PullIsApproved(t *testing.T) {
	respTemplate := `[
		{
			"id": %d,
			"node_id": "MDE3OlB1bGxSZXF1ZXN0UmV2aWV3ODA=",
			"user": {
			  "login": "octocat",
			  "id": 1,
			  "node_id": "MDQ6VXNlcjE=",
			  "avatar_url": "https://github.com/images/error/octocat_happy.gif",
			  "gravatar_id": "",
			  "url": "https://api.github.com/users/octocat",
			  "html_url": "https://github.com/octocat",
			  "followers_url": "https://api.github.com/users/octocat/followers",
			  "following_url": "https://api.github.com/users/octocat/following{/other_user}",
			  "gists_url": "https://api.github.com/users/octocat/gists{/gist_id}",
			  "starred_url": "https://api.github.com/users/octocat/starred{/owner}{/repo}",
			  "subscriptions_url": "https://api.github.com/users/octocat/subscriptions",
			  "organizations_url": "https://api.github.com/users/octocat/orgs",
			  "repos_url": "https://api.github.com/users/octocat/repos",
			  "events_url": "https://api.github.com/users/octocat/events{/privacy}",
			  "received_events_url": "https://api.github.com/users/octocat/received_events",
			  "type": "User",
			  "site_admin": false
			},
			"body": "Here is the body for the review.",
			"commit_id": "ecdd80bb57125d7ba9641ffaa4d7d2c19d3f3091",
			"state": "CHANGES_REQUESTED",
			"html_url": "https://github.com/octocat/Hello-World/pull/12#pullrequestreview-%d",
			"pull_request_url": "https://api.github.com/repos/octocat/Hello-World/pulls/12",
			"_links": {
			  "html": {
				"href": "https://github.com/octocat/Hello-World/pull/12#pullrequestreview-%d"
			  },
			  "pull_request": {
				"href": "https://api.github.com/repos/octocat/Hello-World/pulls/12"
			  }
			}
		  }
]`
	firstResp := fmt.Sprintf(respTemplate, 80, 80, 80)
	secondResp := fmt.Sprintf(respTemplate, 81, 81, 81)
	testServer := httptest.NewTLSServer(
		http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
			switch r.RequestURI {
			// The first request should hit this URL.
			case "/api/v3/repos/owner/repo/pulls/1/reviews?per_page=300":
				// We write a header that means there's an additional page.
				w.Header().Add("Link", `<https://api.github.com/resource?page=2>; rel="next",
      <https://api.github.com/resource?page=2>; rel="last"`)
				w.Write([]byte(firstResp)) // nolint: errcheck
				return
				// The second should hit this URL.
			case "/api/v3/repos/owner/repo/pulls/1/reviews?page=2&per_page=300":
				w.Write([]byte(secondResp)) // nolint: errcheck
			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.NewGithubClient(testServerURL.Host, &vcs.GithubUserCredentials{"user", "pass"}, logging.NewNoopLogger(t))
	Ok(t, err)
	defer disableSSLVerification()()

	approvalStatus, err := client.PullIsApproved(models.Repo{
		FullName:          "owner/repo",
		Owner:             "owner",
		Name:              "repo",
		CloneURL:          "",
		SanitizedCloneURL: "",
		VCSHost: models.VCSHost{
			Type:     models.Github,
			Hostname: "github.com",
		},
	}, models.PullRequest{
		Num: 1,
	})
	Ok(t, err)
	Equals(t, false, approvalStatus.IsApproved)
}

func TestGithubClient_PullIsMergeable(t *testing.T) {
	vcsStatusName := "atlantis-test"
	cases := []struct {
		state               string
		requiredCheckName   string
		requiredCheckStatus string
		expMergeable        bool
	}{
		{
			"dirty",
			"",
			"",
			false,
		},
		{
			"unknown",
			"",
			"",
			false,
		},
		{
			"behind",
			"",
			"",
			false,
		},
		{
			"random",
			"",
			"",
			false,
		},
		{
			"unstable",
			"",
			"",
			true,
		},
		{
			"has_hooks",
			"",
			"",
			true,
		},
		{
			"clean",
			"",
			"",
			true,
		},
		{
			"",
			"",
			"",
			false,
		},
		{
			"blocked",
			fmt.Sprintf("%s/apply", vcsStatusName),
			"failure",
			true,
		},
		{
			"blocked",
			fmt.Sprintf("%s/apply", vcsStatusName),
			"pending",
			true,
		},
		{
			"blocked",
			"required_check",
			"failure",
			false,
		},
	}

	// Use a real GitHub json response and edit the mergeable_state field.
	jsBytes, err := os.ReadFile("fixtures/github-pull-request.json")
	Ok(t, err)
	prJSON := string(jsBytes)

	for _, c := range cases {
		t.Run(c.state, func(t *testing.T) {
			response := strings.Replace(prJSON,
				`"mergeable_state": "clean"`,
				fmt.Sprintf(`"mergeable_state": "%s"`, c.state),
				1,
			)
			responseStatus, _ := json.Marshal(map[string][]map[string]string{
				"statuses": {{
					"context": c.requiredCheckName,
					"state":   c.requiredCheckStatus,
				}},
			})
			responseRequiredChecks, _ := json.Marshal(map[string][]string{
				"contexts": {
					c.requiredCheckName,
				},
			})

			testServer := httptest.NewTLSServer(
				http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
					switch r.RequestURI {
					case "/api/v3/repos/owner/repo/pulls/1":
						w.Write([]byte(response)) // nolint: errcheck
						return
					case "/api/v3/repos/owner/repo/commits/headBranch/status":
						w.Write(responseStatus) // nolint: errcheck
						return
					case "/api/v3/repos/owner/repo/branches/baseBranch/protection/required_status_checks":
						w.Write(responseRequiredChecks) // 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.NewGithubClient(testServerURL.Host, &vcs.GithubUserCredentials{"user", "pass"}, logging.NewNoopLogger(t))
			Ok(t, err)
			defer disableSSLVerification()()

			actMergeable, err := client.PullIsMergeable(models.Repo{
				FullName:          "owner/repo",
				Owner:             "owner",
				Name:              "repo",
				CloneURL:          "",
				SanitizedCloneURL: "",
				VCSHost: models.VCSHost{
					Type:     models.Github,
					Hostname: "github.com",
				},
			}, models.PullRequest{
				Num:        1,
				HeadBranch: "headBranch",
				BaseBranch: "baseBranch",
			}, vcsStatusName)
			Ok(t, err)
			Equals(t, c.expMergeable, actMergeable)
		})
	}
}

func TestGithubClient_MergePullHandlesError(t *testing.T) {
	cases := []struct {
		code    int
		message string
		merged  string
		expErr  string
	}{
		{
			code:    200,
			message: "Pull Request successfully merged",
			merged:  "true",
			expErr:  "",
		},
		{
			code:    405,
			message: "Pull Request is not mergeable",
			expErr:  "405 Pull Request is not mergeable []",
		},
		{
			code:    409,
			message: "Head branch was modified. Review and try the merge again.",
			expErr:  "409 Head branch was modified. Review and try the merge again. []",
		},
	}

	jsBytes, err := os.ReadFile("fixtures/github-repo.json")
	Ok(t, err)

	for _, c := range cases {
		t.Run(c.message, func(t *testing.T) {
			testServer := httptest.NewTLSServer(
				http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
					switch r.RequestURI {
					case "/api/v3/repos/owner/repo":
						w.Write(jsBytes) // nolint: errcheck
						return
					case "/api/v3/repos/owner/repo/pulls/1/merge":
						body, err := io.ReadAll(r.Body)
						Ok(t, err)
						exp := "{\"merge_method\":\"merge\"}\n"
						Equals(t, exp, string(body))
						var resp string
						if c.code == 200 {
							resp = fmt.Sprintf(`{"message":"%s","merged":%s}%s`, c.message, c.merged, "\n")
						} else {
							resp = fmt.Sprintf(`{"message":"%s"}%s`, c.message, "\n")
						}
						defer r.Body.Close() // nolint: errcheck
						w.WriteHeader(c.code)
						w.Write([]byte(resp)) // nolint: errcheck
					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.NewGithubClient(testServerURL.Host, &vcs.GithubUserCredentials{"user", "pass"}, logging.NewNoopLogger(t))
			Ok(t, err)
			defer disableSSLVerification()()

			err = client.MergePull(
				models.PullRequest{
					BaseRepo: models.Repo{
						FullName:          "owner/repo",
						Owner:             "owner",
						Name:              "repo",
						CloneURL:          "",
						SanitizedCloneURL: "",
						VCSHost: models.VCSHost{
							Type:     models.Github,
							Hostname: "github.com",
						},
					},
					Num: 1,
				}, models.PullRequestOptions{
					DeleteSourceBranchOnMerge: false,
				})

			if c.expErr == "" {
				Ok(t, err)
			} else {
				ErrContains(t, c.expErr, err)
			}
		})
	}
}

// Test that if the pull request only allows a certain merge method that we
// use that method
func TestGithubClient_MergePullCorrectMethod(t *testing.T) {
	cases := map[string]struct {
		allowMerge  bool
		allowRebase bool
		allowSquash bool
		expMethod   string
	}{
		"all true": {
			allowMerge:  true,
			allowRebase: true,
			allowSquash: true,
			expMethod:   "merge",
		},
		"all false (edge case)": {
			allowMerge:  false,
			allowRebase: false,
			allowSquash: false,
			expMethod:   "merge",
		},
		"merge: false rebase: true squash: true": {
			allowMerge:  false,
			allowRebase: true,
			allowSquash: true,
			expMethod:   "rebase",
		},
		"merge: false rebase: false squash: true": {
			allowMerge:  false,
			allowRebase: false,
			allowSquash: true,
			expMethod:   "squash",
		},
		"merge: false rebase: true squash: false": {
			allowMerge:  false,
			allowRebase: true,
			allowSquash: false,
			expMethod:   "rebase",
		},
	}

	for name, c := range cases {
		t.Run(name, func(t *testing.T) {

			// Modify response.
			jsBytes, err := os.ReadFile("fixtures/github-repo.json")
			Ok(t, err)
			resp := string(jsBytes)
			resp = strings.Replace(resp,
				`"allow_squash_merge": true`,
				fmt.Sprintf(`"allow_squash_merge": %t`, c.allowSquash),
				-1)
			resp = strings.Replace(resp,
				`"allow_merge_commit": true`,
				fmt.Sprintf(`"allow_merge_commit": %t`, c.allowMerge),
				-1)
			resp = strings.Replace(resp,
				`"allow_rebase_merge": true`,
				fmt.Sprintf(`"allow_rebase_merge": %t`, c.allowRebase),
				-1)

			testServer := httptest.NewTLSServer(
				http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
					switch r.RequestURI {
					case "/api/v3/repos/runatlantis/atlantis":
						w.Write([]byte(resp)) // nolint: errcheck
						return
					case "/api/v3/repos/runatlantis/atlantis/pulls/1/merge":
						body, err := io.ReadAll(r.Body)
						Ok(t, err)
						defer r.Body.Close() // nolint: errcheck
						type bodyJSON struct {
							MergeMethod string `json:"merge_method"`
						}
						expBody := bodyJSON{
							MergeMethod: c.expMethod,
						}
						expBytes, err := json.Marshal(expBody)
						Ok(t, err)
						Equals(t, string(expBytes)+"\n", string(body))

						resp := `{"sha":"6dcb09b5b57875f334f61aebed695e2e4193db5e","merged":true,"message":"Pull Request successfully merged"}`
						w.Write([]byte(resp)) // nolint: errcheck
					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.NewGithubClient(testServerURL.Host, &vcs.GithubUserCredentials{"user", "pass"}, logging.NewNoopLogger(t))
			Ok(t, err)
			defer disableSSLVerification()()

			err = client.MergePull(
				models.PullRequest{
					BaseRepo: models.Repo{
						FullName:          "runatlantis/atlantis",
						Owner:             "runatlantis",
						Name:              "atlantis",
						CloneURL:          "",
						SanitizedCloneURL: "",
						VCSHost: models.VCSHost{
							Type:     models.Github,
							Hostname: "github.com",
						},
					},
					Num: 1,
				}, models.PullRequestOptions{
					DeleteSourceBranchOnMerge: false,
				})

			Ok(t, err)
		})
	}
}

func TestGithubClient_MarkdownPullLink(t *testing.T) {
	client, err := vcs.NewGithubClient("hostname", &vcs.GithubUserCredentials{"user", "pass"}, logging.NewNoopLogger(t))
	Ok(t, err)
	pull := models.PullRequest{Num: 1}
	s, _ := client.MarkdownPullLink(pull)
	exp := "#1"
	Equals(t, exp, s)
}

// disableSSLVerification disables ssl verification for the global http client
// and returns a function to be called in a defer that will re-enable it.
func disableSSLVerification() func() {
	orig := http.DefaultTransport.(*http.Transport).TLSClientConfig
	// nolint: gosec
	http.DefaultTransport.(*http.Transport).TLSClientConfig = &tls.Config{InsecureSkipVerify: true}
	return func() {
		http.DefaultTransport.(*http.Transport).TLSClientConfig = orig
	}
}

func TestGithubClient_SplitComments(t *testing.T) {
	type githubComment struct {
		Body string `json:"body"`
	}
	githubComments := make([]githubComment, 0, 1)

	testServer := httptest.NewTLSServer(
		http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {

			switch r.Method + " " + r.RequestURI {
			case "POST /api/v3/repos/runatlantis/atlantis/issues/1/comments":
				defer r.Body.Close() // nolint: errcheck
				body, err := io.ReadAll(r.Body)
				if err != nil {
					t.Errorf("read body error: %v", err)
					http.Error(w, "server error", http.StatusInternalServerError)
					return
				}
				requestBody := githubComment{}
				err = json.Unmarshal(body, &requestBody)
				if err != nil {
					t.Errorf("parse body error: %v", err)
					http.Error(w, "server error", http.StatusInternalServerError)
					return
				}
				githubComments = append(githubComments, requestBody)
				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.NewGithubClient(testServerURL.Host, &vcs.GithubUserCredentials{"user", "pass"}, logging.NewNoopLogger(t))
	Ok(t, err)
	defer disableSSLVerification()()
	pull := models.PullRequest{Num: 1}
	repo := models.Repo{
		FullName:          "runatlantis/atlantis",
		Owner:             "runatlantis",
		Name:              "atlantis",
		CloneURL:          "",
		SanitizedCloneURL: "",
		VCSHost: models.VCSHost{
			Type:     models.Github,
			Hostname: "github.com",
		},
	}
	// create an extra long string
	comment := strings.Repeat("a", 65537)
	err = client.CreateComment(repo, pull.Num, comment, command.Plan.String())
	Ok(t, err)
	err = client.CreateComment(repo, pull.Num, comment, "")
	Ok(t, err)

	body := strings.Split(githubComments[1].Body, "\n")
	firstSplit := strings.ToLower(body[0])
	body = strings.Split(githubComments[3].Body, "\n")
	secondSplit := strings.ToLower(body[0])

	Equals(t, 4, len(githubComments))
	Assert(t, strings.Contains(firstSplit, command.Plan.String()), fmt.Sprintf("comment should contain the command name but was %q", firstSplit))
	Assert(t, strings.Contains(secondSplit, "continued from previous comment"), fmt.Sprintf("comment should contain no reference to the command name but was %q", secondSplit))
}

// Test that we retry the get pull request call if it 404s.
func TestGithubClient_Retry404(t *testing.T) {
	var numCalls = 0

	testServer := httptest.NewTLSServer(
		http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {

			switch r.Method + " " + r.RequestURI {
			case "GET /api/v3/repos/runatlantis/atlantis/pulls/1":
				defer r.Body.Close() // nolint: errcheck
				numCalls++
				if numCalls < 3 {
					w.WriteHeader(404)
				} else {
					w.WriteHeader(200)
				}
				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.NewGithubClient(testServerURL.Host, &vcs.GithubUserCredentials{"user", "pass"}, logging.NewNoopLogger(t))
	Ok(t, err)
	defer disableSSLVerification()()
	repo := models.Repo{
		FullName:          "runatlantis/atlantis",
		Owner:             "runatlantis",
		Name:              "atlantis",
		CloneURL:          "",
		SanitizedCloneURL: "",
		VCSHost: models.VCSHost{
			Type:     models.Github,
			Hostname: "github.com",
		},
	}
	_, err = client.GetPullRequest(repo, 1)
	Ok(t, err)
	Equals(t, 3, numCalls)
}

// Test that we retry the get pull request files call if it 404s.
func TestGithubClient_Retry404Files(t *testing.T) {
	var numCalls = 0

	testServer := httptest.NewTLSServer(
		http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {

			switch r.Method + " " + r.RequestURI {
			case "GET /api/v3/repos/runatlantis/atlantis/pulls/1/files?per_page=300":
				defer r.Body.Close() // nolint: errcheck
				numCalls++
				if numCalls < 3 {
					w.WriteHeader(404)
				} else {
					w.WriteHeader(200)
				}
				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.NewGithubClient(testServerURL.Host, &vcs.GithubUserCredentials{"user", "pass"}, logging.NewNoopLogger(t))
	Ok(t, err)
	defer disableSSLVerification()()
	repo := models.Repo{
		FullName:          "runatlantis/atlantis",
		Owner:             "runatlantis",
		Name:              "atlantis",
		CloneURL:          "",
		SanitizedCloneURL: "",
		VCSHost: models.VCSHost{
			Type:     models.Github,
			Hostname: "github.com",
		},
	}
	pr := models.PullRequest{Num: 1}
	_, err = client.GetModifiedFiles(repo, pr)
	Ok(t, err)
	Equals(t, 3, numCalls)
}

// GetTeamNamesForUser returns a list of team names for a user.
func TestGithubClient_GetTeamNamesForUser(t *testing.T) {
	logger := logging.NewNoopLogger(t)
	// Mocked GraphQL response for two teams
	resp := `{
		"data":{
		  "organization": {
			"teams":{
				"edges":[
					{"node":{"name":"frontend-developers"}},
					{"node":{"name":"employees"}}
				],
				"pageInfo":{
					"endCursor":"Y3Vyc29yOnYyOpHOAFMoLQ==",
					"hasNextPage":false
				}
			}
		}
	  }
	}`
	testServer := httptest.NewTLSServer(
		http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
			switch r.RequestURI {
			case "/api/graphql":
				w.Write([]byte(resp)) // nolint: errcheck
			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.NewGithubClient(testServerURL.Host, &vcs.GithubUserCredentials{"user", "pass"}, logger)
	Ok(t, err)
	defer disableSSLVerification()()

	teams, err := client.GetTeamNamesForUser(models.Repo{
		Owner: "testrepo",
	}, models.User{
		Username: "testuser",
	})
	Ok(t, err)
	Equals(t, []string{"frontend-developers", "employees"}, teams)
}
