package client

import (
	"encoding/base64"
	"encoding/json"
	"fmt"
	"io"
	"net/http"
	"net/http/httptest"
	"os"
	"sync/atomic"
	"testing"
	"text/template"
	"time"

	"github.com/google/go-github/v80/github"
	"github.com/goreleaser/goreleaser/v2/internal/artifact"
	"github.com/goreleaser/goreleaser/v2/internal/testctx"
	"github.com/goreleaser/goreleaser/v2/internal/testlib"
	"github.com/goreleaser/goreleaser/v2/pkg/config"
	"github.com/goreleaser/goreleaser/v2/pkg/context"
	"github.com/stretchr/testify/assert"
	"github.com/stretchr/testify/require"
)

func TestNewGitHubClient(t *testing.T) {
	t.Run("good urls", func(t *testing.T) {
		githubURL := "https://github.mycompany.com"
		ctx := testctx.WrapWithCfg(t.Context(), config.Project{
			GitHubURLs: config.GitHubURLs{
				API:    githubURL + "/api/v3",
				Upload: githubURL,
			},
		})

		client, err := newGitHub(ctx, ctx.Token)
		require.NoError(t, err)
		require.Equal(t, githubURL+"/api/v3/", client.client.BaseURL.String())
		require.Equal(t, githubURL+"/api/uploads/", client.client.UploadURL.String())
	})

	t.Run("good urls ending with /", func(t *testing.T) {
		githubURL := "https://github.mycompany.com"
		ctx := testctx.WrapWithCfg(t.Context(), config.Project{
			GitHubURLs: config.GitHubURLs{
				API:    githubURL + "/api/v3/",
				Upload: githubURL + "/api/uploads/",
			},
		})

		client, err := newGitHub(ctx, ctx.Token)
		require.NoError(t, err)
		require.Equal(t, githubURL+"/api/v3/", client.client.BaseURL.String())
		require.Equal(t, githubURL+"/api/uploads/", client.client.UploadURL.String())
	})

	t.Run("bad api url", func(t *testing.T) {
		ctx := testctx.WrapWithCfg(t.Context(), config.Project{
			GitHubURLs: config.GitHubURLs{
				API:    "://github.mycompany.com/api",
				Upload: "https://github.mycompany.com/upload",
			},
		})
		_, err := newGitHub(ctx, ctx.Token)

		require.EqualError(t, err, `parse "://github.mycompany.com/api": missing protocol scheme`)
	})

	t.Run("bad upload url", func(t *testing.T) {
		ctx := testctx.WrapWithCfg(t.Context(), config.Project{
			GitHubURLs: config.GitHubURLs{
				API:    "https://github.mycompany.com/api",
				Upload: "not a url:4994",
			},
		})
		_, err := newGitHub(ctx, ctx.Token)

		require.EqualError(t, err, `parse "not a url:4994": first path segment in URL cannot contain colon`)
	})

	t.Run("template", func(t *testing.T) {
		githubURL := "https://github.mycompany.com"
		ctx := testctx.WrapWithCfg(t.Context(), config.Project{
			Env: []string{
				fmt.Sprintf("GORELEASER_TEST_GITHUB_URLS_API=%s", githubURL),
				fmt.Sprintf("GORELEASER_TEST_GITHUB_URLS_UPLOAD=%s", githubURL),
			},
			GitHubURLs: config.GitHubURLs{
				API:    "{{ .Env.GORELEASER_TEST_GITHUB_URLS_API }}",
				Upload: "{{ .Env.GORELEASER_TEST_GITHUB_URLS_UPLOAD }}",
			},
		})

		client, err := newGitHub(ctx, ctx.Token)
		require.NoError(t, err)
		require.Equal(t, githubURL+"/api/v3/", client.client.BaseURL.String())
		require.Equal(t, githubURL+"/api/uploads/", client.client.UploadURL.String())
	})

	t.Run("template invalid api", func(t *testing.T) {
		ctx := testctx.WrapWithCfg(t.Context(), config.Project{
			GitHubURLs: config.GitHubURLs{
				API: "{{ .Env.GORELEASER_NOT_EXISTS }}",
			},
		})

		_, err := newGitHub(ctx, ctx.Token)
		require.ErrorAs(t, err, &template.ExecError{})
	})

	t.Run("template invalid upload", func(t *testing.T) {
		ctx := testctx.WrapWithCfg(t.Context(), config.Project{
			GitHubURLs: config.GitHubURLs{
				API:    "https://github.mycompany.com/api",
				Upload: "{{ .Env.GORELEASER_NOT_EXISTS }}",
			},
		})

		_, err := newGitHub(ctx, ctx.Token)
		require.ErrorAs(t, err, &template.ExecError{})
	})

	t.Run("template invalid", func(t *testing.T) {
		ctx := testctx.WrapWithCfg(t.Context(), config.Project{
			GitHubURLs: config.GitHubURLs{
				API: "{{.dddddddddd",
			},
		})

		_, err := newGitHub(ctx, ctx.Token)
		require.Error(t, err)
	})
}

func TestGitHubUploadReleaseIDNotInt(t *testing.T) {
	ctx := testctx.Wrap(t.Context())
	client, err := newGitHub(ctx, ctx.Token)
	require.NoError(t, err)

	require.EqualError(
		t,
		client.Upload(ctx, "blah", &artifact.Artifact{}, nil),
		`strconv.ParseInt: parsing "blah": invalid syntax`,
	)
}

