package set

import (
	"bytes"
	"encoding/json"
	"fmt"
	"io"
	"net/http"
	"os"
	"testing"

	"github.com/MakeNowJust/heredoc"
	ghContext "github.com/cli/cli/v2/context"
	"github.com/cli/cli/v2/git"
	"github.com/cli/cli/v2/internal/config"
	"github.com/cli/cli/v2/internal/gh"
	"github.com/cli/cli/v2/internal/ghrepo"
	"github.com/cli/cli/v2/internal/prompter"
	"github.com/cli/cli/v2/pkg/cmd/secret/shared"
	"github.com/cli/cli/v2/pkg/cmdutil"
	"github.com/cli/cli/v2/pkg/httpmock"
	"github.com/cli/cli/v2/pkg/iostreams"
	"github.com/google/shlex"
	"github.com/stretchr/testify/assert"
	"github.com/stretchr/testify/require"
)

func TestNewCmdSet(t *testing.T) {
	tests := []struct {
		name            string
		args            string
		wants           SetOptions
		stdinTTY        bool
		wantsErr        bool
		wantsErrMessage string
	}{
		{
			name:     "invalid visibility",
			args:     "cool_secret --org coolOrg -v mistyVeil",
			wantsErr: true,
		},
		{
			name:            "when visibility is selected, requires indication of repos",
			args:            "cool_secret --org coolOrg -v selected",
			wantsErr:        true,
			wantsErrMessage: "`--repos` or `--no-repos-selected` required with `--visibility=selected`",
		},
		{
			name:            "visibilities other than selected do not accept --repos",
			args:            "cool_secret --org coolOrg -v private -r coolRepo",
			wantsErr:        true,
			wantsErrMessage: "`--repos` is only supported with `--visibility=selected`",
		},
		{
			name:            "visibilities other than selected do not accept --no-repos-selected",
			args:            "cool_secret --org coolOrg -v private --no-repos-selected",
			wantsErr:        true,
			wantsErrMessage: "`--no-repos-selected` is only supported with `--visibility=selected`",
		},
		{
			name:            "--repos and --no-repos-selected are mutually exclusive",
			args:            `--repos coolRepo --no-repos-selected cool_secret`,
			wantsErr:        true,
			wantsErrMessage: "specify only one of `--repos` or `--no-repos-selected`",
		},
		{
			name:     "secret name is required",
			args:     "",
			wantsErr: true,
		},
		{
			name:     "multiple positional arguments are not allowed",
			args:     "cool_secret good_secret",
			wantsErr: true,
		},
		{
			name:     "visibility is only allowed with --org",
			args:     "cool_secret -v all",
			wantsErr: true,
		},
		{
			name: "providing --repos without --visibility implies selected visibility",
			args: "cool_secret --body secret-body --org coolOrg --repos coolRepo",
			wants: SetOptions{
				SecretName:      "cool_secret",
				Visibility:      shared.Selected,
				RepositoryNames: []string{"coolRepo"},
				Body:            "secret-body",
				OrgName:         "coolOrg",
			},
		},
		{
			name: "providing --no-repos-selected without --visibility implies selected visibility",
			args: "cool_secret --body secret-body --org coolOrg --no-repos-selected",
			wants: SetOptions{
				SecretName:      "cool_secret",
				Visibility:      shared.Selected,
				RepositoryNames: []string{},
				Body:            "secret-body",
				OrgName:         "coolOrg",
			},
		},
		{
			name: "org with selected repo",
			args: "-o coolOrg --body secret-body -v selected -r coolRepo cool_secret",
			wants: SetOptions{
				SecretName:      "cool_secret",
				Visibility:      shared.Selected,
				RepositoryNames: []string{"coolRepo"},
				Body:            "secret-body",
				OrgName:         "coolOrg",
			},
		},
		{
			name: "org with selected repos",
			args: `--org coolOrg --body secret-body -v selected --repos "coolRepo,radRepo,goodRepo" cool_secret`,
			wants: SetOptions{
				SecretName:      "cool_secret",
				Visibility:      shared.Selected,
				RepositoryNames: []string{"coolRepo", "goodRepo", "radRepo"},
				Body:            "secret-body",
				OrgName:         "coolOrg",
			},
		},
		{
			name: "user with selected repos",
			args: `-u --body secret-body -r "monalisa/coolRepo,cli/cli,github/hub" cool_secret`,
			wants: SetOptions{
				SecretName:      "cool_secret",
				Visibility:      shared.Selected,
				RepositoryNames: []string{"monalisa/coolRepo", "cli/cli", "github/hub"},
				Body:            "secret-body",
			},
		},
		{
			name:            "--user is mutually exclusive with --no-repos-selected",
			args:            `-u --no-repos-selected cool_secret`,
			wantsErr:        true,
			wantsErrMessage: "`--no-repos-selected` must be omitted when used with `--user`",
		},
		{
			name: "repo",
			args: `cool_secret --body "a secret"`,
			wants: SetOptions{
				SecretName: "cool_secret",
				Visibility: shared.Private,
				Body:       "a secret",
				OrgName:    "",
			},
		},
		{
			name: "env",
			args: `cool_secret --body "a secret" --env Release`,
			wants: SetOptions{
				SecretName: "cool_secret",
				Visibility: shared.Private,
				Body:       "a secret",
				OrgName:    "",
				EnvName:    "Release",
			},
		},
		{
			name: "vis all",
			args: `cool_secret --org coolOrg --body "cool" --visibility all`,
			wants: SetOptions{
				SecretName: "cool_secret",
				Visibility: shared.All,
				Body:       "cool",
				OrgName:    "coolOrg",
			},
		},
		{
			name: "no store",
			args: `cool_secret --no-store`,
			wants: SetOptions{
				SecretName: "cool_secret",
				Visibility: shared.Private,
				DoNotStore: true,
			},
		},
		{
			name: "Dependabot repo",
			args: `cool_secret --body "a secret" --app Dependabot`,
			wants: SetOptions{
				SecretName:  "cool_secret",
				Visibility:  shared.Private,
				Body:        "a secret",
				OrgName:     "",
				Application: "Dependabot",
			},
		},
		{
			name: "Dependabot org",
			args: "--org coolOrg --body secret-body --visibility selected --repos coolRepo cool_secret --app Dependabot",
			wants: SetOptions{
				SecretName:      "cool_secret",
				Visibility:      shared.Selected,
				RepositoryNames: []string{"coolRepo"},
				Body:            "secret-body",
				OrgName:         "coolOrg",
				Application:     "Dependabot",
			},
		},
		{
			name: "Codespaces org",
			args: `random_secret --org coolOrg --body "random value" --visibility selected --repos "coolRepo,cli/cli" --app Codespaces`,
			wants: SetOptions{
				SecretName:      "random_secret",
				Visibility:      shared.Selected,
				RepositoryNames: []string{"coolRepo", "cli/cli"},
				Body:            "random value",
				OrgName:         "coolOrg",
				Application:     "Codespaces",
			},
		},
		{
			name: "Agents org",
			args: `random_secret --org coolOrg --body "random value" --visibility selected --repos "coolRepo,cli/cli" --app Agents`,
			wants: SetOptions{
				SecretName:      "random_secret",
				Visibility:      shared.Selected,
				RepositoryNames: []string{"coolRepo", "cli/cli"},
				Body:            "random value",
				OrgName:         "coolOrg",
				Application:     "Agents",
			},
		},
		{
			name: "Agents repo",
			args: `cool_secret --body "a secret" --app Agents`,
			wants: SetOptions{
				SecretName:  "cool_secret",
				Visibility:  shared.Private,
				Body:        "a secret",
				OrgName:     "",
				Application: "Agents",
			},
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			ios, _, _, _ := iostreams.Test()
			f := &cmdutil.Factory{
				IOStreams: ios,
			}

			ios.SetStdinTTY(tt.stdinTTY)

			argv, err := shlex.Split(tt.args)
			assert.NoError(t, err)

			var gotOpts *SetOptions
			cmd := NewCmdSet(f, func(opts *SetOptions) error {
				gotOpts = opts
				return nil
			})
			cmd.SetArgs(argv)
			cmd.SetIn(&bytes.Buffer{})
			cmd.SetOut(&bytes.Buffer{})
			cmd.SetErr(&bytes.Buffer{})

			_, err = cmd.ExecuteC()
			if tt.wantsErr {
				assert.Error(t, err)
				if tt.wantsErrMessage != "" {
					assert.EqualError(t, err, tt.wantsErrMessage)
				}
				return
			}
			assert.NoError(t, err)

			assert.Equal(t, tt.wants.SecretName, gotOpts.SecretName)
			assert.Equal(t, tt.wants.Body, gotOpts.Body)
			assert.Equal(t, tt.wants.Visibility, gotOpts.Visibility)
			assert.Equal(t, tt.wants.OrgName, gotOpts.OrgName)
			assert.Equal(t, tt.wants.EnvName, gotOpts.EnvName)
			assert.Equal(t, tt.wants.DoNotStore, gotOpts.DoNotStore)
			assert.ElementsMatch(t, tt.wants.RepositoryNames, gotOpts.RepositoryNames)
			assert.Equal(t, tt.wants.Application, gotOpts.Application)
		})
	}
}

