package zfs_test

import (
	"fmt"
	"io/ioutil"
	"os"
	"testing"
	"time"

	zfs "github.com/bicomsystems/go-libzfs"
)

/* ------------------------------------------------------------------------- */
// HELPERS:

var TSTPoolName = "TESTPOOL"
var TSTPoolGUID string

func CreateTmpSparse(prefix string, size int64) (path string, err error) {
	sf, err := ioutil.TempFile("/tmp", prefix)
	if err != nil {
		return
	}
	defer sf.Close()
	if err = sf.Truncate(size); err != nil {
		return
	}
	path = sf.Name()
	return
}

var s1path, s2path, s3path string

// This will create sparse files in tmp directory,
// for purpose of creating test pool.
func createTestpoolVdisks() (err error) {
	if s1path, err = CreateTmpSparse("zfs_test_", 0x140000000); err != nil {
		return
	}
	if s2path, err = CreateTmpSparse("zfs_test_", 0x140000000); err != nil {
		// try cleanup
		os.Remove(s1path)
		return
	}
	if s3path, err = CreateTmpSparse("zfs_test_", 0x140000000); err != nil {
		// try cleanup
		os.Remove(s1path)
		os.Remove(s2path)
		return
	}
	return
}

func removeVDisk(path string) {
	if err := os.Remove(path); err != nil {
		println("Error: ", err.Error())
	}
}

// Cleanup sparse files used for tests
func cleanupVDisks() {
	// try cleanup
	removeVDisk(s1path)
	removeVDisk(s2path)
	removeVDisk(s3path)
}

/* ------------------------------------------------------------------------- */
// TESTS:

// Create 3 sparse file in /tmp directory each 5G size, and use them to create
// mirror TESTPOOL with one spare "disk"
func zpoolTestPoolCreate(t *testing.T) {
	println("TEST PoolCreate ... ")
	// first check if pool with same name already exist
	// we don't want conflict
	for {
		p, err := zfs.PoolOpen(TSTPoolName)
		if err != nil {
			break
		}
		p.Close()
		TSTPoolName += "0"
	}
	var err error

	if err = createTestpoolVdisks(); err != nil {
		t.Error(err)
		return
	}

	disks := [2]string{s1path, s2path}

	var vdev zfs.VDevTree
	var vdevs, mdevs, sdevs []zfs.VDevTree
	for _, d := range disks {
		mdevs = append(mdevs,
			zfs.VDevTree{Type: zfs.VDevTypeFile, Path: d})
	}
	sdevs = []zfs.VDevTree{
		{Type: zfs.VDevTypeFile, Path: s3path}}
	vdevs = []zfs.VDevTree{
		zfs.VDevTree{Type: zfs.VDevTypeMirror, Devices: mdevs},
	}
	vdev.Devices = vdevs
	vdev.Spares = sdevs

	props := make(map[zfs.Prop]string)
	fsprops := make(map[zfs.Prop]string)
	features := make(map[string]string)
	fsprops[zfs.DatasetPropMountpoint] = "none"
	features["async_destroy"] = zfs.FENABLED
	features["empty_bpobj"] = zfs.FENABLED
	features["lz4_compress"] = zfs.FENABLED

	pool, err := zfs.PoolCreate(TSTPoolName, vdev, features, props, fsprops)
	if err != nil {
		t.Error(err)
		// try cleanup
		os.Remove(s1path)
		os.Remove(s2path)
		os.Remove(s3path)
		return
	}
	defer pool.Close()

	pguid, _ := pool.GetProperty(zfs.PoolPropGUID)
	TSTPoolGUID = pguid.Value

	print("PASS\n\n")
}

// Open and list all pools and them state on the system
// Then list properties of last pool in the list
func zpoolTestPoolOpenAll(t *testing.T) {
	println("TEST PoolOpenAll() ... ")
	var pname string
	pools, err := zfs.PoolOpenAll()
	if err != nil {
		t.Error(err)
		return
	}
	println("\tThere is ", len(pools), " ZFS pools.")
	for _, p := range pools {
		pname, err = p.Name()
		if err != nil {
			t.Error(err)
			p.Close()
			return
		}
		pstate, err := p.State()
		if err != nil {
			t.Error(err)
			p.Close()
			return
		}
		println("\tPool: ", pname, " state: ", pstate)
		p.Close()
	}
	print("PASS\n\n")
}

