package fuse

import (
	"bytes"
	"fmt"
	"io/ioutil"
	"log"
	"os"
	"path/filepath"
	"rand"
	"strings"
	"syscall"
	"testing"
)

var _ = strings.Join
var _ = log.Println

////////////////
// state for our testcase, mostly constants

const contents string = "ABC"
const mode uint32 = 0757

type testCase struct {
	tmpDir string
	orig   string
	mnt    string

	mountFile    string
	mountSubdir  string
	origFile     string
	origSubdir   string
	tester       *testing.T
	state        *MountState
	connector    *FileSystemConnector
}

const testTtl = 0.1

// Create and mount filesystem.
func NewTestCase(t *testing.T) *testCase {
	me := &testCase{}
	me.tester = t
	paranoia = true

	// Make sure system setting does not affect test.
	syscall.Umask(0)

	const name string = "hello.txt"
	const subdir string = "subdir"

	me.tmpDir = MakeTempDir()
	me.orig = me.tmpDir + "/orig"
	me.mnt = me.tmpDir + "/mnt"

	os.Mkdir(me.orig, 0700)
	os.Mkdir(me.mnt, 0700)

	me.mountFile = filepath.Join(me.mnt, name)
	me.mountSubdir = filepath.Join(me.mnt, subdir)
	me.origFile = filepath.Join(me.orig, name)
	me.origSubdir = filepath.Join(me.orig, subdir)

	var pfs FileSystem
	pfs = NewLoopbackFileSystem(me.orig)
	pfs = NewTimingFileSystem(pfs)
	pfs = NewLockingFileSystem(pfs)

	var rfs RawFileSystem
	me.connector = NewFileSystemConnector(pfs,
		&FileSystemOptions{
			EntryTimeout:    testTtl,
			AttrTimeout:     testTtl,
			NegativeTimeout: 0.0,
		})
	rfs = me.connector
	rfs = NewTimingRawFileSystem(rfs)
	rfs = NewLockingRawFileSystem(rfs)

	me.connector.Debug = true
	me.state = NewMountState(rfs)
	me.state.Mount(me.mnt, nil)

	//me.state.Debug = false
	me.state.Debug = true

	// Unthreaded, but in background.
	go me.state.Loop(false)
	return me
}

// Unmount and del.
func (me *testCase) Cleanup() {
	fmt.Println("Unmounting.")
	err := me.state.Unmount()
	CheckSuccess(err)
	os.Remove(me.tmpDir)
}

func (me *testCase) writeOrigFile() {
}

////////////////
// Tests.

func TestOpenUnreadable(t *testing.T) {
	ts := NewTestCase(t)
	defer ts.Cleanup()
	_, err := os.Open(ts.mnt + "/doesnotexist")
	if err == nil {
		t.Errorf("open non-existent should raise error")
	}
}

func TestTouch(t *testing.T) {
	ts := NewTestCase(t)
	defer ts.Cleanup()

	log.Println("testTouch")
	err := ioutil.WriteFile(ts.origFile, []byte(contents), 0700)
	CheckSuccess(err)
	err = os.Chtimes(ts.mountFile, 42e9, 43e9)
	CheckSuccess(err)
	fi, err := os.Lstat(ts.mountFile)
	CheckSuccess(err)
	if fi.Atime_ns != 42e9 || fi.Mtime_ns != 43e9 {
		t.Errorf("Got wrong timestamps %v", fi)
	}
}

func (me *testCase) TestReadThrough(t *testing.T) {
	ts := NewTestCase(t)
	defer ts.Cleanup()

	err := ioutil.WriteFile(ts.origFile, []byte(contents), 0700)
	CheckSuccess(err)

	fmt.Println("Testing chmod.")
	err = os.Chmod(ts.mountFile, mode)
	CheckSuccess(err)

	fmt.Println("Testing Lstat.")
	fi, err := os.Lstat(ts.mountFile)
	CheckSuccess(err)
	if (fi.Mode & 0777) != mode {
		t.Errorf("Wrong mode %o != %o", fi.Mode, mode)
	}

	// Open (for read), read.
	fmt.Println("Testing open.")
	f, err := os.Open(ts.mountFile)
	CheckSuccess(err)
	defer f.Close()

	fmt.Println("Testing read.")
	var buf [1024]byte
	slice := buf[:]
	n, err := f.Read(slice)

	if len(slice[:n]) != len(contents) {
		t.Errorf("Content error %v", slice)
	}
	fmt.Println("Testing close.")
}

