package vcs

import (
	"encoding/json"
	"fmt"
	"net/http"
	"net/http/httptest"
	"os"
	"path"
	"strings"
	"testing"
	"time"

	"github.com/hashicorp/go-version"
	"github.com/runatlantis/atlantis/server/events/command"
	"github.com/runatlantis/atlantis/server/events/models"
	"github.com/runatlantis/atlantis/server/logging"
	gitlab "gitlab.com/gitlab-org/api/client-go"

	. "github.com/runatlantis/atlantis/testing"
)

var projectID = 4580910

const gitlabPipelineSuccessMrID = 488598

const updateStatusDescription = "description"
const updateStatusTargetUrl = "https://google.com"
const updateStatusSrc = "src"
const updateStatusHeadBranch = "test"

/* UpdateStatus request JSON body object */
type UpdateStatusJsonBody struct {
	State       string `json:"state"`
	Context     string `json:"context"`
	TargetUrl   string `json:"target_url"`
	Description string `json:"description"`
	PipelineId  int    `json:"pipeline_id"`
	Ref         string `json:"ref"`
}

/* GetCommit response last_pipeline JSON object */
type GetCommitResponseLastPipeline struct {
	ID int `json:"id"`
}

/* GetCommit response JSON object */
type GetCommitResponse struct {
	LastPipeline GetCommitResponseLastPipeline `json:"last_pipeline"`
}

/* Empty struct for JSON marshalling */
type EmptyStruct struct{}

// Test that the base url gets set properly.
func TestNewGitlabClient_BaseURL(t *testing.T) {
	gitlabClientUnderTest = true
	defer func() { gitlabClientUnderTest = false }()

	cases := []struct {
		Hostname   string
		ExpBaseURL string
	}{
		{
			"gitlab.com",
			"https://gitlab.com/api/v4/",
		},
		{
			"custom.domain",
			"https://custom.domain/api/v4/",
		},
		{
			"http://custom.domain",
			"http://custom.domain/api/v4/",
		},
		{
			"http://custom.domain:8080",
			"http://custom.domain:8080/api/v4/",
		},
		{
			"https://custom.domain",
			"https://custom.domain/api/v4/",
		},
		{
			"https://custom.domain/",
			"https://custom.domain/api/v4/",
		},
		{
			"https://custom.domain/basepath/",
			"https://custom.domain/basepath/api/v4/",
		},
	}

	for _, c := range cases {
		t.Run(c.Hostname, func(t *testing.T) {
			log := logging.NewNoopLogger(t)
			client, err := NewGitlabClient(c.Hostname, "token", []string{}, log)
			Ok(t, err)
			Equals(t, c.ExpBaseURL, client.Client.BaseURL().String())
		})
	}
}

// This function gets called even if GitlabClient is nil
// so we need to test that.
func TestGitlabClient_SupportsCommonMarkNil(t *testing.T) {
	var gl *GitlabClient
	Equals(t, false, gl.SupportsCommonMark())
}

func TestGitlabClient_SupportsCommonMark(t *testing.T) {
	cases := []struct {
		version string
		exp     bool
	}{
		{
			"11.0",
			false,
		},
		{
			"11.1",
			true,
		},
		{
			"11.2",
			true,
		},
		{
			"12.0",
			true,
		},
	}

	for _, c := range cases {
		t.Run(c.version, func(t *testing.T) {
			vers, err := version.NewVersion(c.version)
			Ok(t, err)
			gl := GitlabClient{
				Version: vers,
			}
			Equals(t, c.exp, gl.SupportsCommonMark())
		})
	}
}

func TestGitlabClient_GetModifiedFiles(t *testing.T) {
	logger := logging.NewNoopLogger(t)
	cases := []struct {
		attempts int
	}{
		{1}, {2}, {3},
	}

	changesPending, err := os.ReadFile("testdata/gitlab-changes-pending.json")
	Ok(t, err)

	changesAvailable, err := os.ReadFile("testdata/gitlab-changes-available.json")
	Ok(t, err)

	for _, c := range cases {
		t.Run(fmt.Sprintf("Gitlab returns MR changes after %d attempts", c.attempts), func(t *testing.T) {
			numAttempts := 0
			testServer := httptest.NewServer(
				http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
					switch r.RequestURI {
					case "/api/v4/projects/lkysow%2Fatlantis-example/merge_requests/8312/changes?page=1&per_page=100":
						w.WriteHeader(200)
						numAttempts++
						if numAttempts < c.attempts {
							w.Write(changesPending) // nolint: errcheck
							t.Logf("returning changesPending for attempt %d", numAttempts)
							return
						}
						t.Logf("returning changesAvailable for attempt %d", numAttempts)
						w.Write(changesAvailable) // nolint: errcheck
					default:
						t.Errorf("got unexpected request at %q", r.RequestURI)
						http.Error(w, "not found", http.StatusNotFound)
					}
				}))

			internalClient, err := gitlab.NewClient("token", gitlab.WithBaseURL(testServer.URL))
			Ok(t, err)
			client := &GitlabClient{
				Client:          internalClient,
				Version:         nil,
				PollingInterval: time.Second * 0,
				PollingTimeout:  time.Second * 10,
			}

			filenames, err := client.GetModifiedFiles(
				logger,
				models.Repo{
					FullName: "lkysow/atlantis-example",
					Owner:    "lkysow",
					Name:     "atlantis-example",
				},
				models.PullRequest{
					Num: 8312,
					BaseRepo: models.Repo{
						FullName: "lkysow/atlantis-example",
						Owner:    "lkysow",
						Name:     "atlantis-example",
					},
				})
			Ok(t, err)
			Equals(t, []string{"somefile.yaml"}, filenames)
		})
	}
}

