package verify

import (
	"bytes"
	"encoding/json"
	"fmt"
	"net/http"
	"strings"
	"testing"

	"github.com/cli/cli/v2/pkg/cmd/attestation/api"
	"github.com/cli/cli/v2/pkg/cmd/attestation/artifact/oci"
	"github.com/cli/cli/v2/pkg/cmd/attestation/io"
	"github.com/cli/cli/v2/pkg/cmd/attestation/test"
	"github.com/cli/cli/v2/pkg/cmd/attestation/verification"
	"github.com/cli/cli/v2/pkg/cmdutil"
	"github.com/spf13/cobra"

	"github.com/cli/cli/v2/pkg/httpmock"
	"github.com/cli/cli/v2/pkg/iostreams"
	"github.com/stretchr/testify/assert"

	"github.com/stretchr/testify/require"
)

const (
	SigstoreSanValue = "https://github.com/sigstore/sigstore-js/.github/workflows/release.yml@refs/heads/main"
	SigstoreSanRegex = "^https://github.com/sigstore/sigstore-js/"
)

var (
	artifactPath = test.NormalizeRelativePath("../test/data/sigstore-js-2.1.0.tgz")
	bundlePath   = test.NormalizeRelativePath("../test/data/sigstore-js-2.1.0-bundle.json")
)