func TestNewCmdSetBaseRepoFuncs(t *testing.T) {
	multipleRemotes := ghContext.Remotes{
		&ghContext.Remote{
			Remote: &git.Remote{
				Name: "origin",
			},
			Repo: ghrepo.New("owner", "fork"),
		},
		&ghContext.Remote{
			Remote: &git.Remote{
				Name: "upstream",
			},
			Repo: ghrepo.New("owner", "repo"),
		},
	}

	singleRemote := ghContext.Remotes{
		&ghContext.Remote{
			Remote: &git.Remote{
				Name: "origin",
			},
			Repo: ghrepo.New("owner", "repo"),
		},
	}

	tests := []struct {
		name          string
		args          string
		env           map[string]string
		remotes       ghContext.Remotes
		prompterStubs func(*prompter.MockPrompter)
		wantRepo      ghrepo.Interface
		wantErr       error
	}{
		{
			name:     "when there is a repo flag provided, the factory base repo func is used",
			args:     "SECRET_NAME --repo owner/repo",
			remotes:  multipleRemotes,
			wantRepo: ghrepo.New("owner", "repo"),
		},
		{
			name: "when GH_REPO env var is provided, the factory base repo func is used",
			args: "SECRET_NAME",
			env: map[string]string{
				"GH_REPO": "owner/repo",
			},
			remotes:  multipleRemotes,
			wantRepo: ghrepo.New("owner", "repo"),
		},
		{
			name:    "when there is no repo flag or GH_REPO env var provided, and no prompting, the base func requiring no ambiguity is used",
			args:    "SECRET_NAME",
			remotes: multipleRemotes,
			wantErr: shared.AmbiguousBaseRepoError{
				Remotes: multipleRemotes,
			},
		},
		{
			name:     "when there is no repo flag or GH_REPO env provided, and there is a single remote, the factory base repo func is used",
			args:     "SECRET_NAME",
			remotes:  singleRemote,
			wantRepo: ghrepo.New("owner", "repo"),
		},
		{
			name:    "when there is no repo flag or GH_REPO env var provided, and can prompt, the base func resolving ambiguity is used",
			args:    "SECRET_NAME",
			remotes: multipleRemotes,
			prompterStubs: func(pm *prompter.MockPrompter) {
				pm.RegisterSelect(
					"Select a repo",
					[]string{"owner/fork", "owner/repo"},
					func(_, _ string, opts []string) (int, error) {
						return prompter.IndexFor(opts, "owner/fork")
					},
				)
			},
			wantRepo: ghrepo.New("owner", "fork"),
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			ios, _, _, _ := iostreams.Test()
			var pm *prompter.MockPrompter
			if tt.prompterStubs != nil {
				ios.SetStdinTTY(true)
				ios.SetStdoutTTY(true)
				ios.SetStderrTTY(true)
				pm = prompter.NewMockPrompter(t)
				tt.prompterStubs(pm)
			}

			f := &cmdutil.Factory{
				IOStreams: ios,
				BaseRepo: func() (ghrepo.Interface, error) {
					return ghrepo.FromFullName("owner/repo")
				},
				Prompter: pm,
				Remotes: func() (ghContext.Remotes, error) {
					return tt.remotes, nil
				},
			}

			for k, v := range tt.env {
				t.Setenv(k, v)
			}

			argv, err := shlex.Split(tt.args)
			assert.NoError(t, err)

			var gotOpts *SetOptions
			cmd := NewCmdSet(f, func(opts *SetOptions) error {
				gotOpts = opts
				return nil
			})
			// Require to support --repo flag
			cmdutil.EnableRepoOverride(cmd, f)
			cmd.SetArgs(argv)
			cmd.SetIn(&bytes.Buffer{})
			cmd.SetOut(io.Discard)
			cmd.SetErr(io.Discard)

			_, err = cmd.ExecuteC()
			require.NoError(t, err)

			baseRepo, err := gotOpts.BaseRepo()
			if tt.wantErr != nil {
				require.Equal(t, tt.wantErr, err)
				return
			}
			require.True(t, ghrepo.IsSame(tt.wantRepo, baseRepo))
		})
	}
}

