package policy

import (
	"context"
	"testing"
	"time"

	gwpb "github.com/moby/buildkit/frontend/gateway/pb"
	"github.com/moby/buildkit/solver/pb"
	moby_buildkit_v1_sourcepolicy "github.com/moby/buildkit/sourcepolicy/pb"
	"github.com/moby/buildkit/sourcepolicy/policysession"
	policyimage "github.com/moby/policy-helpers/image"
	policytypes "github.com/moby/policy-helpers/types"
	ocispecs "github.com/opencontainers/image-spec/specs-go/v1"
	"github.com/sigstore/sigstore-go/pkg/fulcio/certificate"
	"github.com/sirupsen/logrus"
	"github.com/stretchr/testify/require"
)

// makeDefaultPolicy returns a Policy instance backed by the embedded
// default policy module, optionally wired to a mock signature verifier
// returning the supplied SignatureInfo for image attestations.
func makeDefaultPolicy(t *testing.T, sigInfo *policytypes.SignatureInfo) *Policy {
	t.Helper()

	var verifierProvider PolicyVerifierProvider
	if sigInfo != nil {
		verifierProvider = func() (PolicyVerifier, error) {
			return &mockPolicyVerifier{
				verifyImage: func(_ context.Context, _ policyimage.ReferrersProvider, _ ocispecs.Descriptor, _ *ocispecs.Platform) (*policytypes.SignatureInfo, error) {
					return sigInfo, nil
				},
			}, nil
		}
	}
	return NewPolicy(Opt{
		Files: []File{{
			Filename: DefaultPolicyFilename,
			Data:     DefaultPolicyData(),
		}},
		Log: func(level logrus.Level, msg string) {
			t.Logf("[%s] %s", level, msg)
		},
		VerifierProvider: verifierProvider,
	})
}

// dockerGithubBuilderSig returns a SignatureInfo that satisfies the
// docker_github_builder_signature helper for the given source repository
// and ref. Pass an empty ref to omit the SourceRepositoryRef field.
func dockerGithubBuilderSig(sourceRepo, sourceRef string) *policytypes.SignatureInfo {
	return &policytypes.SignatureInfo{
		Kind:          policytypes.KindDockerGithubBuilder,
		SignatureType: policytypes.SignatureBundleV03,
		Timestamps: []policytypes.TimestampVerificationResult{
			{Type: "rekor", URI: "https://rekor.sigstore.dev", Timestamp: time.Date(2024, 1, 1, 0, 0, 0, 0, time.UTC)},
		},
		Signer: &certificate.Summary{
			CertificateIssuer: "CN=sigstore-intermediate,O=sigstore.dev",
			Extensions: certificate.Extensions{
				Issuer:              "https://token.actions.githubusercontent.com",
				SourceRepositoryURI: "https://github.com/" + sourceRepo,
				SourceRepositoryRef: sourceRef,
				RunnerEnvironment:   "github-hosted",
			},
		},
	}
}

// runDefaultPolicyImage evaluates the default policy against the given image
// reference. An attestation chain and an empty image config are always
// supplied so that the policy can fully resolve metadata and produce a
// decision (rather than requesting more data via the next response).
func runDefaultPolicyImage(t *testing.T, p *Policy, ref string) *policysession.DecisionResponse {
	t.Helper()
	src := &gwpb.ResolveSourceMetaResponse{
		Source: &pb.SourceOp{Identifier: "docker-image://" + ref},
		Image: &gwpb.ResolveSourceImageResponse{
			Digest:           "sha256:abababababababababababababababababababababababababababababababab",
			Config:           []byte(`{"created":"2024-01-01T00:00:00Z","config":{}}`),
			AttestationChain: newTestAttestationChain(t),
		},
	}
	resp, _, err := p.CheckPolicy(context.Background(), &policysession.CheckPolicyRequest{
		Platform: &pb.Platform{OS: "linux", Architecture: "amd64"},
		Source:   src,
	})
	require.NoError(t, err)
	require.NotNil(t, resp)
	return resp
}

func TestDefaultPolicyAllowsNonImageSources(t *testing.T) {
	p := makeDefaultPolicy(t, nil)
	src := &gwpb.ResolveSourceMetaResponse{
		Source: &pb.SourceOp{Identifier: "https://example.com/foo.tar.gz"},
		HTTP: &gwpb.ResolveSourceHTTPResponse{
			Checksum: "sha256:0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef",
		},
	}
	resp, _, err := p.CheckPolicy(context.Background(), &policysession.CheckPolicyRequest{
		Source: src,
	})
	require.NoError(t, err)
	require.Equal(t, moby_buildkit_v1_sourcepolicy.PolicyAction_ALLOW, resp.Action)
}

