package docker

import (
	"context"
	"strings"
	"testing"

	"github.com/stretchr/testify/assert"
	"github.com/stretchr/testify/require"
	"go.podman.io/image/v5/docker/reference"
	"go.podman.io/image/v5/types"
)

const (
	sha256digestHex         = "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef"
	sha256digest            = "@sha256:" + sha256digestHex
	unknownDigestSuffixTest = "@@unknown-digest@@"
)

func TestTransportName(t *testing.T) {
	assert.Equal(t, "docker", Transport.Name())
}

func TestTransportParseReference(t *testing.T) {
	testParseReference(t, Transport.ParseReference)
}

func TestTransportValidatePolicyConfigurationScope(t *testing.T) {
	for _, scope := range []string{
		"docker.io/library/busybox" + sha256digest,
		"docker.io/library/busybox:notlatest",
		"docker.io/library/busybox",
		"docker.io/library",
		"docker.io",
		"*.io",
	} {
		err := Transport.ValidatePolicyConfigurationScope(scope)
		assert.NoError(t, err, scope)
	}
}

func TestParseReference(t *testing.T) {
	testParseReference(t, ParseReference)
}

// testParseReference is a test shared for Transport.ParseReference and ParseReference.
func testParseReference(t *testing.T, fn func(string) (types.ImageReference, error)) {
	for _, c := range []struct {
		input, expected       string
		expectedUnknownDigest bool
	}{
		{"busybox", "", false}, // Missing // prefix
		{"//busybox:notlatest", "docker.io/library/busybox:notlatest", false},           // Explicit tag
		{"//busybox" + sha256digest, "docker.io/library/busybox" + sha256digest, false}, // Explicit digest
		{"//busybox", "docker.io/library/busybox:latest", false},                        // Default tag
		// A github.com/distribution/reference value can have a tag and a digest at the same time!
		// The docker/distribution API does not really support that (we can’t ask for an image with a specific
		// tag and digest), so fail.  This MAY be accepted in the future.
		{"//busybox:latest" + sha256digest, "", false},                                         // Both tag and digest
		{"//docker.io/library/busybox:latest", "docker.io/library/busybox:latest", false},      // All implied values explicitly specified
		{"//UPPERCASEISINVALID", "", false},                                                    // Invalid input
		{"//busybox" + unknownDigestSuffixTest, "docker.io/library/busybox", true},             // UnknownDigest suffix
		{"//example.com/ns/busybox" + unknownDigestSuffixTest, "example.com/ns/busybox", true}, // UnknownDigest with registry/repo
		{"//example.com/ns/busybox:tag1" + unknownDigestSuffixTest, "", false},                 // UnknownDigest with tag should fail
		{"//example.com/ns/busybox" + sha256digest + unknownDigestSuffixTest, "", false},       // UnknownDigest with digest should fail
	} {
		ref, err := fn(c.input)
		if c.expected == "" {
			assert.Error(t, err, c.input)
		} else {
			require.NoError(t, err, c.input)
			dockerRef, ok := ref.(dockerReference)
			require.True(t, ok, c.input)
			assert.Equal(t, c.expected, dockerRef.ref.String(), c.input)
			assert.Equal(t, c.expectedUnknownDigest, dockerRef.isUnknownDigest)
		}
	}
}

// A common list of reference formats to test for the various ImageReference methods.
var validReferenceTestCases = []struct {
	input, dockerRef, stringWithinTransport string
	expectedUnknownDigest                   bool
}{
	{"busybox:notlatest", "docker.io/library/busybox:notlatest", "//busybox:notlatest", false},                                                 // Explicit tag
	{"busybox" + sha256digest, "docker.io/library/busybox" + sha256digest, "//busybox" + sha256digest, false},                                  // Explicit digest
	{"docker.io/library/busybox:latest", "docker.io/library/busybox:latest", "//busybox:latest", false},                                        // All implied values explicitly specified
	{"example.com/ns/foo:bar", "example.com/ns/foo:bar", "//example.com/ns/foo:bar", false},                                                    // All values explicitly specified
	{"example.com/ns/busybox" + unknownDigestSuffixTest, "example.com/ns/busybox", "//example.com/ns/busybox" + unknownDigestSuffixTest, true}, // UnknownDigest Suffix full name
	{"busybox" + unknownDigestSuffixTest, "docker.io/library/busybox", "//busybox" + unknownDigestSuffixTest, true},                            // UnknownDigest short name
}