func Test_setRun_repo(t *testing.T) {
	tests := []struct {
		name    string
		opts    *SetOptions
		wantApp string
	}{
		{
			name: "Actions",
			opts: &SetOptions{
				Application: "actions",
			},
			wantApp: "actions",
		},
		{
			name: "Agents",
			opts: &SetOptions{
				Application: "agents",
			},
			wantApp: "agents",
		},
		{
			name: "Dependabot",
			opts: &SetOptions{
				Application: "dependabot",
			},
			wantApp: "dependabot",
		},
		{
			name: "defaults to Actions",
			opts: &SetOptions{
				Application: "",
			},
			wantApp: "actions",
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			reg := &httpmock.Registry{}

			reg.Register(httpmock.REST("GET", fmt.Sprintf("repos/owner/repo/%s/secrets/public-key", tt.wantApp)),
				httpmock.JSONResponse(PubKey{ID: "123", Key: "CDjXqf7AJBXWhMczcy+Fs7JlACEptgceysutztHaFQI="}))

			reg.Register(httpmock.REST("PUT", fmt.Sprintf("repos/owner/repo/%s/secrets/cool_secret", tt.wantApp)),
				httpmock.StatusStringResponse(201, `{}`))

			ios, _, _, _ := iostreams.Test()

			opts := &SetOptions{
				HttpClient: func() (*http.Client, error) {
					return &http.Client{Transport: reg}, nil
				},
				Config: func() (gh.Config, error) { return config.NewBlankConfig(), nil },
				BaseRepo: func() (ghrepo.Interface, error) {
					return ghrepo.FromFullName("owner/repo")
				},
				IO:             ios,
				SecretName:     "cool_secret",
				Body:           "a secret",
				RandomOverride: fakeRandom,
				Application:    tt.opts.Application,
			}

			err := setRun(opts)
			assert.NoError(t, err)

			reg.Verify(t)

			data, err := io.ReadAll(reg.Requests[1].Body)
			assert.NoError(t, err)
			var payload SecretPayload
			err = json.Unmarshal(data, &payload)
			assert.NoError(t, err)
			assert.Equal(t, payload.KeyID, "123")
			assert.Equal(t, payload.EncryptedValue, "UKYUCbHd0DJemxa3AOcZ6XcsBwALG9d4bpB8ZT0gSV39vl3BHiGSgj8zJapDxgB2BwqNqRhpjC4=")
		})
	}
}

