package image

import (
	"fmt"
	"slices"
	"strings"
	"testing"
	"time"

	"github.com/google/go-cmp/cmp/cmpopts"
	"github.com/moby/moby/api/types/image"
	"github.com/moby/moby/client"
	"github.com/moby/moby/client/pkg/versions"
	"github.com/moby/moby/v2/integration/internal/container"
	iimage "github.com/moby/moby/v2/integration/internal/image"
	"github.com/moby/moby/v2/internal/testutil"
	"github.com/moby/moby/v2/internal/testutil/daemon"
	"github.com/moby/moby/v2/internal/testutil/specialimage"
	ocispec "github.com/opencontainers/image-spec/specs-go/v1"
	"gotest.tools/v3/assert"
	is "gotest.tools/v3/assert/cmp"
	"gotest.tools/v3/skip"
)

// Regression : #38171
func TestImagesFilterMultiReference(t *testing.T) {
	ctx := setupTest(t)

	apiClient := testEnv.APIClient()

	name := strings.ToLower(t.Name())
	repoTags := []string{
		name + ":v1",
		name + ":v2",
		name + ":v3",
		name + ":v4",
	}

	for _, repoTag := range repoTags {
		_, err := apiClient.ImageTag(ctx, client.ImageTagOptions{Source: "busybox:latest", Target: repoTag})
		assert.NilError(t, err)
	}

	options := client.ImageListOptions{
		Filters: make(client.Filters).Add("reference", repoTags[:3]...),
	}
	imageList, err := apiClient.ImageList(ctx, options)
	assert.NilError(t, err)

	assert.Assert(t, is.Len(imageList.Items, 1))
	assert.Check(t, is.Len(imageList.Items[0].RepoTags, 3))
	for _, repoTag := range imageList.Items[0].RepoTags {
		if repoTag != repoTags[0] && repoTag != repoTags[1] && repoTag != repoTags[2] {
			t.Errorf("list images doesn't match any repoTag we expected, repoTag: %s", repoTag)
		}
	}
}

func TestImagesFilterUntil(t *testing.T) {
	ctx := setupTest(t)

	apiClient := testEnv.APIClient()

	name := strings.ToLower(t.Name())
	ctr := container.Create(ctx, t, apiClient, container.WithName(name))

	imgs := make([]string, 5)
	for i := range imgs {
		if i > 0 {
			// Make sure each image has a distinct timestamp.
			time.Sleep(time.Millisecond)
		}
		id, err := apiClient.ContainerCommit(ctx, ctr, client.ContainerCommitOptions{Reference: fmt.Sprintf("%s:v%d", name, i)})
		assert.NilError(t, err)
		imgs[i] = id.ID
	}

	olderImage, err := apiClient.ImageInspect(ctx, imgs[2])
	assert.NilError(t, err)
	olderUntil := olderImage.Created

	laterImage, err := apiClient.ImageInspect(ctx, imgs[3])
	assert.NilError(t, err)
	laterUntil := laterImage.Created

	filter := make(client.Filters).
		Add("since", imgs[0]).
		Add("until", olderUntil).
		Add("until", laterUntil).
		Add("before", imgs[len(imgs)-1])
	list, err := apiClient.ImageList(ctx, client.ImageListOptions{Filters: filter})
	assert.NilError(t, err)

	var listedIDs []string
	for _, i := range list.Items {
		t.Logf("ImageList: ID=%v RepoTags=%v", i.ID, i.RepoTags)
		listedIDs = append(listedIDs, i.ID)
	}
	assert.DeepEqual(t, listedIDs, imgs[1:2], cmpopts.SortSlices(func(a, b string) bool { return a < b }))
}

