//go:build integration && linux

package integration

import (
	"compress/gzip"
	"context"
	"encoding/json"
	"errors"
	"fmt"
	"os"
	"path/filepath"
	"runtime"
	"strings"
	"testing"
	"time"

	"github.com/containerd/containerd/v2/client"
	"github.com/containerd/containerd/v2/core/images"
	"github.com/containerd/containerd/v2/pkg/namespaces"
	v1 "github.com/google/go-containerregistry/pkg/v1"
	"github.com/moby/moby/api/types/container"
	"github.com/samber/lo"
	"github.com/stretchr/testify/assert"
	"github.com/stretchr/testify/require"
	"github.com/testcontainers/testcontainers-go"
	"github.com/testcontainers/testcontainers-go/wait"

	"github.com/aquasecurity/trivy/internal/testutil"
	"github.com/aquasecurity/trivy/pkg/cache"
	"github.com/aquasecurity/trivy/pkg/fanal/analyzer"
	"github.com/aquasecurity/trivy/pkg/fanal/applier"
	"github.com/aquasecurity/trivy/pkg/fanal/artifact"
	aimage "github.com/aquasecurity/trivy/pkg/fanal/artifact/image"
	"github.com/aquasecurity/trivy/pkg/fanal/image"
	"github.com/aquasecurity/trivy/pkg/fanal/types"
)

func setupContainerd(t *testing.T, ctx context.Context, namespace string) *client.Client {
	t.Helper()
	tmpDir := t.TempDir()

	containerdDir := filepath.Join(tmpDir, "containerd")
	err := os.MkdirAll(containerdDir, os.ModePerm)
	require.NoError(t, err)

	socketPath := filepath.Join(containerdDir, "containerd.sock")

	// Set a containerd socket
	t.Setenv("CONTAINERD_ADDRESS", socketPath)
	t.Setenv("CONTAINERD_NAMESPACE", namespace)

	startContainerd(t, ctx, tmpDir)

	// Retry up to 3 times until containerd is ready
	var c *client.Client
	iteration, _, err := lo.AttemptWhileWithDelay(3, 1*time.Second, func(int, time.Duration) (error, bool) {
		c, err = client.New(socketPath)
		if err != nil {
			if !errors.Is(err, os.ErrPermission) {
				return err, false // unexpected error
			}
			return err, true
		}
		t.Cleanup(func() {
			require.NoError(t, c.Close())
		})
		return nil, false
	})
	require.NoErrorf(t, err, "attempted %d times ", iteration)

	return c
}

func startContainerd(t *testing.T, ctx context.Context, hostPath string) {
	t.Helper()
	t.Setenv("TESTCONTAINERS_RYUK_DISABLED", "true")

	// Load `containerd` image from tar file to avoid fetching it from remote registry
	cli := testutil.NewDockerClient(t)
	loadedImage := cli.ImageLoad(t, ctx, "../../../../integration/testdata/fixtures/images/containerd.tar.gz")

	req := testcontainers.ContainerRequest{
		Name:  "containerd",
		Image: loadedImage,
		Entrypoint: []string{
			"/bin/sh",
			"-c",
			"/usr/local/bin/containerd",
		},
		Mounts: testcontainers.Mounts(
			testcontainers.BindMount(hostPath, "/run"),
		),
		HostConfigModifier: func(hostConfig *container.HostConfig) {
			hostConfig.AutoRemove = true
		},
		WaitingFor: wait.ForLog("containerd successfully booted"),
	}
	containerdC, err := testcontainers.GenericContainer(ctx, testcontainers.GenericContainerRequest{
		ContainerRequest: req,
		Started:          true,
	})
	require.NoError(t, err)
	testcontainers.CleanupContainer(t, containerdC)

	_, _, err = containerdC.Exec(ctx, []string{
		"chmod",
		"666",
		"/run/containerd/containerd.sock",
	})
	require.NoError(t, err)
}

