// Copyright (C) 2019-2023 Nicola Murino
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published
// by the Free Software Foundation, version 3.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.

package common

import (
	"errors"
	"fmt"
	"os"
	"path"
	"path/filepath"
	"runtime"
	"testing"
	"time"

	"github.com/pkg/sftp"
	"github.com/rs/xid"
	"github.com/sftpgo/sdk"
	"github.com/stretchr/testify/assert"
	"github.com/stretchr/testify/require"

	"github.com/drakkan/sftpgo/v2/internal/dataprovider"
	"github.com/drakkan/sftpgo/v2/internal/kms"
	"github.com/drakkan/sftpgo/v2/internal/util"
	"github.com/drakkan/sftpgo/v2/internal/vfs"
)

var (
	errWalkDir = errors.New("err walk dir")
)

// MockOsFs mockable OsFs
type MockOsFs struct {
	vfs.Fs
	hasVirtualFolders bool
	name              string
	err               error
}

// Name returns the name for the Fs implementation
func (fs *MockOsFs) Name() string {
	if fs.name != "" {
		return fs.name
	}
	return "mockOsFs"
}

// HasVirtualFolders returns true if folders are emulated
func (fs *MockOsFs) HasVirtualFolders() bool {
	return fs.hasVirtualFolders
}

func (fs *MockOsFs) IsUploadResumeSupported() bool {
	return !fs.hasVirtualFolders
}

func (fs *MockOsFs) Chtimes(_ string, _, _ time.Time, _ bool) error {
	return vfs.ErrVfsUnsupported
}

func (fs *MockOsFs) Lstat(name string) (os.FileInfo, error) {
	if fs.err != nil {
		return nil, fs.err
	}
	return fs.Fs.Lstat(name)
}

// Walk returns a duplicate path for testing
func (fs *MockOsFs) Walk(_ string, walkFn filepath.WalkFunc) error {
	if fs.err == errWalkDir {
		walkFn("fsdpath", vfs.NewFileInfo("dpath", true, 0, time.Now(), false), nil)        //nolint:errcheck
		return walkFn("fsdpath", vfs.NewFileInfo("dpath", true, 0, time.Now(), false), nil) //nolint:errcheck
	}
	walkFn("fsfpath", vfs.NewFileInfo("fpath", false, 0, time.Now(), false), nil) //nolint:errcheck
	return fs.err
}

func newMockOsFs(hasVirtualFolders bool, connectionID, rootDir, name string, err error) vfs.Fs {
	return &MockOsFs{
		Fs:                vfs.NewOsFs(connectionID, rootDir, "", nil),
		name:              name,
		hasVirtualFolders: hasVirtualFolders,
		err:               err,
	}
}

func TestRemoveErrors(t *testing.T) {
	mappedPath := filepath.Join(os.TempDir(), "map")
	homePath := filepath.Join(os.TempDir(), "home")

	user := dataprovider.User{
		BaseUser: sdk.BaseUser{
			Username: "remove_errors_user",
			HomeDir:  homePath,
		},
		VirtualFolders: []vfs.VirtualFolder{
			{
				BaseVirtualFolder: vfs.BaseVirtualFolder{
					Name:       filepath.Base(mappedPath),
					MappedPath: mappedPath,
				},
				VirtualPath: "/virtualpath",
			},
		},
	}
	user.Permissions = make(map[string][]string)
	user.Permissions["/"] = []string{dataprovider.PermAny}
	fs := vfs.NewOsFs("", os.TempDir(), "", nil)
	conn := NewBaseConnection("", ProtocolFTP, "", "", user)
	err := conn.IsRemoveDirAllowed(fs, mappedPath, "/virtualpath1")
	if assert.Error(t, err) {
		assert.Contains(t, err.Error(), "permission denied")
	}
	err = conn.RemoveFile(fs, filepath.Join(homePath, "missing_file"), "/missing_file",
		vfs.NewFileInfo("info", false, 100, time.Now(), false))
	assert.Error(t, err)
}

func TestSetStatMode(t *testing.T) {
	oldSetStatMode := Config.SetstatMode
	Config.SetstatMode = 1

	fakePath := "fake path"
	user := dataprovider.User{
		BaseUser: sdk.BaseUser{
			HomeDir: os.TempDir(),
		},
	}
	user.Permissions = make(map[string][]string)
	user.Permissions["/"] = []string{dataprovider.PermAny}
	fs := newMockOsFs(true, "", user.GetHomeDir(), "", nil)
	conn := NewBaseConnection("", ProtocolWebDAV, "", "", user)
	err := conn.handleChmod(fs, fakePath, fakePath, nil)
	assert.NoError(t, err)
	err = conn.handleChown(fs, fakePath, fakePath, nil)
	assert.NoError(t, err)
	err = conn.handleChtimes(fs, fakePath, fakePath, nil)
	assert.NoError(t, err)

	Config.SetstatMode = 2
	err = conn.handleChmod(fs, fakePath, fakePath, nil)
	assert.NoError(t, err)
	err = conn.handleChtimes(fs, fakePath, fakePath, &StatAttributes{
		Atime: time.Now(),
		Mtime: time.Now(),
	})
	assert.NoError(t, err)

	Config.SetstatMode = oldSetStatMode
}

