package common

import (
	"bytes"
	"flag"
	"io"
	"os"
	"path"
	"syscall"
	"testing"

	"github.com/pingcap/log"
	"github.com/stretchr/testify/require"
	"github.com/tidbcloud/ffs/tests"
	"github.com/tidbcloud/ffs/util/logutil"
)

var (
	commonTest = flag.Bool("common", false, "")
)

func TestCommon(t *testing.T) {
	if !*commonTest {
		log.S().Info("skip common test")
		return
	}
	logutil.SetLevel("debug")
	cluster := tests.NewCluster(t)
	defer cluster.Stop()
	cluster.Start()
	log.S().Infof("client dirs %v", cluster.ClientDirs)
	testLink(cluster, t)
	testRename(cluster, t)
	testCase3(cluster, t)
}

func testLink(cluster *tests.Cluster, t *testing.T) {
	// Test on local fs
	{
		tmpDir, err := os.MkdirTemp("", "ffs-tmp")
		require.NoError(t, err)
		defer os.RemoveAll(tmpDir)
		runLink1(tmpDir, t)
		runLink2(tmpDir, t)
	}
	// Test on ffs
	{
		indexes := tests.GetRandomIndexes(len(cluster.Clients))
		testIndex := indexes[0]
		tmpDir := cluster.ClientDirs[testIndex]
		runLink1(tmpDir, t)
		runLink2(tmpDir, t)
	}
}

func runLink1(dir string, t *testing.T) {
	old := path.Join(dir, "tmp-file-old")
	new := path.Join(dir, "tmp-file-new")
	content := []byte("Test Link")
	log.S().Infof("Prepare file %s %s", old, content)
	tests.PrepareFile(old, content, t)
	log.S().Infof("Link  %s %s", old, new)
	err := os.Link(old, new)
	require.NoError(t, err)
	oldStat, err := os.Stat(old)
	require.NoError(t, err)
	newStat, err := os.Stat(new)
	require.NoError(t, err)
	oldSize := oldStat.Size()
	newSize := newStat.Size()
	require.Truef(t, oldSize == newSize, "size old %v new %v", oldSize, newSize)
	oldStatT, oldOk := oldStat.Sys().(*syscall.Stat_t)
	newStatT, newOk := newStat.Sys().(*syscall.Stat_t)
	require.Truef(t, oldOk && newOk, "Sys old %v new %v", oldOk, newOk)
	require.Truef(t, oldStatT.Ino == newStatT.Ino, "Stat Ino old %v new %v", oldStatT, newStatT)
	require.Truef(t, oldStatT.Nlink == newStatT.Nlink, "Stat Nlink old %v new %v", oldStatT, newStatT)
	require.Truef(t, oldStatT.Mode == newStatT.Mode, "Stat Mode old %v new %v", oldStatT, newStatT)
	log.S().Infof("Assert file  %s %s", old, content)
	tests.AssertFile(old, content, t)
	log.S().Infof("Assert file  %s %s", new, content)
	tests.AssertFile(new, content, t)
	log.S().Infof("Remove file  %s", old)
	err = os.Remove(old)
	require.NoError(t, err)
	log.S().Infof("Stat file  %s", old)
	_, err = os.Stat(old)
	require.True(t, os.IsNotExist(err))
	log.S().Infof("Assert file  %s %s", new, content)
	tests.AssertFile(new, content, t)
	log.S().Infof("Remove file  %s", new)
	err = os.Remove(new)
	require.NoError(t, err)

	log.S().Infof("Stat file  %s", new)
	_, err = os.Stat(new)
	require.True(t, os.IsNotExist(err))
}

// Hard link file A to file B, open file B, and then delete file A. This does not affect file B
func runLink2(dir string, t *testing.T) {
	old := path.Join(dir, "tmp-file-old")
	new := path.Join(dir, "tmp-file-new")
	content := []byte("Test Link")
	log.S().Infof("Prepare file %s %s", old, content)
	tests.PrepareFile(old, content, t)
	log.S().Infof("Link  %s %s", old, new)
	err := os.Link(old, new)
	require.NoError(t, err)
	oldStat, err := os.Stat(old)
	require.NoError(t, err)
	newStat, err := os.Stat(new)
	require.NoError(t, err)
	oldSize := oldStat.Size()
	newSize := newStat.Size()
	require.Truef(t, oldSize == newSize, "size old %v new %v", oldSize, newSize)
	oldStatT, oldOk := oldStat.Sys().(*syscall.Stat_t)
	newStatT, newOk := newStat.Sys().(*syscall.Stat_t)
	require.Truef(t, oldOk && newOk, "Sys old %v new %v", oldOk, newOk)
	require.Truef(t, oldStatT.Ino == newStatT.Ino, "Stat Ino old %v new %v", oldStatT, newStatT)
	require.Truef(t, oldStatT.Nlink == newStatT.Nlink, "Stat Nlink old %v new %v", oldStatT, newStatT)
	require.Truef(t, oldStatT.Mode == newStatT.Mode, "Stat Mode old %v new %v", oldStatT, newStatT)
	log.S().Infof("OpenFile file %s %s", new, content)
	f, err := os.OpenFile(new, os.O_RDWR, os.ModePerm)
	require.NoError(t, err)
	log.S().Infof("Remove file  %s", old)
	err = os.Remove(old)
	require.NoError(t, err)
	buf := make([]byte, len(content)*2)
	n, err := f.ReadAt(buf, 0)
	require.Truef(t, err == io.EOF, "Read err %v", err)
	require.Truef(t, n == len(content), "failed to read %d", n)
	require.Truef(t, bytes.Equal(content, buf[:n]), "file %s content [%s] read [%s]", f.Name(), content, buf[:n])
	err = f.Close()
	require.NoError(t, err)
	log.S().Infof("Stat file  %s", old)
	_, err = os.Stat(old)
	require.True(t, os.IsNotExist(err))
	log.S().Infof("Stat file  %s", new)
	_, err = os.Stat(new)
	require.True(t, !os.IsNotExist(err))
	log.S().Infof("Remove file  %s", new)
	err = os.Remove(new)
	require.NoError(t, err)
	log.S().Infof("Stat file  %s", new)
	_, err = os.Stat(new)
	require.True(t, os.IsNotExist(err))
}