func TestRemove(t *testing.T) {
	me := NewTestCase(t)
	defer me.Cleanup()

	err := ioutil.WriteFile(me.origFile, []byte(contents), 0700)
	CheckSuccess(err)

	err = os.Remove(me.mountFile)
	CheckSuccess(err)
	_, err = os.Lstat(me.origFile)
	if err == nil {
		t.Errorf("Lstat() after delete should have generated error.")
	}
}

func TestWriteThrough(t *testing.T) {
	me := NewTestCase(t)
	defer me.Cleanup()

	// Create (for write), write.
	f, err := os.OpenFile(me.mountFile, os.O_WRONLY|os.O_CREATE, 0644)
	CheckSuccess(err)
	defer f.Close()

	n, err := f.WriteString(contents)
	CheckSuccess(err)
	if n != len(contents) {
		t.Errorf("Write mismatch: %v of %v", n, len(contents))
	}

	fi, err := os.Lstat(me.origFile)
	if fi.Mode&0777 != 0644 {
		t.Errorf("create mode error %o", fi.Mode&0777)
	}

	f, err = os.Open(me.origFile)
	CheckSuccess(err)
	defer f.Close()

	var buf [1024]byte
	slice := buf[:]
	n, err = f.Read(slice)
	CheckSuccess(err)
	if string(slice[:n]) != contents {
		t.Errorf("write contents error. Got: %v, expect: %v", string(slice[:n]), contents)
	}
}

func TestMkdirRmdir(t *testing.T) {
	me := NewTestCase(t)
	defer me.Cleanup()

	// Mkdir/Rmdir.
	err := os.Mkdir(me.mountSubdir, 0777)
	CheckSuccess(err)
	fi, err := os.Lstat(me.origSubdir)
	if !fi.IsDirectory() {
		t.Errorf("Not a directory: %o", fi.Mode)
	}

	err = os.Remove(me.mountSubdir)
	CheckSuccess(err)
}

func TestLink(t *testing.T) {
	me := NewTestCase(t)
	defer me.Cleanup()

	t.Log("Testing hard links.")
	err := ioutil.WriteFile(me.origFile, []byte(contents), 0700)
	CheckSuccess(err)
	err = os.Mkdir(me.origSubdir, 0777)
	CheckSuccess(err)

	// Link.
	mountSubfile := filepath.Join(me.mountSubdir, "subfile")
	err = os.Link(me.mountFile, mountSubfile)
	CheckSuccess(err)

	subfi, err := os.Lstat(mountSubfile)
	CheckSuccess(err)
	fi, err := os.Lstat(me.origFile)
	CheckSuccess(err)

	if fi.Nlink != 2 {
		t.Errorf("Expect 2 links: %v", fi)
	}
	if fi.Ino != subfi.Ino {
		t.Errorf("Link succeeded, but inode numbers different: %v %v", fi.Ino, subfi.Ino)
	}
	f, err := os.Open(mountSubfile)

	var buf [1024]byte
	slice := buf[:]
	n, err := f.Read(slice)
	f.Close()

	strContents := string(slice[:n])
	if strContents != contents {
		t.Errorf("Content error: %v", slice[:n])
	}
}

