// Package fstests provides generic tests for testing the Fs and Object interfaces
//
// Run go generate to write the tests for the remotes
package fstests

//go:generate go run gen_tests.go

import (
	"bytes"
	"fmt"
	"io"
	"io/ioutil"
	"os"
	"path"
	"path/filepath"
	"sort"
	"strings"
	"testing"
	"time"

	"github.com/ncw/rclone/fs"
	"github.com/ncw/rclone/fs/config"
	"github.com/ncw/rclone/fs/fserrors"
	"github.com/ncw/rclone/fs/hash"
	"github.com/ncw/rclone/fs/object"
	"github.com/ncw/rclone/fs/operations"
	"github.com/ncw/rclone/fs/walk"
	"github.com/ncw/rclone/fstest"
	"github.com/pkg/errors"
	"github.com/stretchr/testify/assert"
	"github.com/stretchr/testify/require"
)

var (
	remote fs.Fs
	// RemoteName should be set to the name of the remote for testing
	RemoteName    = ""
	subRemoteName = ""
	subRemoteLeaf = ""
	// NilObject should be set to a nil Object from the Fs under test
	NilObject fs.Object
	// ExtraConfig is for adding config to a remote
	ExtraConfig = []ExtraConfigItem{}
	// SkipBadWindowsCharacters skips unusable characters for windows if set
	SkipBadWindowsCharacters = map[string]bool{}
	file1                    = fstest.Item{
		ModTime: fstest.Time("2001-02-03T04:05:06.499999999Z"),
		Path:    "file name.txt",
	}
	file1Contents = ""
	file2         = fstest.Item{
		ModTime: fstest.Time("2001-02-03T04:05:10.123123123Z"),
		Path:    `hello? sausage/êé/Hello, 世界/ " ' @ < > & ? + ≠/z.txt`,
		WinPath: `hello_ sausage/êé/Hello, 世界/ _ ' @ _ _ & _ + ≠/z.txt`,
	}
	file2Contents = ""
	isLocalRemote bool
)

// InternalTester is an optional interface for Fs which allows to execute internal tests
//
// This interface should be implemented in 'backend'_internal_test.go and not in 'backend'.go
type InternalTester interface {
	InternalTest(*testing.T)
}

// ExtraConfigItem describes a config item added on the fly while testing
type ExtraConfigItem struct{ Name, Key, Value string }

// Make the Fs we are testing with, initialising the global variables
// subRemoteName - name of the remote after the TestRemote:
// subRemoteLeaf - a subdirectory to use under that
// remote - the result of  fs.NewFs(TestRemote:subRemoteName)
func newFs(t *testing.T) {
	var err error
	subRemoteName, subRemoteLeaf, err = fstest.RandomRemoteName(RemoteName)
	require.NoError(t, err)
	remote, err = fs.NewFs(subRemoteName)
	if err == fs.ErrorNotFoundInConfigFile {
		t.Logf("Didn't find %q in config file - skipping tests", RemoteName)
		return
	}
	require.NoError(t, err, fmt.Sprintf("unexpected error: %v", err))
}

// TestInit tests basic intitialisation
func TestInit(t *testing.T) {
	var err error

	// Remove bad characters from Windows file name if set
	if SkipBadWindowsCharacters[RemoteName] {
		t.Logf("Removing bad windows characters from test file")
		file2.Path = winPath(file2.Path)
	}

	fstest.Initialise()

	// Set extra config if supplied
	for _, item := range ExtraConfig {
		config.FileSet(item.Name, item.Key, item.Value)
	}
	if *fstest.RemoteName != "" {
		RemoteName = *fstest.RemoteName
	}
	t.Logf("Using remote %q", RemoteName)
	if RemoteName == "" {
		RemoteName, err = fstest.LocalRemote()
		require.NoError(t, err)
		isLocalRemote = true
	}

	newFs(t)

	skipIfNotOk(t)

	err = remote.Mkdir("")
	require.NoError(t, err)
	fstest.CheckListing(t, remote, []fstest.Item{})
}

func skipIfNotOk(t *testing.T) {
	if remote == nil {
		t.Skipf("WARN: %q not configured", RemoteName)
	}
}

// Skip if remote is not ListR capable, otherwise set the useListR
// flag, returning a function to restore its value
func skipIfNotListR(t *testing.T) func() {
	skipIfNotOk(t)
	if remote.Features().ListR == nil {
		t.Skip("FS has no ListR interface")
	}
	previous := fs.Config.UseListR
	fs.Config.UseListR = true
	return func() {
		fs.Config.UseListR = previous
	}
}

