package image_test

import (
	"encoding/json"
	"os"
	"path"
	"testing"

	godigest "github.com/opencontainers/go-digest"
	"github.com/opencontainers/image-spec/specs-go"
	ispec "github.com/opencontainers/image-spec/specs-go/v1"
	. "github.com/smartystreets/goconvey/convey"

	"zotregistry.dev/zot/pkg/api"
	"zotregistry.dev/zot/pkg/api/config"
	tcommon "zotregistry.dev/zot/pkg/test/common"
	. "zotregistry.dev/zot/pkg/test/image-utils"
	"zotregistry.dev/zot/pkg/test/inject"
)

func TestUploadImage(t *testing.T) {
	Convey("Manifest without schemaVersion should fail validation", t, func() {
		port := tcommon.GetFreePort()
		baseURL := tcommon.GetBaseURL(port)

		conf := config.New()
		conf.HTTP.Port = port
		conf.Storage.RootDirectory = t.TempDir()

		ctlr := api.NewController(conf)

		ctlrManager := tcommon.NewControllerManager(ctlr)
		ctlrManager.StartAndWait(port)
		defer ctlrManager.StopServer()

		layerBlob := []byte("test")

		img := Image{
			Layers: [][]byte{
				layerBlob,
			},
			Manifest: ispec.Manifest{
				Layers: []ispec.Descriptor{
					{
						Digest:    godigest.FromBytes(layerBlob),
						Size:      int64(len(layerBlob)),
						MediaType: ispec.MediaTypeImageLayerGzip,
					},
				},
				Config: ispec.DescriptorEmptyJSON,
			},
			Config: ispec.Image{},
		}

		err := UploadImage(img, baseURL, "test", img.DigestStr())
		So(err, ShouldNotBeNil)
	})

	Convey("Post request results in an error", t, func() {
		port := tcommon.GetFreePort()
		baseURL := tcommon.GetBaseURL(port)

		conf := config.New()
		conf.HTTP.Port = port
		conf.Storage.RootDirectory = t.TempDir()

		img := Image{
			Layers: make([][]byte, 10),
		}

		err := UploadImage(img, baseURL, "test", "")
		So(err, ShouldNotBeNil)
	})

	Convey("Post request status differs from accepted", t, func() {
		port := tcommon.GetFreePort()
		baseURL := tcommon.GetBaseURL(port)

		tempDir := t.TempDir()
		conf := config.New()
		conf.HTTP.Port = port
		conf.Storage.RootDirectory = tempDir

		ctlr := api.NewController(conf)

		ctlrManager := tcommon.NewControllerManager(ctlr)
		ctlrManager.StartAndWait(port)
		defer ctlrManager.StopServer()

		err := os.Chmod(tempDir, 0o400)
		if err != nil {
			t.Fatal(err)
		}

		defer func() {
			err := os.Chmod(tempDir, 0o700)
			if err != nil {
				t.Fatal(err)
			}
		}()

		img := Image{
			Layers: make([][]byte, 10),
		}

		err = UploadImage(img, baseURL, "test", "")
		So(err, ShouldNotBeNil)
	})

	Convey("Put request results in an error", t, func() {
		port := tcommon.GetFreePort()
		baseURL := tcommon.GetBaseURL(port)

		conf := config.New()
		conf.HTTP.Port = port
		conf.Storage.RootDirectory = t.TempDir()

		ctlr := api.NewController(conf)

		ctlrManager := tcommon.NewControllerManager(ctlr)
		ctlrManager.StartAndWait(port)
		defer ctlrManager.StopServer()

		img := Image{
			Layers: make([][]byte, 10), // invalid format that will result in an error
			Config: ispec.Image{},
		}

		err := UploadImage(img, baseURL, "test", "")
		So(err, ShouldNotBeNil)
	})

	Convey("Image uploaded successfully", t, func() {
		port := tcommon.GetFreePort()
		baseURL := tcommon.GetBaseURL(port)

		conf := config.New()
		conf.HTTP.Port = port
		conf.Storage.RootDirectory = t.TempDir()

		ctlr := api.NewController(conf)

		ctlrManager := tcommon.NewControllerManager(ctlr)
		ctlrManager.StartAndWait(port)
		defer ctlrManager.StopServer()

		layerBlob := []byte("test")

		img := Image{
			Layers: [][]byte{
				layerBlob,
			},
			Manifest: ispec.Manifest{
				Versioned: specs.Versioned{
					SchemaVersion: 2,
				},
				Layers: []ispec.Descriptor{
					{
						Digest:    godigest.FromBytes(layerBlob),
						Size:      int64(len(layerBlob)),
						MediaType: ispec.MediaTypeImageLayerGzip,
					},
				},
				Config: ispec.DescriptorEmptyJSON,
			},
			Config: ispec.Image{},
		}

		err := UploadImage(img, baseURL, "test", img.DigestStr())
		So(err, ShouldBeNil)
	})

	Convey("Upload image with authentification", t, func() {
		tempDir := t.TempDir()
		conf := config.New()
		port := tcommon.GetFreePort()
		baseURL := tcommon.GetBaseURL(port)

		user1 := "test"
		password1 := "test"
		testString1 := tcommon.GetCredString(user1, password1)

		htpasswdPath := tcommon.MakeHtpasswdFileFromString(testString1)
		defer os.Remove(htpasswdPath)
		conf.HTTP.Auth = &config.AuthConfig{
			HTPasswd: config.AuthHTPasswd{
				Path: htpasswdPath,
			},
		}

		conf.HTTP.Port = port

		conf.HTTP.AccessControl = &config.AccessControlConfig{
			Repositories: config.Repositories{
				"repo": config.PolicyGroup{
					Policies: []config.Policy{
						{
							Users:   []string{user1},
							Actions: []string{"read", "create"},
						},
					},
					DefaultPolicy: []string{},
				},
				"inaccessibleRepo": config.PolicyGroup{
					Policies: []config.Policy{
						{
							Users:   []string{user1},
							Actions: []string{"create"},
						},
					},
					DefaultPolicy: []string{},
				},
			},
			AdminPolicy: config.Policy{
				Users:   []string{},
				Actions: []string{},
			},
		}

		ctlr := api.NewController(conf)

		ctlr.Config.Storage.RootDirectory = tempDir

		ctlrManager := tcommon.NewControllerManager(ctlr)
		ctlrManager.StartAndWait(port)
		defer ctlrManager.StopServer()

		Convey("Request fail while pushing layer", func() {
			err := UploadImageWithBasicAuth(Image{Layers: [][]byte{{1, 2, 3}}}, "badURL", "", "", "", "")
			So(err, ShouldNotBeNil)
		})
		Convey("Request status is not StatusOk while pushing layer", func() {
			err := UploadImageWithBasicAuth(Image{Layers: [][]byte{{1, 2, 3}}}, baseURL, "", "repo", "", "")
			So(err, ShouldNotBeNil)
		})
		Convey("Request fail while pushing config", func() {
			err := UploadImageWithBasicAuth(Image{}, "badURL", "", "", "", "")
			So(err, ShouldNotBeNil)
		})
		Convey("Request status is not StatusOk while pushing config", func() {
			err := UploadImageWithBasicAuth(Image{}, baseURL, "", "repo", "", "")
			So(err, ShouldNotBeNil)
		})
	})

	Convey("Blob upload wrong response status code", t, func() {
		port := tcommon.GetFreePort()
		baseURL := tcommon.GetBaseURL(port)

		tempDir := t.TempDir()
		conf := config.New()
		conf.HTTP.Port = port
		conf.Storage.RootDirectory = tempDir

		ctlr := api.NewController(conf)

		ctlrManager := tcommon.NewControllerManager(ctlr)
		ctlrManager.StartAndWait(port)
		defer ctlrManager.StopServer()

		layerBlob := []byte("test")
		layerBlobDigest := godigest.FromBytes(layerBlob)
		layerPath := path.Join(tempDir, "test", "blobs", "sha256")

		if _, err := os.Stat(layerPath); os.IsNotExist(err) {
			err = os.MkdirAll(layerPath, 0o700)
			if err != nil {
				t.Fatal(err)
			}

			file, err := os.Create(path.Join(layerPath, layerBlobDigest.Encoded()))
			if err != nil {
				t.Fatal(err)
			}

			err = os.Chmod(layerPath, 0o000)
			if err != nil {
				t.Fatal(err)
			}

			defer func() {
				err = os.Chmod(layerPath, 0o700)
				if err != nil {
					t.Fatal(err)
				}

				os.RemoveAll(file.Name())
			}()
		}

		img := Image{
			Layers: [][]byte{
				layerBlob,
			}, // invalid format that will result in an error
			Config: ispec.Image{},
		}

		err := UploadImage(img, baseURL, "test", "")
		So(err, ShouldNotBeNil)
	})

	Convey("CreateBlobUpload wrong response status code", t, func() {
		port := tcommon.GetFreePort()
		baseURL := tcommon.GetBaseURL(port)

		tempDir := t.TempDir()
		conf := config.New()
		conf.HTTP.Port = port
		conf.Storage.RootDirectory = tempDir

		ctlr := api.NewController(conf)

		ctlrManager := tcommon.NewControllerManager(ctlr)
		ctlrManager.StartAndWait(port)
		defer ctlrManager.StopServer()

		layerBlob := []byte("test")

		img := Image{
			Layers: [][]byte{
				layerBlob,
			}, // invalid format that will result in an error
			Config: ispec.Image{},
		}

		Convey("CreateBlobUpload", func() {
			injected := inject.InjectFailure(2)
			if injected {
				err := UploadImage(img, baseURL, "test", img.DigestStr())
				So(err, ShouldNotBeNil)
			}
		})
		Convey("UpdateBlobUpload", func() {
			injected := inject.InjectFailure(4)
			if injected {
				err := UploadImage(img, baseURL, "test", img.DigestStr())
				So(err, ShouldNotBeNil)
			}
		})
	})
}

