package status

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

	"github.com/MakeNowJust/heredoc"
	"github.com/cli/cli/v2/internal/config"
	"github.com/cli/cli/v2/internal/gh"
	"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"
)

type testHostConfig string

func (c testHostConfig) DefaultHost() (string, string) {
	return string(c), ""
}

func TestNewCmdStatus(t *testing.T) {
	tests := []struct {
		name  string
		cli   string
		wants StatusOptions
	}{
		{
			name: "defaults",
		},
		{
			name: "org",
			cli:  "-o cli",
			wants: StatusOptions{
				Org: "cli",
			},
		},
		{
			name: "exclude",
			cli:  "-e cli/cli,cli/go-gh",
			wants: StatusOptions{
				Exclude: []string{"cli/cli", "cli/go-gh"},
			},
		},
	}

	for _, tt := range tests {
		ios, _, _, _ := iostreams.Test()
		ios.SetStdinTTY(true)
		ios.SetStdoutTTY(true)

		if tt.wants.Exclude == nil {
			tt.wants.Exclude = []string{}
		}

		f := &cmdutil.Factory{
			IOStreams: ios,
			Config: func() (gh.Config, error) {
				return config.NewBlankConfig(), nil
			},
		}
		t.Run(tt.name, func(t *testing.T) {
			argv, err := shlex.Split(tt.cli)
			assert.NoError(t, err)

			var gotOpts *StatusOptions
			cmd := NewCmdStatus(f, func(opts *StatusOptions) error {
				gotOpts = opts
				return nil
			})
			cmd.SetArgs(argv)
			cmd.SetIn(&bytes.Buffer{})
			_, err = cmd.ExecuteC()
			assert.NoError(t, err)

			assert.Equal(t, tt.wants.Org, gotOpts.Org)
			assert.Equal(t, tt.wants.Exclude, gotOpts.Exclude)
		})
	}
}