// TestFsString tests the String method
func TestFsString(t *testing.T) {
	skipIfNotOk(t)
	str := remote.String()
	require.NotEqual(t, "", str)
}

// TestFsName tests the Name method
func TestFsName(t *testing.T) {
	skipIfNotOk(t)
	got := remote.Name()
	want := RemoteName
	if isLocalRemote {
		want = "local:"
	}
	require.Equal(t, want, got+":")
}

// TestFsRoot tests the Root method
func TestFsRoot(t *testing.T) {
	skipIfNotOk(t)
	name := remote.Name() + ":"
	root := remote.Root()
	if isLocalRemote {
		// only check last path element on local
		require.Equal(t, filepath.Base(subRemoteName), filepath.Base(root))
	} else {
		require.Equal(t, subRemoteName, name+root)
	}
}

// TestFsRmdirEmpty tests deleting an empty directory
func TestFsRmdirEmpty(t *testing.T) {
	skipIfNotOk(t)
	err := remote.Rmdir("")
	require.NoError(t, err)
}

// TestFsRmdirNotFound tests deleting a non existent directory
func TestFsRmdirNotFound(t *testing.T) {
	skipIfNotOk(t)
	err := remote.Rmdir("")
	assert.Error(t, err, "Expecting error on Rmdir non existent")
}

// TestFsMkdir tests tests making a directory
func TestFsMkdir(t *testing.T) {
	skipIfNotOk(t)

	// Use a new directory here.  This is for the container based
	// remotes which take time to create and destroy a container
	// (eg azure blob)
	newFs(t)

	err := remote.Mkdir("")
	require.NoError(t, err)
	fstest.CheckListing(t, remote, []fstest.Item{})

	err = remote.Mkdir("")
	require.NoError(t, err)
}

// TestFsMkdirRmdirSubdir tests making and removing a sub directory
func TestFsMkdirRmdirSubdir(t *testing.T) {
	skipIfNotOk(t)
	dir := "dir/subdir"
	err := operations.Mkdir(remote, dir)
	require.NoError(t, err)
	fstest.CheckListingWithPrecision(t, remote, []fstest.Item{}, []string{"dir", "dir/subdir"}, fs.Config.ModifyWindow)

	err = operations.Rmdir(remote, dir)
	require.NoError(t, err)
	fstest.CheckListingWithPrecision(t, remote, []fstest.Item{}, []string{"dir"}, fs.Config.ModifyWindow)

	err = operations.Rmdir(remote, "dir")
	require.NoError(t, err)
	fstest.CheckListingWithPrecision(t, remote, []fstest.Item{}, []string{}, fs.Config.ModifyWindow)
}

// TestFsListEmpty tests listing an empty directory
func TestFsListEmpty(t *testing.T) {
	skipIfNotOk(t)
	fstest.CheckListing(t, remote, []fstest.Item{})
}

// winPath converts a path into a windows safe path
func winPath(s string) string {
	return strings.Map(func(r rune) rune {
		switch r {
		case '<', '>', '"', '|', '?', '*', ':':
			return '_'
		}
		return r
	}, s)
}

// dirsToNames returns a sorted list of names
func dirsToNames(dirs []fs.Directory) []string {
	names := []string{}
	for _, dir := range dirs {
		names = append(names, winPath(fstest.Normalize(dir.Remote())))
	}
	sort.Strings(names)
	return names
}

// objsToNames returns a sorted list of object names
func objsToNames(objs []fs.Object) []string {
	names := []string{}
	for _, obj := range objs {
		names = append(names, winPath(fstest.Normalize(obj.Remote())))
	}
	sort.Strings(names)
	return names
}

// TestFsListDirEmpty tests listing the directories from an empty directory
func TestFsListDirEmpty(t *testing.T) {
	skipIfNotOk(t)
	objs, dirs, err := walk.GetAll(remote, "", true, 1)
	require.NoError(t, err)
	assert.Equal(t, []string{}, objsToNames(objs))
	assert.Equal(t, []string{}, dirsToNames(dirs))
}

// TestFsListRDirEmpty tests listing the directories from an empty directory using ListR
func TestFsListRDirEmpty(t *testing.T) {
	defer skipIfNotListR(t)()
	TestFsListDirEmpty(t)
}