func TestNewReference(t *testing.T) {
	for _, c := range validReferenceTestCases {
		if strings.HasSuffix(c.input, unknownDigestSuffixTest) {
			continue
		}
		parsed, err := reference.ParseNormalizedNamed(c.input)
		require.NoError(t, err)
		ref, err := NewReference(parsed)
		require.NoError(t, err, c.input)
		dockerRef, ok := ref.(dockerReference)
		require.True(t, ok, c.input)
		assert.Equal(t, c.dockerRef, dockerRef.ref.String(), c.input)
		assert.Equal(t, false, dockerRef.isUnknownDigest)
	}

	// Neither a tag nor digest
	parsed, err := reference.ParseNormalizedNamed("busybox")
	require.NoError(t, err)
	_, err = NewReference(parsed)
	assert.Error(t, err)

	// A github.com/distribution/reference value can have a tag and a digest at the same time!
	parsed, err = reference.ParseNormalizedNamed("busybox:notlatest" + sha256digest)
	require.NoError(t, err)
	_, ok := parsed.(reference.Canonical)
	require.True(t, ok)
	_, ok = parsed.(reference.NamedTagged)
	require.True(t, ok)
	_, err = NewReference(parsed)
	assert.Error(t, err)
}

func TestNewReferenceUnknownDigest(t *testing.T) {
	// References with tags and digests should be rejected
	for _, c := range validReferenceTestCases {
		in, ok := strings.CutSuffix(c.input, unknownDigestSuffixTest)
		if !ok {
			parsed, err := reference.ParseNormalizedNamed(c.input)
			require.NoError(t, err)
			_, err = NewReferenceUnknownDigest(parsed)
			assert.Error(t, err)
			continue
		}
		parsed, err := reference.ParseNormalizedNamed(in)
		require.NoError(t, err)
		ref, err := NewReferenceUnknownDigest(parsed)
		require.NoError(t, err, c.input)
		dockerRef, ok := ref.(dockerReference)
		require.True(t, ok, c.input)
		assert.Equal(t, c.dockerRef, dockerRef.ref.String(), c.input)
		assert.Equal(t, true, dockerRef.isUnknownDigest)
	}
}

func TestReferenceTransport(t *testing.T) {
	ref, err := ParseReference("//busybox")
	require.NoError(t, err)
	assert.Equal(t, Transport, ref.Transport())
}

func TestReferenceStringWithinTransport(t *testing.T) {
	for _, c := range validReferenceTestCases {
		ref, err := ParseReference("//" + c.input)
		require.NoError(t, err, c.input)
		stringRef := ref.StringWithinTransport()
		assert.Equal(t, c.stringWithinTransport, stringRef, c.input)
		// Do one more round to verify that the output can be parsed, to an equal value.
		ref2, err := Transport.ParseReference(stringRef)
		require.NoError(t, err, c.input)
		stringRef2 := ref2.StringWithinTransport()
		assert.Equal(t, stringRef, stringRef2, c.input)
	}
}

func TestReferenceDockerReference(t *testing.T) {
	for _, c := range validReferenceTestCases {
		ref, err := ParseReference("//" + c.input)
		require.NoError(t, err, c.input)
		dockerRef := ref.DockerReference()
		require.NotNil(t, dockerRef, c.input)
		assert.Equal(t, c.dockerRef, dockerRef.String(), c.input)
	}
}

func TestReferencePolicyConfigurationIdentity(t *testing.T) {
	// Just a smoke test, the substance is tested in policyconfiguration.TestDockerReference.
	ref, err := ParseReference("//busybox")
	require.NoError(t, err)
	assert.Equal(t, "docker.io/library/busybox:latest", ref.PolicyConfigurationIdentity())

	ref, err = ParseReference("//busybox" + unknownDigestSuffixTest)
	require.NoError(t, err)
	assert.Equal(t, "docker.io/library/busybox", ref.PolicyConfigurationIdentity())
}