// Each of these tests imports an image and tags it with the name found in the
// `imageName` field. Then, the containerd store is searched by the reference
// provided in the `searchName` field.
func TestContainerd_SearchLocalStoreByNameOrDigest(t *testing.T) {
	// Each architecture needs different images and test cases.
	// Currently only amd64 architecture is supported
	if runtime.GOARCH != "amd64" {
		t.Skip("'Containerd' test only supports amd64 architecture")
	}

	digest := "sha256:f12582b2f2190f350e3904462c1c23aaf366b4f76705e97b199f9bbded1d816a"
	basename := "hello"
	tag := "world"
	importedImageOriginalName := testutil.ImageName("", "alpine-310", "")

	tests := []struct {
		name       string
		imageName  string
		searchName string
		wantErr    bool
	}{
		{
			name:       "familiarName:tag",
			imageName:  fmt.Sprintf("%s:%s", basename, tag),
			searchName: fmt.Sprintf("%s:%s", basename, tag),
		},
		{
			name:       "compound/name:tag",
			imageName:  fmt.Sprintf("say/%s:%s", basename, tag),
			searchName: fmt.Sprintf("say/%s:%s", basename, tag),
		},
		{
			name:       "docker.io/library/name:tag",
			imageName:  fmt.Sprintf("docker.io/library/%s:%s", basename, tag),
			searchName: fmt.Sprintf("docker.io/library/%s:%s", basename, tag),
		},
		{
			name:       "other-registry.io/library/name:tag",
			imageName:  fmt.Sprintf("other-registry.io/library/%s:%s", basename, tag),
			searchName: fmt.Sprintf("other-registry.io/library/%s:%s", basename, tag),
		},
		{
			name:       "other-registry.io/library/name:wrongTag should fail",
			imageName:  fmt.Sprintf("other-registry.io/library/%s:%s", basename, tag),
			searchName: fmt.Sprintf("other-registry.io/library/%s:badtag", basename),
			wantErr:    true,
		},
		{
			name:       "other-registry.io/library/wrongName:tag should fail",
			imageName:  fmt.Sprintf("other-registry.io/library/%s:%s", basename, tag),
			searchName: fmt.Sprintf("other-registry.io/library/badname:%s", tag),
			wantErr:    true,
		},
		{
			name:       "digest should succeed",
			imageName:  "",
			searchName: digest,
		},
		{
			name:       "wrong digest should fail",
			imageName:  "",
			searchName: "sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
			wantErr:    true,
		},
		{
			name:       "name@digest",
			imageName:  fmt.Sprintf("%s:%s", basename, tag),
			searchName: fmt.Sprintf("hello@%s", digest),
		},
		{
			name:       "compound/name@digest",
			imageName:  fmt.Sprintf("compound/%s:%s", basename, tag),
			searchName: fmt.Sprintf("compound/%s@%s", basename, digest),
		},
		{
			name:       "docker.io/library/name@digest",
			imageName:  fmt.Sprintf("docker.io/library/%s:%s", basename, tag),
			searchName: fmt.Sprintf("docker.io/library/%s@%s", basename, digest),
		},
		{
			name:       "otherdomain.io/name@digest",
			imageName:  fmt.Sprintf("otherdomain.io/%s:%s", basename, tag),
			searchName: fmt.Sprintf("otherdomain.io/%s@%s", basename, digest),
		},
		{
			name:       "wrongName@digest should fail",
			imageName:  fmt.Sprintf("%s:%s", basename, tag),
			searchName: fmt.Sprintf("badname@%s", digest),
			wantErr:    true,
		},
		{
			name:       "compound/wrongName@digest should fail",
			imageName:  fmt.Sprintf("compound/%s:%s", basename, tag),
			searchName: fmt.Sprintf("compound/badname@%s", digest),
			wantErr:    true,
		},
	}

	namespace := "default"
	ctx := namespaces.WithNamespace(context.Background(), namespace)
	client := setupContainerd(t, ctx, namespace)

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			archive, err := os.Open("../../../../integration/testdata/fixtures/images/alpine-310.tar.gz")
			require.NoError(t, err)

			uncompressedArchive, err := gzip.NewReader(archive)
			require.NoError(t, err)
			defer archive.Close()

			cacheDir := t.TempDir()
			c, err := cache.NewFSCache(cacheDir)
			require.NoError(t, err)

			imgs, err := client.Import(ctx, uncompressedArchive)
			require.NoError(t, err)
			_ = imgs

			digestTest := tt.imageName == ""

			if !digestTest {
				// Tag the image, taken from the code in `ctr image tag...`
				imgs[0].Name = tt.imageName
				_, err = client.ImageService().Create(ctx, imgs[0])
				require.NoError(t, err)

				// Remove the image by its original name, to ensure the image
				// is known only by the tag we have given it.
				err = client.ImageService().Delete(ctx, importedImageOriginalName, images.SynchronousDelete())
				require.NoError(t, err)
			}

			t.Cleanup(func() {
				for _, img := range imgs {
					err = client.ImageService().Delete(ctx, img.Name, images.SynchronousDelete())
					require.NoError(t, err)
				}
			})

			// enable only containerd
			img, cleanup, err := image.NewContainerImage(ctx, tt.searchName,
				types.ImageOptions{ImageSources: types.ImageSources{types.ContainerdImageSource}})
			defer cleanup()
			if tt.wantErr {
				require.Error(t, err)
				return
			}
			require.NoError(t, err)

			ar, err := aimage.NewArtifact(img, c, artifact.Option{})
			require.NoError(t, err)

			ref, err := ar.Inspect(ctx)
			require.NoError(t, err)

			if digestTest {
				actualDigest := strings.Split(ref.ImageMetadata.RepoDigests[0], "@")[1]
				require.Equal(t, tt.searchName, actualDigest)
				return
			}

			require.Equal(t, tt.searchName, ref.Name)
		})
	}
}

func TestContainerd_LocalImage(t *testing.T) {
	localImageTestWithNamespace(t, "default")
}

func TestContainerd_LocalImage_Alternative_Namespace(t *testing.T) {
	localImageTestWithNamespace(t, "test")
}