func TestDefaultPolicyImages(t *testing.T) {
	testCases := []struct {
		name    string
		sig     *policytypes.SignatureInfo
		ref     string
		allow   bool
		denyMsg string
	}{
		{
			name:  "allows_unrelated_images",
			ref:   "alpine:latest",
			allow: true,
		},
		{
			name:  "dockerfile_digest_only_always_allowed",
			ref:   "docker/dockerfile@sha256:cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc",
			allow: true,
		},
		{
			name:    "dockerfile_tagged_digest_denied",
			ref:     "docker/dockerfile:1.21.0@sha256:cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc",
			denyMsg: "signature is required for 1.21.0 tag",
		},
		{
			name:  "dockerfile_old_version_allowed_unsigned",
			ref:   "docker/dockerfile:1.20.0",
			allow: true,
		},
		{
			name:    "dockerfile_new_version_requires_signature",
			ref:     "docker/dockerfile:1.21.0",
			denyMsg: "signature is required for 1.21.0 tag",
		},
		{
			name:    "dockerfile_new_minor_version_requires_signature",
			ref:     "docker/dockerfile:1.21",
			denyMsg: "signature is required for 1.21 tag",
		},
		{
			name:    "dockerfile_new_major_version_requires_signature",
			ref:     "docker/dockerfile:1",
			denyMsg: "signature is required for 1 tag",
		},
		{
			name:  "dockerfile_new_version_allowed_with_matching_signature",
			sig:   dockerGithubBuilderSig("moby/buildkit", "refs/tags/dockerfile/1.21.0"),
			ref:   "docker/dockerfile:1.21.0",
			allow: true,
		},
		{
			name:  "dockerfile_new_minor_version_allowed_with_matching_patch_signature",
			sig:   dockerGithubBuilderSig("moby/buildkit", "refs/tags/dockerfile/1.21.0"),
			ref:   "docker/dockerfile:1.21",
			allow: true,
		},
		{
			name:  "dockerfile_new_version_allowed_with_matching_signature_labs",
			sig:   dockerGithubBuilderSig("moby/buildkit", "refs/tags/dockerfile/1.21.0-labs"),
			ref:   "docker/dockerfile:1.21.0-labs",
			allow: true,
		},
		{
			name:  "dockerfile_new_version_allowed_with_matching_signature_labs2",
			sig:   dockerGithubBuilderSig("moby/buildkit", "refs/tags/dockerfile/1.21.0-labs"),
			ref:   "docker/dockerfile:1.21-labs",
			allow: true,
		},
		{
			name:  "dockerfile_new_version_allowed_with_matching_signature_labs3",
			sig:   dockerGithubBuilderSig("moby/buildkit", "refs/tags/dockerfile/1.21.0-labs"),
			ref:   "docker/dockerfile:1-labs",
			allow: true,
		},
		{
			name:  "dockerfile_new_version_allowed_with_matching_signature_labs4",
			sig:   dockerGithubBuilderSig("moby/buildkit", "refs/tags/dockerfile/1.21.0-labs"),
			ref:   "docker/dockerfile:labs",
			allow: true,
		},
		{
			name:    "dockerfile_new_version_denied_with_nonlabs_signature_for_labs_tag",
			sig:     dockerGithubBuilderSig("moby/buildkit", "refs/tags/dockerfile/1.21.0"),
			ref:     "docker/dockerfile:1-labs",
			denyMsg: "signature is required for 1-labs tag",
		},
		{
			name:    "dockerfile_new_version_denied_with_mismatched_ref_labs",
			sig:     dockerGithubBuilderSig("moby/buildkit", "refs/tags/dockerfile/1.22.0-labs"),
			ref:     "docker/dockerfile:1.21.0-labs",
			denyMsg: "signature is required for 1.21.0-labs tag",
		},
		{
			name:    "dockerfile_new_version_denied_with_nonlabs_signature_for_exact_labs_tag",
			sig:     dockerGithubBuilderSig("moby/buildkit", "refs/tags/dockerfile/1.21.0"),
			ref:     "docker/dockerfile:1.21.0-labs",
			denyMsg: "signature is required for 1.21.0-labs tag",
		},
		{
			name:    "dockerfile_new_version_denied_with_wrong_signature_repo",
			sig:     dockerGithubBuilderSig("docker/dockerfile", "refs/tags/dockerfile/1.21.0"),
			ref:     "docker/dockerfile:1.21.0",
			denyMsg: "signature is required for 1.21.0 tag",
		},
		{
			name:    "dockerfile_upstream_new_version_denied_with_wrong_signature_repo",
			sig:     dockerGithubBuilderSig("docker/dockerfile-upstream", "refs/tags/dockerfile/1.21.0"),
			ref:     "docker/dockerfile-upstream:1.21.0",
			denyMsg: "signature is required for 1.21.0 tag",
		},
		{
			name:    "dockerfile_upstream_new_version_requires_signature",
			ref:     "docker/dockerfile-upstream:1.21.0",
			denyMsg: "signature is required for 1.21.0 tag",
		},
		{
			name:  "dockerfile_upstream_new_version_allowed_with_matching_signature",
			sig:   dockerGithubBuilderSig("moby/buildkit", "refs/tags/dockerfile/1.21.0"),
			ref:   "docker/dockerfile-upstream:1.21.0",
			allow: true,
		},
		{
			name:    "dockerfile_upstream_new_version_denied_with_mismatched_ref",
			sig:     dockerGithubBuilderSig("moby/buildkit", "refs/tags/dockerfile/1.22.0"),
			ref:     "docker/dockerfile-upstream:1.21.0",
			denyMsg: "signature is required for 1.21.0 tag",
		},
		{
			name:    "dockerfile_new_version_denied_with_mismatched_ref",
			sig:     dockerGithubBuilderSig("moby/buildkit", "refs/tags/dockerfile/1.22.0"),
			ref:     "docker/dockerfile:1.21.0",
			denyMsg: "signature is required for 1.21.0 tag",
		},
		{
			name:    "dockerfile_new_minor_version_denied_with_newer_patch_ref",
			sig:     dockerGithubBuilderSig("moby/buildkit", "refs/tags/dockerfile/1.23.0"),
			ref:     "docker/dockerfile:1.22",
			denyMsg: "signature is required for 1.22 tag",
		},
		{
			name:    "dockerfile_new_version_denied_with_mismatched_ref_labs",
			sig:     dockerGithubBuilderSig("moby/buildkit", "refs/tags/dockerfile/1.22.0-labs"),
			ref:     "docker/dockerfile:1.21.0-labs",
			denyMsg: "signature is required for 1.21.0-labs tag",
		},
		{
			name:    "dockerfile_new_version_denied_with_wrong_signature_tag",
			sig:     dockerGithubBuilderSig("moby/buildkit", "refs/tags/1.21.0"),
			ref:     "docker/dockerfile:1.21.0",
			denyMsg: "signature is required for 1.21.0 tag",
		},
		{
			name:  "dockerfile_latest_allowed_with_signature_any_ref",
			sig:   dockerGithubBuilderSig("moby/buildkit", "refs/tags/dockerfile/1.30.0"),
			ref:   "docker/dockerfile:latest",
			allow: true,
		},
		{
			name:    "dockerfile_latest_denied_without_signature",
			ref:     "docker/dockerfile:latest",
			denyMsg: "signature is required for latest tag",
		},
		{
			name:  "dockerfile_upstream_latest_allowed_with_signature_any_ref",
			sig:   dockerGithubBuilderSig("moby/buildkit", "refs/tags/dockerfile/1.30.0"),
			ref:   "docker/dockerfile-upstream:latest",
			allow: true,
		},
		{
			name:    "dockerfile_upstream_latest_denied_without_signature",
			ref:     "docker/dockerfile-upstream:latest",
			denyMsg: "signature is required for latest tag",
		},
		{
			name:  "dockerfile_upstream_master_allowed_with_signature_any_ref",
			sig:   dockerGithubBuilderSig("moby/buildkit", "refs/heads/master"),
			ref:   "docker/dockerfile-upstream:master",
			allow: true,
		},
		{
			name:    "dockerfile_upstream_master_denied_without_signature",
			ref:     "docker/dockerfile-upstream:master",
			denyMsg: "signature is required for master tag",
		},
	}

	for _, tc := range testCases {
		t.Run(tc.name, func(t *testing.T) {
			p := makeDefaultPolicy(t, tc.sig)
			resp := runDefaultPolicyImage(t, p, tc.ref)
			if tc.allow {
				require.Equal(t, moby_buildkit_v1_sourcepolicy.PolicyAction_ALLOW, resp.Action)
				require.Empty(t, resp.DenyMessages)
				return
			}

			require.Equal(t, moby_buildkit_v1_sourcepolicy.PolicyAction_DENY, resp.Action)
			require.Len(t, resp.DenyMessages, 1)
			require.Contains(t, resp.DenyMessages[0].Message, tc.denyMsg)
		})
	}
}

