// FIXME(thaJeztah): remove once we are a module; the go:build directive prevents go from downgrading language version to go1.16:
//go:build go1.22

package containerd

import (
	"context"
	"fmt"
	"path/filepath"
	"slices"
	"testing"

	containerdimages "github.com/containerd/containerd/images"
	"github.com/containerd/containerd/namespaces"
	"github.com/containerd/platforms"
	"github.com/docker/docker/errdefs"
	"github.com/docker/docker/internal/testutils/specialimage"
	ocispec "github.com/opencontainers/image-spec/specs-go/v1"
	"gotest.tools/v3/assert"
	is "gotest.tools/v3/assert/cmp"
)

type pushTestCase struct {
	name               string
	indexPlatforms     []ocispec.Platform // all platforms supported by the image
	availablePlatforms []ocispec.Platform // platforms available locally
	requestPlatform    *ocispec.Platform  // platform requested by the client (not the platform selected for push!)
	check              func(t *testing.T, img containerdimages.Image, pushDescriptor ocispec.Descriptor, err error)
	daemonPlatform     *ocispec.Platform
}

func TestImagePushIndex(t *testing.T) {
	ctx := namespaces.WithNamespace(context.TODO(), "testing-"+t.Name())

	csDir := t.TempDir()
	store := &blobsDirContentStore{blobs: filepath.Join(csDir, "blobs/sha256")}

	linuxAmd64 := platforms.MustParse("linux/amd64")
	darwinArm64 := platforms.MustParse("darwin/arm64")
	windowsAmd64 := platforms.MustParse("windows/amd64")

	linuxArm64 := platforms.MustParse("linux/arm64")
	linuxArmv5 := platforms.MustParse("linux/arm/v5")
	linuxArmv7 := platforms.MustParse("linux/arm/v7")

	// Image service will have the daemon host platform mocked to linux/amd64.
	// Unless test cases specify a different platform.
	defaultDaemonPlatform := linuxAmd64

	for _, tc := range []pushTestCase{
		// No explicit platform requested
		{
			name: "none requested, all present",

			indexPlatforms:     []ocispec.Platform{linuxAmd64, darwinArm64, windowsAmd64},
			availablePlatforms: []ocispec.Platform{linuxAmd64, darwinArm64, windowsAmd64},
			check:              wholeIndexSelected,
		},
		{
			name: "none requested, one present",

			indexPlatforms:     []ocispec.Platform{linuxAmd64, darwinArm64, windowsAmd64},
			availablePlatforms: []ocispec.Platform{linuxAmd64},
			check:              singleManifestSelected(linuxAmd64),
		},
		{
			name: "none requested, two present, daemon platform available",

			indexPlatforms:     []ocispec.Platform{linuxAmd64, darwinArm64, windowsAmd64},
			availablePlatforms: []ocispec.Platform{linuxAmd64, darwinArm64},
			check:              singleManifestSelected(linuxAmd64),
		},
		{
			name: "none requested, two present, daemon platform NOT available",

			indexPlatforms:     []ocispec.Platform{linuxAmd64, darwinArm64, windowsAmd64},
			availablePlatforms: []ocispec.Platform{darwinArm64, windowsAmd64},
			check:              multipleCandidates,
		},

		// Specific platform requested
		{
			name: "linux/amd64 requested, all present",

			indexPlatforms:     []ocispec.Platform{linuxAmd64, darwinArm64, windowsAmd64},
			availablePlatforms: []ocispec.Platform{linuxAmd64, darwinArm64, windowsAmd64},
			requestPlatform:    &linuxAmd64,
			check:              singleManifestSelected(linuxAmd64),
		},
		{
			name: "linux/amd64 requested, but not present",

			indexPlatforms:     []ocispec.Platform{linuxAmd64, darwinArm64, windowsAmd64},
			availablePlatforms: []ocispec.Platform{darwinArm64, windowsAmd64},
			requestPlatform:    &linuxAmd64,
			check:              candidateNotFound,
		},

		// Variant tests
		{
			name: "linux/arm/v5 requested, but not in index",

			indexPlatforms:     []ocispec.Platform{linuxAmd64, linuxArmv7},
			availablePlatforms: []ocispec.Platform{linuxAmd64, linuxArmv7},
			requestPlatform:    &linuxArmv5,
			check:              candidateNotFound,
		},
		{
			name: "linux/arm/v5 requested, but not available",

			indexPlatforms:     []ocispec.Platform{linuxArm64, linuxArmv7, linuxArmv5},
			availablePlatforms: []ocispec.Platform{linuxArm64, linuxArmv7},
			requestPlatform:    &linuxArmv5,
			check:              candidateNotFound,
		},
		{
			name: "linux/arm/v7 requested, but not available",

			indexPlatforms:     []ocispec.Platform{linuxArm64, linuxArmv7, linuxArmv5},
			availablePlatforms: []ocispec.Platform{linuxArm64, linuxArmv5},
			requestPlatform:    &linuxArmv7,
			check:              candidateNotFound,
		},
		{
			name: "linux/arm/v7 requested on v7 daemon, but not available",

			indexPlatforms:     []ocispec.Platform{linuxArm64, linuxArmv7, linuxArmv5},
			availablePlatforms: []ocispec.Platform{linuxArm64, linuxArmv5},
			daemonPlatform:     &linuxArmv7,
			requestPlatform:    &linuxArmv7,
			check:              candidateNotFound,
		},
		{
			name: "linux/arm/v7 requested on v5 daemon, all available",

			indexPlatforms:     []ocispec.Platform{linuxArm64, linuxArmv7, linuxArmv5},
			availablePlatforms: []ocispec.Platform{linuxArm64, linuxArmv7, linuxArmv5},
			daemonPlatform:     &linuxArmv5,
			requestPlatform:    &linuxArmv7,
			check:              singleManifestSelected(linuxArmv7),
		},
		{
			name: "linux/arm/v5 requested on v7 daemon, all available",

			indexPlatforms:     []ocispec.Platform{linuxArm64, linuxArmv7, linuxArmv5},
			availablePlatforms: []ocispec.Platform{linuxArm64, linuxArmv7, linuxArmv5},
			daemonPlatform:     &linuxArmv7,
			requestPlatform:    &linuxArmv5,
			check:              singleManifestSelected(linuxArmv5),
		},
		{
			name: "none requested on v5 daemon, arm64 not available",

			indexPlatforms:     []ocispec.Platform{linuxArm64, linuxArmv7, linuxArmv5},
			availablePlatforms: []ocispec.Platform{linuxArmv7, linuxArmv5},
			daemonPlatform:     &linuxArmv5,
			requestPlatform:    nil,
			check:              singleManifestSelected(linuxArmv5),
		},
		{
			name: "none requested on v7 daemon, arm64 not available",

			indexPlatforms:     []ocispec.Platform{linuxArm64, linuxArmv7, linuxArmv5},
			availablePlatforms: []ocispec.Platform{linuxArmv7, linuxArmv5},
			daemonPlatform:     &linuxArmv7,
			requestPlatform:    nil,
			check:              singleManifestSelected(linuxArmv7),
		},
		{
			name: "none requested on v7 daemon, v7 not available",

			indexPlatforms:     []ocispec.Platform{linuxArm64, linuxArmv7, linuxArmv5},
			availablePlatforms: []ocispec.Platform{linuxArm64, linuxArmv5},
			daemonPlatform:     &linuxArmv7,
			requestPlatform:    nil,
			check:              singleManifestSelected(linuxArmv5), // Should it fail, because v5 can't be pushed?
		},

		{
			name: "none requested on v7 daemon, v5 in index but not v7, all present",

			indexPlatforms:     []ocispec.Platform{linuxArm64, linuxArmv5},
			availablePlatforms: []ocispec.Platform{linuxArm64, linuxArmv5},
			daemonPlatform:     &linuxArmv7,
			requestPlatform:    nil,
			check:              wholeIndexSelected,
		},
		{
			name: "none requested on v7 daemon, v5 in index but not v7, v5 present",

			indexPlatforms:     []ocispec.Platform{linuxArm64, linuxArmv5},
			availablePlatforms: []ocispec.Platform{linuxArmv5},
			daemonPlatform:     &linuxArmv7,
			requestPlatform:    nil,
			check:              singleManifestSelected(linuxArmv5),
		},
	} {
		t.Run(tc.name, func(t *testing.T) {
			imgSvc := fakeImageService(t, ctx, store)
			// Mock the daemon platform.
			if tc.daemonPlatform != nil {
				imgSvc.defaultPlatformOverride = platforms.Only(*tc.daemonPlatform)
			} else {
				imgSvc.defaultPlatformOverride = platforms.Only(defaultDaemonPlatform)
			}

			idx, _, err := specialimage.MultiPlatform(csDir, "multiplatform:latest", tc.indexPlatforms)
			assert.NilError(t, err)

			imgs := imagesFromIndex(idx)
			assert.Assert(t, is.Len(imgs, 1))

			img := imgs[0]
			_, err = imgSvc.images.Create(ctx, img)
			assert.NilError(t, err)

			for _, platform := range tc.indexPlatforms {
				if slices.ContainsFunc(tc.availablePlatforms, platforms.OnlyStrict(platform).Match) {
					continue
				}
				assert.NilError(t, deletePlatform(ctx, imgSvc, img, platform))
			}

			desc, err := imgSvc.getPushDescriptor(ctx, img, tc.requestPlatform)

			tc.check(t, img, desc, err)
		})
	}
}

