package base

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

	"github.com/cli/cli/v2/context"
	"github.com/cli/cli/v2/git"
	"github.com/cli/cli/v2/internal/ghrepo"
	"github.com/cli/cli/v2/internal/prompter"
	"github.com/cli/cli/v2/internal/run"
	"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"
)

func TestNewCmdSetDefault(t *testing.T) {
	tests := []struct {
		name     string
		gitStubs func(*run.CommandStubber)
		remotes  func() (context.Remotes, error)
		input    string
		output   SetDefaultOptions
		wantErr  bool
		errMsg   string
	}{
		{
			name: "no argument",
			gitStubs: func(cs *run.CommandStubber) {
				cs.Register(`git rev-parse --git-dir`, 0, ".git")
			},
			input:  "",
			output: SetDefaultOptions{},
		},
		{
			name: "repo argument",
			gitStubs: func(cs *run.CommandStubber) {
				cs.Register(`git rev-parse --git-dir`, 0, ".git")
			},
			input:  "cli/cli",
			output: SetDefaultOptions{Repo: ghrepo.New("cli", "cli")},
		},
		{
			name: "invalid repo argument",
			gitStubs: func(cs *run.CommandStubber) {
				cs.Register(`git rev-parse --git-dir`, 0, ".git")
			},
			input:   "some_invalid_format",
			wantErr: true,
			errMsg:  `given arg is not a valid repo or git remote: expected the "[HOST/]OWNER/REPO" format, got "some_invalid_format"`,
		},
		{
			name: "view flag",
			gitStubs: func(cs *run.CommandStubber) {
				cs.Register(`git rev-parse --git-dir`, 0, ".git")
			},
			input:  "--view",
			output: SetDefaultOptions{ViewMode: true},
		},
		{
			name: "unset flag",
			gitStubs: func(cs *run.CommandStubber) {
				cs.Register(`git rev-parse --git-dir`, 0, ".git")
			},
			input:  "--unset",
			output: SetDefaultOptions{UnsetMode: true},
		},
		{
			name: "run from non-git directory",
			gitStubs: func(cs *run.CommandStubber) {
				cs.Register(`git rev-parse --git-dir`, 128, "")
			},
			input:   "",
			wantErr: true,
			errMsg:  "must be run from inside a git repository",
		},
		{
			name: "remote name argument",
			gitStubs: func(cs *run.CommandStubber) {
				cs.Register(`git rev-parse --git-dir`, 0, ".git")
			},
			remotes: func() (context.Remotes, error) {
				return context.Remotes{
					{
						Remote: &git.Remote{Name: "origin"},
						Repo:   ghrepo.New("OWNER", "REPO"),
					},
				}, nil
			},
			input:  "origin",
			output: SetDefaultOptions{Repo: ghrepo.New("OWNER", "REPO")},
		},
		{
			name: "repo argument despite remote name matching owner/repo",
			gitStubs: func(cs *run.CommandStubber) {
				cs.Register(`git rev-parse --git-dir`, 0, ".git")
			},
			remotes: func() (context.Remotes, error) {
				return context.Remotes{
					{
						Remote: &git.Remote{Name: "OWNER/REPO"},
						Repo:   ghrepo.New("OTHER", "REPO"),
					},
				}, nil
			},
			input:  "OWNER/REPO",
			output: SetDefaultOptions{Repo: ghrepo.New("OWNER", "REPO")},
		},
	}

	for _, tt := range tests {
		io, _, _, _ := iostreams.Test()
		io.SetStdoutTTY(true)
		io.SetStdinTTY(true)
		io.SetStderrTTY(true)
		remotesFunc := tt.remotes
		if remotesFunc == nil {
			remotesFunc = func() (context.Remotes, error) {
				return context.Remotes{}, nil
			}
		}

		f := &cmdutil.Factory{
			IOStreams: io,
			GitClient: &git.Client{GitPath: "/fake/path/to/git"},
			Remotes:   remotesFunc,
		}

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

		t.Run(tt.name, func(t *testing.T) {
			argv, err := shlex.Split(tt.input)
			assert.NoError(t, err)

			cmd.SetArgs(argv)

			cs, teardown := run.Stub()
			defer teardown(t)
			tt.gitStubs(cs)

			_, err = cmd.ExecuteC()
			if tt.wantErr {
				assert.EqualError(t, err, tt.errMsg)
				return
			}

			assert.NoError(t, err)
			assert.Equal(t, tt.output.Repo, gotOpts.Repo)
			assert.Equal(t, tt.output.ViewMode, gotOpts.ViewMode)
		})
	}
}

