package unionfs

import (
	"os"
	"github.com/hanwen/go-fuse/fuse"
	"io/ioutil"
	"fmt"
	"log"
	"testing"
	"time"
)

var _ = fmt.Print
var _ = log.Print

var CheckSuccess = fuse.CheckSuccess

func TestFilePathHash(t *testing.T) {
	// Simple test coverage.
	t.Log(filePathHash("xyz/abc"))
}

var testOpts = UnionFsOptions{
	DeletionCacheTTLSecs: entryTtl,
	DeletionDirName:      "DELETIONS",
	BranchCacheTTLSecs:   entryTtl,
}

func setupUfs(t *testing.T) (workdir string, state *fuse.MountState) {
	wd := fuse.MakeTempDir()
	err := os.Mkdir(wd+"/mount", 0700)
	fuse.CheckSuccess(err)

	err = os.Mkdir(wd+"/rw", 0700)
	fuse.CheckSuccess(err)

	os.Mkdir(wd+"/ro", 0700)
	fuse.CheckSuccess(err)

	var roots []string
	roots = append(roots, wd+"/rw")
	roots = append(roots, wd+"/ro")
	ufs := NewUnionFs(roots, testOpts)

	opts := &fuse.MountOptions{
		EntryTimeout: entryTtl,
		AttrTimeout: entryTtl,
		NegativeTimeout: entryTtl,
	}

	connector := fuse.NewFileSystemConnector(ufs, opts)
	state = fuse.NewMountState(connector)
	state.Mount(wd + "/mount")
	state.Debug = true
	go state.Loop(false)

	return wd, state
}

func writeToFile(path string, contents string) {
	err := ioutil.WriteFile(path, []byte(contents), 0644)
	CheckSuccess(err)
}

func readFromFile(path string) string {
	b, err := ioutil.ReadFile(path)
	fmt.Println(b)
	CheckSuccess(err)
	return string(b)
}

func dirNames(path string) map[string]bool {
	f, err := os.Open(path)
	fuse.CheckSuccess(err)

	result := make(map[string]bool)
	names, err := f.Readdirnames(-1)
	fuse.CheckSuccess(err)
	err = f.Close()
	CheckSuccess(err)

	for _, nm := range names {
		result[nm] = true
	}
	return result
}

func checkMapEq(t *testing.T, m1, m2 map[string]bool) {
	if !mapEq(m1, m2) {
		msg := fmt.Sprintf("mismatch: got %v != expect %v", m1, m2)
		log.Print(msg)
		t.Error(msg)
	}
}

func mapEq(m1, m2 map[string]bool) bool {
	if len(m1) != len(m2) {
		return false
	}

	for k, v := range m1 {
		ok, val := m2[k]
		if !ok || val != v {
			return false
		}
	}
	return true
}

func fileExists(path string) bool {
	f, err := os.Lstat(path)
	return err == nil && f != nil
}

func remove(path string) {
	err := os.Remove(path)
	fuse.CheckSuccess(err)
}

func TestSymlink(t *testing.T) {
	wd, state := setupUfs(t)
	defer state.Unmount()

	err := os.Symlink("/foobar", wd+"/mount/link")
	CheckSuccess(err)

	val, err := os.Readlink(wd + "/mount/link")
	CheckSuccess(err)

	if val != "/foobar" {
		t.Errorf("symlink mismatch: %v", val)
	}
}

func TestChtimes(t *testing.T) {
	wd, state := setupUfs(t)
	defer state.Unmount()

	writeToFile(wd+"/ro/file", "a")
	err := os.Chtimes(wd + "/ro/file", 42e9, 43e9)
	CheckSuccess(err)

	err = os.Chtimes(wd + "/mount/file", 82e9, 83e9)
	CheckSuccess(err)

	fi, err := os.Lstat(wd +"/mount/file")
	if fi.Atime_ns != 82e9 || fi.Mtime_ns != 83e9 {
		t.Error("Incorrect timestamp", fi)
	}
}

func TestChmod(t *testing.T) {
	wd, state := setupUfs(t)
	defer state.Unmount()

	ro_fn := wd + "/ro/file"
	m_fn := wd + "/mount/file"
	writeToFile(ro_fn, "a")
	err := os.Chmod(m_fn, 07070)
	CheckSuccess(err)

	err = os.Chown(m_fn, 0, 0)
	code := fuse.OsErrorToErrno(err)
	if code != fuse.EPERM {
		t.Error("Unexpected error code", code, err)
	}

	fi, err := os.Lstat(m_fn)
	CheckSuccess(err)
	if fi.Mode&07777 != 07070 {
		t.Errorf("Unexpected mode found: %o", fi.Mode)
	}
	_, err = os.Lstat(wd + "/rw/file")
	if err != nil {
		t.Errorf("File not promoted")
	}
}