func TestGitHubReleaseURLTemplate(t *testing.T) {
	tests := []struct {
		name            string
		downloadURL     string
		wantDownloadURL string
		wantErr         bool
	}{
		{
			name:            "default_download_url",
			downloadURL:     DefaultGitHubDownloadURL,
			wantDownloadURL: "https://github.com/owner/name/releases/download/{{ urlPathEscape .Tag }}/{{ .ArtifactName }}",
		},
		{
			name:            "download_url_template",
			downloadURL:     "{{ .Env.GORELEASER_TEST_GITHUB_URLS_DOWNLOAD }}",
			wantDownloadURL: "https://github.mycompany.com/owner/name/releases/download/{{ urlPathEscape .Tag }}/{{ .ArtifactName }}",
		},
		{
			name:        "download_url_template_invalid_value",
			downloadURL: "{{ .Env.GORELEASER_NOT_EXISTS }}",
			wantErr:     true,
		},
		{
			name:        "download_url_template_invalid",
			downloadURL: "{{.dddddddddd",
			wantErr:     true,
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			ctx := testctx.WrapWithCfg(t.Context(), config.Project{
				Env: []string{
					"GORELEASER_TEST_GITHUB_URLS_DOWNLOAD=https://github.mycompany.com",
				},
				GitHubURLs: config.GitHubURLs{
					Download: tt.downloadURL,
				},
				Release: config.Release{
					GitHub: config.Repo{
						Owner: "owner",
						Name:  "name",
					},
				},
			})
			client, err := newGitHub(ctx, ctx.Token)
			require.NoError(t, err)

			urlTpl, err := client.ReleaseURLTemplate(ctx)
			if tt.wantErr {
				require.Error(t, err)
				return
			}

			require.NoError(t, err)
			require.Equal(t, tt.wantDownloadURL, urlTpl)
		})
	}
}

func TestGitHubCreateReleaseWrongNameTemplate(t *testing.T) {
	ctx := testctx.WrapWithCfg(t.Context(), config.Project{
		Release: config.Release{
			NameTemplate: "{{.dddddddddd",
		},
	})
	client, err := newGitHub(ctx, ctx.Token)
	require.NoError(t, err)

	str, err := client.CreateRelease(ctx, "")
	require.Empty(t, str)
	testlib.RequireTemplateError(t, err)
}

func TestGitHubGetDefaultBranch(t *testing.T) {
	totalRequests := 0
	srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		totalRequests++
		defer r.Body.Close()

		if r.URL.Path == "/api/v3/rate_limit" {
			w.WriteHeader(http.StatusOK)
			fmt.Fprint(w, `{"resources":{"core":{"remaining":120}}}`)
			return
		}

		// Assume the request to create a branch was good
		w.WriteHeader(http.StatusOK)
		fmt.Fprint(w, `{"default_branch": "main"}`)
	}))
	t.Cleanup(srv.Close)

	ctx := testctx.WrapWithCfg(t.Context(), config.Project{
		GitHubURLs: config.GitHubURLs{
			API: srv.URL,
		},
	})

	client, err := newGitHub(ctx, "test-token")
	require.NoError(t, err)
	repo := Repo{
		Owner:  "someone",
		Name:   "something",
		Branch: "somebranch",
	}

	b, err := client.getDefaultBranch(ctx, repo)
	require.NoError(t, err)
	require.Equal(t, "main", b)
	require.Equal(t, 2, totalRequests)
}

func TestGitHubGetDefaultBranchErr(t *testing.T) {
	srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		defer r.Body.Close()

		// Assume the request to create a branch was good
		w.WriteHeader(http.StatusNotImplemented)
		fmt.Fprint(w, "{}")
	}))
	t.Cleanup(srv.Close)

	ctx := testctx.WrapWithCfg(t.Context(), config.Project{
		GitHubURLs: config.GitHubURLs{
			API: srv.URL,
		},
	})
	client, err := newGitHub(ctx, "test-token")
	require.NoError(t, err)
	repo := Repo{
		Owner:  "someone",
		Name:   "something",
		Branch: "somebranch",
	}

	_, err = client.getDefaultBranch(ctx, repo)
	require.Error(t, err)
}

func TestGitHubChangelog(t *testing.T) {
	srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		defer r.Body.Close()

		if r.URL.Path == "/api/v3/repos/someone/something/compare/v1.0.0...v1.1.0" {
			r, err := os.Open("testdata/github/compare.json")
			if assert.NoError(t, err) {
				defer r.Close()
				_, err = io.Copy(w, r)
				assert.NoError(t, err)
			}
			return
		}
		if r.URL.Path == "/api/v3/rate_limit" {
			w.WriteHeader(http.StatusOK)
			fmt.Fprint(w, `{"resources":{"core":{"remaining":120}}}`)
			return
		}
	}))
	t.Cleanup(srv.Close)

	ctx := testctx.WrapWithCfg(t.Context(), config.Project{
		GitHubURLs: config.GitHubURLs{
			API: srv.URL,
		},
	})
	client, err := newGitHub(ctx, "test-token")
	require.NoError(t, err)
	repo := Repo{
		Owner:  "someone",
		Name:   "something",
		Branch: "somebranch",
	}

	log, err := client.Changelog(ctx, repo, "v1.0.0", "v1.1.0")
	require.NoError(t, err)
	require.Equal(t, []ChangelogItem{
		{
			SHA:            "6dcb09b5b57875f334f61aebed695e2e4193db5e",
			Message:        "Fix all the bugs",
			AuthorName:     "Octocat",
			AuthorEmail:    "octo@cat",
			AuthorUsername: "octocat",
		},
	}, log)
}

func TestGitHubReleaseNotes(t *testing.T) {
	srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		defer r.Body.Close()

		if r.URL.Path == "/api/v3/repos/someone/something/releases/generate-notes" {
			r, err := os.Open("testdata/github/releasenotes.json")
			if assert.NoError(t, err) {
				defer r.Close()
				_, err = io.Copy(w, r)
				assert.NoError(t, err)
			}
			return
		}
		if r.URL.Path == "/api/v3/rate_limit" {
			w.WriteHeader(http.StatusOK)
			fmt.Fprint(w, `{"resources":{"core":{"remaining":120}}}`)
			return
		}
	}))
	t.Cleanup(srv.Close)

	ctx := testctx.WrapWithCfg(t.Context(), config.Project{
		GitHubURLs: config.GitHubURLs{
			API: srv.URL,
		},
	})
	client, err := newGitHub(ctx, "test-token")
	require.NoError(t, err)
	repo := Repo{
		Owner:  "someone",
		Name:   "something",
		Branch: "somebranch",
	}

	log, err := client.GenerateReleaseNotes(ctx, repo, "v1.0.0", "v1.1.0")
	require.NoError(t, err)
	require.Equal(t, "**Full Changelog**: https://github.com/someone/something/compare/v1.0.0...v1.1.0", log)
}

