package signature

import (
	"context"
	"os"
	"path"
	"testing"

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

// dirImageMock returns a private.UnparsedImage for a directory, claiming a specified dockerReference.
func dirImageMock(t *testing.T, dir, dockerReference string) private.UnparsedImage {
	ref, err := reference.ParseNormalizedNamed(dockerReference)
	require.NoError(t, err)
	return dirImageMockWithRef(t, dir, refImageReferenceMock{ref: ref})
}

// dirImageMockWithRef returns a private.UnparsedImage for a directory, claiming a specified ref.
func dirImageMockWithRef(t *testing.T, dir string, ref types.ImageReference) private.UnparsedImage {
	srcRef, err := directory.NewReference(dir)
	require.NoError(t, err)
	src, err := srcRef.NewImageSource(context.Background(), nil)
	require.NoError(t, err)
	t.Cleanup(func() {
		err := src.Close()
		require.NoError(t, err)
	})
	return image.UnparsedInstance(&dirImageSourceMock{
		ImageSource: imagesource.FromPublic(src),
		ref:         ref,
	}, nil)
}

// dirImageSourceMock inherits dirImageSource, but overrides its Reference method.
type dirImageSourceMock struct {
	private.ImageSource
	ref types.ImageReference
}

func (d *dirImageSourceMock) Reference() types.ImageReference {
	return d.ref
}

func TestPRSignedByIsSignatureAuthorAccepted(t *testing.T) {
	ktGPG := SBKeyTypeGPGKeys
	prm := NewPRMMatchExact()
	testImage := dirImageMock(t, "fixtures/dir-img-valid", "testing/manifest:latest")
	testImageSig, err := os.ReadFile("fixtures/dir-img-valid/signature-1")
	require.NoError(t, err)
	keyData, err := os.ReadFile("fixtures/public-key.gpg")
	require.NoError(t, err)

	// Successful validation, with KeyPath, KeyPaths and KeyData.
	for _, fn := range []func() (PolicyRequirement, error){
		func() (PolicyRequirement, error) {
			return NewPRSignedByKeyPath(ktGPG, "fixtures/public-key.gpg", prm)
		},
		// Test the files in both orders, to make sure the correct public keys accepted in either position.
		func() (PolicyRequirement, error) {
			return NewPRSignedByKeyPaths(ktGPG, []string{"fixtures/public-key-1.gpg", "fixtures/public-key-1.gpg"}, prm)
		},
		func() (PolicyRequirement, error) {
			return NewPRSignedByKeyPaths(ktGPG, []string{"fixtures/public-key-2.gpg", "fixtures/public-key-1.gpg"}, prm)
		},
		func() (PolicyRequirement, error) {
			return NewPRSignedByKeyData(ktGPG, keyData, prm)
		},
	} {
		pr, err := fn()
		require.NoError(t, err)
		sar, parsedSig, err := pr.isSignatureAuthorAccepted(context.Background(), testImage, testImageSig)
		assertSARAccepted(t, sar, parsedSig, err, Signature{
			DockerManifestDigest: TestImageManifestDigest,
			DockerReference:      "testing/manifest:latest",
		})
	}

	// Unimplemented and invalid KeyType values
	for _, keyType := range []sbKeyType{
		SBKeyTypeSignedByGPGKeys,
		SBKeyTypeX509Certificates,
		SBKeyTypeSignedByX509CAs,
		sbKeyType("This is invalid"),
	} {
		// Do not use NewPRSignedByKeyData, because it would reject invalid values.
		pr := &prSignedBy{
			KeyType:        keyType,
			KeyData:        keyData,
			SignedIdentity: prm,
		}
		// Pass nil pointers to, kind of, test that the return value does not depend on the parameters.
		sar, parsedSig, err := pr.isSignatureAuthorAccepted(context.Background(), nil, nil)
		assertSARRejected(t, sar, parsedSig, err)
	}

	// Invalid KeyPath/KeyPaths/KeyData combinations.
	for _, fn := range []func() (PolicyRequirement, error){
		// Two or more of KeyPath, KeyPaths and KeyData set. Do not use NewPRSignedBy*, because it would reject this.
		func() (PolicyRequirement, error) {
			return &prSignedBy{KeyType: ktGPG, KeyPath: "fixtures/public-key.gpg", KeyPaths: []string{"fixtures/public-key-1.gpg", "fixtures/public-key-2.gpg"}, KeyData: keyData, SignedIdentity: prm}, nil
		},
		func() (PolicyRequirement, error) {
			return &prSignedBy{KeyType: ktGPG, KeyPath: "fixtures/public-key.gpg", KeyPaths: []string{"fixtures/public-key-1.gpg", "fixtures/public-key-2.gpg"}, SignedIdentity: prm}, nil
		},
		func() (PolicyRequirement, error) {
			return &prSignedBy{KeyType: ktGPG, KeyPath: "fixtures/public-key.gpg", KeyData: keyData, SignedIdentity: prm}, nil
		},
		func() (PolicyRequirement, error) {
			return &prSignedBy{KeyType: ktGPG, KeyPaths: []string{"fixtures/public-key-1.gpg", "fixtures/public-key-2.gpg"}, KeyData: keyData, SignedIdentity: prm}, nil
		},
		// None of KeyPath, KeyPaths and KeyData set. Do not use NewPRSignedBy*, because it would reject this.
		func() (PolicyRequirement, error) {
			return &prSignedBy{KeyType: ktGPG, SignedIdentity: prm}, nil
		},
		func() (PolicyRequirement, error) { // Invalid KeyPath
			return NewPRSignedByKeyPath(ktGPG, "/this/does/not/exist", prm)
		},
		func() (PolicyRequirement, error) { // Invalid KeyPaths
			return NewPRSignedByKeyPaths(ktGPG, []string{"/this/does/not/exist"}, prm)
		},
		func() (PolicyRequirement, error) { // One of the KeyPaths is invalid
			return NewPRSignedByKeyPaths(ktGPG, []string{"fixtures/public-key.gpg", "/this/does/not/exist"}, prm)
		},
	} {
		pr, err := fn()
		require.NoError(t, err)
		// Pass nil pointers to, kind of, test that the return value does not depend on the parameters.
		sar, parsedSig, err := pr.isSignatureAuthorAccepted(context.Background(), nil, nil)
		assertSARRejected(t, sar, parsedSig, err)
	}

	// Errors initializing the temporary GPG directory and mechanism are not obviously easy to reach.

	// KeyData has no public keys.
	pr, err := NewPRSignedByKeyData(ktGPG, []byte{}, prm)
	require.NoError(t, err)
	// Pass nil pointers to, kind of, test that the return value does not depend on the parameters.
	sar, parsedSig, err := pr.isSignatureAuthorAccepted(context.Background(), nil, nil)
	assertSARRejectedPolicyRequirement(t, sar, parsedSig, err)

	// A signature which does not GPG verify
	pr, err = NewPRSignedByKeyPath(ktGPG, "fixtures/public-key.gpg", prm)
	require.NoError(t, err)
	// Pass a nil pointer to, kind of, test that the return value does not depend on the image parameter.
	sar, parsedSig, err = pr.isSignatureAuthorAccepted(context.Background(), nil, []byte("invalid signature"))
	assertSARRejected(t, sar, parsedSig, err)

	// A valid signature using an unknown key.
	// (This is (currently?) rejected through the "mech.Verify fails" path, not the "!identityFound" path,
	// because we use a temporary directory and only import the trusted keys.)
	pr, err = NewPRSignedByKeyPath(ktGPG, "fixtures/public-key.gpg", prm)
	require.NoError(t, err)
	sig, err := os.ReadFile("fixtures/unknown-key.signature")
	require.NoError(t, err)
	// Pass a nil pointer to, kind of, test that the return value does not depend on the image parameter..
	sar, parsedSig, err = pr.isSignatureAuthorAccepted(context.Background(), nil, sig)
	assertSARRejected(t, sar, parsedSig, err)

	// A valid signature of an invalid JSON.
	pr, err = NewPRSignedByKeyPath(ktGPG, "fixtures/public-key.gpg", prm)
	require.NoError(t, err)
	sig, err = os.ReadFile("fixtures/invalid-blob.signature")
	require.NoError(t, err)
	// Pass a nil pointer to, kind of, test that the return value does not depend on the image parameter..
	sar, parsedSig, err = pr.isSignatureAuthorAccepted(context.Background(), nil, sig)
	assertSARRejected(t, sar, parsedSig, err)
	assert.IsType(t, InvalidSignatureError{}, err)

	// A valid signature with a rejected identity.
	nonmatchingPRM, err := NewPRMExactReference("this/does-not:match")
	require.NoError(t, err)
	pr, err = NewPRSignedByKeyPath(ktGPG, "fixtures/public-key.gpg", nonmatchingPRM)
	require.NoError(t, err)
	sar, parsedSig, err = pr.isSignatureAuthorAccepted(context.Background(), testImage, testImageSig)
	assertSARRejectedPolicyRequirement(t, sar, parsedSig, err)

	// Error reading image manifest
	image := dirImageMock(t, "fixtures/dir-img-no-manifest", "testing/manifest:latest")
	sig, err = os.ReadFile("fixtures/dir-img-no-manifest/signature-1")
	require.NoError(t, err)
	pr, err = NewPRSignedByKeyPath(ktGPG, "fixtures/public-key.gpg", prm)
	require.NoError(t, err)
	sar, parsedSig, err = pr.isSignatureAuthorAccepted(context.Background(), image, sig)
	assertSARRejected(t, sar, parsedSig, err)

	// Error computing manifest digest
	image = dirImageMock(t, "fixtures/dir-img-manifest-digest-error", "testing/manifest:latest")
	sig, err = os.ReadFile("fixtures/dir-img-manifest-digest-error/signature-1")
	require.NoError(t, err)
	pr, err = NewPRSignedByKeyPath(ktGPG, "fixtures/public-key.gpg", prm)
	require.NoError(t, err)
	sar, parsedSig, err = pr.isSignatureAuthorAccepted(context.Background(), image, sig)
	assertSARRejected(t, sar, parsedSig, err)

	// A valid signature with a non-matching manifest
	image = dirImageMock(t, "fixtures/dir-img-modified-manifest", "testing/manifest:latest")
	sig, err = os.ReadFile("fixtures/dir-img-modified-manifest/signature-1")
	require.NoError(t, err)
	pr, err = NewPRSignedByKeyPath(ktGPG, "fixtures/public-key.gpg", prm)
	require.NoError(t, err)
	sar, parsedSig, err = pr.isSignatureAuthorAccepted(context.Background(), image, sig)
	assertSARRejectedPolicyRequirement(t, sar, parsedSig, err)
}

// createInvalidSigDir creates a directory suitable for dirImageMock, in which image.Signatures()
// fails.
func createInvalidSigDir(t *testing.T) string {
	dir := t.TempDir()
	err := os.WriteFile(path.Join(dir, "manifest.json"), []byte("{}"), 0o644)
	require.NoError(t, err)
	// Creating a 000-permissions file would work for unprivileged accounts, but root (in particular,
	// in the Docker container we use for testing) would still have access.  So, create a symlink
	// pointing to itself, to cause an ELOOP. (Note that a symlink pointing to a nonexistent file would be treated
	// just like a nonexistent signature file, and not an error.)
	err = os.Symlink("signature-1", path.Join(dir, "signature-1"))
	require.NoError(t, err)
	return dir
}

func TestPRSignedByIsRunningImageAllowed(t *testing.T) {
	ktGPG := SBKeyTypeGPGKeys
	prm := NewPRMMatchExact()

	// A simple success case: single valid signature.
	image := dirImageMock(t, "fixtures/dir-img-valid", "testing/manifest:latest")
	pr, err := NewPRSignedByKeyPath(ktGPG, "fixtures/public-key.gpg", prm)
	require.NoError(t, err)
	allowed, err := pr.isRunningImageAllowed(context.Background(), image)
	assertRunningAllowed(t, allowed, err)

	// Error reading signatures
	invalidSigDir := createInvalidSigDir(t)
	image = dirImageMock(t, invalidSigDir, "testing/manifest:latest")
	pr, err = NewPRSignedByKeyPath(ktGPG, "fixtures/public-key.gpg", prm)
	require.NoError(t, err)
	allowed, err = pr.isRunningImageAllowed(context.Background(), image)
	assertRunningRejected(t, allowed, err)

	// No signatures
	image = dirImageMock(t, "fixtures/dir-img-unsigned", "testing/manifest:latest")
	pr, err = NewPRSignedByKeyPath(ktGPG, "fixtures/public-key.gpg", prm)
	require.NoError(t, err)
	allowed, err = pr.isRunningImageAllowed(context.Background(), image)
	assertRunningRejectedPolicyRequirement(t, allowed, err)

	// 1 invalid signature: use dir-img-valid, but a non-matching Docker reference
	image = dirImageMock(t, "fixtures/dir-img-valid", "testing/manifest:notlatest")
	pr, err = NewPRSignedByKeyPath(ktGPG, "fixtures/public-key.gpg", prm)
	require.NoError(t, err)
	allowed, err = pr.isRunningImageAllowed(context.Background(), image)
	assertRunningRejectedPolicyRequirement(t, allowed, err)

	// 2 valid signatures
	image = dirImageMock(t, "fixtures/dir-img-valid-2", "testing/manifest:latest")
	pr, err = NewPRSignedByKeyPath(ktGPG, "fixtures/public-key.gpg", prm)
	require.NoError(t, err)
	allowed, err = pr.isRunningImageAllowed(context.Background(), image)
	assertRunningAllowed(t, allowed, err)

	// One invalid, one valid signature (in this order)
	image = dirImageMock(t, "fixtures/dir-img-mixed", "testing/manifest:latest")
	pr, err = NewPRSignedByKeyPath(ktGPG, "fixtures/public-key.gpg", prm)
	require.NoError(t, err)
	allowed, err = pr.isRunningImageAllowed(context.Background(), image)
	assertRunningAllowed(t, allowed, err)

	// 2 invalid signatures: use dir-img-valid-2, but a non-matching Docker reference
	image = dirImageMock(t, "fixtures/dir-img-valid-2", "testing/manifest:notlatest")
	pr, err = NewPRSignedByKeyPath(ktGPG, "fixtures/public-key.gpg", prm)
	require.NoError(t, err)
	allowed, err = pr.isRunningImageAllowed(context.Background(), image)
	assertRunningRejectedPolicyRequirement(t, allowed, err)

	// A valid signature using a subkey
	image = dirImageMock(t, "fixtures/dir-img-valid-subkey", "testing/manifest:latest")
	pr, err = NewPRSignedByKeyPath(ktGPG, "fixtures/public-key-with-subkey.gpg", prm)
	require.NoError(t, err)
	allowed, err = pr.isRunningImageAllowed(context.Background(), image)
	assertRunningAllowed(t, allowed, err)
}

func TestPRSignedByVerifiesSignatures(t *testing.T) {
	pr, err := NewPRSignedByKeyPath(SBKeyTypeGPGKeys, "fixtures/public-key.gpg", NewPRMMatchExact())
	require.NoError(t, err)
	require.True(t, pr.verifiesSignatures())
}
