package mtree

import (
	"bytes"
	"os"
	"path/filepath"
	"testing"
	"time"
)

// simple walk of current directory, and immediately check it.
// may not be parallelizable.
func TestCheck(t *testing.T) {
	dh, err := Walk(".", nil, append(DefaultKeywords, []Keyword{"sha1", "xattr"}...), nil)
	if err != nil {
		t.Fatal(err)
	}

	res, err := Check(".", dh, nil, nil)
	if err != nil {
		t.Fatal(err)
	}

	if len(res) > 0 {
		t.Errorf("%#v", res)
	}
}

// make a directory, walk it, check it, modify the timestamp and ensure it fails.
// only check again for size and sha1, and ignore time, and ensure it passes
func TestCheckKeywords(t *testing.T) {
	content := []byte("I know half of you half as well as I ought to")
	dir, err := os.MkdirTemp("", "test-check-keywords")
	if err != nil {
		t.Fatal(err)
	}
	defer os.RemoveAll(dir) // clean up

	tmpfn := filepath.Join(dir, "tmpfile")
	if err := os.WriteFile(tmpfn, content, 0666); err != nil {
		t.Fatal(err)
	}

	// Walk this tempdir
	dh, err := Walk(dir, nil, append(DefaultKeywords, "sha1"), nil)
	if err != nil {
		t.Fatal(err)
	}

	// Check for sanity. This ought to pass.
	res, err := Check(dir, dh, nil, nil)
	if err != nil {
		t.Fatal(err)
	}
	if len(res) > 0 {
		t.Errorf("%#v", res)
	}

	// Touch a file, so the mtime changes.
	newtime := time.Date(2006, time.February, 1, 3, 4, 5, 0, time.UTC)
	if err := os.Chtimes(tmpfn, newtime, newtime); err != nil {
		t.Fatal(err)
	}

	// Check again. This ought to fail.
	res, err = Check(dir, dh, nil, nil)
	if err != nil {
		t.Fatal(err)
	}
	if len(res) != 1 {
		t.Fatal("expected to get 1 delta on changed mtimes, but did not")
	}
	if res[0].Type() != Modified {
		t.Errorf("expected to get modified delta on changed mtimes, but did not")
	}

	// Check again, but only sha1 and mode. This ought to pass.
	res, err = Check(dir, dh, []Keyword{"sha1", "mode"}, nil)
	if err != nil {
		t.Fatal(err)
	}
	if len(res) > 0 {
		t.Errorf("%#v", res)
	}
}

func ExampleCheck() {
	dh, err := Walk(".", nil, append(DefaultKeywords, "sha1"), nil)
	if err != nil {
		// handle error ...
	}

	res, err := Check(".", dh, nil, nil)
	if err != nil {
		// handle error ...
	}
	if len(res) > 0 {
		// handle failed validity ...
	}
}

// Tests default action for evaluating a symlink, which is just to compare the
// link itself, not to follow it
func TestDefaultBrokenLink(t *testing.T) {
	dh, err := Walk("./testdata/dirwithbrokenlink", nil, append(DefaultKeywords, "sha1"), nil)
	if err != nil {
		t.Fatal(err)
	}
	res, err := Check("./testdata/dirwithbrokenlink", dh, nil, nil)
	if err != nil {
		t.Fatal(err)
	}
	if len(res) > 0 {
		for _, delta := range res {
			t.Error(delta)
		}
	}
}

// https://github.com/vbatts/go-mtree/issues/8
func TestTimeComparison(t *testing.T) {
	dir, err := os.MkdirTemp("", "test-time.")
	if err != nil {
		t.Fatal(err)
	}
	defer os.RemoveAll(dir)

	// This is the format of time from FreeBSD
	spec := `
/set type=file time=5.000000000
.               type=dir
    file       time=5.000000000
..
`

	fh, err := os.Create(filepath.Join(dir, "file"))
	if err != nil {
		t.Fatal(err)
	}
	// This is what mode we're checking for. Round integer of epoch seconds
	epoch := time.Unix(5, 0)
	if err := os.Chtimes(fh.Name(), epoch, epoch); err != nil {
		t.Fatal(err)
	}
	if err := os.Chtimes(dir, epoch, epoch); err != nil {
		t.Fatal(err)
	}
	if err := fh.Close(); err != nil {
		t.Error(err)
	}

	dh, err := ParseSpec(bytes.NewBufferString(spec))
	if err != nil {
		t.Fatal(err)
	}

	res, err := Check(dir, dh, nil, nil)
	if err != nil {
		t.Error(err)
	}
	if len(res) > 0 {
		t.Fatal(res)
	}
}