func TestRecursiveRenameWalkError(t *testing.T) {
	fs := vfs.NewOsFs("", filepath.Clean(os.TempDir()), "", nil)
	conn := NewBaseConnection("", ProtocolWebDAV, "", "", dataprovider.User{
		BaseUser: sdk.BaseUser{
			Permissions: map[string][]string{
				"/": {dataprovider.PermListItems, dataprovider.PermUpload,
					dataprovider.PermDownload, dataprovider.PermRenameDirs},
			},
		},
	})
	err := conn.checkRecursiveRenameDirPermissions(fs, fs, filepath.Join(os.TempDir(), "/source"),
		filepath.Join(os.TempDir(), "/target"), "/source", "/target",
		vfs.NewFileInfo("source", true, 0, time.Now(), false))
	assert.ErrorIs(t, err, os.ErrNotExist)

	fs = newMockOsFs(false, "mockID", filepath.Clean(os.TempDir()), "S3Fs", errWalkDir)
	err = conn.checkRecursiveRenameDirPermissions(fs, fs, filepath.Join(os.TempDir(), "/source"),
		filepath.Join(os.TempDir(), "/target"), "/source", "/target",
		vfs.NewFileInfo("source", true, 0, time.Now(), false))
	if assert.Error(t, err) {
		assert.Equal(t, err.Error(), conn.GetOpUnsupportedError().Error())
	}

	conn.User.Permissions["/"] = []string{dataprovider.PermListItems, dataprovider.PermUpload,
		dataprovider.PermDownload, dataprovider.PermRenameFiles}
	// no dir rename permission, the quick check path returns permission error without walking
	err = conn.checkRecursiveRenameDirPermissions(fs, fs, filepath.Join(os.TempDir(), "/source"),
		filepath.Join(os.TempDir(), "/target"), "/source", "/target",
		vfs.NewFileInfo("source", true, 0, time.Now(), false))
	if assert.Error(t, err) {
		assert.EqualError(t, err, conn.GetPermissionDeniedError().Error())
	}
}

func TestCrossRenameFsErrors(t *testing.T) {
	fs := vfs.NewOsFs("", os.TempDir(), "", nil)
	conn := NewBaseConnection("", ProtocolWebDAV, "", "", dataprovider.User{})
	res := conn.hasSpaceForCrossRename(fs, vfs.QuotaCheckResult{}, 1, "missingsource")
	assert.False(t, res)
	if runtime.GOOS != osWindows {
		dirPath := filepath.Join(os.TempDir(), "d")
		err := os.Mkdir(dirPath, os.ModePerm)
		assert.NoError(t, err)
		err = os.Chmod(dirPath, 0001)
		assert.NoError(t, err)

		res = conn.hasSpaceForCrossRename(fs, vfs.QuotaCheckResult{}, 1, dirPath)
		assert.False(t, res)

		err = os.Chmod(dirPath, os.ModePerm)
		assert.NoError(t, err)
		err = os.Remove(dirPath)
		assert.NoError(t, err)
	}
}

func TestRenameVirtualFolders(t *testing.T) {
	vdir := "/avdir"
	u := dataprovider.User{}
	u.VirtualFolders = append(u.VirtualFolders, vfs.VirtualFolder{
		BaseVirtualFolder: vfs.BaseVirtualFolder{
			Name:       "name",
			MappedPath: "mappedPath",
		},
		VirtualPath: vdir,
	})
	fs := vfs.NewOsFs("", os.TempDir(), "", nil)
	conn := NewBaseConnection("", ProtocolFTP, "", "", u)
	res := conn.isRenamePermitted(fs, fs, "source", "target", vdir, "vdirtarget", nil)
	assert.False(t, res)
}

func TestRenamePerms(t *testing.T) {
	src := "source"
	target := "target"
	sub := "/sub"
	subTarget := sub + "/target"
	u := dataprovider.User{}
	u.Permissions = map[string][]string{}
	u.Permissions["/"] = []string{dataprovider.PermCreateDirs, dataprovider.PermUpload, dataprovider.PermCreateSymlinks,
		dataprovider.PermDeleteFiles}
	conn := NewBaseConnection("", ProtocolSFTP, "", "", u)
	assert.False(t, conn.hasRenamePerms(src, target, nil))
	u.Permissions["/"] = []string{dataprovider.PermRename}
	assert.True(t, conn.hasRenamePerms(src, target, nil))
	u.Permissions["/"] = []string{dataprovider.PermCreateDirs, dataprovider.PermUpload, dataprovider.PermDeleteFiles,
		dataprovider.PermDeleteDirs}
	assert.False(t, conn.hasRenamePerms(src, target, nil))

	info := vfs.NewFileInfo(src, true, 0, time.Now(), false)
	u.Permissions["/"] = []string{dataprovider.PermRenameFiles}
	assert.False(t, conn.hasRenamePerms(src, target, info))
	u.Permissions["/"] = []string{dataprovider.PermRenameDirs}
	assert.True(t, conn.hasRenamePerms(src, target, info))
	u.Permissions["/"] = []string{dataprovider.PermRename}
	assert.True(t, conn.hasRenamePerms(src, target, info))
	u.Permissions["/"] = []string{dataprovider.PermDownload, dataprovider.PermUpload, dataprovider.PermDeleteDirs}
	assert.False(t, conn.hasRenamePerms(src, target, info))
	// test with different permissions between source and target
	u.Permissions["/"] = []string{dataprovider.PermRename}
	u.Permissions[sub] = []string{dataprovider.PermRenameFiles}
	assert.False(t, conn.hasRenamePerms(src, subTarget, info))
	u.Permissions[sub] = []string{dataprovider.PermRenameDirs}
	assert.True(t, conn.hasRenamePerms(src, subTarget, info))
	// test files
	info = vfs.NewFileInfo(src, false, 0, time.Now(), false)
	u.Permissions["/"] = []string{dataprovider.PermRenameDirs}
	assert.False(t, conn.hasRenamePerms(src, target, info))
	u.Permissions["/"] = []string{dataprovider.PermRenameFiles}
	assert.True(t, conn.hasRenamePerms(src, target, info))
	u.Permissions["/"] = []string{dataprovider.PermRename}
	assert.True(t, conn.hasRenamePerms(src, target, info))
	// test with different permissions between source and target
	u.Permissions["/"] = []string{dataprovider.PermRename}
	u.Permissions[sub] = []string{dataprovider.PermRenameDirs}
	assert.False(t, conn.hasRenamePerms(src, subTarget, info))
	u.Permissions[sub] = []string{dataprovider.PermRenameFiles}
	assert.True(t, conn.hasRenamePerms(src, subTarget, info))
}

