// Copyright (c) Acrosync LLC. All rights reserved.
// Free for personal use and commercial trial
// Commercial use requires per-user licenses available from https://duplicacy.com

package duplicacy

import (
	"crypto/sha256"
	"encoding/hex"
	"encoding/json"
	"flag"
	"fmt"
	"io/ioutil"
	"os"
	"path"
	"runtime/debug"
	"strconv"
	"testing"
	"time"

	crypto_rand "crypto/rand"
	"math/rand"
)

var testStorageName = flag.String("storage", "", "the test storage to use")
var testRateLimit = flag.Int("limit-rate", 0, "maximum transfer speed in kbytes/sec")
var testQuickMode = flag.Bool("quick", false, "quick test")
var testThreads = flag.Int("threads", 1, "number of downloading/uploading threads")
var testFixedChunkSize = flag.Bool("fixed-chunk-size", false, "fixed chunk size")
var testRSAEncryption = flag.Bool("rsa", false, "enable RSA encryption")
var testErasureCoding = flag.Bool("erasure-coding", false, "enable Erasure Coding")

func loadStorage(localStoragePath string, threads int) (Storage, error) {

	if *testStorageName == "" || *testStorageName == "file" {
		storage, err := CreateFileStorage(localStoragePath, false, threads)
		if storage != nil {
			// Use a read level of at least 2 because this will catch more errors than a read level of 1.
			storage.SetDefaultNestingLevels([]int{2, 3}, 2)
		}
		return storage, err
	}

	description, err := ioutil.ReadFile("test_storage.conf")
	if err != nil {
		return nil, err
	}

	configs := make(map[string]map[string]string)

	err = json.Unmarshal(description, &configs)
	if err != nil {
		return nil, err
	}

	config, found := configs[*testStorageName]
	if !found {
		return nil, fmt.Errorf("No storage named '%s' found", *testStorageName)
	}

	if *testStorageName == "flat" {
		storage, err := CreateFileStorage(localStoragePath, false, threads)
		storage.SetDefaultNestingLevels([]int{2, 3}, 2)
		return storage, err
	} else if *testStorageName == "samba" {
		storage, err := CreateFileStorage(localStoragePath, true, threads)
		storage.SetDefaultNestingLevels([]int{2, 3}, 2)
		return storage, err
	} else if *testStorageName == "sftp" {
		port, _ := strconv.Atoi(config["port"])
		storage, err := CreateSFTPStorageWithPassword(config["server"], port, config["username"], config["directory"], 2, config["password"], threads)
		storage.SetDefaultNestingLevels([]int{2, 3}, 2)
		return storage, err
	} else if *testStorageName == "s3" {
		storage, err := CreateS3Storage(config["region"], config["endpoint"], config["bucket"], config["directory"], config["access_key"], config["secret_key"], threads, true, false)
		storage.SetDefaultNestingLevels([]int{2, 3}, 2)
		return storage, err
	} else if *testStorageName == "wasabi" {
		storage, err := CreateWasabiStorage(config["region"], config["endpoint"], config["bucket"], config["directory"], config["access_key"], config["secret_key"], threads)
		storage.SetDefaultNestingLevels([]int{2, 3}, 2)
		return storage, err
	} else if *testStorageName == "s3c" {
		storage, err := CreateS3CStorage(config["region"], config["endpoint"], config["bucket"], config["directory"], config["access_key"], config["secret_key"], threads)
		storage.SetDefaultNestingLevels([]int{2, 3}, 2)
		return storage, err
	} else if *testStorageName == "digitalocean" {
		storage, err := CreateS3CStorage(config["region"], config["endpoint"], config["bucket"], config["directory"], config["access_key"], config["secret_key"], threads)
		storage.SetDefaultNestingLevels([]int{2, 3}, 2)
		return storage, err
	} else if *testStorageName == "minio" {
		storage, err := CreateS3Storage(config["region"], config["endpoint"], config["bucket"], config["directory"], config["access_key"], config["secret_key"], threads, false, true)
		storage.SetDefaultNestingLevels([]int{2, 3}, 2)
		return storage, err
	} else if *testStorageName == "minios" {
		storage, err := CreateS3Storage(config["region"], config["endpoint"], config["bucket"], config["directory"], config["access_key"], config["secret_key"], threads, true, true)
		storage.SetDefaultNestingLevels([]int{2, 3}, 2)
		return storage, err
	} else if *testStorageName == "dropbox" {
		storage, err := CreateDropboxStorage(config["token"], config["directory"], 1, threads)
		storage.SetDefaultNestingLevels([]int{2, 3}, 2)
		return storage, err
	} else if *testStorageName == "b2" {
		storage, err := CreateB2Storage(config["account"], config["key"], "", config["bucket"], config["directory"], threads)
		storage.SetDefaultNestingLevels([]int{2, 3}, 2)
		return storage, err
	} else if *testStorageName == "gcs-s3" {
		storage, err := CreateS3Storage(config["region"], config["endpoint"], config["bucket"], config["directory"], config["access_key"], config["secret_key"], threads, true, false)
		storage.SetDefaultNestingLevels([]int{2, 3}, 2)
		return storage, err
	} else if *testStorageName == "gcs" {
		storage, err := CreateGCSStorage(config["token_file"], config["bucket"], config["directory"], threads)
		storage.SetDefaultNestingLevels([]int{2, 3}, 2)
		return storage, err
	} else if *testStorageName == "gcs-sa" {
		storage, err := CreateGCSStorage(config["token_file"], config["bucket"], config["directory"], threads)
		storage.SetDefaultNestingLevels([]int{2, 3}, 2)
		return storage, err
	} else if *testStorageName == "azure" {
		storage, err := CreateAzureStorage(config["account"], config["key"], config["container"], threads)
		storage.SetDefaultNestingLevels([]int{2, 3}, 2)
		return storage, err
	} else if *testStorageName == "acd" {
		storage, err := CreateACDStorage(config["token_file"], config["storage_path"], threads)
		storage.SetDefaultNestingLevels([]int{2, 3}, 2)
		return storage, err
	} else if *testStorageName == "gcd" {
		storage, err := CreateGCDStorage(config["token_file"], "", config["storage_path"], threads)
		storage.SetDefaultNestingLevels([]int{2, 3}, 2)
		return storage, err
	} else if *testStorageName == "gcd-shared" {
		storage, err := CreateGCDStorage(config["token_file"], config["drive"], config["storage_path"], threads)
		storage.SetDefaultNestingLevels([]int{2, 3}, 2)
		return storage, err
	} else if *testStorageName == "gcd-impersonate" {
		storage, err := CreateGCDStorage(config["token_file"], config["drive"], config["storage_path"], threads)
		storage.SetDefaultNestingLevels([]int{2, 3}, 2)
		return storage, err
	} else if *testStorageName == "one" {
		storage, err := CreateOneDriveStorage(config["token_file"], false, config["storage_path"], threads)
		storage.SetDefaultNestingLevels([]int{2, 3}, 2)
		return storage, err
	} else if *testStorageName == "odb" {
		storage, err := CreateOneDriveStorage(config["token_file"], true, config["storage_path"], threads)
		storage.SetDefaultNestingLevels([]int{2, 3}, 2)
		return storage, err
	} else if *testStorageName == "one" {
		storage, err := CreateOneDriveStorage(config["token_file"], false, config["storage_path"], threads)
		storage.SetDefaultNestingLevels([]int{2, 3}, 2)
		return storage, err
	} else if *testStorageName == "hubic" {
		storage, err := CreateHubicStorage(config["token_file"], config["storage_path"], threads)
		storage.SetDefaultNestingLevels([]int{2, 3}, 2)
		return storage, err
	} else if *testStorageName == "memset" {
		storage, err := CreateSwiftStorage(config["storage_url"], config["key"], threads)
		storage.SetDefaultNestingLevels([]int{2, 3}, 2)
		return storage, err
	} else if *testStorageName == "pcloud" || *testStorageName == "box" {
		storage, err := CreateWebDAVStorage(config["host"], 0, config["username"], config["password"], config["storage_path"], false, threads)
		if err != nil {
			return nil, err
		}
		storage.SetDefaultNestingLevels([]int{2, 3}, 2)
		return storage, err
	} else if *testStorageName == "fabric" {
		storage, err := CreateFileFabricStorage(config["endpoint"], config["token"], config["storage_path"], threads)
		if err != nil {
			return nil, err
		}
		storage.SetDefaultNestingLevels([]int{2, 3}, 2)
		return storage, err
	} else if *testStorageName == "storj" {
		storage, err := CreateStorjStorage(config["satellite"], config["key"], config["passphrase"], config["bucket"], config["storage_path"], threads)
		if err != nil {
			return nil, err
		}
		storage.SetDefaultNestingLevels([]int{2, 3}, 2)
		return storage, err
	}

	return nil, fmt.Errorf("Invalid storage named: %s", *testStorageName)
}

