package develop

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

	"github.com/MakeNowJust/heredoc"
	"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/run"
	"github.com/cli/cli/v2/pkg/cmd/issue/argparsetest"
	"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 TestNewCmdDevelop(t *testing.T) {
	// Test shared parsing of issue number / URL.
	argparsetest.TestArgParsing(t, NewCmdDevelop)

	tests := []struct {
		name             string
		input            string
		output           DevelopOptions
		expectedBaseRepo ghrepo.Interface
		wantStdout       string
		wantStderr       string
		wantErr          bool
		errMsg           string
	}{
		{
			name:  "branch-repo flag",
			input: "1 --branch-repo owner/repo",
			output: DevelopOptions{
				IssueNumber: 1,
				BranchRepo:  "owner/repo",
			},
		},
		{
			name:  "base flag",
			input: "1 --base feature",
			output: DevelopOptions{
				IssueNumber: 1,
				BaseBranch:  "feature",
			},
		},
		{
			name:  "checkout flag",
			input: "1 --checkout",
			output: DevelopOptions{
				IssueNumber: 1,
				Checkout:    true,
			},
		},
		{
			name:  "list flag",
			input: "1 --list",
			output: DevelopOptions{
				IssueNumber: 1,
				List:        true,
			},
		},
		{
			name:  "name flag",
			input: "1 --name feature",
			output: DevelopOptions{
				IssueNumber: 1,
				Name:        "feature",
			},
		},
		{
			name:  "issue-repo flag",
			input: "1 --issue-repo cli/cli",
			output: DevelopOptions{
				IssueNumber: 1,
			},
			wantStdout: "Flag --issue-repo has been deprecated, use `--repo` instead\n",
		},
		{
			name:    "list and branch repo flags",
			input:   "1 --list --branch-repo owner/repo",
			wantErr: true,
			errMsg:  "specify only one of `--list` or `--branch-repo`",
		},
		{
			name:    "list and base flags",
			input:   "1 --list --base feature",
			wantErr: true,
			errMsg:  "specify only one of `--list` or `--base`",
		},
		{
			name:    "list and checkout flags",
			input:   "1 --list --checkout",
			wantErr: true,
			errMsg:  "specify only one of `--list` or `--checkout`",
		},
		{
			name:    "list and name flags",
			input:   "1 --list --name my-branch",
			wantErr: true,
			errMsg:  "specify only one of `--list` or `--name`",
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			ios, _, stdOut, stdErr := iostreams.Test()
			f := &cmdutil.Factory{
				IOStreams: ios,
			}
			argv, err := shlex.Split(tt.input)
			assert.NoError(t, err)
			var gotOpts *DevelopOptions
			cmd := NewCmdDevelop(f, func(opts *DevelopOptions) error {
				gotOpts = opts
				return nil
			})
			cmd.SetArgs(argv)
			cmd.SetIn(&bytes.Buffer{})
			cmd.SetOut(stdOut)
			cmd.SetErr(stdErr)

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

			require.NoError(t, err)
			assert.Equal(t, tt.output.IssueNumber, gotOpts.IssueNumber)
			assert.Equal(t, tt.output.Name, gotOpts.Name)
			assert.Equal(t, tt.output.BaseBranch, gotOpts.BaseBranch)
			assert.Equal(t, tt.output.Checkout, gotOpts.Checkout)
			assert.Equal(t, tt.output.List, gotOpts.List)
			assert.Equal(t, tt.wantStdout, stdOut.String())
			assert.Equal(t, tt.wantStderr, stdErr.String())
			if tt.expectedBaseRepo != nil {
				baseRepo, err := gotOpts.BaseRepo()
				require.NoError(t, err)
				require.True(
					t,
					ghrepo.IsSame(tt.expectedBaseRepo, baseRepo),
					"expected base repo %+v, got %+v", tt.expectedBaseRepo, baseRepo,
				)
			}
		})
	}
}

