package label

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

	"github.com/MakeNowJust/heredoc"
	"github.com/cli/cli/v2/internal/ghrepo"
	"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 TestNewCmdList(t *testing.T) {
	tests := []struct {
		name    string
		input   string
		output  listOptions
		wantErr bool
		errMsg  string
	}{
		{
			name:   "no argument",
			input:  "",
			output: listOptions{Query: listQueryOptions{Limit: 30, Order: "asc", Sort: "created"}},
		},
		{
			name:   "limit flag",
			input:  "--limit 10",
			output: listOptions{Query: listQueryOptions{Limit: 10, Order: "asc", Sort: "created"}},
		},
		{
			name:    "invalid limit flag",
			input:   "--limit 0",
			wantErr: true,
			errMsg:  "invalid limit: 0",
		},
		{
			name:   "web flag",
			input:  "--web",
			output: listOptions{Query: listQueryOptions{Limit: 30, Order: "asc", Sort: "created"}, WebMode: true},
		},
		{
			name:   "search flag",
			input:  "--search core",
			output: listOptions{Query: listQueryOptions{Limit: 30, Query: "core", Order: "asc", Sort: "created"}},
		},
		{
			name:   "sort name flag",
			input:  "--sort name",
			output: listOptions{Query: listQueryOptions{Limit: 30, Order: "asc", Sort: "name"}},
		},
		{
			name:   "sort created flag",
			input:  "--sort created",
			output: listOptions{Query: listQueryOptions{Limit: 30, Order: "asc", Sort: "created"}},
		},
		{
			name:    "sort invalid flag",
			input:   "--sort invalid",
			wantErr: true,
			errMsg:  `invalid argument "invalid" for "--sort" flag: valid values are {created|name}`,
		},
		{
			name:   "order asc flag",
			input:  "--order asc",
			output: listOptions{Query: listQueryOptions{Limit: 30, Order: "asc", Sort: "created"}},
		},
		{
			name:   "order desc flag",
			input:  "--order desc",
			output: listOptions{Query: listQueryOptions{Limit: 30, Order: "desc", Sort: "created"}},
		},
		{
			name:    "order invalid flag",
			input:   "--order invalid",
			wantErr: true,
			errMsg:  `invalid argument "invalid" for "--order" flag: valid values are {asc|desc}`,
		},
		{
			name:    "search flag with sort flag",
			input:   "--search test --sort name",
			wantErr: true,
			errMsg:  "cannot specify `--order` or `--sort` with `--search`",
		},
		{
			name:    "search flag with order flag",
			input:   "--search test --order asc",
			wantErr: true,
			errMsg:  "cannot specify `--order` or `--sort` with `--search`",
		},
		{
			name:    "search flag with order and sort flags",
			input:   "--search test --order asc --sort name",
			wantErr: true,
			errMsg:  "cannot specify `--order` or `--sort` with `--search`",
		},
		{
			name:  "json flag",
			input: "--json name,color,description,createdAt",
			output: listOptions{
				Query: listQueryOptions{
					Limit: 30,
					Order: "asc",
					Sort:  "created",
					fields: []string{
						"name",
						"color",
						"description",
						"createdAt",
					},
				},
			},
		},
		{
			name:    "invalid json flag",
			input:   "--json invalid",
			wantErr: true,
			errMsg: heredoc.Doc(`
				Unknown JSON field: "invalid"
				Available fields:
				  color
				  createdAt
				  description
				  id
				  isDefault
				  name
				  updatedAt
				  url`),
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			ios, _, _, _ := iostreams.Test()
			f := &cmdutil.Factory{
				IOStreams: ios,
			}
			argv, err := shlex.Split(tt.input)
			assert.NoError(t, err)
			var gotOpts *listOptions
			cmd := newCmdList(f, func(opts *listOptions) 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.wantErr {
				assert.EqualError(t, err, tt.errMsg)
				return
			}

			assert.NoError(t, err)
			assert.Equal(t, tt.output.Query.Limit, gotOpts.Query.Limit)
			assert.Equal(t, tt.output.Query.Order, gotOpts.Query.Order)
			assert.Equal(t, tt.output.Query.Query, tt.output.Query.Query)
			assert.Equal(t, tt.output.Query.Sort, gotOpts.Query.Sort)
			assert.Equal(t, tt.output.WebMode, gotOpts.WebMode)
		})
	}
}

