package git

import (
	"context"
	"io/ioutil"
	"log"
	"os"
	"path/filepath"
	"testing"

	"github.com/go-git/go-git/v5"
	"github.com/go-git/go-git/v5/plumbing/object"
	"github.com/pkg/errors"
	"github.com/portainer/portainer/api/archive"
	"github.com/stretchr/testify/assert"
)

var bareRepoDir string

func TestMain(m *testing.M) {
	if err := testMain(m); err != nil {
		log.Fatal(err)
	}
}

// testMain does extra setup/teardown before/after testing.
// The function is separated from TestMain due to necessity to call os.Exit/log.Fatal in the latter.
func testMain(m *testing.M) error {
	dir, err := ioutil.TempDir("", "git-repo-")
	if err != nil {
		return errors.Wrap(err, "failed to create a temp dir")
	}
	defer os.RemoveAll(dir)

	bareRepoDir = filepath.Join(dir, "test-clone.git")

	file, err := os.OpenFile("./testdata/test-clone-git-repo.tar.gz", os.O_RDONLY, 0755)
	if err != nil {
		return errors.Wrap(err, "failed to open an archive")
	}
	err = archive.ExtractTarGz(file, dir)
	if err != nil {
		return errors.Wrapf(err, "failed to extract file from the archive to a folder %s\n", dir)
	}

	m.Run()

	return nil
}

func Test_ClonePublicRepository_Shallow(t *testing.T) {
	service := Service{git: gitClient{preserveGitDirectory: true}} // no need for http client since the test access the repo via file system.
	repositoryURL := bareRepoDir
	referenceName := "refs/heads/main"
	destination := "shallow"

	dir, err := ioutil.TempDir("", destination)
	if err != nil {
		t.Fatalf("failed to create a temp dir")
	}
	defer os.RemoveAll(dir)
	t.Logf("Cloning into %s", dir)
	err = service.CloneRepository(dir, repositoryURL, referenceName, "", "")
	assert.NoError(t, err)
	assert.Equal(t, 1, getCommitHistoryLength(t, err, dir), "cloned repo has incorrect depth")
}

func Test_ClonePublicRepository_NoGitDirectory(t *testing.T) {
	service := Service{git: gitClient{preserveGitDirectory: false}} // no need for http client since the test access the repo via file system.
	repositoryURL := bareRepoDir
	referenceName := "refs/heads/main"
	destination := "shallow"

	dir, err := ioutil.TempDir("", destination)
	if err != nil {
		t.Fatalf("failed to create a temp dir")
	}

	defer os.RemoveAll(dir)

	t.Logf("Cloning into %s", dir)
	err = service.CloneRepository(dir, repositoryURL, referenceName, "", "")
	assert.NoError(t, err)
	assert.NoDirExists(t, filepath.Join(dir, ".git"))
}

func Test_cloneRepository(t *testing.T) {
	service := Service{git: gitClient{preserveGitDirectory: true}} // no need for http client since the test access the repo via file system.

	repositoryURL := bareRepoDir
	referenceName := "refs/heads/main"
	destination := "shallow"

	dir, err := ioutil.TempDir("", destination)
	if err != nil {
		t.Fatalf("failed to create a temp dir")
	}
	defer os.RemoveAll(dir)
	t.Logf("Cloning into %s", dir)

	err = service.cloneRepository(dir, cloneOptions{
		repositoryUrl: repositoryURL,
		referenceName: referenceName,
		depth:         10,
	})

	assert.NoError(t, err)
	assert.Equal(t, 4, getCommitHistoryLength(t, err, dir), "cloned repo has incorrect depth")
}

func Test_latestCommitID(t *testing.T) {
	service := Service{git: gitClient{preserveGitDirectory: true}} // no need for http client since the test access the repo via file system.

	repositoryURL := bareRepoDir
	referenceName := "refs/heads/main"

	id, err := service.LatestCommitID(repositoryURL, referenceName, "", "")

	assert.NoError(t, err)
	assert.Equal(t, "68dcaa7bd452494043c64252ab90db0f98ecf8d2", id)
}

func getCommitHistoryLength(t *testing.T, err error, dir string) int {
	repo, err := git.PlainOpen(dir)
	if err != nil {
		t.Fatalf("can't open a git repo at %s with error %v", dir, err)
	}
	iter, err := repo.Log(&git.LogOptions{All: true})
	if err != nil {
		t.Fatalf("can't get a commit history iterator with error %v", err)
	}
	count := 0
	err = iter.ForEach(func(_ *object.Commit) error {
		count++
		return nil
	})
	if err != nil {
		t.Fatalf("can't iterate over the commit history with error %v", err)
	}
	return count
}

type testDownloader struct {
	called bool
}

func (t *testDownloader) download(_ context.Context, _ string, _ cloneOptions) error {
	t.called = true
	return nil
}

func (t *testDownloader) latestCommitID(_ context.Context, _ fetchOptions) (string, error) {
	return "", nil
}

func Test_cloneRepository_azure(t *testing.T) {
	tests := []struct {
		name   string
		url    string
		called bool
	}{
		{
			name:   "Azure HTTP URL",
			url:    "https://Organisation@dev.azure.com/Organisation/Project/_git/Repository",
			called: true,
		},
		{
			name:   "Azure SSH URL",
			url:    "git@ssh.dev.azure.com:v3/Organisation/Project/Repository",
			called: true,
		},
		{
			name:   "Something else",
			url:    "https://example.com",
			called: false,
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			azure := &testDownloader{}
			git := &testDownloader{}

			s := &Service{azure: azure, git: git}
			s.cloneRepository("", cloneOptions{repositoryUrl: tt.url, depth: 1})

			// if azure API is called, git isn't and vice versa
			assert.Equal(t, tt.called, azure.called)
			assert.Equal(t, tt.called, !git.called)
		})
	}
}