//go:build integration

package verify

import (
	"net/http"
	"testing"

	"github.com/cli/cli/v2/internal/config"
	"github.com/cli/cli/v2/internal/gh"
	"github.com/cli/cli/v2/internal/telemetry"
	"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/cmd/factory"
	"github.com/cli/cli/v2/pkg/iostreams"
	o "github.com/cli/cli/v2/pkg/option"
	"github.com/cli/go-gh/v2/pkg/auth"
	"github.com/stretchr/testify/require"
)

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

	sigstoreConfig := verification.SigstoreConfig{
		ExternalHttpClient: http.DefaultClient,
		Logger:             logger,
		TUFMetadataDir:     o.Some(t.TempDir()),
	}

	ios, _, _, _ := iostreams.Test()
	hc, err := factory.HttpClientFunc(
		func() (gh.Config, error) { return config.NewBlankConfig(), nil },
		ios,
		"test",
		"",
		&telemetry.NoOpService{},
	)()
	require.NoError(t, err)

	host, _ := auth.DefaultHost()

	sigstoreVerifier, err := verification.NewLiveSigstoreVerifier(sigstoreConfig)
	require.NoError(t, err)
	publicGoodOpts := Options{
		APIClient:        api.NewLiveClient(hc, http.DefaultClient, host, logger),
		ArtifactPath:     artifactPath,
		BundlePath:       bundlePath,
		DigestAlgorithm:  "sha512",
		Logger:           logger,
		OCIClient:        oci.NewLiveClient(),
		OIDCIssuer:       verification.GitHubOIDCIssuer,
		Owner:            "sigstore",
		PredicateType:    verification.SLSAPredicateV1,
		SANRegex:         "^https://github.com/sigstore/",
		SigstoreVerifier: sigstoreVerifier,
	}

	t.Run("with valid owner", func(t *testing.T) {
		err := runVerify(&publicGoodOpts)
		require.NoError(t, err)
	})

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

		err := runVerify(&opts)
		require.NoError(t, err)
	})

	t.Run("with valid owner and invalid repo", func(t *testing.T) {
		opts := publicGoodOpts
		opts.Repo = "sigstore/fakerepo"

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

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

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

	t.Run("with no matching OIDC issuer", func(t *testing.T) {
		opts := publicGoodOpts
		opts.OIDCIssuer = "some-other-issuer"

		err := runVerify(&opts)
		require.Error(t, err)
		require.ErrorContains(t, err, "expected Issuer to be some-other-issuer, got https://token.actions.githubusercontent.com")
	})

	t.Run("with invalid SAN", func(t *testing.T) {
		opts := publicGoodOpts
		opts.SAN = "fake san"

		err := runVerify(&opts)
		require.Error(t, err)
		require.ErrorContains(t, err, "verifying with issuer \"sigstore.dev\"")
	})

	t.Run("with invalid SAN regex", func(t *testing.T) {
		opts := publicGoodOpts
		opts.SANRegex = "^https://github.com/sigstore/not-real/"

		err := runVerify(&opts)
		require.Error(t, err)
		require.ErrorContains(t, err, "verifying with issuer \"sigstore.dev\"")
	})

	t.Run("with bundle from OCI registry", func(t *testing.T) {
		sigstoreVerifier, err := verification.NewLiveSigstoreVerifier(sigstoreConfig)
		require.NoError(t, err)
		opts := Options{
			APIClient:             api.NewLiveClient(hc, http.DefaultClient, host, logger),
			ArtifactPath:          "oci://ghcr.io/github/artifact-attestations-helm-charts/policy-controller:v0.10.0-github9",
			UseBundleFromRegistry: true,
			DigestAlgorithm:       "sha256",
			Logger:                logger,
			OCIClient:             oci.NewLiveClient(),
			OIDCIssuer:            verification.GitHubOIDCIssuer,
			Owner:                 "github",
			PredicateType:         verification.SLSAPredicateV1,
			SANRegex:              "^https://github.com/github/",
			SigstoreVerifier:      sigstoreVerifier,
		}

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

func TestVerifyIntegrationCustomIssuer(t *testing.T) {
	artifactPath := test.NormalizeRelativePath("../test/data/custom-issuer-artifact")
	bundlePath := test.NormalizeRelativePath("../test/data/custom-issuer.sigstore.json")

	logger := io.NewTestHandler()

	sigstoreConfig := verification.SigstoreConfig{
		ExternalHttpClient: http.DefaultClient,
		Logger:             logger,
		TUFMetadataDir:     o.Some(t.TempDir()),
	}

	ios, _, _, _ := iostreams.Test()
	hc, err := factory.HttpClientFunc(
		func() (gh.Config, error) { return config.NewBlankConfig(), nil },
		ios,
		"test",
		"",
		&telemetry.NoOpService{},
	)()
	require.NoError(t, err)

	host, _ := auth.DefaultHost()

	sigstoreVerifier, err := verification.NewLiveSigstoreVerifier(sigstoreConfig)
	require.NoError(t, err)
	baseOpts := Options{
		APIClient:        api.NewLiveClient(hc, http.DefaultClient, host, logger),
		ArtifactPath:     artifactPath,
		BundlePath:       bundlePath,
		DigestAlgorithm:  "sha256",
		Logger:           logger,
		OCIClient:        oci.NewLiveClient(),
		OIDCIssuer:       "https://token.actions.githubusercontent.com/hammer-time",
		PredicateType:    verification.SLSAPredicateV1,
		SigstoreVerifier: sigstoreVerifier,
	}

	t.Run("with owner and valid workflow SAN", func(t *testing.T) {
		opts := baseOpts
		opts.Owner = "too-legit"
		opts.SAN = "https://github.com/too-legit/attest/.github/workflows/integration.yml@refs/heads/main"

		err := runVerify(&opts)
		require.NoError(t, err)
	})

	t.Run("with owner and valid workflow SAN regex", func(t *testing.T) {
		opts := baseOpts
		opts.Owner = "too-legit"
		opts.SANRegex = "^https://github.com/too-legit/attest"

		err := runVerify(&opts)
		require.NoError(t, err)
	})

	t.Run("with repo and valid workflow SAN", func(t *testing.T) {
		opts := baseOpts
		opts.Owner = "too-legit"
		opts.Repo = "too-legit/attest"
		opts.SAN = "https://github.com/too-legit/attest/.github/workflows/integration.yml@refs/heads/main"

		err := runVerify(&opts)
		require.NoError(t, err)
	})

	t.Run("with repo and valid workflow SAN regex", func(t *testing.T) {
		opts := baseOpts
		opts.Owner = "too-legit"
		opts.Repo = "too-legit/attest"
		opts.SANRegex = "^https://github.com/too-legit/attest"

		err := runVerify(&opts)
		require.NoError(t, err)
	})
}

func TestVerifyIntegrationReusableWorkflow(t *testing.T) {
	artifactPath := test.NormalizeRelativePath("../test/data/reusable-workflow-artifact")
	bundlePath := test.NormalizeRelativePath("../test/data/reusable-workflow-attestation.sigstore.json")

	logger := io.NewTestHandler()

	sigstoreConfig := verification.SigstoreConfig{
		ExternalHttpClient: http.DefaultClient,
		Logger:             logger,
		TUFMetadataDir:     o.Some(t.TempDir()),
	}

	cfg := config.NewBlankConfig()
	ios, _, _, _ := iostreams.Test()
	hc, err := factory.HttpClientFunc(
		func() (gh.Config, error) { return cfg, nil },
		ios,
		"test",
		"",
		&telemetry.NoOpService{},
	)()
	require.NoError(t, err)

	host, _ := auth.DefaultHost()

	sigstoreVerifier, err := verification.NewLiveSigstoreVerifier(sigstoreConfig)
	require.NoError(t, err)
	baseOpts := Options{
		APIClient:        api.NewLiveClient(hc, http.DefaultClient, host, logger),
		ArtifactPath:     artifactPath,
		BundlePath:       bundlePath,
		DigestAlgorithm:  "sha256",
		Logger:           logger,
		OCIClient:        oci.NewLiveClient(),
		OIDCIssuer:       verification.GitHubOIDCIssuer,
		PredicateType:    verification.SLSAPredicateV1,
		SigstoreVerifier: sigstoreVerifier,
	}

	t.Run("with owner and valid reusable workflow SAN", func(t *testing.T) {
		opts := baseOpts
		opts.Owner = "malancas"
		opts.SAN = "https://github.com/github/artifact-attestations-workflows/.github/workflows/attest.yml@09b495c3f12c7881b3cc17209a327792065c1a1d"

		err := runVerify(&opts)
		require.NoError(t, err)
	})

	t.Run("with owner and valid reusable workflow SAN regex", func(t *testing.T) {
		opts := baseOpts
		opts.Owner = "malancas"
		opts.SANRegex = "^https://github.com/github/artifact-attestations-workflows/"

		err := runVerify(&opts)
		require.NoError(t, err)
	})

	t.Run("with owner and valid reusable signer repo", func(t *testing.T) {
		opts := baseOpts
		opts.Owner = "malancas"
		opts.SignerRepo = "github/artifact-attestations-workflows"

		err := runVerify(&opts)
		require.NoError(t, err)
	})

	t.Run("with repo and valid reusable workflow SAN", func(t *testing.T) {
		opts := baseOpts
		opts.Owner = "malancas"
		opts.Repo = "malancas/attest-demo"
		opts.SAN = "https://github.com/github/artifact-attestations-workflows/.github/workflows/attest.yml@09b495c3f12c7881b3cc17209a327792065c1a1d"

		err := runVerify(&opts)
		require.NoError(t, err)
	})

	t.Run("with repo and valid reusable workflow SAN regex", func(t *testing.T) {
		opts := baseOpts
		opts.Owner = "malancas"
		opts.Repo = "malancas/attest-demo"
		opts.SANRegex = "^https://github.com/github/artifact-attestations-workflows/"

		err := runVerify(&opts)
		require.NoError(t, err)
	})

	t.Run("with repo and valid reusable signer repo", func(t *testing.T) {
		opts := baseOpts
		opts.Owner = "malancas"
		opts.Repo = "malancas/attest-demo"
		opts.SignerRepo = "github/artifact-attestations-workflows"

		err := runVerify(&opts)
		require.NoError(t, err)
	})
}

func TestVerifyIntegrationReusableWorkflowSignerWorkflow(t *testing.T) {
	artifactPath := test.NormalizeRelativePath("../test/data/reusable-workflow-artifact")
	bundlePath := test.NormalizeRelativePath("../test/data/reusable-workflow-attestation.sigstore.json")

	logger := io.NewTestHandler()

	sigstoreConfig := verification.SigstoreConfig{
		ExternalHttpClient: http.DefaultClient,
		Logger:             logger,
		TUFMetadataDir:     o.Some(t.TempDir()),
	}

	cfg := config.NewBlankConfig()
	ios, _, _, _ := iostreams.Test()
	hc, err := factory.HttpClientFunc(
		func() (gh.Config, error) { return cfg, nil },
		ios,
		"test",
		"",
		&telemetry.NoOpService{},
	)()
	require.NoError(t, err)

	host, _ := auth.DefaultHost()

	sigstoreVerifier, err := verification.NewLiveSigstoreVerifier(sigstoreConfig)
	require.NoError(t, err)
	baseOpts := Options{
		APIClient:    api.NewLiveClient(hc, http.DefaultClient, host, logger),
		ArtifactPath: artifactPath,
		BundlePath:   bundlePath,
		Config: func() (gh.Config, error) {
			return cfg, nil
		},
		DigestAlgorithm:  "sha256",
		Logger:           logger,
		OCIClient:        oci.NewLiveClient(),
		OIDCIssuer:       verification.GitHubOIDCIssuer,
		Owner:            "malancas",
		PredicateType:    verification.SLSAPredicateV1,
		Repo:             "malancas/attest-demo",
		SigstoreVerifier: sigstoreVerifier,
	}

	type testcase struct {
		name           string
		signerWorkflow string
		expectErr      bool
		host           string
	}

	testcases := []testcase{
		{
			name:           "with invalid signer workflow",
			signerWorkflow: "foo/bar/.github/workflows/attest.yml",
			expectErr:      true,
		},
		{
			name:           "valid signer workflow with host",
			signerWorkflow: "github.com/github/artifact-attestations-workflows/.github/workflows/attest.yml",
			expectErr:      false,
		},
		{
			name:           "valid signer workflow without host (defaults to github.com)",
			signerWorkflow: "github/artifact-attestations-workflows/.github/workflows/attest.yml",
			expectErr:      false,
			host:           "github.com",
		},
	}

	for _, tc := range testcases {
		opts := baseOpts
		opts.SignerWorkflow = tc.signerWorkflow
		opts.Hostname = tc.host

		err := runVerify(&opts)
		if tc.expectErr {
			require.Error(t, err, "expected error for '%s'", tc.name)
		} else {
			require.NoError(t, err, "unexpected error for '%s'", tc.name)
		}
	}
}
