package server_test

import (
	"context"
	"math"
	"path/filepath"
	"testing"
	"time"

	"github.com/google/uuid"
	"github.com/stretchr/testify/require"

	"github.com/kopia/kopia/internal/apiclient"
	"github.com/kopia/kopia/internal/mockfs"
	"github.com/kopia/kopia/internal/repotesting"
	"github.com/kopia/kopia/internal/serverapi"
	"github.com/kopia/kopia/internal/servertesting"
	"github.com/kopia/kopia/internal/testutil"
	"github.com/kopia/kopia/internal/uitask"
	"github.com/kopia/kopia/repo"
	"github.com/kopia/kopia/repo/manifest"
	"github.com/kopia/kopia/snapshot"
	"github.com/kopia/kopia/snapshot/restore"
	"github.com/kopia/kopia/snapshot/upload"
)

func TestRestoreSnapshots(t *testing.T) {
	ctx, env := repotesting.NewEnvironment(t, repotesting.FormatNotImportant)

	si1 := env.LocalPathSourceInfo("/dummy/path")

	var id11 manifest.ID

	require.NoError(t, repo.WriteSession(ctx, env.Repository, repo.WriteSessionOptions{Purpose: "Test"}, func(ctx context.Context, w repo.RepositoryWriter) error {
		u := upload.NewUploader(w)

		dir1 := mockfs.NewDirectory()

		dir1.AddFile("file1", []byte{1, 2, 3}, 0o644)
		dir1.AddDir("dir1", 0o644).AddFile("file2", []byte{1, 2, 4}, 0o644)

		man11, err := u.Upload(ctx, dir1, nil, si1)
		require.NoError(t, err)
		id11, err = snapshot.SaveSnapshot(ctx, w, man11)
		require.NoError(t, err)

		return nil
	}))

	srvInfo := servertesting.StartServer(t, env, false)

	cli, err := apiclient.NewKopiaAPIClient(apiclient.Options{
		BaseURL:                             srvInfo.BaseURL,
		TrustedServerCertificateFingerprint: srvInfo.TrustedServerCertificateFingerprint,
		Username:                            servertesting.TestUIUsername,
		Password:                            servertesting.TestUIPassword,
	})

	require.NoError(t, err)
	require.NoError(t, cli.FetchCSRFTokenForTesting(ctx))

	t.Run("Filesystem", func(t *testing.T) {
		targetPath1 := testutil.TempDirectory(t)
		restoreTask1, err := serverapi.Restore(ctx, cli, &serverapi.RestoreRequest{
			Root: string(id11),
			Options: restore.Options{
				RestoreDirEntryAtDepth: math.MaxInt32,
			},
			Filesystem: &restore.FilesystemOutput{
				TargetPath:      targetPath1,
				SkipOwners:      true,
				SkipPermissions: true,
			},
		})

		require.NoError(t, err)
		waitForTask(t, cli, restoreTask1.TaskID, 30*time.Second)
		require.FileExists(t, filepath.Join(targetPath1, "file1"))
		require.FileExists(t, filepath.Join(targetPath1, "dir1", "file2"))
	})

	t.Run("FilesystemSubdir", func(t *testing.T) {
		targetPath1 := testutil.TempDirectory(t)
		restoreTask1, err := serverapi.Restore(ctx, cli, &serverapi.RestoreRequest{
			Root: string(id11) + "/dir1",
			Options: restore.Options{
				RestoreDirEntryAtDepth: math.MaxInt32,
			},
			Filesystem: &restore.FilesystemOutput{
				TargetPath:      targetPath1,
				SkipOwners:      true,
				SkipPermissions: true,
			},
		})

		require.NoError(t, err)
		require.Equal(t, uitask.StatusSuccess, waitForTask(t, cli, restoreTask1.TaskID, 30*time.Second).Status)

		require.FileExists(t, filepath.Join(targetPath1, "file2"))
	})

	t.Run("FilesystemFullShallowRestore", func(t *testing.T) {
		targetPath1 := testutil.TempDirectory(t)
		restoreTask1, err := serverapi.Restore(ctx, cli, &serverapi.RestoreRequest{
			Root:    string(id11),
			Options: restore.Options{},
			Filesystem: &restore.FilesystemOutput{
				TargetPath:      targetPath1,
				SkipOwners:      true,
				SkipPermissions: true,
			},
		})

		require.NoError(t, err)
		require.Equal(t, uitask.StatusSuccess, waitForTask(t, cli, restoreTask1.TaskID, 30*time.Second).Status)
		require.FileExists(t, filepath.Join(targetPath1, "file1.kopia-entry"))
		require.DirExists(t, filepath.Join(targetPath1, "dir1.kopia-entry"))
		require.FileExists(t, filepath.Join(targetPath1, "dir1.kopia-entry", ".kopia-entry"))
	})

	t.Run("FilesystemPartialShallowRestore", func(t *testing.T) {
		targetPath1 := testutil.TempDirectory(t)
		restoreTask1, err := serverapi.Restore(ctx, cli, &serverapi.RestoreRequest{
			Root: string(id11),
			Options: restore.Options{
				RestoreDirEntryAtDepth: 1,
			},
			Filesystem: &restore.FilesystemOutput{
				TargetPath:      targetPath1,
				SkipOwners:      true,
				SkipPermissions: true,
			},
		})

		require.NoError(t, err)
		require.Equal(t, uitask.StatusSuccess, waitForTask(t, cli, restoreTask1.TaskID, 30*time.Second).Status)
		require.FileExists(t, filepath.Join(targetPath1, "file1"))
		require.DirExists(t, filepath.Join(targetPath1, "dir1"))
		require.FileExists(t, filepath.Join(targetPath1, "dir1", "file2.kopia-entry"))
	})

	t.Run("ZipFile", func(t *testing.T) {
		outputZipFile := filepath.Join(testutil.TempDirectory(t), "test1.zip")
		restoreTask1, err := serverapi.Restore(ctx, cli, &serverapi.RestoreRequest{
			Root: string(id11),
			Options: restore.Options{
				RestoreDirEntryAtDepth: math.MaxInt32,
			},
			ZipFile: outputZipFile,
		})

		require.NoError(t, err)
		require.Equal(t, uitask.StatusSuccess, waitForTask(t, cli, restoreTask1.TaskID, 30*time.Second).Status)
		require.FileExists(t, outputZipFile)
	})

	t.Run("UncompressedZipFile", func(t *testing.T) {
		outputZipFile := filepath.Join(testutil.TempDirectory(t), "test1.zip")
		restoreTask1, err := serverapi.Restore(ctx, cli, &serverapi.RestoreRequest{
			Root: string(id11),
			Options: restore.Options{
				RestoreDirEntryAtDepth: math.MaxInt32,
			},
			ZipFile:         outputZipFile,
			UncompressedZip: true,
		})

		require.NoError(t, err)
		require.Equal(t, uitask.StatusSuccess, waitForTask(t, cli, restoreTask1.TaskID, 30*time.Second).Status)
		require.FileExists(t, outputZipFile)
	})

	t.Run("TarFile", func(t *testing.T) {
		outputTarFile := filepath.Join(testutil.TempDirectory(t), "test1.tar")
		restoreTask1, err := serverapi.Restore(ctx, cli, &serverapi.RestoreRequest{
			Root: string(id11),
			Options: restore.Options{
				RestoreDirEntryAtDepth: math.MaxInt32,
			},
			TarFile: outputTarFile,
		})

		require.NoError(t, err)
		require.Equal(t, uitask.StatusSuccess, waitForTask(t, cli, restoreTask1.TaskID, 30*time.Second).Status)
		require.FileExists(t, outputTarFile)
	})

	t.Run("InvalidRequest", func(t *testing.T) {
		requests := []*serverapi.RestoreRequest{
			{
				Root: string(id11),
				// no output
			},
			{
				// no root
				ZipFile: filepath.Join(testutil.TempDirectory(t), "test1.zip"),
			},
			{
				Root:    string(id11 + "bad"),
				ZipFile: filepath.Join(testutil.TempDirectory(t), "test1.zip"),
			},
			{
				Root:    string(id11),
				ZipFile: "/no/such/directory/" + uuid.NewString() + "/test1.zip",
			},
			{
				Root:    string(id11),
				TarFile: "/no/such/directory/" + uuid.NewString() + "/test1.tar",
			},
		}

		for _, req := range requests {
			_, err := serverapi.Restore(ctx, cli, req)

			var se apiclient.HTTPStatusError

			require.ErrorAs(t, err, &se)
		}
	})
}
