package gitattr

import (
	"io/ioutil"
	"os"
	"testing"

	"github.com/git-lfs/gitobj/v2"
	"github.com/git-lfs/wildmatch/v2"
	"github.com/stretchr/testify/assert"
	"github.com/stretchr/testify/require"
)

var (
	dat = wildmatch.NewWildmatch("*.dat",
		wildmatch.Basename,
		wildmatch.SystemCase)

	example = &Tree{
		Lines: []Line{
			&patternLine{
				pattern: dat,
				lineAttrs: lineAttrs{
					attrs: []*Attr{
						{
							K: "filter", V: "lfs",
						},
						{
							K: "diff", V: "lfs",
						},
						{
							K: "merge", V: "lfs",
						},
						{
							K: "text", V: "false",
						},
					},
				},
			},
		},
		Children: map[string]*Tree{
			"subdir": &Tree{
				Lines: []Line{
					&patternLine{
						pattern: dat,
						lineAttrs: lineAttrs{
							attrs: []*Attr{
								{
									K: "subdir", V: "yes",
								},
							},
						},
					},
				},
			},
		},
	}
)

func TestTreeAppliedInRoot(t *testing.T) {
	attrs := example.Applied("a.dat")

	assert.Len(t, attrs, 4)
	assert.Equal(t, attrs[0], &Attr{K: "filter", V: "lfs"})
	assert.Equal(t, attrs[1], &Attr{K: "diff", V: "lfs"})
	assert.Equal(t, attrs[2], &Attr{K: "merge", V: "lfs"})
	assert.Equal(t, attrs[3], &Attr{K: "text", V: "false"})
}

func TestTreeAppliedInSubtreeRelevant(t *testing.T) {
	attrs := example.Applied("subdir/a.dat")

	assert.Len(t, attrs, 5)
	assert.Equal(t, attrs[0], &Attr{K: "filter", V: "lfs"})
	assert.Equal(t, attrs[1], &Attr{K: "diff", V: "lfs"})
	assert.Equal(t, attrs[2], &Attr{K: "merge", V: "lfs"})
	assert.Equal(t, attrs[3], &Attr{K: "text", V: "false"})
	assert.Equal(t, attrs[4], &Attr{K: "subdir", V: "yes"})
}

func TestTreeAppliedInSubtreeIrrelevant(t *testing.T) {
	attrs := example.Applied("subdir/a.txt")

	assert.Empty(t, attrs)
}

func TestTreeAppliedInIrrelevantSubtree(t *testing.T) {
	attrs := example.Applied("other/subdir/a.dat")

	assert.Len(t, attrs, 4)
	assert.Equal(t, attrs[0], &Attr{K: "filter", V: "lfs"})
	assert.Equal(t, attrs[1], &Attr{K: "diff", V: "lfs"})
	assert.Equal(t, attrs[2], &Attr{K: "merge", V: "lfs"})
	assert.Equal(t, attrs[3], &Attr{K: "text", V: "false"})
}

func TestNewDiscoversSimpleTrees(t *testing.T) {
	tmp, err := ioutil.TempDir("", "")
	require.NoError(t, err)
	defer os.Remove(tmp)

	db, err := gitobj.FromFilesystem(tmp, "")
	require.NoError(t, err)
	defer db.Close()

	blob, err := db.WriteBlob(gitobj.NewBlobFromBytes([]byte(`
		*.dat filter=lfs diff=lfs merge=lfs -text
	`)))
	require.NoError(t, err)

	tree, err := New(db, &gitobj.Tree{Entries: []*gitobj.TreeEntry{
		{
			Name:     ".gitattributes",
			Oid:      blob,
			Filemode: 0100644,
		},
	}})
	require.NoError(t, err)

	attrs := tree.Applied("foo.dat")

	assert.Len(t, attrs, 4)
	assert.Equal(t, attrs[0], &Attr{K: "filter", V: "lfs"})
	assert.Equal(t, attrs[1], &Attr{K: "diff", V: "lfs"})
	assert.Equal(t, attrs[2], &Attr{K: "merge", V: "lfs"})
	assert.Equal(t, attrs[3], &Attr{K: "text", V: "false"})
}

