package drive

import (
	"bytes"
	"context"
	"encoding/json"
	"io"
	"io/ioutil"
	"mime"
	"os"
	"path"
	"path/filepath"
	"strings"
	"testing"
	"time"

	"github.com/pkg/errors"
	_ "github.com/rclone/rclone/backend/local"
	"github.com/rclone/rclone/fs"
	"github.com/rclone/rclone/fs/hash"
	"github.com/rclone/rclone/fs/operations"
	"github.com/rclone/rclone/fstest"
	"github.com/rclone/rclone/fstest/fstests"
	"github.com/rclone/rclone/lib/random"
	"github.com/stretchr/testify/assert"
	"github.com/stretchr/testify/require"
	"google.golang.org/api/drive/v3"
)

func TestDriveScopes(t *testing.T) {
	for _, test := range []struct {
		in       string
		want     []string
		wantFlag bool
	}{
		{"", []string{
			"https://www.googleapis.com/auth/drive",
		}, false},
		{" drive.file , drive.readonly", []string{
			"https://www.googleapis.com/auth/drive.file",
			"https://www.googleapis.com/auth/drive.readonly",
		}, false},
		{" drive.file , drive.appfolder", []string{
			"https://www.googleapis.com/auth/drive.file",
			"https://www.googleapis.com/auth/drive.appfolder",
		}, true},
	} {
		got := driveScopes(test.in)
		assert.Equal(t, test.want, got, test.in)
		gotFlag := driveScopesContainsAppFolder(got)
		assert.Equal(t, test.wantFlag, gotFlag, test.in)
	}
}

/*
var additionalMimeTypes = map[string]string{
	"application/vnd.ms-excel.sheet.macroenabled.12":                          ".xlsm",
	"application/vnd.ms-excel.template.macroenabled.12":                       ".xltm",
	"application/vnd.ms-powerpoint.presentation.macroenabled.12":              ".pptm",
	"application/vnd.ms-powerpoint.slideshow.macroenabled.12":                 ".ppsm",
	"application/vnd.ms-powerpoint.template.macroenabled.12":                  ".potm",
	"application/vnd.ms-powerpoint":                                           ".ppt",
	"application/vnd.ms-word.document.macroenabled.12":                        ".docm",
	"application/vnd.ms-word.template.macroenabled.12":                        ".dotm",
	"application/vnd.openxmlformats-officedocument.presentationml.template":   ".potx",
	"application/vnd.openxmlformats-officedocument.spreadsheetml.template":    ".xltx",
	"application/vnd.openxmlformats-officedocument.wordprocessingml.template": ".dotx",
	"application/vnd.sun.xml.writer":                                          ".sxw",
	"text/richtext":                                                           ".rtf",
}
*/

// Load the example export formats into exportFormats for testing
func TestInternalLoadExampleFormats(t *testing.T) {
	fetchFormatsOnce.Do(func() {})
	buf, err := ioutil.ReadFile(filepath.FromSlash("test/about.json"))
	var about struct {
		ExportFormats map[string][]string `json:"exportFormats,omitempty"`
		ImportFormats map[string][]string `json:"importFormats,omitempty"`
	}
	require.NoError(t, err)
	require.NoError(t, json.Unmarshal(buf, &about))
	_exportFormats = fixMimeTypeMap(about.ExportFormats)
	_importFormats = fixMimeTypeMap(about.ImportFormats)
}

func TestInternalParseExtensions(t *testing.T) {
	for _, test := range []struct {
		in      string
		want    []string
		wantErr error
	}{
		{"doc", []string{".doc"}, nil},
		{" docx ,XLSX, 	pptx,svg", []string{".docx", ".xlsx", ".pptx", ".svg"}, nil},
		{"docx,svg,Docx", []string{".docx", ".svg"}, nil},
		{"docx,potato,docx", []string{".docx"}, errors.New(`couldn't find MIME type for extension ".potato"`)},
	} {
		extensions, _, gotErr := parseExtensions(test.in)
		if test.wantErr == nil {
			assert.NoError(t, gotErr)
		} else {
			assert.EqualError(t, gotErr, test.wantErr.Error())
		}
		assert.Equal(t, test.want, extensions)
	}

	// Test it is appending
	extensions, _, gotErr := parseExtensions("docx,svg", "docx,svg,xlsx")
	assert.NoError(t, gotErr)
	assert.Equal(t, []string{".docx", ".svg", ".xlsx"}, extensions)
}