func zpoolTestPoolDestroy(t *testing.T) {
	println("TEST POOL Destroy( ", TSTPoolName, " ) ... ")
	p, err := zfs.PoolOpen(TSTPoolName)
	if err != nil {
		t.Error(err)
		return
	}
	defer p.Close()
	if err = p.Destroy(TSTPoolName); err != nil {
		t.Error(err.Error())
		return
	}
	print("PASS\n\n")
}

func zpoolTestFailPoolOpen(t *testing.T) {
	println("TEST open of non existing pool ... ")
	pname := "fail to open this pool"
	p, err := zfs.PoolOpen(pname)
	if err != nil {
		print("PASS\n\n")
		return
	}
	t.Error("PoolOpen pass when it should fail")
	p.Close()
}

func zpoolTestExport(t *testing.T) {
	println("TEST POOL Export( ", TSTPoolName, " ) ... ")
	p, err := zfs.PoolOpen(TSTPoolName)
	if err != nil {
		t.Error(err)
		return
	}
	p.Export(false, "Test exporting pool")
	defer p.Close()
	print("PASS\n\n")
}

func zpoolTestExportForce(t *testing.T) {
	println("TEST POOL ExportForce( ", TSTPoolName, " ) ... ")
	p, err := zfs.PoolOpen(TSTPoolName)
	if err != nil {
		t.Error(err)
		return
	}
	p.ExportForce("Test force exporting pool")
	defer p.Close()
	print("PASS\n\n")
}

func zpoolTestImport(t *testing.T) {
	println("TEST POOL Import( ", TSTPoolName, " ) ... ")
	p, err := zfs.PoolImport(TSTPoolName, []string{"/tmp"})
	if err != nil {
		t.Error(err)
		return
	}
	defer p.Close()
	print("PASS\n\n")
}

func zpoolTestImportByGUID(t *testing.T) {
	println("TEST POOL ImportByGUID( ", TSTPoolGUID, " ) ... ")
	p, err := zfs.PoolImportByGUID(TSTPoolGUID, []string{"/tmp"})
	if err != nil {
		t.Error(err)
		return
	}
	defer p.Close()
	print("PASS\n\n")
}

func printVDevTree(vt zfs.VDevTree, pref string) {
	first := pref + vt.Name
	fmt.Printf("%-30s | %-10s | %-10s | %s\n", first, vt.Type,
		vt.Stat.State.String(), vt.Path)
	for _, v := range vt.Devices {
		printVDevTree(v, "  "+pref)
	}
	if len(vt.Spares) > 0 {
		fmt.Println("spares:")
		for _, v := range vt.Spares {
			printVDevTree(v, "  "+pref)
		}
	}

	if len(vt.L2Cache) > 0 {
		fmt.Println("l2cache:")
		for _, v := range vt.L2Cache {
			printVDevTree(v, "  "+pref)
		}
	}
}

func zpoolTestPoolImportSearch(t *testing.T) {
	println("TEST PoolImportSearch")
	pools, err := zfs.PoolImportSearch([]string{"/tmp"})
	if err != nil {
		t.Error(err.Error())
		return
	}
	for _, p := range pools {
		println()
		println("---------------------------------------------------------------")
		println("pool: ", p.Name)
		println("guid: ", p.GUID)
		println("state: ", p.State.String())
		fmt.Printf("%-30s | %-10s | %-10s | %s\n", "NAME", "TYPE", "STATE", "PATH")
		println("---------------------------------------------------------------")
		printVDevTree(p.VDevs, "")
	}
	print("PASS\n\n")
}

func zpoolTestPoolProp(t *testing.T) {
	println("TEST PoolProp on ", TSTPoolName, " ... ")
	if pool, err := zfs.PoolOpen(TSTPoolName); err == nil {
		defer pool.Close()
		// Turn on snapshot listing for pool
		pool.SetProperty(zfs.PoolPropListsnaps, "off")
		// Verify change is succesfull
		if pool.Properties[zfs.PoolPropListsnaps].Value != "off" {
			t.Error(fmt.Errorf("Update of pool property failed"))
			return
		}

		// Test fetching property
		propHealth, err := pool.GetProperty(zfs.PoolPropHealth)
		if err != nil {
			t.Error(err)
			return
		}
		println("Pool property health: ", propHealth.Value)

		propGUID, err := pool.GetProperty(zfs.PoolPropGUID)
		if err != nil {
			t.Error(err)
			return
		}
		println("Pool property GUID: ", propGUID.Value)

		// this test pool should not be bootable
		prop, err := pool.GetProperty(zfs.PoolPropBootfs)
		if err != nil {
			t.Error(err)
			return
		}
		if prop.Value != "-" {
			t.Errorf("Failed at bootable fs property evaluation")
			return
		}

		// fetch all properties
		if err = pool.ReloadProperties(); err != nil {
			t.Error(err)
			return
		}
	} else {
		t.Error(err)
		return
	}
	print("PASS\n\n")
}

