package imports

import (
	"bytes"
	"fmt"
	"io"
	"os"
	"path/filepath"
	"strings"
	"testing"

	"github.com/MakeNowJust/heredoc"
	"github.com/cli/cli/v2/internal/config"
	"github.com/cli/cli/v2/pkg/cmd/alias/shared"
	"github.com/cli/cli/v2/pkg/cmdutil"
	"github.com/cli/cli/v2/pkg/iostreams"
	"github.com/google/shlex"
	"github.com/spf13/cobra"
	"github.com/stretchr/testify/assert"
	"github.com/stretchr/testify/require"
)

func TestNewCmdImport(t *testing.T) {
	tests := []struct {
		name       string
		cli        string
		tty        bool
		wants      ImportOptions
		wantsError string
	}{
		{
			name: "no filename and stdin tty",
			cli:  "",
			tty:  true,
			wants: ImportOptions{
				Filename:          "",
				OverwriteExisting: false,
			},
			wantsError: "no filename passed and nothing on STDIN",
		},
		{
			name: "no filename and stdin is not tty",
			cli:  "",
			tty:  false,
			wants: ImportOptions{
				Filename:          "-",
				OverwriteExisting: false,
			},
		},
		{
			name: "stdin arg",
			cli:  "-",
			wants: ImportOptions{
				Filename:          "-",
				OverwriteExisting: false,
			},
		},
		{
			name: "multiple filenames",
			cli:  "aliases1 aliases2",
			wants: ImportOptions{
				Filename:          "aliases1 aliases2",
				OverwriteExisting: false,
			},
			wantsError: "too many arguments",
		},
		{
			name: "clobber flag",
			cli:  "aliases --clobber",
			wants: ImportOptions{
				Filename:          "aliases",
				OverwriteExisting: true,
			},
		},
	}

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

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

			var gotOpts *ImportOptions
			cmd := NewCmdImport(f, func(opts *ImportOptions) error {
				gotOpts = opts
				return nil
			})
			cmd.SetArgs(argv)
			cmd.SetIn(&bytes.Buffer{})
			cmd.SetOut(io.Discard)
			cmd.SetErr(io.Discard)

			_, err = cmd.ExecuteC()
			if tt.wantsError != "" {
				assert.EqualError(t, err, tt.wantsError)
				return
			}
			assert.NoError(t, err)

			assert.Equal(t, tt.wants.Filename, gotOpts.Filename)
			assert.Equal(t, tt.wants.OverwriteExisting, gotOpts.OverwriteExisting)
		})
	}
}