func TestTarTime(t *testing.T) {
	dir, err := os.MkdirTemp("", "test-tar-time.")
	if err != nil {
		t.Fatal(err)
	}
	defer os.RemoveAll(dir)

	// This is the format of time from FreeBSD
	spec := `
/set type=file time=5.454353132
.               type=dir time=5.123456789
    file       time=5.911134111
..
`

	fh, err := os.Create(filepath.Join(dir, "file"))
	if err != nil {
		t.Fatal(err)
	}
	// This is what mode we're checking for. Round integer of epoch seconds
	epoch := time.Unix(5, 0)
	if err := os.Chtimes(fh.Name(), epoch, epoch); err != nil {
		t.Fatal(err)
	}
	if err := os.Chtimes(dir, epoch, epoch); err != nil {
		t.Fatal(err)
	}
	if err := fh.Close(); err != nil {
		t.Error(err)
	}

	dh, err := ParseSpec(bytes.NewBufferString(spec))
	if err != nil {
		t.Fatal(err)
	}

	keywords := dh.UsedKeywords()

	// make sure "time" keyword works
	_, err = Check(dir, dh, keywords, nil)
	if err != nil {
		t.Error(err)
	}

	// make sure tar_time wins
	res, err := Check(dir, dh, append(keywords, "tar_time"), nil)
	if err != nil {
		t.Error(err)
	}
	if len(res) > 0 {
		t.Fatal(res)
	}
}

func TestIgnoreComments(t *testing.T) {
	dir, err := os.MkdirTemp("", "test-comments.")
	if err != nil {
		t.Fatal(err)
	}
	defer os.RemoveAll(dir)

	// This is the format of time from FreeBSD
	spec := `
/set type=file time=5.000000000
.               type=dir
    file1       time=5.000000000
..
`

	fh, err := os.Create(filepath.Join(dir, "file1"))
	if err != nil {
		t.Fatal(err)
	}
	// This is what mode we're checking for. Round integer of epoch seconds
	epoch := time.Unix(5, 0)
	if err := os.Chtimes(fh.Name(), epoch, epoch); err != nil {
		t.Fatal(err)
	}
	if err := os.Chtimes(dir, epoch, epoch); err != nil {
		t.Fatal(err)
	}
	if err := fh.Close(); err != nil {
		t.Error(err)
	}

	dh, err := ParseSpec(bytes.NewBufferString(spec))
	if err != nil {
		t.Fatal(err)
	}

	res, err := Check(dir, dh, nil, nil)
	if err != nil {
		t.Error(err)
	}

	if len(res) > 0 {
		t.Fatal(res)
	}

	// now change the spec to a comment that looks like an actual Entry but has
	// whitespace in front of it
	spec = `
/set type=file time=5.000000000
.               type=dir
    file1       time=5.000000000
	#file2 		time=5.000000000
..
`
	dh, err = ParseSpec(bytes.NewBufferString(spec))
	if err != nil {
		t.Error(err)
	}

	res, err = Check(dir, dh, nil, nil)
	if err != nil {
		t.Error(err)
	}

	if len(res) > 0 {
		t.Fatal(res)
	}
}

func TestCheckNeedsEncoding(t *testing.T) {
	dir, err := os.MkdirTemp("", "test-needs-encoding")
	if err != nil {
		t.Fatal(err)
	}
	defer os.RemoveAll(dir)

	fh, err := os.Create(filepath.Join(dir, "file[ "))
	if err != nil {
		t.Fatal(err)
	}
	if err := fh.Close(); err != nil {
		t.Error(err)
	}
	fh, err = os.Create(filepath.Join(dir, "    , should work"))
	if err != nil {
		t.Fatal(err)
	}
	if err := fh.Close(); err != nil {
		t.Error(err)
	}

	dh, err := Walk(dir, nil, DefaultKeywords, nil)
	if err != nil {
		t.Fatal(err)
	}
	res, err := Check(dir, dh, nil, nil)
	if err != nil {
		t.Fatal(err)
	}
	if len(res) > 0 {
		t.Fatal(res)
	}
}
