package mega

import (
	"crypto/md5"
	"crypto/rand"
	"fmt"
	"io/ioutil"
	"os"
	"path"
	"sync"
	"testing"
	"time"
)

var USER string = os.Getenv("MEGA_USER")
var PASSWORD string = os.Getenv("MEGA_PASSWD")

// retry runs fn until it succeeds, using what to log and retrying on
// EAGAIN.  It uses exponential backoff
func retry(t *testing.T, what string, fn func() error) {
	const maxTries = 10
	var err error
	sleep := 100 * time.Millisecond
	for i := 1; i <= maxTries; i++ {
		err = fn()
		if err == nil {
			return
		}
		if err != EAGAIN {
			break
		}
		t.Logf("%s failed %d/%d - retrying after %v sleep", what, i, maxTries, sleep)
		time.Sleep(sleep)
		sleep *= 2
	}
	t.Fatalf("%s failed: %v", what, err)
}

func skipIfNoCredentials(t *testing.T) {
	if USER == "" || PASSWORD == "" {
		t.Skip("MEGA_USER and MEGA_PASSWD not set - skipping integration tests")
	}
}

func initSession(t *testing.T) *Mega {
	skipIfNoCredentials(t)
	m := New()
	// m.SetDebugger(log.Printf)
	retry(t, "Login", func() error {
		return m.Login(USER, PASSWORD)
	})
	return m
}

// createFile creates a temporary file of a given size along with its MD5SUM
func createFile(t *testing.T, size int64) (string, string) {
	b := make([]byte, size)
	_, err := rand.Read(b)
	if err != nil {
		t.Fatalf("Error reading rand: %v", err)
	}
	file, err := ioutil.TempFile("/tmp/", "gomega-")
	if err != nil {
		t.Fatalf("Error creating temp file: %v", err)
	}
	_, err = file.Write(b)
	if err != nil {
		t.Fatalf("Error writing temp file: %v", err)
	}
	h := md5.New()
	_, err = h.Write(b)
	if err != nil {
		t.Fatalf("Error on Write while writing temp file: %v", err)
	}
	return file.Name(), fmt.Sprintf("%x", h.Sum(nil))
}

// uploadFile uploads a temporary file of a given size returning the
// node, name and its MD5SUM
func uploadFile(t *testing.T, session *Mega, size int64, parent *Node) (node *Node, name string, md5sum string) {
	name, md5sum = createFile(t, size)
	defer func() {
		_ = os.Remove(name)
	}()
	var err error
	retry(t, fmt.Sprintf("Upload %q", name), func() error {
		node, err = session.UploadFile(name, parent, "", nil)
		return err
	})
	if node == nil {
		t.Fatalf("Failed to obtain node after upload for %q", name)
	}
	return node, name, md5sum
}

// createDir creates a directory under parent
func createDir(t *testing.T, session *Mega, name string, parent *Node) (node *Node) {
	var err error
	retry(t, fmt.Sprintf("Create directory %q", name), func() error {
		node, err = session.CreateDir(name, parent)
		return err
	})
	return node
}

func fileMD5(t *testing.T, name string) string {
	file, err := os.Open(name)
	if err != nil {
		t.Fatalf("Failed to open %q: %v", name, err)
	}
	b, err := ioutil.ReadAll(file)
	if err != nil {
		t.Fatalf("Failed to read all %q: %v", name, err)
	}
	h := md5.New()
	_, err = h.Write(b)
	if err != nil {
		t.Fatalf("Error on hash in fileMD5: %v", err)
	}
	return fmt.Sprintf("%x", h.Sum(nil))
}

func TestLogin(t *testing.T) {
	skipIfNoCredentials(t)

	m := New()
	retry(t, "Login", func() error {
		return m.Login(USER, PASSWORD)
	})
}

func TestGetUser(t *testing.T) {
	session := initSession(t)
	_, err := session.GetUser()
	if err != nil {
		t.Fatal("GetUser failed", err)
	}
}