func TestGitlabClient_MergePull(t *testing.T) {
	logger := logging.NewNoopLogger(t)
	mergeSuccess, err := os.ReadFile("testdata/github-pull-request.json")
	Ok(t, err)

	pipelineSuccess, err := os.ReadFile("testdata/gitlab-pipeline-success.json")
	Ok(t, err)

	projectSuccess, err := os.ReadFile("testdata/gitlab-project-success.json")
	Ok(t, err)

	cases := []struct {
		description string
		glResponse  []byte
		code        int
		expErr      string
	}{
		{
			"success",
			mergeSuccess,
			200,
			"",
		},
		{
			"405",
			[]byte(`{"message":"405 Method Not Allowed"}`),
			405,
			"405 {message: 405 Method Not Allowed}",
		},
		{
			"406",
			[]byte(`{"message":"406 Branch cannot be merged"}`),
			406,
			"406 {message: 406 Branch cannot be merged}",
		},
	}

	for _, c := range cases {
		t.Run(c.description, func(t *testing.T) {
			testServer := httptest.NewServer(
				http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
					switch r.RequestURI {
					// The first request should hit this URL.
					case "/api/v4/projects/runatlantis%2Fatlantis/merge_requests/1/merge":
						w.WriteHeader(c.code)
						w.Write(c.glResponse) // nolint: errcheck
					case "/api/v4/projects/runatlantis%2Fatlantis/merge_requests/1":
						w.WriteHeader(http.StatusOK)
						w.Write(pipelineSuccess) // nolint: errcheck
					case "/api/v4/projects/4580910":
						w.WriteHeader(http.StatusOK)
						w.Write(projectSuccess) // nolint: errcheck
					case "/api/v4/":
						// Rate limiter requests.
						w.WriteHeader(http.StatusOK)
					default:
						t.Errorf("got unexpected request at %q", r.RequestURI)
						http.Error(w, "not found", http.StatusNotFound)
					}
				}))

			internalClient, err := gitlab.NewClient("token", gitlab.WithBaseURL(testServer.URL))
			Ok(t, err)
			client := &GitlabClient{
				Client:  internalClient,
				Version: nil,
			}

			err = client.MergePull(
				logger,
				models.PullRequest{
					Num: 1,
					BaseRepo: models.Repo{
						FullName: "runatlantis/atlantis",
						Owner:    "runatlantis",
						Name:     "atlantis",
					},
				}, 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 TestGitlabClient_UpdateStatus(t *testing.T) {
	logger := logging.NewNoopLogger(t)

	cases := []struct {
		status   models.CommitStatus
		expState string
	}{
		{
			models.PendingCommitStatus,
			"running",
		},
		{
			models.SuccessCommitStatus,
			"success",
		},
		{
			models.FailedCommitStatus,
			"failed",
		},
	}
	for _, c := range cases {
		t.Run(c.expState, func(t *testing.T) {
			gotRequest := false
			testServer := httptest.NewServer(
				http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
					switch r.RequestURI {
					case "/api/v4/projects/runatlantis%2Fatlantis/statuses/sha":
						gotRequest = true

						var updateStatusJsonBody UpdateStatusJsonBody
						err := json.NewDecoder(r.Body).Decode(&updateStatusJsonBody)
						Ok(t, err)

						Equals(t, c.expState, updateStatusJsonBody.State)
						Equals(t, updateStatusSrc, updateStatusJsonBody.Context)
						Equals(t, updateStatusTargetUrl, updateStatusJsonBody.TargetUrl)
						Equals(t, updateStatusDescription, updateStatusJsonBody.Description)
						Equals(t, gitlabPipelineSuccessMrID, updateStatusJsonBody.PipelineId)

						defer r.Body.Close() // nolint: errcheck

						setStatusJsonResponse, err := json.Marshal(EmptyStruct{})
						Ok(t, err)

						_, err = w.Write(setStatusJsonResponse)
						Ok(t, err)

					case "/api/v4/projects/runatlantis%2Fatlantis/repository/commits/sha":
						w.WriteHeader(http.StatusOK)

						getCommitResponse := GetCommitResponse{
							LastPipeline: GetCommitResponseLastPipeline{
								ID: gitlabPipelineSuccessMrID,
							},
						}
						getCommitJsonResponse, err := json.Marshal(getCommitResponse)
						Ok(t, err)

						_, err = w.Write(getCommitJsonResponse)
						Ok(t, err)

					case "/api/v4/":
						// Rate limiter requests.
						w.WriteHeader(http.StatusOK)

					default:
						t.Errorf("got unexpected request at %q", r.RequestURI)
						http.Error(w, "not found", http.StatusNotFound)
					}
				}))

			internalClient, err := gitlab.NewClient("token", gitlab.WithBaseURL(testServer.URL))
			Ok(t, err)
			client := &GitlabClient{
				Client:  internalClient,
				Version: nil,
			}

			repo := models.Repo{
				FullName: "runatlantis/atlantis",
				Owner:    "runatlantis",
				Name:     "atlantis",
			}
			err = client.UpdateStatus(
				logger,
				repo,
				models.PullRequest{
					Num:        1,
					BaseRepo:   repo,
					HeadCommit: "sha",
					HeadBranch: updateStatusHeadBranch,
				},
				c.status,
				updateStatusSrc,
				updateStatusDescription,
				updateStatusTargetUrl,
			)
			Ok(t, err)
			Assert(t, gotRequest, "expected to get the request")
		})
	}
}

func TestGitlabClient_UpdateStatusGetCommitRetryable(t *testing.T) {
	logger := logging.NewNoopLogger(t)

	cases := []struct {
		title                     string
		status                    models.CommitStatus
		commitsWithNoLastPipeline int
		expNumberOfRequests       int
		expRefOrPipelineId        string
	}{
		// Ensure that GetCommit with last pipeline id sets the pipeline id.
		{
			title:                     "GetCommit with a pipeline id",
			status:                    models.PendingCommitStatus,
			commitsWithNoLastPipeline: 0,
			expNumberOfRequests:       1,
			expRefOrPipelineId:        "PipelineId",
		},
		// Ensure that 1 x GetCommit with no pipelines sets the pipeline id.
		{
			title:                     "1 x GetCommit with no last pipeline id",
			status:                    models.PendingCommitStatus,
			commitsWithNoLastPipeline: 1,
			expNumberOfRequests:       2,
			expRefOrPipelineId:        "PipelineId",
		},
		// Ensure that 2 x GetCommit with no last pipeline id sets the ref.
		{
			title:                     "2 x GetCommit with no last pipeline id",
			status:                    models.PendingCommitStatus,
			commitsWithNoLastPipeline: 2,
			expNumberOfRequests:       2,
			expRefOrPipelineId:        "Ref",
		},
	}

	for _, c := range cases {
		t.Run(c.title, func(t *testing.T) {
			handledNumberOfRequests := 0

			testServer := httptest.NewServer(
				http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
					switch r.RequestURI {
					case "/api/v4/projects/runatlantis%2Fatlantis/statuses/sha":
						var updateStatusJsonBody UpdateStatusJsonBody
						err := json.NewDecoder(r.Body).Decode(&updateStatusJsonBody)
						Ok(t, err)

						Equals(t, "running", updateStatusJsonBody.State)
						Equals(t, updateStatusSrc, updateStatusJsonBody.Context)
						Equals(t, updateStatusTargetUrl, updateStatusJsonBody.TargetUrl)
						Equals(t, updateStatusDescription, updateStatusJsonBody.Description)
						if c.expRefOrPipelineId == "Ref" {
							Equals(t, updateStatusHeadBranch, updateStatusJsonBody.Ref)
						} else {
							Equals(t, gitlabPipelineSuccessMrID, updateStatusJsonBody.PipelineId)
						}

						defer r.Body.Close()

						getCommitJsonResponse, err := json.Marshal(EmptyStruct{})
						Ok(t, err)

						_, err = w.Write(getCommitJsonResponse)
						Ok(t, err)

					case "/api/v4/projects/runatlantis%2Fatlantis/repository/commits/sha":
						handledNumberOfRequests++
						noCommitLastPipeline := handledNumberOfRequests <= c.commitsWithNoLastPipeline

						w.WriteHeader(http.StatusOK)
						if noCommitLastPipeline {
							getCommitJsonResponse, err := json.Marshal(EmptyStruct{})
							Ok(t, err)

							_, err = w.Write(getCommitJsonResponse)
							Ok(t, err)
						} else {
							getCommitResponse := GetCommitResponse{
								LastPipeline: GetCommitResponseLastPipeline{
									ID: gitlabPipelineSuccessMrID,
								},
							}
							getCommitJsonResponse, err := json.Marshal(getCommitResponse)
							Ok(t, err)

							_, err = w.Write(getCommitJsonResponse)
							Ok(t, err)
						}

					case "/api/v4/":
						// Rate limiter requests.
						w.WriteHeader(http.StatusOK)

					default:
						t.Errorf("got unexpected request at %q", r.RequestURI)
						http.Error(w, "not found", http.StatusNotFound)
					}
				}))

			internalClient, err := gitlab.NewClient("token", gitlab.WithBaseURL(testServer.URL))
			Ok(t, err)

			client := &GitlabClient{
				Client:          internalClient,
				Version:         nil,
				PollingInterval: 10 * time.Millisecond,
			}

			repo := models.Repo{
				FullName: "runatlantis/atlantis",
				Owner:    "runatlantis",
				Name:     "atlantis",
			}

			err = client.UpdateStatus(
				logger,
				repo,
				models.PullRequest{
					Num:        1,
					BaseRepo:   repo,
					HeadCommit: "sha",
					HeadBranch: updateStatusHeadBranch,
				},
				c.status,
				updateStatusSrc,
				updateStatusDescription,
				updateStatusTargetUrl,
			)
			Ok(t, err)

			Assert(t, c.expNumberOfRequests == handledNumberOfRequests,
				fmt.Sprintf("expected %d number of requests, but processed %d", c.expNumberOfRequests, handledNumberOfRequests))
		})
	}
}