func TestStatusRun(t *testing.T) {
	tests := []struct {
		name       string
		httpStubs  func(*httpmock.Registry)
		opts       *StatusOptions
		wantOut    string
		wantErrMsg string
	}{
		{
			name: "nothing",
			httpStubs: func(reg *httpmock.Registry) {
				reg.Register(
					httpmock.GraphQL("UserCurrent"),
					httpmock.StringResponse(`{"data": {"viewer": {"login": "jillvalentine"}}}`))
				reg.Register(
					httpmock.GraphQL("AssignedSearch"),
					httpmock.StringResponse(`{"data": { "assignments": {"nodes": [] }, "reviewRequested": {"nodes": []}}}`))
				reg.Register(
					httpmock.REST("GET", "notifications"),
					httpmock.StringResponse(`[]`))
				reg.Register(
					httpmock.REST("GET", "users/jillvalentine/received_events"),
					httpmock.StringResponse(`[]`))
			},
			opts:    &StatusOptions{},
			wantOut: "Assigned Issues                       │ Assigned Pull Requests                \nNothing here ^_^                      │ Nothing here ^_^                      \n                                      │                                       \nReview Requests                       │ Mentions                              \nNothing here ^_^                      │ Nothing here ^_^                      \n                                      │                                       \nRepository Activity\nNothing here ^_^\n\n",
		},
		{
			name: "something",
			httpStubs: func(reg *httpmock.Registry) {
				reg.Register(
					httpmock.GraphQL("UserCurrent"),
					httpmock.StringResponse(`{"data": {"viewer": {"login": "jillvalentine"}}}`))
				reg.Register(
					httpmock.REST("GET", "repos/rpd/todo/issues/110"),
					httpmock.StringResponse(`{"body":"hello @jillvalentine how are you"}`))
				reg.Register(
					httpmock.REST("GET", "repos/rpd/todo/issues/4113"),
					httpmock.StringResponse(`{"body":"this is a comment"}`))
				reg.Register(
					httpmock.REST("GET", "repos/cli/cli/issues/1096"),
					httpmock.StringResponse(`{"body":"@jillvalentine hi"}`))
				reg.Register(
					httpmock.REST("GET", "repos/rpd/todo/issues/comments/1065"),
					httpmock.StringResponse(`{"body":"not a real mention"}`))
				reg.Register(
					httpmock.REST("GET", "repos/vilmibm/gh-screensaver/issues/comments/10"),
					httpmock.StringResponse(`{"body":"a message for @jillvalentine"}`))
				reg.Register(
					httpmock.GraphQL("AssignedSearch"),
					httpmock.FileResponse("./fixtures/search.json"))
				reg.Register(
					httpmock.REST("GET", "notifications"),
					httpmock.FileResponse("./fixtures/notifications.json"))
				reg.Register(
					httpmock.REST("GET", "users/jillvalentine/received_events"),
					httpmock.FileResponse("./fixtures/events.json"))
			},
			opts:    &StatusOptions{},
			wantOut: "Assigned Issues                       │ Assigned Pull Requests                \nvilmibm/testing#157     yolo          │ cli/cli#5272  Pin extensions          \ncli/cli#3223            Repo garden...│ rpd/todo#73   Board up RPD windows    \nrpd/todo#514            Reducing zo...│ cli/cli#4768  Issue Frecency          \nvilmibm/testing#74      welp          │                                       \nadreyer/arkestrator#22  complete mo...│                                       \n                                      │                                       \nReview Requests                       │ Mentions                              \ncli/cli#5272          Pin extensions  │ rpd/todo#110               hello @j...\nvilmibm/testing#1234  Foobar          │ cli/cli#1096               @jillval...\nrpd/todo#50           Welcome party...│ vilmibm/gh-screensaver#15  a messag...\ncli/cli#4671          This pull req...│                                       \nrpd/todo#49           Haircut for Leon│                                       \n                                      │                                       \nRepository Activity\nrpd/todo#5326         new PR                        Only write UTF-8 BOM on W...\nvilmibm/testing#5325  comment on Ability to sea...  We are working on dedicat...\ncli/cli#5319          comment on [Codespaces] D...  Wondering if we shouldn't...\ncli/cli#5300          new issue                     Terminal bell when a runn...\n\n",
		},
		{
			name: "exclude a repository",
			httpStubs: func(reg *httpmock.Registry) {
				reg.Register(
					httpmock.GraphQL("UserCurrent"),
					httpmock.StringResponse(`{"data": {"viewer": {"login": "jillvalentine"}}}`))
				reg.Register(
					httpmock.REST("GET", "repos/rpd/todo/issues/110"),
					httpmock.StringResponse(`{"body":"hello @jillvalentine how are you"}`))
				reg.Register(
					httpmock.REST("GET", "repos/rpd/todo/issues/4113"),
					httpmock.StringResponse(`{"body":"this is a comment"}`))
				reg.Register(
					httpmock.REST("GET", "repos/rpd/todo/issues/comments/1065"),
					httpmock.StringResponse(`{"body":"not a real mention"}`))
				reg.Register(
					httpmock.REST("GET", "repos/vilmibm/gh-screensaver/issues/comments/10"),
					httpmock.StringResponse(`{"body":"a message for @jillvalentine"}`))
				reg.Register(
					httpmock.GraphQL("AssignedSearch"),
					httpmock.FileResponse("./fixtures/search.json"))
				reg.Register(
					httpmock.REST("GET", "notifications"),
					httpmock.FileResponse("./fixtures/notifications.json"))
				reg.Register(
					httpmock.REST("GET", "users/jillvalentine/received_events"),
					httpmock.FileResponse("./fixtures/events.json"))
			},
			opts: &StatusOptions{
				Exclude: []string{"cli/cli"},
			},
			// NOTA BENE: you'll see cli/cli in search results because that happens
			// server side and the fixture doesn't account for that
			wantOut: "Assigned Issues                       │ Assigned Pull Requests                \nvilmibm/testing#157     yolo          │ cli/cli#5272  Pin extensions          \ncli/cli#3223            Repo garden...│ rpd/todo#73   Board up RPD windows    \nrpd/todo#514            Reducing zo...│ cli/cli#4768  Issue Frecency          \nvilmibm/testing#74      welp          │                                       \nadreyer/arkestrator#22  complete mo...│                                       \n                                      │                                       \nReview Requests                       │ Mentions                              \ncli/cli#5272          Pin extensions  │ rpd/todo#110               hello @j...\nvilmibm/testing#1234  Foobar          │ vilmibm/gh-screensaver#15  a messag...\nrpd/todo#50           Welcome party...│                                       \ncli/cli#4671          This pull req...│                                       \nrpd/todo#49           Haircut for Leon│                                       \n                                      │                                       \nRepository Activity\nrpd/todo#5326         new PR                        Only write UTF-8 BOM on W...\nvilmibm/testing#5325  comment on Ability to sea...  We are working on dedicat...\n\n",
		},
		{
			name: "exclude repositories",
			httpStubs: func(reg *httpmock.Registry) {
				reg.Register(
					httpmock.GraphQL("UserCurrent"),
					httpmock.StringResponse(`{"data": {"viewer": {"login": "jillvalentine"}}}`))
				reg.Register(
					httpmock.REST("GET", "repos/vilmibm/gh-screensaver/issues/comments/10"),
					httpmock.StringResponse(`{"body":"a message for @jillvalentine"}`))
				reg.Register(
					httpmock.GraphQL("AssignedSearch"),
					httpmock.FileResponse("./fixtures/search.json"))
				reg.Register(
					httpmock.REST("GET", "notifications"),
					httpmock.FileResponse("./fixtures/notifications.json"))
				reg.Register(
					httpmock.REST("GET", "users/jillvalentine/received_events"),
					httpmock.FileResponse("./fixtures/events.json"))
			},
			opts: &StatusOptions{
				Exclude: []string{"cli/cli", "rpd/todo"},
			},
			// NOTA BENE: you'll see cli/cli in search results because that happens
			// server side and the fixture doesn't account for that
			wantOut: "Assigned Issues                       │ Assigned Pull Requests                \nvilmibm/testing#157     yolo          │ cli/cli#5272  Pin extensions          \ncli/cli#3223            Repo garden...│ rpd/todo#73   Board up RPD windows    \nrpd/todo#514            Reducing zo...│ cli/cli#4768  Issue Frecency          \nvilmibm/testing#74      welp          │                                       \nadreyer/arkestrator#22  complete mo...│                                       \n                                      │                                       \nReview Requests                       │ Mentions                              \ncli/cli#5272          Pin extensions  │ vilmibm/gh-screensaver#15  a messag...\nvilmibm/testing#1234  Foobar          │                                       \nrpd/todo#50           Welcome party...│                                       \ncli/cli#4671          This pull req...│                                       \nrpd/todo#49           Haircut for Leon│                                       \n                                      │                                       \nRepository Activity\nvilmibm/testing#5325  comment on Ability to sea...  We are working on dedicat...\n\n",
		},
		{
			name: "filter to an org",
			httpStubs: func(reg *httpmock.Registry) {
				reg.Register(
					httpmock.GraphQL("UserCurrent"),
					httpmock.StringResponse(`{"data": {"viewer": {"login": "jillvalentine"}}}`))
				reg.Register(
					httpmock.REST("GET", "repos/rpd/todo/issues/110"),
					httpmock.StringResponse(`{"body":"hello @jillvalentine how are you"}`))
				reg.Register(
					httpmock.REST("GET", "repos/rpd/todo/issues/4113"),
					httpmock.StringResponse(`{"body":"this is a comment"}`))
				reg.Register(
					httpmock.REST("GET", "repos/rpd/todo/issues/comments/1065"),
					httpmock.StringResponse(`{"body":"not a real mention"}`))
				reg.Register(
					httpmock.GraphQL("AssignedSearch"),
					httpmock.FileResponse("./fixtures/search.json"))
				reg.Register(
					httpmock.REST("GET", "notifications"),
					httpmock.FileResponse("./fixtures/notifications.json"))
				reg.Register(
					httpmock.REST("GET", "users/jillvalentine/received_events"),
					httpmock.FileResponse("./fixtures/events.json"))
			},
			opts: &StatusOptions{
				Org: "rpd",
			},
			wantOut: "Assigned Issues                       │ Assigned Pull Requests                \nvilmibm/testing#157     yolo          │ cli/cli#5272  Pin extensions          \ncli/cli#3223            Repo garden...│ rpd/todo#73   Board up RPD windows    \nrpd/todo#514            Reducing zo...│ cli/cli#4768  Issue Frecency          \nvilmibm/testing#74      welp          │                                       \nadreyer/arkestrator#22  complete mo...│                                       \n                                      │                                       \nReview Requests                       │ Mentions                              \ncli/cli#5272          Pin extensions  │ rpd/todo#110  hello @jillvalentine ...\nvilmibm/testing#1234  Foobar          │                                       \nrpd/todo#50           Welcome party...│                                       \ncli/cli#4671          This pull req...│                                       \nrpd/todo#49           Haircut for Leon│                                       \n                                      │                                       \nRepository Activity\nrpd/todo#5326  new PR  Only write UTF-8 BOM on Windows where it is needed\n\n",
		},
		{
			name: "forbidden errors",
			httpStubs: func(reg *httpmock.Registry) {
				reg.Register(
					httpmock.GraphQL("UserCurrent"),
					httpmock.StringResponse(`{"data": {"viewer": {"login": "jillvalentine"}}}`))
				reg.Register(
					httpmock.REST("GET", "repos/rpd/todo/issues/110"),
					func(req *http.Request) (*http.Response, error) {
						return &http.Response{
							Request:    req,
							StatusCode: 403,
							Body:       io.NopCloser(strings.NewReader(`{"message": "You don't have permission to access this resource."}`)),
							Header: http.Header{
								"Content-Type": []string{"application/json"},
								"X-Github-Sso": []string{"required; url=http://click.me/auth"},
							},
						}, nil
					})
				reg.Register(
					httpmock.REST("GET", "repos/rpd/todo/issues/4113"),
					httpmock.StringResponse(`{"body":"this is a comment"}`))
				reg.Register(
					httpmock.REST("GET", "repos/cli/cli/issues/1096"),
					httpmock.StringResponse(`{"body":"@jillvalentine hi"}`))
				reg.Register(
					httpmock.REST("GET", "repos/rpd/todo/issues/comments/1065"),
					httpmock.StringResponse(`{"body":"not a real mention"}`))
				reg.Register(
					httpmock.REST("GET", "repos/vilmibm/gh-screensaver/issues/comments/10"),
					httpmock.StringResponse(`{"body":"a message for @jillvalentine"}`))
				reg.Register(
					httpmock.GraphQL("AssignedSearch"),
					httpmock.FileResponse("./fixtures/search_forbidden.json"))
				reg.Register(
					httpmock.REST("GET", "notifications"),
					httpmock.FileResponse("./fixtures/notifications.json"))
				reg.Register(
					httpmock.REST("GET", "users/jillvalentine/received_events"),
					httpmock.FileResponse("./fixtures/events.json"))
			},
			opts: &StatusOptions{},
			wantOut: heredoc.Doc(`
				Assigned Issues                       │ Assigned Pull Requests                
				vilmibm/testing#157  yolo             │ cli/cli#5272  Pin extensions          
				                                      │                                       
				Review Requests                       │ Mentions                              
				cli/cli#5272  Pin extensions          │ cli/cli#1096               @jillval...
				                                      │ vilmibm/gh-screensaver#15  a messag...
				                                                                              
				Repository Activity
				rpd/todo#5326         new PR                        Only write UTF-8 BOM on W...
				vilmibm/testing#5325  comment on Ability to sea...  We are working on dedicat...
				cli/cli#5319          comment on [Codespaces] D...  Wondering if we shouldn't...
				cli/cli#5300          new issue                     Terminal bell when a runn...
				
				warning: Resource protected by organization SAML enforcement. You must grant your OAuth token access to this organization.
				warning: You don't have permission to access this resource.
			`),
		},
		{
			name: "not found errors",
			httpStubs: func(reg *httpmock.Registry) {
				reg.Register(
					httpmock.GraphQL("UserCurrent"),
					httpmock.StringResponse(`{"data": {"viewer": {"login": "jillvalentine"}}}`))
				reg.Register(
					httpmock.REST("GET", "repos/rpd/todo/issues/110"),
					httpmock.StringResponse(`{"body":"hello @jillvalentine how are you"}`))
				reg.Register(
					httpmock.REST("GET", "repos/rpd/todo/issues/4113"),
					httpmock.StringResponse(`{"body":"this is a comment"}`))
				reg.Register(
					httpmock.REST("GET", "repos/cli/cli/issues/1096"),
					httpmock.StringResponse(`{"body":"@jillvalentine hi"}`))
				reg.Register(
					httpmock.REST("GET", "repos/rpd/todo/issues/comments/1065"),
					httpmock.StatusStringResponse(404, `{
						"message": "Not Found."
					}`))
				reg.Register(
					httpmock.REST("GET", "repos/vilmibm/gh-screensaver/issues/comments/10"),
					httpmock.StringResponse(`{"body":"a message for @jillvalentine"}`))
				reg.Register(
					httpmock.GraphQL("AssignedSearch"),
					httpmock.FileResponse("./fixtures/search.json"))
				reg.Register(
					httpmock.REST("GET", "notifications"),
					httpmock.FileResponse("./fixtures/notifications.json"))
				reg.Register(
					httpmock.REST("GET", "users/jillvalentine/received_events"),
					httpmock.FileResponse("./fixtures/events.json"))
			},
			opts: &StatusOptions{},
			wantOut: heredoc.Doc(`
				Assigned Issues                       │ Assigned Pull Requests                
				vilmibm/testing#157     yolo          │ cli/cli#5272  Pin extensions          
				cli/cli#3223            Repo garden...│ rpd/todo#73   Board up RPD windows    
				rpd/todo#514            Reducing zo...│ cli/cli#4768  Issue Frecency          
				vilmibm/testing#74      welp          │                                       
				adreyer/arkestrator#22  complete mo...│                                       
				                                      │                                       
				Review Requests                       │ Mentions                              
				cli/cli#5272          Pin extensions  │ rpd/todo#110               hello @j...
				vilmibm/testing#1234  Foobar          │ cli/cli#1096               @jillval...
				rpd/todo#50           Welcome party...│ vilmibm/gh-screensaver#15  a messag...
				cli/cli#4671          This pull req...│                                       
				rpd/todo#49           Haircut for Leon│                                       
				                                      │                                       
				Repository Activity
				rpd/todo#5326         new PR                        Only write UTF-8 BOM on W...
				vilmibm/testing#5325  comment on Ability to sea...  We are working on dedicat...
				cli/cli#5319          comment on [Codespaces] D...  Wondering if we shouldn't...
				cli/cli#5300          new issue                     Terminal bell when a runn...
				
			`),
		},
		{
			name: "notification errors",
			httpStubs: func(reg *httpmock.Registry) {
				reg.Register(
					httpmock.GraphQL("UserCurrent"),
					httpmock.StringResponse(`{"data": {"viewer": {"login": "jillvalentine"}}}`))
				reg.Register(
					httpmock.REST("GET", "repos/rpd/todo/issues/110"),
					httpmock.StatusStringResponse(429, `{
						"message": "Too many requests."
					}`))
				reg.Register(
					httpmock.GraphQL("AssignedSearch"),
					httpmock.StringResponse(`{"data": { "assignments": {"nodes": [] }, "reviewRequested": {"nodes": []}}}`))
				reg.Register(
					httpmock.REST("GET", "notifications"),
					httpmock.StringResponse(`[
						{
							"reason": "mention",
							"subject": {
								"title": "Good",
								"url": "https://api.github.com/repos/rpd/todo/issues/110",
								"latest_comment_url": "https://api.github.com/repos/rpd/todo/issues/110",
								"type": "Issue"
							},
							"repository": {
								"full_name": "rpd/todo",
								"owner": {
									"login": "rpd"
								}
							}
						}
					]`))
				reg.Register(
					httpmock.REST("GET", "users/jillvalentine/received_events"),
					httpmock.StringResponse(`[]`))
			},
			opts:       &StatusOptions{},
			wantErrMsg: "could not load notifications: could not fetch comment: HTTP 429 (https://api.github.com/repos/rpd/todo/issues/110)",
		},
		{
			name: "search errors",
			httpStubs: func(reg *httpmock.Registry) {
				reg.Register(
					httpmock.GraphQL("UserCurrent"),
					httpmock.StringResponse(`{"data": {"viewer": {"login": "jillvalentine"}}}`))
				reg.Register(
					httpmock.GraphQL("AssignedSearch"),
					httpmock.StringResponse(`{
						"data": {
							"assignments": {
								"nodes": [
									null
								]
							}
						},
						"errors": [
							{
								"type": "NOT FOUND",
								"path": [
									"assignments",
									"nodes",
									0
								],
								"message": "Not found."
							}
						]
					}`))
				reg.Register(
					httpmock.REST("GET", "notifications"),
					httpmock.StringResponse(`[]`))
				reg.Register(
					httpmock.REST("GET", "users/jillvalentine/received_events"),
					httpmock.StringResponse(`[]`))
			},
			opts:       &StatusOptions{},
			wantErrMsg: "failed to search: could not search for assignments: GraphQL: Not found. (assignments.nodes.0)",
		},
	}

	for _, tt := range tests {
		reg := &httpmock.Registry{}
		tt.httpStubs(reg)
		tt.opts.HttpClient = func() (*http.Client, error) {
			return &http.Client{Transport: reg}, nil
		}
		tt.opts.CachedClient = func(c *http.Client, _ time.Duration) *http.Client {
			return c
		}
		tt.opts.HostConfig = testHostConfig("github.com")
		ios, _, stdout, _ := iostreams.Test()
		ios.SetStdoutTTY(true)
		tt.opts.IO = ios

		t.Run(tt.name, func(t *testing.T) {
			err := statusRun(tt.opts)
			if tt.wantErrMsg != "" {
				assert.Equal(t, tt.wantErrMsg, err.Error())
				return
			}

			assert.NoError(t, err)

			assert.Equal(t, tt.wantOut, stdout.String())
			reg.Verify(t)
		})
	}
}
