package code

import (
	"bytes"
	"fmt"
	"testing"

	"github.com/cli/cli/v2/internal/browser"
	"github.com/cli/cli/v2/internal/config"
	"github.com/cli/cli/v2/internal/gh"
	ghmock "github.com/cli/cli/v2/internal/gh/mock"
	"github.com/cli/cli/v2/pkg/cmdutil"
	"github.com/cli/cli/v2/pkg/iostreams"
	"github.com/cli/cli/v2/pkg/search"
	"github.com/google/shlex"
	"github.com/stretchr/testify/assert"
)

func TestNewCmdCode(t *testing.T) {
	tests := []struct {
		name    string
		input   string
		output  CodeOptions
		wantErr bool
		errMsg  string
	}{
		{
			name:    "no arguments",
			input:   "",
			wantErr: true,
			errMsg:  "specify search keywords or flags",
		},
		{
			name:  "keyword arguments",
			input: "react lifecycle",
			output: CodeOptions{
				Query: search.Query{
					Keywords: []string{"react", "lifecycle"},
					Kind:     "code",
					Limit:    30,
				},
			},
		},
		{
			name:  "web flag",
			input: "--web",
			output: CodeOptions{
				Query: search.Query{
					Keywords: []string{},
					Kind:     "code",
					Limit:    30,
				},
				WebMode: true,
			},
		},
		{
			name:  "limit flag",
			input: "--limit 10",
			output: CodeOptions{
				Query: search.Query{
					Keywords: []string{},
					Kind:     "code",
					Limit:    10,
				},
			},
		},
		{
			name:    "invalid limit flag",
			input:   "--limit 1001",
			wantErr: true,
			errMsg:  "`--limit` must be between 1 and 1000",
		},
		{
			name: "qualifier flags",
			input: `
			--extension=extension
			--filename=filename
			--match=path,file
			--language=language
			--repo=owner/repo
			--size=5
			--owner=owner
			`,
			output: CodeOptions{
				Query: search.Query{
					Keywords: []string{},
					Kind:     "code",
					Limit:    30,
					Qualifiers: search.Qualifiers{
						Extension: "extension",
						Filename:  "filename",
						In:        []string{"path", "file"},
						Language:  "language",
						Repo:      []string{"owner/repo"},
						Size:      "5",
						User:      []string{"owner"},
					},
				},
			},
		},
	}

	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 *CodeOptions
			cmd := NewCmdCode(f, func(opts *CodeOptions) 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, gotOpts.Query)
			assert.Equal(t, tt.output.WebMode, gotOpts.WebMode)
		})
	}
}