func TestGitlabClient_UpdateStatusSetCommitStatusConflictRetryable(t *testing.T) {
	logger := logging.NewNoopLogger(t)

	cases := []struct {
		status              models.CommitStatus
		numberOfConflicts   int
		expNumberOfRequests int
		expState            string
		expError            bool
	}{
		// Ensure that 0 x 409 Conflict succeeds
		{
			status:              models.PendingCommitStatus,
			numberOfConflicts:   0,
			expNumberOfRequests: 1,
			expState:            "running",
		},
		// Ensure that 5 x 409 Conflict still succeeds
		{
			status:              models.PendingCommitStatus,
			numberOfConflicts:   5,
			expNumberOfRequests: 6,
			expState:            "running",
		},
		// Ensure that 10 x 409 Conflict still fail due to running out of retries
		{
			status:              models.FailedCommitStatus,
			numberOfConflicts:   100, // anything larger than 10 is fine
			expNumberOfRequests: 10,
			expState:            "failed",
			expError:            true,
		},
	}
	for _, c := range cases {
		t.Run(c.expState, func(t *testing.T) {
			handledNumberOfRequests := 0

			testServer := httptest.NewServer(
				http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
					switch r.RequestURI {
					case "/api/v4/projects/runatlantis%2Fatlantis/statuses/sha":
						handledNumberOfRequests++
						shouldSendConflict := handledNumberOfRequests <= c.numberOfConflicts

						var updateStatusJsonBody UpdateStatusJsonBody
						err := json.NewDecoder(r.Body).Decode(&updateStatusJsonBody)
						Ok(t, err)

						Equals(t, c.expState, updateStatusJsonBody.State)
						Equals(t, updateStatusSrc, updateStatusJsonBody.Context)
						Equals(t, updateStatusTargetUrl, updateStatusJsonBody.TargetUrl)
						Equals(t, updateStatusDescription, updateStatusJsonBody.Description)

						defer r.Body.Close() // nolint: errcheck

						if shouldSendConflict {
							w.WriteHeader(http.StatusConflict)
						}

						getCommitJsonResponse, err := json.Marshal(EmptyStruct{})
						Ok(t, err)

						_, err = w.Write(getCommitJsonResponse)
						Ok(t, err)

					case "/api/v4/projects/runatlantis%2Fatlantis/repository/commits/sha":
						w.WriteHeader(http.StatusOK)

						getCommitResponse := GetCommitResponse{
							LastPipeline: GetCommitResponseLastPipeline{
								ID: gitlabPipelineSuccessMrID,
							},
						}
						getCommitJsonResponse, err := json.Marshal(getCommitResponse)
						Ok(t, err)

						_, err = w.Write(getCommitJsonResponse)
						Ok(t, err)

					case "/api/v4/":
						// Rate limiter requests.
						w.WriteHeader(http.StatusOK)

					default:
						t.Errorf("got unexpected request at %q", r.RequestURI)
						http.Error(w, "not found", http.StatusNotFound)
					}
				}))

			internalClient, err := gitlab.NewClient("token", gitlab.WithBaseURL(testServer.URL))
			Ok(t, err)
			client := &GitlabClient{
				Client:          internalClient,
				Version:         nil,
				PollingInterval: 10 * time.Millisecond,
			}

			repo := models.Repo{
				FullName: "runatlantis/atlantis",
				Owner:    "runatlantis",
				Name:     "atlantis",
			}
			err = client.UpdateStatus(
				logger,
				repo,
				models.PullRequest{
					Num:        1,
					BaseRepo:   repo,
					HeadCommit: "sha",
					HeadBranch: "test",
				},
				c.status,
				updateStatusSrc,
				updateStatusDescription,
				updateStatusTargetUrl,
			)

			if c.expError {
				ErrContains(t, "failed to update commit status for 'runatlantis/atlantis' @ 'sha' to 'src' after 10 attempts", err)
				ErrContains(t, "409", err)
			} else {
				Ok(t, err)
			}

			Assert(t, c.expNumberOfRequests == handledNumberOfRequests,
				fmt.Sprintf("expected %d number of requests, but processed %d", c.expNumberOfRequests, handledNumberOfRequests))
		})
	}
}

