// Copyright 2017 The Gitea Authors. All rights reserved.
// Use of this source code is governed by a MIT-style
// license that can be found in the LICENSE file.

package integrations

import (
	"crypto/rand"
	"fmt"
	"io/ioutil"
	"net/http"
	"net/url"
	"os"
	"path"
	"path/filepath"
	"strconv"
	"testing"
	"time"

	"code.gitea.io/gitea/models"
	"code.gitea.io/gitea/modules/git"
	"code.gitea.io/gitea/modules/setting"
	api "code.gitea.io/gitea/modules/structs"

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

const (
	littleSize = 1024              //1ko
	bigSize    = 128 * 1024 * 1024 //128Mo
)

func TestGit(t *testing.T) {
	onGiteaRun(t, testGit)
}

func testGit(t *testing.T, u *url.URL) {
	username := "user2"
	baseAPITestContext := NewAPITestContext(t, username, "repo1")

	u.Path = baseAPITestContext.GitPath()

	forkedUserCtx := NewAPITestContext(t, "user4", "repo1")

	t.Run("HTTP", func(t *testing.T) {
		defer PrintCurrentTest(t)()
		ensureAnonymousClone(t, u)
		httpContext := baseAPITestContext
		httpContext.Reponame = "repo-tmp-17"
		forkedUserCtx.Reponame = httpContext.Reponame

		dstPath, err := ioutil.TempDir("", httpContext.Reponame)
		assert.NoError(t, err)
		defer os.RemoveAll(dstPath)

		t.Run("CreateRepoInDifferentUser", doAPICreateRepository(forkedUserCtx, false))
		t.Run("AddUserAsCollaborator", doAPIAddCollaborator(forkedUserCtx, httpContext.Username, models.AccessModeRead))

		t.Run("ForkFromDifferentUser", doAPIForkRepository(httpContext, forkedUserCtx.Username))

		u.Path = httpContext.GitPath()
		u.User = url.UserPassword(username, userPassword)

		t.Run("Clone", doGitClone(dstPath, u))

		little, big := standardCommitAndPushTest(t, dstPath)
		littleLFS, bigLFS := lfsCommitAndPushTest(t, dstPath)
		rawTest(t, &httpContext, little, big, littleLFS, bigLFS)
		mediaTest(t, &httpContext, little, big, littleLFS, bigLFS)

		t.Run("BranchProtectMerge", doBranchProtectPRMerge(&httpContext, dstPath))
		t.Run("MergeFork", func(t *testing.T) {
			t.Run("CreatePRAndMerge", doMergeFork(httpContext, forkedUserCtx, "master", httpContext.Username+":master"))
			t.Run("DeleteRepository", doAPIDeleteRepository(httpContext))
			rawTest(t, &forkedUserCtx, little, big, littleLFS, bigLFS)
			mediaTest(t, &forkedUserCtx, little, big, littleLFS, bigLFS)
		})

		t.Run("PushCreate", doPushCreate(httpContext, u))
	})
	t.Run("SSH", func(t *testing.T) {
		defer PrintCurrentTest(t)()
		sshContext := baseAPITestContext
		sshContext.Reponame = "repo-tmp-18"
		keyname := "my-testing-key"
		forkedUserCtx.Reponame = sshContext.Reponame
		t.Run("CreateRepoInDifferentUser", doAPICreateRepository(forkedUserCtx, false))
		t.Run("AddUserAsCollaborator", doAPIAddCollaborator(forkedUserCtx, sshContext.Username, models.AccessModeRead))
		t.Run("ForkFromDifferentUser", doAPIForkRepository(sshContext, forkedUserCtx.Username))

		//Setup key the user ssh key
		withKeyFile(t, keyname, func(keyFile string) {
			t.Run("CreateUserKey", doAPICreateUserKey(sshContext, "test-key", keyFile))

			//Setup remote link
			//TODO: get url from api
			sshURL := createSSHUrl(sshContext.GitPath(), u)

			//Setup clone folder
			dstPath, err := ioutil.TempDir("", sshContext.Reponame)
			assert.NoError(t, err)
			defer os.RemoveAll(dstPath)

			t.Run("Clone", doGitClone(dstPath, sshURL))

			little, big := standardCommitAndPushTest(t, dstPath)
			littleLFS, bigLFS := lfsCommitAndPushTest(t, dstPath)
			rawTest(t, &sshContext, little, big, littleLFS, bigLFS)
			mediaTest(t, &sshContext, little, big, littleLFS, bigLFS)

			t.Run("BranchProtectMerge", doBranchProtectPRMerge(&sshContext, dstPath))
			t.Run("MergeFork", func(t *testing.T) {
				t.Run("CreatePRAndMerge", doMergeFork(sshContext, forkedUserCtx, "master", sshContext.Username+":master"))
				t.Run("DeleteRepository", doAPIDeleteRepository(sshContext))
				rawTest(t, &forkedUserCtx, little, big, littleLFS, bigLFS)
				mediaTest(t, &forkedUserCtx, little, big, littleLFS, bigLFS)
			})

			t.Run("PushCreate", doPushCreate(sshContext, sshURL))
		})
	})
}

func ensureAnonymousClone(t *testing.T, u *url.URL) {
	dstLocalPath, err := ioutil.TempDir("", "repo1")
	assert.NoError(t, err)
	defer os.RemoveAll(dstLocalPath)
	t.Run("CloneAnonymous", doGitClone(dstLocalPath, u))

}

func standardCommitAndPushTest(t *testing.T, dstPath string) (little, big string) {
	t.Run("Standard", func(t *testing.T) {
		defer PrintCurrentTest(t)()
		little, big = commitAndPushTest(t, dstPath, "data-file-")
	})
	return
}

func lfsCommitAndPushTest(t *testing.T, dstPath string) (littleLFS, bigLFS string) {
	t.Run("LFS", func(t *testing.T) {
		defer PrintCurrentTest(t)()
		setting.CheckLFSVersion()
		if !setting.LFS.StartServer {
			t.Skip()
			return
		}
		prefix := "lfs-data-file-"
		_, err := git.NewCommand("lfs").AddArguments("install").RunInDir(dstPath)
		assert.NoError(t, err)
		_, err = git.NewCommand("lfs").AddArguments("track", prefix+"*").RunInDir(dstPath)
		assert.NoError(t, err)
		err = git.AddChanges(dstPath, false, ".gitattributes")
		assert.NoError(t, err)

		err = git.CommitChangesWithArgs(dstPath, allowLFSFilters(), git.CommitChangesOptions{
			Committer: &git.Signature{
				Email: "user2@example.com",
				Name:  "User Two",
				When:  time.Now(),
			},
			Author: &git.Signature{
				Email: "user2@example.com",
				Name:  "User Two",
				When:  time.Now(),
			},
			Message: fmt.Sprintf("Testing commit @ %v", time.Now()),
		})
		assert.NoError(t, err)

		littleLFS, bigLFS = commitAndPushTest(t, dstPath, prefix)

		t.Run("Locks", func(t *testing.T) {
			defer PrintCurrentTest(t)()
			lockTest(t, dstPath)
		})
	})
	return
}

func commitAndPushTest(t *testing.T, dstPath, prefix string) (little, big string) {
	t.Run("PushCommit", func(t *testing.T) {
		defer PrintCurrentTest(t)()
		t.Run("Little", func(t *testing.T) {
			defer PrintCurrentTest(t)()
			little = doCommitAndPush(t, littleSize, dstPath, prefix)
		})
		t.Run("Big", func(t *testing.T) {
			if testing.Short() {
				t.Skip("Skipping test in short mode.")
				return
			}
			defer PrintCurrentTest(t)()
			big = doCommitAndPush(t, bigSize, dstPath, prefix)
		})
	})
	return
}

func rawTest(t *testing.T, ctx *APITestContext, little, big, littleLFS, bigLFS string) {
	t.Run("Raw", func(t *testing.T) {
		defer PrintCurrentTest(t)()
		username := ctx.Username
		reponame := ctx.Reponame

		session := loginUser(t, username)

		// Request raw paths
		req := NewRequest(t, "GET", path.Join("/", username, reponame, "/raw/branch/master/", little))
		resp := session.MakeRequest(t, req, http.StatusOK)
		assert.Equal(t, littleSize, resp.Body.Len())

		setting.CheckLFSVersion()
		if setting.LFS.StartServer {
			req = NewRequest(t, "GET", path.Join("/", username, reponame, "/raw/branch/master/", littleLFS))
			resp = session.MakeRequest(t, req, http.StatusOK)
			assert.NotEqual(t, littleSize, resp.Body.Len())
			assert.Contains(t, resp.Body.String(), models.LFSMetaFileIdentifier)
		}

		if !testing.Short() {
			req = NewRequest(t, "GET", path.Join("/", username, reponame, "/raw/branch/master/", big))
			resp = session.MakeRequest(t, req, http.StatusOK)
			assert.Equal(t, bigSize, resp.Body.Len())

			if setting.LFS.StartServer {
				req = NewRequest(t, "GET", path.Join("/", username, reponame, "/raw/branch/master/", bigLFS))
				resp = session.MakeRequest(t, req, http.StatusOK)
				assert.NotEqual(t, bigSize, resp.Body.Len())
				assert.Contains(t, resp.Body.String(), models.LFSMetaFileIdentifier)
			}
		}
	})
}

func mediaTest(t *testing.T, ctx *APITestContext, little, big, littleLFS, bigLFS string) {
	t.Run("Media", func(t *testing.T) {
		defer PrintCurrentTest(t)()

		username := ctx.Username
		reponame := ctx.Reponame

		session := loginUser(t, username)

		// Request media paths
		req := NewRequest(t, "GET", path.Join("/", username, reponame, "/media/branch/master/", little))
		resp := session.MakeRequestNilResponseRecorder(t, req, http.StatusOK)
		assert.Equal(t, littleSize, resp.Length)

		setting.CheckLFSVersion()
		if setting.LFS.StartServer {
			req = NewRequest(t, "GET", path.Join("/", username, reponame, "/media/branch/master/", littleLFS))
			resp = session.MakeRequestNilResponseRecorder(t, req, http.StatusOK)
			assert.Equal(t, littleSize, resp.Length)
		}

		if !testing.Short() {
			req = NewRequest(t, "GET", path.Join("/", username, reponame, "/media/branch/master/", big))
			resp = session.MakeRequestNilResponseRecorder(t, req, http.StatusOK)
			assert.Equal(t, bigSize, resp.Length)

			if setting.LFS.StartServer {
				req = NewRequest(t, "GET", path.Join("/", username, reponame, "/media/branch/master/", bigLFS))
				resp = session.MakeRequestNilResponseRecorder(t, req, http.StatusOK)
				assert.Equal(t, bigSize, resp.Length)
			}
		}
	})
}

func lockTest(t *testing.T, repoPath string) {
	lockFileTest(t, "README.md", repoPath)
}

func lockFileTest(t *testing.T, filename, repoPath string) {
	_, err := git.NewCommand("lfs").AddArguments("locks").RunInDir(repoPath)
	assert.NoError(t, err)
	_, err = git.NewCommand("lfs").AddArguments("lock", filename).RunInDir(repoPath)
	assert.NoError(t, err)
	_, err = git.NewCommand("lfs").AddArguments("locks").RunInDir(repoPath)
	assert.NoError(t, err)
	_, err = git.NewCommand("lfs").AddArguments("unlock", filename).RunInDir(repoPath)
	assert.NoError(t, err)
}

func doCommitAndPush(t *testing.T, size int, repoPath, prefix string) string {
	name, err := generateCommitWithNewData(size, repoPath, "user2@example.com", "User Two", prefix)
	assert.NoError(t, err)
	_, err = git.NewCommand("push", "origin", "master").RunInDir(repoPath) //Push
	assert.NoError(t, err)
	return name
}

func generateCommitWithNewData(size int, repoPath, email, fullName, prefix string) (string, error) {
	//Generate random file
	data := make([]byte, size)
	_, err := rand.Read(data)
	if err != nil {
		return "", err
	}
	tmpFile, err := ioutil.TempFile(repoPath, prefix)
	if err != nil {
		return "", err
	}
	defer tmpFile.Close()
	_, err = tmpFile.Write(data)
	if err != nil {
		return "", err
	}

	//Commit
	// Now here we should explicitly allow lfs filters to run
	globalArgs := allowLFSFilters()
	err = git.AddChangesWithArgs(repoPath, globalArgs, false, filepath.Base(tmpFile.Name()))
	if err != nil {
		return "", err
	}
	err = git.CommitChangesWithArgs(repoPath, globalArgs, git.CommitChangesOptions{
		Committer: &git.Signature{
			Email: email,
			Name:  fullName,
			When:  time.Now(),
		},
		Author: &git.Signature{
			Email: email,
			Name:  fullName,
			When:  time.Now(),
		},
		Message: fmt.Sprintf("Testing commit @ %v", time.Now()),
	})
	return filepath.Base(tmpFile.Name()), err
}

func doBranchProtectPRMerge(baseCtx *APITestContext, dstPath string) func(t *testing.T) {
	return func(t *testing.T) {
		defer PrintCurrentTest(t)()
		t.Run("CreateBranchProtected", doGitCreateBranch(dstPath, "protected"))
		t.Run("PushProtectedBranch", doGitPushTestRepository(dstPath, "origin", "protected"))

		ctx := NewAPITestContext(t, baseCtx.Username, baseCtx.Reponame)
		t.Run("ProtectProtectedBranchNoWhitelist", doProtectBranch(ctx, "protected", ""))
		t.Run("GenerateCommit", func(t *testing.T) {
			_, err := generateCommitWithNewData(littleSize, dstPath, "user2@example.com", "User Two", "branch-data-file-")
			assert.NoError(t, err)
		})
		t.Run("FailToPushToProtectedBranch", doGitPushTestRepositoryFail(dstPath, "origin", "protected"))
		t.Run("PushToUnprotectedBranch", doGitPushTestRepository(dstPath, "origin", "protected:unprotected"))
		var pr api.PullRequest
		var err error
		t.Run("CreatePullRequest", func(t *testing.T) {
			pr, err = doAPICreatePullRequest(ctx, baseCtx.Username, baseCtx.Reponame, "protected", "unprotected")(t)
			assert.NoError(t, err)
		})
		t.Run("MergePR", doAPIMergePullRequest(ctx, baseCtx.Username, baseCtx.Reponame, pr.Index))
		t.Run("PullProtected", doGitPull(dstPath, "origin", "protected"))
		t.Run("ProtectProtectedBranchWhitelist", doProtectBranch(ctx, "protected", baseCtx.Username))

		t.Run("CheckoutMaster", doGitCheckoutBranch(dstPath, "master"))
		t.Run("CreateBranchForced", doGitCreateBranch(dstPath, "toforce"))
		t.Run("GenerateCommit", func(t *testing.T) {
			_, err := generateCommitWithNewData(littleSize, dstPath, "user2@example.com", "User Two", "branch-data-file-")
			assert.NoError(t, err)
		})
		t.Run("FailToForcePushToProtectedBranch", doGitPushTestRepositoryFail(dstPath, "-f", "origin", "toforce:protected"))
		t.Run("MergeProtectedToToforce", doGitMerge(dstPath, "protected"))
		t.Run("PushToProtectedBranch", doGitPushTestRepository(dstPath, "origin", "toforce:protected"))
		t.Run("CheckoutMasterAgain", doGitCheckoutBranch(dstPath, "master"))
	}
}

func doProtectBranch(ctx APITestContext, branch string, userToWhitelist string) func(t *testing.T) {
	// We are going to just use the owner to set the protection.
	return func(t *testing.T) {
		csrf := GetCSRF(t, ctx.Session, fmt.Sprintf("/%s/%s/settings/branches", url.PathEscape(ctx.Username), url.PathEscape(ctx.Reponame)))

		if userToWhitelist == "" {
			// Change branch to protected
			req := NewRequestWithValues(t, "POST", fmt.Sprintf("/%s/%s/settings/branches/%s", url.PathEscape(ctx.Username), url.PathEscape(ctx.Reponame), url.PathEscape(branch)), map[string]string{
				"_csrf":     csrf,
				"protected": "on",
			})
			ctx.Session.MakeRequest(t, req, http.StatusFound)
		} else {
			user, err := models.GetUserByName(userToWhitelist)
			assert.NoError(t, err)
			// Change branch to protected
			req := NewRequestWithValues(t, "POST", fmt.Sprintf("/%s/%s/settings/branches/%s", url.PathEscape(ctx.Username), url.PathEscape(ctx.Reponame), url.PathEscape(branch)), map[string]string{
				"_csrf":            csrf,
				"protected":        "on",
				"enable_push":      "whitelist",
				"enable_whitelist": "on",
				"whitelist_users":  strconv.FormatInt(user.ID, 10),
			})
			ctx.Session.MakeRequest(t, req, http.StatusFound)
		}
		// Check if master branch has been locked successfully
		flashCookie := ctx.Session.GetCookie("macaron_flash")
		assert.NotNil(t, flashCookie)
		assert.EqualValues(t, "success%3DBranch%2Bprotection%2Bfor%2Bbranch%2B%2527"+url.QueryEscape(branch)+"%2527%2Bhas%2Bbeen%2Bupdated.", flashCookie.Value)
	}
}

func doMergeFork(ctx, baseCtx APITestContext, baseBranch, headBranch string) func(t *testing.T) {
	return func(t *testing.T) {
		var pr api.PullRequest
		var err error
		t.Run("CreatePullRequest", func(t *testing.T) {
			pr, err = doAPICreatePullRequest(ctx, baseCtx.Username, baseCtx.Reponame, baseBranch, headBranch)(t)
			assert.NoError(t, err)
		})
		t.Run("MergePR", doAPIMergePullRequest(baseCtx, baseCtx.Username, baseCtx.Reponame, pr.Index))

	}
}

func doPushCreate(ctx APITestContext, u *url.URL) func(t *testing.T) {
	return func(t *testing.T) {
		defer PrintCurrentTest(t)()
		ctx.Reponame = fmt.Sprintf("repo-tmp-push-create-%s", u.Scheme)
		u.Path = ctx.GitPath()

		tmpDir, err := ioutil.TempDir("", ctx.Reponame)
		assert.NoError(t, err)

		err = git.InitRepository(tmpDir, false)
		assert.NoError(t, err)

		_, err = os.Create(filepath.Join(tmpDir, "test.txt"))
		assert.NoError(t, err)

		err = git.AddChanges(tmpDir, true)
		assert.NoError(t, err)

		err = git.CommitChanges(tmpDir, git.CommitChangesOptions{
			Committer: &git.Signature{
				Email: "user2@example.com",
				Name:  "User Two",
				When:  time.Now(),
			},
			Author: &git.Signature{
				Email: "user2@example.com",
				Name:  "User Two",
				When:  time.Now(),
			},
			Message: fmt.Sprintf("Testing push create @ %v", time.Now()),
		})
		assert.NoError(t, err)

		_, err = git.NewCommand("remote", "add", "origin", u.String()).RunInDir(tmpDir)
		assert.NoError(t, err)

		// Push to create disabled
		setting.Repository.EnablePushCreateUser = false
		_, err = git.NewCommand("push", "origin", "master").RunInDir(tmpDir)
		assert.Error(t, err)

		// Push to create enabled
		setting.Repository.EnablePushCreateUser = true
		_, err = git.NewCommand("push", "origin", "master").RunInDir(tmpDir)
		assert.NoError(t, err)

		// Fetch repo from database
		repo, err := models.GetRepositoryByOwnerAndName(ctx.Username, ctx.Reponame)
		assert.NoError(t, err)
		assert.False(t, repo.IsEmpty)
		assert.True(t, repo.IsPrivate)
	}
}