func Test_setRun_env(t *testing.T) {
	reg := &httpmock.Registry{}

	reg.Register(httpmock.REST("GET", "repos/owner/repo/environments/development/secrets/public-key"),
		httpmock.JSONResponse(PubKey{ID: "123", Key: "CDjXqf7AJBXWhMczcy+Fs7JlACEptgceysutztHaFQI="}))

	reg.Register(httpmock.REST("PUT", "repos/owner/repo/environments/development/secrets/cool_secret"), httpmock.StatusStringResponse(201, `{}`))

	ios, _, _, _ := iostreams.Test()

	opts := &SetOptions{
		HttpClient: func() (*http.Client, error) {
			return &http.Client{Transport: reg}, nil
		},
		Config: func() (gh.Config, error) { return config.NewBlankConfig(), nil },
		BaseRepo: func() (ghrepo.Interface, error) {
			return ghrepo.FromFullName("owner/repo")
		},
		EnvName:        "development",
		IO:             ios,
		SecretName:     "cool_secret",
		Body:           "a secret",
		RandomOverride: fakeRandom,
	}

	err := setRun(opts)
	assert.NoError(t, err)

	reg.Verify(t)

	data, err := io.ReadAll(reg.Requests[1].Body)
	assert.NoError(t, err)
	var payload SecretPayload
	err = json.Unmarshal(data, &payload)
	assert.NoError(t, err)
	assert.Equal(t, payload.KeyID, "123")
	assert.Equal(t, payload.EncryptedValue, "UKYUCbHd0DJemxa3AOcZ6XcsBwALG9d4bpB8ZT0gSV39vl3BHiGSgj8zJapDxgB2BwqNqRhpjC4=")
}

