package set

import (
	"bytes"
	"encoding/json"
	"io"
	"net/http"
	"os"
	"testing"

	"github.com/MakeNowJust/heredoc"
	"github.com/cli/cli/v2/internal/config"
	"github.com/cli/cli/v2/internal/gh"
	"github.com/cli/cli/v2/internal/ghrepo"
	"github.com/cli/cli/v2/internal/prompter"
	"github.com/cli/cli/v2/pkg/cmd/variable/shared"
	"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 TestNewCmdSet(t *testing.T) {
	tests := []struct {
		name     string
		cli      string
		wants    SetOptions
		stdinTTY bool
		wantsErr bool
	}{
		{
			name:     "invalid visibility type",
			cli:      "cool_variable --org coolOrg -v'mistyVeil'",
			wantsErr: true,
		},
		{
			name:     "selected visibility with no repos selected",
			cli:      "cool_variable --org coolOrg -v'selected'",
			wantsErr: true,
		},
		{
			name:     "private visibility with repos selected",
			cli:      "cool_variable --org coolOrg -v'private' -rcoolRepo",
			wantsErr: true,
		},
		{
			name:     "no name specified",
			cli:      "",
			wantsErr: true,
		},
		{
			name:     "multiple names specified",
			cli:      "cool_variable good_variable",
			wantsErr: true,
		},
		{
			name:     "visibility without org",
			cli:      "cool_variable -vall",
			wantsErr: true,
		},
		{
			name: "repos without explicit visibility",
			cli:  "cool_variable -bs --org coolOrg -rcoolRepo",
			wants: SetOptions{
				VariableName:    "cool_variable",
				Visibility:      shared.Selected,
				RepositoryNames: []string{"coolRepo"},
				Body:            "s",
				OrgName:         "coolOrg",
			},
		},
		{
			name: "org with explicit visibility and selected repo",
			cli:  "-ocoolOrg -bs -vselected -rcoolRepo cool_variable",
			wants: SetOptions{
				VariableName:    "cool_variable",
				Visibility:      shared.Selected,
				RepositoryNames: []string{"coolRepo"},
				Body:            "s",
				OrgName:         "coolOrg",
			},
		},
		{
			name: "org with explicit visibility and selected repos",
			cli:  `--org=coolOrg -bs -vselected -r="coolRepo,radRepo,goodRepo" cool_variable`,
			wants: SetOptions{
				VariableName:    "cool_variable",
				Visibility:      shared.Selected,
				RepositoryNames: []string{"coolRepo", "goodRepo", "radRepo"},
				Body:            "s",
				OrgName:         "coolOrg",
			},
		},
		{
			name: "repo",
			cli:  `cool_variable -b"a variable"`,
			wants: SetOptions{
				VariableName: "cool_variable",
				Visibility:   shared.Private,
				Body:         "a variable",
				OrgName:      "",
			},
		},
		{
			name: "env",
			cli:  `cool_variable -b"a variable" -eRelease`,
			wants: SetOptions{
				VariableName: "cool_variable",
				Visibility:   shared.Private,
				Body:         "a variable",
				OrgName:      "",
				EnvName:      "Release",
			},
		},
		{
			name: "visibility all",
			cli:  `cool_variable --org coolOrg -b"cool" -vall`,
			wants: SetOptions{
				VariableName: "cool_variable",
				Visibility:   shared.All,
				Body:         "cool",
				OrgName:      "coolOrg",
			},
		},
		{
			name: "env file",
			cli:  `--env-file test.env`,
			wants: SetOptions{
				Visibility: shared.Private,
				EnvFile:    "test.env",
			},
		},
	}

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

			ios.SetStdinTTY(tt.stdinTTY)

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

			var gotOpts *SetOptions
			cmd := NewCmdSet(f, func(opts *SetOptions) 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.wantsErr {
				assert.Error(t, err)
				return
			}
			assert.NoError(t, err)

			assert.Equal(t, tt.wants.VariableName, gotOpts.VariableName)
			assert.Equal(t, tt.wants.Body, gotOpts.Body)
			assert.Equal(t, tt.wants.Visibility, gotOpts.Visibility)
			assert.Equal(t, tt.wants.OrgName, gotOpts.OrgName)
			assert.Equal(t, tt.wants.EnvName, gotOpts.EnvName)
			assert.Equal(t, tt.wants.EnvFile, gotOpts.EnvFile)
			assert.ElementsMatch(t, tt.wants.RepositoryNames, gotOpts.RepositoryNames)
		})
	}
}

