package view

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

	"github.com/MakeNowJust/heredoc"
	"github.com/cli/cli/v2/internal/browser"
	"github.com/cli/cli/v2/internal/ghrepo"
	"github.com/cli/cli/v2/internal/prompter"
	"github.com/cli/cli/v2/pkg/cmd/ruleset/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 Test_NewCmdView(t *testing.T) {
	tests := []struct {
		name    string
		args    string
		isTTY   bool
		want    ViewOptions
		wantErr string
	}{
		{
			name:  "no arguments",
			args:  "",
			isTTY: true,
			want: ViewOptions{
				ID:              "",
				WebMode:         false,
				IncludeParents:  true,
				InteractiveMode: true,
				Organization:    "",
			},
		},
		{
			name:  "only ID",
			args:  "3",
			isTTY: true,
			want: ViewOptions{
				ID:              "3",
				WebMode:         false,
				IncludeParents:  true,
				InteractiveMode: false,
				Organization:    "",
			},
		},
		{
			name:  "org",
			args:  "--org \"my-org\"",
			isTTY: true,
			want: ViewOptions{
				ID:              "",
				WebMode:         false,
				IncludeParents:  true,
				InteractiveMode: true,
				Organization:    "my-org",
			},
		},
		{
			name:  "web mode",
			args:  "--web",
			isTTY: true,
			want: ViewOptions{
				ID:              "",
				WebMode:         true,
				IncludeParents:  true,
				InteractiveMode: true,
				Organization:    "",
			},
		},
		{
			name:  "parents",
			args:  "--parents=false",
			isTTY: true,
			want: ViewOptions{
				ID:              "",
				WebMode:         false,
				IncludeParents:  false,
				InteractiveMode: true,
				Organization:    "",
			},
		},
		{
			name:    "repo and org specified",
			args:    "--org \"my-org\" -R \"owner/repo\"",
			isTTY:   true,
			wantErr: "only one of --repo and --org may be specified",
		},
		{
			name:    "invalid ID",
			args:    "1.5",
			isTTY:   true,
			wantErr: "invalid value for ruleset ID: 1.5 is not an integer",
		},
		{
			name:    "ID not provided and not TTY",
			args:    "",
			isTTY:   false,
			wantErr: "a ruleset ID must be provided when not running interactively",
		},
	}

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

			f := &cmdutil.Factory{
				IOStreams: ios,
			}

			var opts *ViewOptions
			cmd := NewCmdView(f, func(o *ViewOptions) error {
				opts = o
				return nil
			})
			cmd.PersistentFlags().StringP("repo", "R", "", "")

			argv, err := shlex.Split(tt.args)
			require.NoError(t, err)
			cmd.SetArgs(argv)

			cmd.SetIn(&bytes.Buffer{})
			cmd.SetOut(io.Discard)
			cmd.SetErr(io.Discard)

			_, err = cmd.ExecuteC()
			if tt.wantErr != "" {
				require.EqualError(t, err, tt.wantErr)
				return
			} else {
				require.NoError(t, err)
			}

			assert.Equal(t, tt.want.ID, opts.ID)
			assert.Equal(t, tt.want.WebMode, opts.WebMode)
			assert.Equal(t, tt.want.IncludeParents, opts.IncludeParents)
			assert.Equal(t, tt.want.InteractiveMode, opts.InteractiveMode)
			assert.Equal(t, tt.want.Organization, opts.Organization)
		})
	}
}