func TestInjectUploadImage(t *testing.T) {
	Convey("Inject failures for unreachable lines", t, func() {
		port := tcommon.GetFreePort()
		baseURL := tcommon.GetBaseURL(port)

		tempDir := t.TempDir()
		conf := config.New()
		conf.HTTP.Port = port
		conf.Storage.RootDirectory = tempDir

		ctlr := api.NewController(conf)

		ctlrManager := tcommon.NewControllerManager(ctlr)
		ctlrManager.StartAndWait(port)
		defer ctlrManager.StopServer()

		layerBlob := []byte("test")
		layerPath := path.Join(tempDir, "test", ".uploads")

		if _, err := os.Stat(layerPath); os.IsNotExist(err) {
			err = os.MkdirAll(layerPath, 0o700)
			if err != nil {
				t.Fatal(err)
			}
		}

		img := Image{
			Layers: [][]byte{
				layerBlob,
			}, // invalid format that will result in an error
			Config: ispec.Image{},
		}

		Convey("first marshal", func() {
			injected := inject.InjectFailure(0)
			if injected {
				err := UploadImage(img, baseURL, "test", img.DigestStr())
				So(err, ShouldNotBeNil)
			}
		})
		Convey("CreateBlobUpload POST call", func() {
			injected := inject.InjectFailure(1)
			if injected {
				err := UploadImage(img, baseURL, "test", img.DigestStr())
				So(err, ShouldNotBeNil)
			}
		})
		Convey("UpdateBlobUpload PUT call", func() {
			injected := inject.InjectFailure(3)
			if injected {
				err := UploadImage(img, baseURL, "test", img.DigestStr())
				So(err, ShouldNotBeNil)
			}
		})
		Convey("second marshal", func() {
			injected := inject.InjectFailure(5)
			if injected {
				err := UploadImage(img, baseURL, "test", img.DigestStr())
				So(err, ShouldNotBeNil)
			}
		})
	})
}

