package gcs_test

import (
	"context"
	"errors"
	"fmt"
	"io"
	"strings"
	"testing"
	"time"

	"github.com/distribution/distribution/v3/registry/storage/driver"
	. "github.com/smartystreets/goconvey/convey"

	"zotregistry.dev/zot/v2/pkg/extensions/monitoring"
	zlog "zotregistry.dev/zot/v2/pkg/log"
	"zotregistry.dev/zot/v2/pkg/storage/gcs"
	"zotregistry.dev/zot/v2/pkg/test/mocks"
)

var errTest = errors.New("error")

type fileInfoMock struct {
	isDir   bool
	size    int64
	modTime time.Time
	path    string
}

func (f *fileInfoMock) Path() string       { return f.path }
func (f *fileInfoMock) Size() int64        { return f.size }
func (f *fileInfoMock) ModTime() time.Time { return f.modTime }
func (f *fileInfoMock) IsDir() bool        { return f.isDir }

func TestDriver(t *testing.T) {
	Convey("GCS Driver", t, func() {
		storeMock := &mocks.StorageDriverMock{}
		gcsDriver := gcs.New(storeMock)

		Convey("Name", func() {
			So(gcsDriver.Name(), ShouldEqual, "gcs")
		})

		Convey("EnsureDir", func() {
			err := gcsDriver.EnsureDir("/test")
			So(err, ShouldBeNil)
		})

		Convey("DirExists", func() {
			Convey("True", func() {
				storeMock.StatFn = func(ctx context.Context, path string) (driver.FileInfo, error) {
					return &mocks.FileInfoMock{
						IsDirFn: func() bool { return true },
					}, nil
				}
				So(gcsDriver.DirExists("/test"), ShouldBeTrue)
			})

			Convey("False - Not a dir", func() {
				storeMock.StatFn = func(ctx context.Context, path string) (driver.FileInfo, error) {
					return &mocks.FileInfoMock{
						IsDirFn: func() bool { return false },
					}, nil
				}
				So(gcsDriver.DirExists("/test"), ShouldBeFalse)
			})

			Convey("False - Error", func() {
				storeMock.StatFn = func(ctx context.Context, path string) (driver.FileInfo, error) {
					return nil, errTest
				}
				So(gcsDriver.DirExists("/test"), ShouldBeFalse)
			})
		})

		Convey("Reader", func() {
			Convey("Success", func() {
				storeMock.ReaderFn = func(ctx context.Context, path string, offset int64) (io.ReadCloser, error) {
					return io.NopCloser(strings.NewReader("")), nil
				}
				r, err := gcsDriver.Reader("/test", 0)
				So(err, ShouldBeNil)
				So(r, ShouldNotBeNil)
			})

			Convey("InvalidOffsetError", func() {
				storeMock.ReaderFn = func(ctx context.Context, path string, offset int64) (io.ReadCloser, error) {
					return nil, driver.InvalidOffsetError{Path: path, Offset: offset}
				}
				_, err := gcsDriver.Reader("/test", 100)
				So(err, ShouldNotBeNil)

				var invalidOffset driver.InvalidOffsetError
				So(errors.As(err, &invalidOffset), ShouldBeTrue)
				So(invalidOffset.DriverName, ShouldEqual, "gcs")
			})
		})

		Convey("ReadFile", func() {
			Convey("Success", func() {
				storeMock.GetContentFn = func(ctx context.Context, path string) ([]byte, error) {
					return []byte("content"), nil
				}
				content, err := gcsDriver.ReadFile("/test")
				So(err, ShouldBeNil)
				So(string(content), ShouldEqual, "content")
			})

			Convey("PathNotFoundError with empty Path gets path set", func() {
				storeMock.GetContentFn = func(ctx context.Context, path string) ([]byte, error) {
					return nil, driver.PathNotFoundError{Path: ""} // Path empty so driver sets it
				}
				_, err := gcsDriver.ReadFile("/requested/path")
				So(err, ShouldNotBeNil)

				var pathErr driver.PathNotFoundError
				So(errors.As(err, &pathErr), ShouldBeTrue)
				So(pathErr.DriverName, ShouldEqual, "gcs")
				So(pathErr.Path, ShouldEqual, "/requested/path")
			})

			Convey("GCS not-found string becomes PathNotFoundError", func() {
				for _, msg := range []string{"object doesn't exist", "Error 404", "does not exist"} {
					errMsg := msg
					storeMock.GetContentFn = func(ctx context.Context, path string) ([]byte, error) {
						//nolint:err113 // test needs variable not-found message
						return nil, fmt.Errorf("%s", errMsg)
					}
					_, err := gcsDriver.ReadFile("/key")
					So(err, ShouldNotBeNil)

					var pathErr driver.PathNotFoundError
					So(errors.As(err, &pathErr), ShouldBeTrue)
					So(pathErr.Path, ShouldEqual, "/key")
				}
			})

			Convey("Generic error becomes storagedriver.Error", func() {
				storeMock.GetContentFn = func(ctx context.Context, path string) ([]byte, error) {
					return nil, errTest
				}
				_, err := gcsDriver.ReadFile("/test")
				So(err, ShouldNotBeNil)

				var storageErr driver.Error
				So(errors.As(err, &storageErr), ShouldBeTrue)
				So(storageErr.DriverName, ShouldEqual, "gcs")
				So(storageErr.Detail, ShouldEqual, errTest)
			})
		})

		Convey("Delete", func() {
			Convey("Success", func() {
				storeMock.DeleteFn = func(ctx context.Context, path string) error {
					return nil
				}
				err := gcsDriver.Delete("/test")
				So(err, ShouldBeNil)
			})

			Convey("PathNotFoundError is idempotent (return nil)", func() {
				storeMock.DeleteFn = func(ctx context.Context, path string) error {
					return driver.PathNotFoundError{Path: path}
				}
				err := gcsDriver.Delete("/nonexistent")
				So(err, ShouldBeNil)
			})

			Convey("Other error is returned", func() {
				storeMock.DeleteFn = func(ctx context.Context, path string) error {
					return errTest
				}
				err := gcsDriver.Delete("/test")
				So(err, ShouldNotBeNil)
				So(errors.Is(err, errTest), ShouldBeFalse) // wrapped in storagedriver.Error
			})
		})

		Convey("Stat", func() {
			Convey("Success", func() {
				storeMock.StatFn = func(ctx context.Context, path string) (driver.FileInfo, error) {
					return &mocks.FileInfoMock{}, nil
				}
				fi, err := gcsDriver.Stat("/test")
				So(err, ShouldBeNil)
				So(fi, ShouldNotBeNil)
			})

			Convey("InvalidPathError", func() {
				storeMock.StatFn = func(ctx context.Context, path string) (driver.FileInfo, error) {
					return nil, driver.InvalidPathError{Path: path}
				}
				_, err := gcsDriver.Stat("/bad")
				So(err, ShouldNotBeNil)

				var invalidPath driver.InvalidPathError
				So(errors.As(err, &invalidPath), ShouldBeTrue)
				So(invalidPath.DriverName, ShouldEqual, "gcs")
			})
		})

		Convey("Writer", func() {
			Convey("Success", func() {
				storeMock.WriterFn = func(ctx context.Context, path string, isAppend bool) (driver.FileWriter, error) {
					return &mocks.FileWriterMock{}, nil
				}
				w, err := gcsDriver.Writer("/test", false)
				So(err, ShouldBeNil)
				So(w, ShouldNotBeNil)
			})

			Convey("Error", func() {
				storeMock.WriterFn = func(ctx context.Context, path string, isAppend bool) (driver.FileWriter, error) {
					return nil, errTest
				}
				_, err := gcsDriver.Writer("/test", false)
				So(err, ShouldNotBeNil)
			})
		})

		Convey("WriteFile", func() {
			Convey("Success", func() {
				storeMock.WriterFn = func(ctx context.Context, path string, isAppend bool) (driver.FileWriter, error) {
					return &mocks.FileWriterMock{
						WriteFn: func(p []byte) (int, error) {
							return len(p), nil
						},
						CommitFn: func() error {
							return nil
						},
						CloseFn: func() error {
							return nil
						},
					}, nil
				}
				n, err := gcsDriver.WriteFile("/test", []byte("content"))
				So(err, ShouldBeNil)
				So(n, ShouldEqual, 7)
			})

			Convey("Writer Error", func() {
				storeMock.WriterFn = func(ctx context.Context, path string, isAppend bool) (driver.FileWriter, error) {
					return nil, errTest
				}
				_, err := gcsDriver.WriteFile("/test", []byte("content"))
				So(err, ShouldNotBeNil)
			})

			Convey("Write Error", func() {
				storeMock.WriterFn = func(ctx context.Context, path string, isAppend bool) (driver.FileWriter, error) {
					return &mocks.FileWriterMock{
						WriteFn: func(p []byte) (int, error) {
							return 0, errTest
						},
						CloseFn: func() error { return nil },
					}, nil
				}
				_, err := gcsDriver.WriteFile("/test", []byte("content"))
				So(err, ShouldNotBeNil)
			})

			Convey("Commit Error", func() {
				storeMock.WriterFn = func(ctx context.Context, path string, isAppend bool) (driver.FileWriter, error) {
					return &mocks.FileWriterMock{
						WriteFn: func(p []byte) (int, error) {
							return len(p), nil
						},
						CommitFn: func() error {
							return errTest
						},
						CloseFn: func() error { return nil },
					}, nil
				}
				_, err := gcsDriver.WriteFile("/test", []byte("content"))
				So(err, ShouldNotBeNil)
			})
		})

		Convey("Walk", func() {
			Convey("Success", func() {
				storeMock.WalkFn = func(ctx context.Context, path string, f driver.WalkFn, _ ...func(*driver.WalkOptions)) error {
					return nil
				}
				err := gcsDriver.Walk("/test", nil)
				So(err, ShouldBeNil)
			})

			Convey("Direct io.EOF is returned as io.EOF", func() {
				storeMock.WalkFn = func(ctx context.Context, path string, f driver.WalkFn, _ ...func(*driver.WalkOptions)) error {
					return io.EOF
				}
				err := gcsDriver.Walk("/test", nil)
				So(errors.Is(err, io.EOF), ShouldBeTrue)
			})

			Convey("io.EOF wrapped in storagedriver.Error is returned as io.EOF", func() {
				storeMock.WalkFn = func(ctx context.Context, path string, f driver.WalkFn, _ ...func(*driver.WalkOptions)) error {
					return driver.Error{
						DriverName: "gcs",
						Detail:     io.EOF,
					}
				}
				err := gcsDriver.Walk("/test", nil)
				So(errors.Is(err, io.EOF), ShouldBeTrue)
			})

			Convey("Non-EOF error is formatted normally", func() {
				storeMock.WalkFn = func(ctx context.Context, path string, f driver.WalkFn, _ ...func(*driver.WalkOptions)) error {
					return errTest
				}
				err := gcsDriver.Walk("/test", nil)
				So(err, ShouldNotBeNil)
				So(errors.Is(err, io.EOF), ShouldBeFalse)

				var storageErr driver.Error
				So(errors.As(err, &storageErr), ShouldBeTrue)
			})
		})

		Convey("List", func() {
			Convey("Success", func() {
				storeMock.ListFn = func(ctx context.Context, path string) ([]string, error) {
					return []string{"a"}, nil
				}
				l, err := gcsDriver.List("/test")
				So(err, ShouldBeNil)
				So(l, ShouldResemble, []string{"a"})
			})

			Convey("Error", func() {
				storeMock.ListFn = func(ctx context.Context, path string) ([]string, error) {
					return nil, errTest
				}
				_, err := gcsDriver.List("/test")
				So(err, ShouldNotBeNil)
			})
		})

		Convey("Move", func() {
			storeMock.MoveFn = func(ctx context.Context, sourcePath, destPath string) error {
				return nil
			}
			err := gcsDriver.Move("/src", "/dst")
			So(err, ShouldBeNil)
		})

		Convey("SameFile", func() {
			Convey("True", func() {
				now := time.Now()
				storeMock.StatFn = func(ctx context.Context, path string) (driver.FileInfo, error) {
					return &fileInfoMock{
						isDir:   false,
						size:    10,
						modTime: now,
						path:    "/canonical/path",
					}, nil
				}
				So(gcsDriver.SameFile("/path1", "/path2"), ShouldBeTrue)
			})

			Convey("False - Different ModTime", func() {
				storeMock.StatFn = func(ctx context.Context, path string) (driver.FileInfo, error) {
					modTime := time.Now()
					if path == "/path2" {
						modTime = modTime.Add(1 * time.Hour)
					}

					return &fileInfoMock{
						isDir:   false,
						size:    10,
						modTime: modTime,
						path:    path,
					}, nil
				}
				So(gcsDriver.SameFile("/path1", "/path2"), ShouldBeFalse)
			})
		})

		Convey("Link", func() {
			storeMock.PutContentFn = func(ctx context.Context, path string, content []byte) error {
				return nil
			}
			err := gcsDriver.Link("/src", "/dst")
			So(err, ShouldBeNil)
		})
	})
}

func TestNewImageStore(t *testing.T) {
	Convey("NewImageStore", t, func() {
		storeMock := &mocks.StorageDriverMock{}
		log := zlog.NewTestLogger()
		metrics := monitoring.NewMetricsServer(false, log)
		imgStore := gcs.NewImageStore("/tmp", "/tmp", true, true, log, metrics, nil, storeMock, nil, nil, nil)
		So(imgStore, ShouldNotBeNil)
	})
}