func Test_setRun_org(t *testing.T) {
	tests := []struct {
		name                       string
		opts                       *SetOptions
		wantVisibility             shared.Visibility
		wantRepositories           []int64
		wantDependabotRepositories []string
		wantApp                    string
	}{
		{
			name: "all vis",
			opts: &SetOptions{
				OrgName:    "UmbrellaCorporation",
				Visibility: shared.All,
			},
			wantApp: "actions",
		},
		{
			name: "selected visibility",
			opts: &SetOptions{
				OrgName:         "UmbrellaCorporation",
				Visibility:      shared.Selected,
				RepositoryNames: []string{"birkin", "UmbrellaCorporation/wesker"},
			},
			wantRepositories: []int64{1, 2},
			wantApp:          "actions",
		},
		{
			name: "no repos visibility",
			opts: &SetOptions{
				OrgName:         "UmbrellaCorporation",
				Visibility:      shared.Selected,
				RepositoryNames: []string{},
			},
			wantRepositories: []int64{},
			wantApp:          "actions",
		},
		{
			name: "Dependabot",
			opts: &SetOptions{
				OrgName:     "UmbrellaCorporation",
				Visibility:  shared.All,
				Application: shared.Dependabot,
			},
			wantApp: "dependabot",
		},
		{
			name: "Dependabot selected visibility",
			opts: &SetOptions{
				OrgName:         "UmbrellaCorporation",
				Visibility:      shared.Selected,
				Application:     shared.Dependabot,
				RepositoryNames: []string{"birkin", "UmbrellaCorporation/wesker"},
			},
			wantDependabotRepositories: []string{"1", "2"},
			wantApp:                    "dependabot",
		},
		{
			name: "Dependabot no repos visibility",
			opts: &SetOptions{
				OrgName:         "UmbrellaCorporation",
				Visibility:      shared.Selected,
				Application:     shared.Dependabot,
				RepositoryNames: []string{},
			},
			wantRepositories: []int64{},
			wantApp:          "dependabot",
		},
		{
			name: "Agents",
			opts: &SetOptions{
				OrgName:     "UmbrellaCorporation",
				Visibility:  shared.All,
				Application: shared.Agents,
			},
			wantApp: "agents",
		},
		{
			name: "Agents selected visibility",
			opts: &SetOptions{
				OrgName:         "UmbrellaCorporation",
				Visibility:      shared.Selected,
				Application:     shared.Agents,
				RepositoryNames: []string{"birkin", "UmbrellaCorporation/wesker"},
			},
			wantRepositories: []int64{1, 2},
			wantApp:          "agents",
		},
		{
			name: "Agents no repos visibility",
			opts: &SetOptions{
				OrgName:         "UmbrellaCorporation",
				Visibility:      shared.Selected,
				Application:     shared.Agents,
				RepositoryNames: []string{},
			},
			wantRepositories: []int64{},
			wantApp:          "agents",
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			reg := &httpmock.Registry{}

			orgName := tt.opts.OrgName

			reg.Register(httpmock.REST("GET",
				fmt.Sprintf("orgs/%s/%s/secrets/public-key", orgName, tt.wantApp)),
				httpmock.JSONResponse(PubKey{ID: "123", Key: "CDjXqf7AJBXWhMczcy+Fs7JlACEptgceysutztHaFQI="}))

			reg.Register(httpmock.REST("PUT",
				fmt.Sprintf("orgs/%s/%s/secrets/cool_secret", orgName, tt.wantApp)),
				httpmock.StatusStringResponse(201, `{}`))

			if len(tt.opts.RepositoryNames) > 0 {
				reg.Register(httpmock.GraphQL(`query MapRepositoryNames\b`),
					httpmock.StringResponse(`{"data":{"repo_0001":{"databaseId":1},"repo_0002":{"databaseId":2}}}`))
			}

			ios, _, _, _ := iostreams.Test()

			tt.opts.BaseRepo = func() (ghrepo.Interface, error) {
				return ghrepo.FromFullName("owner/repo")
			}
			tt.opts.HttpClient = func() (*http.Client, error) {
				return &http.Client{Transport: reg}, nil
			}
			tt.opts.Config = func() (gh.Config, error) {
				return config.NewBlankConfig(), nil
			}
			tt.opts.IO = ios
			tt.opts.SecretName = "cool_secret"
			tt.opts.Body = "a secret"
			tt.opts.RandomOverride = fakeRandom

			err := setRun(tt.opts)
			assert.NoError(t, err)

			reg.Verify(t)

			data, err := io.ReadAll(reg.Requests[len(reg.Requests)-1].Body)
			assert.NoError(t, err)

			if tt.opts.Application == shared.Dependabot {
				var payload DependabotSecretPayload
				err = json.Unmarshal(data, &payload)
				assert.NoError(t, err)
				assert.Equal(t, payload.KeyID, "123")
				assert.Equal(t, payload.EncryptedValue, "UKYUCbHd0DJemxa3AOcZ6XcsBwALG9d4bpB8ZT0gSV39vl3BHiGSgj8zJapDxgB2BwqNqRhpjC4=")
				assert.Equal(t, payload.Visibility, tt.opts.Visibility)
				assert.ElementsMatch(t, payload.Repositories, tt.wantDependabotRepositories)
			} else {
				var payload SecretPayload
				err = json.Unmarshal(data, &payload)
				assert.NoError(t, err)
				assert.Equal(t, payload.KeyID, "123")
				assert.Equal(t, payload.EncryptedValue, "UKYUCbHd0DJemxa3AOcZ6XcsBwALG9d4bpB8ZT0gSV39vl3BHiGSgj8zJapDxgB2BwqNqRhpjC4=")
				assert.Equal(t, payload.Visibility, tt.opts.Visibility)
				assert.ElementsMatch(t, payload.Repositories, tt.wantRepositories)
			}
		})
	}
}