func TestRenameNestedFolders(t *testing.T) {
	u := dataprovider.User{}
	u.VirtualFolders = append(u.VirtualFolders, vfs.VirtualFolder{
		BaseVirtualFolder: vfs.BaseVirtualFolder{
			Name:       "vfolder",
			MappedPath: filepath.Join(os.TempDir(), "f"),
		},
		VirtualPath: "/vdirs/f",
	})
	conn := NewBaseConnection("", ProtocolSFTP, "", "", u)
	err := conn.checkFolderRename(nil, nil, filepath.Clean(os.TempDir()), filepath.Join(os.TempDir(), "subdir"), "/src", "/dst", nil)
	assert.Error(t, err)
	err = conn.checkFolderRename(nil, nil, filepath.Join(os.TempDir(), "subdir"), filepath.Clean(os.TempDir()), "/src", "/dst", nil)
	assert.Error(t, err)
	err = conn.checkFolderRename(nil, nil, "", "", "/src/sub", "/src", nil)
	assert.Error(t, err)
	err = conn.checkFolderRename(nil, nil, filepath.Join(os.TempDir(), "src"), filepath.Join(os.TempDir(), "vdirs"), "/src", "/vdirs", nil)
	assert.Error(t, err)
}

func TestUpdateQuotaAfterRename(t *testing.T) {
	user := dataprovider.User{
		BaseUser: sdk.BaseUser{
			Username: userTestUsername,
			HomeDir:  filepath.Join(os.TempDir(), "home"),
		},
	}
	mappedPath := filepath.Join(os.TempDir(), "vdir")
	user.Permissions = make(map[string][]string)
	user.Permissions["/"] = []string{dataprovider.PermAny}
	user.VirtualFolders = append(user.VirtualFolders, vfs.VirtualFolder{
		BaseVirtualFolder: vfs.BaseVirtualFolder{
			MappedPath: mappedPath,
		},
		VirtualPath: "/vdir",
		QuotaFiles:  -1,
		QuotaSize:   -1,
	})
	user.VirtualFolders = append(user.VirtualFolders, vfs.VirtualFolder{
		BaseVirtualFolder: vfs.BaseVirtualFolder{
			MappedPath: mappedPath,
		},
		VirtualPath: "/vdir1",
		QuotaFiles:  -1,
		QuotaSize:   -1,
	})
	err := os.MkdirAll(user.GetHomeDir(), os.ModePerm)
	assert.NoError(t, err)
	err = os.MkdirAll(mappedPath, os.ModePerm)
	assert.NoError(t, err)
	fs, err := user.GetFilesystem("id")
	assert.NoError(t, err)
	c := NewBaseConnection("", ProtocolSFTP, "", "", user)
	request := sftp.NewRequest("Rename", "/testfile")
	if runtime.GOOS != osWindows {
		request.Filepath = "/dir"
		request.Target = path.Join("/vdir", "dir")
		testDirPath := filepath.Join(mappedPath, "dir")
		err := os.MkdirAll(testDirPath, os.ModePerm)
		assert.NoError(t, err)
		err = os.Chmod(testDirPath, 0001)
		assert.NoError(t, err)
		err = c.updateQuotaAfterRename(fs, request.Filepath, request.Target, testDirPath, 0, -1, -1)
		assert.Error(t, err)
		err = os.Chmod(testDirPath, os.ModePerm)
		assert.NoError(t, err)
	}
	testFile1 := "/testfile1"
	request.Target = testFile1
	request.Filepath = path.Join("/vdir", "file")
	err = c.updateQuotaAfterRename(fs, request.Filepath, request.Target, filepath.Join(mappedPath, "file"), 0, -1, -1)
	assert.Error(t, err)
	err = os.WriteFile(filepath.Join(mappedPath, "file"), []byte("test content"), os.ModePerm)
	assert.NoError(t, err)
	request.Filepath = testFile1
	request.Target = path.Join("/vdir", "file")
	err = c.updateQuotaAfterRename(fs, request.Filepath, request.Target, filepath.Join(mappedPath, "file"), 12, -1, -1)
	assert.NoError(t, err)
	err = os.WriteFile(filepath.Join(user.GetHomeDir(), "testfile1"), []byte("test content"), os.ModePerm)
	assert.NoError(t, err)
	request.Target = testFile1
	request.Filepath = path.Join("/vdir", "file")
	err = c.updateQuotaAfterRename(fs, request.Filepath, request.Target, filepath.Join(mappedPath, "file"), 12, -1, -1)
	assert.NoError(t, err)
	request.Target = path.Join("/vdir1", "file")
	request.Filepath = path.Join("/vdir", "file")
	err = c.updateQuotaAfterRename(fs, request.Filepath, request.Target, filepath.Join(mappedPath, "file"), 12, -1, -1)
	assert.NoError(t, err)
	err = c.updateQuotaAfterRename(fs, request.Filepath, request.Target, filepath.Join(mappedPath, "file"), 12, 1, 100)
	assert.NoError(t, err)

	err = os.RemoveAll(mappedPath)
	assert.NoError(t, err)
	err = os.RemoveAll(user.GetHomeDir())
	assert.NoError(t, err)
}