func TestGitHubReleaseNotesError(t *testing.T) {
	srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		defer r.Body.Close()

		if r.URL.Path == "/api/v3/repos/someone/something/releases/generate-notes" {
			w.WriteHeader(http.StatusBadRequest)
		}
		if r.URL.Path == "/api/v3/rate_limit" {
			w.WriteHeader(http.StatusOK)
			fmt.Fprint(w, `{"resources":{"core":{"remaining":120}}}`)
			return
		}
	}))
	t.Cleanup(srv.Close)

	ctx := testctx.WrapWithCfg(t.Context(), config.Project{
		GitHubURLs: config.GitHubURLs{
			API: srv.URL,
		},
	})
	client, err := newGitHub(ctx, "test-token")
	require.NoError(t, err)
	repo := Repo{
		Owner:  "someone",
		Name:   "something",
		Branch: "somebranch",
	}

	_, err = client.GenerateReleaseNotes(ctx, repo, "v1.0.0", "v1.1.0")
	require.Error(t, err)
}

func TestGitHubCloseMilestone(t *testing.T) {
	srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		defer r.Body.Close()

		if r.URL.Path == "/api/v3/repos/someone/something/milestones" {
			r, err := os.Open("testdata/github/milestones.json")
			if assert.NoError(t, err) {
				defer r.Close()
				_, err = io.Copy(w, r)
				assert.NoError(t, err)
			}
			return
		}

		if r.URL.Path == "/api/v3/rate_limit" {
			w.WriteHeader(http.StatusOK)
			fmt.Fprint(w, `{"resources":{"core":{"remaining":120}}}`)
			return
		}
	}))
	t.Cleanup(srv.Close)

	ctx := testctx.WrapWithCfg(t.Context(), config.Project{
		GitHubURLs: config.GitHubURLs{
			API: srv.URL,
		},
	})
	client, err := newGitHub(ctx, "test-token")
	require.NoError(t, err)
	repo := Repo{
		Owner: "someone",
		Name:  "something",
	}

	require.NoError(t, client.CloseMilestone(ctx, repo, "v1.13.0"))
}

const testPRTemplate = "fake template\n- [ ] mark this\n---"

func TestGitHubOpenPullRequestCrossRepo(t *testing.T) {
	srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		defer r.Body.Close()

		if r.URL.Path == "/api/v3/repos/someone/something/contents/.github/PULL_REQUEST_TEMPLATE.md" {
			content := github.RepositoryContent{
				Encoding: github.Ptr("base64"),
				Content:  github.Ptr(base64.StdEncoding.EncodeToString([]byte(testPRTemplate))),
			}
			bts, _ := json.Marshal(content)
			_, _ = w.Write(bts)
			return
		}

		if r.URL.Path == "/api/v3/repos/someone/something/pulls" {
			got, err := io.ReadAll(r.Body)
			assert.NoError(t, err)
			var pr github.NewPullRequest
			assert.NoError(t, json.Unmarshal(got, &pr))
			assert.Equal(t, "main", pr.GetBase())
			assert.Equal(t, "someoneelse:something:foo", pr.GetHead())
			assert.Equal(t, testPRTemplate+"\n"+prFooter, pr.GetBody())
			r, err := os.Open("testdata/github/pull.json")
			if assert.NoError(t, err) {
				defer r.Close()
				_, err = io.Copy(w, r)
				assert.NoError(t, err)
			}
			return
		}

		if r.URL.Path == "/api/v3/rate_limit" {
			w.WriteHeader(http.StatusOK)
			fmt.Fprint(w, `{"resources":{"core":{"remaining":120}}}`)
			return
		}

		t.Error("unhandled request: " + r.URL.Path)
	}))
	t.Cleanup(srv.Close)

	ctx := testctx.WrapWithCfg(t.Context(), config.Project{
		GitHubURLs: config.GitHubURLs{
			API: srv.URL,
		},
	})
	client, err := newGitHub(ctx, "test-token")
	require.NoError(t, err)
	base := Repo{
		Owner:  "someone",
		Name:   "something",
		Branch: "main",
	}
	head := Repo{
		Owner:  "someoneelse",
		Name:   "something",
		Branch: "foo",
	}
	require.NoError(t, client.OpenPullRequest(ctx, base, head, "some title", false))
}

func TestGitHubOpenPullRequestHappyPath(t *testing.T) {
	srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		defer r.Body.Close()

		if r.URL.Path == "/api/v3/repos/someone/something/contents/.github/PULL_REQUEST_TEMPLATE.md" {
			content := github.RepositoryContent{
				Encoding: github.Ptr("base64"),
				Content:  github.Ptr(base64.StdEncoding.EncodeToString([]byte(testPRTemplate))),
			}
			bts, _ := json.Marshal(content)
			_, _ = w.Write(bts)
			return
		}

		if r.URL.Path == "/api/v3/repos/someone/something/pulls" {
			r, err := os.Open("testdata/github/pull.json")
			if assert.NoError(t, err) {
				defer r.Close()
				_, err = io.Copy(w, r)
				assert.NoError(t, err)
			}
			return
		}

		if r.URL.Path == "/api/v3/rate_limit" {
			w.WriteHeader(http.StatusOK)
			fmt.Fprint(w, `{"resources":{"core":{"remaining":120}}}`)
			return
		}

		t.Error("unhandled request: " + r.URL.Path)
	}))
	t.Cleanup(srv.Close)

	ctx := testctx.WrapWithCfg(t.Context(), config.Project{
		GitHubURLs: config.GitHubURLs{
			API: srv.URL,
		},
	})
	client, err := newGitHub(ctx, "test-token")
	require.NoError(t, err)
	repo := Repo{
		Owner:  "someone",
		Name:   "something",
		Branch: "main",
	}

	require.NoError(t, client.OpenPullRequest(ctx, repo, Repo{}, "some title", false))
}