func TestSymlink(t *testing.T) {
	me := NewTestCase(t)
	defer me.Cleanup()

	t.Log("testing symlink/readlink.")
	err := ioutil.WriteFile(me.origFile, []byte(contents), 0700)
	CheckSuccess(err)

	linkFile := "symlink-file"
	orig := "hello.txt"
	err = os.Symlink(orig, filepath.Join(me.mnt, linkFile))

	CheckSuccess(err)

	origLink := filepath.Join(me.orig, linkFile)
	fi, err := os.Lstat(origLink)
	CheckSuccess(err)

	if !fi.IsSymlink() {
		t.Errorf("not a symlink: %o", fi.Mode)
		return
	}

	read, err := os.Readlink(filepath.Join(me.mnt, linkFile))
	CheckSuccess(err)

	if read != orig {
		t.Errorf("unexpected symlink value '%v'", read)
	}
}

func TestRename(t *testing.T) {
	me := NewTestCase(t)
	defer me.Cleanup()

	t.Log("Testing rename.")
	err := ioutil.WriteFile(me.origFile, []byte(contents), 0700)
	CheckSuccess(err)
	sd := me.mnt + "/testRename"
	err = os.MkdirAll(sd, 0777)

	subFile := sd + "/subfile"
	err = os.Rename(me.mountFile, subFile)
	CheckSuccess(err)
	f, _ := os.Lstat(me.origFile)
	if f != nil {
		t.Errorf("original %v still exists.", me.origFile)
	}
	f, _ = os.Lstat(subFile)
	if f == nil {
		t.Errorf("destination %v does not exist.", subFile)
	}
}

// Flaky test, due to rename race condition.
func TestDelRename(t *testing.T) {
	me := NewTestCase(t)
	defer me.Cleanup()

	t.Log("Testing del+rename.")

	sd := me.mnt + "/testDelRename"
	err := os.MkdirAll(sd, 0755)
	CheckSuccess(err)

	d := sd + "/dest"
	err = ioutil.WriteFile(d, []byte("blabla"), 0644)
	CheckSuccess(err)

	f, err := os.Open(d)
	CheckSuccess(err)
	defer f.Close()

	err = os.Remove(d)
	CheckSuccess(err)

	s := sd + "/src"
	err = ioutil.WriteFile(s, []byte("blabla"), 0644)
	CheckSuccess(err)

	err = os.Rename(s, d)
	CheckSuccess(err)
}

func TestOverwriteRename(t *testing.T) {
	me := NewTestCase(t)
	defer me.Cleanup()

	t.Log("Testing rename overwrite.")

	sd := me.mnt + "/testOverwriteRename"
	err := os.MkdirAll(sd, 0755)
	CheckSuccess(err)

	d := sd + "/dest"
	err = ioutil.WriteFile(d, []byte("blabla"), 0644)
	CheckSuccess(err)

	s := sd + "/src"
	err = ioutil.WriteFile(s, []byte("blabla"), 0644)
	CheckSuccess(err)

	err = os.Rename(s, d)
	CheckSuccess(err)
}

func TestAccess(t *testing.T) {
	if os.Geteuid() == 0 {
		t.Log("Skipping TestAccess() as root.")
		return
	}
	me := NewTestCase(t)
	defer me.Cleanup()

	err := ioutil.WriteFile(me.origFile, []byte(contents), 0700)
	CheckSuccess(err)
	err = os.Chmod(me.origFile, 0)
	CheckSuccess(err)
	// Ugh - copied from unistd.h
	const W_OK uint32 = 2

	errCode := syscall.Access(me.mountFile, W_OK)
	if errCode != syscall.EACCES {
		t.Errorf("Expected EACCES for non-writable, %v %v", errCode, syscall.EACCES)
	}
	err = os.Chmod(me.origFile, 0222)
	CheckSuccess(err)
	errCode = syscall.Access(me.mountFile, W_OK)
	if errCode != 0 {
		t.Errorf("Expected no error code for writable. %v", errCode)
	}
}

func TestMknod(t *testing.T) {
	me := NewTestCase(t)
	defer me.Cleanup()

	t.Log("Testing mknod.")
	errNo := syscall.Mknod(me.mountFile, syscall.S_IFIFO|0777, 0)
	if errNo != 0 {
		t.Errorf("Mknod %v", errNo)
	}
	fi, _ := os.Lstat(me.origFile)
	if fi == nil || !fi.IsFifo() {
		t.Errorf("Expected FIFO filetype.")
	}
}