func TestNewVerifyCmd(t *testing.T) {
	testIO, _, _, _ := iostreams.Test()
	var testReg httpmock.Registry
	var metaResp = api.MetaResponse{
		Domains: api.Domain{
			ArtifactAttestations: api.ArtifactAttestations{
				TrustDomain: "foo",
			},
		},
	}
	testReg.Register(httpmock.REST(http.MethodGet, "meta"),
		httpmock.StatusJSONResponse(200, &metaResp))

	f := &cmdutil.Factory{
		IOStreams: testIO,
		HttpClient: func() (*http.Client, error) {
			reg := &testReg
			client := &http.Client{}
			httpmock.ReplaceTripper(client, reg)
			return client, nil
		},
	}

	testcases := []struct {
		name          string
		cli           string
		wants         Options
		wantsErr      bool
		wantsExporter bool
	}{
		{
			name: "Invalid digest-alg flag",
			cli:  fmt.Sprintf("%s --bundle %s --digest-alg sha384 --owner sigstore", artifactPath, bundlePath),
			wants: Options{
				ArtifactPath:     test.NormalizeRelativePath("../test/data/sigstore-js-2.1.0.tgz"),
				BundlePath:       test.NormalizeRelativePath("../test/data/sigstore-js-2.1.0-bundle.json"),
				DigestAlgorithm:  "sha384",
				Hostname:         "github.com",
				Limit:            30,
				OIDCIssuer:       verification.GitHubOIDCIssuer,
				Owner:            "sigstore",
				PredicateType:    verification.SLSAPredicateV1,
				SigstoreVerifier: verification.NewMockSigstoreVerifier(t),
			},
			wantsErr: true,
		},
		{
			name: "Use default digest-alg value",
			cli:  fmt.Sprintf("%s --bundle %s --owner sigstore", artifactPath, bundlePath),
			wants: Options{
				ArtifactPath:     test.NormalizeRelativePath("../test/data/sigstore-js-2.1.0.tgz"),
				BundlePath:       test.NormalizeRelativePath("../test/data/sigstore-js-2.1.0-bundle.json"),
				DigestAlgorithm:  "sha256",
				Hostname:         "github.com",
				Limit:            30,
				OIDCIssuer:       verification.GitHubOIDCIssuer,
				Owner:            "sigstore",
				PredicateType:    verification.SLSAPredicateV1,
				SigstoreVerifier: verification.NewMockSigstoreVerifier(t),
			},
			wantsErr: false,
		},
		{
			name: "Custom host",
			cli:  fmt.Sprintf("%s --bundle %s --owner sigstore --hostname foo.ghe.com", artifactPath, bundlePath),
			wants: Options{
				ArtifactPath:     test.NormalizeRelativePath("../test/data/sigstore-js-2.1.0.tgz"),
				BundlePath:       test.NormalizeRelativePath("../test/data/sigstore-js-2.1.0-bundle.json"),
				DigestAlgorithm:  "sha256",
				Hostname:         "foo.ghe.com",
				Limit:            30,
				OIDCIssuer:       verification.GitHubOIDCIssuer,
				Owner:            "sigstore",
				PredicateType:    verification.SLSAPredicateV1,
				SigstoreVerifier: verification.NewMockSigstoreVerifier(t),
			},
			wantsErr: false,
		},
		{
			name: "Invalid custom host",
			cli:  fmt.Sprintf("%s --bundle %s --owner sigstore --hostname foo.bar.com", artifactPath, bundlePath),
			wants: Options{
				ArtifactPath:     test.NormalizeRelativePath("../test/data/sigstore-js-2.1.0.tgz"),
				BundlePath:       test.NormalizeRelativePath("../test/data/sigstore-js-2.1.0-bundle.json"),
				DigestAlgorithm:  "sha256",
				Hostname:         "foo.ghe.com",
				Limit:            30,
				OIDCIssuer:       verification.GitHubOIDCIssuer,
				Owner:            "sigstore",
				PredicateType:    verification.SLSAPredicateV1,
				SigstoreVerifier: verification.NewMockSigstoreVerifier(t),
			},
			wantsErr: true,
		},
		{
			name: "Use custom digest-alg value",
			cli:  fmt.Sprintf("%s --bundle %s --owner sigstore --digest-alg sha512", artifactPath, bundlePath),
			wants: Options{
				ArtifactPath:     test.NormalizeRelativePath("../test/data/sigstore-js-2.1.0.tgz"),
				BundlePath:       test.NormalizeRelativePath("../test/data/sigstore-js-2.1.0-bundle.json"),
				DigestAlgorithm:  "sha512",
				Hostname:         "github.com",
				Limit:            30,
				OIDCIssuer:       verification.GitHubOIDCIssuer,
				Owner:            "sigstore",
				PredicateType:    verification.SLSAPredicateV1,
				SigstoreVerifier: verification.NewMockSigstoreVerifier(t),
			},
			wantsErr: false,
		},
		{
			name: "Missing owner and repo flags",
			cli:  artifactPath,
			wants: Options{
				ArtifactPath:     test.NormalizeRelativePath("../test/data/sigstore-js-2.1.0.tgz"),
				DigestAlgorithm:  "sha256",
				Hostname:         "github.com",
				Limit:            30,
				OIDCIssuer:       verification.GitHubOIDCIssuer,
				Owner:            "sigstore",
				PredicateType:    verification.SLSAPredicateV1,
				SANRegex:         "(?i)^https://github.com/sigstore/",
				SigstoreVerifier: verification.NewMockSigstoreVerifier(t),
			},
			wantsErr: true,
		},
		{
			name: "Has both owner and repo flags",
			cli:  fmt.Sprintf("%s --owner sigstore --repo sigstore/sigstore-js", artifactPath),
			wants: Options{
				ArtifactPath:     artifactPath,
				DigestAlgorithm:  "sha256",
				Hostname:         "github.com",
				Limit:            30,
				OIDCIssuer:       verification.GitHubOIDCIssuer,
				Owner:            "sigstore",
				PredicateType:    verification.SLSAPredicateV1,
				Repo:             "sigstore/sigstore-js",
				SigstoreVerifier: verification.NewMockSigstoreVerifier(t),
			},
			wantsErr: true,
		},
		{
			name: "Uses default limit flag",
			cli:  fmt.Sprintf("%s --owner sigstore", artifactPath),
			wants: Options{
				ArtifactPath:     artifactPath,
				DigestAlgorithm:  "sha256",
				Hostname:         "github.com",
				Limit:            30,
				OIDCIssuer:       verification.GitHubOIDCIssuer,
				Owner:            "sigstore",
				PredicateType:    verification.SLSAPredicateV1,
				SigstoreVerifier: verification.NewMockSigstoreVerifier(t),
			},
			wantsErr: false,
		},
		{
			name: "Uses custom limit flag",
			cli:  fmt.Sprintf("%s --owner sigstore --limit 101", artifactPath),
			wants: Options{
				ArtifactPath:     artifactPath,
				DigestAlgorithm:  "sha256",
				Hostname:         "github.com",
				Limit:            101,
				OIDCIssuer:       verification.GitHubOIDCIssuer,
				Owner:            "sigstore",
				PredicateType:    verification.SLSAPredicateV1,
				SigstoreVerifier: verification.NewMockSigstoreVerifier(t),
			},
			wantsErr: false,
		},
		{
			name: "Uses invalid limit flag",
			cli:  fmt.Sprintf("%s --owner sigstore --limit 0", artifactPath),
			wants: Options{
				ArtifactPath:     artifactPath,
				DigestAlgorithm:  "sha256",
				Hostname:         "github.com",
				Limit:            0,
				OIDCIssuer:       verification.GitHubOIDCIssuer,
				Owner:            "sigstore",
				PredicateType:    verification.SLSAPredicateV1,
				SANRegex:         "(?i)^https://github.com/sigstore/",
				SigstoreVerifier: verification.NewMockSigstoreVerifier(t),
			},
			wantsErr: true,
		},
		{
			name: "Has both cert-identity and cert-identity-regex flags",
			cli:  fmt.Sprintf("%s --owner sigstore --cert-identity https://github.com/sigstore/ --cert-identity-regex ^https://github.com/sigstore/", artifactPath),
			wants: Options{
				ArtifactPath:     artifactPath,
				DigestAlgorithm:  "sha256",
				Hostname:         "github.com",
				Limit:            30,
				OIDCIssuer:       verification.GitHubOIDCIssuer,
				Owner:            "sigstore",
				PredicateType:    verification.SLSAPredicateV1,
				SAN:              "https://github.com/sigstore/",
				SANRegex:         "(?i)^https://github.com/sigstore/",
				SigstoreVerifier: verification.NewMockSigstoreVerifier(t),
			},
			wantsErr: true,
		},
		{
			name: "Prints output in JSON format",
			cli:  fmt.Sprintf("%s --bundle %s --owner sigstore --format json", artifactPath, bundlePath),
			wants: Options{
				ArtifactPath:     artifactPath,
				BundlePath:       bundlePath,
				DigestAlgorithm:  "sha256",
				Hostname:         "github.com",
				Limit:            30,
				OIDCIssuer:       verification.GitHubOIDCIssuer,
				Owner:            "sigstore",
				PredicateType:    verification.SLSAPredicateV1,
				SigstoreVerifier: verification.NewMockSigstoreVerifier(t),
			},
			wantsExporter: true,
		},
		{
			name: "Use specified predicate type",
			cli:  fmt.Sprintf("%s --bundle %s --owner sigstore --predicate-type https://spdx.dev/Document/v2.3 --format json", artifactPath, bundlePath),
			wants: Options{
				ArtifactPath:     artifactPath,
				BundlePath:       bundlePath,
				DigestAlgorithm:  "sha256",
				Hostname:         "github.com",
				Limit:            30,
				OIDCIssuer:       verification.GitHubOIDCIssuer,
				Owner:            "sigstore",
				PredicateType:    "https://spdx.dev/Document/v2.3",
				SigstoreVerifier: verification.NewMockSigstoreVerifier(t),
			},
			wantsExporter: true,
		},
	}

	for _, tc := range testcases {
		t.Run(tc.name, func(t *testing.T) {
			var opts *Options
			cmd := NewVerifyCmd(f, func(o *Options) error {
				opts = o
				return nil
			})

			argv := strings.Split(tc.cli, " ")
			cmd.SetArgs(argv)
			cmd.SetIn(&bytes.Buffer{})
			cmd.SetOut(&bytes.Buffer{})
			cmd.SetErr(&bytes.Buffer{})
			_, err := cmd.ExecuteC()
			if tc.wantsErr {
				assert.Error(t, err)
				return
			}
			assert.NoError(t, err)

			assert.Equal(t, tc.wants.ArtifactPath, opts.ArtifactPath)
			assert.Equal(t, tc.wants.BundlePath, opts.BundlePath)
			assert.Equal(t, tc.wants.DenySelfHostedRunner, opts.DenySelfHostedRunner)
			assert.Equal(t, tc.wants.DigestAlgorithm, opts.DigestAlgorithm)
			assert.Equal(t, tc.wants.Hostname, opts.Hostname)
			assert.Equal(t, tc.wants.Limit, opts.Limit)
			assert.Equal(t, tc.wants.NoPublicGood, opts.NoPublicGood)
			assert.Equal(t, tc.wants.OIDCIssuer, opts.OIDCIssuer)
			assert.Equal(t, tc.wants.Owner, opts.Owner)
			assert.Equal(t, tc.wants.PredicateType, opts.PredicateType)
			assert.Equal(t, tc.wants.Repo, opts.Repo)
			assert.Equal(t, tc.wants.SAN, opts.SAN)
			assert.Equal(t, tc.wants.SANRegex, opts.SANRegex)
			assert.Equal(t, tc.wants.TrustedRoot, opts.TrustedRoot)
			assert.NotNil(t, opts.APIClient)
			assert.NotNil(t, opts.Logger)
			assert.NotNil(t, opts.OCIClient)
			assert.Equal(t, tc.wantsExporter, opts.exporter != nil)
		})
	}
}