func TestErrorsMapping(t *testing.T) {
	fs := vfs.NewOsFs("", os.TempDir(), "", nil)
	conn := NewBaseConnection("", ProtocolSFTP, "", "", dataprovider.User{BaseUser: sdk.BaseUser{HomeDir: os.TempDir()}})
	osErrorsProtocols := []string{ProtocolWebDAV, ProtocolFTP, ProtocolHTTP, ProtocolHTTPShare,
		ProtocolDataRetention, ProtocolOIDC, protocolEventAction}
	for _, protocol := range supportedProtocols {
		conn.SetProtocol(protocol)
		err := conn.GetFsError(fs, os.ErrNotExist)
		if protocol == ProtocolSFTP {
			assert.ErrorIs(t, err, sftp.ErrSSHFxNoSuchFile)
		} else if util.Contains(osErrorsProtocols, protocol) {
			assert.EqualError(t, err, os.ErrNotExist.Error())
		} else {
			assert.EqualError(t, err, ErrNotExist.Error())
		}
		err = conn.GetFsError(fs, os.ErrPermission)
		if protocol == ProtocolSFTP {
			assert.EqualError(t, err, sftp.ErrSSHFxPermissionDenied.Error())
		} else {
			assert.EqualError(t, err, ErrPermissionDenied.Error())
		}
		err = conn.GetFsError(fs, os.ErrClosed)
		if protocol == ProtocolSFTP {
			assert.ErrorIs(t, err, sftp.ErrSSHFxFailure)
		} else {
			assert.EqualError(t, err, ErrGenericFailure.Error())
		}
		err = conn.GetFsError(fs, ErrPermissionDenied)
		if protocol == ProtocolSFTP {
			assert.ErrorIs(t, err, sftp.ErrSSHFxFailure)
		} else {
			assert.EqualError(t, err, ErrPermissionDenied.Error())
		}
		err = conn.GetFsError(fs, vfs.ErrVfsUnsupported)
		if protocol == ProtocolSFTP {
			assert.EqualError(t, err, sftp.ErrSSHFxOpUnsupported.Error())
		} else {
			assert.EqualError(t, err, ErrOpUnsupported.Error())
		}
		err = conn.GetFsError(fs, vfs.ErrStorageSizeUnavailable)
		if protocol == ProtocolSFTP {
			assert.ErrorIs(t, err, sftp.ErrSSHFxOpUnsupported)
			assert.Contains(t, err.Error(), vfs.ErrStorageSizeUnavailable.Error())
		} else {
			assert.EqualError(t, err, vfs.ErrStorageSizeUnavailable.Error())
		}
		err = conn.GetQuotaExceededError()
		assert.True(t, conn.IsQuotaExceededError(err))
		err = conn.GetReadQuotaExceededError()
		if protocol == ProtocolSFTP {
			assert.ErrorIs(t, err, sftp.ErrSSHFxFailure)
			assert.Contains(t, err.Error(), ErrReadQuotaExceeded.Error())
		} else {
			assert.ErrorIs(t, err, ErrReadQuotaExceeded)
		}
		err = conn.GetNotExistError()
		assert.True(t, conn.IsNotExistError(err))
		err = conn.GetFsError(fs, nil)
		assert.NoError(t, err)
		err = conn.GetOpUnsupportedError()
		if protocol == ProtocolSFTP {
			assert.EqualError(t, err, sftp.ErrSSHFxOpUnsupported.Error())
		} else {
			assert.EqualError(t, err, ErrOpUnsupported.Error())
		}
		err = conn.GetFsError(fs, ErrShuttingDown)
		if protocol == ProtocolSFTP {
			assert.ErrorIs(t, err, sftp.ErrSSHFxFailure)
			assert.Contains(t, err.Error(), ErrShuttingDown.Error())
		} else {
			assert.EqualError(t, err, ErrShuttingDown.Error())
		}
	}
}

func TestMaxWriteSize(t *testing.T) {
	permissions := make(map[string][]string)
	permissions["/"] = []string{dataprovider.PermAny}
	user := dataprovider.User{
		BaseUser: sdk.BaseUser{
			Username:    userTestUsername,
			Permissions: permissions,
			HomeDir:     filepath.Clean(os.TempDir()),
		},
	}
	fs, err := user.GetFilesystem("123")
	assert.NoError(t, err)
	conn := NewBaseConnection("", ProtocolFTP, "", "", user)
	quotaResult := vfs.QuotaCheckResult{
		HasSpace: true,
	}
	size, err := conn.GetMaxWriteSize(quotaResult, false, 0, fs.IsUploadResumeSupported())
	assert.NoError(t, err)
	assert.Equal(t, int64(0), size)

	conn.User.Filters.MaxUploadFileSize = 100
	size, err = conn.GetMaxWriteSize(quotaResult, false, 0, fs.IsUploadResumeSupported())
	assert.NoError(t, err)
	assert.Equal(t, int64(100), size)

	quotaResult.QuotaSize = 1000
	size, err = conn.GetMaxWriteSize(quotaResult, false, 50, fs.IsUploadResumeSupported())
	assert.NoError(t, err)
	assert.Equal(t, int64(100), size)

	quotaResult.QuotaSize = 1000
	quotaResult.UsedSize = 990
	size, err = conn.GetMaxWriteSize(quotaResult, false, 50, fs.IsUploadResumeSupported())
	assert.NoError(t, err)
	assert.Equal(t, int64(60), size)

	quotaResult.QuotaSize = 0
	quotaResult.UsedSize = 0
	size, err = conn.GetMaxWriteSize(quotaResult, true, 100, fs.IsUploadResumeSupported())
	assert.True(t, conn.IsQuotaExceededError(err))
	assert.Equal(t, int64(0), size)

	size, err = conn.GetMaxWriteSize(quotaResult, true, 10, fs.IsUploadResumeSupported())
	assert.NoError(t, err)
	assert.Equal(t, int64(90), size)

	fs = newMockOsFs(true, fs.ConnectionID(), user.GetHomeDir(), "", nil)
	size, err = conn.GetMaxWriteSize(quotaResult, true, 100, fs.IsUploadResumeSupported())
	assert.EqualError(t, err, ErrOpUnsupported.Error())
	assert.Equal(t, int64(0), size)
}