func testRename(cluster *tests.Cluster, t *testing.T) {
	{
		tmpDir, err := os.MkdirTemp("", "ffs-tmp")
		require.NoError(t, err)
		defer os.RemoveAll(tmpDir)
		runRename1(tmpDir, t)
		runRename2(tmpDir, t)
	}
	{
		indexes := tests.GetRandomIndexes(len(cluster.Clients))
		testIndex := indexes[0]
		tmpDir := cluster.ClientDirs[testIndex]
		runRename1(tmpDir, t)
		runRename2(tmpDir, t)
	}
}

func runRename1(dir string, t *testing.T) {
	old := path.Join(dir, "tmp-file-old")
	new := path.Join(dir, "tmp-file-new")
	content := []byte("Test Rename")
	log.S().Infof("Prepare file %s %s", old, content)
	tests.PrepareFile(old, content, t)
	log.S().Infof("Rename file %s %s", old, new)
	err := os.Rename(old, new)
	require.NoError(t, err)
	log.S().Infof("Stat file %s", old)
	_, err = os.Stat(old)
	require.True(t, os.IsNotExist(err))
	log.S().Infof("Assert file  %s %s", new, content)
	tests.AssertFile(new, content, t)
	log.S().Infof("Remove file  %s", new)
	err = os.Remove(new)
	require.NoError(t, err)
	log.S().Infof("Stat file %s", new)
	_, err = os.Stat(new)
	require.True(t, os.IsNotExist(err))
}

func runRename2(dir string, t *testing.T) {
	old := path.Join(dir, "tmp-file-old")
	new := path.Join(dir, "tmp-file-new")
	content := []byte("Test Rename")
	log.S().Infof("Create file %s %s", old, content)
	f, err := os.Create(old)
	require.NoError(t, err)
	log.S().Infof("Rename file %s %s", old, new)
	err = os.Rename(old, new)
	require.NoError(t, err)
	log.S().Infof("Stat file %s", old)
	_, err = os.Stat(old)
	require.True(t, os.IsNotExist(err))
	n, err := f.Write(content)
	require.NoError(t, err)
	require.Truef(t, n == len(content), "failed to write %d", n)
	log.S().Infof("Sync file %s", old)
	err = f.Sync()
	require.NoError(t, err)
	log.S().Infof("Close file %s", old)
	err = f.Close()
	require.NoError(t, err)
	log.S().Infof("Assert file  %s %s", new, content)
	tests.AssertFile(new, content, t)
	log.S().Infof("Remove file  %s", new)
	err = os.Remove(new)
	require.NoError(t, err)
	log.S().Infof("Stat file %s", new)
	_, err = os.Stat(new)
	require.True(t, os.IsNotExist(err))
	log.S().Infof("Stat file %s", old)
	_, err = os.Stat(old)
	require.True(t, os.IsNotExist(err))
}

func testCase3(cluster *tests.Cluster, t *testing.T) {
	indexes := tests.GetRandomIndexes(len(cluster.Clients))
	testIndex := indexes[0]
	tmp := path.Join(cluster.ClientDirs[testIndex], "tmp-file.sst.tmp")
	sst := path.Join(cluster.ClientDirs[testIndex], "tmp-file.sst")
	clone := path.Join(cluster.ClientDirs[testIndex], "tmp-file.sst.clone")
	new := path.Join(cluster.ClientDirs[testIndex], "tmp-file-new")
	content := []byte("Test Rename")
	log.S().Infof("Prepare file %s %s", tmp, content)
	tests.PrepareFile(tmp, content, t)
	log.S().Infof("Rename file %s %s", tmp, sst)
	err := os.Rename(tmp, sst)
	require.NoError(t, err)
	log.S().Infof("Stat file %s", tmp)
	_, err = os.Stat(tmp)
	require.True(t, os.IsNotExist(err))
	log.S().Infof("Link file %s %s", sst, clone)
	err = os.Link(sst, clone)
	require.NoError(t, err)
	log.S().Infof("Stat file %s", clone)
	_, err = os.Stat(clone)
	require.True(t, !os.IsNotExist(err))
	log.S().Infof("Link file %s %s", clone, new)
	err = os.Link(clone, new)
	require.NoError(t, err)
	log.S().Infof("Stat file %s", new)
	_, err = os.Stat(new)
	require.True(t, !os.IsNotExist(err))
	log.S().Infof("Remove file  %s", clone)
	err = os.Remove(clone)
	require.NoError(t, err)
	log.S().Infof("Stat file %s", clone)
	_, err = os.Stat(clone)
	require.True(t, os.IsNotExist(err))
	log.S().Infof("Remove file  %s", sst)
	err = os.Remove(sst)
	require.NoError(t, err)
	log.S().Infof("Stat file %s", sst)
	_, err = os.Stat(sst)
	require.True(t, os.IsNotExist(err))
	log.S().Infof("Assert file  %s %s", new, content)
	tests.AssertFile(new, content, t)
	log.S().Infof("Remove file  %s", new)
	err = os.Remove(new)
	require.NoError(t, err)
	log.S().Infof("Stat file %s", new)
	_, err = os.Stat(new)
	require.True(t, os.IsNotExist(err))
}