func Test_setRun_repo(t *testing.T) {
	tests := []struct {
		name      string
		httpStubs func(*httpmock.Registry)
		wantErr   bool
	}{
		{
			name: "create actions variable",
			httpStubs: func(reg *httpmock.Registry) {
				reg.Register(httpmock.REST("POST", "repos/owner/repo/actions/variables"),
					httpmock.StatusStringResponse(201, `{}`))
			},
		},
		{
			name: "update actions variable",
			httpStubs: func(reg *httpmock.Registry) {
				reg.Register(httpmock.REST("POST", "repos/owner/repo/actions/variables"),
					httpmock.StatusStringResponse(409, `{}`))
				reg.Register(httpmock.REST("PATCH", "repos/owner/repo/actions/variables/cool_variable"),
					httpmock.StatusStringResponse(204, `{}`))
			},
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			reg := &httpmock.Registry{}
			defer reg.Verify(t)
			if tt.httpStubs != nil {
				tt.httpStubs(reg)
			}

			ios, _, _, _ := iostreams.Test()

			opts := &SetOptions{
				HttpClient: func() (*http.Client, error) {
					return &http.Client{Transport: reg}, nil
				},
				Config: func() (gh.Config, error) { return config.NewBlankConfig(), nil },
				BaseRepo: func() (ghrepo.Interface, error) {
					return ghrepo.FromFullName("owner/repo")
				},
				IO:           ios,
				VariableName: "cool_variable",
				Body:         "a variable",
			}

			err := setRun(opts)
			if tt.wantErr {
				assert.Error(t, err)
				return
			} else {
				assert.NoError(t, err)
			}

			data, err := io.ReadAll(reg.Requests[len(reg.Requests)-1].Body)
			assert.NoError(t, err)

			var payload setPayload
			err = json.Unmarshal(data, &payload)
			assert.NoError(t, err)
			assert.Equal(t, payload.Value, "a variable")
		})
	}
}

func Test_setRun_env(t *testing.T) {
	tests := []struct {
		name      string
		opts      *SetOptions
		httpStubs func(*httpmock.Registry)
		wantErr   bool
	}{
		{
			name: "create env variable",
			opts: &SetOptions{},
			httpStubs: func(reg *httpmock.Registry) {
				reg.Register(httpmock.GraphQL(`query MapRepositoryNames\b`),
					httpmock.StringResponse(`{"data":{"repo_0001":{"databaseId":1}}}`))
				reg.Register(httpmock.REST("POST", "repositories/1/environments/release/variables"),
					httpmock.StatusStringResponse(201, `{}`))
			},
		},
		{
			name: "update env variable",
			opts: &SetOptions{},
			httpStubs: func(reg *httpmock.Registry) {
				reg.Register(httpmock.GraphQL(`query MapRepositoryNames\b`),
					httpmock.StringResponse(`{"data":{"repo_0001":{"databaseId":1}}}`))
				reg.Register(httpmock.REST("POST", "repositories/1/environments/release/variables"),
					httpmock.StatusStringResponse(409, `{}`))
				reg.Register(httpmock.REST("PATCH", "repositories/1/environments/release/variables/cool_variable"),
					httpmock.StatusStringResponse(204, `{}`))
			},
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			reg := &httpmock.Registry{}
			defer reg.Verify(t)
			if tt.httpStubs != nil {
				tt.httpStubs(reg)
			}

			ios, _, _, _ := iostreams.Test()

			opts := &SetOptions{
				HttpClient: func() (*http.Client, error) {
					return &http.Client{Transport: reg}, nil
				},
				Config: func() (gh.Config, error) { return config.NewBlankConfig(), nil },
				BaseRepo: func() (ghrepo.Interface, error) {
					return ghrepo.FromFullName("owner/repo")
				},
				IO:           ios,
				VariableName: "cool_variable",
				Body:         "a variable",
				EnvName:      "release",
			}

			err := setRun(opts)
			if tt.wantErr {
				assert.Error(t, err)
				return
			} else {
				assert.NoError(t, err)
			}

			data, err := io.ReadAll(reg.Requests[len(reg.Requests)-1].Body)
			assert.NoError(t, err)

			var payload setPayload
			err = json.Unmarshal(data, &payload)
			assert.NoError(t, err)
			assert.Equal(t, payload.Value, "a variable")
		})
	}
}