func TestImagesFilterBeforeSince(t *testing.T) {
	ctx := setupTest(t)

	apiClient := testEnv.APIClient()

	name := strings.ToLower(t.Name())
	ctr := container.Create(ctx, t, apiClient, container.WithName(name))

	imgs := make([]string, 5)
	for i := range imgs {
		if i > 0 {
			// Make sure each image has a distinct timestamp.
			time.Sleep(time.Millisecond)
		}
		id, err := apiClient.ContainerCommit(ctx, ctr, client.ContainerCommitOptions{Reference: fmt.Sprintf("%s:v%d", name, i)})
		assert.NilError(t, err)
		imgs[i] = id.ID
	}

	filter := make(client.Filters).
		Add("since", imgs[0]).
		Add("before", imgs[len(imgs)-1])
	list, err := apiClient.ImageList(ctx, client.ImageListOptions{Filters: filter})
	assert.NilError(t, err)

	var listedIDs []string
	for _, i := range list.Items {
		t.Logf("ImageList: ID=%v RepoTags=%v", i.ID, i.RepoTags)
		listedIDs = append(listedIDs, i.ID)
	}
	// The ImageList API sorts the list by created timestamp... truncated to
	// 1-second precision. Since all the images were created within
	// milliseconds of each other, listedIDs is effectively unordered and
	// the assertion must therefore be order-independent.
	assert.DeepEqual(t, listedIDs, imgs[1:len(imgs)-1], cmpopts.SortSlices(func(a, b string) bool { return a < b }))
}

func TestAPIImagesFilters(t *testing.T) {
	ctx := setupTest(t)
	apiClient := testEnv.APIClient()

	for _, n := range []string{"utest:tag1", "utest/docker:tag2", "utest:5000/docker:tag3"} {
		_, err := apiClient.ImageTag(ctx, client.ImageTagOptions{Source: "busybox:latest", Target: n})
		assert.NilError(t, err)
	}

	testcases := []struct {
		name             string
		filters          client.Filters
		expectedImages   int
		expectedRepoTags int
	}{
		{
			name:             "repository regex",
			filters:          make(client.Filters).Add("reference", "utest*/*"),
			expectedImages:   1,
			expectedRepoTags: 2,
		},
		{
			name:             "image name regex",
			filters:          make(client.Filters).Add("reference", "utest*"),
			expectedImages:   1,
			expectedRepoTags: 1,
		},
		{
			name:             "image name without a tag",
			filters:          make(client.Filters).Add("reference", "utest"),
			expectedImages:   1,
			expectedRepoTags: 1,
		},
		{
			name:             "registry port regex",
			filters:          make(client.Filters).Add("reference", "*5000*/*"),
			expectedImages:   1,
			expectedRepoTags: 1,
		},
		{
			name:             "canonical name without a tag",
			filters:          make(client.Filters).Add("reference", "docker.io/library/utest"),
			expectedImages:   1,
			expectedRepoTags: 1,
		},
		{
			name:             "canonical name with glob tag",
			filters:          make(client.Filters).Add("reference", "docker.io/library/utest:*"),
			expectedImages:   1,
			expectedRepoTags: 1,
		},
		{
			name:             "canonical namespaced name",
			filters:          make(client.Filters).Add("reference", "docker.io/utest/docker"),
			expectedImages:   1,
			expectedRepoTags: 1,
		},
	}

	for _, tc := range testcases {
		t.Run(tc.name, func(t *testing.T) {
			t.Parallel()

			ctx := testutil.StartSpan(ctx, t)
			imageList, err := apiClient.ImageList(ctx, client.ImageListOptions{
				Filters: tc.filters,
			})
			assert.Check(t, err)
			assert.Assert(t, is.Len(imageList.Items, tc.expectedImages))
			assert.Check(t, is.Len(imageList.Items[0].RepoTags, tc.expectedRepoTags))
		})
	}
}