func TestGitlabClient_PullIsMergeable(t *testing.T) {
	logger := logging.NewNoopLogger(t)
	gitlabClientUnderTest = true
	gitlabVersionOver15_6 := "15.8.3-ee"
	gitlabVersion15_6 := "15.6.0-ee"
	gitlabVersionUnder15_6 := "15.3.2-ce"
	gitlabServerVersions := []string{gitlabVersionOver15_6, gitlabVersion15_6, gitlabVersionUnder15_6}
	vcsStatusName := "atlantis-test"
	defaultMr := 1
	noHeadPipelineMR := 2
	ciMustPassSuccessMR := 3
	ciMustPassFailureMR := 4
	needRebaseMR := 5

	pipelineSuccess, err := os.ReadFile("testdata/gitlab-pipeline-success.json")
	Ok(t, err)

	projectSuccess, err := os.ReadFile("testdata/gitlab-project-success.json")
	Ok(t, err)

	detailedMergeStatusCiMustPass, err := os.ReadFile("testdata/gitlab-detailed-merge-status-ci-must-pass.json")
	Ok(t, err)

	detailedMergeStatusNeedRebase, err := os.ReadFile("testdata/gitlab-detailed-merge-status-need-rebase.json")
	Ok(t, err)

	headPipelineNotAvailable, err := os.ReadFile("testdata/gitlab-head-pipeline-not-available.json")
	Ok(t, err)

	cases := []struct {
		statusName    string
		status        models.CommitStatus
		gitlabVersion []string
		mrID          int
		expState      bool
	}{
		{
			fmt.Sprintf("%s/apply: resource/default", vcsStatusName),
			models.FailedCommitStatus,
			gitlabServerVersions,
			defaultMr,
			true,
		},
		{
			fmt.Sprintf("%s/apply", vcsStatusName),
			models.FailedCommitStatus,
			gitlabServerVersions,
			defaultMr,
			true,
		},
		{
			fmt.Sprintf("%s/plan: resource/default", vcsStatusName),
			models.FailedCommitStatus,
			gitlabServerVersions,
			defaultMr,
			false,
		},
		{
			fmt.Sprintf("%s/plan", vcsStatusName),
			models.PendingCommitStatus,
			gitlabServerVersions,
			defaultMr,
			false,
		},
		{
			fmt.Sprintf("%s/plan", vcsStatusName),
			models.SuccessCommitStatus,
			gitlabServerVersions,
			defaultMr,
			true,
		},
		{
			fmt.Sprintf("%s/apply", vcsStatusName),
			models.FailedCommitStatus,
			gitlabServerVersions,
			ciMustPassSuccessMR,
			true,
		},
		{
			fmt.Sprintf("%s/plan", vcsStatusName),
			models.FailedCommitStatus,
			gitlabServerVersions,
			ciMustPassFailureMR,
			false,
		},
		{
			fmt.Sprintf("%s/apply", vcsStatusName),
			models.FailedCommitStatus,
			gitlabServerVersions,
			needRebaseMR,
			true,
		},
		{
			fmt.Sprintf("%s/apply: resource/default", vcsStatusName),
			models.FailedCommitStatus,
			gitlabServerVersions,
			noHeadPipelineMR,
			true,
		},
		{
			fmt.Sprintf("%s/apply", vcsStatusName),
			models.FailedCommitStatus,
			gitlabServerVersions,
			noHeadPipelineMR,
			true,
		},
		{
			fmt.Sprintf("%s/plan: resource/default", vcsStatusName),
			models.FailedCommitStatus,
			gitlabServerVersions,
			noHeadPipelineMR,
			false,
		},
		{
			fmt.Sprintf("%s/plan", vcsStatusName),
			models.PendingCommitStatus,
			gitlabServerVersions,
			noHeadPipelineMR,
			false,
		},
		{
			fmt.Sprintf("%s/plan", vcsStatusName),
			models.FailedCommitStatus,
			gitlabServerVersions,
			noHeadPipelineMR,
			false,
		},
		{
			fmt.Sprintf("%s/plan", vcsStatusName),
			models.SuccessCommitStatus,
			gitlabServerVersions,
			noHeadPipelineMR,
			true,
		},
	}
	for _, serverVersion := range gitlabServerVersions {
		for _, c := range cases {
			t.Run(c.statusName, func(t *testing.T) {
				testServer := httptest.NewServer(
					http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
						switch r.RequestURI {
						case "/api/v4/":
							// Rate limiter requests.
							w.WriteHeader(http.StatusOK)
						case fmt.Sprintf("/api/v4/projects/runatlantis%%2Fatlantis/merge_requests/%v", defaultMr):
							w.WriteHeader(http.StatusOK)
							w.Write(pipelineSuccess) // nolint: errcheck
						case fmt.Sprintf("/api/v4/projects/runatlantis%%2Fatlantis/merge_requests/%v", noHeadPipelineMR):
							w.WriteHeader(http.StatusOK)
							w.Write(headPipelineNotAvailable) // nolint: errcheck
						case fmt.Sprintf("/api/v4/projects/runatlantis%%2Fatlantis/merge_requests/%v", ciMustPassSuccessMR):
							w.WriteHeader(http.StatusOK)
							w.Write(detailedMergeStatusCiMustPass) // nolint: errcheck
						case fmt.Sprintf("/api/v4/projects/runatlantis%%2Fatlantis/merge_requests/%v", ciMustPassFailureMR):
							w.WriteHeader(http.StatusOK)
							w.Write(detailedMergeStatusCiMustPass) // nolint: errcheck
						case fmt.Sprintf("/api/v4/projects/runatlantis%%2Fatlantis/merge_requests/%v", needRebaseMR):
							w.WriteHeader(http.StatusOK)
							w.Write(detailedMergeStatusNeedRebase) // nolint: errcheck
						case fmt.Sprintf("/api/v4/projects/%v", projectID):
							w.WriteHeader(http.StatusOK)
							w.Write(projectSuccess) // nolint: errcheck
						case fmt.Sprintf("/api/v4/projects/%v/repository/commits/67cb91d3f6198189f433c045154a885784ba6977/statuses", projectID):
							w.WriteHeader(http.StatusOK)
							response := fmt.Sprintf(`[{"id":133702594,"sha":"67cb91d3f6198189f433c045154a885784ba6977","ref":"patch-1","status":"%s","name":"%s","target_url":null,"description":"ApplySuccess","created_at":"2018-12-12T18:31:57.957Z","started_at":null,"finished_at":"2018-12-12T18:31:58.480Z","allow_failure":false,"coverage":null,"author":{"id":1755902,"username":"lkysow","name":"LukeKysow","state":"active","avatar_url":"https://secure.gravatar.com/avatar/25fd57e71590fe28736624ff24d41c5f?s=80&d=identicon","web_url":"https://gitlab.com/lkysow"}}]`, c.status, c.statusName)
							w.Write([]byte(response)) // nolint: errcheck
						case "/api/v4/version":
							w.WriteHeader(http.StatusOK)
							w.Header().Set("Content-Type", "application/json")
							type version struct {
								Version string
							}
							v := version{Version: serverVersion}
							err := json.NewEncoder(w).Encode(v)
							Ok(t, err)
						default:
							t.Errorf("got unexpected request at %q", r.RequestURI)
							http.Error(w, "not found", http.StatusNotFound)
						}
					}))

				internalClient, err := gitlab.NewClient("token", gitlab.WithBaseURL(testServer.URL))
				Ok(t, err)
				client := &GitlabClient{
					Client:  internalClient,
					Version: nil,
				}

				repo := models.Repo{
					FullName: "runatlantis/atlantis",
					Owner:    "runatlantis",
					Name:     "atlantis",
					VCSHost: models.VCSHost{
						Type:     models.Gitlab,
						Hostname: "gitlab.com",
					},
				}

				mergeable, err := client.PullIsMergeable(
					logger,
					repo,
					models.PullRequest{
						Num:        c.mrID,
						BaseRepo:   repo,
						HeadCommit: "67cb91d3f6198189f433c045154a885784ba6977",
					}, vcsStatusName, []string{})

				Ok(t, err)
				Equals(t, c.expState, mergeable)
			})
		}
	}
}