func TestUploadMultiarchImage(t *testing.T) {
	Convey("make controller", t, func() {
		port := tcommon.GetFreePort()
		baseURL := tcommon.GetBaseURL(port)

		conf := config.New()
		conf.HTTP.Port = port
		conf.Storage.RootDirectory = t.TempDir()

		ctlr := api.NewController(conf)

		ctlrManager := tcommon.NewControllerManager(ctlr)
		ctlrManager.StartAndWait(port)
		defer ctlrManager.StopServer()

		layerBlob := []byte("test")

		img := Image{
			Layers: [][]byte{
				layerBlob,
			},
			Manifest: ispec.Manifest{
				Versioned: specs.Versioned{
					SchemaVersion: 2,
				},
				Layers: []ispec.Descriptor{
					{
						Digest:    godigest.FromBytes(layerBlob),
						Size:      int64(len(layerBlob)),
						MediaType: ispec.MediaTypeImageLayerGzip,
					},
				},
				Config: ispec.DescriptorEmptyJSON,
			},
			Config: ispec.Image{},
		}

		manifestBuf, err := json.Marshal(img.Manifest)
		So(err, ShouldBeNil)

		Convey("Multiarch image uploaded successfully", func() {
			err = UploadMultiarchImage(MultiarchImage{
				Index: ispec.Index{
					Versioned: specs.Versioned{
						SchemaVersion: 2,
					},
					MediaType: ispec.MediaTypeImageIndex,
					Manifests: []ispec.Descriptor{
						{
							MediaType: ispec.MediaTypeImageManifest,
							Digest:    godigest.FromBytes(manifestBuf),
							Size:      int64(len(manifestBuf)),
						},
					},
				},
				Images: []Image{img},
			}, baseURL, "test", "index")
			So(err, ShouldBeNil)
		})

		Convey("Multiarch image without schemaVersion should fail validation", func() {
			err = UploadMultiarchImage(MultiarchImage{
				Index: ispec.Index{
					MediaType: ispec.MediaTypeImageIndex,
					Manifests: []ispec.Descriptor{
						{
							MediaType: ispec.MediaTypeImageManifest,
							Digest:    godigest.FromBytes(manifestBuf),
							Size:      int64(len(manifestBuf)),
						},
					},
				},
				Images: []Image{img},
			}, baseURL, "test", "index")
			So(err, ShouldNotBeNil)
		})
	})
}

