package verify

import (
	"bytes"
	"net/http"
	"testing"

	"github.com/cli/cli/v2/internal/ghrepo"
	"github.com/cli/cli/v2/pkg/cmd/attestation/api"
	"github.com/cli/cli/v2/pkg/cmd/attestation/test/data"
	"github.com/cli/cli/v2/pkg/cmd/attestation/verification"
	"github.com/cli/cli/v2/pkg/cmd/release/shared"
	"github.com/cli/cli/v2/pkg/cmdutil"
	"github.com/cli/cli/v2/pkg/httpmock"
	"github.com/cli/cli/v2/pkg/iostreams"
	"github.com/stretchr/testify/assert"
	"github.com/stretchr/testify/require"
)

func TestNewCmdVerify_Args(t *testing.T) {
	tests := []struct {
		name    string
		args    []string
		wantTag string
		wantErr string
	}{
		{
			name:    "valid tag arg",
			args:    []string{"v1.2.3"},
			wantTag: "v1.2.3",
		},
		{
			name:    "no tag arg",
			args:    []string{},
			wantTag: "",
		},
	}
	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			testIO, _, _, _ := iostreams.Test()
			f := &cmdutil.Factory{
				IOStreams: testIO,
				HttpClient: func() (*http.Client, error) {
					return nil, nil
				},
				ExternalHttpClient: func() (*http.Client, error) {
					return nil, nil
				},
				BaseRepo: func() (ghrepo.Interface, error) {
					return ghrepo.FromFullName("owner/repo")
				},
			}

			var cfg *VerifyConfig
			cmd := NewCmdVerify(f, func(c *VerifyConfig) error {
				cfg = c
				return nil
			})
			cmd.SetArgs(tt.args)
			cmd.SetIn(&bytes.Buffer{})
			cmd.SetOut(&bytes.Buffer{})
			cmd.SetErr(&bytes.Buffer{})

			_, err := cmd.ExecuteC()

			require.NoError(t, err)
			assert.Equal(t, tt.wantTag, cfg.Opts.TagName)
		})
	}
}

func Test_verifyRun_Success(t *testing.T) {
	ios, _, _, _ := iostreams.Test()
	tagName := "v6"

	fakeHTTP := &httpmock.Registry{}
	defer fakeHTTP.Verify(t)

	fakeSHA := "1234567890abcdef1234567890abcdef12345678"
	shared.StubFetchRefSHA(t, fakeHTTP, "owner", "repo", tagName, fakeSHA)

	baseRepo, err := ghrepo.FromFullName("owner/repo")
	require.NoError(t, err)

	result := &verification.AttestationProcessingResult{
		Attestation: &api.Attestation{
			Bundle:    data.GitHubReleaseBundle(t),
			BundleURL: "https://example.com",
		},
		VerificationResult: nil,
	}

	cfg := &VerifyConfig{
		Opts: &VerifyOptions{
			TagName:  tagName,
			BaseRepo: baseRepo,
			Exporter: nil,
		},
		IO:          ios,
		HttpClient:  &http.Client{Transport: fakeHTTP},
		AttClient:   api.NewTestClient(),
		AttVerifier: shared.NewMockVerifier(result),
	}

	err = verifyRun(cfg)
	require.NoError(t, err)
}

func Test_verifyRun_FailedNoAttestations_SHA1(t *testing.T) {
	ios, _, _, _ := iostreams.Test()
	tagName := "v1"

	fakeHTTP := &httpmock.Registry{}
	defer fakeHTTP.Verify(t)
	fakeSHA := "1234567890abcdef1234567890abcdef12345678"
	shared.StubFetchRefSHA(t, fakeHTTP, "owner", "repo", tagName, fakeSHA)

	baseRepo, err := ghrepo.FromFullName("owner/repo")
	require.NoError(t, err)

	var capturedParams api.FetchParams
	attClient := &api.MockClient{
		OnGetByDigest: func(params api.FetchParams) ([]*api.Attestation, error) {
			capturedParams = params
			return api.OnGetByDigestFailure(params)
		},
	}

	cfg := &VerifyConfig{
		Opts: &VerifyOptions{
			TagName:  tagName,
			BaseRepo: baseRepo,
			Exporter: nil,
		},
		IO:          ios,
		HttpClient:  &http.Client{Transport: fakeHTTP},
		AttClient:   attClient,
		AttVerifier: nil,
	}

	err = verifyRun(cfg)
	require.ErrorContains(t, err, "no attestations for tag v1")
	require.ErrorContains(t, err, "sha1:"+fakeSHA)
	require.Equal(t, "sha1:"+fakeSHA, capturedParams.Digest)
}

func Test_verifyRun_FailedNoAttestations_SHA256(t *testing.T) {
	ios, _, _, _ := iostreams.Test()
	tagName := "v1"

	fakeHTTP := &httpmock.Registry{}
	defer fakeHTTP.Verify(t)
	fakeSHA := "1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef"
	shared.StubFetchRefSHA(t, fakeHTTP, "owner", "repo", tagName, fakeSHA)

	baseRepo, err := ghrepo.FromFullName("owner/repo")
	require.NoError(t, err)

	var capturedParams api.FetchParams
	attClient := &api.MockClient{
		OnGetByDigest: func(params api.FetchParams) ([]*api.Attestation, error) {
			capturedParams = params
			return api.OnGetByDigestFailure(params)
		},
	}

	cfg := &VerifyConfig{
		Opts: &VerifyOptions{
			TagName:  tagName,
			BaseRepo: baseRepo,
			Exporter: nil,
		},
		IO:          ios,
		HttpClient:  &http.Client{Transport: fakeHTTP},
		AttClient:   attClient,
		AttVerifier: nil,
	}

	err = verifyRun(cfg)
	require.ErrorContains(t, err, "no attestations for tag v1")
	require.ErrorContains(t, err, "sha256:"+fakeSHA)
	require.Equal(t, "sha256:"+fakeSHA, capturedParams.Digest)
}

func Test_verifyRun_FailedTagNotInAttestation(t *testing.T) {
	ios, _, _, _ := iostreams.Test()

	// Tag name does not match the one present in the attestation which
	// will be returned by the mock client. Simulates a scenario where
	// multiple releases may point to the same commit SHA, but not all
	// of them are attested.
	tagName := "v1.2.3"

	fakeHTTP := &httpmock.Registry{}
	defer fakeHTTP.Verify(t)
	fakeSHA := "1234567890abcdef1234567890abcdef12345678"
	shared.StubFetchRefSHA(t, fakeHTTP, "owner", "repo", tagName, fakeSHA)

	baseRepo, err := ghrepo.FromFullName("owner/repo")
	require.NoError(t, err)

	cfg := &VerifyConfig{
		Opts: &VerifyOptions{
			TagName:  tagName,
			BaseRepo: baseRepo,
			Exporter: nil,
		},
		IO:          ios,
		HttpClient:  &http.Client{Transport: fakeHTTP},
		AttClient:   api.NewTestClient(),
		AttVerifier: nil,
	}

	err = verifyRun(cfg)
	require.ErrorContains(t, err, "no attestations found for release v1.2.3")
}
