package unionfs

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

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: 0.01,
	DeletionDirName:      "DELETIONS",
	BranchCacheTTLSecs:   0.01,
}

func setup(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)

	connector := fuse.NewFileSystemConnector(ufs, nil)
	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)
	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 := setup(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 TestChmod(t *testing.T) {
	wd, state := setup(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)

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

func TestBasic(t *testing.T) {
	wd, state := setup(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)

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

	if readFromFile(wd+"/mount/new") != "new contents" {
		t.Errorf("read mismatch.")
	}

	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 := setup(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 := setup(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 := setup(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 := setup(t)
	defer state.Unmount()

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

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

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 := setup(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 := setup(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 := setup(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)
	}
}