func TestGitlabClient_MarkdownPullLink(t *testing.T) {
	logger := logging.NewNoopLogger(t)
	gitlabClientUnderTest = true
	defer func() { gitlabClientUnderTest = false }()
	client, err := NewGitlabClient("gitlab.com", "token", []string{}, logger)
	Ok(t, err)
	pull := models.PullRequest{Num: 1}
	s, _ := client.MarkdownPullLink(pull)
	exp := "!1"
	Equals(t, exp, s)
}

func TestGitlabClient_HideOldComments(t *testing.T) {
	logger := logging.NewNoopLogger(t)
	type notePutCallDetails struct {
		noteID  string
		comment []string
	}
	type jsonBody struct {
		Body string
	}

	authorID := 1
	authorUserName := "pipin"
	authorEmail := "admin@example.com"
	pullNum := 123

	userCommentIDs := [1]string{"1"}
	planCommentIDs := [2]string{"3", "5"}
	systemCommentIDs := [1]string{"4"}
	summaryCommentIDs := [1]string{"2"}
	planComments := [3]string{"Ran Plan for 2 projects:", "Ran Plan for dir: `stack1` workspace: `default`", "Ran Plan for 2 projects:"}
	summaryHeader := fmt.Sprintf("<!--- +-Superseded Command-+ ---><details><summary>Superseded Atlantis %s</summary>",
		command.Plan.TitleString())
	summaryFooter := "</details>"
	lineFeed := "\\n"

	issueResp := "[" +
		fmt.Sprintf(`{"id":%s,"body":"User comment","author":{"id": %d, "username":"%s", "email":"%s"},"system": false,"project_id": %d}`,
			userCommentIDs[0], authorID, authorUserName, authorEmail, pullNum) + "," +
		fmt.Sprintf(`{"id":%s,"body":"%s","author":{"id": %d, "username":"%s", "email":"%s"},"system": false,"project_id": %d}`,
			summaryCommentIDs[0], summaryHeader+lineFeed+planComments[2]+lineFeed+summaryFooter, authorID, authorUserName, authorEmail, pullNum) + "," +
		fmt.Sprintf(`{"id":%s,"body":"%s","author":{"id": %d, "username":"%s", "email":"%s"},"system": false,"project_id": %d}`,
			planCommentIDs[0], planComments[0], authorID, authorUserName, authorEmail, pullNum) + "," +
		fmt.Sprintf(`{"id":%s,"body":"System comment","author":{"id": %d, "username":"%s", "email":"%s"},"system": true,"project_id": %d}`,
			systemCommentIDs[0], authorID, authorUserName, authorEmail, pullNum) + "," +
		fmt.Sprintf(`{"id":%s,"body":"%s","author":{"id": %d, "username":"%s", "email":"%s"},"system": false,"project_id": %d}`,
			planCommentIDs[1], planComments[1], authorID, authorUserName, authorEmail, pullNum) +
		"]"

	repo := models.Repo{
		FullName: "runatlantis/atlantis",
		Owner:    "runatlantis",
		Name:     "atlantis",
		VCSHost: models.VCSHost{
			Type:     models.Gitlab,
			Hostname: "gitlab.com",
		},
	}

	cases := []struct {
		dir                  string
		processedComments    int
		processedCommentIds  []string
		processedPlanComment []string
	}{
		{
			"",
			2,
			[]string{planCommentIDs[0], planCommentIDs[1]},
			[]string{planComments[0], planComments[1]},
		},
		{
			"stack1",
			1,
			[]string{planCommentIDs[1]},
			[]string{planComments[1]},
		},
		{
			"stack2",
			0,
			[]string{},
			[]string{},
		},
	}

	for _, c := range cases {
		t.Run(c.dir, func(t *testing.T) {
			gitlabClientUnderTest = true
			defer func() { gitlabClientUnderTest = false }()
			gotNotePutCalls := make([]notePutCallDetails, 0, 1)
			testServer := httptest.NewServer(
				http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
					switch r.Method {
					case "GET":
						switch r.RequestURI {
						case "/api/v4/user":
							w.WriteHeader(http.StatusOK)
							w.Header().Set("Content-Type", "application/json")
							response := fmt.Sprintf(`{"id": %d,"username": "%s", "email": "%s"}`, authorID, authorUserName, authorEmail)
							w.Write([]byte(response)) // nolint: errcheck
						case fmt.Sprintf("/api/v4/projects/runatlantis%%2Fatlantis/merge_requests/%d/notes?order_by=created_at&sort=asc", pullNum):
							w.WriteHeader(http.StatusOK)
							response := issueResp
							w.Write([]byte(response)) // nolint: errcheck
						default:
							t.Errorf("got unexpected request at %q", r.RequestURI)
							http.Error(w, "not found", http.StatusNotFound)
						}
					case "PUT":
						switch {
						case strings.HasPrefix(r.RequestURI, fmt.Sprintf("/api/v4/projects/runatlantis%%2Fatlantis/merge_requests/%d/notes/", pullNum)):
							w.WriteHeader(http.StatusOK)
							var body jsonBody
							json.NewDecoder(r.Body).Decode(&body) // nolint: errcheck
							notePutCallDetail := notePutCallDetails{
								noteID:  path.Base(r.RequestURI),
								comment: strings.Split(body.Body, "\n"),
							}
							gotNotePutCalls = append(gotNotePutCalls, notePutCallDetail)
							response := "{}"
							w.Write([]byte(response)) // nolint: errcheck
						default:
							t.Errorf("got unexpected request at %q", r.RequestURI)
							http.Error(w, "not found", http.StatusNotFound)
						}
					default:
						t.Errorf("got unexpected method at %q", r.Method)
						http.Error(w, "not found", http.StatusNotFound)
					}
				}),
			)

			internalClient, err := gitlab.NewClient("token", gitlab.WithBaseURL(testServer.URL))
			Ok(t, err)
			client := &GitlabClient{
				Client:  internalClient,
				Version: nil,
			}

			err = client.HidePrevCommandComments(logger, repo, pullNum, command.Plan.TitleString(), c.dir)
			Ok(t, err)

			// Check the correct number of plan comments have been processed
			Equals(t, c.processedComments, len(gotNotePutCalls))
			// Check the correct comments have been processed
			for i := 0; i < c.processedComments; i++ {
				Equals(t, c.processedCommentIds[i], gotNotePutCalls[i].noteID)
				Equals(t, summaryHeader, gotNotePutCalls[i].comment[0])
				Equals(t, c.processedPlanComment[i], gotNotePutCalls[i].comment[1])
				Equals(t, summaryFooter, gotNotePutCalls[i].comment[2])
			}
		})
	}
}

