/*
   Copyright The containerd Authors.

   Licensed under the Apache License, Version 2.0 (the "License");
   you may not use this file except in compliance with the License.
   You may obtain a copy of the License at

       http://www.apache.org/licenses/LICENSE-2.0

   Unless required by applicable law or agreed to in writing, software
   distributed under the License is distributed on an "AS IS" BASIS,
   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
   See the License for the specific language governing permissions and
   limitations under the License.
*/

package image

import (
	"context"
	"errors"
	"sort"
	"sync"
	"testing"

	"github.com/containerd/containerd/v2/core/images"
	"github.com/containerd/errdefs"
	"github.com/opencontainers/go-digest"
	ocispec "github.com/opencontainers/image-spec/specs-go/v1"
)

func TestStore(t *testing.T) {
	for _, testCase := range []struct {
		Name       string
		ImageStore *Store
		// Annotations are the different references annotations to run the test with,
		// the possible values:
		// - "OCI": Uses the OCI defined annotation "org.opencontainers.image.ref.name"
		//   This annotation may be a full reference or tag only
		// - "containerd": Uses the containerd defined annotation "io.containerd.image.name"
		//   This annotation is always a full reference as used by containerd
		// - "Annotation": Sets the annotation flag but does not set a reference annotation
		//   Use this case to test the default where no reference is provided
		// - "NoAnnotation": Does not set the annotation flag
		//   Use this case to test storing of the index images by reference
		Annotations []string
		ImageName   string
		Images      []string
		Err         error
	}{
		{
			Name: "Prefix",
			ImageStore: &Store{
				extraReferences: []Reference{
					{
						Name:     "registry.test/image",
						IsPrefix: true,
					},
				},
			},
			Annotations: []string{"OCI", "containerd"},
			ImageName:   "registry.test/image:latest",
			Images:      []string{"registry.test/image:latest"},
		},
		{
			Name: "Overwrite",
			ImageStore: &Store{
				extraReferences: []Reference{
					{
						Name:           "placeholder",
						IsPrefix:       true,
						AllowOverwrite: true,
					},
				},
			},
			Annotations: []string{"OCI", "containerd"},
			ImageName:   "registry.test/image:latest",
			Images:      []string{"registry.test/image:latest"},
		},
		{
			Name: "TagOnly",
			ImageStore: &Store{
				extraReferences: []Reference{
					{
						Name:     "registry.test/image",
						IsPrefix: true,
					},
				},
			},
			Annotations: []string{"OCI"},
			ImageName:   "latest",
			Images:      []string{"registry.test/image:latest"},
		},
		{
			Name: "AddDigest",
			ImageStore: &Store{
				extraReferences: []Reference{
					{
						Name:      "registry.test/base",
						IsPrefix:  true,
						AddDigest: true,
					},
				},
			},
			Annotations: []string{"Annotation"},
			Images:      []string{"registry.test/base@"},
		},
		{
			Name: "NameAndDigest",
			ImageStore: &Store{
				extraReferences: []Reference{
					{
						Name:      "registry.test/base",
						IsPrefix:  true,
						AddDigest: true,
					},
				},
			},
			Annotations: []string{"OCI"},
			ImageName:   "latest",
			Images:      []string{"registry.test/base:latest", "registry.test/base@"},
		},
		{
			Name: "NameSkipDigest",
			ImageStore: &Store{
				extraReferences: []Reference{
					{
						Name:            "registry.test/base",
						IsPrefix:        true,
						AddDigest:       true,
						SkipNamedDigest: true,
					},
				},
			},
			Annotations: []string{"OCI"},
			ImageName:   "latest",
			Images:      []string{"registry.test/base:latest"},
		},
		{
			Name: "OverwriteNameDigest",
			ImageStore: &Store{
				extraReferences: []Reference{
					{
						Name:           "base-name",
						IsPrefix:       true,
						AllowOverwrite: true,
						AddDigest:      true,
					},
				},
			},
			Annotations: []string{"OCI", "containerd"},
			ImageName:   "registry.test/base:latest",
			Images:      []string{"registry.test/base:latest", "base-name@"},
		},
		{
			Name: "OverwriteNameSkipDigest",
			ImageStore: &Store{
				extraReferences: []Reference{
					{
						Name:            "base-name",
						IsPrefix:        true,
						AllowOverwrite:  true,
						AddDigest:       true,
						SkipNamedDigest: true,
					},
				},
			},
			Annotations: []string{"OCI", "containerd"},
			ImageName:   "registry.test/base:latest",
			Images:      []string{"registry.test/base:latest"},
		},
		{
			Name: "ReferenceNotFound",
			ImageStore: &Store{
				extraReferences: []Reference{
					{
						Name:     "registry.test/image",
						IsPrefix: true,
					},
				},
			},
			Annotations: []string{"OCI", "containerd"},
			ImageName:   "registry.test/base:latest",
			Err:         errdefs.ErrNotFound,
		},
		{
			Name:        "NoReference",
			ImageStore:  &Store{},
			Annotations: []string{"Annotation", "NoAnnotation"},
			Err:         errdefs.ErrNotFound,
		},
		{
			Name: "ImageName",
			ImageStore: &Store{
				imageName: "registry.test/index:latest",
			},
			Annotations: []string{"NoAnnotation"},
			Images:      []string{"registry.test/index:latest"},
		},
	} {
		for _, a := range testCase.Annotations {
			name := testCase.Name + "_" + a
			dgst := digest.Canonical.FromString(name)
			desc := ocispec.Descriptor{
				Digest:      dgst,
				Annotations: map[string]string{},
			}
			expected := make([]string, len(testCase.Images))
			for i, img := range testCase.Images {
				if img[len(img)-1] == '@' {
					img = img + dgst.String()
				}
				expected[i] = img
			}
			switch a {
			case "containerd":
				desc.Annotations["io.containerd.import.ref-source"] = "annotation"
				desc.Annotations[images.AnnotationImageName] = testCase.ImageName
			case "OCI":
				desc.Annotations["io.containerd.import.ref-source"] = "annotation"
				desc.Annotations[ocispec.AnnotationRefName] = testCase.ImageName
			case "Annotation":
				desc.Annotations["io.containerd.import.ref-source"] = "annotation"
			}
			t.Run(name, func(t *testing.T) {
				imgs, err := testCase.ImageStore.Store(context.Background(), desc, newSimpleImageStore())
				if err != nil {
					if testCase.Err == nil {
						t.Fatal(err)
					}
					if !errors.Is(err, testCase.Err) {
						t.Fatalf("unexpected error %v: expeceted %v", err, testCase.Err)
					}
					return
				} else if testCase.Err != nil {
					t.Fatalf("succeeded but expected error: %v", testCase.Err)
				}

				if len(imgs) != len(expected) {
					t.Fatalf("mismatched array length\nexpected:\n\t%v\nactual\n\t%v", expected, imgs)
				}
				for i, name := range expected {
					if imgs[i].Name != name {
						t.Fatalf("wrong image name %q, expected %q", imgs[i].Name, name)
					}
					if imgs[i].Target.Digest != dgst {
						t.Fatalf("wrong image digest %s, expected %s", imgs[i].Target.Digest, dgst)
					}
				}
			})
		}

	}
}