func TestVerifyCmdAuthChecks(t *testing.T) {
	f := &cmdutil.Factory{}

	t.Run("by default auth check is required", func(t *testing.T) {
		cmd := NewVerifyCmd(f, func(o *Options) error {
			return nil
		})

		// IsAuthCheckEnabled assumes commands under test are subcommands
		parent := &cobra.Command{Use: "root"}
		parent.AddCommand(cmd)

		require.NoError(t, cmd.ParseFlags([]string{}))
		require.True(t, cmdutil.IsAuthCheckEnabled(cmd), "expected auth check to be required")
	})

	t.Run("when --bundle flag is provided, auth check is not required", func(t *testing.T) {
		cmd := NewVerifyCmd(f, func(o *Options) error {
			return nil
		})

		// IsAuthCheckEnabled assumes commands under test are subcommands
		parent := &cobra.Command{Use: "root"}
		parent.AddCommand(cmd)

		require.NoError(t, cmd.ParseFlags([]string{"--bundle", "not-important"}))
		require.False(t, cmdutil.IsAuthCheckEnabled(cmd), "expected auth check not to be required due to --bundle flag")
	})
}

func TestJSONOutput(t *testing.T) {
	testIO, _, out, _ := iostreams.Test()
	opts := Options{
		ArtifactPath:     artifactPath,
		BundlePath:       bundlePath,
		DigestAlgorithm:  "sha512",
		APIClient:        api.NewTestClient(),
		Logger:           io.NewHandler(testIO),
		OCIClient:        oci.MockClient{},
		OIDCIssuer:       verification.GitHubOIDCIssuer,
		Owner:            "sigstore",
		PredicateType:    verification.SLSAPredicateV1,
		SANRegex:         "^https://github.com/sigstore/",
		SigstoreVerifier: verification.NewMockSigstoreVerifier(t),
		exporter:         cmdutil.NewJSONExporter(),
	}
	require.NoError(t, runVerify(&opts))

	var target []*verification.AttestationProcessingResult
	err := json.Unmarshal(out.Bytes(), &target)
	require.NoError(t, err)
}