func TestReaddir(t *testing.T) {
	me := NewTestCase(t)
	defer me.Cleanup()

	t.Log("Testing readdir.")
	err := ioutil.WriteFile(me.origFile, []byte(contents), 0700)
	CheckSuccess(err)
	err = os.Mkdir(me.origSubdir, 0777)
	CheckSuccess(err)

	dir, err := os.Open(me.mnt)
	CheckSuccess(err)
	infos, err := dir.Readdir(10)
	CheckSuccess(err)

	wanted := map[string]bool{
		"hello.txt": true,
		"subdir":    true,
	}
	if len(wanted) != len(infos) {
		t.Errorf("Length mismatch %v", infos)
	} else {
		for _, v := range infos {
			_, ok := wanted[v.Name]
			if !ok {
				t.Errorf("Unexpected name %v", v.Name)
			}
		}
	}

	dir.Close()
}

func TestFSync(t *testing.T) {
	me := NewTestCase(t)
	defer me.Cleanup()

	t.Log("Testing fsync.")
	err := ioutil.WriteFile(me.origFile, []byte(contents), 0700)
	CheckSuccess(err)

	f, err := os.OpenFile(me.mountFile, os.O_WRONLY, 0)
	_, err = f.WriteString("hello there")
	CheckSuccess(err)

	// How to really test fsync ?
	errNo := syscall.Fsync(f.Fd())
	if errNo != 0 {
		t.Errorf("fsync returned %v", errNo)
	}
	f.Close()
}

func TestLargeRead(t *testing.T) {
	me := NewTestCase(t)
	defer me.Cleanup()

	t.Log("Testing large read.")
	name := filepath.Join(me.orig, "large")
	f, err := os.OpenFile(name, os.O_WRONLY|os.O_CREATE, 0777)
	CheckSuccess(err)

	b := bytes.NewBuffer(nil)

	for i := 0; i < 20*1024; i++ {
		b.WriteString("bla")
	}
	b.WriteString("something extra to not be round")

	slice := b.Bytes()
	n, err := f.Write(slice)
	CheckSuccess(err)

	err = f.Close()
	CheckSuccess(err)

	// Read in one go.
	g, err := os.Open(filepath.Join(me.mnt, "large"))
	CheckSuccess(err)
	readSlice := make([]byte, len(slice))
	m, err := g.Read(readSlice)
	if m != n {
		t.Errorf("read mismatch %v %v", m, n)
	}
	for i, v := range readSlice {
		if slice[i] != v {
			t.Errorf("char mismatch %v %v %v", i, slice[i], v)
			break
		}
	}

	CheckSuccess(err)
	g.Close()

	// Read in chunks
	g, err = os.Open(filepath.Join(me.mnt, "large"))
	CheckSuccess(err)
	defer g.Close()
	readSlice = make([]byte, 4096)
	total := 0
	for {
		m, err := g.Read(readSlice)
		if m == 0 && err == os.EOF {
			break
		}
		CheckSuccess(err)
		total += m
	}
	if total != len(slice) {
		t.Errorf("slice error %d", total)
	}
}

func randomLengthString(length int) string {
	r := rand.Intn(length)
	j := 0

	b := make([]byte, r)
	for i := 0; i < r; i++ {
		j = (j + 1) % 10
		b[i] = byte(j) + byte('0')
	}
	return string(b)
}

