package builder

import (
	"fmt"
	"io/ioutil"
	"net/http"
	"net/http/httptest"
	"os"
	"os/exec"
	"path"
	"path/filepath"
	"strings"
	"testing"
	"time"

	"github.com/openshift/origin/pkg/build/api"
	"github.com/openshift/origin/pkg/generate/git"
)

func TestCheckRemoteGit(t *testing.T) {
	server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		w.WriteHeader(http.StatusUnauthorized)
	}))
	defer server.Close()
	gitRepo := git.NewRepositoryWithEnv([]string{"GIT_ASKPASS=true"})

	var err error
	err = checkRemoteGit(gitRepo, server.URL, 10*time.Second)
	switch v := err.(type) {
	case gitAuthError:
	default:
		t.Errorf("expected gitAuthError, got %q", v)
	}

	err = checkRemoteGit(gitRepo, "https://github.com/openshift/origin", 10*time.Second)
	if err != nil {
		t.Errorf("unexpected error %q", err)
	}
}

type testGitRepo struct {
	Name      string
	Path      string
	Files     []string
	Submodule *testGitRepo
}

func initializeTestGitRepo(name string) (*testGitRepo, error) {
	repo := &testGitRepo{Name: name}
	dir, err := ioutil.TempDir("", "test-"+repo.Name)
	if err != nil {
		return repo, err
	}
	repo.Path = dir
	tmpfn := filepath.Join(dir, "initial-file")
	if err := ioutil.WriteFile(tmpfn, []byte("test"), 0666); err != nil {
		return repo, fmt.Errorf("unable to create temporary file")
	}
	repo.Files = append(repo.Files, tmpfn)
	initCmd := exec.Command("git", "init")
	initCmd.Dir = dir
	if out, err := initCmd.CombinedOutput(); err != nil {
		return repo, fmt.Errorf("unable to initialize repository: %q", out)
	}

	configEmailCmd := exec.Command("git", "config", "user.email", "me@example.com")
	configEmailCmd.Dir = dir
	if out, err := configEmailCmd.CombinedOutput(); err != nil {
		return repo, fmt.Errorf("unable to set git email prefs: %q", out)
	}
	configNameCmd := exec.Command("git", "config", "user.name", "Me Myself")
	configNameCmd.Dir = dir
	if out, err := configNameCmd.CombinedOutput(); err != nil {
		return repo, fmt.Errorf("unable to set git name prefs: %q", out)
	}

	return repo, nil
}

func (r *testGitRepo) addSubmodule() error {
	subRepo, err := initializeTestGitRepo("submodule")
	if err != nil {
		return err
	}
	if err := subRepo.addCommit(); err != nil {
		return err
	}
	subCmd := exec.Command("git", "submodule", "add", "file://"+subRepo.Path, "sub")
	subCmd.Dir = r.Path
	if out, err := subCmd.CombinedOutput(); err != nil {
		return fmt.Errorf("unable to add submodule: %q", out)
	}
	r.Submodule = subRepo
	return nil
}

// getRef returns the sha256 of the commit specified by the negative offset.
// The '0' is the current HEAD.
func (r *testGitRepo) getRef(offset int) (string, error) {
	q := ""
	for i := offset; i != 0; i++ {
		q += "^"
	}
	refCmd := exec.Command("git", "rev-parse", "HEAD"+q)
	refCmd.Dir = r.Path
	if out, err := refCmd.CombinedOutput(); err != nil {
		return "", fmt.Errorf("unable to checkout %d offset: %q", offset, out)
	} else {
		return strings.TrimSpace(string(out)), nil
	}
}

func (r *testGitRepo) createBranch(name string) error {
	refCmd := exec.Command("git", "checkout", "-b", name)
	refCmd.Dir = r.Path
	if out, err := refCmd.CombinedOutput(); err != nil {
		return fmt.Errorf("unable to checkout new branch: %q", out)
	}
	return nil
}

func (r *testGitRepo) switchBranch(name string) error {
	refCmd := exec.Command("git", "checkout", name)
	refCmd.Dir = r.Path
	if out, err := refCmd.CombinedOutput(); err != nil {
		return fmt.Errorf("unable to checkout branch: %q", out)
	}
	return nil
}

func (r *testGitRepo) cleanup() {
	os.RemoveAll(r.Path)
	if r.Submodule != nil {
		os.RemoveAll(r.Submodule.Path)
	}
}

func (r *testGitRepo) addCommit() error {
	f, err := ioutil.TempFile(r.Path, "")
	if err != nil {
		return err
	}
	if err := ioutil.WriteFile(f.Name(), []byte("test"), 0666); err != nil {
		return fmt.Errorf("unable to create temporary file %q", f.Name())
	}
	addCmd := exec.Command("git", "add", ".")
	addCmd.Dir = r.Path
	if out, err := addCmd.CombinedOutput(); err != nil {
		return fmt.Errorf("unable to add files to repo: %q", out)
	}
	commitCmd := exec.Command("git", "commit", "-a", "-m", "test commit")
	commitCmd.Dir = r.Path
	out, err := commitCmd.CombinedOutput()
	if err != nil {
		return fmt.Errorf("unable to commit: %q", out)
	}
	r.Files = append(r.Files, f.Name())
	return nil
}

