/*
   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"
	"time"

	"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"},
		},
		{
			Name: "ImageNameWithPrefixAddDigest",
			ImageStore: &Store{
				imageName: "registry.test/index:latest",
				extraReferences: []Reference{
					{
						Name:      "registry.test/base",
						IsPrefix:  true,
						AddDigest: true,
					},
				},
			},
			Annotations: []string{"NoAnnotation"},
			Images:      []string{"registry.test/index:latest", "registry.test/base@"},
		},
		{
			Name: "ImageNameWithExtraRef",
			ImageStore: &Store{
				imageName: "registry.test/index:latest",
				extraReferences: []Reference{
					{
						Name: "registry.test/extra:v1",
					},
				},
			},
			Annotations: []string{"NoAnnotation"},
			Images:      []string{"registry.test/index:latest", "registry.test/extra:v1"},
		},
		{
			// SkipNamedDigest only suppresses digest refs in the annotation
			// branch, where a named reference has been resolved from the
			// descriptor's own annotations via the prefix. In the
			// NoAnnotation branch the top-level imageName is not a
			// prefix-matched name, so the digest ref must still be stored.
			// This matches the integration test DigestRefsSkipNamed where
			// the index digest reference is expected alongside the primary
			// image name.
			Name: "ImageNameWithPrefixAddDigestSkipNamed",
			ImageStore: &Store{
				imageName: "registry.test/index:latest",
				extraReferences: []Reference{
					{
						Name:            "registry.test/base",
						IsPrefix:        true,
						AddDigest:       true,
						SkipNamedDigest: true,
					},
				},
			},
			Annotations: []string{"NoAnnotation"},
			Images:      []string{"registry.test/index:latest", "registry.test/base@"},
		},
	} {
		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: expected %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)
				}
				primaryName := testCase.ImageStore.imageName
				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)
					}

					// The primary image is never collectible via back-reference;
					// extra references are tied to the primary via gc.bref.image
					// with an immediate gc.expire so they are collected alongside
					// it. When no primary image is set, extra references are
					// standalone and receive no GC labels.
					bref := imgs[i].Labels["containerd.io/gc.bref.image"]
					expire := imgs[i].Labels["containerd.io/gc.expire"]
					switch {
					case imgs[i].Name == primaryName:
						if bref != "" || expire != "" {
							t.Fatalf("primary image %q unexpectedly has GC labels: bref=%q expire=%q", imgs[i].Name, bref, expire)
						}
					case primaryName == "":
						if bref != "" || expire != "" {
							t.Fatalf("extra ref %q unexpectedly has GC labels when no primary image: bref=%q expire=%q", imgs[i].Name, bref, expire)
						}
					default:
						if bref != primaryName {
							t.Fatalf("extra ref %q has gc.bref.image=%q, expected %q", imgs[i].Name, bref, primaryName)
						}
						if expire == "" {
							t.Fatalf("extra ref %q missing gc.expire label", imgs[i].Name)
						}
						if _, err := time.Parse(time.RFC3339, expire); err != nil {
							t.Fatalf("extra ref %q has invalid gc.expire %q: %v", imgs[i].Name, expire, err)
						}
					}
				}
			})
		}

	}
}

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
}
