package cp

import (
	"context"
	"io/fs"
	"net/http"
	"testing"

	"github.com/h2non/gock"
	"github.com/spf13/afero"
	"github.com/stretchr/testify/assert"
	"github.com/stretchr/testify/require"
	"github.com/supabase/cli/internal/testing/apitest"
	"github.com/supabase/cli/internal/utils"
	"github.com/supabase/cli/internal/utils/flags"
	"github.com/supabase/cli/pkg/api"
	"github.com/supabase/cli/pkg/cast"
	"github.com/supabase/cli/pkg/fetcher"
	"github.com/supabase/cli/pkg/storage"
)

var mockFile = storage.ObjectResponse{
	Name:           "abstract.pdf",
	Id:             cast.Ptr("9b7f9f48-17a6-4ca8-b14a-39b0205a63e9"),
	UpdatedAt:      cast.Ptr("2023-10-13T18:08:22.068Z"),
	CreatedAt:      cast.Ptr("2023-10-13T18:08:22.068Z"),
	LastAccessedAt: cast.Ptr("2023-10-13T18:08:22.068Z"),
	Metadata: &storage.ObjectMetadata{
		ETag:           `"887ea9be3c68e6f2fca7fd2d7c77d8fe"`,
		Size:           82702,
		Mimetype:       "application/pdf",
		CacheControl:   "max-age=3600",
		LastModified:   "2023-10-13T18:08:22.000Z",
		ContentLength:  82702,
		HttpStatusCode: 200,
	},
}

var mockApi = storage.StorageAPI{Fetcher: fetcher.NewFetcher(
	"http://127.0.0.1",
)}