func TestDefaultPolicySyftScannerImages(t *testing.T) {
	testCases := []struct {
		name    string
		sig     *policytypes.SignatureInfo
		ref     string
		allow   bool
		denyMsg string
	}{
		{
			name:  "syft_scanner_old_version_allowed_unsigned",
			ref:   "docker/buildkit-syft-scanner:1.9.0",
			allow: true,
		},
		{
			name:    "syft_scanner_new_version_requires_signature",
			ref:     "docker/buildkit-syft-scanner:1.10.0",
			denyMsg: "signature is required for 1.10.0 tag",
		},
		{
			name:    "syft_scanner_new_minor_version_requires_signature",
			ref:     "docker/buildkit-syft-scanner:1.10",
			denyMsg: "signature is required for 1.10 tag",
		},
		{
			name:    "syft_scanner_new_major_version_requires_signature",
			ref:     "docker/buildkit-syft-scanner:1",
			denyMsg: "signature is required for 1 tag",
		},
		{
			name:  "syft_scanner_new_version_allowed_with_matching_signature",
			sig:   dockerGithubBuilderSig("docker/buildkit-syft-scanner", "refs/tags/1.10.0"),
			ref:   "docker/buildkit-syft-scanner:1.10.0",
			allow: true,
		},
		{
			name:  "syft_scanner_new_minor_version_allowed_with_matching_patch_signature",
			sig:   dockerGithubBuilderSig("docker/buildkit-syft-scanner", "refs/tags/1.10.0"),
			ref:   "docker/buildkit-syft-scanner:1.10",
			allow: true,
		},
		{
			name:  "syft_scanner_new_major_version_allowed_with_matching_minor_signature",
			sig:   dockerGithubBuilderSig("docker/buildkit-syft-scanner", "refs/tags/1.10.0"),
			ref:   "docker/buildkit-syft-scanner:1",
			allow: true,
		},
		{
			name:    "syft_scanner_new_version_denied_with_wrong_signature_repo",
			sig:     dockerGithubBuilderSig("moby/buildkit", "refs/tags/1.10.0"),
			ref:     "docker/buildkit-syft-scanner:1.10.0",
			denyMsg: "signature is required for 1.10.0 tag",
		},
		{
			name:    "syft_scanner_new_version_denied_with_mismatched_ref",
			sig:     dockerGithubBuilderSig("docker/buildkit-syft-scanner", "refs/tags/1.11.0"),
			ref:     "docker/buildkit-syft-scanner:1.10.0",
			denyMsg: "signature is required for 1.10.0 tag",
		},
		{
			name:    "syft_scanner_new_minor_version_denied_with_newer_patch_ref",
			sig:     dockerGithubBuilderSig("docker/buildkit-syft-scanner", "refs/tags/1.11.0"),
			ref:     "docker/buildkit-syft-scanner:1.10",
			denyMsg: "signature is required for 1.10 tag",
		},
		{
			name:  "syft_scanner_latest_allowed_with_signature_any_ref",
			sig:   dockerGithubBuilderSig("docker/buildkit-syft-scanner", "refs/tags/1.10.0"),
			ref:   "docker/buildkit-syft-scanner:latest",
			allow: true,
		},
		{
			name:    "syft_scanner_latest_denied_without_signature",
			ref:     "docker/buildkit-syft-scanner:latest",
			denyMsg: "signature is required for latest tag",
		},
	}

	for _, tc := range testCases {
		t.Run(tc.name, func(t *testing.T) {
			p := makeDefaultPolicy(t, tc.sig)
			resp := runDefaultPolicyImage(t, p, tc.ref)
			if tc.allow {
				require.Equal(t, moby_buildkit_v1_sourcepolicy.PolicyAction_ALLOW, resp.Action)
				require.Empty(t, resp.DenyMessages)
				return
			}

			require.Equal(t, moby_buildkit_v1_sourcepolicy.PolicyAction_DENY, resp.Action)
			require.Len(t, resp.DenyMessages, 1)
			require.Contains(t, resp.DenyMessages[0].Message, tc.denyMsg)
		})
	}
}