func zpoolTestPoolStatusAndState(t *testing.T) {
	println("TEST pool Status/State ( ", TSTPoolName, " ) ... ")
	pool, err := zfs.PoolOpen(TSTPoolName)
	if err != nil {
		t.Error(err.Error())
		return
	}
	defer pool.Close()

	if _, err = pool.Status(); err != nil {
		t.Error(err.Error())
		return
	}

	var pstate zfs.PoolState
	if pstate, err = pool.State(); err != nil {
		t.Error(err.Error())
		return
	}
	println("POOL", TSTPoolName, "state:", zfs.PoolStateToName(pstate))

	print("PASS\n\n")
}

func zpoolTestPoolVDevTree(t *testing.T) {
	var vdevs zfs.VDevTree
	println("TEST pool VDevTree ( ", TSTPoolName, " ) ... ")
	pool, err := zfs.PoolOpen(TSTPoolName)
	if err != nil {
		t.Error(err.Error())
		return
	}
	defer pool.Close()
	vdevs, err = pool.VDevTree()
	if err != nil {
		t.Error(err.Error())
		return
	}
	fmt.Printf("%-30s | %-10s | %-10s | %s\n", "NAME", "TYPE", "STATE", "PATH")
	println("---------------------------------------------------------------")
	printVDevTree(vdevs, "")
	print("PASS\n\n")
}

func zpoolTestInitialization(t *testing.T) {
	println("TEST POOL Initialization ( ", TSTPoolName, " ) ... ")
	pool, err := zfs.PoolOpen(TSTPoolName)
	if err != nil {
		t.Error(err.Error())
		return
	}
	defer pool.Close()

	err = pool.Initialize()
	if err != nil {
		t.Error(err.Error())
		return
	}
	time.Sleep(1 * time.Second)
	err = pool.SuspendInitialization()
	if err != nil {
		t.Error(err.Error())
		return
	}
	err = pool.Initialize()
	if err != nil {
		t.Error(err.Error())
		return
	}
	time.Sleep(1 * time.Second)
	err = pool.CancelInitialization()
	if err != nil {
		t.Error(err.Error())
		return
	}
	print("PASS\n\n")
}

/* ------------------------------------------------------------------------- */
// EXAMPLES:

func ExamplePoolProp() {
	if pool, err := zfs.PoolOpen("SSD"); err == nil {
		print("Pool size is: ", pool.Properties[zfs.PoolPropSize].Value)
		// Turn on snapshot listing for pool
		pool.SetProperty(zfs.PoolPropListsnaps, "on")
		println("Changed property",
			zfs.PoolPropertyToName(zfs.PoolPropListsnaps), "to value:",
			pool.Properties[zfs.PoolPropListsnaps].Value)

		prop, err := pool.GetProperty(zfs.PoolPropHealth)
		if err != nil {
			panic(err)
		}
		println("Update and print out pool health:", prop.Value)
	} else {
		print("Error: ", err)
	}
}

// Open and list all pools on system with them properties
func ExamplePoolOpenAll() {
	// Lets open handles to all active pools on system
	pools, err := zfs.PoolOpenAll()
	if err != nil {
		println(err)
	}

	// Print each pool name and properties
	for _, p := range pools {
		// Print fancy header
		fmt.Printf("\n -----------------------------------------------------------\n")
		fmt.Printf("   POOL: %49s   \n", p.Properties[zfs.PoolPropName].Value)
		fmt.Printf("|-----------------------------------------------------------|\n")
		fmt.Printf("|  PROPERTY      |  VALUE                |  SOURCE          |\n")
		fmt.Printf("|-----------------------------------------------------------|\n")

		// Iterate pool properties and print name, value and source
		for key, prop := range p.Properties {
			pkey := zfs.Prop(key)
			if pkey == zfs.PoolPropName {
				continue // Skip name its already printed above
			}
			fmt.Printf("|%14s  | %20s  | %15s  |\n",
				zfs.PoolPropertyToName(pkey),
				prop.Value, prop.Source)
			println("")
		}
		println("")

		// Close pool handle and free memory, since it will not be used anymore
		p.Close()
	}
}

