package perms

import (
	"strings"
	"testing"

	"github.com/hashicorp/boundary/internal/types/action"
	"github.com/hashicorp/boundary/internal/types/resource"
	"github.com/hashicorp/boundary/internal/types/scope"
	"github.com/stretchr/testify/assert"
	"github.com/stretchr/testify/require"
)

func Test_ActionParsingValidation(t *testing.T) {
	t.Parallel()

	type input struct {
		name      string
		input     Grant
		errResult string
		result    Grant
	}

	tests := []input{
		{
			name:      "no actions",
			errResult: "no actions specified",
		},
		{
			name: "empty action",
			input: Grant{
				actionsBeingParsed: []string{"create", "", "read"},
			},
			errResult: "empty action found",
		},
		{
			name: "unknown action",
			input: Grant{
				actionsBeingParsed: []string{"create", "foobar", "read"},
			},
			errResult: `unknown action "foobar"`,
		},
		{
			name: "all",
			input: Grant{
				actionsBeingParsed: []string{"*"},
			},
			result: Grant{
				actions: map[action.Type]bool{
					action.All: true,
				},
			},
		},
		{
			name: "all valid plus all",
			input: Grant{
				actionsBeingParsed: []string{"list", "create", "update", "*", "read", "delete", "authenticate", "authorize"},
			},
			errResult: `"*" cannot be specified with other actions`,
		},
		{
			name: "all valid",
			input: Grant{
				actionsBeingParsed: []string{"list", "create", "update", "read", "delete", "authenticate", "authorize"},
			},
			result: Grant{
				actions: map[action.Type]bool{
					action.List:         true,
					action.Create:       true,
					action.Update:       true,
					action.Read:         true,
					action.Delete:       true,
					action.Authenticate: true,
					action.Authorize:    true,
				},
			},
		},
	}

	for _, test := range tests {
		t.Run(test.name, func(t *testing.T) {
			err := test.input.parseAndValidateActions()
			if test.errResult == "" {
				require.NoError(t, err)
				assert.Equal(t, test.result, test.input)
			} else {
				require.Error(t, err)
				assert.Equal(t, test.errResult, err.Error())
			}
		})
	}
}

func Test_ValidateType(t *testing.T) {
	t.Parallel()

	type input struct {
		name      string
		input     Grant
		errResult string
	}

	tests := []input{
		{
			name: "no specifier",
		},
		{
			name: "valid specifier",
			input: Grant{
				typ: resource.HostCatalog,
			},
		},
	}

	for _, test := range tests {
		t.Run(test.name, func(t *testing.T) {
			err := test.input.validateType()
			if test.errResult == "" {
				require.NoError(t, err)
			} else {
				require.Error(t, err)
				assert.Equal(t, test.errResult, err.Error())
			}
		})
	}
}

func Test_MarshalingAndCloning(t *testing.T) {
	t.Parallel()

	type input struct {
		name            string
		input           Grant
		jsonOutput      string
		canonicalString string
	}

	tests := []input{
		{
			name: "empty",
			input: Grant{
				scope: Scope{
					Type: scope.Org,
				},
			},
			jsonOutput:      `{}`,
			canonicalString: ``,
		},
		{
			name: "type and id",
			input: Grant{
				id: "baz",
				scope: Scope{
					Type: scope.Project,
				},
				typ: resource.Group,
			},
			jsonOutput:      `{"id":"baz","type":"group"}`,
			canonicalString: `id=baz;type=group`,
		},
		{
			name: "everything",
			input: Grant{
				id: "baz",
				scope: Scope{
					Type: scope.Project,
				},
				typ: resource.Group,
				actions: map[action.Type]bool{
					action.Create: true,
					action.Read:   true,
				},
				actionsBeingParsed: []string{"create", "read"},
			},
			jsonOutput:      `{"actions":["create","read"],"id":"baz","type":"group"}`,
			canonicalString: `id=baz;type=group;actions=create,read`,
		},
	}

	for _, test := range tests {
		t.Run(test.name, func(t *testing.T) {
			output, err := test.input.MarshalJSON()
			require.NoError(t, err)
			assert.Equal(t, test.jsonOutput, string(output))
			assert.Equal(t, test.canonicalString, test.input.CanonicalString())
			assert.Equal(t, &test.input, test.input.clone())
		})
	}
}