// TestFsNewObjectNotFound tests not finding a object
func TestFsNewObjectNotFound(t *testing.T) {
	skipIfNotOk(t)
	// Object in an existing directory
	o, err := remote.NewObject("potato")
	assert.Nil(t, o)
	assert.Equal(t, fs.ErrorObjectNotFound, err)
	// Now try an object in a non existing directory
	o, err = remote.NewObject("directory/not/found/potato")
	assert.Nil(t, o)
	assert.Equal(t, fs.ErrorObjectNotFound, err)
}

func findObject(t *testing.T, Name string) fs.Object {
	var obj fs.Object
	var err error
	for i := 1; i <= *fstest.ListRetries; i++ {
		obj, err = remote.NewObject(Name)
		if err == nil {
			break
		}
		t.Logf("Sleeping for 1 second for findObject eventual consistency: %d/%d (%v)", i, *fstest.ListRetries, err)
		time.Sleep(1 * time.Second)
	}
	require.NoError(t, err)
	return obj
}

func testPut(t *testing.T, file *fstest.Item) string {
	tries := 1
	const maxTries = 10
again:
	contents := fstest.RandomString(100)
	buf := bytes.NewBufferString(contents)
	hash := hash.NewMultiHasher()
	in := io.TeeReader(buf, hash)

	file.Size = int64(buf.Len())
	obji := object.NewStaticObjectInfo(file.Path, file.ModTime, file.Size, true, nil, nil)
	obj, err := remote.Put(in, obji)
	if err != nil {
		// Retry if err returned a retry error
		if fserrors.IsRetryError(err) && tries < maxTries {
			t.Logf("Put error: %v - low level retry %d/%d", err, tries, maxTries)
			time.Sleep(2 * time.Second)

			tries++
			goto again
		}
		require.NoError(t, err, fmt.Sprintf("Put error: %v", err))
	}
	file.Hashes = hash.Sums()
	file.Check(t, obj, remote.Precision())
	// Re-read the object and check again
	obj = findObject(t, file.Path)
	file.Check(t, obj, remote.Precision())
	return contents
}

// TestFsPutFile1 tests putting a file
func TestFsPutFile1(t *testing.T) {
	skipIfNotOk(t)
	file1Contents = testPut(t, &file1)
}

type errorReader struct {
	err error
}

func (er errorReader) Read(p []byte) (n int, err error) {
	return 0, er.err
}

// TestFsPutError tests uploading a file where there is an error
//
// It makes sure that aborting a file half way through does not create
// a file on the remote.
func TestFsPutError(t *testing.T) {
	skipIfNotOk(t)

	// Read 50 bytes then produce an error
	contents := fstest.RandomString(50)
	buf := bytes.NewBufferString(contents)
	er := &errorReader{errors.New("potato")}
	in := io.MultiReader(buf, er)

	obji := object.NewStaticObjectInfo(file2.Path, file2.ModTime, 100, true, nil, nil)
	_, err := remote.Put(in, obji)
	// assert.Nil(t, obj) - FIXME some remotes return the object even on nil
	assert.NotNil(t, err)

	obj, err := remote.NewObject(file2.Path)
	assert.Nil(t, obj)
	assert.Equal(t, fs.ErrorObjectNotFound, err)
}

// TestFsPutFile2 tests putting a file into a subdirectory
func TestFsPutFile2(t *testing.T) {
	skipIfNotOk(t)
	file2Contents = testPut(t, &file2)
}

// TestFsUpdateFile1 tests updating file1 with new contents
func TestFsUpdateFile1(t *testing.T) {
	skipIfNotOk(t)
	file1Contents = testPut(t, &file1)
	// Note that the next test will check there are no duplicated file names
}