func TestRunVerify(t *testing.T) {
	logger := io.NewTestHandler()

	publicGoodOpts := Options{
		ArtifactPath:     artifactPath,
		BundlePath:       bundlePath,
		DigestAlgorithm:  "sha512",
		APIClient:        api.NewTestClient(),
		Logger:           logger,
		OCIClient:        oci.MockClient{},
		OIDCIssuer:       verification.GitHubOIDCIssuer,
		Owner:            "sigstore",
		PredicateType:    verification.SLSAPredicateV1,
		SANRegex:         "^https://github.com/sigstore/",
		SigstoreVerifier: verification.NewMockSigstoreVerifier(t),
	}

	t.Run("with valid artifact and bundle", func(t *testing.T) {
		require.NoError(t, runVerify(&publicGoodOpts))
	})

	t.Run("with failing OCI artifact fetch", func(t *testing.T) {
		opts := publicGoodOpts
		opts.ArtifactPath = "oci://ghcr.io/github/test"
		opts.OCIClient = oci.ReferenceFailClient{}

		err := runVerify(&opts)
		require.Error(t, err)
		require.ErrorContains(t, err, "failed to parse reference")
	})

	t.Run("with missing artifact path", func(t *testing.T) {
		opts := publicGoodOpts
		opts.ArtifactPath = "../test/data/non-existent-artifact.zip"
		require.Error(t, runVerify(&opts))
	})

	t.Run("with missing bundle path", func(t *testing.T) {
		opts := publicGoodOpts
		opts.BundlePath = "../test/data/non-existent-sigstoreBundle.json"
		require.Error(t, runVerify(&opts))
	})

	t.Run("with owner", func(t *testing.T) {
		opts := publicGoodOpts
		opts.BundlePath = ""
		opts.Owner = "sigstore"

		require.NoError(t, runVerify(&opts))
	})

	t.Run("with owner which not matches SourceRepositoryOwnerURI", func(t *testing.T) {
		opts := publicGoodOpts
		opts.BundlePath = ""
		opts.Owner = "owner"

		err := runVerify(&opts)
		require.ErrorContains(t, err, "expected SourceRepositoryOwnerURI to be https://github.com/owner, got https://github.com/sigstore")
	})

	t.Run("with repo", func(t *testing.T) {
		opts := publicGoodOpts
		opts.BundlePath = ""
		opts.Repo = "sigstore/sigstore-js"

		require.Nil(t, runVerify(&opts))
	})

	// Test with bad tenancy
	t.Run("with bad tenancy", func(t *testing.T) {
		opts := publicGoodOpts
		opts.BundlePath = ""
		opts.Repo = "sigstore/sigstore-js"
		opts.Tenant = "foo"

		err := runVerify(&opts)
		require.ErrorContains(t, err, "expected SourceRepositoryOwnerURI to be https://foo.ghe.com/sigstore, got https://github.com/sigstore")
	})

	t.Run("with repo which not matches SourceRepositoryURI", func(t *testing.T) {
		opts := publicGoodOpts
		opts.BundlePath = ""
		opts.Repo = "sigstore/wrong"

		err := runVerify(&opts)
		require.ErrorContains(t, err, "expected SourceRepositoryURI to be https://github.com/sigstore/wrong, got https://github.com/sigstore/sigstore-js")
	})

	t.Run("with invalid repo", func(t *testing.T) {
		opts := publicGoodOpts
		opts.BundlePath = ""
		opts.Repo = "wrong/example"
		opts.APIClient = api.NewFailTestClient()

		err := runVerify(&opts)
		require.Error(t, err)
		require.ErrorContains(t, err, "failed to fetch attestations from wrong/example")
	})

	t.Run("with invalid owner", func(t *testing.T) {
		opts := publicGoodOpts
		opts.BundlePath = ""
		opts.APIClient = api.NewFailTestClient()
		opts.Owner = "wrong-owner"

		err := runVerify(&opts)
		require.Error(t, err)
		require.ErrorContains(t, err, "failed to fetch attestations from wrong-owner")
	})

	t.Run("with missing API client", func(t *testing.T) {
		customOpts := publicGoodOpts
		customOpts.APIClient = nil
		customOpts.BundlePath = ""
		require.Error(t, runVerify(&customOpts))
	})

	t.Run("with valid OCI artifact", func(t *testing.T) {
		customOpts := publicGoodOpts
		customOpts.ArtifactPath = "oci://ghcr.io/github/test"
		customOpts.BundlePath = ""

		require.Nil(t, runVerify(&customOpts))
	})

	t.Run("with valid OCI artifact with UseBundleFromRegistry flag", func(t *testing.T) {
		customOpts := publicGoodOpts
		customOpts.ArtifactPath = "oci://ghcr.io/github/test"
		customOpts.BundlePath = ""
		customOpts.UseBundleFromRegistry = true

		require.Nil(t, runVerify(&customOpts))
	})

	t.Run("with valid OCI artifact with UseBundleFromRegistry flag and unknown predicate type", func(t *testing.T) {
		customOpts := publicGoodOpts
		customOpts.ArtifactPath = "oci://ghcr.io/github/test"
		customOpts.BundlePath = ""
		customOpts.UseBundleFromRegistry = true
		customOpts.PredicateType = "https://predicate.type"

		err := runVerify(&customOpts)
		require.Error(t, err)
		require.ErrorContains(t, err, "no attestations found with predicate type")
	})

	t.Run("with valid OCI artifact with UseBundleFromRegistry flag but no bundle return from registry", func(t *testing.T) {
		customOpts := publicGoodOpts
		customOpts.ArtifactPath = "oci://ghcr.io/github/test"
		customOpts.BundlePath = ""
		customOpts.UseBundleFromRegistry = true
		customOpts.OCIClient = oci.NoAttestationsClient{}

		require.ErrorContains(t, runVerify(&customOpts), "no attestations found in the OCI registry. Retry the command without the --bundle-from-oci flag to check GitHub for the attestation")
	})

	t.Run("with valid OCI artifact with UseBundleFromRegistry flag but fail on fetching bundle from registry", func(t *testing.T) {
		customOpts := publicGoodOpts
		customOpts.ArtifactPath = "oci://ghcr.io/github/test"
		customOpts.BundlePath = ""
		customOpts.UseBundleFromRegistry = true
		customOpts.OCIClient = oci.NoAttestationsClient{}

		require.ErrorContains(t, runVerify(&customOpts), "no attestations found in the OCI registry. Retry the command without the --bundle-from-oci flag to check GitHub for the attestation")
	})
}
