// Copyright 2024 The Forgejo Authors. All rights reserved.
// SPDX-License-Identifier: MIT

package integration

import (
	"fmt"
	"net/http"
	"net/http/httptest"
	"net/url"
	"strings"
	"testing"
	"time"

	"code.gitea.io/gitea/models/db"
	git_model "code.gitea.io/gitea/models/git"
	issues_model "code.gitea.io/gitea/models/issues"
	unit_model "code.gitea.io/gitea/models/unit"
	"code.gitea.io/gitea/models/unittest"
	user_model "code.gitea.io/gitea/models/user"
	"code.gitea.io/gitea/modules/git"
	"code.gitea.io/gitea/modules/translation"
	gitea_context "code.gitea.io/gitea/services/context"
	issue_service "code.gitea.io/gitea/services/issue"
	pull_service "code.gitea.io/gitea/services/pull"
	repo_service "code.gitea.io/gitea/services/repository"
	files_service "code.gitea.io/gitea/services/repository/files"
	"code.gitea.io/gitea/tests"

	"github.com/stretchr/testify/assert"
	"github.com/stretchr/testify/require"
)

func TestPullrequestReopen(t *testing.T) {
	onGiteaRun(t, func(t *testing.T, u *url.URL) {
		user2 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2})
		org26 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 26})

		// Create an base repository.
		baseRepo, _, f := CreateDeclarativeRepo(t, user2, "reopen-base",
			[]unit_model.Type{unit_model.TypePullRequests}, nil, nil,
		)
		defer f()

		// Create a new branch on the base branch, so it can be deleted later.
		_, err := files_service.ChangeRepoFiles(git.DefaultContext, baseRepo, user2, &files_service.ChangeRepoFilesOptions{
			Files: []*files_service.ChangeRepoFile{
				{
					Operation:     "update",
					TreePath:      "README.md",
					ContentReader: strings.NewReader("New README.md"),
				},
			},
			Message:   "Modify README for base",
			OldBranch: "main",
			NewBranch: "base-branch",
			Author: &files_service.IdentityOptions{
				Name:  user2.Name,
				Email: user2.Email,
			},
			Committer: &files_service.IdentityOptions{
				Name:  user2.Name,
				Email: user2.Email,
			},
			Dates: &files_service.CommitDateOptions{
				Author:    time.Now(),
				Committer: time.Now(),
			},
		})
		require.NoError(t, err)

		// Create an head repository.
		headRepo, err := repo_service.ForkRepository(git.DefaultContext, user2, org26, repo_service.ForkRepoOptions{
			BaseRepo: baseRepo,
			Name:     "reopen-head",
		})
		require.NoError(t, err)
		assert.NotEmpty(t, headRepo)

		// Add a change to the head repository, so a pull request can be opened.
		_, err = files_service.ChangeRepoFiles(git.DefaultContext, headRepo, user2, &files_service.ChangeRepoFilesOptions{
			Files: []*files_service.ChangeRepoFile{
				{
					Operation:     "update",
					TreePath:      "README.md",
					ContentReader: strings.NewReader("Updated README.md"),
				},
			},
			Message:   "Modify README for head",
			OldBranch: "main",
			NewBranch: "head-branch",
			Author: &files_service.IdentityOptions{
				Name:  user2.Name,
				Email: user2.Email,
			},
			Committer: &files_service.IdentityOptions{
				Name:  user2.Name,
				Email: user2.Email,
			},
			Dates: &files_service.CommitDateOptions{
				Author:    time.Now(),
				Committer: time.Now(),
			},
		})
		require.NoError(t, err)

		// Create the pull request.
		pullIssue := &issues_model.Issue{
			RepoID:   baseRepo.ID,
			Title:    "Testing reopen functionality",
			PosterID: user2.ID,
			Poster:   user2,
			IsPull:   true,
		}
		pullRequest := &issues_model.PullRequest{
			HeadRepoID: headRepo.ID,
			BaseRepoID: baseRepo.ID,
			HeadBranch: "head-branch",
			BaseBranch: "base-branch",
			HeadRepo:   headRepo,
			BaseRepo:   baseRepo,
			Type:       issues_model.PullRequestGitea,
		}
		err = pull_service.NewPullRequest(git.DefaultContext, baseRepo, pullIssue, nil, nil, pullRequest, nil)
		require.NoError(t, err)

		issue := unittest.AssertExistsAndLoadBean(t, &issues_model.Issue{Title: "Testing reopen functionality"})

		// Close the PR.
		err = issue_service.ChangeStatus(db.DefaultContext, issue, user2, "", true)
		require.NoError(t, err)

		session := loginUser(t, "user2")

		reopenPR := func(t *testing.T, expectedStatus int) *httptest.ResponseRecorder {
			t.Helper()

			link := fmt.Sprintf("%s/pulls/%d/comments", baseRepo.FullName(), issue.Index)
			req := NewRequestWithValues(t, "POST", link, map[string]string{
				"_csrf":  GetCSRF(t, session, fmt.Sprintf("%s/pulls/%d", baseRepo.FullName(), issue.Index)),
				"status": "reopen",
			})
			return session.MakeRequest(t, req, expectedStatus)
		}

		restoreBranch := func(t *testing.T, repoName, branchName string, branchID int64) {
			t.Helper()

			link := fmt.Sprintf("/%s/branches", repoName)
			req := NewRequestWithValues(t, "POST", fmt.Sprintf("%s/restore?branch_id=%d&name=%s", link, branchID, branchName), map[string]string{
				"_csrf": GetCSRF(t, session, link),
			})
			session.MakeRequest(t, req, http.StatusOK)

			flashCookie := session.GetCookie(gitea_context.CookieNameFlash)
			assert.NotNil(t, flashCookie)
			assert.Contains(t, flashCookie.Value, "success%3DBranch%2B%2522"+branchName+"%2522%2Bhas%2Bbeen%2Brestored.")
		}

		deleteBranch := func(t *testing.T, repoName, branchName string) {
			t.Helper()

			link := fmt.Sprintf("/%s/branches", repoName)
			req := NewRequestWithValues(t, "POST", fmt.Sprintf("%s/delete?name=%s", link, branchName), map[string]string{
				"_csrf": GetCSRF(t, session, link),
			})
			session.MakeRequest(t, req, http.StatusOK)

			flashCookie := session.GetCookie(gitea_context.CookieNameFlash)
			assert.NotNil(t, flashCookie)
			assert.Contains(t, flashCookie.Value, "success%3DBranch%2B%2522"+branchName+"%2522%2Bhas%2Bbeen%2Bdeleted.")
		}

		type errorJSON struct {
			Error string `json:"errorMessage"`
		}

		t.Run("Base branch deleted", func(t *testing.T) {
			defer tests.PrintCurrentTest(t)()

			branch := unittest.AssertExistsAndLoadBean(t, &git_model.Branch{Name: "base-branch", RepoID: baseRepo.ID})
			defer func() {
				restoreBranch(t, baseRepo.FullName(), branch.Name, branch.ID)
			}()

			deleteBranch(t, baseRepo.FullName(), branch.Name)
			resp := reopenPR(t, http.StatusBadRequest)

			var errorResp errorJSON
			DecodeJSON(t, resp, &errorResp)
			assert.EqualValues(t, translation.NewLocale("en-US").Tr("repo.pulls.reopen_failed.base_branch"), errorResp.Error)
		})

		t.Run("Head branch deleted", func(t *testing.T) {
			defer tests.PrintCurrentTest(t)()

			branch := unittest.AssertExistsAndLoadBean(t, &git_model.Branch{Name: "head-branch", RepoID: headRepo.ID})
			defer func() {
				restoreBranch(t, headRepo.FullName(), branch.Name, branch.ID)
			}()

			deleteBranch(t, headRepo.FullName(), branch.Name)
			resp := reopenPR(t, http.StatusBadRequest)

			var errorResp errorJSON
			DecodeJSON(t, resp, &errorResp)
			assert.EqualValues(t, translation.NewLocale("en-US").Tr("repo.pulls.reopen_failed.head_branch"), errorResp.Error)
		})

		t.Run("Normal", func(t *testing.T) {
			defer tests.PrintCurrentTest(t)()

			reopenPR(t, http.StatusOK)
		})
	})
}