// TestFsListDirFile2 tests the files are correctly uploaded by doing
// Depth 1 directory listings
func TestFsListDirFile2(t *testing.T) {
	skipIfNotOk(t)
	list := func(dir string, expectedDirNames, expectedObjNames []string) {
		var objNames, dirNames []string
		for i := 1; i <= *fstest.ListRetries; i++ {
			objs, dirs, err := walk.GetAll(remote, dir, true, 1)
			if errors.Cause(err) == fs.ErrorDirNotFound {
				objs, dirs, err = walk.GetAll(remote, winPath(dir), true, 1)
			}
			require.NoError(t, err)
			objNames = objsToNames(objs)
			dirNames = dirsToNames(dirs)
			if len(objNames) >= len(expectedObjNames) && len(dirNames) >= len(expectedDirNames) {
				break
			}
			t.Logf("Sleeping for 1 second for TestFsListDirFile2 eventual consistency: %d/%d", i, *fstest.ListRetries)
			time.Sleep(1 * time.Second)
		}
		assert.Equal(t, expectedDirNames, dirNames)
		assert.Equal(t, expectedObjNames, objNames)
	}
	dir := file2.Path
	deepest := true
	for dir != "" {
		expectedObjNames := []string{}
		expectedDirNames := []string{}
		child := dir
		dir = path.Dir(dir)
		if dir == "." {
			dir = ""
			expectedObjNames = append(expectedObjNames, winPath(file1.Path))
		}
		if deepest {
			expectedObjNames = append(expectedObjNames, winPath(file2.Path))
			deepest = false
		} else {
			expectedDirNames = append(expectedDirNames, winPath(child))
		}
		list(dir, expectedDirNames, expectedObjNames)
	}
}

// TestFsListRDirFile2 tests the files are correctly uploaded by doing
// Depth 1 directory listings using ListR
func TestFsListRDirFile2(t *testing.T) {
	defer skipIfNotListR(t)()
	TestFsListDirFile2(t)
}

// TestFsListDirRoot tests that DirList works in the root
func TestFsListDirRoot(t *testing.T) {
	skipIfNotOk(t)
	rootRemote, err := fs.NewFs(RemoteName)
	require.NoError(t, err)
	_, dirs, err := walk.GetAll(rootRemote, "", true, 1)
	require.NoError(t, err)
	assert.Contains(t, dirsToNames(dirs), subRemoteLeaf, "Remote leaf not found")
}

// TestFsListRDirRoot tests that DirList works in the root using ListR
func TestFsListRDirRoot(t *testing.T) {
	defer skipIfNotListR(t)()
	TestFsListDirRoot(t)
}

// TestFsListSubdir tests List works for a subdirectory
func TestFsListSubdir(t *testing.T) {
	skipIfNotOk(t)
	fileName := file2.Path
	var err error
	var objs []fs.Object
	var dirs []fs.Directory
	for i := 0; i < 2; i++ {
		dir, _ := path.Split(fileName)
		dir = dir[:len(dir)-1]
		objs, dirs, err = walk.GetAll(remote, dir, true, -1)
		if err != fs.ErrorDirNotFound {
			break
		}
		fileName = file2.WinPath
	}
	require.NoError(t, err)
	require.Len(t, objs, 1)
	assert.Equal(t, fileName, objs[0].Remote())
	require.Len(t, dirs, 0)
}

// TestFsListRSubdir tests List works for a subdirectory using ListR
func TestFsListRSubdir(t *testing.T) {
	defer skipIfNotListR(t)()
	TestFsListSubdir(t)
}

// TestFsListLevel2 tests List works for 2 levels
func TestFsListLevel2(t *testing.T) {
	skipIfNotOk(t)
	objs, dirs, err := walk.GetAll(remote, "", true, 2)
	if err == fs.ErrorLevelNotSupported {
		return
	}
	require.NoError(t, err)
	assert.Equal(t, []string{file1.Path}, objsToNames(objs))
	assert.Equal(t, []string{`hello_ sausage`, `hello_ sausage/êé`}, dirsToNames(dirs))
}

// TestFsListRLevel2 tests List works for 2 levels using ListR
func TestFsListRLevel2(t *testing.T) {
	defer skipIfNotListR(t)()
	TestFsListLevel2(t)
}

// TestFsListFile1 tests file present
func TestFsListFile1(t *testing.T) {
	skipIfNotOk(t)
	fstest.CheckListing(t, remote, []fstest.Item{file1, file2})
}

// TestFsNewObject tests NewObject
func TestFsNewObject(t *testing.T) {
	skipIfNotOk(t)
	obj := findObject(t, file1.Path)
	file1.Check(t, obj, remote.Precision())
}

// TestFsListFile1and2 tests two files present
func TestFsListFile1and2(t *testing.T) {
	skipIfNotOk(t)
	fstest.CheckListing(t, remote, []fstest.Item{file1, file2})
}

// TestFsNewObjectDir tests NewObject on a directory which should produce an error
func TestFsNewObjectDir(t *testing.T) {
	skipIfNotOk(t)
	dir := path.Dir(file2.Path)
	obj, err := remote.NewObject(dir)
	assert.Nil(t, obj)
	assert.NotNil(t, err)
}