func TestGitlabClient_GetPullLabels(t *testing.T) {
	logger := logging.NewNoopLogger(t)
	mergeSuccessWithLabel, err := os.ReadFile("testdata/gitlab-merge-success-with-label.json")
	Ok(t, err)

	testServer := httptest.NewServer(
		http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
			switch r.RequestURI {
			case "/api/v4/projects/runatlantis%2Fatlantis/merge_requests/1":
				w.WriteHeader(http.StatusOK)
				w.Write(mergeSuccessWithLabel) // nolint: errcheck
			default:
				t.Errorf("got unexpected request at %q", r.RequestURI)
				http.Error(w, "not found", http.StatusNotFound)
			}
		}))

	internalClient, err := gitlab.NewClient("token", gitlab.WithBaseURL(testServer.URL))
	Ok(t, err)
	client := &GitlabClient{
		Client:  internalClient,
		Version: nil,
	}

	labels, err := client.GetPullLabels(
		logger,
		models.Repo{
			FullName: "runatlantis/atlantis",
		},
		models.PullRequest{
			Num: 1,
		},
	)
	Ok(t, err)
	Equals(t, []string{"work in progress"}, labels)
}

func TestGitlabClient_GetPullLabels_EmptyResponse(t *testing.T) {
	logger := logging.NewNoopLogger(t)
	pipelineSuccess, err := os.ReadFile("testdata/gitlab-pipeline-success.json")
	Ok(t, err)

	testServer := httptest.NewServer(
		http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
			switch r.RequestURI {
			case "/api/v4/projects/runatlantis%2Fatlantis/merge_requests/1":
				w.WriteHeader(http.StatusOK)
				w.Write(pipelineSuccess) // nolint: errcheck
			default:
				t.Errorf("got unexpected request at %q", r.RequestURI)
				http.Error(w, "not found", http.StatusNotFound)
			}
		}))

	internalClient, err := gitlab.NewClient("token", gitlab.WithBaseURL(testServer.URL))
	Ok(t, err)
	client := &GitlabClient{
		Client:  internalClient,
		Version: nil,
	}

	labels, err := client.GetPullLabels(
		logger,
		models.Repo{
			FullName: "runatlantis/atlantis",
		}, models.PullRequest{
			Num: 1,
		})
	Ok(t, err)
	Equals(t, 0, len(labels))
}

