package refresh

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

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

// TODO prompt cfg test

func Test_NewCmdRefresh(t *testing.T) {
	tests := []struct {
		name        string
		cli         string
		wants       RefreshOptions
		wantsErr    bool
		tty         bool
		neverPrompt bool
	}{
		{
			name: "tty no arguments",
			tty:  true,
			wants: RefreshOptions{
				Hostname: "",
			},
		},
		{
			name:     "nontty no arguments",
			wantsErr: true,
		},
		{
			name: "nontty hostname",
			cli:  "-h aline.cedrac",
			wants: RefreshOptions{
				Hostname: "aline.cedrac",
			},
		},
		{
			name: "tty hostname",
			tty:  true,
			cli:  "-h aline.cedrac",
			wants: RefreshOptions{
				Hostname: "aline.cedrac",
			},
		},
		{
			name:        "prompts disabled, no args",
			tty:         true,
			cli:         "",
			neverPrompt: true,
			wantsErr:    true,
		},
		{
			name:        "prompts disabled, hostname",
			tty:         true,
			cli:         "-h aline.cedrac",
			neverPrompt: true,
			wants: RefreshOptions{
				Hostname: "aline.cedrac",
			},
		},
		{
			name: "tty one scope",
			tty:  true,
			cli:  "--scopes repo:invite",
			wants: RefreshOptions{
				Scopes: []string{"repo:invite"},
			},
		},
		{
			name: "tty scopes",
			tty:  true,
			cli:  "--scopes repo:invite,read:public_key",
			wants: RefreshOptions{
				Scopes: []string{"repo:invite", "read:public_key"},
			},
		},
		{
			name: "secure storage",
			tty:  true,
			cli:  "--secure-storage",
			wants: RefreshOptions{
				SecureStorage: true,
			},
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			ios, _, _, _ := iostreams.Test()
			f := &cmdutil.Factory{
				IOStreams: ios,
			}
			ios.SetStdinTTY(tt.tty)
			ios.SetStdoutTTY(tt.tty)
			ios.SetNeverPrompt(tt.neverPrompt)

			argv, err := shlex.Split(tt.cli)
			assert.NoError(t, err)

			var gotOpts *RefreshOptions
			cmd := NewCmdRefresh(f, func(opts *RefreshOptions) error {
				gotOpts = opts
				return nil
			})
			// TODO cobra hack-around
			cmd.Flags().BoolP("help", "x", false, "")

			cmd.SetArgs(argv)
			cmd.SetIn(&bytes.Buffer{})
			cmd.SetOut(&bytes.Buffer{})
			cmd.SetErr(&bytes.Buffer{})

			_, err = cmd.ExecuteC()
			if tt.wantsErr {
				assert.Error(t, err)
				return
			}
			assert.NoError(t, err)
			assert.Equal(t, tt.wants.Hostname, gotOpts.Hostname)
			assert.Equal(t, tt.wants.Scopes, gotOpts.Scopes)
		})
	}
}

type authArgs struct {
	hostname      string
	scopes        []string
	interactive   bool
	secureStorage bool
}