// Verify that the size calculation operates on ChainIDs and not DiffIDs.
// This test calls an image list with two images that share one, top layer.
func TestAPIImagesListSizeShared(t *testing.T) {
	skip.If(t, testEnv.DaemonInfo.OSType != "linux")

	ctx := setupTest(t)

	d := daemon.New(t)
	d.Start(t)
	defer d.Stop(t)

	apiClient := d.NewClientT(t)

	iimage.Load(ctx, t, apiClient, func(dir string) (*ocispec.Index, error) {
		return specialimage.MultiLayerCustom(dir, "multilayer:latest", []specialimage.SingleFileLayer{
			{Name: "bar", Content: []byte("2")},
			{Name: "foo", Content: []byte("1")},
		})
	})

	iimage.Load(ctx, t, apiClient, func(dir string) (*ocispec.Index, error) {
		return specialimage.MultiLayerCustom(dir, "multilayer2:latest", []specialimage.SingleFileLayer{
			{Name: "asdf", Content: []byte("3")},
			{Name: "foo", Content: []byte("1")},
		})
	})

	_, err := apiClient.ImageList(ctx, client.ImageListOptions{SharedSize: true})
	assert.NilError(t, err)
}

func TestAPIImagesListManifests(t *testing.T) {
	skip.If(t, !testEnv.UsingSnapshotter())
	// Sub-daemons not supported on Windows
	skip.If(t, testEnv.DaemonInfo.OSType == "windows")

	ctx := setupTest(t)

	d := daemon.New(t)
	d.Start(t)
	defer d.Stop(t)

	apiClient := d.NewClientT(t)

	testPlatforms := []ocispec.Platform{
		{OS: "windows", Architecture: "amd64"},
		{OS: "linux", Architecture: "arm", Variant: "v7"},
		{OS: "darwin", Architecture: "arm64"},
	}
	iimage.Load(ctx, t, apiClient, func(dir string) (*ocispec.Index, error) {
		idx, _, err := specialimage.MultiPlatform(dir, "multiplatform:latest", testPlatforms)
		return idx, err
	})

	containerPlatform := testPlatforms[1]

	cid := container.Create(ctx, t, apiClient,
		container.WithImage("multiplatform:latest"),
		container.WithPlatform(&containerPlatform))

	t.Run("unsupported before 1.47", func(t *testing.T) {
		// TODO: Remove when MinAPIVersion >= 1.47
		c := d.NewClientT(t, client.WithAPIVersion("1.46"))

		imageList, err := c.ImageList(ctx, client.ImageListOptions{Manifests: true})
		assert.NilError(t, err)

		assert.Assert(t, is.Len(imageList.Items, 1))
		assert.Check(t, is.Nil(imageList.Items[0].Manifests))
	})

	skip.If(t, versions.LessThan(testEnv.DaemonAPIVersion(), "1.47"), "requires API v1.47")

	api147 := d.NewClientT(t, client.WithAPIVersion("1.47"))

	t.Run("no manifests if not requested", func(t *testing.T) {
		imageList, err := api147.ImageList(ctx, client.ImageListOptions{})
		assert.NilError(t, err)

		assert.Assert(t, is.Len(imageList.Items, 1))
		assert.Check(t, is.Nil(imageList.Items[0].Manifests))
	})

	imageList, err := api147.ImageList(ctx, client.ImageListOptions{Manifests: true})
	assert.NilError(t, err)

	assert.Check(t, is.Len(imageList.Items, 1))
	assert.Check(t, imageList.Items[0].Manifests != nil)
	assert.Check(t, is.Len(imageList.Items[0].Manifests, 3))

	for _, mfst := range imageList.Items[0].Manifests {
		// All manifests should be image manifests
		assert.Check(t, is.Equal(mfst.Kind, image.ManifestKindImage))

		// Full image was loaded so all manifests should be available
		assert.Check(t, mfst.Available)

		// The platform should be one of the test platforms
		if assert.Check(t, is.Contains(testPlatforms, mfst.ImageData.Platform)) {
			testPlatforms = slices.DeleteFunc(testPlatforms, func(p ocispec.Platform) bool {
				op := mfst.ImageData.Platform
				return p.OS == op.OS && p.Architecture == op.Architecture && p.Variant == op.Variant
			})
		}

		if mfst.ImageData.Platform.OS == containerPlatform.OS &&
			mfst.ImageData.Platform.Architecture == containerPlatform.Architecture &&
			mfst.ImageData.Platform.Variant == containerPlatform.Variant {

			assert.Check(t, is.DeepEqual(mfst.ImageData.Containers, []string{cid}))
		}
	}
}