func Test_setRun_user(t *testing.T) {
	tests := []struct {
		name             string
		opts             *SetOptions
		wantVisibility   shared.Visibility
		wantRepositories []int64
	}{
		{
			name: "all vis",
			opts: &SetOptions{
				UserSecrets: true,
				Visibility:  shared.All,
			},
		},
		{
			name: "selected visibility",
			opts: &SetOptions{
				UserSecrets:     true,
				Visibility:      shared.Selected,
				RepositoryNames: []string{"cli/cli", "github/hub"},
			},
			wantRepositories: []int64{212613049, 401025},
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			reg := &httpmock.Registry{}

			reg.Register(httpmock.REST("GET", "user/codespaces/secrets/public-key"),
				httpmock.JSONResponse(PubKey{ID: "123", Key: "CDjXqf7AJBXWhMczcy+Fs7JlACEptgceysutztHaFQI="}))

			reg.Register(httpmock.REST("PUT", "user/codespaces/secrets/cool_secret"),
				httpmock.StatusStringResponse(201, `{}`))

			if len(tt.opts.RepositoryNames) > 0 {
				reg.Register(httpmock.GraphQL(`query MapRepositoryNames\b`),
					httpmock.StringResponse(`{"data":{"repo_0001":{"databaseId":212613049},"repo_0002":{"databaseId":401025}}}`))
			}

			ios, _, _, _ := iostreams.Test()

			tt.opts.HttpClient = func() (*http.Client, error) {
				return &http.Client{Transport: reg}, nil
			}
			tt.opts.Config = func() (gh.Config, error) {
				return config.NewBlankConfig(), nil
			}
			tt.opts.IO = ios
			tt.opts.SecretName = "cool_secret"
			tt.opts.Body = "a secret"
			tt.opts.RandomOverride = fakeRandom

			err := setRun(tt.opts)
			assert.NoError(t, err)

			reg.Verify(t)

			data, err := io.ReadAll(reg.Requests[len(reg.Requests)-1].Body)
			assert.NoError(t, err)
			var payload SecretPayload
			err = json.Unmarshal(data, &payload)
			assert.NoError(t, err)
			assert.Equal(t, payload.KeyID, "123")
			assert.Equal(t, payload.EncryptedValue, "UKYUCbHd0DJemxa3AOcZ6XcsBwALG9d4bpB8ZT0gSV39vl3BHiGSgj8zJapDxgB2BwqNqRhpjC4=")
			assert.ElementsMatch(t, payload.Repositories, tt.wantRepositories)
		})
	}
}