func TestDevelopRun(t *testing.T) {
	featureEnabledPayload := `{"data":{"LinkedBranch":{"fields":[{"name":"id"},{"name":"ref"}]}}}`
	featureDisabledPayload := `{"data":{"LinkedBranch":null}}`

	tests := []struct {
		name           string
		opts           *DevelopOptions
		cmdStubs       func(*run.CommandStubber)
		runStubs       func(*run.CommandStubber)
		remotes        map[string]string
		httpStubs      func(*httpmock.Registry, *testing.T)
		expectedOut    string
		expectedErrOut string
		wantErr        string
		tty            bool
	}{
		{
			name: "returns an error when the feature is not supported by the API",
			opts: &DevelopOptions{
				IssueNumber: 42,
				List:        true,
			},
			httpStubs: func(reg *httpmock.Registry, t *testing.T) {
				reg.Register(
					httpmock.GraphQL(`query IssueByNumber\b`),
					httpmock.StringResponse(`{"data":{"repository":{"hasIssuesEnabled":true,"issue":{"id":"SOMEID","number":42}}}}`),
				)
				reg.Register(
					httpmock.GraphQL(`query LinkedBranchFeature\b`),
					httpmock.StringResponse(featureDisabledPayload),
				)
			},
			wantErr: "the `gh issue develop` command is not currently available",
		},
		{
			name: "list branches for an issue",
			opts: &DevelopOptions{
				IssueNumber: 42,
				List:        true,
			},
			httpStubs: func(reg *httpmock.Registry, t *testing.T) {
				reg.Register(
					httpmock.GraphQL(`query IssueByNumber\b`),
					httpmock.StringResponse(`{"data":{"repository":{"hasIssuesEnabled":true,"issue":{"id":"SOMEID","number":42}}}}`),
				)
				reg.Register(
					httpmock.GraphQL(`query LinkedBranchFeature\b`),
					httpmock.StringResponse(featureEnabledPayload),
				)
				reg.Register(
					httpmock.GraphQL(`query ListLinkedBranches\b`),
					httpmock.GraphQLQuery(`
		        {"data":{"repository":{"issue":{"linkedBranches":{"nodes":[{"ref":{"name":"foo","repository":{"url":"https://github.com/OWNER/REPO"}}},{"ref":{"name":"bar","repository":{"url":"https://github.com/OWNER/REPO"}}}]}}}}}
					`, func(query string, inputs map[string]interface{}) {
						assert.Equal(t, float64(42), inputs["number"])
						assert.Equal(t, "OWNER", inputs["owner"])
						assert.Equal(t, "REPO", inputs["name"])
					}))
			},
			expectedOut: "foo\thttps://github.com/OWNER/REPO/tree/foo\nbar\thttps://github.com/OWNER/REPO/tree/bar\n",
		},
		{
			name: "list branches for an issue in tty",
			opts: &DevelopOptions{
				IssueNumber: 42,
				List:        true,
			},
			tty: true,
			httpStubs: func(reg *httpmock.Registry, t *testing.T) {
				reg.Register(
					httpmock.GraphQL(`query IssueByNumber\b`),
					httpmock.StringResponse(`{"data":{"repository":{"hasIssuesEnabled":true,"issue":{"id":"SOMEID","number":42}}}}`),
				)
				reg.Register(
					httpmock.GraphQL(`query LinkedBranchFeature\b`),
					httpmock.StringResponse(featureEnabledPayload),
				)
				reg.Register(
					httpmock.GraphQL(`query ListLinkedBranches\b`),
					httpmock.GraphQLQuery(`
		        {"data":{"repository":{"issue":{"linkedBranches":{"nodes":[{"ref":{"name":"foo","repository":{"url":"https://github.com/OWNER/REPO"}}},{"ref":{"name":"bar","repository":{"url":"https://github.com/OWNER/OTHER-REPO"}}}]}}}}}
					`, func(query string, inputs map[string]interface{}) {
						assert.Equal(t, float64(42), inputs["number"])
						assert.Equal(t, "OWNER", inputs["owner"])
						assert.Equal(t, "REPO", inputs["name"])
					}))
			},
			expectedOut: heredoc.Doc(`

				Showing linked branches for OWNER/REPO#42

				BRANCH  URL
				foo     https://github.com/OWNER/REPO/tree/foo
				bar     https://github.com/OWNER/OTHER-REPO/tree/bar
			`),
		},
		{
			name: "develop new branch",
			opts: &DevelopOptions{
				IssueNumber: 123,
			},
			remotes: map[string]string{
				"origin": "OWNER/REPO",
			},
			httpStubs: func(reg *httpmock.Registry, t *testing.T) {
				reg.Register(
					httpmock.GraphQL(`query LinkedBranchFeature\b`),
					httpmock.StringResponse(featureEnabledPayload),
				)
				reg.Register(
					httpmock.GraphQL(`query IssueByNumber\b`),
					httpmock.StringResponse(`{"data":{"repository":{"hasIssuesEnabled":true,"issue":{"id": "SOMEID","number":123,"title":"my issue"}}}}`),
				)
				reg.Register(
					httpmock.GraphQL(`query FindRepoBranchID\b`),
					httpmock.StringResponse(`{"data":{"repository":{"id":"REPOID","defaultBranchRef":{"target":{"oid":"DEFAULTOID"}},"ref":{"target":{"oid":""}}}}}`),
				)
				reg.Register(
					httpmock.GraphQL(`mutation CreateLinkedBranch\b`),
					httpmock.GraphQLMutation(`{"data":{"createLinkedBranch":{"linkedBranch":{"id":"2","ref":{"name":"my-issue-1"}}}}}`,
						func(inputs map[string]interface{}) {
							assert.Equal(t, "REPOID", inputs["repositoryId"])
							assert.Equal(t, "SOMEID", inputs["issueId"])
							assert.Equal(t, "DEFAULTOID", inputs["oid"])
						}),
				)
			},
			runStubs: func(cs *run.CommandStubber) {
				cs.Register(`git fetch origin \+refs/heads/my-issue-1:refs/remotes/origin/my-issue-1`, 0, "")
			},
			expectedOut: "github.com/OWNER/REPO/tree/my-issue-1\n",
		},
		{
			name: "develop new branch in different repo than issue",
			opts: &DevelopOptions{
				IssueNumber: 123,
				BranchRepo:  "OWNER2/REPO",
			},
			remotes: map[string]string{
				"origin": "OWNER2/REPO",
			},
			httpStubs: func(reg *httpmock.Registry, t *testing.T) {
				reg.Register(
					httpmock.GraphQL(`query LinkedBranchFeature\b`),
					httpmock.StringResponse(featureEnabledPayload),
				)
				reg.Register(
					httpmock.GraphQL(`query IssueByNumber\b`),
					httpmock.GraphQLQuery(`{"data":{"repository":{"hasIssuesEnabled":true,"issue":{"id": "SOMEID","number":123,"title":"my issue"}}}}`,
						func(_ string, inputs map[string]interface{}) {
							assert.Equal(t, "OWNER", inputs["owner"])
							assert.Equal(t, "REPO", inputs["repo"])
							assert.Equal(t, float64(123), inputs["number"])
						}),
				)
				reg.Register(
					httpmock.GraphQL(`query FindRepoBranchID\b`),
					httpmock.GraphQLQuery(`{"data":{"repository":{"id":"REPOID","defaultBranchRef":{"target":{"oid":"DEFAULTOID"}},"ref":{"target":{"oid":""}}}}}`,
						func(_ string, inputs map[string]interface{}) {
							assert.Equal(t, "OWNER2", inputs["owner"])
							assert.Equal(t, "REPO", inputs["name"])
						}),
				)
				reg.Register(
					httpmock.GraphQL(`mutation CreateLinkedBranch\b`),
					httpmock.GraphQLMutation(`{"data":{"createLinkedBranch":{"linkedBranch":{"id":"2","ref":{"name":"my-issue-1"}}}}}`,
						func(inputs map[string]interface{}) {
							assert.Equal(t, "REPOID", inputs["repositoryId"])
							assert.Equal(t, "SOMEID", inputs["issueId"])
							assert.Equal(t, "DEFAULTOID", inputs["oid"])
						}),
				)
			},
			runStubs: func(cs *run.CommandStubber) {
				cs.Register(`git fetch origin \+refs/heads/my-issue-1:refs/remotes/origin/my-issue-1`, 0, "")
			},
			expectedOut: "github.com/OWNER2/REPO/tree/my-issue-1\n",
		},
		{
			name: "develop new branch with name and base specified",
			opts: &DevelopOptions{
				Name:        "my-branch",
				BaseBranch:  "main",
				IssueNumber: 123,
			},
			remotes: map[string]string{
				"origin": "OWNER/REPO",
			},
			httpStubs: func(reg *httpmock.Registry, t *testing.T) {
				reg.Register(
					httpmock.GraphQL(`query LinkedBranchFeature\b`),
					httpmock.StringResponse(featureEnabledPayload),
				)
				reg.Register(
					httpmock.GraphQL(`query IssueByNumber\b`),
					httpmock.StringResponse(`{"data":{"repository":{ "hasIssuesEnabled":true,"issue":{"id":"SOMEID","number":123,"title":"my issue"}}}}`),
				)
				reg.Register(
					httpmock.GraphQL(`query FindRepoBranchID\b`),
					httpmock.StringResponse(`{"data":{"repository":{"id":"REPOID","ref":{"target":{"oid":"OID"}}}}}`))
				reg.Register(
					httpmock.GraphQL(`mutation CreateLinkedBranch\b`),
					httpmock.GraphQLMutation(`{"data":{"createLinkedBranch":{"linkedBranch":{"id":"2","ref":{"name":"my-branch"}}}}}`,
						func(inputs map[string]interface{}) {
							assert.Equal(t, "REPOID", inputs["repositoryId"])
							assert.Equal(t, "SOMEID", inputs["issueId"])
							assert.Equal(t, "OID", inputs["oid"])
							assert.Equal(t, "my-branch", inputs["name"])
						}),
				)
			},
			runStubs: func(cs *run.CommandStubber) {
				cs.Register(`git fetch origin \+refs/heads/my-branch:refs/remotes/origin/my-branch`, 0, "")
				cs.Register(`git config branch\.my-branch\.gh-merge-base main`, 0, "")
			},
			expectedOut: "github.com/OWNER/REPO/tree/my-branch\n",
		},
		{
			name: "develop new branch outside of local git repo",
			opts: &DevelopOptions{
				IssueNumber: 123,
				BaseRepo: func() (ghrepo.Interface, error) {
					return ghrepo.New("cli", "cli"), nil
				},
			},
			httpStubs: func(reg *httpmock.Registry, t *testing.T) {
				reg.Register(
					httpmock.GraphQL(`query LinkedBranchFeature\b`),
					httpmock.StringResponse(featureEnabledPayload),
				)
				reg.Register(
					httpmock.GraphQL(`query IssueByNumber\b`),
					httpmock.StringResponse(`{"data":{"repository":{"hasIssuesEnabled":true,"issue":{"id": "SOMEID","number":123,"title":"my issue"}}}}`),
				)
				reg.Register(
					httpmock.GraphQL(`query FindRepoBranchID\b`),
					httpmock.StringResponse(`{"data":{"repository":{"id":"REPOID","defaultBranchRef":{"target":{"oid":"DEFAULTOID"}},"ref":{"target":{"oid":""}}}}}`),
				)
				reg.Register(
					httpmock.GraphQL(`mutation CreateLinkedBranch\b`),
					httpmock.GraphQLMutation(`{"data":{"createLinkedBranch":{"linkedBranch":{"id":"2","ref":{"name":"my-issue-1"}}}}}`,
						func(inputs map[string]interface{}) {
							assert.Equal(t, "REPOID", inputs["repositoryId"])
							assert.Equal(t, "SOMEID", inputs["issueId"])
							assert.Equal(t, "DEFAULTOID", inputs["oid"])
						}),
				)
			},
			expectedOut: "github.com/cli/cli/tree/my-issue-1\n",
		},
		{
			name: "develop new branch with checkout when local branch exists",
			opts: &DevelopOptions{
				Name:        "my-branch",
				IssueNumber: 123,
				Checkout:    true,
			},
			remotes: map[string]string{
				"origin": "OWNER/REPO",
			},
			httpStubs: func(reg *httpmock.Registry, t *testing.T) {
				reg.Register(
					httpmock.GraphQL(`query LinkedBranchFeature\b`),
					httpmock.StringResponse(featureEnabledPayload),
				)
				reg.Register(
					httpmock.GraphQL(`query IssueByNumber\b`),
					httpmock.StringResponse(`{"data":{"repository":{"hasIssuesEnabled":true,"issue":{"id": "SOMEID","number":123,"title":"my issue"}}}}`),
				)
				reg.Register(
					httpmock.GraphQL(`query FindRepoBranchID\b`),
					httpmock.StringResponse(`{"data":{"repository":{"id":"REPOID","ref":{"target":{"oid":"OID"}}}}}`),
				)
				reg.Register(
					httpmock.GraphQL(`mutation CreateLinkedBranch\b`),
					httpmock.GraphQLMutation(`{"data":{"createLinkedBranch":{"linkedBranch":{"id":"2","ref":{"name":"my-branch"}}}}}`,
						func(inputs map[string]interface{}) {
							assert.Equal(t, "REPOID", inputs["repositoryId"])
							assert.Equal(t, "SOMEID", inputs["issueId"])
							assert.Equal(t, "OID", inputs["oid"])
							assert.Equal(t, "my-branch", inputs["name"])
						}),
				)
			},
			runStubs: func(cs *run.CommandStubber) {
				cs.Register(`git fetch origin \+refs/heads/my-branch:refs/remotes/origin/my-branch`, 0, "")
				cs.Register(`git rev-parse --verify refs/heads/my-branch`, 0, "")
				cs.Register(`git checkout my-branch`, 0, "")
				cs.Register(`git pull --ff-only origin my-branch`, 0, "")
			},
			expectedOut: "github.com/OWNER/REPO/tree/my-branch\n",
		},
		{
			name: "develop new branch with checkout when local branch does not exist",
			opts: &DevelopOptions{
				Name:        "my-branch",
				IssueNumber: 123,
				Checkout:    true,
			},
			remotes: map[string]string{
				"origin": "OWNER/REPO",
			},
			httpStubs: func(reg *httpmock.Registry, t *testing.T) {
				reg.Register(
					httpmock.GraphQL(`query LinkedBranchFeature\b`),
					httpmock.StringResponse(featureEnabledPayload),
				)
				reg.Register(
					httpmock.GraphQL(`query IssueByNumber\b`),
					httpmock.StringResponse(`{"data":{"repository":{"hasIssuesEnabled":true,"issue":{"id": "SOMEID","number":123,"title":"my issue"}}}}`),
				)
				reg.Register(
					httpmock.GraphQL(`query FindRepoBranchID\b`),
					httpmock.StringResponse(`{"data":{"repository":{"id":"REPOID","ref":{"target":{"oid":"OID"}}}}}`),
				)
				reg.Register(
					httpmock.GraphQL(`mutation CreateLinkedBranch\b`),
					httpmock.GraphQLMutation(`{"data":{"createLinkedBranch":{"linkedBranch":{"id":"2","ref":{"name":"my-branch"}}}}}`,
						func(inputs map[string]interface{}) {
							assert.Equal(t, "REPOID", inputs["repositoryId"])
							assert.Equal(t, "SOMEID", inputs["issueId"])
							assert.Equal(t, "OID", inputs["oid"])
							assert.Equal(t, "my-branch", inputs["name"])
						}),
				)
			},
			runStubs: func(cs *run.CommandStubber) {
				cs.Register(`git fetch origin \+refs/heads/my-branch:refs/remotes/origin/my-branch`, 0, "")
				cs.Register(`git rev-parse --verify refs/heads/my-branch`, 1, "")
				cs.Register(`git checkout -b my-branch --track origin/my-branch`, 0, "")
			},
			expectedOut: "github.com/OWNER/REPO/tree/my-branch\n",
		},
		{
			name: "develop with base branch which does not exist",
			opts: &DevelopOptions{
				IssueNumber: 123,
				BaseBranch:  "does-not-exist-branch",
			},
			remotes: map[string]string{
				"origin": "OWNER/REPO",
			},
			httpStubs: func(reg *httpmock.Registry, t *testing.T) {
				reg.Register(
					httpmock.GraphQL(`query LinkedBranchFeature\b`),
					httpmock.StringResponse(featureEnabledPayload),
				)
				reg.Register(
					httpmock.GraphQL(`query IssueByNumber\b`),
					httpmock.StringResponse(`{"data":{"repository":{"hasIssuesEnabled":true,"issue":{"id": "SOMEID","number":123,"title":"my issue"}}}}`),
				)
				reg.Register(
					httpmock.GraphQL(`query FindRepoBranchID\b`),
					httpmock.StringResponse(`{"data":{"repository":{"id":"REPOID","defaultBranchRef":{"target":{"oid":"DEFAULTOID"}},"ref":null}}}`),
				)
			},
			wantErr: "could not find branch \"does-not-exist-branch\" in OWNER/REPO",
		},
	}
	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			opts := tt.opts

			reg := &httpmock.Registry{}
			defer reg.Verify(t)
			if tt.httpStubs != nil {
				tt.httpStubs(reg, t)
			}
			opts.HttpClient = func() (*http.Client, error) {
				return &http.Client{Transport: reg}, nil
			}

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

			if opts.BaseRepo == nil {
				opts.BaseRepo = func() (ghrepo.Interface, error) {
					return ghrepo.New("OWNER", "REPO"), nil
				}
			}

			opts.Remotes = func() (context.Remotes, error) {
				if len(tt.remotes) == 0 {
					return nil, errors.New("no remotes")
				}
				var remotes context.Remotes
				for name, repo := range tt.remotes {
					r, err := ghrepo.FromFullName(repo)
					if err != nil {
						return remotes, err
					}
					remotes = append(remotes, &context.Remote{
						Remote: &git.Remote{Name: name},
						Repo:   r,
					})
				}
				return remotes, nil
			}

			opts.GitClient = &git.Client{
				GhPath:  "some/path/gh",
				GitPath: "some/path/git",
			}

			cmdStubs, cmdTeardown := run.Stub()
			defer cmdTeardown(t)
			if tt.runStubs != nil {
				tt.runStubs(cmdStubs)
			}

			err := developRun(opts)
			if tt.wantErr != "" {
				assert.EqualError(t, err, tt.wantErr)
				return
			} else {
				assert.NoError(t, err)
				assert.Equal(t, tt.expectedOut, stdout.String())
				assert.Equal(t, tt.expectedErrOut, stderr.String())
			}
		})
	}
}