func deletePlatform(ctx context.Context, imgSvc *ImageService, img containerdimages.Image, platform ocispec.Platform) error {
	var blobs []ocispec.Descriptor
	pm := platforms.OnlyStrict(platform)
	err := imgSvc.walkImageManifests(ctx, img, func(im *ImageManifest) error {
		imPlatform, err := im.ImagePlatform(ctx)
		if err != nil {
			return fmt.Errorf("failed to determine platform of image manifest %v: %w", im.Target(), err)
		}

		if !pm.Match(imPlatform) {
			return nil
		}

		return imgSvc.walkPresentChildren(ctx, im.Target(), func(ctx context.Context, d ocispec.Descriptor) error {
			blobs = append(blobs, d)
			return nil
		})
	})
	if err != nil {
		return fmt.Errorf("failed to walk image manifests: %w", err)
	}

	for _, d := range blobs {
		err := imgSvc.content.Delete(ctx, d.Digest)
		if err != nil {
			return fmt.Errorf("failed to delete blob %v: %w", d.Digest, err)
		}
	}

	return nil
}

// wholeIndexSelected asserts that the push descriptor candidate is for the whole index.
func wholeIndexSelected(t *testing.T, img containerdimages.Image, pushDescriptor ocispec.Descriptor, err error) {
	assert.NilError(t, err)
	assert.Check(t, is.Equal(pushDescriptor.Digest, img.Target.Digest))
}

