package git

import (
	"os"
	"testing"
	"time"

	"github.com/go-git/go-git/v5/plumbing"
	"github.com/go-git/go-git/v5/plumbing/cache"
	"github.com/go-git/go-git/v5/plumbing/format/packfile"
	"github.com/go-git/go-git/v5/plumbing/object"
	"github.com/go-git/go-git/v5/storage/filesystem"
	"github.com/go-git/go-git/v5/storage/memory"

	"github.com/go-git/go-billy/v5"
	"github.com/go-git/go-billy/v5/memfs"
	"github.com/go-git/go-billy/v5/osfs"
	"github.com/go-git/go-billy/v5/util"
	fixtures "github.com/go-git/go-git-fixtures/v4"
	. "gopkg.in/check.v1"
)

func Test(t *testing.T) { TestingT(t) }

type BaseSuite struct {
	fixtures.Suite
	Repository *Repository

	cache map[string]*Repository
}

func (s *BaseSuite) SetUpSuite(c *C) {
	s.buildBasicRepository(c)

	s.cache = make(map[string]*Repository)
}

func (s *BaseSuite) TearDownSuite(c *C) {
	s.Suite.TearDownSuite(c)
}

func (s *BaseSuite) buildBasicRepository(_ *C) {
	f := fixtures.Basic().One()
	s.Repository = s.NewRepository(f)
}

// NewRepository returns a new repository using the .git folder, if the fixture
// is tagged as worktree the filesystem from fixture is used, otherwise a new
// memfs filesystem is used as worktree.
func (s *BaseSuite) NewRepository(f *fixtures.Fixture) *Repository {
	var worktree, dotgit billy.Filesystem
	if f.Is("worktree") {
		r, err := PlainOpen(f.Worktree().Root())
		if err != nil {
			panic(err)
		}

		return r
	}

	dotgit = f.DotGit()
	worktree = memfs.New()

	st := filesystem.NewStorage(dotgit, cache.NewObjectLRUDefault())

	r, err := Open(st, worktree)
	if err != nil {
		panic(err)
	}

	return r
}

// NewRepositoryWithEmptyWorktree returns a new repository using the .git folder
// from the fixture but without a empty memfs worktree, the index and the
// modules are deleted from the .git folder.
func (s *BaseSuite) NewRepositoryWithEmptyWorktree(f *fixtures.Fixture) *Repository {
	dotgit := f.DotGit()
	err := dotgit.Remove("index")
	if err != nil {
		panic(err)
	}

	err = util.RemoveAll(dotgit, "modules")
	if err != nil {
		panic(err)
	}

	worktree := memfs.New()

	st := filesystem.NewStorage(dotgit, cache.NewObjectLRUDefault())

	r, err := Open(st, worktree)
	if err != nil {
		panic(err)
	}

	return r

}

func (s *BaseSuite) NewRepositoryFromPackfile(f *fixtures.Fixture) *Repository {
	h := f.PackfileHash
	if r, ok := s.cache[h]; ok {
		return r
	}

	storer := memory.NewStorage()
	p := f.Packfile()
	defer func() { _ = p.Close() }()

	if err := packfile.UpdateObjectStorage(storer, p); err != nil {
		panic(err)
	}

	err := storer.SetReference(plumbing.NewHashReference(plumbing.HEAD, plumbing.NewHash(f.Head)))
	if err != nil {
		panic(err)
	}

	r, err := Open(storer, memfs.New())
	if err != nil {
		panic(err)
	}

	s.cache[h] = r
	return r
}

func (s *BaseSuite) GetBasicLocalRepositoryURL() string {
	fixture := fixtures.Basic().One()
	return s.GetLocalRepositoryURL(fixture)
}

func (s *BaseSuite) GetLocalRepositoryURL(f *fixtures.Fixture) string {
	return f.DotGit().Root()
}

func (s *BaseSuite) TemporalDir() (path string, clean func()) {
	fs := osfs.New(os.TempDir())
	relPath, err := util.TempDir(fs, "", "")
	if err != nil {
		panic(err)
	}

	path = fs.Join(fs.Root(), relPath)
	clean = func() {
		_ = util.RemoveAll(fs, relPath)
	}

	return
}

func (s *BaseSuite) TemporalHomeDir() (path string, clean func()) {
	home, err := os.UserHomeDir()
	if err != nil {
		panic(err)
	}

	fs := osfs.New(home)
	relPath, err := util.TempDir(fs, "", "")
	if err != nil {
		panic(err)
	}

	path = fs.Join(fs.Root(), relPath)
	clean = func() {
		_ = util.RemoveAll(fs, relPath)
	}

	return
}

func (s *BaseSuite) TemporalFilesystem() (fs billy.Filesystem, clean func()) {
	fs = osfs.New(os.TempDir())
	path, err := util.TempDir(fs, "", "")
	if err != nil {
		panic(err)
	}

	fs, err = fs.Chroot(path)
	if err != nil {
		panic(err)
	}

	clean = func() {
		_ = util.RemoveAll(fs, path)
	}

	return
}

type SuiteCommon struct{}

var _ = Suite(&SuiteCommon{})

var countLinesTests = [...]struct {
	i string // the string we want to count lines from
	e int    // the expected number of lines in i
}{
	{"", 0},
	{"a", 1},
	{"a\n", 1},
	{"a\nb", 2},
	{"a\nb\n", 2},
	{"a\nb\nc", 3},
	{"a\nb\nc\n", 3},
	{"a\n\n\nb\n", 4},
	{"first line\n\tsecond line\nthird line\n", 3},
}

func (s *SuiteCommon) TestCountLines(c *C) {
	for i, t := range countLinesTests {
		o := countLines(t.i)
		c.Assert(o, Equals, t.e, Commentf("subtest %d, input=%q", i, t.i))
	}
}

func AssertReferences(c *C, r *Repository, expected map[string]string) {
	for name, target := range expected {
		expected := plumbing.NewReferenceFromStrings(name, target)

		obtained, err := r.Reference(expected.Name(), true)
		c.Assert(err, IsNil)

		c.Assert(obtained, DeepEquals, expected)
	}
}

func AssertReferencesMissing(c *C, r *Repository, expected []string) {
	for _, name := range expected {
		_, err := r.Reference(plumbing.ReferenceName(name), false)
		c.Assert(err, NotNil)
		c.Assert(err, Equals, plumbing.ErrReferenceNotFound)
	}
}

func CommitNewFile(c *C, repo *Repository, fileName string) plumbing.Hash {
	wt, err := repo.Worktree()
	c.Assert(err, IsNil)

	fd, err := wt.Filesystem.Create(fileName)
	c.Assert(err, IsNil)

	_, err = fd.Write([]byte("# test file"))
	c.Assert(err, IsNil)

	err = fd.Close()
	c.Assert(err, IsNil)

	_, err = wt.Add(fileName)
	c.Assert(err, IsNil)

	sha, err := wt.Commit("test commit", &CommitOptions{
		Author: &object.Signature{
			Name:  "test",
			Email: "test@example.com",
			When:  time.Now(),
		},
		Committer: &object.Signature{
			Name:  "test",
			Email: "test@example.com",
			When:  time.Now(),
		},
	})
	c.Assert(err, IsNil)

	return sha
}