func Test_setRun_org(t *testing.T) {
	tests := []struct {
		name             string
		opts             *SetOptions
		httpStubs        func(*httpmock.Registry)
		wantVisibility   shared.Visibility
		wantRepositories []int64
	}{
		{
			name: "create org variable",
			opts: &SetOptions{
				OrgName:    "UmbrellaCorporation",
				Visibility: shared.All,
			},
			httpStubs: func(reg *httpmock.Registry) {
				reg.Register(httpmock.REST("POST", "orgs/UmbrellaCorporation/actions/variables"),
					httpmock.StatusStringResponse(201, `{}`))
			},
		},
		{
			name: "update org variable",
			opts: &SetOptions{
				OrgName:    "UmbrellaCorporation",
				Visibility: shared.All,
			},
			httpStubs: func(reg *httpmock.Registry) {
				reg.Register(httpmock.REST("POST", "orgs/UmbrellaCorporation/actions/variables"),
					httpmock.StatusStringResponse(409, `{}`))
				reg.Register(httpmock.REST("PATCH", "orgs/UmbrellaCorporation/actions/variables/cool_variable"),
					httpmock.StatusStringResponse(204, `{}`))
			},
		},
		{
			name: "create org variable with selected visibility",
			opts: &SetOptions{
				OrgName:         "UmbrellaCorporation",
				Visibility:      shared.Selected,
				RepositoryNames: []string{"birkin", "UmbrellaCorporation/wesker"},
			},
			httpStubs: func(reg *httpmock.Registry) {
				reg.Register(httpmock.GraphQL(`query MapRepositoryNames\b`),
					httpmock.StringResponse(`{"data":{"repo_0001":{"databaseId":1},"repo_0002":{"databaseId":2}}}`))
				reg.Register(httpmock.REST("POST", "orgs/UmbrellaCorporation/actions/variables"),
					httpmock.StatusStringResponse(201, `{}`))
			},
			wantRepositories: []int64{1, 2},
		},
		{
			name: "update org variable with selected visibility",
			opts: &SetOptions{
				OrgName:         "UmbrellaCorporation",
				Visibility:      shared.Selected,
				RepositoryNames: []string{"birkin", "UmbrellaCorporation/wesker"},
			},
			httpStubs: func(reg *httpmock.Registry) {
				reg.Register(httpmock.GraphQL(`query MapRepositoryNames\b`),
					httpmock.StringResponse(`{"data":{"repo_0001":{"databaseId":1},"repo_0002":{"databaseId":2}}}`))
				reg.Register(httpmock.REST("POST", "orgs/UmbrellaCorporation/actions/variables"),
					httpmock.StatusStringResponse(409, `{}`))
				reg.Register(httpmock.REST("PATCH", "orgs/UmbrellaCorporation/actions/variables/cool_variable"),
					httpmock.StatusStringResponse(204, `{}`))
			},
			wantRepositories: []int64{1, 2},
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			reg := &httpmock.Registry{}
			defer reg.Verify(t)
			if tt.httpStubs != nil {
				tt.httpStubs(reg)
			}

			ios, _, _, _ := iostreams.Test()

			tt.opts.BaseRepo = func() (ghrepo.Interface, error) {
				return ghrepo.FromFullName("owner/repo")
			}
			tt.opts.HttpClient = func() (*http.Client, error) {
				return &http.Client{Transport: reg}, nil
			}
			tt.opts.Config = func() (gh.Config, error) {
				return config.NewBlankConfig(), nil
			}
			tt.opts.IO = ios
			tt.opts.VariableName = "cool_variable"
			tt.opts.Body = "a variable"

			err := setRun(tt.opts)
			assert.NoError(t, err)

			data, err := io.ReadAll(reg.Requests[len(reg.Requests)-1].Body)
			assert.NoError(t, err)
			var payload setPayload
			err = json.Unmarshal(data, &payload)
			assert.NoError(t, err)
			assert.Equal(t, payload.Value, "a variable")
			assert.Equal(t, payload.Visibility, tt.opts.Visibility)
			assert.ElementsMatch(t, payload.Repositories, tt.wantRepositories)
		})
	}
}