func TestBasic(t *testing.T) {
	wd, state := setupUfs(t)
	defer state.Unmount()

	writeToFile(wd+"/rw/rw", "a")
	writeToFile(wd+"/ro/ro1", "a")
	writeToFile(wd+"/ro/ro2", "b")

	names := dirNames(wd + "/mount")
	expected := map[string]bool{
		"rw": true, "ro1": true, "ro2": true,
	}
	checkMapEq(t, names, expected)

	log.Println("new contents")
	writeToFile(wd+"/mount/new", "new contents")
	if !fileExists(wd + "/rw/new") {
		t.Errorf("missing file in rw layer", names)
	}

	contents := readFromFile(wd+"/mount/new")
	if contents != "new contents" {
		t.Errorf("read mismatch: '%v'", contents)
	}
	return
	writeToFile(wd+"/mount/ro1", "promote me")
	if !fileExists(wd + "/rw/ro1") {
		t.Errorf("missing file in rw layer", names)
	}

	remove(wd + "/mount/new")
	names = dirNames(wd + "/mount")
	checkMapEq(t, names, map[string]bool{
		"rw": true, "ro1": true, "ro2": true,
	})

	names = dirNames(wd + "/rw")
	checkMapEq(t, names, map[string]bool{
		testOpts.DeletionDirName: true,
		"rw":                     true, "ro1": true,
	})
	names = dirNames(wd + "/rw/" + testOpts.DeletionDirName)
	if len(names) != 0 {
		t.Errorf("Expected 0 entry in %v", names)
	}

	remove(wd + "/mount/ro1")
	names = dirNames(wd + "/mount")
	checkMapEq(t, names, map[string]bool{
		"rw": true, "ro2": true,
	})

	names = dirNames(wd + "/rw")
	checkMapEq(t, names, map[string]bool{
		"rw": true, testOpts.DeletionDirName: true,
	})

	names = dirNames(wd + "/rw/" + testOpts.DeletionDirName)
	if len(names) != 1 {
		t.Errorf("Expected 1 entry in %v", names)
	}
}

func TestPromote(t *testing.T) {
	wd, state := setupUfs(t)
	defer state.Unmount()

	err := os.Mkdir(wd+"/ro/subdir", 0755)
	CheckSuccess(err)
	writeToFile(wd+"/ro/subdir/file", "content")
	writeToFile(wd+"/mount/subdir/file", "other-content")
}

func TestCreate(t *testing.T) {
	wd, state := setupUfs(t)
	defer state.Unmount()

	err := os.MkdirAll(wd+"/ro/subdir/sub2", 0755)
	CheckSuccess(err)
	writeToFile(wd+"/mount/subdir/sub2/file", "other-content")
	_, err = os.Lstat(wd + "/mount/subdir/sub2/file")
	CheckSuccess(err)
}

func TestOpenUndeletes(t *testing.T) {
	wd, state := setupUfs(t)
	defer state.Unmount()

	writeToFile(wd+"/ro/file", "X")
	err := os.Remove(wd + "/mount/file")
	CheckSuccess(err)
	writeToFile(wd+"/mount/file", "X")
	_, err = os.Lstat(wd + "/mount/file")
	CheckSuccess(err)
}

func TestMkdir(t *testing.T) {
	wd, state := setupUfs(t)
	defer state.Unmount()

	dirname := wd + "/mount/subdir"
	err := os.Mkdir(dirname, 0755)
	CheckSuccess(err)

	err = os.Remove(dirname)
	CheckSuccess(err)
}