func TestStorageCP(t *testing.T) {
	flags.ProjectRef = apitest.RandomProjectRef()
	// Setup valid access token
	token := apitest.RandomAccessToken(t)
	t.Setenv("SUPABASE_ACCESS_TOKEN", string(token))

	t.Run("copy local to remote", func(t *testing.T) {
		// Setup in-memory fs
		fsys := afero.NewMemMapFs()
		require.NoError(t, afero.WriteFile(fsys, "/tmp/file", []byte{}, 0644))
		// Setup mock api
		defer gock.OffAll()
		gock.New(utils.DefaultApiHost).
			Get("/v1/projects/" + flags.ProjectRef + "/api-keys").
			Reply(http.StatusOK).
			JSON([]api.ApiKeyResponse{{
				Name:   "service_role",
				ApiKey: "service-key",
			}})
		gock.New("https://" + utils.GetSupabaseHost(flags.ProjectRef)).
			Post("/storage/v1/object/private/file").
			Reply(http.StatusOK)
		// Run test
		err := Run(context.Background(), "/tmp/file", "ss:///private/file", false, 1, fsys)
		// Check error
		assert.NoError(t, err)
		assert.Empty(t, apitest.ListUnmatchedRequests())
	})

	t.Run("throws error on missing file", func(t *testing.T) {
		// Setup in-memory fs
		fsys := afero.NewMemMapFs()
		// Setup mock api
		defer gock.OffAll()
		gock.New(utils.DefaultApiHost).
			Get("/v1/projects/" + flags.ProjectRef + "/api-keys").
			Reply(http.StatusOK).
			JSON([]api.ApiKeyResponse{{
				Name:   "service_role",
				ApiKey: "service-key",
			}})
		gock.New("https://" + utils.GetSupabaseHost(flags.ProjectRef)).
			Get("/storage/v1/bucket").
			Reply(http.StatusOK).
			JSON([]storage.BucketResponse{})
		// Run test
		err := Run(context.Background(), "abstract.pdf", "ss:///private", true, 1, fsys)
		// Check error
		assert.ErrorIs(t, err, fs.ErrNotExist)
		assert.Empty(t, apitest.ListUnmatchedRequests())
	})

	t.Run("copy remote to local", func(t *testing.T) {
		// Setup in-memory fs
		fsys := afero.NewMemMapFs()
		// Setup mock api
		defer gock.OffAll()
		gock.New(utils.DefaultApiHost).
			Get("/v1/projects/" + flags.ProjectRef + "/api-keys").
			Reply(http.StatusOK).
			JSON([]api.ApiKeyResponse{{
				Name:   "service_role",
				ApiKey: "service-key",
			}})
		gock.New("https://" + utils.GetSupabaseHost(flags.ProjectRef)).
			Get("/storage/v1/object/private/file").
			Reply(http.StatusOK)
		// Run test
		err := Run(context.Background(), "ss:///private/file", "abstract.pdf", false, 1, fsys)
		// Check error
		assert.NoError(t, err)
		assert.Empty(t, apitest.ListUnmatchedRequests())
		exists, err := afero.Exists(fsys, "abstract.pdf")
		assert.NoError(t, err)
		assert.True(t, exists)
	})

	t.Run("throws error on missing bucket", func(t *testing.T) {
		// Setup in-memory fs
		fsys := afero.NewMemMapFs()
		// Setup mock api
		defer gock.OffAll()
		gock.New(utils.DefaultApiHost).
			Get("/v1/projects/" + flags.ProjectRef + "/api-keys").
			Reply(http.StatusOK).
			JSON([]api.ApiKeyResponse{{
				Name:   "service_role",
				ApiKey: "service-key",
			}})
		gock.New("https://" + utils.GetSupabaseHost(flags.ProjectRef)).
			Get("/storage/v1/bucket").
			Reply(http.StatusOK).
			JSON([]storage.BucketResponse{})
		// Run test
		err := Run(context.Background(), "ss:///private", ".", true, 1, fsys)
		// Check error
		assert.ErrorContains(t, err, "Object not found: /private")
		assert.Empty(t, apitest.ListUnmatchedRequests())
	})

	t.Run("throws error on invalid src", func(t *testing.T) {
		// Setup in-memory fs
		fsys := afero.NewMemMapFs()
		// Run test
		err := Run(context.Background(), ":", ".", false, 1, fsys)
		// Check error
		assert.ErrorContains(t, err, "missing protocol scheme")
	})

	t.Run("throws error on invalid dst", func(t *testing.T) {
		// Setup in-memory fs
		fsys := afero.NewMemMapFs()
		// Run test
		err := Run(context.Background(), ".", ":", false, 1, fsys)
		// Check error
		assert.ErrorContains(t, err, "missing protocol scheme")
	})

	t.Run("throws error on unsupported operation", func(t *testing.T) {
		// Setup in-memory fs
		fsys := afero.NewMemMapFs()
		// Setup mock api
		defer gock.OffAll()
		gock.New(utils.DefaultApiHost).
			Get("/v1/projects/" + flags.ProjectRef + "/api-keys").
			Reply(http.StatusOK).
			JSON([]api.ApiKeyResponse{{
				Name:   "service_role",
				ApiKey: "service-key",
			}})
		// Run test
		err := Run(context.Background(), ".", ".", false, 1, fsys)
		// Check error
		assert.ErrorIs(t, err, errUnsupportedOperation)
	})
}