func Test_Unmarshaling(t *testing.T) {
	t.Parallel()

	type input struct {
		name      string
		jsonInput string
		textInput string
		jsonErr   string
		textErr   string
		expected  Grant
	}

	tests := []input{
		{
			name:      "empty",
			expected:  Grant{},
			jsonInput: `{}`,
			textInput: ``,
			textErr:   `segment "" not formatted correctly, wrong number of equal signs`,
		},
		{
			name:      "bad json",
			jsonInput: `w329uf`,
			jsonErr:   "invalid character 'w' looking for beginning of value",
		},
		{
			name:      "bad segment",
			jsonInput: `{"id":true}`,
			jsonErr:   `unable to interpret "id" as string`,
			textInput: `id=`,
			textErr:   `segment "id=" not formatted correctly, missing value`,
		},
		{
			name: "good id",
			expected: Grant{
				id: "foobar",
			},
			jsonInput: `{"id":"foobar"}`,
			textInput: `id=foobar`,
		},
		{
			name:      "bad id",
			jsonInput: `{"id":true}`,
			jsonErr:   `unable to interpret "id" as string`,
			textInput: `=id`,
			textErr:   `segment "=id" not formatted correctly, missing key`,
		},
		{
			name: "good type",
			expected: Grant{
				typ: resource.HostCatalog,
			},
			jsonInput: `{"type":"host-catalog"}`,
			textInput: `type=host-catalog`,
		},
		{
			name:      "bad type",
			jsonInput: `{"type":true}`,
			jsonErr:   `unable to interpret "type" as string`,
			textInput: `type=host-catalog=id`,
			textErr:   `segment "type=host-catalog=id" not formatted correctly, wrong number of equal signs`,
		},
		{
			name: "good actions",
			expected: Grant{
				actionsBeingParsed: []string{"create", "read"},
			},
			jsonInput: `{"actions":["create","read"]}`,
			textInput: `actions=create,read`,
		},
		{
			name:      "bad actions",
			jsonInput: `{"actions":true}`,
			jsonErr:   `unable to interpret "actions" as array`,
			textInput: `type=host-catalog=id`,
			textErr:   `segment "type=host-catalog=id" not formatted correctly, wrong number of equal signs`,
		},
		{
			name:      "empty actions",
			jsonInput: `{"actions":[""]}`,
			jsonErr:   `empty action found`,
			textInput: `actions=,`,
			textErr:   `empty action found`,
		},
		{
			name:      "bad json action",
			jsonInput: `{"actions":[1, true]}`,
			jsonErr:   `unable to interpret 1 in actions array as string`,
		},
	}

	for _, test := range tests {
		t.Run(test.name, func(t *testing.T) {
			assert := assert.New(t)
			require := require.New(t)
			var g Grant
			if test.jsonInput != "" {
				err := g.unmarshalJSON([]byte(test.jsonInput))
				if test.jsonErr != "" {
					require.Error(err)
					assert.Equal(test.jsonErr, err.Error())
				} else {
					require.NoError(err)
					assert.Equal(test.expected, g)
				}
			}
			g = Grant{}
			if test.textInput != "" {
				err := g.unmarshalText(test.textInput)
				if test.textErr != "" {
					require.Error(err)
					assert.Equal(test.textErr, err.Error())
				} else {
					require.NoError(err)
					assert.Equal(test.expected, g)
				}
			}
		})
	}
}