func TestInternalFindExportFormat(t *testing.T) {
	item := &drive.File{
		Name:     "file",
		MimeType: "application/vnd.google-apps.document",
	}
	for _, test := range []struct {
		extensions    []string
		wantExtension string
		wantMimeType  string
	}{
		{[]string{}, "", ""},
		{[]string{".pdf"}, ".pdf", "application/pdf"},
		{[]string{".pdf", ".rtf", ".xls"}, ".pdf", "application/pdf"},
		{[]string{".xls", ".rtf", ".pdf"}, ".rtf", "application/rtf"},
		{[]string{".xls", ".csv", ".svg"}, "", ""},
	} {
		f := new(Fs)
		f.exportExtensions = test.extensions
		gotExtension, gotFilename, gotMimeType, gotIsDocument := f.findExportFormat(item)
		assert.Equal(t, test.wantExtension, gotExtension)
		if test.wantExtension != "" {
			assert.Equal(t, item.Name+gotExtension, gotFilename)
		} else {
			assert.Equal(t, "", gotFilename)
		}
		assert.Equal(t, test.wantMimeType, gotMimeType)
		assert.Equal(t, true, gotIsDocument)
	}
}

func TestMimeTypesToExtension(t *testing.T) {
	for mimeType, extension := range _mimeTypeToExtension {
		extensions, err := mime.ExtensionsByType(mimeType)
		assert.NoError(t, err)
		assert.Contains(t, extensions, extension)
	}
}

func TestExtensionToMimeType(t *testing.T) {
	for mimeType, extension := range _mimeTypeToExtension {
		gotMimeType := mime.TypeByExtension(extension)
		mediatype, _, err := mime.ParseMediaType(gotMimeType)
		assert.NoError(t, err)
		assert.Equal(t, mimeType, mediatype)
	}
}

func TestExtensionsForExportFormats(t *testing.T) {
	if _exportFormats == nil {
		t.Error("exportFormats == nil")
	}
	for fromMT, toMTs := range _exportFormats {
		for _, toMT := range toMTs {
			if !isInternalMimeType(toMT) {
				extensions, err := mime.ExtensionsByType(toMT)
				assert.NoError(t, err, "invalid MIME type %q", toMT)
				assert.NotEmpty(t, extensions, "No extension found for %q (from: %q)", fromMT, toMT)
			}
		}
	}
}

func TestExtensionsForImportFormats(t *testing.T) {
	t.Skip()
	if _importFormats == nil {
		t.Error("_importFormats == nil")
	}
	for fromMT := range _importFormats {
		if !isInternalMimeType(fromMT) {
			extensions, err := mime.ExtensionsByType(fromMT)
			assert.NoError(t, err, "invalid MIME type %q", fromMT)
			assert.NotEmpty(t, extensions, "No extension found for %q", fromMT)
		}
	}
}

func (f *Fs) InternalTestDocumentImport(t *testing.T) {
	oldAllow := f.opt.AllowImportNameChange
	f.opt.AllowImportNameChange = true
	defer func() {
		f.opt.AllowImportNameChange = oldAllow
	}()

	testFilesPath, err := filepath.Abs(filepath.FromSlash("test/files"))
	require.NoError(t, err)

	testFilesFs, err := fs.NewFs(testFilesPath)
	require.NoError(t, err)

	_, f.importMimeTypes, err = parseExtensions("odt,ods,doc")
	require.NoError(t, err)

	err = operations.CopyFile(context.Background(), f, testFilesFs, "example2.doc", "example2.doc")
	require.NoError(t, err)
}

func (f *Fs) InternalTestDocumentUpdate(t *testing.T) {
	testFilesPath, err := filepath.Abs(filepath.FromSlash("test/files"))
	require.NoError(t, err)

	testFilesFs, err := fs.NewFs(testFilesPath)
	require.NoError(t, err)

	_, f.importMimeTypes, err = parseExtensions("odt,ods,doc")
	require.NoError(t, err)

	err = operations.CopyFile(context.Background(), f, testFilesFs, "example2.xlsx", "example1.ods")
	require.NoError(t, err)
}

func (f *Fs) InternalTestDocumentExport(t *testing.T) {
	var buf bytes.Buffer
	var err error

	f.exportExtensions, _, err = parseExtensions("txt")
	require.NoError(t, err)

	obj, err := f.NewObject(context.Background(), "example2.txt")
	require.NoError(t, err)

	rc, err := obj.Open(context.Background())
	require.NoError(t, err)
	defer func() { require.NoError(t, rc.Close()) }()

	_, err = io.Copy(&buf, rc)
	require.NoError(t, err)
	text := buf.String()

	for _, excerpt := range []string{
		"Lorem ipsum dolor sit amet, consectetur",
		"porta at ultrices in, consectetur at augue.",
	} {
		require.Contains(t, text, excerpt)
	}
}