func Test_viewRun(t *testing.T) {
	repoRulesetStdout := heredoc.Doc(`

	Test Ruleset
	ID: 42
	Source: my-owner/repo-name (Repository)
	Enforcement: Active
	You can bypass: pull requests only

	Bypass List
	- OrganizationAdmin (ID: 1), mode: always
	- RepositoryRole (ID: 5), mode: always

	Conditions
	- ref_name: [exclude: []] [include: [~ALL]] 

	Rules
	- commit_author_email_pattern: [name: ] [negate: false] [operator: ends_with] [pattern: @example.com] 
	- commit_message_pattern: [name: ] [negate: false] [operator: contains] [pattern: asdf] 
	- creation
	`)

	tests := []struct {
		name          string
		isTTY         bool
		opts          ViewOptions
		httpStubs     func(*httpmock.Registry)
		prompterStubs func(*prompter.MockPrompter)
		wantErr       string
		wantStdout    string
		wantStderr    string
		wantBrowse    string
	}{
		{
			name:  "view repo ruleset",
			isTTY: true,
			opts: ViewOptions{
				ID: "42",
			},
			wantStdout: repoRulesetStdout,
			httpStubs: func(reg *httpmock.Registry) {
				reg.Register(
					httpmock.REST("GET", "repos/my-owner/repo-name/rulesets/42"),
					httpmock.FileResponse("./fixtures/rulesetViewRepo.json"),
				)
			},
			wantStderr: "",
			wantBrowse: "",
		},
		{
			name:  "view org ruleset",
			isTTY: true,
			opts: ViewOptions{
				ID:           "74",
				Organization: "my-owner",
			},
			wantStdout: heredoc.Doc(`

			My Org Ruleset
			ID: 74
			Source: my-owner (Organization)
			Enforcement: Evaluate Mode (not enforced)

			Bypass List
			This ruleset cannot be bypassed

			Conditions
			- ref_name: [exclude: []] [include: [~ALL]] 
			- repository_name: [exclude: []] [include: [~ALL]] [protected: true] 

			Rules
			- commit_author_email_pattern: [name: ] [negate: false] [operator: ends_with] [pattern: @example.com] 
			- commit_message_pattern: [name: ] [negate: false] [operator: contains] [pattern: asdf] 
			- creation
			`),
			httpStubs: func(reg *httpmock.Registry) {
				reg.Register(
					httpmock.REST("GET", "orgs/my-owner/rulesets/74"),
					httpmock.FileResponse("./fixtures/rulesetViewOrg.json"),
				)
			},
			wantStderr: "",
			wantBrowse: "",
		},
		{
			name:  "interactive mode, repo, no rulesets found",
			isTTY: true,
			opts: ViewOptions{
				InteractiveMode: true,
			},
			wantStdout: "",
			httpStubs: func(reg *httpmock.Registry) {
				reg.Register(
					httpmock.GraphQL(`query RepoRulesetList\b`),
					httpmock.JSONResponse(shared.RulesetList{
						TotalCount: 0,
						Rulesets:   []shared.RulesetGraphQL{},
					}),
				)
			},
			wantErr:    "no rulesets found in my-owner/repo-name",
			wantStderr: "",
			wantBrowse: "",
		},
		{
			name:  "interactive mode, org, no rulesets found",
			isTTY: true,
			opts: ViewOptions{
				InteractiveMode: true,
				Organization:    "my-owner",
			},
			wantStdout: "",
			httpStubs: func(reg *httpmock.Registry) {
				reg.Register(
					httpmock.GraphQL(`query OrgRulesetList\b`),
					httpmock.JSONResponse(shared.RulesetList{
						TotalCount: 0,
						Rulesets:   []shared.RulesetGraphQL{},
					}),
				)
			},
			wantErr:    "no rulesets found in my-owner",
			wantStderr: "",
			wantBrowse: "",
		},
		{
			name:  "interactive mode, prompter",
			isTTY: true,
			opts: ViewOptions{
				InteractiveMode: true,
			},
			wantStdout: repoRulesetStdout,
			httpStubs: func(reg *httpmock.Registry) {
				reg.Register(
					httpmock.GraphQL(`query RepoRulesetList\b`),
					httpmock.FileResponse("./fixtures/rulesetViewMultiple.json"),
				)
				reg.Register(
					httpmock.REST("GET", "repos/my-owner/repo-name/rulesets/42"),
					httpmock.FileResponse("./fixtures/rulesetViewRepo.json"),
				)
			},
			prompterStubs: func(pm *prompter.MockPrompter) {
				const repoRuleset = "42: Test Ruleset | active | contains 3 rules | configured in my-owner/repo-name (repo)"
				pm.RegisterSelect("Which ruleset would you like to view?",
					[]string{
						"74: My Org Ruleset | evaluate | contains 3 rules | configured in my-owner (org)",
						repoRuleset,
					},
					func(_, _ string, opts []string) (int, error) {
						return prompter.IndexFor(opts, repoRuleset)
					})
			},
		},
		{
			name:  "web mode, TTY, repo",
			isTTY: true,
			opts: ViewOptions{
				ID:      "42",
				WebMode: true,
			},
			httpStubs: func(reg *httpmock.Registry) {
				reg.Register(
					httpmock.REST("GET", "repos/my-owner/repo-name/rulesets/42"),
					httpmock.FileResponse("./fixtures/rulesetViewRepo.json"),
				)
			},
			wantStdout: "Opening https://github.com/my-owner/repo-name/rules/42 in your browser.\n",
			wantStderr: "",
			wantBrowse: "https://github.com/my-owner/repo-name/rules/42",
		},
		{
			name:  "web mode, non-TTY, repo",
			isTTY: false,
			opts: ViewOptions{
				ID:      "42",
				WebMode: true,
			},
			httpStubs: func(reg *httpmock.Registry) {
				reg.Register(
					httpmock.REST("GET", "repos/my-owner/repo-name/rulesets/42"),
					httpmock.FileResponse("./fixtures/rulesetViewRepo.json"),
				)
			},
			wantStdout: "",
			wantStderr: "",
			wantBrowse: "https://github.com/my-owner/repo-name/rules/42",
		},
		{
			name:  "web mode, TTY, org",
			isTTY: true,
			opts: ViewOptions{
				ID:           "74",
				Organization: "my-owner",
				WebMode:      true,
			},
			httpStubs: func(reg *httpmock.Registry) {
				reg.Register(
					httpmock.REST("GET", "orgs/my-owner/rulesets/74"),
					httpmock.FileResponse("./fixtures/rulesetViewOrg.json"),
				)
			},
			wantStdout: "Opening https://github.com/organizations/my-owner/settings/rules/74 in your browser.\n",
			wantStderr: "",
			wantBrowse: "https://github.com/organizations/my-owner/settings/rules/74",
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			ios, _, stdout, stderr := iostreams.Test()
			ios.SetStdoutTTY(tt.isTTY)
			ios.SetStdinTTY(tt.isTTY)
			ios.SetStderrTTY(tt.isTTY)

			pm := prompter.NewMockPrompter(t)
			if tt.prompterStubs != nil {
				tt.prompterStubs(pm)
			}
			tt.opts.Prompter = pm

			reg := &httpmock.Registry{}
			defer reg.Verify(t)
			if tt.httpStubs != nil {
				tt.httpStubs(reg)
			}

			tt.opts.IO = ios
			tt.opts.HttpClient = func() (*http.Client, error) {
				return &http.Client{Transport: reg}, nil
			}

			// only set this if org is not set, because the repo isn't needed if --org is provided and
			// leaving it undefined will catch potential errors
			if tt.opts.Organization == "" {
				tt.opts.BaseRepo = func() (ghrepo.Interface, error) {
					return ghrepo.FromFullName("my-owner/repo-name")
				}
			}

			browser := &browser.Stub{}
			tt.opts.Browser = browser

			err := viewRun(&tt.opts)

			if tt.wantErr != "" {
				require.EqualError(t, err, tt.wantErr)
				return
			} else {
				require.NoError(t, err)
			}

			if tt.wantBrowse != "" {
				browser.Verify(t, tt.wantBrowse)
			}

			assert.Equal(t, tt.wantStdout, stdout.String())
			assert.Equal(t, tt.wantStderr, stderr.String())
		})
	}
}
