//go:build !containers_image_storage_stub

package storage

import (
	"fmt"
	"strings"
	"testing"

	"github.com/containers/image/v5/docker/reference"
	"github.com/containers/image/v5/pkg/blobinfocache/memory"
	"github.com/containers/storage"
	"github.com/containers/storage/pkg/archive"
	"github.com/stretchr/testify/assert"
	"github.com/stretchr/testify/require"
)

func TestNewReference(t *testing.T) {
	newStore(t)
	st, ok := Transport.(*storageTransport)
	require.True(t, ok)
	// Success is tested throughout; test only the failure
	_, err := newReference(*st, nil, "")
	assert.Error(t, err)
	_, err = newReference(*st, nil, "ab")
	assert.Error(t, err)
	ref, err := reference.ParseNormalizedNamed("busybox")
	require.NoError(t, err)
	_, err = newReference(*st, ref, "")
	assert.Error(t, err)
}

func TestStorageReferenceTransport(t *testing.T) {
	newStore(t)
	ref, err := Transport.ParseReference("busybox")
	require.NoError(t, err)
	transport := ref.Transport()
	st, ok := transport.(*storageTransport)
	require.True(t, ok)
	assert.Equal(t, *(Transport.(*storageTransport)), *st)
}

// A common list of reference formats to test for the various ImageReference methods.
var validReferenceTestCases = []struct {
	input, dockerRef, canonical string
	namespaces                  []string
}{
	{
		"busybox", "docker.io/library/busybox:latest", "docker.io/library/busybox:latest",
		[]string{"docker.io/library/busybox", "docker.io/library", "docker.io"},
	},
	{
		"example.com/myns/ns2/busybox:notlatest", "example.com/myns/ns2/busybox:notlatest", "example.com/myns/ns2/busybox:notlatest",
		[]string{"example.com/myns/ns2/busybox", "example.com/myns/ns2", "example.com/myns", "example.com"},
	},
	{
		"@" + sha256digestHex, "", "@" + sha256digestHex,
		[]string{},
	},
	{
		"busybox@" + sha256digestHex, "docker.io/library/busybox:latest", "docker.io/library/busybox:latest@" + sha256digestHex,
		[]string{"docker.io/library/busybox:latest", "docker.io/library/busybox", "docker.io/library", "docker.io"},
	},
	{
		"busybox@sha256:" + sha256digestHex, "docker.io/library/busybox@sha256:" + sha256digestHex, "docker.io/library/busybox@sha256:" + sha256digestHex,
		[]string{"docker.io/library/busybox", "docker.io/library", "docker.io"},
	},
	{
		"busybox:notlatest@" + sha256digestHex, "docker.io/library/busybox:notlatest", "docker.io/library/busybox:notlatest@" + sha256digestHex,
		[]string{"docker.io/library/busybox:notlatest", "docker.io/library/busybox", "docker.io/library", "docker.io"},
	},
	{
		"busybox:notlatest@sha256:" + sha256digestHex, "docker.io/library/busybox:notlatest@sha256:" + sha256digestHex, "docker.io/library/busybox:notlatest@sha256:" + sha256digestHex,
		[]string{"docker.io/library/busybox:notlatest", "docker.io/library/busybox", "docker.io/library", "docker.io"},
	},
	{
		"busybox@" + sha256Digest2 + "@" + sha256digestHex, "docker.io/library/busybox@" + sha256Digest2, "docker.io/library/busybox@" + sha256Digest2 + "@" + sha256digestHex,
		[]string{"docker.io/library/busybox@" + sha256Digest2, "docker.io/library/busybox", "docker.io/library", "docker.io"},
	},
	{
		"busybox:notlatest@" + sha256Digest2 + "@" + sha256digestHex, "docker.io/library/busybox:notlatest@" + sha256Digest2, "docker.io/library/busybox:notlatest@" + sha256Digest2 + "@" + sha256digestHex,
		[]string{"docker.io/library/busybox:notlatest@" + sha256Digest2, "docker.io/library/busybox:notlatest", "docker.io/library/busybox", "docker.io/library", "docker.io"},
	},
}

