package gitclient_test

import (
	"context"
	"fmt"
	"reflect"
	"testing"

	goGit "github.com/go-git/go-git/v5"
	"github.com/go-git/go-git/v5/config"
	"github.com/go-git/go-git/v5/plumbing"
	"github.com/go-git/go-git/v5/plumbing/object"
	"github.com/go-git/go-git/v5/plumbing/transport"
	"github.com/go-git/go-git/v5/plumbing/transport/http"
	"github.com/golang/mock/gomock"

	"github.com/aws/eks-anywhere/pkg/git"
	"github.com/aws/eks-anywhere/pkg/git/gitclient"
	mockGitClient "github.com/aws/eks-anywhere/pkg/git/gitclient/mocks"
)

const (
	repoDir = "testrepo"
)

func TestGoGitClone(t *testing.T) {
	tests := []struct {
		name       string
		wantErr    bool
		throwError error
		matchError error
	}{
		{
			name:    "clone repo success",
			wantErr: false,
		},
		{
			name:       "empty repository error",
			wantErr:    true,
			throwError: fmt.Errorf("remote repository is empty"),
			matchError: &git.RepositoryIsEmptyError{
				Repository: "testrepo",
			},
		},
	}
	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			ctx, client := newGoGitMock(t)
			repoUrl := "testurl"
			auth := &http.BasicAuth{}

			g := &gitclient.GitClient{
				RepoDirectory: repoDir,
				RepoUrl:       repoUrl,
				Auth:          auth,
				Client:        client,
			}

			client.EXPECT().Clone(ctx, repoDir, repoUrl, auth).Return(&goGit.Repository{}, tt.throwError)

			err := g.Clone(ctx)
			if (err != nil) != tt.wantErr {
				t.Errorf("Clone() error = %v, wantErr = %v", err, tt.wantErr)
				return
			}
			if tt.wantErr {
				if !reflect.DeepEqual(err, tt.matchError) {
					t.Errorf("Clone() error = %v, matchError %v", err, tt.matchError)
				}
			}
		})
	}
}

func TestGoGitAdd(t *testing.T) {
	_, client := newGoGitMock(t)
	filename := "testfile"

	client.EXPECT().OpenDir(repoDir).Return(&goGit.Repository{}, nil)
	client.EXPECT().OpenWorktree(gomock.Any()).Do(func(arg0 *goGit.Repository) {}).Return(&goGit.Worktree{}, nil)
	client.EXPECT().AddGlob(gomock.Any(), gomock.Any()).Do(func(arg0 string, arg1 *goGit.Worktree) {}).Return(nil)

	g := &gitclient.GitClient{
		RepoDirectory: repoDir,
		Client:        client,
	}

	err := g.Add(filename)
	if err != nil {
		t.Errorf("Add() error = %v", err)
		return
	}
}

func TestGoGitRemove(t *testing.T) {
	_, client := newGoGitMock(t)
	filename := "testfile"

	client.EXPECT().OpenDir(repoDir).Return(&goGit.Repository{}, nil)
	client.EXPECT().OpenWorktree(gomock.Any()).Do(func(arg0 *goGit.Repository) {}).Return(&goGit.Worktree{}, nil)
	client.EXPECT().Remove(gomock.Any(), gomock.Any()).Do(func(arg0 string, arg1 *goGit.Worktree) {}).Return(plumbing.Hash{}, nil)

	g := &gitclient.GitClient{
		RepoDirectory: repoDir,
		Client:        client,
	}

	err := g.Remove(filename)
	if err != nil {
		t.Errorf("Remove() error = %v", err)
		return
	}
}