func TestImportRun(t *testing.T) {
	tmpFile := filepath.Join(t.TempDir(), "aliases.yml")
	importFileMsg := fmt.Sprintf(`- Importing aliases from file %q`, tmpFile)
	importStdinMsg := "- Importing aliases from standard input"

	tests := []struct {
		name          string
		opts          *ImportOptions
		stdin         string
		fileContents  string
		initConfig    string
		aliasCommands []*cobra.Command
		wantConfig    string
		wantStderr    string
	}{
		{
			name: "with no existing aliases",
			opts: &ImportOptions{
				Filename:          tmpFile,
				OverwriteExisting: false,
			},
			fileContents: heredoc.Doc(`
                co: pr checkout
                igrep: '!gh issue list --label="$1" | grep "$2"'
            `),
			wantConfig: heredoc.Doc(`
                aliases:
                    co: pr checkout
                    igrep: '!gh issue list --label="$1" | grep "$2"'
            `),
			wantStderr: importFileMsg + "\n✓ Added alias co\n✓ Added alias igrep\n\n",
		},
		{
			name: "with existing aliases",
			opts: &ImportOptions{
				Filename:          tmpFile,
				OverwriteExisting: false,
			},
			fileContents: heredoc.Doc(`
                users: |-
                    api graphql -F name="$1" -f query='
                        query ($name: String!) {
                            user(login: $name) {
                                name
                            }
                        }'
                co: pr checkout
            `),
			initConfig: heredoc.Doc(`
                aliases:
                    igrep: '!gh issue list --label="$1" | grep "$2"'
                editor: vim
            `),
			aliasCommands: []*cobra.Command{
				{Use: "igrep"},
			},
			wantConfig: heredoc.Doc(`
                aliases:
                    igrep: '!gh issue list --label="$1" | grep "$2"'
                    co: pr checkout
                    users: |-
                        api graphql -F name="$1" -f query='
                            query ($name: String!) {
                                user(login: $name) {
                                    name
                                }
                            }'
                editor: vim
            `),
			wantStderr: importFileMsg + "\n✓ Added alias co\n✓ Added alias users\n\n",
		},
		{
			name: "from stdin",
			opts: &ImportOptions{
				Filename:          "-",
				OverwriteExisting: false,
			},
			stdin: heredoc.Doc(`
                co: pr checkout
                features: |-
                    issue list
                    --label=enhancement
                igrep: '!gh issue list --label="$1" | grep "$2"'
            `),
			wantConfig: heredoc.Doc(`
                aliases:
                    co: pr checkout
                    features: |-
                        issue list
                        --label=enhancement
                    igrep: '!gh issue list --label="$1" | grep "$2"'
            `),
			wantStderr: importStdinMsg + "\n✓ Added alias co\n✓ Added alias features\n✓ Added alias igrep\n\n",
		},
		{
			name: "already taken aliases",
			opts: &ImportOptions{
				Filename:          tmpFile,
				OverwriteExisting: false,
			},
			fileContents: heredoc.Doc(`
                co: pr checkout -R cool/repo
                igrep: '!gh issue list --label="$1" | grep "$2"'
            `),
			initConfig: heredoc.Doc(`
                aliases:
                    co: pr checkout
                editor: vim
            `),
			aliasCommands: []*cobra.Command{
				{Use: "co"},
			},
			wantConfig: heredoc.Doc(`
                aliases:
                    co: pr checkout
                    igrep: '!gh issue list --label="$1" | grep "$2"'
                editor: vim
            `),
			wantStderr: importFileMsg + "\nX Could not import alias co: name already taken\n✓ Added alias igrep\n\n",
		},
		{
			name: "override aliases",
			opts: &ImportOptions{
				Filename:          tmpFile,
				OverwriteExisting: true,
			},
			fileContents: heredoc.Doc(`
                co: pr checkout -R cool/repo
                igrep: '!gh issue list --label="$1" | grep "$2"'
            `),
			initConfig: heredoc.Doc(`
                aliases:
                    co: pr checkout
                editor: vim
            `),
			aliasCommands: []*cobra.Command{
				{Use: "co"},
			},
			wantConfig: heredoc.Doc(`
                aliases:
                    co: pr checkout -R cool/repo
                    igrep: '!gh issue list --label="$1" | grep "$2"'
                editor: vim
            `),
			wantStderr: importFileMsg + "\n! Changed alias co\n✓ Added alias igrep\n\n",
		},
		{
			name: "alias is a gh command",
			opts: &ImportOptions{
				Filename:          tmpFile,
				OverwriteExisting: false,
			},
			fileContents: heredoc.Doc(`
                pr: pr checkout
                issue: issue list
                api: api graphql
            `),
			wantStderr: strings.Join(
				[]string{
					importFileMsg,
					"X Could not import alias api: already a gh command or extension",
					"X Could not import alias issue: already a gh command or extension",
					"X Could not import alias pr: already a gh command or extension\n\n",
				},
				"\n",
			),
		},
		{
			name: "invalid expansion",
			opts: &ImportOptions{
				Filename:          tmpFile,
				OverwriteExisting: false,
			},
			fileContents: heredoc.Doc(`
                alias1:
                alias2: ps checkout
            `),
			wantStderr: strings.Join(
				[]string{
					importFileMsg,
					"X Could not import alias alias1: expansion does not correspond to a gh command, extension, or alias",
					"X Could not import alias alias2: expansion does not correspond to a gh command, extension, or alias\n\n",
				},
				"\n",
			),
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			if tt.fileContents != "" {
				err := os.WriteFile(tmpFile, []byte(tt.fileContents), 0600)
				require.NoError(t, err)
			}

			ios, stdin, _, stderr := iostreams.Test()
			ios.SetStdinTTY(true)
			ios.SetStdoutTTY(true)
			ios.SetStderrTTY(true)
			tt.opts.IO = ios

			readConfigs := config.StubWriteConfig(t)
			cfg := config.NewFromString(tt.initConfig)
			tt.opts.Config = func() (config.Config, error) {
				return cfg, nil
			}

			// Create fake command structure for testing.
			rootCmd := &cobra.Command{}
			prCmd := &cobra.Command{Use: "pr"}
			prCmd.AddCommand(&cobra.Command{Use: "checkout"})
			prCmd.AddCommand(&cobra.Command{Use: "status"})
			rootCmd.AddCommand(prCmd)
			issueCmd := &cobra.Command{Use: "issue"}
			issueCmd.AddCommand(&cobra.Command{Use: "list"})
			rootCmd.AddCommand(issueCmd)
			apiCmd := &cobra.Command{Use: "api"}
			apiCmd.AddCommand(&cobra.Command{Use: "graphql"})
			rootCmd.AddCommand(apiCmd)
			for _, cmd := range tt.aliasCommands {
				rootCmd.AddCommand(cmd)
			}

			tt.opts.validAliasName = shared.ValidAliasNameFunc(rootCmd)
			tt.opts.validAliasExpansion = shared.ValidAliasExpansionFunc(rootCmd)

			if tt.stdin != "" {
				stdin.WriteString(tt.stdin)
			}

			err := importRun(tt.opts)
			require.NoError(t, err)

			configOut := bytes.Buffer{}
			readConfigs(&configOut, io.Discard)

			assert.Equal(t, tt.wantStderr, stderr.String())
			assert.Equal(t, tt.wantConfig, configOut.String())
		})
	}
}