// TestFsCopy tests Copy
func TestFsCopy(t *testing.T) {
	skipIfNotOk(t)

	// Check have Copy
	doCopy := remote.Features().Copy
	if doCopy == nil {
		t.Skip("FS has no Copier interface")
	}

	// Test with file2 so have + and ' ' in file name
	var file2Copy = file2
	file2Copy.Path += "-copy"

	// do the copy
	src := findObject(t, file2.Path)
	dst, err := doCopy(src, file2Copy.Path)
	if err == fs.ErrorCantCopy {
		t.Skip("FS can't copy")
	}
	require.NoError(t, err, fmt.Sprintf("Error: %#v", err))

	// check file exists in new listing
	fstest.CheckListing(t, remote, []fstest.Item{file1, file2, file2Copy})

	// Check dst lightly - list above has checked ModTime/Hashes
	assert.Equal(t, file2Copy.Path, dst.Remote())

	// Delete copy
	err = dst.Remove()
	require.NoError(t, err)

}

// TestFsMove tests Move
func TestFsMove(t *testing.T) {
	skipIfNotOk(t)

	// Check have Move
	doMove := remote.Features().Move
	if doMove == nil {
		t.Skip("FS has no Mover interface")
	}

	// state of files now:
	// 1: file name.txt
	// 2: hello sausage?/../z.txt

	var file1Move = file1
	var file2Move = file2

	// check happy path, i.e. no naming conflicts when rename and move are two
	// separate operations
	file2Move.Path = "other.txt"
	file2Move.WinPath = ""
	src := findObject(t, file2.Path)
	dst, err := doMove(src, file2Move.Path)
	if err == fs.ErrorCantMove {
		t.Skip("FS can't move")
	}
	require.NoError(t, err)
	// check file exists in new listing
	fstest.CheckListing(t, remote, []fstest.Item{file1, file2Move})
	// Check dst lightly - list above has checked ModTime/Hashes
	assert.Equal(t, file2Move.Path, dst.Remote())
	// 1: file name.txt
	// 2: other.txt

	// Check conflict on "rename, then move"
	file1Move.Path = "moveTest/other.txt"
	src = findObject(t, file1.Path)
	_, err = doMove(src, file1Move.Path)
	require.NoError(t, err)
	fstest.CheckListing(t, remote, []fstest.Item{file1Move, file2Move})
	// 1: moveTest/other.txt
	// 2: other.txt

	// Check conflict on "move, then rename"
	src = findObject(t, file1Move.Path)
	_, err = doMove(src, file1.Path)
	require.NoError(t, err)
	fstest.CheckListing(t, remote, []fstest.Item{file1, file2Move})
	// 1: file name.txt
	// 2: other.txt

	src = findObject(t, file2Move.Path)
	_, err = doMove(src, file2.Path)
	require.NoError(t, err)
	fstest.CheckListing(t, remote, []fstest.Item{file1, file2})
	// 1: file name.txt
	// 2: hello sausage?/../z.txt
}

// Move src to this remote using server side move operations.
//
// Will only be called if src.Fs().Name() == f.Name()
//
// If it isn't possible then return fs.ErrorCantDirMove
//
// If destination exists then return fs.ErrorDirExists

// TestFsDirMove tests DirMove
//
// go test -v -run '^Test(Setup|Init|FsMkdir|FsPutFile1|FsPutFile2|FsUpdateFile1|FsDirMove)$
func TestFsDirMove(t *testing.T) {
	skipIfNotOk(t)

	// Check have DirMove
	doDirMove := remote.Features().DirMove
	if doDirMove == nil {
		t.Skip("FS has no DirMover interface")
	}

	// Check it can't move onto itself
	err := doDirMove(remote, "", "")
	require.Equal(t, fs.ErrorDirExists, err)

	// new remote
	newRemote, _, removeNewRemote, err := fstest.RandomRemote(RemoteName, false)
	require.NoError(t, err)
	defer removeNewRemote()

	const newName = "new_name/sub_new_name"
	// try the move
	err = newRemote.Features().DirMove(remote, "", newName)
	require.NoError(t, err)

	// check remotes
	// FIXME: Prints errors.
	fstest.CheckListing(t, remote, []fstest.Item{})
	file1Copy := file1
	file1Copy.Path = path.Join(newName, file1.Path)
	file2Copy := file2
	file2Copy.Path = path.Join(newName, file2.Path)
	file2Copy.WinPath = path.Join(newName, file2.WinPath)
	fstest.CheckListing(t, newRemote, []fstest.Item{file2Copy, file1Copy})

	// move it back
	err = doDirMove(newRemote, newName, "")
	require.NoError(t, err)

	// check remotes
	fstest.CheckListing(t, remote, []fstest.Item{file2, file1})
	fstest.CheckListing(t, newRemote, []fstest.Item{})
}