func (f *Fs) InternalTestDocumentLink(t *testing.T) {
	var buf bytes.Buffer
	var err error

	f.exportExtensions, _, err = parseExtensions("link.html")
	require.NoError(t, err)

	obj, err := f.NewObject(context.Background(), "example2.link.html")
	require.NoError(t, err)

	rc, err := obj.Open(context.Background())
	require.NoError(t, err)
	defer func() { require.NoError(t, rc.Close()) }()

	_, err = io.Copy(&buf, rc)
	require.NoError(t, err)
	text := buf.String()

	require.True(t, strings.HasPrefix(text, "<html>"))
	require.True(t, strings.HasSuffix(text, "</html>\n"))
	for _, excerpt := range []string{
		`<meta http-equiv="refresh"`,
		`Loading <a href="`,
	} {
		require.Contains(t, text, excerpt)
	}
}

const (
	// from fstest/fstests/fstests.go
	existingDir    = "hello? sausage"
	existingFile   = `hello? sausage/êé/Hello, 世界/ " ' @ < > & ? + ≠/z.txt`
	existingSubDir = "êé"
)

// TestIntegration/FsMkdir/FsPutFiles/Internal/Shortcuts
func (f *Fs) InternalTestShortcuts(t *testing.T) {
	ctx := context.Background()
	srcObj, err := f.NewObject(ctx, existingFile)
	require.NoError(t, err)
	srcHash, err := srcObj.Hash(ctx, hash.MD5)
	require.NoError(t, err)
	assert.NotEqual(t, "", srcHash)
	t.Run("Errors", func(t *testing.T) {
		_, err := f.makeShortcut(ctx, "", f, "")
		assert.Error(t, err)
		assert.Contains(t, err.Error(), "can't be root")

		_, err = f.makeShortcut(ctx, "notfound", f, "dst")
		assert.Error(t, err)
		assert.Contains(t, err.Error(), "can't find source")

		_, err = f.makeShortcut(ctx, existingFile, f, existingFile)
		assert.Error(t, err)
		assert.Contains(t, err.Error(), "not overwriting")
		assert.Contains(t, err.Error(), "existing file")

		_, err = f.makeShortcut(ctx, existingFile, f, existingDir)
		assert.Error(t, err)
		assert.Contains(t, err.Error(), "not overwriting")
		assert.Contains(t, err.Error(), "existing directory")
	})
	t.Run("File", func(t *testing.T) {
		dstObj, err := f.makeShortcut(ctx, existingFile, f, "shortcut.txt")
		require.NoError(t, err)
		require.NotNil(t, dstObj)
		assert.Equal(t, "shortcut.txt", dstObj.Remote())
		dstHash, err := dstObj.Hash(ctx, hash.MD5)
		require.NoError(t, err)
		assert.Equal(t, srcHash, dstHash)
		require.NoError(t, dstObj.Remove(ctx))
	})
	t.Run("Dir", func(t *testing.T) {
		dstObj, err := f.makeShortcut(ctx, existingDir, f, "shortcutdir")
		require.NoError(t, err)
		require.Nil(t, dstObj)
		entries, err := f.List(ctx, "shortcutdir")
		require.NoError(t, err)
		require.Equal(t, 1, len(entries))
		require.Equal(t, "shortcutdir/"+existingSubDir, entries[0].Remote())
		require.NoError(t, f.Rmdir(ctx, "shortcutdir"))
	})
	t.Run("Command", func(t *testing.T) {
		_, err := f.Command(ctx, "shortcut", []string{"one"}, nil)
		require.Error(t, err)
		require.Contains(t, err.Error(), "need exactly 2 arguments")

		_, err = f.Command(ctx, "shortcut", []string{"one", "two"}, map[string]string{
			"target": "doesnotexistremote:",
		})
		require.Error(t, err)
		require.Contains(t, err.Error(), "couldn't find target")

		_, err = f.Command(ctx, "shortcut", []string{"one", "two"}, map[string]string{
			"target": ".",
		})
		require.Error(t, err)
		require.Contains(t, err.Error(), "target is not a drive backend")

		dstObjI, err := f.Command(ctx, "shortcut", []string{existingFile, "shortcut2.txt"}, map[string]string{
			"target": fs.ConfigString(f),
		})
		require.NoError(t, err)
		dstObj := dstObjI.(*Object)
		assert.Equal(t, "shortcut2.txt", dstObj.Remote())
		dstHash, err := dstObj.Hash(ctx, hash.MD5)
		require.NoError(t, err)
		assert.Equal(t, srcHash, dstHash)
		require.NoError(t, dstObj.Remove(ctx))

		dstObjI, err = f.Command(ctx, "shortcut", []string{existingFile, "shortcut3.txt"}, nil)
		require.NoError(t, err)
		dstObj = dstObjI.(*Object)
		assert.Equal(t, "shortcut3.txt", dstObj.Remote())
		dstHash, err = dstObj.Hash(ctx, hash.MD5)
		require.NoError(t, err)
		assert.Equal(t, srcHash, dstHash)
		require.NoError(t, dstObj.Remove(ctx))
	})
}