func Test_setRun_shouldNotStore(t *testing.T) {
	reg := &httpmock.Registry{}
	defer reg.Verify(t)

	reg.Register(httpmock.REST("GET", "repos/owner/repo/actions/secrets/public-key"),
		httpmock.JSONResponse(PubKey{ID: "123", Key: "CDjXqf7AJBXWhMczcy+Fs7JlACEptgceysutztHaFQI="}))

	ios, _, stdout, stderr := iostreams.Test()

	opts := &SetOptions{
		HttpClient: func() (*http.Client, error) {
			return &http.Client{Transport: reg}, nil
		},
		Config: func() (gh.Config, error) {
			return config.NewBlankConfig(), nil
		},
		BaseRepo: func() (ghrepo.Interface, error) {
			return ghrepo.FromFullName("owner/repo")
		},
		IO:             ios,
		Body:           "a secret",
		DoNotStore:     true,
		RandomOverride: fakeRandom,
	}

	err := setRun(opts)
	assert.NoError(t, err)

	assert.Equal(t, "UKYUCbHd0DJemxa3AOcZ6XcsBwALG9d4bpB8ZT0gSV39vl3BHiGSgj8zJapDxgB2BwqNqRhpjC4=\n", stdout.String())
	assert.Equal(t, "", stderr.String())
}