// TestFsRmdirFull tests removing a non empty directory
func TestFsRmdirFull(t *testing.T) {
	skipIfNotOk(t)
	err := remote.Rmdir("")
	require.Error(t, err, "Expecting error on RMdir on non empty remote")
}

// TestFsPrecision tests the Precision of the Fs
func TestFsPrecision(t *testing.T) {
	skipIfNotOk(t)
	precision := remote.Precision()
	if precision == fs.ModTimeNotSupported {
		return
	}
	if precision > time.Second || precision < 0 {
		t.Fatalf("Precision out of range %v", precision)
	}
	// FIXME check expected precision
}

// TestFsChangeNotify tests that changes are properly
// propagated
//
// go test -v -remote TestDrive: -run '^Test(Setup|Init|FsChangeNotify)$' -verbose
func TestFsChangeNotify(t *testing.T) {
	skipIfNotOk(t)

	// Check have ChangeNotify
	doChangeNotify := remote.Features().ChangeNotify
	if doChangeNotify == nil {
		t.Skip("FS has no ChangeNotify interface")
	}

	err := operations.Mkdir(remote, "dir")
	require.NoError(t, err)

	dirChanges := []string{}
	objChanges := []string{}
	quitChannel := doChangeNotify(func(x string, e fs.EntryType) {
		if e == fs.EntryDirectory {
			dirChanges = append(dirChanges, x)
		} else if e == fs.EntryObject {
			objChanges = append(objChanges, x)
		}
	}, time.Second)
	defer func() { close(quitChannel) }()

	for _, idx := range []int{1, 3, 2} {
		err = operations.Mkdir(remote, fmt.Sprintf("dir/subdir%d", idx))
		require.NoError(t, err)
	}

	contents := fstest.RandomString(100)
	buf := bytes.NewBufferString(contents)

	for _, idx := range []int{2, 4, 3} {
		obji := object.NewStaticObjectInfo(fmt.Sprintf("dir/file%d", idx), time.Now(), int64(buf.Len()), true, nil, nil)
		_, err = remote.Put(buf, obji)
		require.NoError(t, err)
	}

	time.Sleep(3 * time.Second)

	assert.Equal(t, []string{"dir/subdir1", "dir/subdir3", "dir/subdir2"}, dirChanges)
	assert.Equal(t, []string{"dir/file2", "dir/file4", "dir/file3"}, objChanges)
}

// TestObjectString tests the Object String method
func TestObjectString(t *testing.T) {
	skipIfNotOk(t)
	obj := findObject(t, file1.Path)
	assert.Equal(t, file1.Path, obj.String())
	assert.Equal(t, "<nil>", NilObject.String())
}

// TestObjectFs tests the object can be found
func TestObjectFs(t *testing.T) {
	skipIfNotOk(t)
	obj := findObject(t, file1.Path)
	testRemote := remote
	if obj.Fs() != testRemote {
		// Check to see if this wraps something else
		if doUnWrap := testRemote.Features().UnWrap; doUnWrap != nil {
			testRemote = doUnWrap()
		}
	}
	assert.Equal(t, obj.Fs(), testRemote)
}

// TestObjectRemote tests the Remote is correct
func TestObjectRemote(t *testing.T) {
	skipIfNotOk(t)
	obj := findObject(t, file1.Path)
	assert.Equal(t, file1.Path, obj.Remote())
}

// TestObjectHashes checks all the hashes the object supports
func TestObjectHashes(t *testing.T) {
	skipIfNotOk(t)
	obj := findObject(t, file1.Path)
	file1.CheckHashes(t, obj)
}

// TestObjectModTime tests the ModTime of the object is correct
func TestObjectModTime(t *testing.T) {
	skipIfNotOk(t)
	obj := findObject(t, file1.Path)
	file1.CheckModTime(t, obj, obj.ModTime(), remote.Precision())
}