// TestIntegration/FsMkdir/FsPutFiles/Internal/UnTrash
func (f *Fs) InternalTestUnTrash(t *testing.T) {
	ctx := context.Background()

	// Make some objects, one in a subdir
	contents := random.String(100)
	file1 := fstest.NewItem("trashDir/toBeTrashed", contents, time.Now())
	_, obj1 := fstests.PutTestContents(ctx, t, f, &file1, contents, false)
	file2 := fstest.NewItem("trashDir/subdir/toBeTrashed", contents, time.Now())
	_, _ = fstests.PutTestContents(ctx, t, f, &file2, contents, false)

	// Check objects
	checkObjects := func() {
		fstest.CheckListingWithRoot(t, f, "trashDir", []fstest.Item{
			file1,
			file2,
		}, []string{
			"trashDir/subdir",
		}, f.Precision())
	}
	checkObjects()

	// Make sure we are using the trash
	require.Equal(t, true, f.opt.UseTrash)

	// Remove the object and the dir
	require.NoError(t, obj1.Remove(ctx))
	require.NoError(t, f.Purge(ctx, "trashDir/subdir"))

	// Check objects gone
	fstest.CheckListingWithRoot(t, f, "trashDir", []fstest.Item{}, []string{}, f.Precision())

	// Restore the object and directory
	r, err := f.unTrashDir(ctx, "trashDir", true)
	require.NoError(t, err)
	assert.Equal(t, unTrashResult{Errors: 0, Untrashed: 2}, r)

	// Check objects restored
	checkObjects()

	// Remove the test dir
	require.NoError(t, f.Purge(ctx, "trashDir"))
}

// TestIntegration/FsMkdir/FsPutFiles/Internal/CopyID
func (f *Fs) InternalTestCopyID(t *testing.T) {
	ctx := context.Background()
	obj, err := f.NewObject(ctx, existingFile)
	require.NoError(t, err)
	o := obj.(*Object)

	dir, err := ioutil.TempDir("", "rclone-drive-copyid-test")
	require.NoError(t, err)
	defer func() {
		_ = os.RemoveAll(dir)
	}()

	checkFile := func(name string) {
		filePath := filepath.Join(dir, name)
		fi, err := os.Stat(filePath)
		require.NoError(t, err)
		assert.Equal(t, int64(100), fi.Size())
		err = os.Remove(filePath)
		require.NoError(t, err)
	}

	t.Run("BadID", func(t *testing.T) {
		err = f.copyID(ctx, "ID-NOT-FOUND", dir+"/")
		require.Error(t, err)
		assert.Contains(t, err.Error(), "couldn't find id")
	})

	t.Run("Directory", func(t *testing.T) {
		rootID, err := f.dirCache.RootID(ctx, false)
		require.NoError(t, err)
		err = f.copyID(ctx, rootID, dir+"/")
		require.Error(t, err)
		assert.Contains(t, err.Error(), "can't copy directory")
	})

	t.Run("WithoutDestName", func(t *testing.T) {
		err = f.copyID(ctx, o.id, dir+"/")
		require.NoError(t, err)
		checkFile(path.Base(existingFile))
	})

	t.Run("WithDestName", func(t *testing.T) {
		err = f.copyID(ctx, o.id, dir+"/potato.txt")
		require.NoError(t, err)
		checkFile("potato.txt")
	})
}

func (f *Fs) InternalTest(t *testing.T) {
	// These tests all depend on each other so run them as nested tests
	t.Run("DocumentImport", func(t *testing.T) {
		f.InternalTestDocumentImport(t)
		t.Run("DocumentUpdate", func(t *testing.T) {
			f.InternalTestDocumentUpdate(t)
			t.Run("DocumentExport", func(t *testing.T) {
				f.InternalTestDocumentExport(t)
				t.Run("DocumentLink", func(t *testing.T) {
					f.InternalTestDocumentLink(t)
				})
			})
		})
	})
	t.Run("Shortcuts", f.InternalTestShortcuts)
	t.Run("UnTrash", f.InternalTestUnTrash)
	t.Run("CopyID", f.InternalTestCopyID)
}

var _ fstests.InternalTester = (*Fs)(nil)