func TestGitHubOpenPullRequestNoBaseBranchDraft(t *testing.T) {
	srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		defer r.Body.Close()

		if r.URL.Path == "/api/v3/repos/someone/something/contents/.github/PULL_REQUEST_TEMPLATE.md" {
			w.WriteHeader(http.StatusNotFound)
			return
		}

		if r.URL.Path == "/api/v3/repos/someone/something/pulls" {
			got, err := io.ReadAll(r.Body)
			assert.NoError(t, err)
			var pr github.NewPullRequest
			assert.NoError(t, json.Unmarshal(got, &pr))
			assert.Equal(t, "main", pr.GetBase())
			assert.Equal(t, "someone:something:foo", pr.GetHead())
			assert.True(t, pr.GetDraft())

			r, err := os.Open("testdata/github/pull.json")
			if assert.NoError(t, err) {
				defer r.Close()
				_, err = io.Copy(w, r)
				assert.NoError(t, err)
			}
			return
		}

		if r.URL.Path == "/api/v3/repos/someone/something" {
			w.WriteHeader(http.StatusOK)
			fmt.Fprint(w, `{"default_branch": "main"}`)
			return
		}

		if r.URL.Path == "/api/v3/rate_limit" {
			w.WriteHeader(http.StatusOK)
			fmt.Fprint(w, `{"resources":{"core":{"remaining":120}}}`)
			return
		}

		t.Error("unhandled request: " + r.URL.Path)
	}))
	t.Cleanup(srv.Close)

	ctx := testctx.WrapWithCfg(t.Context(), config.Project{
		GitHubURLs: config.GitHubURLs{
			API: srv.URL,
		},
	})
	client, err := newGitHub(ctx, "test-token")
	require.NoError(t, err)
	repo := Repo{
		Owner: "someone",
		Name:  "something",
	}

	require.NoError(t, client.OpenPullRequest(ctx, repo, Repo{
		Branch: "foo",
	}, "some title", true))
}

func TestGitHubOpenPullRequestPRExists(t *testing.T) {
	srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		defer r.Body.Close()

		if r.URL.Path == "/api/v3/repos/someone/something/contents/.github/PULL_REQUEST_TEMPLATE.md" {
			w.WriteHeader(http.StatusNotFound)
			return
		}

		if r.URL.Path == "/api/v3/repos/someone/something/pulls" {
			w.WriteHeader(http.StatusUnprocessableEntity)
			r, err := os.Open("testdata/github/pull.json")
			if assert.NoError(t, err) {
				defer r.Close()
				_, err = io.Copy(w, r)
				assert.NoError(t, err)
			}
			return
		}

		if r.URL.Path == "/api/v3/rate_limit" {
			w.WriteHeader(http.StatusOK)
			fmt.Fprint(w, `{"resources":{"core":{"remaining":120}}}`)
			return
		}

		t.Error("unhandled request: " + r.URL.Path)
	}))
	t.Cleanup(srv.Close)

	ctx := testctx.WrapWithCfg(t.Context(), config.Project{
		GitHubURLs: config.GitHubURLs{
			API: srv.URL,
		},
	})
	client, err := newGitHub(ctx, "test-token")
	require.NoError(t, err)
	repo := Repo{
		Owner:  "someone",
		Name:   "something",
		Branch: "main",
	}

	require.NoError(t, client.OpenPullRequest(ctx, repo, Repo{}, "some title", false))
}

func TestGitHubOpenPullRequestBaseEmpty(t *testing.T) {
	srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		defer r.Body.Close()

		if r.URL.Path == "/api/v3/repos/someone/something/contents/.github/PULL_REQUEST_TEMPLATE.md" {
			w.WriteHeader(http.StatusNotFound)
			return
		}

		if r.URL.Path == "/api/v3/repos/someone/something/pulls" {
			r, err := os.Open("testdata/github/pull.json")
			if assert.NoError(t, err) {
				defer r.Close()
				_, err = io.Copy(w, r)
				assert.NoError(t, err)
			}
			return
		}

		if r.URL.Path == "/api/v3/repos/someone/something" {
			w.WriteHeader(http.StatusOK)
			fmt.Fprint(w, `{"default_branch": "main"}`)
			return
		}

		if r.URL.Path == "/api/v3/rate_limit" {
			w.WriteHeader(http.StatusOK)
			fmt.Fprint(w, `{"resources":{"core":{"remaining":120}}}`)
			return
		}

		t.Error("unhandled request: " + r.URL.Path)
	}))
	t.Cleanup(srv.Close)

	ctx := testctx.WrapWithCfg(t.Context(), config.Project{
		GitHubURLs: config.GitHubURLs{
			API: srv.URL,
		},
	})
	client, err := newGitHub(ctx, "test-token")
	require.NoError(t, err)
	repo := Repo{
		Owner:  "someone",
		Name:   "something",
		Branch: "foo",
	}

	require.NoError(t, client.OpenPullRequest(ctx, Repo{}, repo, "some title", false))
}

