// 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"
	"testing"
	"time"

	"code.gitea.io/gitea/models"
	"code.gitea.io/gitea/modules/git"

	"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()

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

		dstPath, err := ioutil.TempDir("", httpContext.Reponame)
		var little, big, littleLFS, bigLFS string

		assert.NoError(t, err)
		defer os.RemoveAll(dstPath)
		t.Run("Standard", func(t *testing.T) {
			PrintCurrentTest(t)
			ensureAnonymousClone(t, u)

			t.Run("CreateRepo", doAPICreateRepository(httpContext, false))

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

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

			t.Run("PushCommit", func(t *testing.T) {
				PrintCurrentTest(t)
				t.Run("Little", func(t *testing.T) {
					PrintCurrentTest(t)
					little = commitAndPush(t, littleSize, dstPath)
				})
				t.Run("Big", func(t *testing.T) {
					PrintCurrentTest(t)
					big = commitAndPush(t, bigSize, dstPath)
				})
			})
		})
		t.Run("LFS", func(t *testing.T) {
			PrintCurrentTest(t)
			t.Run("PushCommit", func(t *testing.T) {
				PrintCurrentTest(t)
				//Setup git LFS
				_, err = git.NewCommand("lfs").AddArguments("install").RunInDir(dstPath)
				assert.NoError(t, err)
				_, err = git.NewCommand("lfs").AddArguments("track", "data-file-*").RunInDir(dstPath)
				assert.NoError(t, err)
				err = git.AddChanges(dstPath, false, ".gitattributes")
				assert.NoError(t, err)

				t.Run("Little", func(t *testing.T) {
					PrintCurrentTest(t)
					littleLFS = commitAndPush(t, littleSize, dstPath)
				})
				t.Run("Big", func(t *testing.T) {
					PrintCurrentTest(t)
					bigLFS = commitAndPush(t, bigSize, dstPath)
				})
			})
			t.Run("Locks", func(t *testing.T) {
				PrintCurrentTest(t)
				lockTest(t, u.String(), dstPath)
			})
		})
		t.Run("Raw", func(t *testing.T) {
			PrintCurrentTest(t)
			session := loginUser(t, "user2")

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

			req = NewRequest(t, "GET", path.Join("/user2/repo-tmp-17/raw/branch/master/", big))
			nilResp := session.MakeRequestNilResponseRecorder(t, req, http.StatusOK)
			assert.Equal(t, bigSize, nilResp.Length)

			req = NewRequest(t, "GET", path.Join("/user2/repo-tmp-17/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)

			req = NewRequest(t, "GET", path.Join("/user2/repo-tmp-17/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)

		})
		t.Run("Media", func(t *testing.T) {
			PrintCurrentTest(t)
			session := loginUser(t, "user2")

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

			req = NewRequest(t, "GET", path.Join("/user2/repo-tmp-17/media/branch/master/", big))
			resp = session.MakeRequestNilResponseRecorder(t, req, http.StatusOK)
			assert.Equal(t, bigSize, resp.Length)

			req = NewRequest(t, "GET", path.Join("/user2/repo-tmp-17/media/branch/master/", littleLFS))
			resp = session.MakeRequestNilResponseRecorder(t, req, http.StatusOK)
			assert.Equal(t, littleSize, resp.Length)

			req = NewRequest(t, "GET", path.Join("/user2/repo-tmp-17/media/branch/master/", bigLFS))
			resp = session.MakeRequestNilResponseRecorder(t, req, http.StatusOK)
			assert.Equal(t, bigSize, resp.Length)
		})

	})
	t.Run("SSH", func(t *testing.T) {
		PrintCurrentTest(t)
		sshContext := baseAPITestContext
		sshContext.Reponame = "repo-tmp-18"
		keyname := "my-testing-key"
		//Setup key the user ssh key
		withKeyFile(t, keyname, func(keyFile string) {
			t.Run("CreateUserKey", doAPICreateUserKey(sshContext, "test-key", keyFile))
			PrintCurrentTest(t)

			//Setup remote link
			sshURL := createSSHUrl(sshContext.GitPath(), u)

			//Setup clone folder
			dstPath, err := ioutil.TempDir("", sshContext.Reponame)
			assert.NoError(t, err)
			defer os.RemoveAll(dstPath)
			var little, big, littleLFS, bigLFS string

			t.Run("Standard", func(t *testing.T) {
				PrintCurrentTest(t)
				t.Run("CreateRepo", doAPICreateRepository(sshContext, false))

				//TODO get url from api
				t.Run("Clone", doGitClone(dstPath, sshURL))

				//time.Sleep(5 * time.Minute)
				t.Run("PushCommit", func(t *testing.T) {
					PrintCurrentTest(t)
					t.Run("Little", func(t *testing.T) {
						PrintCurrentTest(t)
						little = commitAndPush(t, littleSize, dstPath)
					})
					t.Run("Big", func(t *testing.T) {
						PrintCurrentTest(t)
						big = commitAndPush(t, bigSize, dstPath)
					})
				})
			})
			t.Run("LFS", func(t *testing.T) {
				PrintCurrentTest(t)
				t.Run("PushCommit", func(t *testing.T) {
					PrintCurrentTest(t)
					//Setup git LFS
					_, err = git.NewCommand("lfs").AddArguments("install").RunInDir(dstPath)
					assert.NoError(t, err)
					_, err = git.NewCommand("lfs").AddArguments("track", "data-file-*").RunInDir(dstPath)
					assert.NoError(t, err)
					err = git.AddChanges(dstPath, false, ".gitattributes")
					assert.NoError(t, err)

					t.Run("Little", func(t *testing.T) {
						PrintCurrentTest(t)
						littleLFS = commitAndPush(t, littleSize, dstPath)
					})
					t.Run("Big", func(t *testing.T) {
						PrintCurrentTest(t)
						bigLFS = commitAndPush(t, bigSize, dstPath)
					})
				})
				t.Run("Locks", func(t *testing.T) {
					PrintCurrentTest(t)
					lockTest(t, u.String(), dstPath)
				})
			})
			t.Run("Raw", func(t *testing.T) {
				PrintCurrentTest(t)
				session := loginUser(t, "user2")

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

				req = NewRequest(t, "GET", path.Join("/user2/repo-tmp-18/raw/branch/master/", big))
				resp = session.MakeRequest(t, req, http.StatusOK)
				assert.Equal(t, bigSize, resp.Body.Len())

				req = NewRequest(t, "GET", path.Join("/user2/repo-tmp-18/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)

				req = NewRequest(t, "GET", path.Join("/user2/repo-tmp-18/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)

			})
			t.Run("Media", func(t *testing.T) {
				PrintCurrentTest(t)
				session := loginUser(t, "user2")

				// Request media paths
				req := NewRequest(t, "GET", path.Join("/user2/repo-tmp-18/media/branch/master/", little))
				resp := session.MakeRequest(t, req, http.StatusOK)
				assert.Equal(t, littleSize, resp.Body.Len())

				req = NewRequest(t, "GET", path.Join("/user2/repo-tmp-18/media/branch/master/", big))
				resp = session.MakeRequest(t, req, http.StatusOK)
				assert.Equal(t, bigSize, resp.Body.Len())

				req = NewRequest(t, "GET", path.Join("/user2/repo-tmp-18/media/branch/master/", littleLFS))
				resp = session.MakeRequest(t, req, http.StatusOK)
				assert.Equal(t, littleSize, resp.Body.Len())

				req = NewRequest(t, "GET", path.Join("/user2/repo-tmp-18/media/branch/master/", bigLFS))
				resp = session.MakeRequest(t, req, http.StatusOK)
				assert.Equal(t, bigSize, resp.Body.Len())
			})

		})

	})
}

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 lockTest(t *testing.T, remote, repoPath string) {
	_, err := git.NewCommand("remote").AddArguments("set-url", "origin", remote).RunInDir(repoPath) //TODO add test ssh git-lfs-creds
	assert.NoError(t, err)
	_, err = git.NewCommand("lfs").AddArguments("locks").RunInDir(repoPath)
	assert.NoError(t, err)
	_, err = git.NewCommand("lfs").AddArguments("lock", "README.md").RunInDir(repoPath)
	assert.NoError(t, err)
	_, err = git.NewCommand("lfs").AddArguments("locks").RunInDir(repoPath)
	assert.NoError(t, err)
	_, err = git.NewCommand("lfs").AddArguments("unlock", "README.md").RunInDir(repoPath)
	assert.NoError(t, err)
}

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

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

	//Commit
	err = git.AddChanges(repoPath, false, filepath.Base(tmpFile.Name()))
	if err != nil {
		return "", err
	}
	err = git.CommitChanges(repoPath, 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
}