func TestStorageReferenceDockerReference(t *testing.T) {
	newStore(t)
	for _, c := range validReferenceTestCases {
		ref, err := Transport.ParseReference(c.input)
		require.NoError(t, err, c.input)
		if c.dockerRef != "" {
			dr := ref.DockerReference()
			require.NotNil(t, dr, c.input)
			assert.Equal(t, c.dockerRef, dr.String(), c.input)
		} else {
			dr := ref.DockerReference()
			assert.Nil(t, dr, c.input)
		}
	}
}

// The […] part of references created for store
func storeSpecForStringWithinTransport(store storage.Store) string {
	optionsList := ""
	options := store.GraphOptions()
	if len(options) > 0 {
		optionsList = ":" + strings.Join(options, ",")
	}
	return fmt.Sprintf("[%s@%s+%s%s]", store.GraphDriverName(), store.GraphRoot(), store.RunRoot(), optionsList)
}

func TestStorageReferenceStringWithinTransport(t *testing.T) {
	store := newStore(t)
	storeSpec := storeSpecForStringWithinTransport(store)

	for _, c := range validReferenceTestCases {
		ref, err := Transport.ParseReference(c.input)
		require.NoError(t, err, c.input)
		assert.Equal(t, storeSpec+c.canonical, ref.StringWithinTransport(), c.input)
	}
}

func TestStorageReferencePolicyConfigurationIdentity(t *testing.T) {
	store := newStore(t)
	storeSpec := fmt.Sprintf("[%s@%s]", store.GraphDriverName(), store.GraphRoot())

	for _, c := range validReferenceTestCases {
		ref, err := Transport.ParseReference(c.input)
		require.NoError(t, err, c.input)
		assert.Equal(t, storeSpec+c.canonical, ref.PolicyConfigurationIdentity(), c.input)
	}
}

func TestStorageReferencePolicyConfigurationNamespaces(t *testing.T) {
	store := newStore(t)
	storeSpec := fmt.Sprintf("[%s@%s]", store.GraphDriverName(), store.GraphRoot())

	for _, c := range validReferenceTestCases {
		ref, err := Transport.ParseReference(c.input)
		require.NoError(t, err, c.input)
		expectedNS := []string{}
		for _, ns := range c.namespaces {
			expectedNS = append(expectedNS, storeSpec+ns)
		}
		expectedNS = append(expectedNS, storeSpec)
		expectedNS = append(expectedNS, fmt.Sprintf("[%s]", store.GraphRoot()))
		assert.Equal(t, expectedNS, ref.PolicyConfigurationNamespaces(), c.input)
	}
}

// NewImage, NewImageSource, NewImageDestination, DeleteImage tested in storage_test.go

func TestResolveReference(t *testing.T) {
	// This is, so far, only a minimal smoke test

	ensureTestCanCreateImages(t)

	store := newStore(t)
	storeSpec := storeSpecForStringWithinTransport(store)
	cache := memory.New()

	id := "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"

	// Create an image with a known name and ID
	ref, err := Transport.ParseStoreReference(store, "test@"+id)
	require.NoError(t, err)
	createImage(t, ref, cache, []testBlob{makeLayer(t, archive.Gzip)}, nil)

	for _, c := range []struct {
		input    string
		expected string // "" on error
	}{
		{ // No ID match
			"@bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb", "",
		},
		{"@" + id, "@" + id},                                  // ID-only lookup
		{"test", "docker.io/library/test:latest@" + id},       // Name is resolved to include ID
		{"nottest", ""},                                       // No name match
		{"test@" + id, "docker.io/library/test:latest@" + id}, // Name+ID works, and is unchanged
		{"nottest@" + id, ""},                                 // Name mismatch is rejected even with an ID
	} {
		input, err := Transport.ParseStoreReference(store, c.input)
		require.NoError(t, err, c.input)
		inputClone := *input
		resolved, img, err := ResolveReference(input)
		if c.expected == "" {
			assert.Error(t, err, c.input)
		} else {
			require.NoError(t, err, c.input)
			require.Equal(t, &inputClone, input) // input was not modified in-place
			assert.Equal(t, id, img.ID, c.input)
			assert.Equal(t, storeSpec+c.expected, resolved.StringWithinTransport(), c.input)
		}
	}
}