func TestDefaultRun(t *testing.T) {
	repo1, _ := ghrepo.FromFullName("OWNER/REPO")
	repo2, _ := ghrepo.FromFullName("OWNER2/REPO2")
	repo3, _ := ghrepo.FromFullName("OWNER3/REPO3")
	repo4, _ := ghrepo.FromFullName("OWNER4/REPO4")
	repo5, _ := ghrepo.FromFullName("OWNER5/REPO5")
	repo6, _ := ghrepo.FromFullName("OWNER6/REPO6")

	tests := []struct {
		name          string
		tty           bool
		opts          SetDefaultOptions
		remotes       []*context.Remote
		httpStubs     func(*httpmock.Registry)
		gitStubs      func(*run.CommandStubber)
		prompterStubs func(*prompter.PrompterMock)
		wantStdout    string
		wantStderr    string
		wantErr       bool
		errMsg        string
	}{
		{
			name: "unset mode with base resolved current default",
			tty:  true,
			opts: SetDefaultOptions{UnsetMode: true},
			remotes: []*context.Remote{
				{
					Remote: &git.Remote{Name: "origin", Resolved: "base"},
					Repo:   repo1,
				},
			},
			gitStubs: func(cs *run.CommandStubber) {
				cs.Register(`git config --unset remote.origin.gh-resolved`, 0, "")
			},
			wantStdout: "✓ Unset OWNER/REPO as default repository\n",
		},
		{
			name: "unset mode no current default",
			tty:  true,
			opts: SetDefaultOptions{UnsetMode: true},
			remotes: []*context.Remote{
				{
					Remote: &git.Remote{Name: "origin"},
					Repo:   repo1,
				},
			},
			wantStdout: "no default repository has been set\n",
		},
		{
			name: "tty view mode no current default",
			tty:  true,
			opts: SetDefaultOptions{ViewMode: true},
			remotes: []*context.Remote{
				{
					Remote: &git.Remote{Name: "origin"},
					Repo:   repo1,
				},
			},
			wantStderr: "X No default remote repository has been set. To learn more about the default repository, run: gh repo set-default --help\n",
		},
		{
			name: "view mode no current default",
			tty:  false,
			opts: SetDefaultOptions{ViewMode: true},
			remotes: []*context.Remote{
				{
					Remote: &git.Remote{Name: "origin"},
					Repo:   repo1,
				},
			},
			wantStderr: "X No default remote repository has been set. To learn more about the default repository, run: gh repo set-default --help\n",
		},
		{
			name: "view mode with base resolved current default",
			opts: SetDefaultOptions{ViewMode: true},
			remotes: []*context.Remote{
				{
					Remote: &git.Remote{Name: "origin", Resolved: "base"},
					Repo:   repo1,
				},
			},
			wantStdout: "OWNER/REPO\n",
		},
		{
			name: "view mode with non-base resolved current default",
			opts: SetDefaultOptions{ViewMode: true},
			remotes: []*context.Remote{
				{
					Remote: &git.Remote{Name: "origin", Resolved: "PARENT/REPO"},
					Repo:   repo1,
				},
			},
			wantStdout: "PARENT/REPO\n",
		},
		{
			name: "tty non-interactive mode no current default",
			tty:  true,
			opts: SetDefaultOptions{Repo: repo2},
			remotes: []*context.Remote{
				{
					Remote: &git.Remote{Name: "origin"},
					Repo:   repo1,
				},
				{
					Remote: &git.Remote{Name: "upstream"},
					Repo:   repo2,
				},
			},
			httpStubs: func(reg *httpmock.Registry) {
				reg.Register(
					httpmock.GraphQL(`query RepositoryNetwork\b`),
					httpmock.StringResponse(`{"data":{"repo_000":{"name":"REPO2","owner":{"login":"OWNER2"}}}}`),
				)
			},
			gitStubs: func(cs *run.CommandStubber) {
				cs.Register(`git config --add remote.upstream.gh-resolved base`, 0, "")
			},
			wantStdout: "✓ Set OWNER2/REPO2 as the default repository for the current directory\n",
		},
		{
			name: "tty non-interactive mode set non-base default",
			tty:  true,
			opts: SetDefaultOptions{Repo: repo2},
			remotes: []*context.Remote{
				{
					Remote: &git.Remote{Name: "origin"},
					Repo:   repo1,
				},
				{
					Remote: &git.Remote{Name: "upstream"},
					Repo:   repo3,
				},
			},
			httpStubs: func(reg *httpmock.Registry) {
				reg.Register(
					httpmock.GraphQL(`query RepositoryNetwork\b`),
					httpmock.StringResponse(`{"data":{"repo_000":{"name":"REPO","owner":{"login":"OWNER"},"parent":{"name":"REPO2","owner":{"login":"OWNER2"}}}}}`),
				)
			},
			gitStubs: func(cs *run.CommandStubber) {
				cs.Register(`git config --add remote.upstream.gh-resolved OWNER2/REPO2`, 0, "")
			},
			wantStdout: "✓ Set OWNER2/REPO2 as the default repository for the current directory\n",
		},
		{
			name: "non-tty non-interactive mode no current default",
			opts: SetDefaultOptions{Repo: repo2},
			remotes: []*context.Remote{
				{
					Remote: &git.Remote{Name: "origin"},
					Repo:   repo1,
				},
				{
					Remote: &git.Remote{Name: "upstream"},
					Repo:   repo2,
				},
			},
			httpStubs: func(reg *httpmock.Registry) {
				reg.Register(
					httpmock.GraphQL(`query RepositoryNetwork\b`),
					httpmock.StringResponse(`{"data":{"repo_000":{"name":"REPO2","owner":{"login":"OWNER2"}}}}`),
				)
			},
			gitStubs: func(cs *run.CommandStubber) {
				cs.Register(`git config --add remote.upstream.gh-resolved base`, 0, "")
			},
			wantStdout: "",
		},
		{
			name: "non-interactive mode with current default",
			tty:  true,
			opts: SetDefaultOptions{Repo: repo2},
			remotes: []*context.Remote{
				{
					Remote: &git.Remote{Name: "origin", Resolved: "base"},
					Repo:   repo1,
				},
				{
					Remote: &git.Remote{Name: "upstream"},
					Repo:   repo2,
				},
			},
			httpStubs: func(reg *httpmock.Registry) {
				reg.Register(
					httpmock.GraphQL(`query RepositoryNetwork\b`),
					httpmock.StringResponse(`{"data":{"repo_000":{"name":"REPO2","owner":{"login":"OWNER2"}}}}`),
				)
			},
			gitStubs: func(cs *run.CommandStubber) {
				cs.Register(`git config --unset remote.origin.gh-resolved`, 0, "")
				cs.Register(`git config --add remote.upstream.gh-resolved base`, 0, "")
			},
			wantStdout: "✓ Set OWNER2/REPO2 as the default repository for the current directory\n",
		},
		{
			name: "non-interactive mode no known hosts",
			opts: SetDefaultOptions{Repo: repo2},
			remotes: []*context.Remote{
				{
					Remote: &git.Remote{Name: "origin"},
					Repo:   repo1,
				},
			},
			httpStubs: func(reg *httpmock.Registry) {
				reg.Register(
					httpmock.GraphQL(`query RepositoryNetwork\b`),
					httpmock.StringResponse(`{"data":{}}`),
				)
			},
			wantErr: true,
			errMsg:  "none of the git remotes correspond to a valid remote repository",
		},
		{
			name: "non-interactive mode no matching remotes",
			opts: SetDefaultOptions{Repo: repo2},
			remotes: []*context.Remote{
				{
					Remote: &git.Remote{Name: "origin"},
					Repo:   repo1,
				},
			},
			httpStubs: func(reg *httpmock.Registry) {
				reg.Register(
					httpmock.GraphQL(`query RepositoryNetwork\b`),
					httpmock.StringResponse(`{"data":{"repo_000":{"name":"REPO","owner":{"login":"OWNER"}}}}`),
				)
			},
			wantErr: true,
			errMsg:  "OWNER2/REPO2 does not correspond to any git remotes",
		},
		{
			name: "interactive mode",
			tty:  true,
			opts: SetDefaultOptions{},
			remotes: []*context.Remote{
				{
					Remote: &git.Remote{Name: "origin"},
					Repo:   repo1,
				},
				{
					Remote: &git.Remote{Name: "upstream"},
					Repo:   repo2,
				},
			},
			httpStubs: func(reg *httpmock.Registry) {
				reg.Register(
					httpmock.GraphQL(`query RepositoryNetwork\b`),
					httpmock.StringResponse(`{"data":{"repo_000":{"name":"REPO","owner":{"login":"OWNER"}},"repo_001":{"name":"REPO2","owner":{"login":"OWNER2"}}}}`),
				)
			},
			gitStubs: func(cs *run.CommandStubber) {
				cs.Register(`git config --add remote.upstream.gh-resolved base`, 0, "")
			},
			prompterStubs: func(pm *prompter.PrompterMock) {
				pm.SelectFunc = func(p, d string, opts []string) (int, error) {
					switch p {
					case "Which repository should be the default?":
						prompter.AssertOptions(t, []string{"OWNER/REPO", "OWNER2/REPO2"}, opts)
						return prompter.IndexFor(opts, "OWNER2/REPO2")
					default:
						return -1, prompter.NoSuchPromptErr(p)
					}
				}
			},
			wantStdout: "This command sets the default remote repository to use when querying the\nGitHub API for the locally cloned repository.\n\ngh uses the default repository for things like:\n\n - viewing and creating pull requests\n - viewing and creating issues\n - viewing and creating releases\n - working with GitHub Actions\n\n### NOTE: gh does not use the default repository for managing repository and environment secrets.\n\n✓ Set OWNER2/REPO2 as the default repository for the current directory\n",
		},
		{
			name: "interactive mode only one known host",
			tty:  true,
			opts: SetDefaultOptions{},
			remotes: []*context.Remote{
				{
					Remote: &git.Remote{Name: "origin"},
					Repo:   repo1,
				},
				{
					Remote: &git.Remote{Name: "upstream"},
					Repo:   repo2,
				},
			},
			httpStubs: func(reg *httpmock.Registry) {
				reg.Register(
					httpmock.GraphQL(`query RepositoryNetwork\b`),
					httpmock.StringResponse(`{"data":{"repo_000":{"name":"REPO2","owner":{"login":"OWNER2"}}}}`),
				)
			},
			gitStubs: func(cs *run.CommandStubber) {
				cs.Register(`git config --add remote.upstream.gh-resolved base`, 0, "")
			},
			wantStdout: "Found only one known remote repo, OWNER2/REPO2 on github.com.\n✓ Set OWNER2/REPO2 as the default repository for the current directory\n",
		},
		{
			name: "interactive mode more than five remotes",
			tty:  true,
			opts: SetDefaultOptions{},
			remotes: []*context.Remote{
				{Remote: &git.Remote{Name: "origin"}, Repo: repo1},
				{Remote: &git.Remote{Name: "upstream"}, Repo: repo2},
				{Remote: &git.Remote{Name: "other1"}, Repo: repo3},
				{Remote: &git.Remote{Name: "other2"}, Repo: repo4},
				{Remote: &git.Remote{Name: "other3"}, Repo: repo5},
				{Remote: &git.Remote{Name: "other4"}, Repo: repo6},
			},
			httpStubs: func(reg *httpmock.Registry) {
				reg.Register(
					httpmock.GraphQL(`query RepositoryNetwork\b`),
					httpmock.GraphQLQuery(`{"data":{
            "repo_000":{"name":"REPO","owner":{"login":"OWNER"}},
            "repo_001":{"name":"REPO2","owner":{"login":"OWNER2"}},
            "repo_002":{"name":"REPO3","owner":{"login":"OWNER3"}},
            "repo_003":{"name":"REPO4","owner":{"login":"OWNER4"}},
            "repo_004":{"name":"REPO5","owner":{"login":"OWNER5"}},
            "repo_005":{"name":"REPO6","owner":{"login":"OWNER6"}}
          }}`,
						func(query string, inputs map[string]interface{}) {
							assert.Contains(t, query, "repo_000")
							assert.Contains(t, query, "repo_001")
							assert.Contains(t, query, "repo_002")
							assert.Contains(t, query, "repo_003")
							assert.Contains(t, query, "repo_004")
							assert.Contains(t, query, "repo_005")
						}),
				)
			},
			gitStubs: func(cs *run.CommandStubber) {
				cs.Register(`git config --add remote.upstream.gh-resolved base`, 0, "")
			},
			prompterStubs: func(pm *prompter.PrompterMock) {
				pm.SelectFunc = func(p, d string, opts []string) (int, error) {
					switch p {
					case "Which repository should be the default?":
						prompter.AssertOptions(t, []string{"OWNER/REPO", "OWNER2/REPO2", "OWNER3/REPO3", "OWNER4/REPO4", "OWNER5/REPO5", "OWNER6/REPO6"}, opts)
						return prompter.IndexFor(opts, "OWNER2/REPO2")
					default:
						return -1, prompter.NoSuchPromptErr(p)
					}
				}
			},
			wantStdout: "This command sets the default remote repository to use when querying the\nGitHub API for the locally cloned repository.\n\ngh uses the default repository for things like:\n\n - viewing and creating pull requests\n - viewing and creating issues\n - viewing and creating releases\n - working with GitHub Actions\n\n### NOTE: gh does not use the default repository for managing repository and environment secrets.\n\n✓ Set OWNER2/REPO2 as the default repository for the current directory\n",
		},
	}

	for _, tt := range tests {
		reg := &httpmock.Registry{}
		if tt.httpStubs != nil {
			tt.httpStubs(reg)
		}
		tt.opts.HttpClient = func() (*http.Client, error) {
			return &http.Client{Transport: reg}, nil
		}

		io, _, stdout, stderr := iostreams.Test()
		io.SetStdinTTY(tt.tty)
		io.SetStdoutTTY(tt.tty)
		io.SetStderrTTY(tt.tty)
		tt.opts.IO = io

		tt.opts.Remotes = func() (context.Remotes, error) {
			return tt.remotes, nil
		}

		tt.opts.GitClient = &git.Client{}

		pm := &prompter.PrompterMock{}
		if tt.prompterStubs != nil {
			tt.prompterStubs(pm)
		}

		tt.opts.Prompter = pm

		t.Run(tt.name, func(t *testing.T) {
			cs, teardown := run.Stub()
			defer teardown(t)
			if tt.gitStubs != nil {
				tt.gitStubs(cs)
			}
			defer reg.Verify(t)
			err := setDefaultRun(&tt.opts)
			if tt.wantErr {
				assert.EqualError(t, err, tt.errMsg)
				return
			}
			assert.NoError(t, err)
			if tt.wantStdout != "" {
				assert.Equal(t, tt.wantStdout, stdout.String())
			} else {
				assert.Equal(t, tt.wantStderr, stderr.String())
			}
		})
	}
}