func TestLookup(t *testing.T) {
	ctx := context.Background()
	is := newSimpleImageStore()
	for _, name := range []string{
		"registry.io/test1:latest",
		"registry.io/test1:v1",
	} {
		is.Create(ctx, images.Image{
			Name: name,
		})
	}
	for _, testCase := range []struct {
		Name       string
		ImageStore *Store
		Expected   []string
		Err        error
	}{
		{
			Name: "SingleImage",
			ImageStore: &Store{
				imageName: "registry.io/test1:latest",
			},
			Expected: []string{"registry.io/test1:latest"},
		},
		{
			Name: "MultipleReferences",
			ImageStore: &Store{
				imageName: "registry.io/test1:latest",
				extraReferences: []Reference{
					{
						Name: "registry.io/test1:v1",
					},
				},
			},
			Expected: []string{"registry.io/test1:latest", "registry.io/test1:v1"},
		},
		{
			Name: "OnlyReferences",
			ImageStore: &Store{
				extraReferences: []Reference{
					{
						Name: "registry.io/test1:latest",
					},
					{
						Name: "registry.io/test1:v1",
					},
				},
			},
			Expected: []string{"registry.io/test1:latest", "registry.io/test1:v1"},
		},
		{
			Name: "UnsupportedPrefix",
			ImageStore: &Store{
				extraReferences: []Reference{
					{
						Name:     "registry.io/test1:latest",
						IsPrefix: true,
					},
				},
			},
			Err: errdefs.ErrNotImplemented,
		},
	} {
		t.Run(testCase.Name, func(t *testing.T) {
			images, err := testCase.ImageStore.Lookup(ctx, is)
			if err != nil {
				if !errors.Is(err, testCase.Err) {
					t.Errorf("unexpected error %v, expected %v", err, testCase.Err)
				}
				return
			} else if testCase.Err != nil {
				t.Fatal("expected error")
			}
			imageNames := make([]string, len(images))
			for i, img := range images {
				imageNames[i] = img.Name
			}
			sort.Strings(imageNames)
			sort.Strings(testCase.Expected)
			if len(images) != len(testCase.Expected) {
				t.Fatalf("unexpected images:\n\t%v\nexpected:\n\t%v", imageNames, testCase.Expected)
			}
			for i := range imageNames {
				if imageNames[i] != testCase.Expected[i] {
					t.Fatalf("unexpected images:\n\t%v\nexpected:\n\t%v", imageNames, testCase.Expected)
				}
			}
		})
	}
}

