package cephfs

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

	"github.com/ceph/go-ceph/rados"

	"github.com/stretchr/testify/assert"
	"github.com/stretchr/testify/require"
)

var (
	testMdsName = "Z"
)

func init() {
	mdsName := os.Getenv("GO_CEPH_TEST_MDS_NAME")
	if mdsName != "" {
		testMdsName = mdsName
	}
}

func TestCreateMount(t *testing.T) {
	mount, err := CreateMount()
	assert.NoError(t, err)
	assert.NotNil(t, mount)
	assert.NoError(t, mount.Release())
}

func fsConnect(t require.TestingT) *MountInfo {
	mount, err := CreateMount()
	require.NoError(t, err)
	require.NotNil(t, mount)

	err = mount.ReadDefaultConfigFile()
	require.NoError(t, err)

	timeout := time.After(time.Second * 5)
	ch := make(chan error)
	go func(mount *MountInfo) {
		ch <- mount.Mount()
	}(mount)
	select {
	case err = <-ch:
	case <-timeout:
		err = fmt.Errorf("timed out waiting for connect")
	}
	require.NoError(t, err)
	return mount
}

func fsDisconnect(t assert.TestingT, mount *MountInfo) {
	assert.NoError(t, mount.Unmount())
	assert.NoError(t, mount.Release())
}

func TestMountRoot(t *testing.T) {
	mount := fsConnect(t)
	fsDisconnect(t, mount)
}

func TestSyncFs(t *testing.T) {
	mount := fsConnect(t)
	defer fsDisconnect(t, mount)

	err := mount.SyncFs()
	assert.NoError(t, err)
}

func TestUnmountMount(t *testing.T) {
	t.Run("neverMounted", func(t *testing.T) {
		mount, err := CreateMount()
		require.NoError(t, err)
		require.NotNil(t, mount)
		assert.False(t, mount.IsMounted())
		assert.NoError(t, mount.Release())
	})
	t.Run("mountUnmount", func(t *testing.T) {
		mount := fsConnect(t)
		defer func() { assert.NoError(t, mount.Release()) }()
		assert.True(t, mount.IsMounted())

		err := mount.Unmount()
		assert.NoError(t, err)
		assert.False(t, mount.IsMounted())
	})
}

func TestReleaseMount(t *testing.T) {
	mount, err := CreateMount()
	assert.NoError(t, err)
	require.NotNil(t, mount)

	assert.NoError(t, mount.Release())
	// call release again to ensure idempotency of the func
	assert.NoError(t, mount.Release())
}

func radosConnect(t *testing.T) *rados.Conn {
	conn, err := rados.NewConn()
	require.NoError(t, err)
	err = conn.ReadDefaultConfigFile()
	require.NoError(t, err)

	timeout := time.After(time.Second * 5)
	ch := make(chan error)
	go func(conn *rados.Conn) {
		ch <- conn.Connect()
	}(conn)
	select {
	case err = <-ch:
	case <-timeout:
		err = fmt.Errorf("timed out waiting for connect")
	}
	require.NoError(t, err)
	return conn
}

func TestCreateFromRados(t *testing.T) {
	conn := radosConnect(t)
	mount, err := CreateFromRados(conn)
	assert.NoError(t, err)
	assert.NotNil(t, mount)
}

func TestCreateMountWithId(t *testing.T) {
	mount, err := CreateMountWithId("bobolink")
	assert.NoError(t, err)
	assert.NotNil(t, mount)
	defer func() { assert.NoError(t, mount.Release()) }()

	err = mount.ReadDefaultConfigFile()
	assert.NoError(t, err)

	err = mount.Mount()
	assert.NoError(t, err)
	defer func() { assert.NoError(t, mount.Unmount()) }()

	// verify the custom entity_id is visible in the 'session ls' output
	// of mds.
	cmd := []byte(`{"prefix": "session ls"}`)
	buf, info, err := mount.MdsCommand(
		testMdsName,
		[][]byte{cmd})
	assert.NoError(t, err)
	assert.NotEqual(t, "", string(buf))
	assert.Equal(t, "", string(info))
	assert.Contains(t, string(buf), `"bobolink"`)
}

func TestMountWithRoot(t *testing.T) {
	bMount := fsConnect(t)
	defer fsDisconnect(t, bMount)

	dir1 := "/test-mount-with-root"
	err := bMount.MakeDir(dir1, 0755)
	assert.NoError(t, err)
	defer bMount.RemoveDir(dir1)

	sub1 := "/i.was.here"
	dir2 := dir1 + sub1
	err = bMount.MakeDir(dir2, 0755)
	assert.NoError(t, err)
	defer bMount.RemoveDir(dir2)

	t.Run("withRoot", func(t *testing.T) {
		mount, err := CreateMount()
		require.NoError(t, err)
		require.NotNil(t, mount)
		defer func() {
			assert.NoError(t, mount.Release())
		}()

		err = mount.ReadDefaultConfigFile()
		require.NoError(t, err)

		err = mount.MountWithRoot(dir1)
		assert.NoError(t, err)
		defer func() {
			assert.NoError(t, mount.Unmount())
		}()

		err = mount.ChangeDir(sub1)
		assert.NoError(t, err)
	})
	t.Run("badRoot", func(t *testing.T) {
		mount, err := CreateMount()
		require.NoError(t, err)
		require.NotNil(t, mount)
		defer func() {
			assert.NoError(t, mount.Release())
		}()

		err = mount.ReadDefaultConfigFile()
		require.NoError(t, err)

		err = mount.MountWithRoot("/i-yam-what-i-yam")
		assert.Error(t, err)
	})
}