func Test_getBody_prompt(t *testing.T) {
	ios, _, _, _ := iostreams.Test()
	ios.SetStdinTTY(true)
	ios.SetStdoutTTY(true)
	prompterMock := &prompter.PrompterMock{}
	prompterMock.InputFunc = func(message, defaultValue string) (string, error) {
		return "a variable", nil
	}
	opts := &SetOptions{
		IO:       ios,
		Prompter: prompterMock,
	}
	body, err := getBody(opts)
	assert.NoError(t, err)
	assert.Equal(t, "a variable", body)
}

func Test_getBody(t *testing.T) {
	tests := []struct {
		name    string
		bodyArg string
		want    string
		stdin   string
	}{
		{
			name:    "literal value",
			bodyArg: "a variable",
			want:    "a variable",
		},
		{
			name:  "from stdin",
			want:  "a variable",
			stdin: "a variable",
		},
		{
			name:  "from stdin with trailing newline character",
			want:  "a variable",
			stdin: "a variable\n",
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			ios, stdin, _, _ := iostreams.Test()
			ios.SetStdinTTY(false)
			_, err := stdin.WriteString(tt.stdin)
			assert.NoError(t, err)
			opts := &SetOptions{
				IO:   ios,
				Body: tt.bodyArg,
			}
			body, err := getBody(opts)
			assert.NoError(t, err)
			assert.Equal(t, tt.want, body)
		})
	}
}

func Test_getVariablesFromOptions(t *testing.T) {
	genFile := func(s string) string {
		f, err := os.CreateTemp("", "gh-env.*")
		if err != nil {
			t.Fatal(err)
			return ""
		}
		defer f.Close()
		t.Cleanup(func() {
			_ = os.Remove(f.Name())
		})
		_, err = f.WriteString(s)
		if err != nil {
			t.Fatal(err)
		}
		return f.Name()
	}

	tests := []struct {
		name    string
		opts    SetOptions
		isTTY   bool
		stdin   string
		want    map[string]string
		wantErr bool
	}{
		{
			name: "variable from arg",
			opts: SetOptions{
				VariableName: "FOO",
				Body:         "bar",
			},
			want: map[string]string{"FOO": "bar"},
		},
		{
			name: "variable from stdin",
			opts: SetOptions{
				Body:    "",
				EnvFile: "-",
			},
			stdin: `FOO=bar`,
			want:  map[string]string{"FOO": "bar"},
		},
		{
			name: "variables from file",
			opts: SetOptions{
				Body: "",
				EnvFile: genFile(heredoc.Doc(`
					FOO=bar
					QUOTED="my value"
					#IGNORED=true
					export SHELL=bash
				`)),
			},
			stdin: `FOO=bar`,
			want: map[string]string{
				"FOO":    "bar",
				"SHELL":  "bash",
				"QUOTED": "my value",
			},
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			ios, stdin, _, _ := iostreams.Test()
			ios.SetStdinTTY(tt.isTTY)
			ios.SetStdoutTTY(tt.isTTY)
			stdin.WriteString(tt.stdin)
			opts := tt.opts
			opts.IO = ios
			gotVariables, err := getVariablesFromOptions(&opts)
			if tt.wantErr {
				assert.Error(t, err)
			} else {
				assert.NoError(t, err)
			}
			assert.Equal(t, len(tt.want), len(gotVariables))
			for k, v := range gotVariables {
				assert.Equal(t, tt.want[k], v)
			}
		})
	}
}