func Test_refreshRun(t *testing.T) {
	tests := []struct {
		name          string
		opts          *RefreshOptions
		prompterStubs func(*prompter.PrompterMock)
		cfgHosts      []string
		config        config.Config
		oldScopes     string
		wantErr       string
		nontty        bool
		wantAuthArgs  authArgs
	}{
		{
			name:    "no hosts configured",
			opts:    &RefreshOptions{},
			wantErr: `not logged in to any hosts`,
		},
		{
			name: "hostname given but dne",
			cfgHosts: []string{
				"github.com",
				"aline.cedrac",
			},
			opts: &RefreshOptions{
				Hostname: "obed.morton",
			},
			wantErr: `not logged in to obed.morton`,
		},
		{
			name: "hostname provided and is configured",
			cfgHosts: []string{
				"obed.morton",
				"github.com",
			},
			opts: &RefreshOptions{
				Hostname: "obed.morton",
			},
			wantAuthArgs: authArgs{
				hostname: "obed.morton",
				scopes:   nil,
			},
		},
		{
			name: "no hostname, one host configured",
			cfgHosts: []string{
				"github.com",
			},
			opts: &RefreshOptions{
				Hostname: "",
			},
			wantAuthArgs: authArgs{
				hostname: "github.com",
				scopes:   nil,
			},
		},
		{
			name: "no hostname, multiple hosts configured",
			cfgHosts: []string{
				"github.com",
				"aline.cedrac",
			},
			opts: &RefreshOptions{
				Hostname: "",
			},
			prompterStubs: func(pm *prompter.PrompterMock) {
				pm.SelectFunc = func(_, _ string, opts []string) (int, error) {
					return prompter.IndexFor(opts, "github.com")
				}
			},
			wantAuthArgs: authArgs{
				hostname: "github.com",
				scopes:   nil,
			},
		},
		{
			name: "scopes provided",
			cfgHosts: []string{
				"github.com",
			},
			opts: &RefreshOptions{
				Scopes: []string{"repo:invite", "public_key:read"},
			},
			wantAuthArgs: authArgs{
				hostname: "github.com",
				scopes:   []string{"repo:invite", "public_key:read"},
			},
		},
		{
			name: "scopes provided",
			cfgHosts: []string{
				"github.com",
			},
			oldScopes: "delete_repo, codespace",
			opts: &RefreshOptions{
				Scopes: []string{"repo:invite", "public_key:read"},
			},
			wantAuthArgs: authArgs{
				hostname: "github.com",
				scopes:   []string{"repo:invite", "public_key:read", "delete_repo", "codespace"},
			},
		},
		{
			name: "explicit secure storage",
			cfgHosts: []string{
				"obed.morton",
			},
			opts: &RefreshOptions{
				Hostname:      "obed.morton",
				SecureStorage: true,
			},
			wantAuthArgs: authArgs{
				hostname:      "obed.morton",
				scopes:        nil,
				secureStorage: true,
			},
		},
		{
			name: "implicit secure storage",
			config: func() config.Config {
				cfg := config.NewFromString("")
				authCfg := cfg.Authentication()
				authCfg.SetHosts([]string{"obed.morton"})
				authCfg.SetToken("abc123", "keyring")
				cfg.AuthenticationFunc = func() *config.AuthConfig {
					return authCfg
				}
				return cfg
			}(),
			opts: &RefreshOptions{
				Hostname: "obed.morton",
			},
			wantAuthArgs: authArgs{
				hostname:      "obed.morton",
				scopes:        nil,
				secureStorage: true,
			},
		},
	}
	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			aa := authArgs{}
			tt.opts.AuthFlow = func(_ *config.AuthConfig, _ *iostreams.IOStreams, hostname string, scopes []string, interactive, secureStorage bool) error {
				aa.hostname = hostname
				aa.scopes = scopes
				aa.interactive = interactive
				aa.secureStorage = secureStorage
				return nil
			}

			var cfg config.Config
			if tt.config != nil {
				cfg = tt.config
			} else {
				cfg = config.NewFromString("")
				for _, hostname := range tt.cfgHosts {
					cfg.Set(hostname, "oauth_token", "abc123")
				}
			}
			tt.opts.Config = func() (config.Config, error) {
				return cfg, nil
			}

			ios, _, _, _ := iostreams.Test()
			ios.SetStdinTTY(!tt.nontty)
			ios.SetStdoutTTY(!tt.nontty)
			tt.opts.IO = ios

			httpReg := &httpmock.Registry{}
			httpReg.Register(
				httpmock.REST("GET", ""),
				func(req *http.Request) (*http.Response, error) {
					statusCode := 200
					if req.Header.Get("Authorization") != "token abc123" {
						statusCode = 400
					}
					return &http.Response{
						Request:    req,
						StatusCode: statusCode,
						Body:       io.NopCloser(strings.NewReader(``)),
						Header: http.Header{
							"X-Oauth-Scopes": {tt.oldScopes},
						},
					}, nil
				},
			)
			tt.opts.HttpClient = &http.Client{Transport: httpReg}

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

			err := refreshRun(tt.opts)
			if tt.wantErr != "" {
				if assert.Error(t, err) {
					assert.Contains(t, err.Error(), tt.wantErr)
				}
			} else {
				assert.NoError(t, err)
			}

			assert.Equal(t, tt.wantAuthArgs.hostname, aa.hostname)
			assert.Equal(t, tt.wantAuthArgs.scopes, aa.scopes)
			assert.Equal(t, tt.wantAuthArgs.interactive, aa.interactive)
			assert.Equal(t, tt.wantAuthArgs.secureStorage, aa.secureStorage)
		})
	}
}