func TestUnqualifiedClone(t *testing.T) {
	repo, err := initializeTestGitRepo("unqualified")
	defer repo.cleanup()
	if err != nil {
		t.Errorf("%v", err)
	}
	if err := repo.addSubmodule(); err != nil {
		t.Errorf("%v", err)
	}
	// add two commits to check that shallow clone take account
	if err := repo.addCommit(); err != nil {
		t.Errorf("unable to add commit: %v", err)
	}
	if err := repo.addCommit(); err != nil {
		t.Errorf("unable to add commit: %v", err)
	}
	destDir, err := ioutil.TempDir("", "clone-dest-")
	defer os.RemoveAll(destDir)
	client := git.NewRepositoryWithEnv([]string{})
	source := &api.GitBuildSource{URI: "file://" + repo.Path}
	revision := api.SourceRevision{Git: &api.GitSourceRevision{}}
	if _, err = extractGitSource(client, source, &revision, destDir, 10*time.Second); err != nil {
		t.Errorf("%v", err)
	}
	for _, f := range repo.Files {
		if _, err := os.Stat(filepath.Join(destDir, path.Base(f))); os.IsNotExist(err) {
			t.Errorf("unable to find repository file %q", path.Base(f))
		}
	}
	if _, err := os.Stat(filepath.Join(destDir, "sub")); os.IsNotExist(err) {
		t.Errorf("unable to find submodule dir")
	}
	for _, f := range repo.Submodule.Files {
		if _, err := os.Stat(filepath.Join(destDir, "sub/"+path.Base(f))); os.IsNotExist(err) {
			t.Errorf("unable to find submodule repository file %q", path.Base(f))
		}
	}
}

func TestCloneFromRef(t *testing.T) {
	repo, err := initializeTestGitRepo("commit")
	defer repo.cleanup()
	if err != nil {
		t.Errorf("%v", err)
	}
	if err := repo.addSubmodule(); err != nil {
		t.Errorf("%v", err)
	}
	// add two commits to check that shallow clone take account
	if err := repo.addCommit(); err != nil {
		t.Errorf("unable to add commit: %v", err)
	}
	if err := repo.addCommit(); err != nil {
		t.Errorf("unable to add commit: %v", err)
	}
	destDir, err := ioutil.TempDir("", "commit-dest-")
	defer os.RemoveAll(destDir)
	client := git.NewRepositoryWithEnv([]string{})
	firstCommitRef, err := repo.getRef(-1)
	if err != nil {
		t.Errorf("%v", err)
	}
	source := &api.GitBuildSource{
		URI: "file://" + repo.Path,
		Ref: firstCommitRef,
	}
	revision := api.SourceRevision{Git: &api.GitSourceRevision{}}
	if _, err = extractGitSource(client, source, &revision, destDir, 10*time.Second); err != nil {
		t.Errorf("%v", err)
	}
	for _, f := range repo.Files[:len(repo.Files)-1] {
		if _, err := os.Stat(filepath.Join(destDir, path.Base(f))); os.IsNotExist(err) {
			t.Errorf("unable to find repository file %q", path.Base(f))
		}
	}
	if _, err := os.Stat(filepath.Join(destDir, path.Base(repo.Files[len(repo.Files)-1]))); !os.IsNotExist(err) {
		t.Errorf("last file should not exists in this checkout")
	}
	if _, err := os.Stat(filepath.Join(destDir, "sub")); os.IsNotExist(err) {
		t.Errorf("unable to find submodule dir")
	}
	for _, f := range repo.Submodule.Files {
		if _, err := os.Stat(filepath.Join(destDir, "sub/"+path.Base(f))); os.IsNotExist(err) {
			t.Errorf("unable to find submodule repository file %q", path.Base(f))
		}
	}
}

func TestCloneFromBranch(t *testing.T) {
	repo, err := initializeTestGitRepo("branch")
	defer repo.cleanup()
	if err != nil {
		t.Errorf("%v", err)
	}
	if err := repo.addSubmodule(); err != nil {
		t.Errorf("%v", err)
	}
	// add two commits to check that shallow clone take account
	if err := repo.addCommit(); err != nil {
		t.Errorf("unable to add commit: %v", err)
	}
	if err := repo.createBranch("test"); err != nil {
		t.Errorf("%v", err)
	}
	if err := repo.addCommit(); err != nil {
		t.Errorf("unable to add commit: %v", err)
	}
	if err := repo.switchBranch("master"); err != nil {
		t.Errorf("%v", err)
	}
	if err := repo.addCommit(); err != nil {
		t.Errorf("unable to add commit: %v", err)
	}
	destDir, err := ioutil.TempDir("", "branch-dest-")
	defer os.RemoveAll(destDir)
	client := git.NewRepositoryWithEnv([]string{})
	source := &api.GitBuildSource{
		URI: "file://" + repo.Path,
		Ref: "test",
	}
	revision := api.SourceRevision{Git: &api.GitSourceRevision{}}
	if _, err = extractGitSource(client, source, &revision, destDir, 10*time.Second); err != nil {
		t.Errorf("%v", err)
	}
	for _, f := range repo.Files[:len(repo.Files)-1] {
		if _, err := os.Stat(filepath.Join(destDir, path.Base(f))); os.IsNotExist(err) {
			t.Errorf("file %q should not exists in the test branch", f)
		}
	}
	if _, err := os.Stat(filepath.Join(destDir, path.Base(repo.Files[len(repo.Files)-1]))); !os.IsNotExist(err) {
		t.Errorf("last file should not exists in the test branch")
	}
	if _, err := os.Stat(filepath.Join(destDir, "sub")); os.IsNotExist(err) {
		t.Errorf("unable to find submodule dir")
	}
	for _, f := range repo.Submodule.Files {
		if _, err := os.Stat(filepath.Join(destDir, "sub/"+path.Base(f))); os.IsNotExist(err) {
			t.Errorf("unable to find submodule repository file %q", path.Base(f))
		}
	}
}