func TestUploadDownload(t *testing.T) {
	session := initSession(t)
	for i := range []int{0, 1} {
		if i == 0 {
			t.Log("HTTP Test")
			session.SetHTTPS(false)
		} else {
			t.Log("HTTPS Test")
			session.SetHTTPS(true)
		}

		node, name, h1 := uploadFile(t, session, 314573, session.FS.root)

		session.FS.mutex.Lock()
		phash := session.FS.root.hash
		n := session.FS.lookup[node.hash]
		if n.parent.hash != phash {
			t.Error("Parent of uploaded file mismatch")
		}
		session.FS.mutex.Unlock()

		err := session.DownloadFile(node, name, nil)
		if err != nil {
			t.Fatal("Download failed", err)
		}

		h2 := fileMD5(t, name)
		err = os.Remove(name)
		if err != nil {
			t.Error("Failed to remove file", err)
		}

		if h1 != h2 {
			t.Error("MD5 mismatch for downloaded file")
		}
	}
	session.SetHTTPS(false)
}

func TestMove(t *testing.T) {
	session := initSession(t)
	node, _, _ := uploadFile(t, session, 31, session.FS.root)

	hash := node.hash
	phash := session.FS.trash.hash
	err := session.Move(node, session.FS.trash)
	if err != nil {
		t.Fatal("Move failed", err)
	}

	session.FS.mutex.Lock()
	n := session.FS.lookup[hash]
	if n.parent.hash != phash {
		t.Error("Move happened to wrong parent", phash, n.parent.hash)
	}
	session.FS.mutex.Unlock()
}

func TestRename(t *testing.T) {
	session := initSession(t)
	node, _, _ := uploadFile(t, session, 31, session.FS.root)

	err := session.Rename(node, "newname.txt")
	if err != nil {
		t.Fatal("Rename failed", err)
	}

	session.FS.mutex.Lock()
	newname := session.FS.lookup[node.hash].name
	if newname != "newname.txt" {
		t.Error("Renamed to wrong name", newname)
	}
	session.FS.mutex.Unlock()
}

func TestDelete(t *testing.T) {
	session := initSession(t)
	node, _, _ := uploadFile(t, session, 31, session.FS.root)

	retry(t, "Soft delete", func() error {
		return session.Delete(node, false)
	})

	session.FS.mutex.Lock()
	node = session.FS.lookup[node.hash]
	if node.parent != session.FS.trash {
		t.Error("Expects file to be moved to trash")
	}
	session.FS.mutex.Unlock()

	retry(t, "Hard delete", func() error {
		return session.Delete(node, true)
	})

	time.Sleep(1 * time.Second) // wait for the event

	session.FS.mutex.Lock()
	if _, ok := session.FS.lookup[node.hash]; ok {
		t.Error("Expects file to be dissapeared")
	}
	session.FS.mutex.Unlock()
}

func TestCreateDir(t *testing.T) {
	session := initSession(t)
	node := createDir(t, session, "testdir1", session.FS.root)
	node2 := createDir(t, session, "testdir2", node)

	session.FS.mutex.Lock()
	nnode2 := session.FS.lookup[node2.hash]
	if nnode2.parent.hash != node.hash {
		t.Error("Wrong directory parent")
	}
	session.FS.mutex.Unlock()
}

func TestConfig(t *testing.T) {
	skipIfNoCredentials(t)

	m := New()
	m.SetAPIUrl("http://invalid.domain")
	err := m.Login(USER, PASSWORD)
	if err == nil {
		t.Error("API Url: Expected failure")
	}

	err = m.SetDownloadWorkers(100)
	if err != EWORKER_LIMIT_EXCEEDED {
		t.Error("Download: Expected EWORKER_LIMIT_EXCEEDED error")
	}

	err = m.SetUploadWorkers(100)
	if err != EWORKER_LIMIT_EXCEEDED {
		t.Error("Upload: Expected EWORKER_LIMIT_EXCEEDED error")
	}

	// TODO: Add timeout test cases

}