func TestCheckParentDirsErrors(t *testing.T) {
	permissions := make(map[string][]string)
	permissions["/"] = []string{dataprovider.PermAny}
	user := dataprovider.User{
		BaseUser: sdk.BaseUser{
			Username:    userTestUsername,
			Permissions: permissions,
			HomeDir:     filepath.Clean(os.TempDir()),
		},
		FsConfig: vfs.Filesystem{
			Provider: sdk.CryptedFilesystemProvider,
		},
	}
	c := NewBaseConnection(xid.New().String(), ProtocolSFTP, "", "", user)
	err := c.CheckParentDirs("/a/dir")
	assert.Error(t, err)

	user.FsConfig.Provider = sdk.LocalFilesystemProvider
	user.VirtualFolders = nil
	user.VirtualFolders = append(user.VirtualFolders, vfs.VirtualFolder{
		BaseVirtualFolder: vfs.BaseVirtualFolder{
			FsConfig: vfs.Filesystem{
				Provider: sdk.CryptedFilesystemProvider,
			},
		},
		VirtualPath: "/vdir",
	})
	user.VirtualFolders = append(user.VirtualFolders, vfs.VirtualFolder{
		BaseVirtualFolder: vfs.BaseVirtualFolder{
			MappedPath: filepath.Clean(os.TempDir()),
		},
		VirtualPath: "/vdir/sub",
	})
	c = NewBaseConnection(xid.New().String(), ProtocolSFTP, "", "", user)
	err = c.CheckParentDirs("/vdir/sub/dir")
	assert.Error(t, err)

	user = dataprovider.User{
		BaseUser: sdk.BaseUser{
			Username:    userTestUsername,
			Permissions: permissions,
			HomeDir:     filepath.Clean(os.TempDir()),
		},
		FsConfig: vfs.Filesystem{
			Provider: sdk.S3FilesystemProvider,
			S3Config: vfs.S3FsConfig{
				BaseS3FsConfig: sdk.BaseS3FsConfig{
					Bucket:    "buck",
					Region:    "us-east-1",
					AccessKey: "key",
				},
				AccessSecret: kms.NewPlainSecret("s3secret"),
			},
		},
	}
	c = NewBaseConnection(xid.New().String(), ProtocolSFTP, "", "", user)
	err = c.CheckParentDirs("/a/dir")
	assert.NoError(t, err)

	user.VirtualFolders = append(user.VirtualFolders, vfs.VirtualFolder{
		BaseVirtualFolder: vfs.BaseVirtualFolder{
			MappedPath: filepath.Clean(os.TempDir()),
		},
		VirtualPath: "/local/dir",
	})

	c = NewBaseConnection(xid.New().String(), ProtocolSFTP, "", "", user)
	err = c.CheckParentDirs("/local/dir/sub-dir")
	assert.NoError(t, err)
	err = os.RemoveAll(filepath.Join(os.TempDir(), "sub-dir"))
	assert.NoError(t, err)
}

func TestErrorResolvePath(t *testing.T) {
	u := dataprovider.User{
		BaseUser: sdk.BaseUser{
			HomeDir: filepath.Join(os.TempDir(), "u"),
			Status:  1,
			Permissions: map[string][]string{
				"/": {dataprovider.PermAny},
			},
		},
	}
	u.FsConfig.Provider = sdk.GCSFilesystemProvider
	u.FsConfig.GCSConfig.Bucket = "test"
	u.FsConfig.GCSConfig.Credentials = kms.NewPlainSecret("invalid JSON for credentials")
	u.VirtualFolders = []vfs.VirtualFolder{
		{
			BaseVirtualFolder: vfs.BaseVirtualFolder{
				Name:       "f",
				MappedPath: filepath.Join(os.TempDir(), "f"),
			},
			VirtualPath: "/f",
		},
	}

	conn := NewBaseConnection("", ProtocolSFTP, "", "", u)
	err := conn.doRecursiveRemoveDirEntry("/vpath", nil)
	assert.Error(t, err)
	err = conn.checkCopy(vfs.NewFileInfo("name", true, 0, time.Unix(0, 0), false), nil, "/source", "/target")
	assert.Error(t, err)
	sourceFile := filepath.Join(os.TempDir(), "f", "source")
	err = os.MkdirAll(filepath.Dir(sourceFile), os.ModePerm)
	assert.NoError(t, err)
	err = os.WriteFile(sourceFile, []byte(""), 0666)
	assert.NoError(t, err)
	err = conn.checkCopy(vfs.NewFileInfo("name", true, 0, time.Unix(0, 0), false), nil, "/f/source", "/target")
	assert.Error(t, err)
	err = conn.checkCopy(vfs.NewFileInfo("source", false, 0, time.Unix(0, 0), false), vfs.NewFileInfo("target", true, 0, time.Unix(0, 0), false), "/f/source", "/f/target")
	assert.Error(t, err)
	err = os.RemoveAll(filepath.Dir(sourceFile))
	assert.NoError(t, err)
}

func TestConnectionKeepAlive(t *testing.T) {
	conn := NewBaseConnection("", ProtocolWebDAV, "", "", dataprovider.User{})
	lastActivity := conn.GetLastActivity()
	done := make(chan bool)
	go func() {
		time.Sleep(200 * time.Millisecond)
		close(done)
	}()
	keepConnectionAlive(conn, done, 50*time.Millisecond)
	assert.Greater(t, conn.GetLastActivity(), lastActivity)
}

func TestFsFileCopier(t *testing.T) {
	fs := vfs.Fs(&vfs.AzureBlobFs{})
	_, ok := fs.(vfs.FsFileCopier)
	assert.True(t, ok)
	fs = vfs.Fs(&vfs.OsFs{})
	_, ok = fs.(vfs.FsFileCopier)
	assert.False(t, ok)
	fs = vfs.Fs(&vfs.SFTPFs{})
	_, ok = fs.(vfs.FsFileCopier)
	assert.False(t, ok)
	fs = vfs.Fs(&vfs.GCSFs{})
	_, ok = fs.(vfs.FsFileCopier)
	assert.True(t, ok)
	fs = vfs.Fs(&vfs.S3Fs{})
	_, ok = fs.(vfs.FsFileCopier)
	assert.True(t, ok)
}