// TestObjectMimeType tests the MimeType of the object is correct
func TestObjectMimeType(t *testing.T) {
	skipIfNotOk(t)
	obj := findObject(t, file1.Path)
	do, ok := obj.(fs.MimeTyper)
	if !ok {
		t.Skip("MimeType method not supported")
	}
	mimeType := do.MimeType()
	if strings.ContainsRune(mimeType, ';') {
		assert.Equal(t, "text/plain; charset=utf-8", mimeType)
	} else {
		assert.Equal(t, "text/plain", mimeType)
	}
}

// TestObjectSetModTime tests that SetModTime works
func TestObjectSetModTime(t *testing.T) {
	skipIfNotOk(t)
	newModTime := fstest.Time("2011-12-13T14:15:16.999999999Z")
	obj := findObject(t, file1.Path)
	err := obj.SetModTime(newModTime)
	if err == fs.ErrorCantSetModTime || err == fs.ErrorCantSetModTimeWithoutDelete {
		t.Log(err)
		return
	}
	require.NoError(t, err)
	file1.ModTime = newModTime
	file1.CheckModTime(t, obj, obj.ModTime(), remote.Precision())
	// And make a new object and read it from there too
	TestObjectModTime(t)
}

// TestObjectSize tests that Size works
func TestObjectSize(t *testing.T) {
	skipIfNotOk(t)
	obj := findObject(t, file1.Path)
	assert.Equal(t, file1.Size, obj.Size())
}

// read the contents of an object as a string
func readObject(t *testing.T, obj fs.Object, limit int64, options ...fs.OpenOption) string {
	what := fmt.Sprintf("readObject(%q) limit=%d, options=%+v", obj, limit, options)
	in, err := obj.Open(options...)
	require.NoError(t, err, what)
	var r io.Reader = in
	if limit >= 0 {
		r = &io.LimitedReader{R: r, N: limit}
	}
	contents, err := ioutil.ReadAll(r)
	require.NoError(t, err, what)
	err = in.Close()
	require.NoError(t, err, what)
	return string(contents)
}

// TestObjectOpen tests that Open works
func TestObjectOpen(t *testing.T) {
	skipIfNotOk(t)
	obj := findObject(t, file1.Path)
	assert.Equal(t, file1Contents, readObject(t, obj, -1), "contents of file1 differ")
}

// TestObjectOpenSeek tests that Open works with SeekOption
func TestObjectOpenSeek(t *testing.T) {
	skipIfNotOk(t)
	obj := findObject(t, file1.Path)
	assert.Equal(t, file1Contents[50:], readObject(t, obj, -1, &fs.SeekOption{Offset: 50}), "contents of file1 differ after seek")
}

// TestObjectOpenRange tests that Open works with RangeOption
func TestObjectOpenRange(t *testing.T) {
	skipIfNotOk(t)
	obj := findObject(t, file1.Path)
	for _, test := range []struct {
		ro                 fs.RangeOption
		wantStart, wantEnd int
	}{
		{fs.RangeOption{Start: 5, End: 15}, 5, 16},
		{fs.RangeOption{Start: 80, End: -1}, 80, 100},
		{fs.RangeOption{Start: 81, End: 100000}, 81, 100},
		{fs.RangeOption{Start: -1, End: 20}, 80, 100}, // if start is omitted this means get the final bytes
		// {fs.RangeOption{Start: -1, End: -1}, 0, 100}, - this seems to work but the RFC doesn't define it
	} {
		got := readObject(t, obj, -1, &test.ro)
		foundAt := strings.Index(file1Contents, got)
		help := fmt.Sprintf("%#v failed want [%d:%d] got [%d:%d]", test.ro, test.wantStart, test.wantEnd, foundAt, foundAt+len(got))
		assert.Equal(t, file1Contents[test.wantStart:test.wantEnd], got, help)
	}
}

// TestObjectPartialRead tests that reading only part of the object does the correct thing
func TestObjectPartialRead(t *testing.T) {
	skipIfNotOk(t)
	obj := findObject(t, file1.Path)
	assert.Equal(t, file1Contents[:50], readObject(t, obj, 50), "contents of file1 differ after limited read")
}

// TestObjectUpdate tests that Update works
func TestObjectUpdate(t *testing.T) {
	skipIfNotOk(t)
	contents := fstest.RandomString(200)
	buf := bytes.NewBufferString(contents)
	hash := hash.NewMultiHasher()
	in := io.TeeReader(buf, hash)

	file1.Size = int64(buf.Len())
	obj := findObject(t, file1.Path)
	obji := object.NewStaticObjectInfo(file1.Path, file1.ModTime, int64(len(contents)), true, nil, obj.Fs())
	err := obj.Update(in, obji)
	require.NoError(t, err)
	file1.Hashes = hash.Sums()

	// check the object has been updated
	file1.Check(t, obj, remote.Precision())

	// Re-read the object and check again
	obj = findObject(t, file1.Path)
	file1.Check(t, obj, remote.Precision())

	// check contents correct
	assert.Equal(t, contents, readObject(t, obj, -1), "contents of updated file1 differ")
	file1Contents = contents
}