func TestCodeRun(t *testing.T) {
	var query = search.Query{
		Keywords: []string{"map"},
		Kind:     "code",
		Limit:    30,
		Qualifiers: search.Qualifiers{
			Language: "go",
			Repo:     []string{"cli/cli"},
		},
	}
	tests := []struct {
		errMsg     string
		name       string
		opts       *CodeOptions
		tty        bool
		wantErr    bool
		wantStderr string
		wantStdout string
		wantBrowse string
	}{
		{
			name: "displays results tty",
			opts: &CodeOptions{
				Query: query,
				Searcher: &search.SearcherMock{
					CodeFunc: func(query search.Query) (search.CodeResult, error) {
						return search.CodeResult{
							IncompleteResults: false,
							Items: []search.Code{
								{
									Name: "context.go",
									Path: "context/context.go",
									Repository: search.Repository{
										FullName: "cli/cli",
									},
									TextMatches: []search.TextMatch{
										{
											Fragment: "\t}\n\n\tvar repos []*api.Repository\n\trepoMap := map[string]bool{}\n\n\tadd := func(r *api.Repository) {\n\t\tfn := ghrepo.FullName(r)",
											Matches: []search.Match{
												{
													Text:    "Map",
													Indices: []int{38, 41},
												},
												{
													Text:    "map",
													Indices: []int{45, 48},
												},
											},
										},
									},
								},
								{
									Name: "pr.go",
									Path: "pkg/cmd/pr/pr.go",
									Repository: search.Repository{
										FullName: "cli/cli",
									},
									TextMatches: []search.TextMatch{
										{

											Fragment: "\t\t\t$ gh pr create --fill\n\t\t\t$ gh pr view --web\n\t\t`),\n\t\tAnnotations: map[string]string{\n\t\t\t\"help:arguments\": heredoc.Doc(`\n\t\t\t\tA pull request can be supplied as argument in any of the following formats:\n\t\t\t\t- by number, e.g. \"123\";",
											Matches: []search.Match{
												{
													Text:    "map",
													Indices: []int{68, 71},
												},
											},
										},
									},
								},
							},
							Total: 2,
						}, nil
					},
				},
			},
			tty:        true,
			wantStdout: "\nShowing 2 of 2 results\n\ncli/cli context/context.go\n\trepoMap := map[string]bool{}\n\ncli/cli pkg/cmd/pr/pr.go\n\tAnnotations: map[string]string{\n",
		},
		{
			name: "displays results notty",
			opts: &CodeOptions{
				Query: query,
				Searcher: &search.SearcherMock{
					CodeFunc: func(query search.Query) (search.CodeResult, error) {
						return search.CodeResult{
							IncompleteResults: false,
							Items: []search.Code{
								{
									Name: "context.go",
									Path: "context/context.go",
									Repository: search.Repository{
										FullName: "cli/cli",
									},
									TextMatches: []search.TextMatch{
										{
											Fragment: "\t}\n\n\tvar repos []*api.Repository\n\trepoMap := map[string]bool{}\n\n\tadd := func(r *api.Repository) {\n\t\tfn := ghrepo.FullName(r)",
											Matches: []search.Match{
												{
													Text:    "Map",
													Indices: []int{38, 41},
												},
												{
													Text:    "map",
													Indices: []int{45, 48},
												},
											},
										},
									},
								},
								{
									Name: "pr.go",
									Path: "pkg/cmd/pr/pr.go",
									Repository: search.Repository{
										FullName: "cli/cli",
									},
									TextMatches: []search.TextMatch{
										{

											Fragment: "\t\t\t$ gh pr create --fill\n\t\t\t$ gh pr view --web\n\t\t`),\n\t\tAnnotations: map[string]string{\n\t\t\t\"help:arguments\": heredoc.Doc(`\n\t\t\t\tA pull request can be supplied as argument in any of the following formats:\n\t\t\t\t- by number, e.g. \"123\";",
											Matches: []search.Match{
												{
													Text:    "map",
													Indices: []int{68, 71},
												},
											},
										},
									},
								},
							},
							Total: 2,
						}, nil
					},
				},
			},
			tty:        false,
			wantStdout: "cli/cli:context/context.go: repoMap := map[string]bool{}\ncli/cli:pkg/cmd/pr/pr.go: Annotations: map[string]string{\n",
		},
		{
			name: "displays no results",
			opts: &CodeOptions{
				Query: query,
				Searcher: &search.SearcherMock{
					CodeFunc: func(query search.Query) (search.CodeResult, error) {
						return search.CodeResult{}, nil
					},
				},
			},
			wantErr: true,
			errMsg:  "no code results matched your search",
		},
		{
			name: "displays search error",
			opts: &CodeOptions{
				Query: query,
				Searcher: &search.SearcherMock{
					CodeFunc: func(query search.Query) (search.CodeResult, error) {
						return search.CodeResult{}, fmt.Errorf("error with query")
					},
				},
			},
			wantErr: true,
			errMsg:  "error with query",
		},
		{
			name: "opens browser for web mode tty",
			opts: &CodeOptions{
				Query: query,
				Searcher: &search.SearcherMock{
					URLFunc: func(query search.Query) string {
						return "https://github.com/search?type=code&q=map+repo%3Acli%2Fcli"
					},
				},
				WebMode: true,
			},
			tty:        true,
			wantStderr: "Opening https://github.com/search in your browser.\n",
			wantBrowse: "https://github.com/search?type=code&q=map+repo%3Acli%2Fcli",
		},
		{
			name: "opens browser for web mode notty",
			opts: &CodeOptions{
				Query: query,
				Searcher: &search.SearcherMock{
					URLFunc: func(query search.Query) string {
						return "https://github.com/search?type=code&q=map+repo%3Acli%2Fcli"
					},
				},
				WebMode: true,
			},
			wantBrowse: "https://github.com/search?type=code&q=map+repo%3Acli%2Fcli",
		},
		{
			name: "converts filename and extension qualifiers for github.com web search",
			opts: &CodeOptions{
				Config: func() (gh.Config, error) { return config.NewBlankConfig(), nil },
				Query: search.Query{
					Keywords: []string{"map"},
					Kind:     "code",
					Limit:    30,
					Qualifiers: search.Qualifiers{
						Filename:  "testing",
						Extension: "go",
					},
				},
				Searcher: search.NewSearcher(nil, "github.com"),
				WebMode:  true,
			},
			wantBrowse: "https://github.com/search?q=map+path%3Atesting.go&type=code",
		},
		{
			name: "properly handles extension with dot prefix when converting to path qualifier",
			opts: &CodeOptions{
				Config: func() (gh.Config, error) { return config.NewBlankConfig(), nil },
				Query: search.Query{
					Keywords: []string{"map"},
					Kind:     "code",
					Limit:    30,
					Qualifiers: search.Qualifiers{
						Filename:  "testing",
						Extension: ".cpp",
					},
				},
				Searcher: search.NewSearcher(nil, "github.com"),
				WebMode:  true,
			},
			wantBrowse: "https://github.com/search?q=map+path%3Atesting.cpp&type=code",
		},
		{
			name: "does not convert filename and extension qualifiers for GHES web search",
			opts: &CodeOptions{
				Config: func() (gh.Config, error) {
					cfg := &ghmock.ConfigMock{
						AuthenticationFunc: func() gh.AuthConfig {
							authCfg := &config.AuthConfig{}
							authCfg.SetDefaultHost("example.com", "GH_HOST")
							return authCfg
						},
					}
					return cfg, nil
				},
				Query: search.Query{
					Keywords: []string{"map"},
					Kind:     "code",
					Limit:    30,
					Qualifiers: search.Qualifiers{
						Filename:  "testing",
						Extension: "go",
					},
				},
				Searcher: search.NewSearcher(nil, "example.com"),
				WebMode:  true,
			},
			wantBrowse: "https://example.com/search?q=map+extension%3Ago+filename%3Atesting&type=code",
		},
	}

	for _, tt := range tests {
		ios, _, stdout, stderr := iostreams.Test()
		ios.SetStdinTTY(tt.tty)
		ios.SetStdoutTTY(tt.tty)
		ios.SetStderrTTY(tt.tty)
		tt.opts.IO = ios
		browser := &browser.Stub{}
		tt.opts.Browser = browser
		t.Run(tt.name, func(t *testing.T) {
			err := codeRun(tt.opts)
			if tt.wantErr {
				assert.EqualError(t, err, tt.errMsg)
				return
			} else if err != nil {
				t.Fatalf("codeRun unexpected error: %v", err)
			}
			assert.Equal(t, tt.wantStdout, stdout.String())
			assert.Equal(t, tt.wantStderr, stderr.String())
			browser.Verify(t, tt.wantBrowse)
		})
	}
}