func TestListRun(t *testing.T) {
	tests := []struct {
		name       string
		tty        bool
		opts       *listOptions
		httpStubs  func(*httpmock.Registry)
		wantErr    bool
		wantErrMsg string
		wantStdout string
		wantStderr string
	}{
		{
			name: "lists labels",
			tty:  true,
			opts: &listOptions{},
			httpStubs: func(reg *httpmock.Registry) {
				reg.Register(
					httpmock.GraphQL(`query LabelList\b`),
					httpmock.StringResponse(`
						{
							"data": {
								"repository": {
									"labels": {
										"totalCount": 2,
										"nodes": [
											{
												"name": "bug",
												"color": "d73a4a",
												"description": "This is a bug label"
											},
											{
												"name": "docs",
												"color": "ffa8da",
												"description": "This is a docs label"
											}
										],
										"pageInfo": {
											"hasNextPage": false,
											"endCursor": "Y3Vyc29yOnYyOpK5MjAxOS0xMC0xMVQwMTozODowMyswODowMM5f3HZq"
										}
									}
								}
							}
						}`,
					),
				)
			},
			wantStdout: "\nShowing 2 of 2 labels in OWNER/REPO\n\nbug   This is a bug label   #d73a4a\ndocs  This is a docs label  #ffa8da\n",
		},
		{
			name: "lists labels notty",
			tty:  false,
			opts: &listOptions{},
			httpStubs: func(reg *httpmock.Registry) {
				reg.Register(
					httpmock.GraphQL(`query LabelList\b`),
					httpmock.StringResponse(`
						{
							"data": {
								"repository": {
									"labels": {
										"totalCount": 2,
										"nodes": [
											{
												"name": "bug",
												"color": "d73a4a",
												"description": "This is a bug label"
											},
											{
												"name": "docs",
												"color": "ffa8da",
												"description": "This is a docs label"
											}
										],
										"pageInfo": {
											"hasNextPage": false,
											"endCursor": "Y3Vyc29yOnYyOpK5MjAxOS0xMC0xMVQwMTozODowMyswODowMM5f3HZq"
										}
									}
								}
							}
						}`,
					),
				)
			},
			wantStdout: "bug\tThis is a bug label\t#d73a4a\ndocs\tThis is a docs label\t#ffa8da\n",
		},
		{
			name: "empty label list",
			tty:  true,
			opts: &listOptions{},
			httpStubs: func(reg *httpmock.Registry) {
				reg.Register(
					httpmock.GraphQL(`query LabelList\b`),
					httpmock.StringResponse(`
						{"data":{"repository":{"labels":{"totalCount":0,"nodes":[],"pageInfo":{"hasNextPage":false,"endCursor":null}}}}}`,
					),
				)
			},
			wantErr:    true,
			wantErrMsg: "no labels found in OWNER/REPO",
		},
		{
			name: "empty label list search",
			tty:  true,
			opts: &listOptions{Query: listQueryOptions{Query: "test"}},
			httpStubs: func(reg *httpmock.Registry) {
				reg.Register(
					httpmock.GraphQL(`query LabelList\b`),
					httpmock.StringResponse(`
						{"data":{"repository":{"labels":{"totalCount":0,"nodes":[],"pageInfo":{"hasNextPage":false,"endCursor":null}}}}}`,
					),
				)
			},
			wantErr:    true,
			wantErrMsg: "no labels in OWNER/REPO matched your search",
		},
		{
			name:       "web mode",
			tty:        true,
			opts:       &listOptions{WebMode: true},
			wantStderr: "Opening github.com/OWNER/REPO/labels in your browser.\n",
		},
		{
			name: "order by name ascending",
			tty:  true,
			opts: &listOptions{
				Query: listQueryOptions{
					Limit: 30,
					Order: "asc",
					Sort:  "name",
				},
			},
			httpStubs: func(reg *httpmock.Registry) {
				reg.Register(
					httpmock.GraphQL(`query LabelList\b`),
					httpmock.GraphQLQuery(`
					{
						"data": {
							"repository": {
								"labels": {
									"totalCount": 2,
									"nodes": [
										{
											"name": "bug",
											"color": "d73a4a",
											"description": "This is a bug label",
											"createdAt": "2022-04-20T06:17:50Z"
										},
										{
											"name": "docs",
											"color": "ffa8da",
											"description": "This is a docs label",
											"createdAt": "2022-04-20T06:17:49Z"
										}
									],
									"pageInfo": {
										"hasNextPage": false,
										"endCursor": "Y3Vyc29yOnYyOpK5MjAxOS0xMC0xMVQwMTozODowMyswODowMM5f3HZq"
									}
								}
							}
						}
					}`, func(s string, m map[string]interface{}) {
						assert.Equal(t, "OWNER", m["owner"])
						assert.Equal(t, "REPO", m["repo"])
						assert.Equal(t, float64(30), m["limit"].(float64))
						assert.Equal(t, map[string]interface{}{"direction": "ASC", "field": "NAME"}, m["orderBy"])
					}),
				)
			},
			wantStdout: heredoc.Doc(`

			Showing 2 of 2 labels in OWNER/REPO

			bug   This is a bug label   #d73a4a
			docs  This is a docs label  #ffa8da
			`),
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			reg := &httpmock.Registry{}
			if tt.httpStubs != nil {
				tt.httpStubs(reg)
			}
			tt.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)
			tt.opts.IO = ios
			tt.opts.Browser = &cmdutil.TestBrowser{}
			tt.opts.BaseRepo = func() (ghrepo.Interface, error) {
				return ghrepo.New("OWNER", "REPO"), nil
			}
			defer reg.Verify(t)
			err := listRun(tt.opts)
			if tt.wantErr {
				if tt.wantErrMsg != "" {
					assert.EqualError(t, err, tt.wantErrMsg)
				} else {
					assert.Error(t, err)
				}
			} else {
				assert.NoError(t, err)
			}
			assert.Equal(t, tt.wantStdout, stdout.String())
			assert.Equal(t, tt.wantStderr, stderr.String())
		})
	}
}