func ExamplePoolCreate() {
	disks := [2]string{"/dev/disk/by-id/ATA-123", "/dev/disk/by-id/ATA-456"}

	var vdev zfs.VDevTree
	var vdevs, mdevs, sdevs []zfs.VDevTree

	// build mirror devices specs
	for _, d := range disks {
		mdevs = append(mdevs,
			zfs.VDevTree{Type: zfs.VDevTypeDisk, Path: d})
	}

	// spare device specs
	sdevs = []zfs.VDevTree{
		{Type: zfs.VDevTypeDisk, Path: "/dev/disk/by-id/ATA-789"}}

	// pool specs
	vdevs = []zfs.VDevTree{
		zfs.VDevTree{Type: zfs.VDevTypeMirror, Devices: mdevs},
	}

	vdev.Devices = vdevs
	vdev.Spares = sdevs

	// pool properties
	props := make(map[zfs.Prop]string)
	// root dataset filesystem properties
	fsprops := make(map[zfs.Prop]string)
	// pool features
	features := make(map[string]string)

	// Turn off auto mounting by ZFS
	fsprops[zfs.DatasetPropMountpoint] = "none"

	// Enable some features
	features["async_destroy"] = "enabled"
	features["empty_bpobj"] = "enabled"
	features["lz4_compress"] = "enabled"

	// Based on specs formed above create test pool as 2 disk mirror and
	// one spare disk
	pool, err := zfs.PoolCreate("TESTPOOL", vdev, features, props, fsprops)
	if err != nil {
		println("Error: ", err.Error())
		return
	}
	defer pool.Close()
}

func ExamplePool_Destroy() {
	pname := "TESTPOOL"

	// Need handle to pool at first place
	p, err := zfs.PoolOpen(pname)
	if err != nil {
		println("Error: ", err.Error())
		return
	}

	// Make sure pool handle is free after we are done here
	defer p.Close()

	if err = p.Destroy("Example of pool destroy (TESTPOOL)"); err != nil {
		println("Error: ", err.Error())
		return
	}
}

func ExamplePoolImport() {
	p, err := zfs.PoolImport("TESTPOOL", []string{"/dev/disk/by-id"})
	if err != nil {
		panic(err)
	}
	p.Close()
}

func ExamplePool_Export() {
	p, err := zfs.PoolOpen("TESTPOOL")
	if err != nil {
		panic(err)
	}
	defer p.Close()
	if err = p.Export(false, "Example exporting pool"); err != nil {
		panic(err)
	}
}

func ExamplePool_ExportForce() {
	p, err := zfs.PoolOpen("TESTPOOL")
	if err != nil {
		panic(err)
	}
	defer p.Close()
	if err = p.ExportForce("Example exporting pool"); err != nil {
		panic(err)
	}
}

func ExamplePool_State() {
	p, err := zfs.PoolOpen("TESTPOOL")
	if err != nil {
		panic(err)
	}
	defer p.Close()
	pstate, err := p.State()
	if err != nil {
		panic(err)
	}
	println("POOL TESTPOOL state:", zfs.PoolStateToName(pstate))
}

// func TestPool_VDevTree(t *testing.T) {
// 	type fields struct {
// 		poolName string
// 	}
// 	tests := []struct {
// 		name    string
// 		fields  fields
// 		wantErr bool
// 	}{
// 		// TODO: Add test cases.
// 		{
// 			name:    "test1",
// 			fields:  fields{"TESTPOOL"},
// 			wantErr: false,
// 		},
// 	}
// 	for _, tt := range tests {
// 		t.Run(tt.name, func(t *testing.T) {
// 			pool, _ := zfs.PoolOpen(tt.fields.poolName)
// 			defer pool.Close()
// 			gotVdevs, err := pool.VDevTree()
// 			if (err != nil) != tt.wantErr {
// 				t.Errorf("Pool.VDevTree() error = %v, wantErr %v", err, tt.wantErr)
// 				return
// 			}
// 			jsonData, _ := json.MarshalIndent(gotVdevs, "", "\t")
// 			t.Logf("gotVdevs: %s", string(jsonData))
// 		})
// 	}
// }