func TestInjectUploadImageWithBasicAuth(t *testing.T) {
	Convey("Inject failures for unreachable lines", t, func() {
		port := tcommon.GetFreePort()
		baseURL := tcommon.GetBaseURL(port)

		tempDir := t.TempDir()
		conf := config.New()
		conf.HTTP.Port = port
		conf.Storage.RootDirectory = tempDir

		user := "user"
		password := "password"
		testString := tcommon.GetCredString(user, password)

		htpasswdPath := tcommon.MakeHtpasswdFileFromString(testString)
		defer os.Remove(htpasswdPath)
		conf.HTTP.Auth = &config.AuthConfig{
			HTPasswd: config.AuthHTPasswd{
				Path: htpasswdPath,
			},
		}

		ctlr := api.NewController(conf)

		ctlrManager := tcommon.NewControllerManager(ctlr)
		ctlrManager.StartAndWait(port)
		defer ctlrManager.StopServer()

		layerBlob := []byte("test")
		layerPath := path.Join(tempDir, "test", ".uploads")

		if _, err := os.Stat(layerPath); os.IsNotExist(err) {
			err = os.MkdirAll(layerPath, 0o700)
			if err != nil {
				t.Fatal(err)
			}
		}

		img := Image{
			Layers: [][]byte{
				layerBlob,
			}, // invalid format that will result in an error
			Config: ispec.Image{},
		}

		Convey("first marshal", func() {
			injected := inject.InjectFailure(0)
			if injected {
				err := UploadImageWithBasicAuth(img, baseURL, "test", img.DigestStr(), "user", "password")
				So(err, ShouldNotBeNil)
			}
		})
		Convey("CreateBlobUpload POST call", func() {
			injected := inject.InjectFailure(1)
			if injected {
				err := UploadImageWithBasicAuth(img, baseURL, "test", img.DigestStr(), "user", "password")
				So(err, ShouldNotBeNil)
			}
		})
		Convey("UpdateBlobUpload PUT call", func() {
			injected := inject.InjectFailure(3)
			if injected {
				err := UploadImageWithBasicAuth(img, baseURL, "test", img.DigestStr(), "user", "password")
				So(err, ShouldNotBeNil)
			}
		})
		Convey("second marshal", func() {
			injected := inject.InjectFailure(5)
			if injected {
				err := UploadImageWithBasicAuth(img, baseURL, "test", img.DigestStr(), "user", "password")
				So(err, ShouldNotBeNil)
			}
		})
	})
}