func TestNewDiscoversSimpleChildrenTrees(t *testing.T) {
	tmp, err := ioutil.TempDir("", "")
	require.NoError(t, err)
	defer os.Remove(tmp)

	db, err := gitobj.FromFilesystem(tmp, "")
	require.NoError(t, err)
	defer db.Close()

	blob, err := db.WriteBlob(gitobj.NewBlobFromBytes([]byte(`
		*.dat filter=lfs diff=lfs merge=lfs -text
	`)))
	require.NoError(t, err)

	child, err := db.WriteTree(&gitobj.Tree{Entries: []*gitobj.TreeEntry{
		{
			Name:     ".gitattributes",
			Oid:      blob,
			Filemode: 0100644,
		},
	}})
	require.NoError(t, err)

	tree, err := New(db, &gitobj.Tree{Entries: []*gitobj.TreeEntry{
		{
			Name:     "child",
			Oid:      child,
			Filemode: 040000,
		},
	}})
	require.NoError(t, err)
	assert.Empty(t, tree.Lines)
	assert.Len(t, tree.Children, 1)

	attrs := tree.Applied("child/foo.dat")

	assert.Len(t, attrs, 4)
	assert.Equal(t, attrs[0], &Attr{K: "filter", V: "lfs"})
	assert.Equal(t, attrs[1], &Attr{K: "diff", V: "lfs"})
	assert.Equal(t, attrs[2], &Attr{K: "merge", V: "lfs"})
	assert.Equal(t, attrs[3], &Attr{K: "text", V: "false"})
}

func TestNewDiscoversIndirectChildrenTrees(t *testing.T) {
	tmp, err := ioutil.TempDir("", "")
	require.NoError(t, err)
	defer os.Remove(tmp)

	db, err := gitobj.FromFilesystem(tmp, "")
	require.NoError(t, err)
	defer db.Close()

	blob, err := db.WriteBlob(gitobj.NewBlobFromBytes([]byte(`
		*.dat filter=lfs diff=lfs merge=lfs -text
	`)))
	require.NoError(t, err)

	indirect, err := db.WriteTree(&gitobj.Tree{Entries: []*gitobj.TreeEntry{
		{
			Name:     ".gitattributes",
			Oid:      blob,
			Filemode: 0100644,
		},
	}})
	require.NoError(t, err)

	child, err := db.WriteTree(&gitobj.Tree{Entries: []*gitobj.TreeEntry{
		{
			Name:     "indirect",
			Oid:      indirect,
			Filemode: 040000,
		},
	}})
	require.NoError(t, err)

	tree, err := New(db, &gitobj.Tree{Entries: []*gitobj.TreeEntry{
		{
			Name:     "child",
			Oid:      child,
			Filemode: 040000,
		},
	}})
	require.NoError(t, err)
	assert.Empty(t, tree.Lines)
	assert.Len(t, tree.Children, 1)

	attrs := tree.Applied("child/indirect/foo.dat")

	assert.Len(t, attrs, 4)
	assert.Equal(t, attrs[0], &Attr{K: "filter", V: "lfs"})
	assert.Equal(t, attrs[1], &Attr{K: "diff", V: "lfs"})
	assert.Equal(t, attrs[2], &Attr{K: "merge", V: "lfs"})
	assert.Equal(t, attrs[3], &Attr{K: "text", V: "false"})
}

func TestNewIgnoresChildrenAppropriately(t *testing.T) {
	tmp, err := ioutil.TempDir("", "")
	require.NoError(t, err)
	defer os.Remove(tmp)

	db, err := gitobj.FromFilesystem(tmp, "")
	require.NoError(t, err)
	defer db.Close()

	blob, err := db.WriteBlob(gitobj.NewBlobFromBytes([]byte(`
		*.dat filter=lfs diff=lfs merge=lfs -text
	`)))
	require.NoError(t, err)

	child, err := db.WriteTree(&gitobj.Tree{Entries: []*gitobj.TreeEntry{
		{
			Name:     "README.md",
			Oid:      []byte("00000000000000000000"),
			Filemode: 0100644,
		},
	}})
	require.NoError(t, err)

	tree, err := New(db, &gitobj.Tree{Entries: []*gitobj.TreeEntry{
		{
			Name:     ".gitattributes",
			Oid:      blob,
			Filemode: 0100644,
		},
		{
			Name:     "child",
			Oid:      child,
			Filemode: 040000,
		},
	}})
	require.NoError(t, err)

	assert.NotContains(t, tree.Children, "child")
}
