/*
   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 metadata

import (
	"errors"
	"fmt"
	"reflect"
	"testing"
	"time"

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

func TestImagesList(t *testing.T) {
	ctx, db := testEnv(t)
	store := NewImageStore(NewDB(db, nil, nil))

	testset := map[string]*images.Image{}
	for i := range 4 {
		id := "image-" + fmt.Sprint(i)
		testset[id] = &images.Image{
			Name: id,
			Labels: map[string]string{
				"namelabel": id,
				"even":      fmt.Sprint(i%2 == 0),
				"odd":       fmt.Sprint(i%2 != 0),
			},
			Target: ocispec.Descriptor{
				Size:      10,
				MediaType: "application/vnd.containerd.test",
				Digest:    digest.FromString(id),
				Annotations: map[string]string{
					"foo": "bar",
				},
			},
		}

		now := time.Now()
		result, err := store.Create(ctx, *testset[id])
		if err != nil {
			t.Fatal(err)
		}

		checkImageTimestamps(t, &result, now, true)
		testset[id].UpdatedAt, testset[id].CreatedAt = result.UpdatedAt, result.CreatedAt
		checkImagesEqual(t, &result, testset[id], "ensure that containers were created as expected for list")
	}

	for _, testcase := range []struct {
		name    string
		filters []string
	}{
		{
			name: "FullSet",
		},
		{
			name:    "FullSetFiltered", // full set, but because we have OR filter
			filters: []string{"labels.even==true", "labels.odd==true"},
		},
		{
			name:    "Even",
			filters: []string{"labels.even==true"},
		},
		{
			name:    "Odd",
			filters: []string{"labels.odd==true"},
		},
		{
			name:    "ByName",
			filters: []string{"name==image-0"},
		},
		{
			name:    "ByNameLabelEven",
			filters: []string{"labels.namelabel==image-0,labels.even==true"},
		},
		{
			name:    "ByMediaType",
			filters: []string{"target.mediatype~=application/vnd.*"},
		},
	} {
		t.Run(testcase.name, func(t *testing.T) {
			testset := testset
			if len(testcase.filters) > 0 {
				fs, err := filters.ParseAll(testcase.filters...)
				if err != nil {
					t.Fatal(err)
				}

				newtestset := make(map[string]*images.Image, len(testset))
				for k, v := range testset {
					if fs.Match(adaptImage(*v)) {
						newtestset[k] = v
					}
				}
				testset = newtestset
			}

			results, err := store.List(ctx, testcase.filters...)
			if err != nil {
				t.Fatal(err)
			}

			if len(results) == 0 { // all tests return a non-empty result set
				t.Fatalf("no results returned")
			}

			if len(results) != len(testset) {
				t.Fatalf("length of result does not match testset: %v != %v", len(results), len(testset))
			}

			for _, result := range results {
				checkImagesEqual(t, &result, testset[result.Name], "list results did not match")
			}
		})
	}

	// delete everything to test it
	for id := range testset {
		if err := store.Delete(ctx, id); err != nil {
			t.Fatal(err)
		}

		// try it again, get NotFound
		if err := store.Delete(ctx, id); !errdefs.IsNotFound(err) {
			t.Fatalf("unexpected error %v", err)
		}
	}
}
func TestImagesCreateUpdateDelete(t *testing.T) {
	ctx, db := testEnv(t)
	store := NewImageStore(NewDB(db, nil, nil))

	for _, testcase := range []struct {
		name       string
		original   images.Image
		createerr  error
		input      images.Image // Input target size determines target digest, base image uses 10
		fieldpaths []string
		expected   images.Image
		cause      error
		deleteerr  error
	}{
		{
			name:     "Touch",
			original: imageBase(),
			input: images.Image{
				Labels: map[string]string{
					"foo": "bar",
					"baz": "boo",
				},
				Target: ocispec.Descriptor{
					Size:      10,
					MediaType: "application/vnd.oci.blab",
					Annotations: map[string]string{
						"foo": "bar",
					},
				},
			},
			expected: images.Image{
				Labels: map[string]string{
					"foo": "bar",
					"baz": "boo",
				},
				Target: ocispec.Descriptor{
					Size:      10,
					MediaType: "application/vnd.oci.blab",
					Annotations: map[string]string{
						"foo": "bar",
					},
				},
			},
		},
		{
			name: "NoTarget",
			original: images.Image{
				Labels: map[string]string{
					"foo": "bar",
					"baz": "boo",
				},
				Target: ocispec.Descriptor{},
			},
			createerr: errdefs.ErrInvalidArgument,
		},
		{
			name:     "ReplaceLabels",
			original: imageBase(),
			input: images.Image{
				Labels: map[string]string{
					"for": "bar",
					"boo": "boo",
				},
				Target: ocispec.Descriptor{
					Size:      10,
					MediaType: "application/vnd.oci.blab",
					Annotations: map[string]string{
						"foo": "bar",
					},
				},
			},
			expected: images.Image{
				Labels: map[string]string{
					"for": "bar",
					"boo": "boo",
				},
				Target: ocispec.Descriptor{
					Size:      10,
					MediaType: "application/vnd.oci.blab",
					Annotations: map[string]string{
						"foo": "bar",
					},
				},
			},
		},
		{
			name:     "ReplaceLabelsFieldPath",
			original: imageBase(),
			input: images.Image{
				Labels: map[string]string{
					"for": "bar",
					"boo": "boo",
				},
				Target: ocispec.Descriptor{
					Size:      10,
					MediaType: "application/vnd.oci.blab+ignored", // make sure other stuff is ignored
					Annotations: map[string]string{
						"not": "bar",
						"new": "boo",
					},
				},
			},
			fieldpaths: []string{"labels"},
			expected: images.Image{
				Labels: map[string]string{
					"for": "bar",
					"boo": "boo",
				},
				Target: ocispec.Descriptor{
					Size:      10,
					MediaType: "application/vnd.oci.blab",
					Annotations: map[string]string{
						"foo": "bar",
						"baz": "boo",
					},
				},
			},
		},
		{
			name:     "ReplaceLabelsAnnotationsFieldPath",
			original: imageBase(),
			input: images.Image{
				Labels: map[string]string{
					"for": "bar",
					"boo": "boo",
				},
				Target: ocispec.Descriptor{
					Size:      10,
					MediaType: "application/vnd.oci.blab+ignored", // make sure other stuff is ignored
					Annotations: map[string]string{
						"foo": "boo",
					},
				},
			},
			fieldpaths: []string{"annotations", "labels"},
			expected: images.Image{
				Labels: map[string]string{
					"for": "bar",
					"boo": "boo",
				},
				Target: ocispec.Descriptor{
					Size:      10,
					MediaType: "application/vnd.oci.blab",
					Annotations: map[string]string{
						"foo": "boo",
					},
				},
			},
		},
		{
			name:     "ReplaceLabel",
			original: imageBase(),
			input: images.Image{
				Labels: map[string]string{
					"foo": "baz",
					"baz": "bunk",
				},
				Target: ocispec.Descriptor{
					Size:      10,
					MediaType: "application/vnd.oci.blab+ignored", // make sure other stuff is ignored
					Annotations: map[string]string{
						"foo": "bar",
					},
				},
			},
			fieldpaths: []string{"labels.foo"},
			expected: images.Image{
				Labels: map[string]string{
					"foo": "baz",
					"baz": "boo",
				},
				Target: ocispec.Descriptor{
					Size:      10,
					MediaType: "application/vnd.oci.blab",
					Annotations: map[string]string{
						"foo": "bar",
						"baz": "boo",
					},
				},
			},
		},
		{
			name:     "ReplaceAnnotation",
			original: imageBase(),
			input: images.Image{
				Labels: map[string]string{
					"foo": "baz",
					"baz": "bunk",
				},
				Target: ocispec.Descriptor{
					Size:      10,
					MediaType: "application/vnd.oci.blab+ignored", // make sure other stuff is ignored
					Annotations: map[string]string{
						"foo": "baz",
						"baz": "bunk",
					},
				},
			},
			fieldpaths: []string{"annotations.foo"},
			expected: images.Image{
				Labels: map[string]string{
					"foo": "bar",
					"baz": "boo",
				},
				Target: ocispec.Descriptor{
					Size:      10,
					MediaType: "application/vnd.oci.blab",
					Annotations: map[string]string{
						"foo": "baz",
						"baz": "boo",
					},
				},
			},
		},
		{
			name:     "ReplaceTargetTypeAndAnnotations", // target must be updated as a unit
			original: imageBase(),
			input: images.Image{
				Target: ocispec.Descriptor{
					Size:      10,
					MediaType: "application/vnd.oci.blab+replaced",
					Annotations: map[string]string{
						"fox": "dog",
					},
				},
			},
			fieldpaths: []string{"target"},
			expected: images.Image{
				Labels: map[string]string{
					"foo": "bar",
					"baz": "boo",
				},
				Target: ocispec.Descriptor{
					Size:      10,
					MediaType: "application/vnd.oci.blab+replaced",
					Annotations: map[string]string{
						"fox": "dog",
					},
				},
			},
		},
		{
			name:     "ReplaceTargetFieldpath", // target must be updated as a unit
			original: imageBase(),
			input: images.Image{
				Target: ocispec.Descriptor{
					Size:      20,
					MediaType: "application/vnd.oci.blab+replaced",
					Annotations: map[string]string{
						"fox": "dog",
					},
				},
			},
			fieldpaths: []string{"target"},
			expected: images.Image{
				Labels: map[string]string{ // Labels not updated
					"foo": "bar",
					"baz": "boo",
				},
				Target: ocispec.Descriptor{
					Size:      20,
					MediaType: "application/vnd.oci.blab+replaced",
					Annotations: map[string]string{
						"fox": "dog",
					},
				},
			},
			deleteerr: errdefs.ErrNotFound,
		},
		{
			name:     "ReplaceTarget", // target must be updated as a unit
			original: imageBase(),
			input: images.Image{
				Target: ocispec.Descriptor{
					Size:      20,
					MediaType: "application/vnd.oci.blab+replaced",
					Annotations: map[string]string{
						"fox": "dog",
					},
				},
			},
			expected: images.Image{
				Target: ocispec.Descriptor{
					Size:      20,
					MediaType: "application/vnd.oci.blab+replaced",
					Annotations: map[string]string{
						"fox": "dog",
					},
				},
			},
			deleteerr: errdefs.ErrNotFound,
		},
		{
			name:     "EmptySize",
			original: imageBase(),
			input: images.Image{
				Labels: map[string]string{
					"foo": "bar",
					"baz": "boo",
				},
				Target: ocispec.Descriptor{
					Size:      0,
					MediaType: "application/vnd.oci.blab",
					Annotations: map[string]string{
						"foo": "bar",
					},
				},
			},
			cause: errdefs.ErrInvalidArgument,
		},
		{
			name: "EmptySizeOnCreate",
			original: images.Image{
				Labels: map[string]string{
					"foo": "bar",
					"baz": "boo",
				},
				Target: ocispec.Descriptor{
					MediaType: "application/vnd.oci.blab",
					Annotations: map[string]string{
						"foo": "bar",
					},
				},
			},
			createerr: errdefs.ErrInvalidArgument,
		},
		{
			name:     "EmptyMediaType",
			original: imageBase(),
			input: images.Image{
				Labels: map[string]string{
					"foo": "bar",
					"baz": "boo",
				},
				Target: ocispec.Descriptor{
					Size: 10,
				},
			},
			cause: errdefs.ErrInvalidArgument,
		},
		{
			name: "EmptySizeOnCreate",
			original: images.Image{
				Labels: map[string]string{
					"foo": "bar",
					"baz": "boo",
				},
				Target: ocispec.Descriptor{
					Size: 10,
				},
			},
			createerr: errdefs.ErrInvalidArgument,
		},
		{
			name: "TryUpdateNameFail",
			original: images.Image{
				Labels: map[string]string{
					"foo": "bar",
					"baz": "boo",
				},
				Target: ocispec.Descriptor{
					Size:      10,
					MediaType: "application/vnd.oci.blab",
					Annotations: map[string]string{
						"foo": "bar",
					},
				},
			},
			input: images.Image{
				Name: "test should fail",
				Labels: map[string]string{
					"foo": "bar",
					"baz": "boo",
				},
				Target: ocispec.Descriptor{
					Size:      10,
					MediaType: "application/vnd.oci.blab",
					Annotations: map[string]string{
						"foo": "bar",
					},
				},
			},
			cause: errdefs.ErrNotFound,
		},
	} {
		t.Run(testcase.name, func(t *testing.T) {
			testcase.original.Name = testcase.name
			if testcase.input.Name == "" {
				testcase.input.Name = testcase.name
			}
			testcase.expected.Name = testcase.name

			testcase.original.Target.Digest = digest.FromString(fmt.Sprintf("%s-%d", testcase.name, testcase.original.Target.Size))
			testcase.input.Target.Digest = digest.FromString(fmt.Sprintf("%s-%d", testcase.name, testcase.input.Target.Size))
			testcase.expected.Target.Digest = testcase.input.Target.Digest

			// Create
			now := time.Now()
			created, err := store.Create(ctx, testcase.original)
			if !errors.Is(err, testcase.createerr) {
				if testcase.createerr == nil {
					t.Fatalf("unexpected error: %v", err)
				} else {
					t.Fatalf("cause of %v (cause: %v) != %v", err, errors.Unwrap(err), testcase.createerr)
				}
			} else if testcase.createerr != nil {
				return
			}

			checkImageTimestamps(t, &created, now, true)

			testcase.original.CreatedAt = created.CreatedAt
			testcase.expected.CreatedAt = created.CreatedAt
			testcase.original.UpdatedAt = created.UpdatedAt
			testcase.expected.UpdatedAt = created.UpdatedAt

			checkImagesEqual(t, &created, &testcase.original, "unexpected image on creation")

			// Update
			now = time.Now()
			updated, err := store.Update(ctx, testcase.input, testcase.fieldpaths...)
			if !errors.Is(err, testcase.cause) {
				if testcase.cause == nil {
					t.Fatalf("unexpected error: %v", err)
				} else {
					t.Fatalf("cause of %v (cause: %v) != %v", err, errors.Unwrap(err), testcase.cause)
				}
			} else if testcase.cause != nil {
				return
			}

			checkImageTimestamps(t, &updated, now, false)
			testcase.expected.UpdatedAt = updated.UpdatedAt
			checkImagesEqual(t, &updated, &testcase.expected, "updated failed to get expected result")

			// Get
			result, err := store.Get(ctx, testcase.original.Name)
			if err != nil {
				t.Fatal(err)
			}

			checkImagesEqual(t, &result, &testcase.expected, "get after failed to get expected result")

			if testcase.original.Target.Digest != testcase.expected.Target.Digest {
				t.Log("Delete should fail")
			}
			// Delete
			err = store.Delete(ctx, testcase.original.Name, images.DeleteTarget(&testcase.original.Target))
			if err != nil {
				if testcase.deleteerr == nil {
					t.Fatal(err)
				}
				if !errors.Is(err, testcase.deleteerr) {
					t.Fatal("unexpected error", err, ", expected", testcase.deleteerr)
				}
			} else if testcase.deleteerr != nil {
				t.Fatal("no error on deleted, expected", testcase.deleteerr)
			}
		})
	}
}

func imageBase() images.Image {
	return images.Image{
		Labels: map[string]string{
			"foo": "bar",
			"baz": "boo",
		},
		Target: ocispec.Descriptor{
			Size:      10,
			MediaType: "application/vnd.oci.blab",
			Annotations: map[string]string{
				"foo": "bar",
				"baz": "boo",
			},
		},
	}
}

func checkImageTimestamps(t *testing.T, im *images.Image, now time.Time, oncreate bool) {
	t.Helper()
	if im.UpdatedAt.IsZero() || im.CreatedAt.IsZero() {
		t.Fatalf("timestamps not set")
	}

	if oncreate {
		if !im.CreatedAt.Equal(im.UpdatedAt) {
			t.Fatal("timestamps should be equal on create")
		}

	} else {
		// ensure that updatedat is always after createdat
		if !im.UpdatedAt.After(im.CreatedAt) {
			t.Fatalf("timestamp for updatedat not after createdat: %v <= %v", im.UpdatedAt, im.CreatedAt)
		}
	}

	if im.UpdatedAt.Before(now) {
		t.Fatal("createdat time incorrect should be after the start of the operation")
	}
}

func checkImagesEqual(t *testing.T, a, b *images.Image, format string, args ...any) {
	t.Helper()
	if !reflect.DeepEqual(a, b) {
		t.Fatalf("images not equal \n\t%v != \n\t%v: "+format, append([]any{a, b}, args...)...)
	}
}