func cleanStorage(storage Storage) {

	directories := make([]string, 0, 1024)
	snapshots := make([]string, 0, 1024)

	directories = append(directories, "snapshots/")

	LOG_INFO("STORAGE_LIST", "Listing snapshots in the storage")
	for len(directories) > 0 {

		dir := directories[len(directories)-1]
		directories = directories[:len(directories)-1]

		files, _, err := storage.ListFiles(0, dir)
		if err != nil {
			LOG_ERROR("STORAGE_LIST", "Failed to list the directory %s: %v", dir, err)
			return
		}

		for _, file := range files {
			if len(file) > 0 && file[len(file)-1] == '/' {
				directories = append(directories, dir+file)
			} else {
				snapshots = append(snapshots, dir+file)
			}
		}
	}

	LOG_INFO("STORAGE_DELETE", "Deleting %d snapshots in the storage", len(snapshots))
	for _, snapshot := range snapshots {
		storage.DeleteFile(0, snapshot)
	}

	for _, chunk := range listChunks(storage) {
		storage.DeleteFile(0, "chunks/"+chunk)
	}

	storage.DeleteFile(0, "config")

	return
}

func listChunks(storage Storage) (chunks []string) {

	directories := make([]string, 0, 1024)

	directories = append(directories, "chunks/")

	for len(directories) > 0 {

		dir := directories[len(directories)-1]
		directories = directories[:len(directories)-1]

		files, _, err := storage.ListFiles(0, dir)
		if err != nil {
			LOG_ERROR("CHUNK_LIST", "Failed to list the directory %s: %v", dir, err)
			return nil
		}

		for _, file := range files {
			if len(file) > 0 && file[len(file)-1] == '/' {
				directories = append(directories, dir+file)
			} else {
				chunk := dir + file
				chunk = chunk[len("chunks/"):]
				chunks = append(chunks, chunk)
			}
		}
	}

	return
}