func TestGitHubOpenPullRequestHeadEmpty(t *testing.T) {
	srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		defer r.Body.Close()

		if r.URL.Path == "/api/v3/repos/someone/something/contents/.github/PULL_REQUEST_TEMPLATE.md" {
			w.WriteHeader(http.StatusNotFound)
			return
		}

		if r.URL.Path == "/api/v3/repos/someone/something/pulls" {
			r, err := os.Open("testdata/github/pull.json")
			if assert.NoError(t, err) {
				defer r.Close()
				_, err = io.Copy(w, r)
				assert.NoError(t, err)
			}
			return
		}

		if r.URL.Path == "/api/v3/repos/someone/something" {
			w.WriteHeader(http.StatusOK)
			fmt.Fprint(w, `{"default_branch": "main"}`)
			return
		}

		if r.URL.Path == "/api/v3/rate_limit" {
			w.WriteHeader(http.StatusOK)
			fmt.Fprint(w, `{"resources":{"core":{"remaining":120}}}`)
			return
		}

		t.Error("unhandled request: " + r.URL.Path)
	}))
	t.Cleanup(srv.Close)

	ctx := testctx.WrapWithCfg(t.Context(), config.Project{
		GitHubURLs: config.GitHubURLs{
			API: srv.URL,
		},
	})
	client, err := newGitHub(ctx, "test-token")
	require.NoError(t, err)
	repo := Repo{
		Owner:  "someone",
		Name:   "something",
		Branch: "main",
	}

	require.NoError(t, client.OpenPullRequest(ctx, repo, Repo{}, "some title", false))
}

func TestGitHubCreateFileHappyPathCreate(t *testing.T) {
	srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		defer r.Body.Close()

		if r.URL.Path == "/api/v3/repos/someone/something" {
			w.WriteHeader(http.StatusOK)
			fmt.Fprint(w, `{"default_branch": "main"}`)
			return
		}

		if r.URL.Path == "/api/v3/repos/someone/something/contents/file.txt" && r.Method == http.MethodGet {
			w.WriteHeader(http.StatusNotFound)
			return
		}

		if r.URL.Path == "/api/v3/repos/someone/something/contents/file.txt" && r.Method == http.MethodPut {
			var data github.RepositoryContentFileOptions
			assert.NoError(t, json.NewDecoder(r.Body).Decode(&data))
			assert.Nil(t, data.SHA)
			w.WriteHeader(http.StatusOK)
			return
		}

		if r.URL.Path == "/api/v3/rate_limit" {
			w.WriteHeader(http.StatusOK)
			fmt.Fprint(w, `{"resources":{"core":{"remaining":120}}}`)
			return
		}

		t.Error("unhandled request: " + r.URL.Path)
	}))
	t.Cleanup(srv.Close)

	ctx := testctx.WrapWithCfg(t.Context(), config.Project{
		GitHubURLs: config.GitHubURLs{
			API: srv.URL,
		},
	})
	client, err := newGitHub(ctx, "test-token")
	require.NoError(t, err)
	repo := Repo{
		Owner: "someone",
		Name:  "something",
	}

	require.NoError(t, client.CreateFile(ctx, config.CommitAuthor{}, repo, []byte("content"), "file.txt", "message"))
}

func TestGitHubCreateFileHappyPathUpdate(t *testing.T) {
	srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		defer r.Body.Close()

		if r.URL.Path == "/api/v3/repos/someone/something" {
			w.WriteHeader(http.StatusOK)
			fmt.Fprint(w, `{"default_branch": "main"}`)
			return
		}

		if r.URL.Path == "/api/v3/repos/someone/something/contents/file.txt" && r.Method == http.MethodGet {
			w.WriteHeader(http.StatusOK)
			fmt.Fprint(w, `{"sha": "fake"}`)
			return
		}

		if r.URL.Path == "/api/v3/repos/someone/something/contents/file.txt" && r.Method == http.MethodPut {
			var data github.RepositoryContentFileOptions
			assert.NoError(t, json.NewDecoder(r.Body).Decode(&data))
			assert.Equal(t, "fake", data.GetSHA())
			w.WriteHeader(http.StatusOK)
			return
		}

		if r.URL.Path == "/api/v3/rate_limit" {
			w.WriteHeader(http.StatusOK)
			fmt.Fprint(w, `{"resources":{"core":{"remaining":120}}}`)
			return
		}

		t.Error("unhandled request: " + r.URL.Path)
	}))
	t.Cleanup(srv.Close)

	ctx := testctx.WrapWithCfg(t.Context(), config.Project{
		GitHubURLs: config.GitHubURLs{
			API: srv.URL,
		},
	})
	client, err := newGitHub(ctx, "test-token")
	require.NoError(t, err)
	repo := Repo{
		Owner: "someone",
		Name:  "something",
	}

	require.NoError(t, client.CreateFile(ctx, config.CommitAuthor{}, repo, []byte("content"), "file.txt", "message"))
}

func TestGitHubCreateFileFeatureBranchAlreadyExists(t *testing.T) {
	srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		defer r.Body.Close()

		if r.URL.Path == "/api/v3/repos/someone/something/branches/feature" && r.Method == http.MethodGet {
			w.WriteHeader(http.StatusNotFound)
			return
		}

		if r.URL.Path == "/api/v3/repos/someone/something/git/ref/heads/main" {
			fmt.Fprint(w, `{"object": {"sha": "fake-sha"}}`)
			return
		}

		if r.URL.Path == "/api/v3/repos/someone/something/git/refs" && r.Method == http.MethodPost {
			w.WriteHeader(http.StatusUnprocessableEntity)
			fmt.Fprintf(w, `{"message": "Reference already exists"}`)
			return
		}

		if r.URL.Path == "/api/v3/repos/someone/something" {
			w.WriteHeader(http.StatusOK)
			fmt.Fprint(w, `{"default_branch": "main"}`)
			return
		}

		if r.URL.Path == "/api/v3/repos/someone/something/contents/file.txt" && r.Method == http.MethodGet {
			w.WriteHeader(http.StatusNotFound)
			return
		}

		if r.URL.Path == "/api/v3/repos/someone/something/contents/file.txt" && r.Method == http.MethodPut {
			w.WriteHeader(http.StatusOK)
			return
		}

		if r.URL.Path == "/api/v3/rate_limit" {
			w.WriteHeader(http.StatusOK)
			fmt.Fprint(w, `{"resources":{"core":{"remaining":120}}}`)
			return
		}

		t.Error("unhandled request: " + r.Method + " " + r.URL.Path)
	}))
	t.Cleanup(srv.Close)

	ctx := testctx.WrapWithCfg(t.Context(), config.Project{
		GitHubURLs: config.GitHubURLs{
			API: srv.URL,
		},
	})
	client, err := newGitHub(ctx, "test-token")
	require.NoError(t, err)
	repo := Repo{
		Owner:  "someone",
		Name:   "something",
		Branch: "feature",
	}

	require.NoError(t, client.CreateFile(ctx, config.CommitAuthor{}, repo, []byte("content"), "file.txt", "message"))
}

