package git

import (
	"fmt"
	"io/ioutil"
	"os"
	"path"
	"reflect"
	"runtime"
	"testing"
	"time"
)

func TestStash(t *testing.T) {
	repo := createTestRepo(t)
	defer cleanupTestRepo(t, repo)

	prepareStashRepo(t, repo)

	sig := &Signature{
		Name:  "Rand Om Hacker",
		Email: "random@hacker.com",
		When:  time.Now(),
	}

	stash1, err := repo.Stashes.Save(sig, "First stash", StashDefault)
	checkFatal(t, err)

	_, err = repo.LookupCommit(stash1)
	checkFatal(t, err)

	b, err := ioutil.ReadFile(pathInRepo(repo, "README"))
	checkFatal(t, err)
	if string(b) == "Update README goes to stash\n" {
		t.Errorf("README still contains the uncommitted changes")
	}

	if !fileExistsInRepo(repo, "untracked.txt") {
		t.Errorf("untracked.txt doesn't exist in the repo; should be untracked")
	}

	// Apply: default

	opts, err := DefaultStashApplyOptions()
	checkFatal(t, err)

	err = repo.Stashes.Apply(0, opts)
	checkFatal(t, err)

	b, err = ioutil.ReadFile(pathInRepo(repo, "README"))
	checkFatal(t, err)
	if string(b) != "Update README goes to stash\n" {
		t.Errorf("README changes aren't here")
	}

	// Apply: no stash for the given index

	err = repo.Stashes.Apply(1, opts)
	if !IsErrorCode(err, ErrorCodeNotFound) {
		t.Errorf("expecting GIT_ENOTFOUND error code %d, got %v", ErrorCodeNotFound, err)
	}

	// Apply: callback stopped

	opts.ProgressCallback = func(progress StashApplyProgress) error {
		if progress == StashApplyProgressCheckoutModified {
			return fmt.Errorf("Stop")
		}
		return nil
	}

	err = repo.Stashes.Apply(0, opts)
	if err.Error() != "Stop" {
		t.Errorf("expecting error 'Stop', got %v", err)
	}

	// Create second stash with ignored files

	os.MkdirAll(pathInRepo(repo, "tmp"), os.ModeDir|os.ModePerm)
	err = ioutil.WriteFile(pathInRepo(repo, "tmp/ignored.txt"), []byte("Ignore me\n"), 0644)
	checkFatal(t, err)

	stash2, err := repo.Stashes.Save(sig, "Second stash", StashIncludeIgnored)
	checkFatal(t, err)

	if fileExistsInRepo(repo, "tmp/ignored.txt") {
		t.Errorf("tmp/ignored.txt should not exist anymore in the work dir")
	}

	// Stash foreach

	expected := []stash{
		{0, "On master: Second stash", stash2.String()},
		{1, "On master: First stash", stash1.String()},
	}
	checkStashes(t, repo, expected)

	// Stash pop

	opts, _ = DefaultStashApplyOptions()
	err = repo.Stashes.Pop(1, opts)
	checkFatal(t, err)

	b, err = ioutil.ReadFile(pathInRepo(repo, "README"))
	checkFatal(t, err)
	if string(b) != "Update README goes to stash\n" {
		t.Errorf("README changes aren't here")
	}

	expected = []stash{
		{0, "On master: Second stash", stash2.String()},
	}
	checkStashes(t, repo, expected)

	// Stash drop

	err = repo.Stashes.Drop(0)
	checkFatal(t, err)

	expected = []stash{}
	checkStashes(t, repo, expected)
}

type stash struct {
	index int
	msg   string
	id    string
}

func checkStashes(t *testing.T, repo *Repository, expected []stash) {
	var actual []stash

	repo.Stashes.Foreach(func(index int, msg string, id *Oid) error {
		stash := stash{index, msg, id.String()}
		if len(expected) > len(actual) {
			if s := expected[len(actual)]; s.id == "" {
				stash.id = "" //  don't check id
			}
		}
		actual = append(actual, stash)
		return nil
	})

	if len(expected) > 0 && !reflect.DeepEqual(expected, actual) {
		// The failure happens at wherever we were called, not here
		_, file, line, ok := runtime.Caller(1)
		if !ok {
			t.Fatalf("Unable to get caller")
		}
		t.Errorf("%v:%v: expecting %#v\ngot %#v", path.Base(file), line, expected, actual)
	}
}

func prepareStashRepo(t *testing.T, repo *Repository) {
	seedTestRepo(t, repo)

	err := ioutil.WriteFile(pathInRepo(repo, ".gitignore"), []byte("tmp\n"), 0644)
	checkFatal(t, err)

	sig := &Signature{
		Name:  "Rand Om Hacker",
		Email: "random@hacker.com",
		When:  time.Now(),
	}

	idx, err := repo.Index()
	checkFatal(t, err)
	err = idx.AddByPath(".gitignore")
	checkFatal(t, err)
	treeID, err := idx.WriteTree()
	checkFatal(t, err)
	err = idx.Write()
	checkFatal(t, err)

	currentBranch, err := repo.Head()
	checkFatal(t, err)
	currentTip, err := repo.LookupCommit(currentBranch.Target())
	checkFatal(t, err)

	message := "Add .gitignore\n"
	tree, err := repo.LookupTree(treeID)
	checkFatal(t, err)
	_, err = repo.CreateCommit("HEAD", sig, sig, message, tree, currentTip)
	checkFatal(t, err)

	err = ioutil.WriteFile(pathInRepo(repo, "README"), []byte("Update README goes to stash\n"), 0644)
	checkFatal(t, err)

	err = ioutil.WriteFile(pathInRepo(repo, "untracked.txt"), []byte("Hello, World\n"), 0644)
	checkFatal(t, err)
}

func fileExistsInRepo(repo *Repository, name string) bool {
	if _, err := os.Stat(pathInRepo(repo, name)); err != nil {
		return false
	}
	return true
}