func localImageTestWithNamespace(t *testing.T, namespace string) {
	// Each architecture needs different images and test cases.
	// Currently only amd64 architecture is supported
	if runtime.GOARCH != "amd64" {
		t.Skip("'Containerd' test only supports amd64 architecture")
	}

	tests := []struct {
		name         string
		imageName    string
		tarArchive   string
		wantMetadata artifact.ImageMetadata
	}{
		{
			name:       "alpine 3.10",
			imageName:  testutil.ImageName("", "alpine-310", ""),
			tarArchive: "../../../../integration/testdata/fixtures/images/alpine-310.tar.gz",
			wantMetadata: artifact.ImageMetadata{
				ID: "sha256:961769676411f082461f9ef46626dd7a2d1e2b2a38e6a44364bcbecf51e66dd4",
				DiffIDs: []string{
					"sha256:03901b4a2ea88eeaad62dbe59b072b28b6efa00491962b8741081c5df50c65e0",
				},
				RepoTags:    []string{testutil.ImageName("", "alpine-310", "")},
				RepoDigests: []string{testutil.ImageName("", "", "sha256:f12582b2f2190f350e3904462c1c23aaf366b4f76705e97b199f9bbded1d816a")},
				Reference:   testutil.MustParseReference(t, testutil.ImageName("", "alpine-310", "")),
				ConfigFile: v1.ConfigFile{
					Architecture: "amd64",
					Created: v1.Time{
						Time: time.Date(2019, 8, 20, 20, 19, 55, 211423266, time.UTC),
					},
					OS: "linux",
					RootFS: v1.RootFS{
						Type: "layers",
						DiffIDs: []v1.Hash{
							{
								Algorithm: "sha256",
								Hex:       "03901b4a2ea88eeaad62dbe59b072b28b6efa00491962b8741081c5df50c65e0",
							},
						},
					},
					Config: v1.Config{
						Cmd: []string{
							"/bin/sh",
						},
						Env: []string{
							"PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin",
						},
						ArgsEscaped: true,
					},
					History: []v1.History{
						{
							Created:   v1.Time{Time: time.Date(2019, 8, 20, 20, 19, 55, 62606894, time.UTC)},
							CreatedBy: "/bin/sh -c #(nop) ADD file:fe64057fbb83dccb960efabbf1cd8777920ef279a7fa8dbca0a8801c651bdf7c in / ",
						},
						{
							Created:    v1.Time{Time: time.Date(2019, 8, 20, 20, 19, 55, 211423266, time.UTC)},
							CreatedBy:  "/bin/sh -c #(nop)  CMD [\"/bin/sh\"]",
							EmptyLayer: true,
						},
					},
				},
			},
		},
		{
			name:       "vulnimage",
			imageName:  testutil.ImageName("", "vulnimage", ""),
			tarArchive: "../../../../integration/testdata/fixtures/images/vulnimage.tar.gz",
			wantMetadata: artifact.ImageMetadata{
				ID:        "sha256:c17083664da903e13e9092fa3a3a1aeee2431aa2728298e3dbcec72f26369c41",
				Reference: testutil.MustParseReference(t, testutil.ImageName("", "vulnimage", "")),
				DiffIDs: []string{
					"sha256:ebf12965380b39889c99a9c02e82ba465f887b45975b6e389d42e9e6a3857888",
					"sha256:0ea33a93585cf1917ba522b2304634c3073654062d5282c1346322967790ef33",
					"sha256:9922bc15eeefe1637b803ef2106f178152ce19a391f24aec838cbe2e48e73303",
					"sha256:dc00fbef458ad3204bbb548e2d766813f593d857b845a940a0de76aed94c94d1",
					"sha256:5cb2a5009179b1e78ecfef81a19756328bb266456cf9a9dbbcf9af8b83b735f0",
					"sha256:9bdb2c849099a99c8ab35f6fd7469c623635e8f4479a0a5a3df61e22bae509f6",
					"sha256:6408527580eade39c2692dbb6b0f6a9321448d06ea1c2eef06bb7f37da9c5013",
					"sha256:83abef706f5ae199af65d1c13d737d0eb36219f0d18e36c6d8ff06159df39a63",
					"sha256:c03283c257abd289a30b4f5e9e1345da0e9bfdc6ca398ee7e8fac6d2c1456227",
					"sha256:2da3602d664dd3f71fae83cbc566d4e80b432c6ee8bb4efd94c8e85122f503d4",
					"sha256:82c59ac8ee582542648e634ca5aff9a464c68ff8a054f105a58689fb52209e34",
					"sha256:2f4a5c9187c249834ebc28783bd3c65bdcbacaa8baa6620ddaa27846dd3ef708",
					"sha256:6ca56f561e677ae06c3bc87a70792642d671a4416becb9a101577c1a6e090e36",
					"sha256:154ad0735c360b212b167f424d33a62305770a1fcfb6363882f5c436cfbd9812",
					"sha256:b2a1a2d80bf0c747a4f6b0ca6af5eef23f043fcdb1ed4f3a3e750aef2dc68079",
					"sha256:4d116f47cb2cc77a88d609b9805f2b011a5d42339b67300166654b3922685ac9",
					"sha256:9b1326af1cf81505fd8e596b7f622b679ae5d290e46b25214ba26e4f7c661d60",
					"sha256:a66245f885f2a210071e415f0f8ac4f21f5e4eab6c0435b4082e5c3637c411cb",
					"sha256:ba17950e91742d6ac7055ea3a053fe764486658ca1ce8188f1e427b1fe2bc4da",
					"sha256:6ef42db7800507577383edf1937cb203b9b85f619feed6046594208748ceb52c",
				},
				RepoTags:    []string{testutil.ImageName("", "vulnimage", "")},
				RepoDigests: []string{testutil.ImageName("", "", "sha256:e74abbfd81e00baaf464cf9e09f8b24926e5255171e3150a60aa341ce064688f")},
				ConfigFile: v1.ConfigFile{
					Architecture: "amd64",
					Created: v1.Time{
						Time: time.Date(2019, 8, 7, 7, 25, 58, 651649800, time.UTC),
					},
					OS: "linux",
					RootFS: v1.RootFS{
						Type: "layers",
						DiffIDs: []v1.Hash{
							{
								Algorithm: "sha256",
								Hex:       "ebf12965380b39889c99a9c02e82ba465f887b45975b6e389d42e9e6a3857888",
							},
							{
								Algorithm: "sha256",
								Hex:       "0ea33a93585cf1917ba522b2304634c3073654062d5282c1346322967790ef33",
							},
							{
								Algorithm: "sha256",
								Hex:       "9922bc15eeefe1637b803ef2106f178152ce19a391f24aec838cbe2e48e73303",
							},
							{
								Algorithm: "sha256",
								Hex:       "dc00fbef458ad3204bbb548e2d766813f593d857b845a940a0de76aed94c94d1",
							},
							{
								Algorithm: "sha256",
								Hex:       "5cb2a5009179b1e78ecfef81a19756328bb266456cf9a9dbbcf9af8b83b735f0",
							},
							{
								Algorithm: "sha256",
								Hex:       "9bdb2c849099a99c8ab35f6fd7469c623635e8f4479a0a5a3df61e22bae509f6",
							},
							{
								Algorithm: "sha256",
								Hex:       "6408527580eade39c2692dbb6b0f6a9321448d06ea1c2eef06bb7f37da9c5013",
							},
							{
								Algorithm: "sha256",
								Hex:       "83abef706f5ae199af65d1c13d737d0eb36219f0d18e36c6d8ff06159df39a63",
							},
							{
								Algorithm: "sha256",
								Hex:       "c03283c257abd289a30b4f5e9e1345da0e9bfdc6ca398ee7e8fac6d2c1456227",
							},
							{
								Algorithm: "sha256",
								Hex:       "2da3602d664dd3f71fae83cbc566d4e80b432c6ee8bb4efd94c8e85122f503d4",
							},
							{
								Algorithm: "sha256",
								Hex:       "82c59ac8ee582542648e634ca5aff9a464c68ff8a054f105a58689fb52209e34",
							},
							{
								Algorithm: "sha256",
								Hex:       "2f4a5c9187c249834ebc28783bd3c65bdcbacaa8baa6620ddaa27846dd3ef708",
							},
							{
								Algorithm: "sha256",
								Hex:       "6ca56f561e677ae06c3bc87a70792642d671a4416becb9a101577c1a6e090e36",
							},
							{
								Algorithm: "sha256",
								Hex:       "154ad0735c360b212b167f424d33a62305770a1fcfb6363882f5c436cfbd9812",
							},
							{
								Algorithm: "sha256",
								Hex:       "b2a1a2d80bf0c747a4f6b0ca6af5eef23f043fcdb1ed4f3a3e750aef2dc68079",
							},
							{
								Algorithm: "sha256",
								Hex:       "4d116f47cb2cc77a88d609b9805f2b011a5d42339b67300166654b3922685ac9",
							},
							{
								Algorithm: "sha256",
								Hex:       "9b1326af1cf81505fd8e596b7f622b679ae5d290e46b25214ba26e4f7c661d60",
							},
							{
								Algorithm: "sha256",
								Hex:       "a66245f885f2a210071e415f0f8ac4f21f5e4eab6c0435b4082e5c3637c411cb",
							},
							{
								Algorithm: "sha256",
								Hex:       "ba17950e91742d6ac7055ea3a053fe764486658ca1ce8188f1e427b1fe2bc4da",
							},
							{
								Algorithm: "sha256",
								Hex:       "6ef42db7800507577383edf1937cb203b9b85f619feed6046594208748ceb52c",
							},
						},
					},
					Config: v1.Config{
						Cmd: []string{"composer"},
						Env: []string{
							"PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin",
							"PHPIZE_DEPS=autoconf \t\tdpkg-dev dpkg \t\tfile \t\tg++ \t\tgcc \t\tlibc-dev \t\tmake \t\tpkgconf \t\tre2c",
							"PHP_INI_DIR=/usr/local/etc/php",
							"PHP_CFLAGS=-fstack-protector-strong -fpic -fpie -O2",
							"PHP_CPPFLAGS=-fstack-protector-strong -fpic -fpie -O2",
							"PHP_LDFLAGS=-Wl,-O1 -Wl,--hash-style=both -pie",
							"GPG_KEYS=1729F83938DA44E27BA0F4D3DBDB397470D12172 B1B44D8F021E4E2D6021E995DC9FF8D3EE5AF27F",
							"PHP_VERSION=7.2.11",
							"PHP_URL=https://secure.php.net/get/php-7.2.11.tar.xz/from/this/mirror",
							"PHP_ASC_URL=https://secure.php.net/get/php-7.2.11.tar.xz.asc/from/this/mirror",
							"PHP_SHA256=da1a705c0bc46410e330fc6baa967666c8cd2985378fb9707c01a8e33b01d985",
							"PHP_MD5=",
							"COMPOSER_ALLOW_SUPERUSER=1",
							"COMPOSER_HOME=/tmp",
							"COMPOSER_VERSION=1.7.2",
						},
						WorkingDir: "/app",
						Entrypoint: []string{
							"/bin/sh",
							"/docker-entrypoint.sh",
						},
						ArgsEscaped: true,
					},
					History: []v1.History{
						{
							Created:   v1.Time{Time: time.Date(2018, 9, 11, 22, 19, 38, 885299940, time.UTC)},
							CreatedBy: "/bin/sh -c #(nop) ADD file:49f9e47e678d868d5b023482aa8dded71276a241a665c4f8b55ca77269321b34 in / ",
						},
						{
							Created:    v1.Time{Time: time.Date(2018, 9, 11, 22, 19, 39, 58628442, time.UTC)},
							CreatedBy:  "/bin/sh -c #(nop)  CMD [\"/bin/sh\"]",
							EmptyLayer: true,
						},
						{
							Created:    v1.Time{Time: time.Date(2018, 9, 12, 1, 26, 59, 951316015, time.UTC)},
							CreatedBy:  "/bin/sh -c #(nop)  ENV PHPIZE_DEPS=autoconf \t\tdpkg-dev dpkg \t\tfile \t\tg++ \t\tgcc \t\tlibc-dev \t\tmake \t\tpkgconf \t\tre2c",
							EmptyLayer: true,
						},
						{
							Created:   v1.Time{Time: time.Date(2018, 9, 12, 1, 27, 1, 470388635, time.UTC)},
							CreatedBy: "/bin/sh -c apk add --no-cache --virtual .persistent-deps \t\tca-certificates \t\tcurl \t\ttar \t\txz \t\tlibressl",
						},
						{
							Created:   v1.Time{Time: time.Date(2018, 9, 12, 1, 27, 2, 432381785, time.UTC)},
							CreatedBy: "/bin/sh -c set -x \t&& addgroup -g 82 -S www-data \t&& adduser -u 82 -D -S -G www-data www-data",
						},
						{
							Created:    v1.Time{Time: time.Date(2018, 9, 12, 1, 27, 2, 715120309, time.UTC)},
							CreatedBy:  "/bin/sh -c #(nop)  ENV PHP_INI_DIR=/usr/local/etc/php",
							EmptyLayer: true,
						},
						{
							Created:   v1.Time{Time: time.Date(2018, 9, 12, 1, 27, 3, 655421341, time.UTC)},
							CreatedBy: "/bin/sh -c mkdir -p $PHP_INI_DIR/conf.d",
						},
						{
							Created:    v1.Time{Time: time.Date(2018, 9, 12, 1, 27, 3, 931799562, time.UTC)},
							CreatedBy:  "/bin/sh -c #(nop)  ENV PHP_CFLAGS=-fstack-protector-strong -fpic -fpie -O2",
							EmptyLayer: true,
						},
						{
							Created:    v1.Time{Time: time.Date(2018, 9, 12, 1, 27, 4, 210945499, time.UTC)},
							CreatedBy:  "/bin/sh -c #(nop)  ENV PHP_CPPFLAGS=-fstack-protector-strong -fpic -fpie -O2",
							EmptyLayer: true,
						},
						{
							Created:    v1.Time{Time: time.Date(2018, 9, 12, 1, 27, 4, 523116501, time.UTC)},
							CreatedBy:  "/bin/sh -c #(nop)  ENV PHP_LDFLAGS=-Wl,-O1 -Wl,--hash-style=both -pie",
							EmptyLayer: true,
						},
						{
							Created:    v1.Time{Time: time.Date(2018, 9, 12, 1, 27, 4, 795176159, time.UTC)},
							CreatedBy:  "/bin/sh -c #(nop)  ENV GPG_KEYS=1729F83938DA44E27BA0F4D3DBDB397470D12172 B1B44D8F021E4E2D6021E995DC9FF8D3EE5AF27F",
							EmptyLayer: true,
						},
						{
							Created:    v1.Time{Time: time.Date(2018, 10, 15, 19, 2, 18, 415761689, time.UTC)},
							CreatedBy:  "/bin/sh -c #(nop)  ENV PHP_VERSION=7.2.11",
							EmptyLayer: true,
						},
						{
							Created:    v1.Time{Time: time.Date(2018, 10, 15, 19, 2, 18, 599097853, time.UTC)},
							CreatedBy:  "/bin/sh -c #(nop)  ENV PHP_URL=https://secure.php.net/get/php-7.2.11.tar.xz/from/this/mirror PHP_ASC_URL=https://secure.php.net/get/php-7.2.11.tar.xz.asc/from/this/mirror",
							EmptyLayer: true,
						},
						{
							Created:    v1.Time{Time: time.Date(2018, 10, 15, 19, 2, 18, 782890412, time.UTC)},
							CreatedBy:  "/bin/sh -c #(nop)  ENV PHP_SHA256=da1a705c0bc46410e330fc6baa967666c8cd2985378fb9707c01a8e33b01d985 PHP_MD5=",
							EmptyLayer: true,
						},
						{
							Created:   v1.Time{Time: time.Date(2018, 10, 15, 19, 2, 22, 795846753, time.UTC)},
							CreatedBy: "/bin/sh -c set -xe; \t\tapk add --no-cache --virtual .fetch-deps \t\tgnupg \t\twget \t; \t\tmkdir -p /usr/src; \tcd /usr/src; \t\twget -O php.tar.xz \"$PHP_URL\"; \t\tif [ -n \"$PHP_SHA256\" ]; then \t\techo \"$PHP_SHA256 *php.tar.xz\" | sha256sum -c -; \tfi; \tif [ -n \"$PHP_MD5\" ]; then \t\techo \"$PHP_MD5 *php.tar.xz\" | md5sum -c -; \tfi; \t\tif [ -n \"$PHP_ASC_URL\" ]; then \t\twget -O php.tar.xz.asc \"$PHP_ASC_URL\"; \t\texport GNUPGHOME=\"$(mktemp -d)\"; \t\tfor key in $GPG_KEYS; do \t\t\tgpg --keyserver ha.pool.sks-keyservers.net --recv-keys \"$key\"; \t\tdone; \t\tgpg --batch --verify php.tar.xz.asc php.tar.xz; \t\tcommand -v gpgconf > /dev/null && gpgconf --kill all; \t\trm -rf \"$GNUPGHOME\"; \tfi; \t\tapk del .fetch-deps",
						},
						{
							Created:   v1.Time{Time: time.Date(2018, 10, 15, 19, 2, 23, 71406376, time.UTC)},
							CreatedBy: "/bin/sh -c #(nop) COPY file:207c686e3fed4f71f8a7b245d8dcae9c9048d276a326d82b553c12a90af0c0ca in /usr/local/bin/ ",
						},
						{
							Created:   v1.Time{Time: time.Date(2018, 10, 15, 19, 7, 13, 93396680, time.UTC)},
							CreatedBy: "/bin/sh -c set -xe \t&& apk add --no-cache --virtual .build-deps \t\t$PHPIZE_DEPS \t\tcoreutils \t\tcurl-dev \t\tlibedit-dev \t\tlibressl-dev \t\tlibsodium-dev \t\tlibxml2-dev \t\tsqlite-dev \t\t&& export CFLAGS=\"$PHP_CFLAGS\" \t\tCPPFLAGS=\"$PHP_CPPFLAGS\" \t\tLDFLAGS=\"$PHP_LDFLAGS\" \t&& docker-php-source extract \t&& cd /usr/src/php \t&& gnuArch=\"$(dpkg-architecture --query DEB_BUILD_GNU_TYPE)\" \t&& ./configure \t\t--build=\"$gnuArch\" \t\t--with-config-file-path=\"$PHP_INI_DIR\" \t\t--with-config-file-scan-dir=\"$PHP_INI_DIR/conf.d\" \t\t\t\t--enable-option-checking=fatal \t\t\t\t--with-mhash \t\t\t\t--enable-ftp \t\t--enable-mbstring \t\t--enable-mysqlnd \t\t--with-sodium=shared \t\t\t\t--with-curl \t\t--with-libedit \t\t--with-openssl \t\t--with-zlib \t\t\t\t$(test \"$gnuArch\" = 's390x-linux-gnu' && echo '--without-pcre-jit') \t\t\t\t$PHP_EXTRA_CONFIGURE_ARGS \t&& make -j \"$(nproc)\" \t&& make install \t&& { find /usr/local/bin /usr/local/sbin -type f -perm +0111 -exec strip --strip-all '{}' + || true; } \t&& make clean \t\t&& cp -v php.ini-* \"$PHP_INI_DIR/\" \t\t&& cd / \t&& docker-php-source delete \t\t&& runDeps=\"$( \t\tscanelf --needed --nobanner --format '%n#p' --recursive /usr/local \t\t\t| tr ',' '\\n' \t\t\t| sort -u \t\t\t| awk 'system(\"[ -e /usr/local/lib/\" $1 \" ]\") == 0 { next } { print \"so:\" $1 }' \t)\" \t&& apk add --no-cache --virtual .php-rundeps $runDeps \t\t&& apk del .build-deps \t\t&& pecl update-channels \t&& rm -rf /tmp/pear ~/.pearrc",
						},
						{
							Created:   v1.Time{Time: time.Date(2018, 10, 15, 19, 7, 13, 722586262, time.UTC)},
							CreatedBy: "/bin/sh -c #(nop) COPY multi:2cdcedabcf5a3b9ae610fab7848e94bc2f64b4d85710d55fd6f79e44dacf73d8 in /usr/local/bin/ ",
						},
						{
							Created:   v1.Time{Time: time.Date(2018, 10, 15, 19, 7, 14, 618087104, time.UTC)},
							CreatedBy: "/bin/sh -c docker-php-ext-enable sodium",
						},
						{
							Created:    v1.Time{Time: time.Date(2018, 10, 15, 19, 7, 14, 826981756, time.UTC)},
							CreatedBy:  "/bin/sh -c #(nop)  ENTRYPOINT [\"docker-php-entrypoint\"]",
							EmptyLayer: true,
						},
						{
							Created:    v1.Time{Time: time.Date(2018, 10, 15, 19, 7, 15, 10831572, time.UTC)},
							CreatedBy:  "/bin/sh -c #(nop)  CMD [\"php\" \"-a\"]",
							EmptyLayer: true,
						},
						{
							Created:   v1.Time{Time: time.Date(2018, 10, 15, 21, 28, 21, 919735971, time.UTC)},
							CreatedBy: "/bin/sh -c apk --no-cache add git subversion openssh mercurial tini bash patch",
						},
						{
							Created:   v1.Time{Time: time.Date(2018, 10, 15, 21, 28, 22, 611763893, time.UTC)},
							CreatedBy: "/bin/sh -c echo \"memory_limit=-1\" > \"$PHP_INI_DIR/conf.d/memory-limit.ini\"  && echo \"date.timezone=${PHP_TIMEZONE:-UTC}\" > \"$PHP_INI_DIR/conf.d/date_timezone.ini\"",
						},
						{
							Created:   v1.Time{Time: time.Date(2018, 10, 15, 21, 28, 50, 224278478, time.UTC)},
							CreatedBy: "/bin/sh -c apk add --no-cache --virtual .build-deps zlib-dev  && docker-php-ext-install zip  && runDeps=\"$(     scanelf --needed --nobanner --format '%n#p' --recursive /usr/local/lib/php/extensions     | tr ',' '\\n'     | sort -u     | awk 'system(\"[ -e /usr/local/lib/\" $1 \" ]\") == 0 { next } { print \"so:\" $1 }'     )\"  && apk add --virtual .composer-phpext-rundeps $runDeps  && apk del .build-deps",
						},
						{
							Created:    v1.Time{Time: time.Date(2018, 10, 15, 21, 28, 50, 503010161, time.UTC)},
							CreatedBy:  "/bin/sh -c #(nop)  ENV COMPOSER_ALLOW_SUPERUSER=1",
							EmptyLayer: true,
						},
						{
							Created:    v1.Time{Time: time.Date(2018, 10, 15, 21, 28, 50, 775378559, time.UTC)},
							CreatedBy:  "/bin/sh -c #(nop)  ENV COMPOSER_HOME=/tmp",
							EmptyLayer: true,
						},
						{
							Created:    v1.Time{Time: time.Date(2018, 10, 15, 21, 28, 51, 35012363, time.UTC)},
							CreatedBy:  "/bin/sh -c #(nop)  ENV COMPOSER_VERSION=1.7.2",
							EmptyLayer: true,
						},
						{
							Created:   v1.Time{Time: time.Date(2018, 10, 15, 21, 28, 52, 491402624, time.UTC)},
							CreatedBy: "/bin/sh -c curl --silent --fail --location --retry 3 --output /tmp/installer.php --url https://raw.githubusercontent.com/composer/getcomposer.org/b107d959a5924af895807021fcef4ffec5a76aa9/web/installer  && php -r \"     \\$signature = '544e09ee996cdf60ece3804abc52599c22b1f40f4323403c44d44fdfdd586475ca9813a858088ffbc1f233e9b180f061';     \\$hash = hash('SHA384', file_get_contents('/tmp/installer.php'));     if (!hash_equals(\\$signature, \\$hash)) {         unlink('/tmp/installer.php');         echo 'Integrity check failed, installer is either corrupt or worse.' . PHP_EOL;         exit(1);     }\"  && php /tmp/installer.php --no-ansi --install-dir=/usr/bin --filename=composer --version=${COMPOSER_VERSION}  && composer --ansi --version --no-interaction  && rm -rf /tmp/* /tmp/.htaccess",
						},
						{
							Created:   v1.Time{Time: time.Date(2018, 10, 15, 21, 28, 52, 948859545, time.UTC)},
							CreatedBy: "/bin/sh -c #(nop) COPY file:295943a303e8f27de4302b6aa3687bce4b1d1392335efaaab9ecd37bec5ab4c5 in /docker-entrypoint.sh ",
						},
						{
							Created:   v1.Time{Time: time.Date(2018, 10, 15, 21, 28, 53, 295399872, time.UTC)},
							CreatedBy: "/bin/sh -c #(nop) WORKDIR /app",
						},
						{
							Created:    v1.Time{Time: time.Date(2018, 10, 15, 21, 28, 53, 582920705, time.UTC)},
							CreatedBy:  "/bin/sh -c #(nop)  ENTRYPOINT [\"/bin/sh\" \"/docker-entrypoint.sh\"]",
							EmptyLayer: true,
						},
						{
							Created:    v1.Time{Time: time.Date(2018, 10, 15, 21, 28, 53, 798628678, time.UTC)},
							CreatedBy:  "/bin/sh -c #(nop)  CMD [\"composer\"]",
							EmptyLayer: true,
						},
						{
							Created:   v1.Time{Time: time.Date(2019, 8, 7, 7, 25, 57, 211142800, time.UTC)},
							CreatedBy: "/bin/sh -c #(nop) ADD file:842584685f26edb24dc305d76894f51cfda2bad0c24a05e727f9d4905d184a70 in /php-app/composer.lock ",
						},
						{
							Created:   v1.Time{Time: time.Date(2019, 8, 7, 7, 25, 57, 583779000, time.UTC)},
							CreatedBy: "/bin/sh -c #(nop) ADD file:c6d0373d380252b91829a5bb3c81d5b1afa574c91cef7752d18170a231c31f6d in /ruby-app/Gemfile.lock ",
						},
						{
							Created:   v1.Time{Time: time.Date(2019, 8, 7, 7, 25, 57, 921730100, time.UTC)},
							CreatedBy: "/bin/sh -c #(nop) ADD file:54a1c52556a5ebe98fd124f51c25d071f9e29e2714c72c80d6d3d254b9e83386 in /node-app/package-lock.json ",
						},
						{
							Created:   v1.Time{Time: time.Date(2019, 8, 7, 7, 25, 58, 311593100, time.UTC)},
							CreatedBy: "/bin/sh -c #(nop) ADD file:097d32f46acde76c4da9e55f17110d69d02cc6d16c86da907980da335fc0fc5f in /python-app/Pipfile.lock ",
						},
						{
							Created:   v1.Time{Time: time.Date(2019, 8, 7, 7, 25, 58, 651649800, time.UTC)},
							CreatedBy: "/bin/sh -c #(nop) ADD file:7f147d85de19bfb905c260a0c175f227a433259022c163017b96d0efacdcd105 in /rust-app/Cargo.lock ",
						},
					},
				},
			},
		},
	}

	t.Helper()
	ctx := namespaces.WithNamespace(context.Background(), namespace)
	client := setupContainerd(t, ctx, namespace)

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			cacheDir := t.TempDir()
			c, err := cache.NewFSCache(cacheDir)
			require.NoError(t, err)

			defer func() {
				c.Clear(t.Context())
				c.Close()
			}()

			archive, err := os.Open(tt.tarArchive)
			require.NoError(t, err)

			uncompressedArchive, err := gzip.NewReader(archive)
			require.NoError(t, err)
			defer archive.Close()

			_, err = client.Import(ctx, uncompressedArchive)
			require.NoError(t, err)

			// Enable only containerd
			img, cleanup, err := image.NewContainerImage(ctx, tt.imageName, types.ImageOptions{
				ImageSources: types.ImageSources{types.ContainerdImageSource},
			})
			require.NoError(t, err)
			defer cleanup()

			ar, err := aimage.NewArtifact(img, c, artifact.Option{
				DisabledAnalyzers: []analyzer.Type{
					analyzer.TypeExecutable,
					analyzer.TypeLicenseFile,
				},
			})
			require.NoError(t, err)

			ref, err := ar.Inspect(ctx)
			require.NoError(t, err)
			require.Equal(t, tt.wantMetadata, ref.ImageMetadata)

			a := applier.NewApplier(c)
			got, err := a.ApplyLayers(ctx, ref.ID, ref.BlobIDs)
			require.NoError(t, err)

			// Clear package detail fields
			clearPackageDetailFields(got.Packages)

			tag := strings.Split(tt.imageName, ":")[1]
			goldenFile := fmt.Sprintf("testdata/goldens/packages/%s.json.golden", tag)

			if *update {
				b, err := json.MarshalIndent(got.Packages, "", "  ")
				require.NoError(t, err)
				err = os.WriteFile(goldenFile, b, 0666)
				require.NoError(t, err)
			}

			// Parse a golden file
			golden, err := os.Open(goldenFile)
			require.NoError(t, err)
			defer golden.Close()

			var wantPkgs types.Packages
			err = json.NewDecoder(golden).Decode(&wantPkgs)
			require.NoError(t, err)

			// Assert
			assert.Equal(t, wantPkgs, got.Packages)
		})
	}
}