func TestGitHubCreateFileFeatureBranchDoesNotExist(t *testing.T) {
	srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		defer r.Body.Close()

		if r.URL.Path == "/api/v3/repos/someone/something/branches/feature" && r.Method == http.MethodGet {
			w.WriteHeader(http.StatusNotFound)
			return
		}

		if r.URL.Path == "/api/v3/repos/someone/something/git/ref/heads/main" {
			fmt.Fprint(w, `{"object": {"sha": "fake-sha"}}`)
			return
		}

		if r.URL.Path == "/api/v3/repos/someone/something/git/refs" && r.Method == http.MethodPost {
			w.WriteHeader(http.StatusOK)
			return
		}

		if r.URL.Path == "/api/v3/repos/someone/something" {
			w.WriteHeader(http.StatusOK)
			fmt.Fprint(w, `{"default_branch": "main"}`)
			return
		}

		if r.URL.Path == "/api/v3/repos/someone/something/contents/file.txt" && r.Method == http.MethodGet {
			w.WriteHeader(http.StatusNotFound)
			return
		}

		if r.URL.Path == "/api/v3/repos/someone/something/contents/file.txt" && r.Method == http.MethodPut {
			w.WriteHeader(http.StatusOK)
			return
		}

		if r.URL.Path == "/api/v3/rate_limit" {
			w.WriteHeader(http.StatusOK)
			fmt.Fprint(w, `{"resources":{"core":{"remaining":120}}}`)
			return
		}

		t.Error("unhandled request: " + r.Method + " " + r.URL.Path)
	}))
	t.Cleanup(srv.Close)

	ctx := testctx.WrapWithCfg(t.Context(), config.Project{
		GitHubURLs: config.GitHubURLs{
			API: srv.URL,
		},
	})
	client, err := newGitHub(ctx, "test-token")
	require.NoError(t, err)
	repo := Repo{
		Owner:  "someone",
		Name:   "something",
		Branch: "feature",
	}

	require.NoError(t, client.CreateFile(ctx, config.CommitAuthor{}, repo, []byte("content"), "file.txt", "message"))
}

func TestGitHubCreateFileFeatureBranchNilObject(t *testing.T) {
	srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		defer r.Body.Close()

		if r.URL.Path == "/api/v3/repos/someone/something/branches/feature" && r.Method == http.MethodGet {
			w.WriteHeader(http.StatusNotFound)
			return
		}

		if r.URL.Path == "/api/v3/repos/someone/something/git/ref/heads/main" {
			// Return ref with nil object
			fmt.Fprint(w, `{}`)
			return
		}

		if r.URL.Path == "/api/v3/repos/someone/something" {
			w.WriteHeader(http.StatusOK)
			fmt.Fprint(w, `{"default_branch": "main"}`)
			return
		}

		if r.URL.Path == "/api/v3/rate_limit" {
			w.WriteHeader(http.StatusOK)
			fmt.Fprint(w, `{"resources":{"core":{"remaining":120}}}`)
			return
		}

		t.Error("unhandled request: " + r.Method + " " + r.URL.Path)
	}))
	t.Cleanup(srv.Close)

	ctx := testctx.WrapWithCfg(t.Context(), config.Project{
		GitHubURLs: config.GitHubURLs{
			API: srv.URL,
		},
	})
	client, err := newGitHub(ctx, "test-token")
	require.NoError(t, err)
	repo := Repo{
		Owner:  "someone",
		Name:   "something",
		Branch: "feature",
	}

	err = client.CreateFile(ctx, config.CommitAuthor{}, repo, []byte("content"), "file.txt", "message")
	require.Error(t, err)
	require.Contains(t, err.Error(), "could not create ref")
	require.Contains(t, err.Error(), "sha must be provided")
}

func TestGitHubCheckRateLimit(t *testing.T) {
	now := time.Now().UTC()
	reset := now.Add(1392 * time.Millisecond)
	var first atomic.Bool
	srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		defer r.Body.Close()
		if r.URL.Path == "/api/v3/rate_limit" {
			w.WriteHeader(http.StatusOK)
			resetstr, _ := github.Timestamp{Time: reset}.MarshalJSON()
			if first.Load() {
				// second time asking for the rate limit
				fmt.Fprintf(w, `{"resources":{"core":{"remaining":138,"reset":%s}}}`, string(resetstr))
				return
			}

			// first time asking for the rate limit
			fmt.Fprintf(w, `{"resources":{"core":{"remaining":98,"reset":%s}}}`, string(resetstr))
			first.Store(true)
			return
		}
		t.Error("unhandled request: " + r.Method + " " + r.URL.Path)
	}))
	t.Cleanup(srv.Close)

	ctx := testctx.WrapWithCfg(t.Context(), config.Project{
		GitHubURLs: config.GitHubURLs{
			API: srv.URL,
		},
	})
	client, err := newGitHub(ctx, "test-token")
	require.NoError(t, err)
	client.checkRateLimit(ctx)
	require.True(t, time.Now().UTC().After(reset))
}