func TestGetSetConfigOption(t *testing.T) {
	// we don't need an active connection for this, just the handle
	mount, err := CreateMount()
	require.NoError(t, err)
	require.NotNil(t, mount)
	defer func() { assert.NoError(t, mount.Release()) }()

	err = mount.SetConfigOption("__dne__", "value")
	assert.Error(t, err)
	_, err = mount.GetConfigOption("__dne__")
	assert.Error(t, err)

	origVal, err := mount.GetConfigOption("log_file")
	assert.NoError(t, err)

	err = mount.SetConfigOption("log_file", "/dev/null")
	assert.NoError(t, err)
	currVal, err := mount.GetConfigOption("log_file")
	assert.NoError(t, err)
	assert.Equal(t, "/dev/null", currVal)

	err = mount.SetConfigOption("log_file", origVal)
	assert.NoError(t, err)
	currVal, err = mount.GetConfigOption("log_file")
	assert.NoError(t, err)
	assert.Equal(t, origVal, currVal)
}

func TestParseConfigArgv(t *testing.T) {
	mount, err := CreateMount()
	require.NoError(t, err)
	require.NotNil(t, mount)
	defer func() { assert.NoError(t, mount.Release()) }()

	origVal, err := mount.GetConfigOption("log_file")
	assert.NoError(t, err)

	err = mount.ParseConfigArgv(
		[]string{"cephfs.test", "--log_file", "/dev/null"})
	assert.NoError(t, err)

	currVal, err := mount.GetConfigOption("log_file")
	assert.NoError(t, err)
	assert.Equal(t, "/dev/null", currVal)
	assert.NotEqual(t, "/dev/null", origVal)

	// ensure that an empty slice triggers an error (not a crash)
	err = mount.ParseConfigArgv([]string{})
	assert.Error(t, err)

	// ensure we get an error for an invalid mount value
	badMount := &MountInfo{}
	err = badMount.ParseConfigArgv(
		[]string{"cephfs.test", "--log_file", "/dev/null"})
	assert.Error(t, err)
}

func TestParseDefaultConfigEnv(t *testing.T) {
	mount, err := CreateMount()
	require.NoError(t, err)
	require.NotNil(t, mount)
	defer func() { assert.NoError(t, mount.Release()) }()

	origVal, err := mount.GetConfigOption("log_file")
	assert.NoError(t, err)

	err = os.Setenv("CEPH_ARGS", "--log_file /dev/null")
	assert.NoError(t, err)
	err = mount.ParseDefaultConfigEnv()
	assert.NoError(t, err)

	currVal, err := mount.GetConfigOption("log_file")
	assert.NoError(t, err)
	assert.Equal(t, "/dev/null", currVal)
	assert.NotEqual(t, "/dev/null", origVal)
}

func TestValidate(t *testing.T) {
	mount, err := CreateMount()
	assert.NoError(t, err)
	assert.NotNil(t, mount)
	defer assert.NoError(t, mount.Release())

	t.Run("mountCurrentDir", func(t *testing.T) {
		path := mount.CurrentDir()
		assert.Equal(t, path, "")
	})

	t.Run("mountChangeDir", func(t *testing.T) {
		err := mount.ChangeDir("someDir")
		assert.Error(t, err)
		assert.Equal(t, err, ErrNotConnected)
	})

	t.Run("mountMakeDir", func(t *testing.T) {
		err := mount.MakeDir("someName", 0444)
		assert.Error(t, err)
		assert.Equal(t, err, ErrNotConnected)
	})

	t.Run("mountRemoveDir", func(t *testing.T) {
		err := mount.RemoveDir("someDir")
		assert.Error(t, err)
		assert.Equal(t, err, ErrNotConnected)
	})

	t.Run("mountLink", func(t *testing.T) {
		err := mount.Link("/", "/")
		assert.Error(t, err)
		assert.Equal(t, err, ErrNotConnected)
	})

	t.Run("mountUnlink", func(t *testing.T) {
		err := mount.Unlink("someFile")
		assert.Error(t, err)
		assert.Equal(t, err, ErrNotConnected)
	})

	t.Run("mountSymlink", func(t *testing.T) {
		err := mount.Symlink("/", "/")
		assert.Error(t, err)
		assert.Equal(t, err, ErrNotConnected)
	})

	t.Run("mountReadlink", func(t *testing.T) {
		_, err := mount.Readlink("somePath")
		assert.Error(t, err)
		assert.Equal(t, err, ErrNotConnected)
	})
}

func TestReadConfigFile(t *testing.T) {
	file, err := ioutil.TempFile("/tmp", "cephfs.conf")
	require.NoError(t, err)
	defer func() {
		assert.NoError(t, file.Close())
		assert.NoError(t, os.Remove(file.Name()))
	}()
	_, err = io.WriteString(
		file, "[global]\nfsid = 04862775-14d5-46e0-a015-000000000000\n")
	require.NoError(t, err)

	mount, err := CreateMount()
	require.NoError(t, err)
	require.NotNil(t, mount)

	err = mount.ReadConfigFile(file.Name())
	require.NoError(t, err)

	v, err := mount.GetConfigOption("fsid")
	assert.NoError(t, err)
	assert.Equal(t, "04862775-14d5-46e0-a015-000000000000", v)
}

func TestVersion(t *testing.T) {
	var major, minor, patch = Version()
	assert.False(t, major < 0 || major > 1000, "invalid major")
	assert.False(t, minor < 0 || minor > 1000, "invalid minor")
	assert.False(t, patch < 0 || patch > 1000, "invalid patch")
}