// GetTeamNamesForUser returns the names of the GitLab groups that the user belongs to.
func TestGitlabClient_GetTeamNamesForUser(t *testing.T) {
	logger := logging.NewNoopLogger(t)

	groupMembershipSuccess, err := os.ReadFile("testdata/gitlab-group-membership-success.json")
	Ok(t, err)

	userSuccess, err := os.ReadFile("testdata/gitlab-user-success.json")
	Ok(t, err)

	userEmpty, err := os.ReadFile("testdata/gitlab-user-none.json")
	Ok(t, err)

	multipleUsers, err := os.ReadFile("testdata/gitlab-user-multiple.json")
	Ok(t, err)

	configuredGroups := []string{"someorg/group1", "someorg/group2", "someorg/group3", "someorg/group4"}

	cases := []struct {
		userName string
		expErr   string
		expTeams []string
	}{
		{
			userName: "testuser",
			expTeams: []string{"someorg/group1", "someorg/group2"},
		},
		{
			userName: "none",
			expErr:   "GET /users returned no user",
		},
		{
			userName: "multiuser",
			expErr:   "GET /users returned more than 1 user",
		},
	}
	for _, c := range cases {
		t.Run(c.userName, func(t *testing.T) {

			testServer := httptest.NewServer(
				http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
					switch r.RequestURI {
					case "/api/v4/users?username=testuser":
						w.WriteHeader(http.StatusOK)
						w.Write(userSuccess) // nolint: errcheck
					case "/api/v4/users?username=none":
						w.WriteHeader(http.StatusOK)
						w.Write(userEmpty) // nolint: errcheck
					case "/api/v4/users?username=multiuser":
						w.WriteHeader(http.StatusOK)
						w.Write(multipleUsers) // nolint: errcheck
					case "/api/v4/groups/someorg%2Fgroup1/members/123", "/api/v4/groups/someorg%2Fgroup2/members/123":
						w.WriteHeader(http.StatusOK)
						w.Write(groupMembershipSuccess) // nolint: errcheck
					case "/api/v4/groups/someorg%2Fgroup3/members/123":
						http.Error(w, "forbidden", http.StatusForbidden)
					case "/api/v4/groups/someorg%2Fgroup4/members/123":
						http.Error(w, "not found", http.StatusNotFound)
					default:
						t.Errorf("got unexpected request at %q", r.RequestURI)
						http.Error(w, "not found", http.StatusNotFound)
					}
				}))
			internalClient, err := gitlab.NewClient("token", gitlab.WithBaseURL(testServer.URL))
			Ok(t, err)
			client := &GitlabClient{
				Client:           internalClient,
				Version:          nil,
				ConfiguredGroups: configuredGroups,
			}

			teams, err := client.GetTeamNamesForUser(
				logger,
				models.Repo{
					Owner: "someorg",
				}, models.User{
					Username: c.userName,
				})
			if c.expErr == "" {
				Ok(t, err)
				Equals(t, c.expTeams, teams)
			} else {
				ErrContains(t, c.expErr, err)

			}

		})
	}
}