func TestReferencePolicyConfigurationNamespaces(t *testing.T) {
	// Just a smoke test, the substance is tested in policyconfiguration.TestDockerReference.
	ref, err := ParseReference("//busybox")
	require.NoError(t, err)
	assert.Equal(t, []string{
		"docker.io/library/busybox",
		"docker.io/library",
		"docker.io",
		"*.io",
	}, ref.PolicyConfigurationNamespaces())

	ref, err = ParseReference("//busybox" + unknownDigestSuffixTest)
	require.NoError(t, err)
	assert.Equal(t, []string{
		"docker.io/library",
		"docker.io",
		"*.io",
	}, ref.PolicyConfigurationNamespaces())
}

func TestReferenceNewImage(t *testing.T) {
	sysCtx := &types.SystemContext{
		RegistriesDirPath:        "/this/does/not/exist",
		DockerPerHostCertDirPath: "/this/does/not/exist",
		ArchitectureChoice:       "amd64",
		OSChoice:                 "linux",
	}
	ref, err := ParseReference("//quay.io/libpod/busybox")
	require.NoError(t, err)
	img, err := ref.NewImage(context.Background(), sysCtx)
	require.NoError(t, err)
	defer img.Close()

	// unknownDigest case should return error
	ref, err = ParseReference("//quay.io/libpod/busybox" + unknownDigestSuffixTest)
	require.NoError(t, err)
	_, err = ref.NewImage(context.Background(), sysCtx)
	assert.Error(t, err)
}

func TestReferenceNewImageSource(t *testing.T) {
	sysCtx := &types.SystemContext{
		RegistriesDirPath:        "/this/does/not/exist",
		DockerPerHostCertDirPath: "/this/does/not/exist",
	}
	ref, err := ParseReference("//quay.io/libpod/busybox")
	require.NoError(t, err)
	src, err := ref.NewImageSource(context.Background(), sysCtx)
	require.NoError(t, err)
	defer src.Close()

	// unknownDigest case should return error
	ref, err = ParseReference("//quay.io/libpod/busybox" + unknownDigestSuffixTest)
	require.NoError(t, err)
	_, err = ref.NewImageSource(context.Background(), sysCtx)
	assert.Error(t, err)
}

func TestReferenceNewImageDestination(t *testing.T) {
	ref, err := ParseReference("//quay.io/libpod/busybox")
	require.NoError(t, err)
	dest, err := ref.NewImageDestination(context.Background(),
		&types.SystemContext{RegistriesDirPath: "/this/does/not/exist", DockerPerHostCertDirPath: "/this/does/not/exist"})
	require.NoError(t, err)
	defer dest.Close()

	ref, err = ParseReference("//quay.io/libpod/busybox" + unknownDigestSuffixTest)
	require.NoError(t, err)
	dest2, err := ref.NewImageDestination(context.Background(),
		&types.SystemContext{RegistriesDirPath: "/this/does/not/exist", DockerPerHostCertDirPath: "/this/does/not/exist"})
	require.NoError(t, err)
	defer dest2.Close()
}

func TestReferenceTagOrDigest(t *testing.T) {
	for input, expected := range map[string]string{
		"//busybox:notlatest":      "notlatest",
		"//busybox" + sha256digest: "sha256:" + sha256digestHex,
	} {
		ref, err := ParseReference(input)
		require.NoError(t, err, input)
		dockerRef, ok := ref.(dockerReference)
		require.True(t, ok, input)
		tod, err := dockerRef.tagOrDigest()
		require.NoError(t, err, input)
		assert.Equal(t, expected, tod, input)
	}

	// Invalid input
	ref, err := reference.ParseNormalizedNamed("busybox")
	require.NoError(t, err)
	dockerRef := dockerReference{ref: ref}
	_, err = dockerRef.tagOrDigest()
	assert.Error(t, err)

	// Invalid input, unknownDigest case
	ref, err = reference.ParseNormalizedNamed("busybox")
	require.NoError(t, err)
	dockerRef = dockerReference{ref: ref, isUnknownDigest: true}
	_, err = dockerRef.tagOrDigest()
	assert.Error(t, err)
}