func Test_Parse(t *testing.T) {
	t.Parallel()

	type input struct {
		name          string
		input         string
		userId        string
		err           string
		scopeOverride string
		expected      Grant
	}

	tests := []input{
		{
			name: "empty",
			err:  `grant string is empty`,
		},
		{
			name:  "bad json",
			input: "{2:193}",
			err:   `unable to parse JSON grant string:`,
		},
		{
			name:  "bad text",
			input: "id=foo=bar",
			err:   `unable to parse grant string:`,
		},
		{
			name:  "bad type",
			input: "id=foobar;type=barfoo;actions=create,read",
			err:   `unknown type specifier "barfoo"`,
		},
		{
			name:  "bad actions",
			input: "id=foobar;type=host-catalog;actions=createread",
			err:   `unknown action "createread"`,
		},
		{
			name:  "empty id and type",
			input: "actions=create",
			err:   `parsed grant string would not result in any action being authorized`,
		},
		{
			name:  "good json type",
			input: `{"type":"host-catalog","actions":["create"]}`,
			expected: Grant{
				scope: Scope{
					Id:   "o_scope",
					Type: scope.Org,
				},
				typ: resource.HostCatalog,
				actions: map[action.Type]bool{
					action.Create: true,
				},
			},
		},
		{
			name:  "good json id",
			input: `{"id":"foobar","actions":["read"]}`,
			expected: Grant{
				scope: Scope{
					Id:   "o_scope",
					Type: scope.Org,
				},
				id:  "foobar",
				typ: resource.Unknown,
				actions: map[action.Type]bool{
					action.Read: true,
				},
			},
		},
		{
			name:  "good text type",
			input: `type=host-catalog;actions=create`,
			expected: Grant{
				scope: Scope{
					Id:   "o_scope",
					Type: scope.Org,
				},
				typ: resource.HostCatalog,
				actions: map[action.Type]bool{
					action.Create: true,
				},
			},
		},
		{
			name:  "good text id",
			input: `id=foobar;actions=read`,
			expected: Grant{
				scope: Scope{
					Id:   "o_scope",
					Type: scope.Org,
				},
				id:  "foobar",
				typ: resource.Unknown,
				actions: map[action.Type]bool{
					action.Read: true,
				},
			},
		},
		{
			name:          "default project scope",
			input:         `id=foobar;actions=read`,
			scopeOverride: "p_1234",
			expected: Grant{
				scope: Scope{
					Id:   "p_1234",
					Type: scope.Project,
				},
				id:  "foobar",
				typ: resource.Unknown,
				actions: map[action.Type]bool{
					action.Read: true,
				},
			},
		},
		{
			name:          "default org scope",
			input:         `id=foobar;actions=read`,
			scopeOverride: "o_1234",
			expected: Grant{
				scope: Scope{
					Id:   "o_1234",
					Type: scope.Org,
				},
				id:  "foobar",
				typ: resource.Unknown,
				actions: map[action.Type]bool{
					action.Read: true,
				},
			},
		},
		{
			name:          "default global scope",
			input:         `id=foobar;actions=read`,
			scopeOverride: "global",
			expected: Grant{
				scope: Scope{
					Id:   "global",
					Type: scope.Global,
				},
				id:  "foobar",
				typ: resource.Unknown,
				actions: map[action.Type]bool{
					action.Read: true,
				},
			},
		},
		{
			name:   "bad user id template",
			input:  `id={{superman}};actions=create,read`,
			userId: "u_abcd1234",
			err:    `unknown template "{{superman}}" in grant "id" value`,
		},
		{
			name:   "good user id template",
			input:  `id={{    user.id}};actions=create,read`,
			userId: "u_abcd1234",
			expected: Grant{
				scope: Scope{
					Id:   "o_scope",
					Type: scope.Org,
				},
				id: "u_abcd1234",
				actions: map[action.Type]bool{
					action.Create: true,
					action.Read:   true,
				},
			},
		},
	}

	_, err := Parse("", "", "", false)
	require.Error(t, err)
	assert.Equal(t, "grant string is empty", err.Error())

	_, err = Parse("", "", "{}", false)
	require.Error(t, err)
	assert.Equal(t, "no scope ID provided", err.Error())

	for _, test := range tests {
		t.Run(test.name, func(t *testing.T) {
			assert := assert.New(t)
			require := require.New(t)
			scope := "o_scope"
			if test.scopeOverride != "" {
				scope = test.scopeOverride
			}
			grant, err := Parse(scope, test.userId, test.input, false)
			if test.err != "" {
				require.Error(err)
				assert.True(strings.Contains(err.Error(), test.err), err.Error())
			} else {
				require.NoError(err)
				assert.Equal(test.expected, grant)
			}
		})
	}
}