func TestFilePatterns(t *testing.T) {
	filters := dataprovider.UserFilters{
		BaseUserFilters: sdk.BaseUserFilters{
			FilePatterns: []sdk.PatternsFilter{
				{
					Path:            "/dir1",
					DenyPolicy:      sdk.DenyPolicyDefault,
					AllowedPatterns: []string{"*.jpg"},
				},
				{
					Path:            "/dir2",
					DenyPolicy:      sdk.DenyPolicyHide,
					AllowedPatterns: []string{"*.jpg"},
				},
				{
					Path:           "/dir3",
					DenyPolicy:     sdk.DenyPolicyDefault,
					DeniedPatterns: []string{"*.jpg"},
				},
				{
					Path:           "/dir4",
					DenyPolicy:     sdk.DenyPolicyHide,
					DeniedPatterns: []string{"*"},
				},
			},
		},
	}
	virtualFolders := []vfs.VirtualFolder{
		{
			VirtualPath: "/dir1/vdir1",
		},
		{
			VirtualPath: "/dir1/vdir2",
		},
		{
			VirtualPath: "/dir1/vdir3",
		},
		{
			VirtualPath: "/dir2/vdir1",
		},
		{
			VirtualPath: "/dir2/vdir2",
		},
		{
			VirtualPath: "/dir2/vdir3.jpg",
		},
	}
	user := dataprovider.User{
		Filters:        filters,
		VirtualFolders: virtualFolders,
	}

	dirContents := []os.FileInfo{
		vfs.NewFileInfo("file1.txt", false, 123, time.Now(), false),
		vfs.NewFileInfo("file1.jpg", false, 123, time.Now(), false),
	}
	// dirContents are modified in place, we need to redefine them each time
	filtered := user.FilterListDir(dirContents, "/dir1")
	assert.Len(t, filtered, 5)

	dirContents = []os.FileInfo{
		vfs.NewFileInfo("file1.txt", false, 123, time.Now(), false),
		vfs.NewFileInfo("file1.jpg", false, 123, time.Now(), false),
	}
	filtered = user.FilterListDir(dirContents, "/dir1/vdir1")
	assert.Len(t, filtered, 2)

	dirContents = []os.FileInfo{
		vfs.NewFileInfo("file1.txt", false, 123, time.Now(), false),
		vfs.NewFileInfo("file1.jpg", false, 123, time.Now(), false),
	}
	filtered = user.FilterListDir(dirContents, "/dir2/vdir2")
	require.Len(t, filtered, 1)
	assert.Equal(t, "file1.jpg", filtered[0].Name())

	dirContents = []os.FileInfo{
		vfs.NewFileInfo("file1.txt", false, 123, time.Now(), false),
		vfs.NewFileInfo("file1.jpg", false, 123, time.Now(), false),
	}
	filtered = user.FilterListDir(dirContents, "/dir2/vdir2/sub")
	require.Len(t, filtered, 1)
	assert.Equal(t, "file1.jpg", filtered[0].Name())

	res, _ := user.IsFileAllowed("/dir1/vdir1/file.txt")
	assert.False(t, res)
	res, _ = user.IsFileAllowed("/dir1/vdir1/sub/file.txt")
	assert.False(t, res)
	res, _ = user.IsFileAllowed("/dir1/vdir1/file.jpg")
	assert.True(t, res)
	res, _ = user.IsFileAllowed("/dir1/vdir1/sub/file.jpg")
	assert.True(t, res)
	res, _ = user.IsFileAllowed("/dir3/file.jpg")
	assert.False(t, res)
	res, _ = user.IsFileAllowed("/dir3/dir1/file.jpg")
	assert.False(t, res)
	res, _ = user.IsFileAllowed("/dir3/dir1/sub/file.jpg")
	assert.False(t, res)
	res, _ = user.IsFileAllowed("/dir4/file.jpg")
	assert.False(t, res)
	res, _ = user.IsFileAllowed("/dir4/dir1/sub/file.jpg")
	assert.False(t, res)

	dirContents = []os.FileInfo{
		vfs.NewFileInfo("file1.txt", false, 123, time.Now(), false),
		vfs.NewFileInfo("file1.jpg", false, 123, time.Now(), false),
	}
	filtered = user.FilterListDir(dirContents, "/dir4")
	require.Len(t, filtered, 0)

	dirContents = []os.FileInfo{
		vfs.NewFileInfo("file1.txt", false, 123, time.Now(), false),
		vfs.NewFileInfo("file1.jpg", false, 123, time.Now(), false),
	}
	filtered = user.FilterListDir(dirContents, "/dir4/vdir2/sub")
	require.Len(t, filtered, 0)

	dirContents = []os.FileInfo{
		vfs.NewFileInfo("file1.txt", false, 123, time.Now(), false),
		vfs.NewFileInfo("file1.jpg", false, 123, time.Now(), false),
	}

	filtered = user.FilterListDir(dirContents, "/dir2")
	assert.Len(t, filtered, 2)

	dirContents = []os.FileInfo{
		vfs.NewFileInfo("file1.txt", false, 123, time.Now(), false),
		vfs.NewFileInfo("file1.jpg", false, 123, time.Now(), false),
	}

	filtered = user.FilterListDir(dirContents, "/dir4")
	assert.Len(t, filtered, 0)

	dirContents = []os.FileInfo{
		vfs.NewFileInfo("file1.txt", false, 123, time.Now(), false),
		vfs.NewFileInfo("file1.jpg", false, 123, time.Now(), false),
	}

	filtered = user.FilterListDir(dirContents, "/dir4/sub")
	assert.Len(t, filtered, 0)

	dirContents = []os.FileInfo{
		vfs.NewFileInfo("file1.txt", false, 123, time.Now(), false),
		vfs.NewFileInfo("vdir3.jpg", false, 123, time.Now(), false),
	}

	filtered = user.FilterListDir(dirContents, "/dir1")
	assert.Len(t, filtered, 5)

	filtered = user.FilterListDir(dirContents, "/dir2")
	if assert.Len(t, filtered, 1) {
		assert.True(t, filtered[0].IsDir())
	}

	user.VirtualFolders = nil
	dirContents = []os.FileInfo{
		vfs.NewFileInfo("file1.txt", false, 123, time.Now(), false),
		vfs.NewFileInfo("vdir3.jpg", false, 123, time.Now(), false),
	}
	filtered = user.FilterListDir(dirContents, "/dir1")
	assert.Len(t, filtered, 2)

	dirContents = []os.FileInfo{
		vfs.NewFileInfo("file1.txt", false, 123, time.Now(), false),
		vfs.NewFileInfo("vdir3.jpg", false, 123, time.Now(), false),
	}
	filtered = user.FilterListDir(dirContents, "/dir2")
	if assert.Len(t, filtered, 1) {
		assert.False(t, filtered[0].IsDir())
	}

	dirContents = []os.FileInfo{
		vfs.NewFileInfo("file1.jpg", false, 123, time.Now(), false),
		vfs.NewFileInfo("file1.txt", false, 123, time.Now(), false),
		vfs.NewFileInfo("file2.txt", false, 123, time.Now(), false),
		vfs.NewFileInfo("vdir3.jpg", false, 123, time.Now(), false),
	}
	filtered = user.FilterListDir(dirContents, "/dir2")
	if assert.Len(t, filtered, 2) {
		assert.False(t, filtered[0].IsDir())
		assert.False(t, filtered[1].IsDir())
	}

	user.VirtualFolders = virtualFolders
	user.Filters = filters
	filtered = user.FilterListDir(nil, "/dir1")
	assert.Len(t, filtered, 3)
	filtered = user.FilterListDir(nil, "/dir2")
	assert.Len(t, filtered, 1)

	dirContents = []os.FileInfo{
		vfs.NewFileInfo("file1.jPg", false, 123, time.Now(), false),
		vfs.NewFileInfo("file1.txt", false, 123, time.Now(), false),
		vfs.NewFileInfo("file2.txt", false, 123, time.Now(), false),
		vfs.NewFileInfo("vdir3.jpg", false, 456, time.Now(), false),
	}
	filtered = user.FilterListDir(dirContents, "/dir2")
	assert.Len(t, filtered, 2)

	user = dataprovider.User{
		Filters: dataprovider.UserFilters{
			BaseUserFilters: sdk.BaseUserFilters{
				FilePatterns: []sdk.PatternsFilter{
					{
						Path:            "/dir3",
						AllowedPatterns: []string{"ic35"},
						DeniedPatterns:  []string{"*"},
						DenyPolicy:      sdk.DenyPolicyHide,
					},
				},
			},
		},
	}
	dirContents = []os.FileInfo{
		vfs.NewFileInfo("file1.jpg", false, 123, time.Now(), false),
		vfs.NewFileInfo("file1.txt", false, 123, time.Now(), false),
		vfs.NewFileInfo("file2.txt", false, 123, time.Now(), false),
		vfs.NewFileInfo("vdir3.jpg", false, 456, time.Now(), false),
	}
	filtered = user.FilterListDir(dirContents, "/dir3")
	assert.Len(t, filtered, 0)

	dirContents = nil
	for i := 0; i < 100; i++ {
		dirContents = append(dirContents, vfs.NewFileInfo(fmt.Sprintf("ic%02d", i), i%2 == 0, int64(i), time.Now(), false))
	}
	dirContents = append(dirContents, vfs.NewFileInfo("ic350", false, 123, time.Now(), false))
	dirContents = append(dirContents, vfs.NewFileInfo(".ic35", false, 123, time.Now(), false))
	dirContents = append(dirContents, vfs.NewFileInfo("ic35.", false, 123, time.Now(), false))
	dirContents = append(dirContents, vfs.NewFileInfo("*ic35", false, 123, time.Now(), false))
	dirContents = append(dirContents, vfs.NewFileInfo("ic35*", false, 123, time.Now(), false))
	dirContents = append(dirContents, vfs.NewFileInfo("ic35.*", false, 123, time.Now(), false))
	dirContents = append(dirContents, vfs.NewFileInfo("file.jpg", false, 123, time.Now(), false))

	filtered = user.FilterListDir(dirContents, "/dir3")
	require.Len(t, filtered, 1)
	assert.Equal(t, "ic35", filtered[0].Name())

	dirContents = []os.FileInfo{
		vfs.NewFileInfo("file1.jpg", false, 123, time.Now(), false),
		vfs.NewFileInfo("file1.txt", false, 123, time.Now(), false),
		vfs.NewFileInfo("file2.txt", false, 123, time.Now(), false),
	}
	filtered = user.FilterListDir(dirContents, "/dir3/ic36")
	require.Len(t, filtered, 0)

	dirContents = []os.FileInfo{
		vfs.NewFileInfo("file1.jpg", false, 123, time.Now(), false),
		vfs.NewFileInfo("file1.txt", false, 123, time.Now(), false),
		vfs.NewFileInfo("file2.txt", false, 123, time.Now(), false),
	}
	filtered = user.FilterListDir(dirContents, "/dir3/ic35")
	require.Len(t, filtered, 3)

	dirContents = []os.FileInfo{
		vfs.NewFileInfo("file1.jpg", false, 123, time.Now(), false),
		vfs.NewFileInfo("file1.txt", false, 123, time.Now(), false),
		vfs.NewFileInfo("file2.txt", false, 123, time.Now(), false),
	}
	filtered = user.FilterListDir(dirContents, "/dir3/ic35/sub")
	require.Len(t, filtered, 3)

	res, _ = user.IsFileAllowed("/dir3/file.txt")
	assert.False(t, res)
	res, _ = user.IsFileAllowed("/dir3/ic35a")
	assert.False(t, res)
	res, policy := user.IsFileAllowed("/dir3/ic35a/file")
	assert.False(t, res)
	assert.Equal(t, sdk.DenyPolicyHide, policy)
	res, _ = user.IsFileAllowed("/dir3/ic35")
	assert.True(t, res)
	res, _ = user.IsFileAllowed("/dir3/ic35/file.jpg")
	assert.True(t, res)
	res, _ = user.IsFileAllowed("/dir3/ic35/file.txt")
	assert.True(t, res)
	res, _ = user.IsFileAllowed("/dir3/ic35/sub/file.txt")
	assert.True(t, res)

	dirContents = []os.FileInfo{
		vfs.NewFileInfo("file1.jpg", false, 123, time.Now(), false),
		vfs.NewFileInfo("file1.txt", false, 123, time.Now(), false),
		vfs.NewFileInfo("file2.txt", false, 123, time.Now(), false),
	}
	filtered = user.FilterListDir(dirContents, "/dir3/ic35/sub")
	require.Len(t, filtered, 3)

	user.Filters.FilePatterns = append(user.Filters.FilePatterns, sdk.PatternsFilter{
		Path:            "/dir3/ic35/sub1",
		AllowedPatterns: []string{"*.jpg"},
		DenyPolicy:      sdk.DenyPolicyDefault,
	})
	user.Filters.FilePatterns = append(user.Filters.FilePatterns, sdk.PatternsFilter{
		Path:           "/dir3/ic35/sub2",
		DeniedPatterns: []string{"*.jpg"},
		DenyPolicy:     sdk.DenyPolicyHide,
	})

	dirContents = []os.FileInfo{
		vfs.NewFileInfo("file1.jpg", false, 123, time.Now(), false),
		vfs.NewFileInfo("file1.txt", false, 123, time.Now(), false),
		vfs.NewFileInfo("file2.txt", false, 123, time.Now(), false),
	}
	filtered = user.FilterListDir(dirContents, "/dir3/ic35/sub1")
	require.Len(t, filtered, 3)

	dirContents = []os.FileInfo{
		vfs.NewFileInfo("file1.jpg", false, 123, time.Now(), false),
		vfs.NewFileInfo("file1.txt", false, 123, time.Now(), false),
		vfs.NewFileInfo("file2.txt", false, 123, time.Now(), false),
	}
	filtered = user.FilterListDir(dirContents, "/dir3/ic35/sub2")
	require.Len(t, filtered, 2)

	dirContents = []os.FileInfo{
		vfs.NewFileInfo("file1.jpg", false, 123, time.Now(), false),
		vfs.NewFileInfo("file1.txt", false, 123, time.Now(), false),
		vfs.NewFileInfo("file2.txt", false, 123, time.Now(), false),
	}
	filtered = user.FilterListDir(dirContents, "/dir3/ic35/sub2/sub1")
	require.Len(t, filtered, 2)

	res, _ = user.IsFileAllowed("/dir3/ic35/file.jpg")
	assert.True(t, res)
	res, _ = user.IsFileAllowed("/dir3/ic35/file.txt")
	assert.True(t, res)
	res, _ = user.IsFileAllowed("/dir3/ic35/sub/dir/file.txt")
	assert.True(t, res)
	res, _ = user.IsFileAllowed("/dir3/ic35/sub/dir/file.jpg")
	assert.True(t, res)

	res, _ = user.IsFileAllowed("/dir3/ic35/sub1/file.jpg")
	assert.True(t, res)
	res, _ = user.IsFileAllowed("/dir3/ic35/sub1/file.txt")
	assert.False(t, res)
	res, _ = user.IsFileAllowed("/dir3/ic35/sub1/sub/file.jpg")
	assert.True(t, res)
	res, _ = user.IsFileAllowed("/dir3/ic35/sub1/sub2/file.txt")
	assert.False(t, res)

	res, _ = user.IsFileAllowed("/dir3/ic35/sub2/file.jpg")
	assert.False(t, res)
	res, _ = user.IsFileAllowed("/dir3/ic35/sub2/file.txt")
	assert.True(t, res)
	res, _ = user.IsFileAllowed("/dir3/ic35/sub2/sub/file.jpg")
	assert.False(t, res)
	res, _ = user.IsFileAllowed("/dir3/ic35/sub2/sub1/file.txt")
	assert.True(t, res)

	user.Filters.FilePatterns = append(user.Filters.FilePatterns, sdk.PatternsFilter{
		Path:           "/dir3/ic35",
		DeniedPatterns: []string{"*.txt"},
		DenyPolicy:     sdk.DenyPolicyHide,
	})
	res, _ = user.IsFileAllowed("/dir3/ic35/file.jpg")
	assert.True(t, res)
	res, _ = user.IsFileAllowed("/dir3/ic35/file.txt")
	assert.False(t, res)
	res, _ = user.IsFileAllowed("/dir3/ic35/adir/sub/file.jpg")
	assert.True(t, res)
	res, _ = user.IsFileAllowed("/dir3/ic35/adir/file.txt")
	assert.False(t, res)

	res, _ = user.IsFileAllowed("/dir3/ic35/sub2/file.jpg")
	assert.False(t, res)
	res, _ = user.IsFileAllowed("/dir3/ic35/sub2/file.txt")
	assert.True(t, res)
	res, _ = user.IsFileAllowed("/dir3/ic35/sub2/sub/file.jpg")
	assert.False(t, res)
	res, _ = user.IsFileAllowed("/dir3/ic35/sub2/sub1/file.txt")
	assert.True(t, res)

	dirContents = []os.FileInfo{
		vfs.NewFileInfo("file1.jpg", false, 123, time.Now(), false),
		vfs.NewFileInfo("file1.txt", false, 123, time.Now(), false),
		vfs.NewFileInfo("file2.txt", false, 123, time.Now(), false),
	}
	filtered = user.FilterListDir(dirContents, "/dir3/ic35")
	require.Len(t, filtered, 1)

	dirContents = []os.FileInfo{
		vfs.NewFileInfo("file1.jpg", false, 123, time.Now(), false),
		vfs.NewFileInfo("file1.txt", false, 123, time.Now(), false),
		vfs.NewFileInfo("file2.txt", false, 123, time.Now(), false),
	}
	filtered = user.FilterListDir(dirContents, "/dir3/ic35/abc")
	require.Len(t, filtered, 1)
}