func Test_getBody(t *testing.T) {
	tests := []struct {
		name    string
		bodyArg string
		want    string
		stdin   string
	}{
		{
			name:    "literal value",
			bodyArg: "a secret",
			want:    "a secret",
		},
		{
			name:  "from stdin",
			want:  "a secret",
			stdin: "a secret",
		},
		{
			name:  "from stdin with trailing newline character",
			want:  "a secret",
			stdin: "a secret\n",
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			ios, stdin, _, _ := iostreams.Test()

			ios.SetStdinTTY(false)

			_, err := stdin.WriteString(tt.stdin)
			assert.NoError(t, err)

			body, err := getBody(&SetOptions{
				Body: tt.bodyArg,
				IO:   ios,
			})
			assert.NoError(t, err)

			assert.Equal(t, tt.want, string(body))
		})
	}
}

func Test_getBodyPrompt(t *testing.T) {
	ios, _, _, _ := iostreams.Test()
	ios.SetStdinTTY(true)
	ios.SetStdoutTTY(true)

	pm := prompter.NewMockPrompter(t)
	pm.RegisterPassword("Paste your secret:", func(_ string) (string, error) {
		return "cool secret", nil
	})

	body, err := getBody(&SetOptions{
		IO:       ios,
		Prompter: pm,
	})
	assert.NoError(t, err)
	assert.Equal(t, string(body), "cool secret")
}

func Test_getSecretsFromOptions(t *testing.T) {
	genFile := func(s string) string {
		f, err := os.CreateTemp("", "gh-env.*")
		if err != nil {
			t.Fatal(err)
			return ""
		}
		defer f.Close()
		t.Cleanup(func() {
			_ = os.Remove(f.Name())
		})
		_, err = f.WriteString(s)
		if err != nil {
			t.Fatal(err)
		}
		return f.Name()
	}

	tests := []struct {
		name    string
		opts    SetOptions
		isTTY   bool
		stdin   string
		want    map[string]string
		wantErr bool
	}{
		{
			name: "secret from arg",
			opts: SetOptions{
				SecretName: "FOO",
				Body:       "bar",
				EnvFile:    "",
			},
			want: map[string]string{"FOO": "bar"},
		},
		{
			name: "secrets from stdin",
			opts: SetOptions{
				Body:    "",
				EnvFile: "-",
			},
			stdin: `FOO=bar`,
			want:  map[string]string{"FOO": "bar"},
		},
		{
			name: "secrets from file",
			opts: SetOptions{
				Body: "",
				EnvFile: genFile(heredoc.Doc(`
					FOO=bar
					QUOTED="my value"
					#IGNORED=true
					export SHELL=bash
				`)),
			},
			stdin: `FOO=bar`,
			want: map[string]string{
				"FOO":    "bar",
				"SHELL":  "bash",
				"QUOTED": "my value",
			},
		},
	}
	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			ios, stdin, _, _ := iostreams.Test()
			ios.SetStdinTTY(tt.isTTY)
			ios.SetStdoutTTY(tt.isTTY)
			stdin.WriteString(tt.stdin)
			opts := tt.opts
			opts.IO = ios
			gotSecrets, err := getSecretsFromOptions(&opts)
			if err != nil {
				if !tt.wantErr {
					t.Fatalf("getSecretsFromOptions() error = %v, wantErr %v", err, tt.wantErr)
				}
			} else if tt.wantErr {
				t.Fatalf("getSecretsFromOptions() error = %v, wantErr %v", err, tt.wantErr)
			}
			if len(gotSecrets) != len(tt.want) {
				t.Fatalf("getSecretsFromOptions() = got %d secrets, want %d", len(gotSecrets), len(tt.want))
			}
			for k, v := range gotSecrets {
				if tt.want[k] != string(v) {
					t.Errorf("getSecretsFromOptions() %s = got %q, want %q", k, string(v), tt.want[k])
				}
			}
		})
	}
}

func fakeRandom() io.Reader {
	return bytes.NewReader(bytes.Repeat([]byte{5}, 32))
}