// singleManifestSelected asserts that the push descriptor candidate is for a single platform-specific manifest.
func singleManifestSelected(platform ocispec.Platform) func(t *testing.T, img containerdimages.Image, pushDescriptor ocispec.Descriptor, err error) {
	pm := platforms.OnlyStrict(platform)
	return func(t *testing.T, img containerdimages.Image, pushDescriptor ocispec.Descriptor, err error) {
		assert.NilError(t, err)
		assert.Assert(t, is.Equal(pushDescriptor.MediaType, ocispec.MediaTypeImageManifest), "the push descriptor isn't for a manifest")
		assert.Assert(t, pushDescriptor.Platform != nil, "the push descriptor doesn't have a platform")
		assert.Assert(t, pm.Match(*pushDescriptor.Platform), "the push descriptor isn't for the selected platform")
	}
}

// candidateNotFound asserts that the no matching candidate was found.
func candidateNotFound(t *testing.T, _ containerdimages.Image, desc ocispec.Descriptor, err error) {
	assert.Check(t, errdefs.IsNotFound(err), "expected NotFound error, got %v, candidate: %v", err, desc.Platform)
}

// multipleCandidates asserts that multiple matching candidates were found and no decision could be made.
func multipleCandidates(t *testing.T, _ containerdimages.Image, desc ocispec.Descriptor, err error) {
	assert.Check(t, errdefs.IsConflict(err), "expected Conflict error, got %v, candidate: %v", err, desc.Platform)
}