// simpleImageStore is for testing images in memory,
// no filter support
type simpleImageStore struct {
	l      sync.Mutex
	images map[string]images.Image
}

func newSimpleImageStore() images.Store {
	return &simpleImageStore{
		images: make(map[string]images.Image),
	}
}

func (is *simpleImageStore) Get(ctx context.Context, name string) (images.Image, error) {
	is.l.Lock()
	defer is.l.Unlock()
	img, ok := is.images[name]
	if !ok {
		return images.Image{}, errdefs.ErrNotFound
	}
	return img, nil
}

func (is *simpleImageStore) List(ctx context.Context, filters ...string) ([]images.Image, error) {
	is.l.Lock()
	defer is.l.Unlock()
	var imgs []images.Image

	// filters not supported, return all
	for _, img := range is.images {
		imgs = append(imgs, img)
	}
	return imgs, nil
}

func (is *simpleImageStore) Create(ctx context.Context, image images.Image) (images.Image, error) {
	is.l.Lock()
	defer is.l.Unlock()

	if _, ok := is.images[image.Name]; ok {
		return images.Image{}, errdefs.ErrAlreadyExists
	}
	is.images[image.Name] = image

	return image, nil
}

func (is *simpleImageStore) Update(ctx context.Context, image images.Image, fieldpaths ...string) (images.Image, error) {
	is.l.Lock()
	defer is.l.Unlock()

	if _, ok := is.images[image.Name]; !ok {
		return images.Image{}, errdefs.ErrNotFound
	}
	// fieldpaths no supported, update entire image
	is.images[image.Name] = image

	return image, nil
}

func (is *simpleImageStore) Delete(ctx context.Context, name string, opts ...images.DeleteOpt) error {
	is.l.Lock()
	defer is.l.Unlock()

	if _, ok := is.images[name]; !ok {
		return errdefs.ErrNotFound
	}
	delete(is.images, name)

	return nil
}