func TestPathLookup(t *testing.T) {
	session := initSession(t)

	rs, err := randString(5)
	if err != nil {
		t.Fatalf("failed to make random string: %v", err)
	}
	node1 := createDir(t, session, "dir-1-"+rs, session.FS.root)
	node21 := createDir(t, session, "dir-2-1-"+rs, node1)
	node22 := createDir(t, session, "dir-2-2-"+rs, node1)
	node31 := createDir(t, session, "dir-3-1-"+rs, node21)
	node32 := createDir(t, session, "dir-3-2-"+rs, node22)
	_ = node32

	_, name1, _ := uploadFile(t, session, 31, node31)
	_, _, _ = uploadFile(t, session, 31, node31)
	_, name3, _ := uploadFile(t, session, 31, node22)

	testpaths := [][]string{
		{"dir-1-" + rs, "dir-2-2-" + rs, path.Base(name3)},
		{"dir-1-" + rs, "dir-2-1-" + rs, "dir-3-1-" + rs},
		{"dir-1-" + rs, "dir-2-1-" + rs, "dir-3-1-" + rs, path.Base(name1)},
		{"dir-1-" + rs, "dir-2-1-" + rs, "none"},
	}

	results := []error{nil, nil, nil, ENOENT}

	for i, tst := range testpaths {
		ns, e := session.FS.PathLookup(session.FS.root, tst)
		switch {
		case e != results[i]:
			t.Errorf("Test %d failed: wrong result", i)
		default:
			if results[i] == nil && len(tst) != len(ns) {
				t.Errorf("Test %d failed: result array len (%d) mismatch", i, len(ns))

			}

			arr := []string{}
			for n := range ns {
				if tst[n] != ns[n].name {
					t.Errorf("Test %d failed: result node mismatches (%v) and (%v)", i, tst, arr)
					break
				}
				arr = append(arr, tst[n])
			}
		}
	}
}

func TestEventNotify(t *testing.T) {
	session1 := initSession(t)
	session2 := initSession(t)

	node, _, _ := uploadFile(t, session1, 31, session1.FS.root)

	for i := 0; i < 60; i++ {
		time.Sleep(time.Second * 1)
		node = session2.FS.HashLookup(node.GetHash())
		if node != nil {
			break
		}
	}

	if node == nil {
		t.Fatal("Expects file to found in second client's FS")
	}

	retry(t, "Delete", func() error {
		return session2.Delete(node, true)
	})

	time.Sleep(time.Second * 5)
	node = session1.FS.HashLookup(node.hash)
	if node != nil {
		t.Fatal("Expects file to not-found in first client's FS")
	}
}

func TestExportLink(t *testing.T) {
	session := initSession(t)
	node, _, _ := uploadFile(t, session, 31, session.FS.root)

	// Don't include decryption key
	retry(t, "Failed to export link (key not included)", func() error {
		_, err := session.Link(node, false)
		return err
	})

	// Do include decryption key
	retry(t, "Failed to export link (key included)", func() error {
		_, err := session.Link(node, true)
		return err
	})
}

func TestWaitEvents(t *testing.T) {
	m := &Mega{}
	m.SetLogger(t.Logf)
	m.SetDebugger(t.Logf)
	var wg sync.WaitGroup
	// in the background fire the event timer after 100mS
	wg.Add(1)
	go func() {
		time.Sleep(100 * time.Millisecond)
		m.waitEventsFire()
		wg.Done()
	}()
	wait := func(d time.Duration, pb *bool) {
		e := m.WaitEventsStart()
		*pb = m.WaitEvents(e, d)
		wg.Done()
	}
	// wait for each event in a separate goroutine
	var b1, b2, b3 bool
	wg.Add(3)
	go wait(10*time.Second, &b1)
	go wait(2*time.Second, &b2)
	go wait(1*time.Millisecond, &b3)
	wg.Wait()
	if b1 != false {
		t.Errorf("Unexpected timeout for b1")
	}
	if b2 != false {
		t.Errorf("Unexpected timeout for b2")
	}
	if b3 != true {
		t.Errorf("Unexpected event for b3")
	}
	if m.waitEvents != nil {
		t.Errorf("Expecting waitEvents to be empty")
	}
	// Check nothing happens if we fire the event with no listeners
	m.waitEventsFire()
}