func TestUploadAll(t *testing.T) {
	t.Run("uploads directory to new bucket", func(t *testing.T) {
		// Setup in-memory fs
		fsys := afero.NewMemMapFs()
		require.NoError(t, afero.WriteFile(fsys, "/tmp/readme.md", []byte{}, 0644))
		// Setup mock api
		defer gock.OffAll()
		gock.New("http://127.0.0.1").
			Get("/storage/v1/bucket").
			Reply(http.StatusOK).
			JSON([]storage.BucketResponse{})
		gock.New("http://127.0.0.1").
			Post("/storage/v1/object/tmp/readme.md").
			Reply(http.StatusNotFound).
			JSON(map[string]string{"error": "Bucket not found"})
		gock.New("http://127.0.0.1").
			Post("/storage/v1/bucket").
			Reply(http.StatusOK).
			JSON(storage.CreateBucketResponse{Name: "tmp"})
		gock.New("http://127.0.0.1").
			Post("/storage/v1/object/tmp/readme.md").
			Reply(http.StatusOK)
		// Run test
		err := UploadStorageObjectAll(context.Background(), mockApi, "", "/tmp", 1, fsys)
		// Check error
		assert.NoError(t, err)
		assert.Empty(t, apitest.ListUnmatchedRequests())
	})

	t.Run("throws error on failure to create bucket", func(t *testing.T) {
		// Setup in-memory fs
		fsys := afero.NewMemMapFs()
		require.NoError(t, afero.WriteFile(fsys, "/tmp/readme.md", []byte{}, 0644))
		// Setup mock api
		defer gock.OffAll()
		gock.New("http://127.0.0.1").
			Get("/storage/v1/bucket").
			Reply(http.StatusOK).
			JSON([]storage.BucketResponse{})
		gock.New("http://127.0.0.1").
			Post("/storage/v1/object/tmp/readme.md").
			Reply(http.StatusNotFound).
			JSON(map[string]string{"error": "Bucket not found"})
		gock.New("http://127.0.0.1").
			Post("/storage/v1/bucket").
			Reply(http.StatusServiceUnavailable)
		// Run test
		err := UploadStorageObjectAll(context.Background(), mockApi, "", "/tmp", 1, fsys)
		// Check error
		assert.ErrorContains(t, err, "Error status 503:")
		assert.Empty(t, apitest.ListUnmatchedRequests())
	})

	t.Run("uploads directory to existing prefix", func(t *testing.T) {
		// Setup in-memory fs
		fsys := afero.NewMemMapFs()
		require.NoError(t, afero.WriteFile(fsys, "/tmp/readme.md", []byte{}, 0644))
		require.NoError(t, afero.WriteFile(fsys, "/tmp/docs/api.md", []byte{}, 0644))
		// Setup mock api
		defer gock.OffAll()
		gock.New("http://127.0.0.1").
			Post("/storage/v1/object/list/private").
			Reply(http.StatusOK).
			JSON([]storage.ObjectResponse{{
				Name: "dir",
			}})
		gock.New("http://127.0.0.1").
			Post("/storage/v1/object/private/dir/tmp/readme.md").
			Reply(http.StatusOK)
		gock.New("http://127.0.0.1").
			Post("/storage/v1/object/private/dir/tmp/docs/api.md").
			Reply(http.StatusOK)
		// Run test
		err := UploadStorageObjectAll(context.Background(), mockApi, "/private/dir/", "/tmp", 1, fsys)
		// Check error
		assert.NoError(t, err)
		assert.Empty(t, apitest.ListUnmatchedRequests())
	})

	t.Run("uploads file to existing bucket", func(t *testing.T) {
		// Setup in-memory fs
		fsys := afero.NewMemMapFs()
		require.NoError(t, afero.WriteFile(fsys, "/tmp/readme.md", []byte{}, 0644))
		// Setup mock api
		defer gock.OffAll()
		gock.New("http://127.0.0.1").
			Get("/storage/v1/bucket").
			Reply(http.StatusOK).
			JSON([]storage.BucketResponse{{
				Id:        "private",
				Name:      "private",
				CreatedAt: "2023-10-13T17:48:58.491Z",
				UpdatedAt: "2023-10-13T17:48:58.491Z",
			}})
		gock.New("http://127.0.0.1").
			Post("/storage/v1/object/private/readme.md").
			Reply(http.StatusOK)
		// Run test
		err := UploadStorageObjectAll(context.Background(), mockApi, "private", "/tmp/readme.md", 1, fsys)
		// Check error
		assert.NoError(t, err)
		assert.Empty(t, apitest.ListUnmatchedRequests())
	})

	t.Run("uploads file to existing object", func(t *testing.T) {
		// Setup in-memory fs
		fsys := afero.NewMemMapFs()
		require.NoError(t, afero.WriteFile(fsys, "/tmp/readme.md", []byte{}, 0644))
		// Setup mock api
		defer gock.OffAll()
		fileObject := mockFile
		fileObject.Name = "file"
		gock.New("http://127.0.0.1").
			Post("/storage/v1/object/list/private").
			Reply(http.StatusOK).
			JSON([]storage.ObjectResponse{fileObject})
		gock.New("http://127.0.0.1").
			Post("/storage/v1/object/private/file").
			Reply(http.StatusOK)
		// Run test
		err := UploadStorageObjectAll(context.Background(), mockApi, "private/file", "/tmp/readme.md", 1, fsys)
		// Check error
		assert.NoError(t, err)
		assert.Empty(t, apitest.ListUnmatchedRequests())
	})

	t.Run("throws error on service unavailable", func(t *testing.T) {
		// Setup in-memory fs
		fsys := afero.NewMemMapFs()
		// Setup mock api
		defer gock.OffAll()
		gock.New("http://127.0.0.1").
			Get("/storage/v1/bucket").
			Reply(http.StatusServiceUnavailable)
		// Run test
		err := UploadStorageObjectAll(context.Background(), mockApi, "missing", ".", 1, fsys)
		// Check error
		assert.ErrorContains(t, err, "Error status 503:")
		assert.Empty(t, apitest.ListUnmatchedRequests())
	})
}

