package common

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

	"github.com/pkg/sftp"
	"github.com/stretchr/testify/assert"

	"github.com/drakkan/sftpgo/v2/dataprovider"
	"github.com/drakkan/sftpgo/v2/sdk"
	"github.com/drakkan/sftpgo/v2/vfs"
)

// MockOsFs mockable OsFs
type MockOsFs struct {
	vfs.Fs
	hasVirtualFolders bool
}

// Name returns the name for the Fs implementation
func (fs MockOsFs) Name() string {
	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 newMockOsFs(hasVirtualFolders bool, connectionID, rootDir string) vfs.Fs {
	return &MockOsFs{
		Fs:                vfs.NewOsFs(connectionID, rootDir, ""),
		hasVirtualFolders: hasVirtualFolders,
	}
}

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(), "")
	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())
	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)

	Config.SetstatMode = oldSetStatMode
}

func TestRecursiveRenameWalkError(t *testing.T) {
	fs := vfs.NewOsFs("", os.TempDir(), "")
	conn := NewBaseConnection("", ProtocolWebDAV, "", "", dataprovider.User{})
	err := conn.checkRecursiveRenameDirPermissions(fs, fs, "/source", "/target")
	assert.ErrorIs(t, err, os.ErrNotExist)
}

func TestCrossRenameFsErrors(t *testing.T) {
	fs := vfs.NewOsFs("", os.TempDir(), "")
	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(), "")
	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"
	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.PermCreateDirs, dataprovider.PermUpload, dataprovider.PermCreateSymlinks,
		dataprovider.PermDeleteFiles, dataprovider.PermDeleteDirs}
	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.PermCreateDirs, dataprovider.PermUpload, dataprovider.PermDeleteFiles}
	assert.False(t, conn.hasRenamePerms(src, target, info))
	u.Permissions["/"] = []string{dataprovider.PermCreateDirs, dataprovider.PermUpload, dataprovider.PermDeleteDirs}
	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))
}

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)
		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)
	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)
	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)
	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)
	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(), "")
	conn := NewBaseConnection("", ProtocolSFTP, "", "", dataprovider.User{BaseUser: sdk.BaseUser{HomeDir: os.TempDir()}})
	for _, protocol := range supportedProtocols {
		conn.SetProtocol(protocol)
		err := conn.GetFsError(fs, os.ErrNotExist)
		if protocol == ProtocolSFTP {
			assert.ErrorIs(t, err, sftp.ErrSSHFxNoSuchFile)
		} else if protocol == ProtocolWebDAV || protocol == ProtocolFTP || protocol == ProtocolHTTP {
			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)
			assert.Contains(t, err.Error(), os.ErrClosed.Error())
		} else {
			assert.EqualError(t, err, ErrGenericFailure.Error())
		}
		err = conn.GetFsError(fs, ErrPermissionDenied)
		if protocol == ProtocolSFTP {
			assert.ErrorIs(t, err, sftp.ErrSSHFxFailure)
			assert.Contains(t, err.Error(), ErrPermissionDenied.Error())
		} 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.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())
		}
	}
}

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())
	size, err = conn.GetMaxWriteSize(quotaResult, true, 100, fs.IsUploadResumeSupported())
	assert.EqualError(t, err, ErrOpUnsupported.Error())
	assert.Equal(t, int64(0), size)
}