func moveChunk(t *testing.T, storage Storage, chunkID string, isFossil bool, delay int) {

	filePath, exist, _, err := storage.FindChunk(0, chunkID, isFossil)

	if err != nil {
		t.Errorf("Error find chunk %s: %v", chunkID, err)
		return
	}

	to := filePath + ".fsl"
	if isFossil {
		to = filePath[:len(filePath)-len(".fsl")]
	}

	err = storage.MoveFile(0, filePath, to)
	if err != nil {
		t.Errorf("Error renaming file %s to %s: %v", filePath, to, err)
	}

	time.Sleep(time.Duration(delay) * time.Second)

	_, exist, _, err = storage.FindChunk(0, chunkID, isFossil)
	if err != nil {
		t.Errorf("Error get file info for chunk %s: %v", chunkID, err)
	}

	if exist {
		t.Errorf("File %s still exists after renaming", filePath)
	}

	_, exist, _, err = storage.FindChunk(0, chunkID, !isFossil)
	if err != nil {
		t.Errorf("Error get file info for %s: %v", to, err)
	}

	if !exist {
		t.Errorf("File %s doesn't exist", to)
	}

}

func TestStorage(t *testing.T) {

	rand.Seed(time.Now().UnixNano())
	setTestingT(t)
	SetLoggingLevel(INFO)

	defer func() {
		if r := recover(); r != nil {
			switch e := r.(type) {
			case Exception:
				t.Errorf("%s %s", e.LogID, e.Message)
				debug.PrintStack()
			default:
				t.Errorf("%v", e)
				debug.PrintStack()
			}
		}
	}()

	testDir := path.Join(os.TempDir(), "duplicacy_test", "storage_test")
	os.RemoveAll(testDir)
	os.MkdirAll(testDir, 0700)

	LOG_INFO("STORAGE_TEST", "storage: %s", *testStorageName)

	threads := 8
	storage, err := loadStorage(testDir, threads)
	if err != nil {
		t.Errorf("Failed to create storage: %v", err)
		return
	}
	storage.EnableTestMode()
	storage.SetRateLimits(*testRateLimit, *testRateLimit)

	delay := 0
	if _, ok := storage.(*ACDStorage); ok {
		delay = 5
	}
	if _, ok := storage.(*HubicStorage); ok {
		delay = 2
	}

	for _, dir := range []string{"chunks", "snapshots"} {
		err = storage.CreateDirectory(0, dir)
		if err != nil {
			t.Errorf("Failed to create directory %s: %v", dir, err)
			return
		}
	}

	storage.CreateDirectory(0, "snapshots/repository1")
	storage.CreateDirectory(0, "snapshots/repository2")

	storage.CreateDirectory(0, "shared")

	// Upload to the same directory by multiple goroutines
	count := threads
	finished := make(chan int, count)
	for i := 0; i < count; i++ {
		go func(threadIndex int, name string) {
			err := storage.UploadFile(threadIndex, name, []byte("this is a test file"))
			if err != nil {
				t.Errorf("Error to upload '%s': %v", name, err)
			}
			finished <- 0
		}(i, fmt.Sprintf("shared/a/b/c/%d", i))
	}

	for i := 0; i < count; i++ {
		<-finished
	}

	for i := 0; i < count; i++ {
		storage.DeleteFile(0, fmt.Sprintf("shared/a/b/c/%d", i))
	}
	storage.DeleteFile(0, "shared/a/b/c")
	storage.DeleteFile(0, "shared/a/b")
	storage.DeleteFile(0, "shared/a")

	time.Sleep(time.Duration(delay) * time.Second)
	{

		// Upload fake snapshot files so that for storages having no concept of directories,
		// ListFiles("snapshots") still returns correct snapshot IDs.

		// Create a random file not a text file to make ACD Storage happy.
		content := make([]byte, 100)
		_, err = crypto_rand.Read(content)
		if err != nil {
			t.Errorf("Error generating random content: %v", err)
			return
		}

		err = storage.UploadFile(0, "snapshots/repository1/1", content)
		if err != nil {
			t.Errorf("Error to upload snapshots/repository1/1: %v", err)
		}

		err = storage.UploadFile(0, "snapshots/repository2/1", content)
		if err != nil {
			t.Errorf("Error to upload snapshots/repository2/1: %v", err)
		}
	}

	time.Sleep(time.Duration(delay) * time.Second)

	snapshotDirs, _, err := storage.ListFiles(0, "snapshots/")
	if err != nil {
		t.Errorf("Failed to list snapshot ids: %v", err)
		return
	}

	snapshotIDs := []string{}
	for _, snapshotDir := range snapshotDirs {
		if len(snapshotDir) > 0 && snapshotDir[len(snapshotDir)-1] == '/' {
			snapshotIDs = append(snapshotIDs, snapshotDir[:len(snapshotDir)-1])
		}
	}

	if len(snapshotIDs) < 2 {
		t.Errorf("Snapshot directories not created")
		return
	}

	for _, snapshotID := range snapshotIDs {
		snapshots, _, err := storage.ListFiles(0, "snapshots/"+snapshotID)
		if err != nil {
			t.Errorf("Failed to list snapshots for %s: %v", snapshotID, err)
			return
		}
		for _, snapshot := range snapshots {
			storage.DeleteFile(0, "snapshots/"+snapshotID+"/"+snapshot)
		}
	}

	time.Sleep(time.Duration(delay) * time.Second)

	storage.DeleteFile(0, "config")

	for _, file := range []string{"snapshots/repository1/1", "snapshots/repository2/1"} {
		exist, _, _, err := storage.GetFileInfo(0, file)
		if err != nil {
			t.Errorf("Failed to get file info for %s: %v", file, err)
			return
		}
		if exist {
			t.Errorf("File %s still exists after deletion", file)
			return
		}
	}

	numberOfFiles := 10
	maxFileSize := 64 * 1024

	if *testQuickMode {
		numberOfFiles = 2
	}

	chunks := []string{}

	for i := 0; i < numberOfFiles; i++ {
		content := make([]byte, rand.Int()%maxFileSize+1)
		_, err = crypto_rand.Read(content)
		if err != nil {
			t.Errorf("Error generating random content: %v", err)
			return
		}

		hasher := sha256.New()
		hasher.Write(content)
		chunkID := hex.EncodeToString(hasher.Sum(nil))
		chunks = append(chunks, chunkID)

		filePath, exist, _, err := storage.FindChunk(0, chunkID, false)
		if err != nil {
			t.Errorf("Failed to list the chunk %s: %v", chunkID, err)
			return
		}
		if exist {
			t.Errorf("Chunk %s already exists", chunkID)
		}

		err = storage.UploadFile(0, filePath, content)
		if err != nil {
			t.Errorf("Failed to upload the file %s: %v", filePath, err)
			return
		}
		LOG_INFO("STORAGE_CHUNK", "Uploaded chunk: %s, size: %d", filePath, len(content))
	}

	LOG_INFO("STORAGE_FOSSIL", "Making %s a fossil", chunks[0])
	moveChunk(t, storage, chunks[0], false, delay)
	LOG_INFO("STORAGE_FOSSIL", "Making %s a chunk", chunks[0])
	moveChunk(t, storage, chunks[0], true, delay)

	config := CreateConfig()
	config.MinimumChunkSize = 100
	config.chunkPool = make(chan *Chunk, numberOfFiles*2)

	chunk := CreateChunk(config, true)

	for _, chunkID := range chunks {

		chunk.Reset(false)
		filePath, exist, _, err := storage.FindChunk(0, chunkID, false)
		if err != nil {
			t.Errorf("Error getting file info for chunk %s: %v", chunkID, err)
			continue
		} else if !exist {
			t.Errorf("Chunk %s does not exist", chunkID)
			continue
		} else {
			err = storage.DownloadFile(0, filePath, chunk)
			if err != nil {
				t.Errorf("Error downloading file %s: %v", filePath, err)
				continue
			}
			LOG_INFO("STORAGE_CHUNK", "Downloaded chunk: %s, size: %d", filePath, chunk.GetLength())
		}

		hasher := sha256.New()
		hasher.Write(chunk.GetBytes())
		hash := hex.EncodeToString(hasher.Sum(nil))

		if hash != chunkID {
			t.Errorf("File %s, hash %s, size %d", chunkID, hash, chunk.GetBytes())
		}
	}

	LOG_INFO("STORAGE_FOSSIL", "Making %s a fossil", chunks[1])
	moveChunk(t, storage, chunks[1], false, delay)

	filePath, exist, _, err := storage.FindChunk(0, chunks[1], true)
	if err != nil {
		t.Errorf("Error getting file info for fossil %s: %v", chunks[1], err)
	} else if !exist {
		t.Errorf("Fossil %s does not exist", chunks[1])
	} else {
		err = storage.DeleteFile(0, filePath)
		if err != nil {
			t.Errorf("Failed to delete file %s: %v", filePath, err)
		} else {
			time.Sleep(time.Duration(delay) * time.Second)
			filePath, exist, _, err = storage.FindChunk(0, chunks[1], true)
			if err != nil {
				t.Errorf("Error get file info for deleted fossil %s: %v", chunks[1], err)
			} else if exist {
				t.Errorf("Fossil %s still exists after deletion", chunks[1])
			}
		}
	}

	allChunks := []string{}
	for _, file := range listChunks(storage) {
		allChunks = append(allChunks, file)
	}

	for _, file := range allChunks {

		err = storage.DeleteFile(0, "chunks/"+file)
		if err != nil {
			t.Errorf("Failed to delete the file %s: %v", file, err)
			return
		}
	}

}