func TestGitHubCreateRelease(t *testing.T) {
	srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		defer r.Body.Close()

		if r.URL.Path == "/api/v3/repos/goreleaser/test/releases/tags/v1.0.0" {
			w.WriteHeader(http.StatusNotFound)
			return
		}

		if r.URL.Path == "/api/v3/repos/goreleaser/test/releases" && r.Method == http.MethodPost {
			got, err := io.ReadAll(r.Body)
			assert.NoError(t, err)
			assert.JSONEq(t, `{"name": "v1.0.0", "tag_name": "v1.0.0", "target_commitish": "test", "body": "test release", "draft": true, "prerelease": false}`, string(got))

			w.WriteHeader(http.StatusCreated)
			fmt.Fprint(w, `{"id": 1, "html_url": "https://github.com/goreleaser/test/releases/v1.0.0"}`)
			return
		}

		if r.URL.Path == "/api/v3/rate_limit" {
			w.WriteHeader(http.StatusOK)
			fmt.Fprint(w, `{"resources":{"core":{"remaining":120}}}`)
			return
		}

		t.Error("unhandled request: " + r.Method + " " + r.URL.Path)
	}))
	t.Cleanup(srv.Close)

	ctx := testctx.WrapWithCfg(t.Context(),
		config.Project{
			GitHubURLs: config.GitHubURLs{
				API: srv.URL,
			},
			Release: config.Release{
				NameTemplate: "v1.0.0",
				GitHub: config.Repo{
					Owner: "goreleaser",
					Name:  "test",
				},
				TargetCommitish: "test",
			},
		},
		testctx.WithGitInfo(context.GitInfo{
			CurrentTag: "v1.0.0",
		}),
	)

	client, err := newGitHub(ctx, "test-token")
	require.NoError(t, err)

	release, err := client.CreateRelease(ctx, "test release")
	require.NoError(t, err)
	require.Equal(t, "1", release)
}

func TestGitHubCreateReleaseDeleteExistingDraft(t *testing.T) {
	srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		defer r.Body.Close()

		if r.URL.Path == "/api/v3/repos/goreleaser/test/releases" && r.Method == http.MethodGet {
			w.WriteHeader(http.StatusOK)
			r, err := os.Open("testdata/github/releases.json")
			if assert.NoError(t, err) {
				defer r.Close()
				_, err = io.Copy(w, r)
				assert.NoError(t, err)
			}
			return
		}

		if r.URL.Path == "/api/v3/repos/goreleaser/test/releases/1" && r.Method == http.MethodDelete {
			w.WriteHeader(http.StatusNoContent)
			return
		}

		if r.URL.Path == "/api/v3/repos/goreleaser/test/releases/tags/v1.0.0" {
			w.WriteHeader(http.StatusNotFound)
			return
		}

		if r.URL.Path == "/api/v3/repos/goreleaser/test/releases" && r.Method == http.MethodPost {
			w.WriteHeader(http.StatusCreated)
			fmt.Fprint(w, `{"id": 2, "html_url": "https://github.com/goreleaser/test/releases/v1.0.0"}`)
			return
		}

		if r.URL.Path == "/api/v3/rate_limit" {
			w.WriteHeader(http.StatusOK)
			fmt.Fprint(w, `{"resources":{"core":{"remaining":120}}}`)
			return
		}

		t.Error("unhandled request: " + r.Method + " " + r.URL.Path)
	}))
	t.Cleanup(srv.Close)

	ctx := testctx.WrapWithCfg(t.Context(),
		config.Project{
			GitHubURLs: config.GitHubURLs{
				API: srv.URL,
			},
			Release: config.Release{
				NameTemplate: "v1.0.0",
				GitHub: config.Repo{
					Owner: "goreleaser",
					Name:  "test",
				},
				Draft:                true,
				ReplaceExistingDraft: true,
			},
		},
		testctx.WithGitInfo(context.GitInfo{
			CurrentTag: "v1.0.0",
		}),
	)

	client, err := newGitHub(ctx, "test-token")
	require.NoError(t, err)

	release, err := client.CreateRelease(ctx, "test draft release")
	require.NoError(t, err)
	require.Equal(t, "2", release)
}

func TestGitHubCreateReleaseUpdateExisting(t *testing.T) {
	srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		defer r.Body.Close()

		if r.URL.Path == "/api/v3/repos/goreleaser/test/releases/tags/v1.0.0" {
			w.WriteHeader(http.StatusOK)
			fmt.Fprint(w, `{"id": 3, "name": "v1.0.0", "body": "This is an existing release"}`)
			return
		}

		if r.URL.Path == "/api/v3/repos/goreleaser/test/releases/3" && r.Method == http.MethodPatch {
			got, err := io.ReadAll(r.Body)
			assert.NoError(t, err)
			assert.JSONEq(t, `{"name": "v1.0.0", "tag_name": "v1.0.0", "body": "This is an existing release", "prerelease": false}`, string(got))

			w.WriteHeader(http.StatusOK)
			fmt.Fprint(w, `{"id": 3, "name": "v1.0.0", "body": "This is an existing release"}`)
			return
		}

		if r.URL.Path == "/api/v3/rate_limit" {
			w.WriteHeader(http.StatusOK)
			fmt.Fprint(w, `{"resources":{"core":{"remaining":120}}}`)
			return
		}

		t.Error("unhandled request: " + r.Method + " " + r.URL.Path)
	}))
	t.Cleanup(srv.Close)

	ctx := testctx.WrapWithCfg(t.Context(),
		config.Project{
			GitHubURLs: config.GitHubURLs{
				API: srv.URL,
			},
			Release: config.Release{
				NameTemplate: "v1.0.0",
				GitHub: config.Repo{
					Owner: "goreleaser",
					Name:  "test",
				},
			},
		},
		testctx.WithGitInfo(context.GitInfo{
			CurrentTag: "v1.0.0",
		}),
	)

	client, err := newGitHub(ctx, "test-token")
	require.NoError(t, err)

	release, err := client.CreateRelease(ctx, "test update release")
	require.NoError(t, err)
	require.Equal(t, "3", release)
}

