package git_test

import (
	"net/url"
	"os"
	"os/exec"
	"path/filepath"
	"testing"

	"github.com/stretchr/testify/require"
	"github.com/thomiceli/opengist/internal/db"
	webtest "github.com/thomiceli/opengist/internal/web/test"
)

func gitClone(baseUrl, creds, user, gistId, destDir string) error {
	authUrl := baseUrl
	if creds != "" {
		authUrl = "http://" + creds + "@" + baseUrl[len("http://"):]
	}
	return exec.Command("git", "clone", authUrl+"/"+user+"/"+gistId+".git", destDir).Run()
}

func gitPush(repoDir, filename, content string) error {
	if err := os.WriteFile(filepath.Join(repoDir, filename), []byte(content), 0644); err != nil {
		return err
	}
	if err := exec.Command("git", "-C", repoDir, "add", filename).Run(); err != nil {
		return err
	}
	if err := exec.Command("git", "-C", repoDir, "commit", "-m", "add "+filename).Run(); err != nil {
		return err
	}
	return exec.Command("git", "-C", repoDir, "push", "origin").Run()
}

func TestGitClonePull(t *testing.T) {
	s := webtest.Setup(t)
	defer webtest.Teardown(t)

	baseUrl := s.StartHttpServer(t)

	s.Register(t, "thomas")
	s.Register(t, "alice")

	_, _, user, publicId := s.CreateGist(t, "0")
	_, _, _, unlistedId := s.CreateGist(t, "1")
	_, _, _, privateId := s.CreateGist(t, "2")

	type credTest struct {
		name   string
		creds  string
		expect [3]bool // [public, unlisted, private]
	}

	tests := []struct {
		name     string
		settings map[string]string
		creds    []credTest
	}{
		{
			name: "Default",
			creds: []credTest{
				{"OwnerAuth", "thomas:thomas", [3]bool{true, true, true}},
				{"OtherUserAuth", "alice:alice", [3]bool{true, true, false}},
				{"WrongPassword", "thomas:wrong", [3]bool{true, true, false}},
				{"WrongUser", "aze:aze", [3]bool{true, true, false}},
				{"Anonymous", "", [3]bool{true, true, false}},
			},
		},
		{
			name:     "RequireLogin",
			settings: map[string]string{db.SettingRequireLogin: "1"},
			creds: []credTest{
				{"OwnerAuth", "thomas:thomas", [3]bool{true, true, true}},
				{"OtherUserAuth", "alice:alice", [3]bool{true, true, false}},
				{"WrongPassword", "thomas:wrong", [3]bool{false, false, false}},
				{"WrongUser", "aze:aze", [3]bool{false, false, false}},
				{"Anonymous", "", [3]bool{false, false, false}},
			},
		},
		{
			name:     "AllowGistsWithoutLogin",
			settings: map[string]string{db.SettingRequireLogin: "1", db.SettingAllowGistsWithoutLogin: "1"},
			creds: []credTest{
				{"OwnerAuth", "thomas:thomas", [3]bool{true, true, true}},
				{"OtherUserAuth", "alice:alice", [3]bool{true, true, false}},
				{"WrongPassword", "thomas:wrong", [3]bool{true, true, false}},
				{"WrongUser", "aze:aze", [3]bool{true, true, false}},
				{"Anonymous", "", [3]bool{true, true, false}},
			},
		},
	}

	gists := [3]string{publicId, unlistedId, privateId}
	labels := [3]string{"Public", "Unlisted", "Private"}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			s.Login(t, "thomas")
			for k, v := range tt.settings {
				s.Request(t, "PUT", "/admin-panel/set-config", url.Values{"key": {k}, "value": {v}}, 200)
			}

			for _, ct := range tt.creds {
				t.Run(ct.name, func(t *testing.T) {
					for i, id := range gists {
						t.Run(labels[i], func(t *testing.T) {
							dest := t.TempDir()
							err := gitClone(baseUrl, ct.creds, user, id, dest)
							if ct.expect[i] {
								require.NoError(t, err)
								_, err = os.Stat(filepath.Join(dest, "file.txt"))
								require.NoError(t, err)
							} else {
								require.Error(t, err)
							}
						})
					}
				})
			}

			// Reset settings
			s.Login(t, "thomas")
			for k := range tt.settings {
				s.Request(t, "PUT", "/admin-panel/set-config", url.Values{"key": {k}, "value": {"0"}}, 200)
			}
		})
	}
}