func TestContainerd_PullImage(t *testing.T) {
	// Each architecture needs different images and test cases.
	// Currently only amd64 architecture is supported
	if runtime.GOARCH != "amd64" {
		t.Skip("'Containerd' test only supports amd64 architecture")
	}

	tests := []struct {
		name         string
		imageName    string
		wantMetadata artifact.ImageMetadata
	}{
		{
			name:      "remote alpine 3.10",
			imageName: testutil.ImageName("", "alpine-310", ""),
			wantMetadata: artifact.ImageMetadata{
				ID: "sha256:961769676411f082461f9ef46626dd7a2d1e2b2a38e6a44364bcbecf51e66dd4",
				DiffIDs: []string{
					"sha256:03901b4a2ea88eeaad62dbe59b072b28b6efa00491962b8741081c5df50c65e0",
				},
				RepoTags:    []string{testutil.ImageName("", "alpine-310", "")},
				RepoDigests: []string{testutil.ImageName("", "", "sha256:72c42ed48c3a2db31b7dafe17d275b634664a708d901ec9fd57b1529280f01fb")},
				Reference:   testutil.MustParseReference(t, testutil.ImageName("", "alpine-310", "")),
				ConfigFile: v1.ConfigFile{
					Architecture: "amd64",
					Created: v1.Time{
						Time: time.Date(2019, 8, 20, 20, 19, 55, 211423266, time.UTC),
					},
					OS: "linux",
					RootFS: v1.RootFS{
						Type: "layers",
						DiffIDs: []v1.Hash{
							{
								Algorithm: "sha256",
								Hex:       "03901b4a2ea88eeaad62dbe59b072b28b6efa00491962b8741081c5df50c65e0",
							},
						},
					},
					Config: v1.Config{
						Cmd: []string{
							"/bin/sh",
						},
						Env: []string{
							"PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin",
						},
						ArgsEscaped: true,
					},
					History: []v1.History{
						{
							Created:   v1.Time{time.Date(2019, 8, 20, 20, 19, 55, 62606894, time.UTC)},
							CreatedBy: "/bin/sh -c #(nop) ADD file:fe64057fbb83dccb960efabbf1cd8777920ef279a7fa8dbca0a8801c651bdf7c in / ",
						},
						{
							Created:    v1.Time{time.Date(2019, 8, 20, 20, 19, 55, 211423266, time.UTC)},
							CreatedBy:  "/bin/sh -c #(nop)  CMD [\"/bin/sh\"]",
							EmptyLayer: true,
						},
					},
				},
			},
		},
	}

	namespace := "default"
	ctx := namespaces.WithNamespace(context.Background(), namespace)
	client := setupContainerd(t, ctx, namespace)

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			cacheDir := t.TempDir()
			c, err := cache.NewFSCache(cacheDir)
			require.NoError(t, err)

			defer func() {
				c.Clear(t.Context())
				c.Close()
			}()

			_, err = client.Pull(ctx, tt.imageName)
			require.NoError(t, err)

			// Enable only containerd
			img, cleanup, err := image.NewContainerImage(ctx, tt.imageName, types.ImageOptions{
				ImageSources: types.ImageSources{types.ContainerdImageSource},
			})
			require.NoError(t, err)
			defer cleanup()

			art, err := aimage.NewArtifact(img, c, artifact.Option{
				DisabledAnalyzers: []analyzer.Type{
					analyzer.TypeExecutable,
					analyzer.TypeLicenseFile,
				},
			})
			require.NoError(t, err)
			require.NotNil(t, art)

			ref, err := art.Inspect(context.Background())
			require.NoError(t, err)
			require.Equal(t, tt.wantMetadata, ref.ImageMetadata)

			a := applier.NewApplier(c)
			got, err := a.ApplyLayers(ctx, ref.ID, ref.BlobIDs)
			require.NoError(t, err)

			// Parse a golden file
			tag := strings.Split(tt.imageName, ":")[1]
			golden, err := os.Open(fmt.Sprintf("testdata/goldens/packages/%s.json.golden", tag))
			require.NoError(t, err)

			var wantPkgs types.Packages
			err = json.NewDecoder(golden).Decode(&wantPkgs)
			require.NoError(t, err)

			// Clear package detail fields for comparison
			clearPackageDetailFields(got.Packages)

			// Assert
			assert.Equal(t, wantPkgs, got.Packages)
		})
	}
}