func TestGitHubCreateReleaseUseExistingDraft(t *testing.T) {
	srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		defer r.Body.Close()

		if r.URL.Path == "/api/v3/repos/goreleaser/test/releases" && r.Method == http.MethodGet {
			w.WriteHeader(http.StatusOK)
			r, err := os.Open("testdata/github/releases.json")
			if assert.NoError(t, err) {
				defer r.Close()
				_, err = io.Copy(w, r)
				assert.NoError(t, err)
			}
			return
		}

		if r.URL.Path == "/api/v3/repos/goreleaser/test/releases/1" && r.Method == http.MethodPatch {
			got, err := io.ReadAll(r.Body)
			assert.NoError(t, err)
			assert.JSONEq(t, `{"name": "v1.0.0", "tag_name": "v1.0.0", "body": "Existing draft release", "draft": true, "prerelease": false}`, string(got))

			w.WriteHeader(http.StatusOK)
			fmt.Fprint(w, `{"id": 1, "name": "v1.0.0"}`)
			return
		}

		if r.URL.Path == "/api/v3/rate_limit" {
			w.WriteHeader(http.StatusOK)
			fmt.Fprint(w, `{"resources":{"core":{"remaining":120}}}`)
			return
		}

		t.Error("unhandled request: " + r.Method + " " + r.URL.Path)
	}))
	t.Cleanup(srv.Close)

	ctx := testctx.WrapWithCfg(t.Context(),
		config.Project{
			GitHubURLs: config.GitHubURLs{
				API: srv.URL,
			},
			Release: config.Release{
				NameTemplate: "v1.0.0",
				GitHub: config.Repo{
					Owner: "goreleaser",
					Name:  "test",
				},
				UseExistingDraft: true,
			},
		},
		testctx.WithGitInfo(context.GitInfo{
			CurrentTag: "v1.0.0",
		}),
	)

	client, err := newGitHub(ctx, "test-token")
	require.NoError(t, err)

	release, err := client.CreateRelease(ctx, "test update draft release")
	require.NoError(t, err)
	require.Equal(t, "1", release)
}

func TestGitHubCreateFileWithGitHubAppToken(t *testing.T) {
	srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		defer r.Body.Close()

		if r.URL.Path == "/api/v3/repos/someone/something" {
			w.WriteHeader(http.StatusOK)
			fmt.Fprint(w, `{"default_branch": "main"}`)
			return
		}

		if r.URL.Path == "/api/v3/repos/someone/something/contents/file.txt" && r.Method == http.MethodGet {
			w.WriteHeader(http.StatusNotFound)
			return
		}

		if r.URL.Path == "/api/v3/repos/someone/something/contents/file.txt" && r.Method == http.MethodPut {
			// Verify that committer is not set in the request
			body, err := io.ReadAll(r.Body)
			assert.NoError(t, err)

			var reqData map[string]interface{}
			assert.NoError(t, json.Unmarshal(body, &reqData))

			// Verify committer is not present when using GitHub App token
			_, hasCommitter := reqData["committer"]
			assert.False(t, hasCommitter, "committer should not be set when using GitHub App token")

			w.WriteHeader(http.StatusOK)
			return
		}

		if r.URL.Path == "/api/v3/rate_limit" {
			w.WriteHeader(http.StatusOK)
			fmt.Fprint(w, `{"resources":{"core":{"remaining":120}}}`)
			return
		}

		t.Error("unhandled request: " + r.URL.Path)
	}))
	t.Cleanup(srv.Close)

	ctx := testctx.WrapWithCfg(t.Context(), config.Project{
		GitHubURLs: config.GitHubURLs{
			API: srv.URL,
		},
	})
	client, err := newGitHub(ctx, "test-token")
	require.NoError(t, err)
	repo := Repo{
		Owner: "someone",
		Name:  "something",
	}

	require.NoError(t, client.CreateFile(ctx, config.CommitAuthor{
		UseGitHubAppToken: true,
	}, repo, []byte("content"), "file.txt", "message"))
}

func TestGitHubCreateFileWithoutGitHubAppToken(t *testing.T) {
	srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		defer r.Body.Close()

		if r.URL.Path == "/api/v3/repos/someone/something" {
			w.WriteHeader(http.StatusOK)
			fmt.Fprint(w, `{"default_branch": "main"}`)
			return
		}

		if r.URL.Path == "/api/v3/repos/someone/something/contents/file.txt" && r.Method == http.MethodGet {
			w.WriteHeader(http.StatusNotFound)
			return
		}

		if r.URL.Path == "/api/v3/repos/someone/something/contents/file.txt" && r.Method == http.MethodPut {
			// Verify that committer is set in the request
			body, err := io.ReadAll(r.Body)
			assert.NoError(t, err)

			var reqData map[string]interface{}
			assert.NoError(t, json.Unmarshal(body, &reqData))

			// Verify committer is present when not using GitHub App token
			committer, hasCommitter := reqData["committer"]
			assert.True(t, hasCommitter, "committer should be set when not using GitHub App token")

			committerMap := committer.(map[string]interface{})
			assert.Equal(t, "test-author", committerMap["name"])
			assert.Equal(t, "test@example.com", committerMap["email"])

			w.WriteHeader(http.StatusOK)
			return
		}

		if r.URL.Path == "/api/v3/rate_limit" {
			w.WriteHeader(http.StatusOK)
			fmt.Fprint(w, `{"resources":{"core":{"remaining":120}}}`)
			return
		}

		t.Error("unhandled request: " + r.URL.Path)
	}))
	t.Cleanup(srv.Close)

	ctx := testctx.WrapWithCfg(t.Context(), config.Project{
		GitHubURLs: config.GitHubURLs{
			API: srv.URL,
		},
	})
	client, err := newGitHub(ctx, "test-token")
	require.NoError(t, err)
	repo := Repo{
		Owner: "someone",
		Name:  "something",
	}

	require.NoError(t, client.CreateFile(ctx, config.CommitAuthor{
		Name:  "test-author",
		Email: "test@example.com",
	}, repo, []byte("content"), "file.txt", "message"))
}

// TODO: test create upload file to release
// TODO: test create PR