func TestLargeDirRead(t *testing.T) {
	me := NewTestCase(t)
	defer me.Cleanup()

	t.Log("Testing large readdir.")
	created := 100

	names := make([]string, created)

	subdir := filepath.Join(me.orig, "readdirSubdir")
	os.Mkdir(subdir, 0700)
	longname := "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"

	nameSet := make(map[string]bool)
	for i := 0; i < created; i++ {
		// Should vary file name length.
		base := fmt.Sprintf("file%d%s", i,
			randomLengthString(len(longname)))
		name := filepath.Join(subdir, base)

		nameSet[base] = true

		f, err := os.OpenFile(name, os.O_WRONLY|os.O_CREATE, 0777)
		CheckSuccess(err)
		f.WriteString("bla")
		f.Close()

		names[i] = name
	}

	dir, err := os.Open(filepath.Join(me.mnt, "readdirSubdir"))
	CheckSuccess(err)
	defer dir.Close()

	// Chunked read.
	total := 0
	readSet := make(map[string]bool)
	for {
		namesRead, err := dir.Readdirnames(200)
		if len(namesRead) == 0 || err == os.EOF {
			break
		}
		CheckSuccess(err)
		for _, v := range namesRead {
			readSet[v] = true
		}
		total += len(namesRead)
	}

	if total != created {
		t.Errorf("readdir mismatch got %v wanted %v", total, created)
	}
	for k, _ := range nameSet {
		_, ok := readSet[k]
		if !ok {
			t.Errorf("Name %v not found in output", k)
		}
	}
}

func TestRootDir(t *testing.T) {
	ts := NewTestCase(t)
	defer ts.Cleanup()

	d, err := os.Open(ts.mnt)
	CheckSuccess(err)
	_, err = d.Readdirnames(-1)
	CheckSuccess(err)
	err = d.Close()
	CheckSuccess(err)
}

func TestIoctl(t *testing.T) {
	ts := NewTestCase(t)
	defer ts.Cleanup()

	f, err := os.OpenFile(filepath.Join(ts.mnt, "hello.txt"),
		os.O_WRONLY|os.O_CREATE, 0777)
	defer f.Close()
	CheckSuccess(err)
	v, e := ioctl(f.Fd(), 0x5401, 42)
	fmt.Println("ioctl", v, e)
}

func TestStatFs(t *testing.T) {
	ts := NewTestCase(t)
	defer ts.Cleanup()

	empty := syscall.Statfs_t{}
	s1 := empty
	err := syscall.Statfs(ts.orig, &s1)
	if err != 0 {
		t.Fatal("statfs orig", err)
	}

	s2 := syscall.Statfs_t{}
	err = syscall.Statfs(ts.mnt, &s2)

	s1.Type = 0
	s2.Type = 0

	s1.Fsid = empty.Fsid
	s2.Fsid = empty.Fsid

	s1.Spare = empty.Spare
	s2.Spare = empty.Spare

	if err != 0 {
		t.Fatal("statfs mnt", err)
	}

	if fmt.Sprintf("%v", s2) != fmt.Sprintf("%v", s1) {
		t.Error("Mismatch", s1, s2)
	}
}

func TestOriginalIsSymlink(t *testing.T) {
	tmpDir := MakeTempDir()
	defer os.RemoveAll(tmpDir)
	orig := tmpDir + "/orig"
	err := os.Mkdir(orig, 0755)
	CheckSuccess(err)
	link := tmpDir + "/link"
	mnt := tmpDir + "/mnt"
	err = os.Mkdir(mnt, 0755)
	CheckSuccess(err)
	err = os.Symlink("orig", link)
	CheckSuccess(err)

	fs := NewLoopbackFileSystem(link)
	state, _, err := MountFileSystem(mnt, fs, nil)
	CheckSuccess(err)
	defer state.Unmount()

	go state.Loop(false)

	_, err = os.Lstat(mnt)
	CheckSuccess(err)
}

func TestDoubleOpen(t *testing.T) {
        ts := NewTestCase(t)
        defer ts.Cleanup()

        err := ioutil.WriteFile(ts.orig +"/file", []byte("blabla"), 0644)
        CheckSuccess(err)
        
        roFile, err := os.Open(ts.mnt + "/file")
        CheckSuccess(err)
        defer roFile.Close()

        rwFile, err := os.OpenFile(ts.mnt + "/file", os.O_WRONLY | os.O_TRUNC, 0666)
        CheckSuccess(err)
        defer rwFile.Close()
}