func TestGoGitCommit(t *testing.T) {
	_, client := newGoGitMock(t)
	message := "message"

	client.EXPECT().OpenDir(repoDir).Return(&goGit.Repository{}, nil)
	client.EXPECT().OpenWorktree(gomock.Any()).Do(func(arg0 *goGit.Repository) {}).Return(&goGit.Worktree{}, nil)
	client.EXPECT().Commit(gomock.Any(), gomock.Any(), gomock.Any()).Do(func(arg0 string, arg1 *object.Signature, arg2 *goGit.Worktree) {}).Return(plumbing.Hash{}, nil)
	client.EXPECT().CommitObject(gomock.Any(), gomock.Any()).Do(func(arg0 *goGit.Repository, arg1 plumbing.Hash) {}).Return(&object.Commit{}, nil)

	g := &gitclient.GitClient{
		RepoDirectory: repoDir,
		Client:        client,
	}

	err := g.Commit(message)
	if err != nil {
		t.Errorf("Commit() error = %v", err)
		return
	}
}

func TestGoGitPush(t *testing.T) {
	ctx, client := newGoGitMock(t)

	g := &gitclient.GitClient{
		RepoDirectory: repoDir,
		Client:        client,
	}

	client.EXPECT().OpenDir(repoDir).Return(&goGit.Repository{}, nil)
	client.EXPECT().PushWithContext(ctx, gomock.Any(), gomock.Any()).Do(func(arg0 context.Context, arg1 *goGit.Repository, arg2 transport.AuthMethod) {}).Return(nil)

	err := g.Push(ctx)
	if err != nil {
		t.Errorf("Push() error = %v", err)
		return
	}
}

func TestGoGitPull(t *testing.T) {
	tests := []struct {
		name       string
		wantErr    bool
		throwError error
		matchError error
	}{
		{
			name:    "pull success",
			wantErr: false,
		},
		{
			name:       "repo already up-to-date",
			wantErr:    true,
			throwError: fmt.Errorf("already up-to-date"),
			matchError: fmt.Errorf("pulling from remote: %v", goGit.NoErrAlreadyUpToDate),
		},
	}
	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			ctx, client := newGoGitMock(t)
			branch := "testbranch"

			g := &gitclient.GitClient{
				RepoDirectory: repoDir,
				Client:        client,
			}

			client.EXPECT().OpenDir(repoDir).Return(&goGit.Repository{}, nil)
			client.EXPECT().OpenWorktree(gomock.Any()).Do(func(arg0 *goGit.Repository) {}).Return(&goGit.Worktree{}, nil)
			client.EXPECT().PullWithContext(ctx, gomock.Any(), gomock.Any(), gomock.Any()).Do(func(arg0 context.Context, arg1 *goGit.Worktree, arg2 transport.AuthMethod, name plumbing.ReferenceName) {
			}).Return(tt.throwError)
			if !tt.wantErr {
				client.EXPECT().Head(gomock.Any()).Do(func(arg0 *goGit.Repository) {}).Return(&plumbing.Reference{}, nil)
				client.EXPECT().CommitObject(gomock.Any(), gomock.Any()).Do(func(arg0 *goGit.Repository, arg1 plumbing.Hash) {}).Return(&object.Commit{}, nil)
			}

			err := g.Pull(ctx, branch)
			if (err != nil) != tt.wantErr {
				t.Errorf("Pull() error = %v, wantErr %v", err, tt.wantErr)
				return
			}
			if tt.wantErr {
				if !reflect.DeepEqual(err, tt.matchError) {
					t.Errorf("Pull() error = %v, matchError %v", err, tt.matchError)
				}
			}
		})
	}
}

func TestGoGitInit(t *testing.T) {
	_, client := newGoGitMock(t)
	url := "testurl"

	client.EXPECT().Init(repoDir).Return(&goGit.Repository{}, nil)
	client.EXPECT().Create(gomock.Any(), url).Do(func(arg0 *goGit.Repository, arg1 string) {}).Return(&goGit.Remote{}, nil)

	g := &gitclient.GitClient{
		RepoDirectory: repoDir,
		RepoUrl:       url,
		Client:        client,
	}

	err := g.Init()
	if err != nil {
		t.Errorf("Init() error = %v", err)
		return
	}
}

