package main

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

	"github.com/stretchr/testify/assert"

	"github.com/influxdata/influxdb/v2"
	"github.com/influxdata/influxdb/v2/mock"
	"github.com/spf13/cobra"
	"github.com/stretchr/testify/require"
)

func TestCmdOrg(t *testing.T) {
	fakeOrgSVCFn := func(svc influxdb.OrganizationService) orgSVCFn {
		return func() (influxdb.OrganizationService, influxdb.UserResourceMappingService, influxdb.UserService, error) {
			return svc, mock.NewUserResourceMappingService(), mock.NewUserService(), nil
		}
	}

	fakeOrgUrmSVCsFn := func(svc influxdb.OrganizationService, urmSVC influxdb.UserResourceMappingService) orgSVCFn {
		return func() (influxdb.OrganizationService, influxdb.UserResourceMappingService, influxdb.UserService, error) {
			return svc, urmSVC, mock.NewUserService(), nil
		}
	}

	t.Run("create", func(t *testing.T) {
		tests := []struct {
			name     string
			expected influxdb.Organization
			flags    []string
		}{
			{
				name:  "all",
				flags: []string{"--name=new name", "--description=desc"},
				expected: influxdb.Organization{
					Name:        "new name",
					Description: "desc",
				},
			},
			{
				name:  "shorts",
				flags: []string{"-n=new name", "-d=desc"},
				expected: influxdb.Organization{
					Name:        "new name",
					Description: "desc",
				},
			},
		}

		cmdFn := func(expectedOrg influxdb.Organization) func(*globalFlags, genericCLIOpts) *cobra.Command {
			svc := mock.NewOrganizationService()
			svc.CreateOrganizationF = func(ctx context.Context, org *influxdb.Organization) error {
				if expectedOrg != *org {
					return fmt.Errorf("unexpected org;\n\twant= %+v\n\tgot=  %+v", expectedOrg, *org)
				}
				return nil
			}

			return func(_ *globalFlags, opt genericCLIOpts) *cobra.Command {
				builder := newCmdOrgBuilder(fakeOrgSVCFn(svc), 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{"org", "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:       "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.NewOrganizationService()
			svc.FindOrganizationByIDF = func(ctx context.Context, id influxdb.ID) (*influxdb.Organization, error) {
				return &influxdb.Organization{ID: id}, nil
			}
			svc.DeleteOrganizationF = 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 := newCmdOrgBuilder(fakeOrgSVCFn(svc), 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{"org", "find", 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
			flags    []string
			command  string
			envVars  map[string]string
		}{
			{
				name:     "org id",
				flags:    []string{"--id=" + influxdb.ID(3).String()},
				envVars:  envVarsZeroMap,
				expected: called{id: 3},
			},
			{
				name:     "name",
				flags:    []string{"--name=name1"},
				envVars:  envVarsZeroMap,
				expected: called{name: "name1"},
			},
			{
				name: "shorts",
				flags: []string{
					"-n=name1",
					"-i=" + influxdb.ID(1).String(),
				},
				envVars:  envVarsZeroMap,
				expected: called{name: "name1", id: 1},
			},
			{
				name: "env vars",
				envVars: map[string]string{
					"INFLUX_ORG_ID": influxdb.ID(1).String(),
					"INFLUX_ORG":    "name1",
				},
				flags:    []string{"-i=" + influxdb.ID(1).String()},
				expected: called{name: "name1", id: 1},
			},
			{
				name:     "ls alias",
				command:  "ls",
				flags:    []string{"--name=name1"},
				envVars:  envVarsZeroMap,
				expected: called{name: "name1"},
			},
			{
				name:     "find alias",
				command:  "find",
				flags:    []string{"--name=name1"},
				envVars:  envVarsZeroMap,
				expected: called{name: "name1"},
			},
		}

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

			svc := mock.NewOrganizationService()
			svc.FindOrganizationsF = func(ctx context.Context, f influxdb.OrganizationFilter, opt ...influxdb.FindOptions) ([]*influxdb.Organization, 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 := newCmdOrgBuilder(fakeOrgSVCFn(svc), opt)
				return builder.cmd()
			}, calls
		}

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

				builder := newInfluxCmdBuilder(
					in(new(bytes.Buffer)),
					out(ioutil.Discard),
				)
				cmdFn, calls := cmdFn()
				cmd := builder.cmd(cmdFn)

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

				cmd.SetArgs(append([]string{"org", 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.OrganizationUpdate
			flags    []string
			envVars  map[string]string
		}{
			{
				name: "basic just name",
				flags: []string{
					"--id=" + influxdb.ID(3).String(),
					"--name=new name",
				},
				expected: influxdb.OrganizationUpdate{
					Name: strPtr("new name"),
				},
			},
			{
				name: "with all fields",
				flags: []string{
					"--id=" + influxdb.ID(3).String(),
					"--name=new name",
					"--description=desc",
				},
				expected: influxdb.OrganizationUpdate{
					Name:        strPtr("new name"),
					Description: strPtr("desc"),
				},
			},
			{
				name: "shorts",
				flags: []string{
					"-i=" + influxdb.ID(3).String(),
					"-n=new name",
					"-d=desc",
				},
				expected: influxdb.OrganizationUpdate{
					Name:        strPtr("new name"),
					Description: strPtr("desc"),
				},
			},
			{
				name: "env var",
				envVars: map[string]string{
					"INFLUX_ORG":             "new name",
					"INFLUX_ORG_ID":          influxdb.ID(3).String(),
					"INFLUX_ORG_DESCRIPTION": "desc",
				},
				expected: influxdb.OrganizationUpdate{
					Name:        strPtr("new name"),
					Description: strPtr("desc"),
				},
			},
		}

		cmdFn := func(expectedUpdate influxdb.OrganizationUpdate) func(*globalFlags, genericCLIOpts) *cobra.Command {
			svc := mock.NewOrganizationService()
			svc.UpdateOrganizationF = func(ctx context.Context, id influxdb.ID, upd influxdb.OrganizationUpdate) (*influxdb.Organization, 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 bucket update;\n\twant= %+v\n\tgot=  %+v", expectedUpdate, upd)
				}
				return &influxdb.Organization{}, nil
			}

			return func(g *globalFlags, opt genericCLIOpts) *cobra.Command {
				builder := newCmdOrgBuilder(fakeOrgSVCFn(svc), 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{"org", "update"}, tt.flags...))

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

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

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

			testCase struct {
				name        string
				expected    called
				memberFlags []string
				envVars     map[string]string
			}
		)

		testMemberFn := func(t *testing.T, cmdName string, cmdFn func() (func(*globalFlags, genericCLIOpts) *cobra.Command, *called), testCases ...testCase) {
			for _, tt := range testCases {
				fn := func(t *testing.T) {
					envVars := tt.envVars
					if len(envVars) == 0 {
						envVars = envVarsZeroMap
					}
					defer addEnvVars(t, envVars)()

					outBuf := new(bytes.Buffer)
					defer func() {
						if t.Failed() && outBuf.Len() > 0 {
							t.Log(outBuf.String())
						}
					}()

					builder := newInfluxCmdBuilder(
						in(new(bytes.Buffer)),
						out(outBuf),
					)
					nestedCmd, calls := cmdFn()
					cmd := builder.cmd(nestedCmd)
					cmd.SetArgs(append([]string{"org", "members", cmdName}, tt.memberFlags...))

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

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

		t.Run("list", func(t *testing.T) {
			tests := []testCase{
				{
					name:        "org id",
					memberFlags: []string{"--id=" + influxdb.ID(3).String()},
					envVars:     envVarsZeroMap,
					expected:    called{id: 3},
				},
				{
					name:        "org id short",
					memberFlags: []string{"-i=" + influxdb.ID(3).String()},
					envVars:     envVarsZeroMap,
					expected:    called{id: 3},
				},
				{
					name: "org id env var",
					envVars: map[string]string{
						"INFLUX_ORG":    "",
						"INFLUX_ORG_ID": influxdb.ID(3).String(),
					},
					expected: called{id: 3},
				},
				{
					name:        "name",
					memberFlags: []string{"--name=name1"},
					envVars:     envVarsZeroMap,
					expected:    called{name: "name1"},
				},
				{
					name:        "name short",
					memberFlags: []string{"-n=name1"},
					envVars:     envVarsZeroMap,
					expected:    called{name: "name1"},
				},
				{
					name:     "name env var",
					envVars:  map[string]string{"INFLUX_ORG": "name1"},
					expected: called{name: "name1"},
				},
			}

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

				svc := mock.NewOrganizationService()
				svc.FindOrganizationF = func(ctx context.Context, f influxdb.OrganizationFilter) (*influxdb.Organization, error) {
					if f.ID != nil {
						calls.id = *f.ID
					}
					if f.Name != nil {
						calls.name = *f.Name
					}
					return &influxdb.Organization{ID: 1}, nil
				}

				return func(g *globalFlags, opt genericCLIOpts) *cobra.Command {
					builder := newCmdOrgBuilder(fakeOrgSVCFn(svc), opt)
					return builder.cmd()
				}, calls
			}

			testMemberFn(t, "list", cmdFn, tests...)
			testMemberFn(t, "ls", cmdFn, tests[0:1]...)
			testMemberFn(t, "find", cmdFn, tests[0:1]...)
		})

		t.Run("add", func(t *testing.T) {
			cmdFn := func() (func(*globalFlags, genericCLIOpts) *cobra.Command, *called) {
				calls := new(called)

				svc := mock.NewOrganizationService()
				svc.FindOrganizationF = func(ctx context.Context, f influxdb.OrganizationFilter) (*influxdb.Organization, error) {
					if f.ID != nil {
						calls.id = *f.ID
					}
					if f.Name != nil {
						calls.name = *f.Name
					}
					return &influxdb.Organization{ID: 1}, nil
				}
				urmSVC := mock.NewUserResourceMappingService()
				urmSVC.CreateMappingFn = func(ctx context.Context, m *influxdb.UserResourceMapping) error {
					calls.memberID = m.UserID
					return nil
				}

				return func(g *globalFlags, opt genericCLIOpts) *cobra.Command {
					builder := newCmdOrgBuilder(fakeOrgUrmSVCsFn(svc, urmSVC), opt)
					return builder.cmd()
				}, calls
			}

			addTests := []testCase{
				{
					name: "org id",
					memberFlags: []string{
						"--id=" + influxdb.ID(3).String(),
						"--member=" + influxdb.ID(4).String(),
					},
					envVars:  envVarsZeroMap,
					expected: called{id: 3, memberID: 4},
				},
				{
					name: "org id shorts",
					memberFlags: []string{
						"-i=" + influxdb.ID(3).String(),
						"-m=" + influxdb.ID(4).String(),
					},
					envVars:  envVarsZeroMap,
					expected: called{id: 3, memberID: 4},
				},
				{
					name: "org name",
					memberFlags: []string{
						"--name=name1",
						"--member=" + influxdb.ID(4).String(),
					},
					envVars:  envVarsZeroMap,
					expected: called{name: "name1", memberID: 4},
				},
				{
					name: "org name shorts",
					memberFlags: []string{
						"-n=name1",
						"-m=" + influxdb.ID(4).String(),
					},
					envVars:  envVarsZeroMap,
					expected: called{name: "name1", memberID: 4},
				},
			}

			testMemberFn(t, "add", cmdFn, addTests...)
		})

		t.Run("remove", func(t *testing.T) {
			cmdFn := func() (func(*globalFlags, genericCLIOpts) *cobra.Command, *called) {
				calls := new(called)

				svc := mock.NewOrganizationService()
				svc.FindOrganizationF = func(ctx context.Context, f influxdb.OrganizationFilter) (*influxdb.Organization, error) {
					if f.ID != nil {
						calls.id = *f.ID
					}
					if f.Name != nil {
						calls.name = *f.Name
					}
					return &influxdb.Organization{ID: 1}, nil
				}
				urmSVC := mock.NewUserResourceMappingService()
				urmSVC.DeleteMappingFn = func(ctx context.Context, resourceID, userID influxdb.ID) error {
					calls.memberID = userID
					return nil
				}

				return func(g *globalFlags, opt genericCLIOpts) *cobra.Command {
					builder := newCmdOrgBuilder(fakeOrgUrmSVCsFn(svc, urmSVC), opt)
					return builder.cmd()
				}, calls
			}

			addTests := []testCase{
				{
					name: "org id",
					memberFlags: []string{
						"--id=" + influxdb.ID(3).String(),
						"--member=" + influxdb.ID(4).String(),
					},
					envVars:  envVarsZeroMap,
					expected: called{id: 3, memberID: 4},
				},
				{
					name: "org id shorts",
					memberFlags: []string{
						"-i=" + influxdb.ID(3).String(),
						"-m=" + influxdb.ID(4).String(),
					},
					envVars:  envVarsZeroMap,
					expected: called{id: 3, memberID: 4},
				},
				{
					name: "org name",
					memberFlags: []string{
						"--name=name1",
						"--member=" + influxdb.ID(4).String(),
					},
					envVars:  envVarsZeroMap,
					expected: called{name: "name1", memberID: 4},
				},
				{
					name: "org name shorts",
					memberFlags: []string{
						"-n=name1",
						"-m=" + influxdb.ID(4).String(),
					},
					envVars:  envVarsZeroMap,
					expected: called{name: "name1", memberID: 4},
				},
			}

			testMemberFn(t, "remove", cmdFn, addTests...)
		})
	})
}

var envVarsZeroMap = map[string]string{
	"INFLUX_ORG_ID": "",
	"INFLUX_ORG":    "",
}
