package main

import (
	"bytes"
	"context"
	"fmt"
	"io/ioutil"
	"reflect"
	"testing"

	"github.com/influxdata/influxdb/v2"
	platform "github.com/influxdata/influxdb/v2"
	"github.com/influxdata/influxdb/v2/mock"
	"github.com/spf13/cobra"
	"github.com/stretchr/testify/assert"
	"github.com/stretchr/testify/require"
	input "github.com/tcnksm/go-input"
)

func TestCmdUser(t *testing.T) {
	type userResult struct {
		user     influxdb.User
		password string
	}

	fakeSVCFn := func(dep cmdUserDeps) userSVCsFn {
		return func() (cmdUserDeps, error) {
			return dep, nil
		}
	}

	newCMDUserDeps := func(
		userSVC influxdb.UserService,
		passSVC influxdb.PasswordsService,
		getPassFn func(*input.UI, bool) string,
	) cmdUserDeps {
		return cmdUserDeps{
			userSVC: userSVC,
			orgSvc: &mock.OrganizationService{
				FindOrganizationF: func(ctx context.Context, filter influxdb.OrganizationFilter) (*influxdb.Organization, error) {
					return &influxdb.Organization{ID: influxdb.ID(9000), Name: "influxdata"}, nil
				},
			},
			passSVC: passSVC,
			urmSVC: &mock.UserResourceMappingService{
				CreateMappingFn: func(context.Context, *platform.UserResourceMapping) error {
					return nil
				},
			},
			getPassFn: getPassFn,
		}
	}

	t.Run("create", func(t *testing.T) {
		tests := []struct {
			name     string
			expected userResult
			flags    []string
			envVars  map[string]string
		}{
			{
				name:  "basic just name",
				flags: []string{"--name=new name", "--org=org name"},
				expected: userResult{
					user: influxdb.User{
						Name: "new name",
					},
				},
			},
			{
				name: "with password",
				flags: []string{
					"--name=new name",
					"--password=pass1",
					"--org=org name",
				},
				expected: userResult{
					user: influxdb.User{
						Name: "new name",
					},
					password: "pass1",
				},
			},
			{
				name: "with password and env",
				flags: []string{
					"--name=new name",
					"--password=pass1",
				},
				envVars: map[string]string{
					"INFLUX_ORG":    "",
					"INFLUX_ORG_ID": influxdb.ID(1).String(),
				},
				expected: userResult{
					user: influxdb.User{
						Name: "new name",
					},
					password: "pass1",
				},
			},
			{
				name: "shorts",
				flags: []string{
					"-n=new name",
					"-o=org name",
				},
				expected: userResult{
					user: influxdb.User{
						Name: "new name",
					},
				},
			},
		}

		cmdFn := func(expected userResult) func(*globalFlags, genericCLIOpts) *cobra.Command {
			svc := mock.NewUserService()
			svc.CreateUserFn = func(ctx context.Context, User *influxdb.User) error {
				if expected.user != *User {
					return fmt.Errorf("unexpected User;\n\twant= %+v\n\tgot=  %+v", expected, *User)
				}
				return nil
			}
			passSVC := mock.NewPasswordsService()
			passSVC.SetPasswordFn = func(ctx context.Context, id influxdb.ID, password string) error {
				if expected.password != password {
					return fmt.Errorf("unexpected password;\n\twant= %+v\n\tgot=  %+v", expected.password, password)
				}
				return nil
			}

			return func(g *globalFlags, opt genericCLIOpts) *cobra.Command {
				builder := newCmdUserBuilder(fakeSVCFn(newCMDUserDeps(svc, passSVC, nil)), opt)
				return builder.cmd()
			}

		}

		for _, tt := range tests {
			fn := func(t *testing.T) {
				defer addEnvVars(t, tt.envVars)()

				builder := newInfluxCmdBuilder(
					in(new(bytes.Buffer)),
					out(ioutil.Discard),
				)
				cmd := builder.cmd(cmdFn(tt.expected))
				cmd.SetArgs(append([]string{"user", "create"}, tt.flags...))

				require.NoError(t, cmd.Execute())
			}

			t.Run(tt.name, fn)
		}
	})

	t.Run("delete", func(t *testing.T) {
		tests := []struct {
			name       string
			expectedID influxdb.ID
			flag       string
		}{
			{
				name:       "long id",
				expectedID: influxdb.ID(1),
				flag:       "--id=",
			},
			{
				name:       "shorts",
				expectedID: influxdb.ID(1),
				flag:       "-i=",
			},
		}

		cmdFn := func(expectedID influxdb.ID) func(*globalFlags, genericCLIOpts) *cobra.Command {
			svc := mock.NewUserService()
			svc.FindUserByIDFn = func(ctx context.Context, id influxdb.ID) (*influxdb.User, error) {
				return &influxdb.User{ID: id}, nil
			}
			svc.DeleteUserFn = func(ctx context.Context, id influxdb.ID) error {
				if expectedID != id {
					return fmt.Errorf("unexpected id:\n\twant= %s\n\tgot=  %s", expectedID, id)
				}
				return nil
			}

			return func(g *globalFlags, opt genericCLIOpts) *cobra.Command {
				builder := newCmdUserBuilder(fakeSVCFn(newCMDUserDeps(svc, nil, nil)), opt)
				return builder.cmd()
			}
		}

		for _, tt := range tests {
			fn := func(t *testing.T) {
				builder := newInfluxCmdBuilder(
					in(new(bytes.Buffer)),
					out(ioutil.Discard),
				)
				cmd := builder.cmd(cmdFn(tt.expectedID))
				idFlag := tt.flag + tt.expectedID.String()
				cmd.SetArgs([]string{"user", "delete", idFlag})

				require.NoError(t, cmd.Execute())
			}

			t.Run(tt.name, fn)
		}
	})

	t.Run("list", func(t *testing.T) {
		type called struct {
			name string
			id   influxdb.ID
		}

		tests := []struct {
			name     string
			expected called
			command  string
			flags    []string
		}{
			{
				name: "id",
				flags: []string{
					"--id=" + influxdb.ID(2).String(),
				},
				expected: called{
					id: 2,
				},
			},
			{
				name:     "name",
				flags:    []string{"--name=name1"},
				expected: called{name: "name1"},
			},
			{
				name: "shorts",
				flags: []string{
					"-n=name1",
					"-i=" + influxdb.ID(1).String(),
				},
				expected: called{name: "name1", id: 1},
			},
			{
				name:    "ls alias",
				command: "ls",
				flags: []string{
					"--id=" + influxdb.ID(2).String(),
				},
				expected: called{
					id: 2,
				},
			},
			{
				name:    "find alias",
				command: "find",
				flags: []string{
					"--id=" + influxdb.ID(2).String(),
				},
				expected: called{
					id: 2,
				},
			},
		}

		cmdFn := func() (func(*globalFlags, genericCLIOpts) *cobra.Command, *called) {
			calls := new(called)

			svc := mock.NewUserService()
			svc.FindUsersFn = func(ctx context.Context, f influxdb.UserFilter, opt ...influxdb.FindOptions) ([]*influxdb.User, int, error) {
				if f.ID != nil {
					calls.id = *f.ID
				}
				if f.Name != nil {
					calls.name = *f.Name
				}
				return nil, 0, nil
			}

			return func(g *globalFlags, opt genericCLIOpts) *cobra.Command {
				builder := newCmdUserBuilder(fakeSVCFn(newCMDUserDeps(svc, nil, nil)), opt)
				return builder.cmd()
			}, calls
		}

		for _, tt := range tests {
			fn := func(t *testing.T) {
				builder := newInfluxCmdBuilder(
					in(new(bytes.Buffer)),
					out(ioutil.Discard),
				)
				nestedCmdFn, calls := cmdFn()
				cmd := builder.cmd(nestedCmdFn)

				if tt.command == "" {
					tt.command = "list"
				}

				cmd.SetArgs(append([]string{"user", tt.command}, tt.flags...))

				require.NoError(t, cmd.Execute())
				assert.Equal(t, tt.expected, *calls)
			}

			t.Run(tt.name, fn)
		}
	})

	t.Run("update", func(t *testing.T) {
		tests := []struct {
			name     string
			expected influxdb.UserUpdate
			flags    []string
		}{
			{
				name: "basic just name",
				flags: []string{
					"--id=" + influxdb.ID(3).String(),
					"--name=new name",
				},
				expected: influxdb.UserUpdate{
					Name: strPtr("new name"),
				},
			},
			{
				name: "with all fields",
				flags: []string{
					"--id=" + influxdb.ID(3).String(),
					"--name=new name",
				},
				expected: influxdb.UserUpdate{
					Name: strPtr("new name"),
				},
			},
			{
				name: "shorts",
				flags: []string{
					"-i=" + influxdb.ID(3).String(),
					"-n=new name",
				},
				expected: influxdb.UserUpdate{
					Name: strPtr("new name"),
				},
			},
		}

		cmdFn := func(expectedUpdate influxdb.UserUpdate) func(*globalFlags, genericCLIOpts) *cobra.Command {
			svc := mock.NewUserService()
			svc.UpdateUserFn = func(ctx context.Context, id influxdb.ID, upd influxdb.UserUpdate) (*influxdb.User, error) {
				if id != 3 {
					return nil, fmt.Errorf("unexpecte id:\n\twant= %s\n\tgot=  %s", influxdb.ID(3), id)
				}
				if !reflect.DeepEqual(expectedUpdate, upd) {
					return nil, fmt.Errorf("unexpected User update;\n\twant= %+v\n\tgot=  %+v", expectedUpdate, upd)
				}
				return &influxdb.User{}, nil
			}

			return func(g *globalFlags, opt genericCLIOpts) *cobra.Command {
				builder := newCmdUserBuilder(fakeSVCFn(newCMDUserDeps(svc, nil, nil)), opt)
				return builder.cmd()
			}
		}

		for _, tt := range tests {
			fn := func(t *testing.T) {
				builder := newInfluxCmdBuilder(
					in(new(bytes.Buffer)),
					out(ioutil.Discard),
				)
				cmd := builder.cmd(cmdFn(tt.expected))
				cmd.SetArgs(append([]string{"user", "update"}, tt.flags...))

				require.NoError(t, cmd.Execute())
			}

			t.Run(tt.name, fn)
		}
	})

	t.Run("password", func(t *testing.T) {
		tests := []struct {
			name     string
			expected string
			flags    []string
		}{
			{
				name: "basic id",
				flags: []string{
					"--id=" + influxdb.ID(3).String(),
				},
				expected: "pass1",
			},
			{
				name: "shorts",
				flags: []string{
					"-i=" + influxdb.ID(3).String(),
					"-n=new name",
				},
				expected: "pass1",
			},
		}

		cmdFn := func(expected string) func(*globalFlags, genericCLIOpts) *cobra.Command {
			svc := mock.NewUserService()
			svc.FindUserFn = func(ctx context.Context, f influxdb.UserFilter) (*influxdb.User, error) {
				usr := new(influxdb.User)
				if id := f.ID; id != nil {
					usr.ID = *id
				}
				if name := f.Name; name != nil {
					usr.Name = *name
				}
				return usr, nil
			}
			passSVC := mock.NewPasswordsService()
			passSVC.SetPasswordFn = func(ctx context.Context, id influxdb.ID, pass string) error {
				if pass != expected {
					return fmt.Errorf("unexpecte id:\n\twant= %s\n\tgot=  %s", pass, expected)
				}
				return nil
			}

			getPassFn := func(*input.UI, bool) string {
				return expected
			}

			return func(g *globalFlags, opt genericCLIOpts) *cobra.Command {
				builder := newCmdUserBuilder(fakeSVCFn(newCMDUserDeps(svc, passSVC, getPassFn)), opt)
				return builder.cmd()
			}
		}

		for _, tt := range tests {
			fn := func(t *testing.T) {
				builder := newInfluxCmdBuilder(
					in(new(bytes.Buffer)),
					out(ioutil.Discard),
				)
				cmd := builder.cmd(cmdFn(tt.expected))
				cmd.SetArgs(append([]string{"user", "password"}, tt.flags...))

				require.NoError(t, cmd.Execute())
			}

			t.Run(tt.name, fn)
		}
	})
}