func TestGoGitBranch(t *testing.T) {
	_, client := newGoGitMock(t)

	repo := &goGit.Repository{}
	headRef := &plumbing.Reference{}
	worktree := &goGit.Worktree{}
	bOpts := &config.Branch{
		Name:   "testBranch",
		Remote: "origin",
		Merge:  "refs/heads/testBranch",
		Rebase: "true",
	}
	cOpts := &goGit.CheckoutOptions{
		Branch: plumbing.NewBranchReferenceName("testBranch"),
		Force:  true,
	}

	client.EXPECT().OpenDir(repoDir).Return(repo, nil)
	client.EXPECT().CreateBranch(repo, bOpts).Return(nil)
	client.EXPECT().Head(repo).Return(headRef, nil)
	client.EXPECT().OpenWorktree(gomock.Any()).Do(func(arg0 *goGit.Repository) {}).Return(worktree, nil)
	client.EXPECT().SetRepositoryReference(repo, gomock.Any()).Return(nil)
	client.EXPECT().Checkout(worktree, cOpts).Return(nil)
	client.EXPECT().ListRemotes(repo, gomock.Any()).Return(nil, nil)

	g := &gitclient.GitClient{
		RepoDirectory: repoDir,
		Client:        client,
	}

	err := g.Branch("testBranch")
	if err != nil {
		t.Errorf("Branch() error = %v", err)
		return
	}
}

func TestGoGitBranchRemoteExists(t *testing.T) {
	_, client := newGoGitMock(t)

	repo := &goGit.Repository{}
	headRef := &plumbing.Reference{}
	worktree := &goGit.Worktree{}
	bOpts := &config.Branch{
		Name:   "testBranch",
		Remote: "origin",
		Merge:  "refs/heads/testBranch",
		Rebase: "true",
	}
	localBranchRef := plumbing.NewBranchReferenceName("testBranch")
	cOpts := &goGit.CheckoutOptions{
		Branch: localBranchRef,
		Force:  true,
	}

	returnReferences := []*plumbing.Reference{
		plumbing.NewHashReference("refs/heads/testBranch", headRef.Hash()),
	}

	client.EXPECT().OpenDir(repoDir).Return(repo, nil)
	client.EXPECT().CreateBranch(repo, bOpts).Return(nil)
	client.EXPECT().Head(repo).Return(headRef, nil)
	client.EXPECT().OpenWorktree(gomock.Any()).Do(func(arg0 *goGit.Repository) {}).Return(worktree, nil)
	client.EXPECT().SetRepositoryReference(repo, gomock.Any()).Return(nil)
	client.EXPECT().Checkout(worktree, cOpts).Return(nil)
	client.EXPECT().ListRemotes(repo, gomock.Any()).Return(returnReferences, nil)
	client.EXPECT().PullWithContext(gomock.Any(), worktree, gomock.Any(), localBranchRef)

	g := &gitclient.GitClient{
		RepoDirectory: repoDir,
		Client:        client,
	}

	err := g.Branch("testBranch")
	if err != nil {
		t.Errorf("Branch() error = %v", err)
		return
	}
}

func TestGoGitValidateRemoteExists(t *testing.T) {
	tests := []struct {
		name       string
		wantErr    bool
		throwError error
	}{
		{
			name:    "validate success",
			wantErr: false,
		},
		{
			name:       "invalid repository error",
			wantErr:    true,
			throwError: fmt.Errorf("remote repository does not exist"),
		},
	}
	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			ctx, client := newGoGitMock(t)

			g := &gitclient.GitClient{
				RepoUrl: "testurl",
				Client:  client,
			}
			remote := &goGit.Remote{}

			client.EXPECT().NewRemote(g.RepoUrl, goGit.DefaultRemoteName).Return(remote)
			client.EXPECT().ListWithContext(ctx, remote, g.Auth).Return([]*plumbing.Reference{}, tt.throwError)

			err := g.ValidateRemoteExists(ctx)
			if (err != nil) != tt.wantErr {
				t.Errorf("Clone() error = %v, wantErr = %v", err, tt.wantErr)
			}
		})
	}
}

func newGoGitMock(t *testing.T) (context.Context, *mockGitClient.MockGoGit) {
	ctx := context.Background()
	ctrl := gomock.NewController(t)
	client := mockGitClient.NewMockGoGit(ctrl)

	return ctx, client
}