func TestMkdirPromote(t *testing.T) {
	wd, state := setupUfs(t)
	defer state.Unmount()

	dirname := wd + "/ro/subdir/subdir2"
	err := os.MkdirAll(dirname, 0755)
	CheckSuccess(err)

	err = os.Mkdir(wd+"/mount/subdir/subdir2/dir3", 0755)
	CheckSuccess(err)
	fi, _ := os.Lstat(wd+"/rw/subdir/subdir2/dir3")
	CheckSuccess(err)
	if fi == nil || !fi.IsDirectory() {
		t.Error("is not a directory: ", fi)
	}
}
func TestRename(t *testing.T) {
	type Config struct {
		f1_ro bool
		f1_rw bool
		f2_ro bool
		f2_rw bool
	}

	configs := make([]Config, 0)
	for i := 0; i < 16; i++ {
		c := Config{i&0x1 != 0, i&0x2 != 0, i&0x4 != 0, i&0x8 != 0}
		if !(c.f1_ro || c.f1_rw) {
			continue
		}

		configs = append(configs, c)
	}

	for i, c := range configs {
		t.Log("Config", i, c)
		wd, state := setupUfs(t)
		if c.f1_ro {
			writeToFile(wd+"/ro/file1", "c1")
		}
		if c.f1_rw {
			writeToFile(wd+"/rw/file1", "c2")
		}
		if c.f2_ro {
			writeToFile(wd+"/ro/file2", "c3")
		}
		if c.f2_rw {
			writeToFile(wd+"/rw/file2", "c4")
		}

		err := os.Rename(wd+"/mount/file1", wd+"/mount/file2")
		CheckSuccess(err)

		_, err = os.Lstat(wd + "/mount/file1")
		if err == nil {
			t.Errorf("Should have lost file1")
		}
		_, err = os.Lstat(wd + "/mount/file2")
		CheckSuccess(err)

		err = os.Rename(wd+"/mount/file2", wd+"/mount/file1")
		CheckSuccess(err)

		_, err = os.Lstat(wd + "/mount/file2")
		if err == nil {
			t.Errorf("Should have lost file2")
		}
		_, err = os.Lstat(wd + "/mount/file1")
		CheckSuccess(err)

		state.Unmount()
	}
}

func TestWritableDir(t *testing.T) {
	t.Log("TestWritableDir")
	wd, state := setupUfs(t)
	defer state.Unmount()

	dirname := wd + "/ro/subdir"
	err := os.Mkdir(dirname, 0555)
	CheckSuccess(err)

	fi, err := os.Lstat(wd + "/mount/subdir")
	CheckSuccess(err)
	if fi.Permission()&0222 == 0 {
		t.Errorf("unexpected permission %o", fi.Permission())
	}
}

func TestTruncate(t *testing.T) {
	t.Log("TestTruncate")
	wd, state := setupUfs(t)
	defer state.Unmount()

	writeToFile(wd+"/ro/file", "hello")
	os.Truncate(wd+"/mount/file", 2)
	content := readFromFile(wd + "/mount/file")
	if content != "he" {
		t.Errorf("unexpected content %v", content)
	}
	content2 := readFromFile(wd + "/rw/file")
	if content2 != content {
		t.Errorf("unexpected rw content %v", content2)
	}
}

func TestCopyChmod(t *testing.T) {
	t.Log("TestCopyChmod")
	wd, state := setupUfs(t)
	defer state.Unmount()

	contents := "hello"
	fn := wd + "/mount/y"
	err := ioutil.WriteFile(fn, []byte(contents), 0644)
	CheckSuccess(err)

	err = os.Chmod(fn, 0755)
	CheckSuccess(err)

	fi, err := os.Lstat(fn)
	CheckSuccess(err)
	if fi.Mode & 0111 == 0 {
		t.Errorf("1st attr error %o", fi.Mode)
	}
	time.Sleep(entryTtl * 1.1e9)
	fi, err = os.Lstat(fn)
	CheckSuccess(err)
	if fi.Mode & 0111 == 0 {
		t.Errorf("uncached attr error %o", fi.Mode)
	}
}

func abs(dt int64) int64 {
	if dt >= 0 {
		return dt
	} 
	return -dt
}

func TestTruncateTimestamp(t *testing.T) {
	t.Log("TestTruncateTimestamp")
	wd, state := setupUfs(t)
	defer state.Unmount()

	contents := "hello"
	fn := wd + "/mount/y"
	err := ioutil.WriteFile(fn, []byte(contents), 0644)
	CheckSuccess(err)
	time.Sleep(0.2e9)

	truncTs := time.Nanoseconds()
	err = os.Truncate(fn, 3)
	CheckSuccess(err)

	fi, err := os.Lstat(fn)
	CheckSuccess(err)

	if abs(truncTs - fi.Mtime_ns) > 0.1e9 {
		t.Error("timestamp drift", truncTs, fi.Mtime_ns)
	}
}