func TestCleanStorage(t *testing.T) {
	setTestingT(t)
	SetLoggingLevel(INFO)

	defer func() {
		if r := recover(); r != nil {
			switch e := r.(type) {
			case Exception:
				t.Errorf("%s %s", e.LogID, e.Message)
				debug.PrintStack()
			default:
				t.Errorf("%v", e)
				debug.PrintStack()
			}
		}
	}()

	testDir := path.Join(os.TempDir(), "duplicacy_test", "storage_test")
	os.RemoveAll(testDir)
	os.MkdirAll(testDir, 0700)

	LOG_INFO("STORAGE_TEST", "storage: %s", *testStorageName)

	storage, err := loadStorage(testDir, 1)
	if err != nil {
		t.Errorf("Failed to create storage: %v", err)
		return
	}

	directories := make([]string, 0, 1024)
	directories = append(directories, "snapshots/")
	directories = append(directories, "chunks/")

	for len(directories) > 0 {

		dir := directories[len(directories)-1]
		directories = directories[:len(directories)-1]

		LOG_INFO("LIST_FILES", "Listing %s", dir)

		files, _, err := storage.ListFiles(0, dir)
		if err != nil {
			LOG_ERROR("LIST_FILES", "Failed to list the directory %s: %v", dir, err)
			return
		}

		for _, file := range files {
			if len(file) > 0 && file[len(file)-1] == '/' {
				directories = append(directories, dir+file)
			} else {
				storage.DeleteFile(0, dir+file)
				LOG_INFO("DELETE_FILE", "Deleted file %s", file)
			}
		}
	}

	storage.DeleteFile(0, "config")
	LOG_INFO("DELETE_FILE", "Deleted config")

	files, _, err := storage.ListFiles(0, "chunks/")
	for _, file := range files {
		if len(file) > 0 && file[len(file)-1] != '/' {
			LOG_DEBUG("FILE_EXIST", "File %s exists after deletion", file)
		}
	}

}