// TestObjectStorable tests that Storable works
func TestObjectStorable(t *testing.T) {
	skipIfNotOk(t)
	obj := findObject(t, file1.Path)
	require.NotNil(t, !obj.Storable(), "Expecting object to be storable")
}

// TestFsIsFile tests that an error is returned along with a valid fs
// which points to the parent directory.
func TestFsIsFile(t *testing.T) {
	skipIfNotOk(t)
	remoteName := subRemoteName + "/" + file2.Path
	file2Copy := file2
	file2Copy.Path = "z.txt"
	file2Copy.WinPath = ""
	fileRemote, err := fs.NewFs(remoteName)
	assert.Equal(t, fs.ErrorIsFile, err)
	fstest.CheckListing(t, fileRemote, []fstest.Item{file2Copy})
}

// TestFsIsFileNotFound tests that an error is not returned if no object is found
func TestFsIsFileNotFound(t *testing.T) {
	skipIfNotOk(t)
	remoteName := subRemoteName + "/not found.txt"
	fileRemote, err := fs.NewFs(remoteName)
	require.NoError(t, err)
	fstest.CheckListing(t, fileRemote, []fstest.Item{})
}

// TestObjectRemove tests Remove
func TestObjectRemove(t *testing.T) {
	skipIfNotOk(t)
	obj := findObject(t, file1.Path)
	err := obj.Remove()
	require.NoError(t, err)
	fstest.CheckListing(t, remote, []fstest.Item{file2})
}

// TestFsPutStream tests uploading files when size is not known in advance
func TestFsPutStream(t *testing.T) {
	skipIfNotOk(t)
	if remote.Features().PutStream == nil {
		t.Skip("FS has no PutStream interface")
	}

	file := fstest.Item{
		ModTime: fstest.Time("2001-02-03T04:05:06.499999999Z"),
		Path:    "piped data.txt",
		Size:    -1, // use unknown size during upload
	}

	tries := 1
	const maxTries = 10
again:
	contentSize := 100
	contents := fstest.RandomString(contentSize)
	buf := bytes.NewBufferString(contents)
	hash := hash.NewMultiHasher()
	in := io.TeeReader(buf, hash)

	file.Size = -1
	obji := object.NewStaticObjectInfo(file.Path, file.ModTime, file.Size, true, nil, nil)
	obj, err := remote.Features().PutStream(in, obji)
	if err != nil {
		// Retry if err returned a retry error
		if fserrors.IsRetryError(err) && tries < maxTries {
			t.Logf("Put error: %v - low level retry %d/%d", err, tries, maxTries)
			time.Sleep(2 * time.Second)

			tries++
			goto again
		}
		require.NoError(t, err, fmt.Sprintf("PutStream Unknown Length error: %v", err))
	}
	file.Hashes = hash.Sums()
	file.Size = int64(contentSize) // use correct size when checking
	file.Check(t, obj, remote.Precision())
	// Re-read the object and check again
	obj = findObject(t, file.Path)
	file.Check(t, obj, remote.Precision())
}

// TestObjectPurge tests Purge
func TestObjectPurge(t *testing.T) {
	skipIfNotOk(t)

	err := operations.Purge(remote, "")
	require.NoError(t, err)
	fstest.CheckListing(t, remote, []fstest.Item{})

	err = operations.Purge(remote, "")
	assert.Error(t, err, "Expecting error after on second purge")
}

// TestInternal calls InternalTest() on the Fs
func TestInternal(t *testing.T) {
	skipIfNotOk(t)
	if it, ok := remote.(InternalTester); ok {
		it.InternalTest(t)
	} else {
		t.Skipf("%T does not implement InternalTester", remote)
	}
}

// TestFinalise tidies up after the previous tests
func TestFinalise(t *testing.T) {
	skipIfNotOk(t)
	if strings.HasPrefix(RemoteName, "/") {
		// Remove temp directory
		err := os.Remove(RemoteName)
		require.NoError(t, err)
	}
}