func TestGitPush(t *testing.T) {
	s := webtest.Setup(t)
	defer webtest.Teardown(t)

	baseUrl := s.StartHttpServer(t)

	s.Register(t, "thomas")
	s.Register(t, "alice")

	_, _, user, publicId := s.CreateGist(t, "0")
	_, _, _, unlistedId := s.CreateGist(t, "1")
	_, _, _, privateId := s.CreateGist(t, "2")

	type credTest struct {
		name   string
		creds  string
		expect [3]bool // [public, unlisted, private]
	}

	tests := []credTest{
		{"OwnerAuth", "thomas:thomas", [3]bool{true, true, true}},
		{"OtherUserAuth", "alice:alice", [3]bool{false, false, false}},
		{"WrongPassword", "thomas:wrong", [3]bool{false, false, false}},
		{"WrongUser", "aze:aze", [3]bool{false, false, false}},
		{"Anonymous", "", [3]bool{false, false, false}},
	}

	gists := [3]string{publicId, unlistedId, privateId}
	labels := [3]string{"Public", "Unlisted", "Private"}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			for i, id := range gists {
				t.Run(labels[i], func(t *testing.T) {
					dest := t.TempDir()
					require.NoError(t, gitClone(baseUrl, "thomas:thomas", user, id, dest))

					if tt.creds != "thomas:thomas" {
						require.NoError(t, exec.Command("git", "-C", dest, "remote", "set-url", "origin",
							"http://"+tt.creds+"@"+baseUrl[len("http://"):]+"/"+user+"/"+id+".git").Run())
					}

					err := gitPush(dest, "newfile.txt", "new content")
					if tt.expect[i] {
						require.NoError(t, err)
					} else {
						require.Error(t, err)
					}
				})
			}
		})
	}
}

func TestGitPushArchived(t *testing.T) {
	s := webtest.Setup(t)
	defer webtest.Teardown(t)

	baseUrl := s.StartHttpServer(t)

	s.Register(t, "thomas")

	_, _, user, gistId := s.CreateGist(t, "0")

	dest := t.TempDir()
	require.NoError(t, gitClone(baseUrl, "thomas:thomas", user, gistId, dest))

	// Pushing works before the gist is archived.
	require.NoError(t, gitPush(dest, "before.txt", "content"))

	// Archive the gist.
	s.Login(t, "thomas")
	s.Request(t, "POST", "/"+user+"/"+gistId+"/archive", nil, 302)

	// Pushing to an archived gist is rejected, even by the owner.
	require.Error(t, gitPush(dest, "after.txt", "content"))

	// Unarchive the gist.
	s.Request(t, "POST", "/"+user+"/"+gistId+"/archive", nil, 302)

	// Pushing works again once unarchived.
	require.NoError(t, gitPush(dest, "afterunarchive.txt", "content"))
}

func TestGitCreatePush(t *testing.T) {
	s := webtest.Setup(t)
	defer webtest.Teardown(t)

	baseUrl := s.StartHttpServer(t)

	s.Register(t, "thomas")
	s.Register(t, "alice")

	gitInitAndPush := func(t *testing.T, creds, remoteUrl string) error {
		dest := t.TempDir()
		require.NoError(t, exec.Command("git", "init", "--initial-branch=master", dest).Run())
		require.NoError(t, exec.Command("git", "-C", dest, "remote", "add", "origin",
			"http://"+creds+"@"+baseUrl[len("http://"):]+remoteUrl).Run())

		require.NoError(t, os.WriteFile(filepath.Join(dest, "hello.txt"), []byte("hello"), 0644))
		require.NoError(t, exec.Command("git", "-C", dest, "add", "hello.txt").Run())
		require.NoError(t, exec.Command("git", "-C", dest, "commit", "-m", "initial").Run())
		return exec.Command("git", "-C", dest, "push", "origin").Run()
	}

	tests := []struct {
		name      string
		creds     string
		url       string
		expect    bool
		gistOwner string // if expect=true, verify gist exists at this owner/identifier
		gistId    string
	}{
		{"OwnerCreates", "thomas:thomas", "/thomas/mygist.git", true, "thomas", "mygist"},
		{"OtherUserCreatesOnOwnUrl", "alice:alice", "/alice/alicegist.git", true, "alice", "alicegist"},
		{"WrongPassword", "thomas:wrong", "/thomas/newgist.git", false, "", ""},
		{"OtherUserCannotCreateOnOwner", "alice:alice", "/thomas/hackgist.git", false, "", ""},
		{"WrongUser", "aze:aze", "/aze/azegist.git", false, "", ""},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			err := gitInitAndPush(t, tt.creds, tt.url)
			if tt.expect {
				require.NoError(t, err)
				gist, err := db.GetGist(tt.gistOwner, tt.gistId)
				require.NoError(t, err)
				require.NotNil(t, gist)
				require.Equal(t, tt.gistId, gist.Identifier())
			} else {
				require.Error(t, err)
			}
		})
	}
}