func TestGithubClient_DiscardReviews(t *testing.T) {
	logger := logging.NewNoopLogger(t)
	cases := []struct {
		description   string
		repoFullName  string
		pullReqeustId int
		wantErr       bool
	}{
		{
			"success",
			"runatlantis/atlantis",
			42,
			false,
		},
		{
			"error",
			"runatlantis/atlantis",
			32,
			true,
		},
	}
	for _, c := range cases {
		t.Run(c.description, func(t *testing.T) {
			testServer := httptest.NewServer(
				http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
					switch r.RequestURI {
					case "/api/v4/projects/runatlantis%2Fatlantis/merge_requests/42/reset_approvals":
						w.WriteHeader(http.StatusOK)
					case "/api/v4/projects/runatlantis%2Fatlantis/merge_requests/32/reset_approvals":
						http.Error(w, "No bot token", http.StatusUnauthorized)
					default:
						t.Errorf("got unexpected request at %q", r.RequestURI)
						http.Error(w, "not found", http.StatusNotFound)
					}
				}))
			internalClient, err := gitlab.NewClient("token", gitlab.WithBaseURL(testServer.URL))
			Ok(t, err)
			client := &GitlabClient{
				Client:  internalClient,
				Version: nil,
			}

			repo := models.Repo{
				FullName: c.repoFullName,
			}

			pr := models.PullRequest{
				Num: c.pullReqeustId,
			}

			if err := client.DiscardReviews(logger, repo, pr); (err != nil) != c.wantErr {
				t.Errorf("DiscardReviews() error = %v", err)
			}
		})
	}
}