func TestDownloadAll(t *testing.T) {
	t.Run("downloads buckets to existing directory", func(t *testing.T) {
		// Setup in-memory fs
		fsys := afero.NewMemMapFs()
		// Setup mock api
		defer gock.OffAll()
		gock.New("http://127.0.0.1").
			Get("/storage/v1/bucket").
			Reply(http.StatusOK).
			JSON([]storage.BucketResponse{{
				Id:        "test",
				Name:      "test",
				Public:    true,
				CreatedAt: "2023-10-13T17:48:58.491Z",
				UpdatedAt: "2023-10-13T17:48:58.491Z",
			}, {
				Id:        "private",
				Name:      "private",
				CreatedAt: "2023-10-13T17:48:58.491Z",
				UpdatedAt: "2023-10-13T17:48:58.491Z",
			}})
		gock.New("http://127.0.0.1").
			Post("/storage/v1/object/list/private").
			Reply(http.StatusOK).
			JSON([]storage.ObjectResponse{})
		gock.New("http://127.0.0.1").
			Post("/storage/v1/object/list/test").
			Reply(http.StatusOK).
			JSON([]storage.ObjectResponse{})
		// Run test
		err := DownloadStorageObjectAll(context.Background(), mockApi, "", "/", 1, fsys)
		// Check error
		assert.NoError(t, err)
		assert.Empty(t, apitest.ListUnmatchedRequests())
		exists, err := afero.DirExists(fsys, "/private")
		assert.NoError(t, err)
		assert.True(t, exists)
		exists, err = afero.DirExists(fsys, "/test")
		assert.NoError(t, err)
		assert.True(t, exists)
	})

	t.Run("downloads empty bucket to new directory", func(t *testing.T) {
		// Setup in-memory fs
		fsys := afero.NewMemMapFs()
		// Setup mock api
		defer gock.OffAll()
		gock.New("http://127.0.0.1").
			Get("/storage/v1/object/private").
			Reply(http.StatusNotFound).
			JSON(map[string]string{"error": "Not Found"})
		gock.New("http://127.0.0.1").
			Get("/storage/v1/bucket").
			Reply(http.StatusOK).
			JSON([]storage.BucketResponse{{
				Id:        "private",
				Name:      "private",
				CreatedAt: "2023-10-13T17:48:58.491Z",
				UpdatedAt: "2023-10-13T17:48:58.491Z",
			}})
		gock.New("http://127.0.0.1").
			Post("/storage/v1/object/list/private").
			Reply(http.StatusOK).
			JSON([]storage.ObjectResponse{})
		// Run test
		err := DownloadStorageObjectAll(context.Background(), mockApi, "/private", "/tmp", 1, fsys)
		// Check error
		assert.NoError(t, err)
		assert.Empty(t, apitest.ListUnmatchedRequests())
		exists, err := afero.DirExists(fsys, "/private")
		assert.NoError(t, err)
		assert.False(t, exists)
		exists, err = afero.DirExists(fsys, "/tmp")
		assert.NoError(t, err)
		assert.True(t, exists)
	})

	t.Run("throws error on empty directory", func(t *testing.T) {
		// Setup in-memory fs
		fsys := afero.NewMemMapFs()
		// Setup mock api
		defer gock.OffAll()
		gock.New("http://127.0.0.1").
			Post("/storage/v1/object/list/private").
			Reply(http.StatusOK).
			JSON([]storage.ObjectResponse{})
		// Run test
		err := DownloadStorageObjectAll(context.Background(), mockApi, "private/dir/", "/", 1, fsys)
		// Check error
		assert.ErrorContains(t, err, "Object not found: private/dir/")
		assert.Empty(t, apitest.ListUnmatchedRequests())
		exists, err := afero.DirExists(fsys, "/private")
		assert.NoError(t, err)
		assert.False(t, exists)
	})

	t.Run("downloads objects to existing directory", func(t *testing.T) {
		// Setup in-memory fs
		fsys := afero.NewMemMapFs()
		// Setup mock api
		defer gock.OffAll()
		// Lists /private/tmp directory
		gock.New("http://127.0.0.1").
			Post("/storage/v1/object/list/private").
			JSON(storage.ListObjectsQuery{
				Prefix: "tmp/",
				Search: "",
				Limit:  storage.PAGE_LIMIT,
				Offset: 0,
			}).
			Reply(http.StatusOK).
			JSON([]storage.ObjectResponse{{
				Name: "docs",
			}, mockFile})
		gock.New("http://127.0.0.1").
			Get("/storage/v1/object/private/tmp/abstract.pdf").
			Reply(http.StatusOK)
		// Lists /private/tmp/docs directory
		readme := mockFile
		readme.Name = "readme.md"
		gock.New("http://127.0.0.1").
			Post("/storage/v1/object/list/private").
			JSON(storage.ListObjectsQuery{
				Prefix: "tmp/docs/",
				Search: "",
				Limit:  storage.PAGE_LIMIT,
				Offset: 0,
			}).
			Reply(http.StatusOK).
			JSON([]storage.ObjectResponse{readme})
		gock.New("http://127.0.0.1").
			Get("/storage/v1/object/private/tmp/docs/readme.md").
			Reply(http.StatusOK)
		// Run test
		err := DownloadStorageObjectAll(context.Background(), mockApi, "private/tmp/", "/", 1, fsys)
		// Check error
		assert.NoError(t, err)
		assert.Empty(t, apitest.ListUnmatchedRequests())
		exists, err := afero.Exists(fsys, "/tmp/abstract.pdf")
		assert.NoError(t, err)
		assert.True(t, exists)
		exists, err = afero.Exists(fsys, "/tmp/docs/readme.md")
		assert.NoError(t, err)
		assert.True(t, exists)
	})

	t.Run("downloads object to existing file", func(t *testing.T) {
		// Setup in-memory fs
		fsys := afero.NewMemMapFs()
		// Setup mock api
		defer gock.OffAll()
		gock.New("http://127.0.0.1").
			Post("/storage/v1/object/list/private").
			Reply(http.StatusOK).
			JSON([]storage.ObjectResponse{mockFile})
		gock.New("http://127.0.0.1").
			Get("/storage/v1/object/private/abstract.pdf").
			Reply(http.StatusOK)
		// Run test
		err := DownloadStorageObjectAll(context.Background(), mockApi, "/private/abstract.pdf", "/tmp/file", 1, fsys)
		// Check error
		assert.NoError(t, err)
		assert.Empty(t, apitest.ListUnmatchedRequests())
		exists, err := afero.DirExists(fsys, "/private")
		assert.NoError(t, err)
		assert.False(t, exists)
		exists, err = afero.Exists(fsys, "/tmp/file")
		assert.NoError(t, err)
		assert.True(t, exists)
	})
}
