package rbac

import (
	"testing"

	"github.com/google/uuid"
	"github.com/open-policy-agent/opa/ast"
	"github.com/stretchr/testify/require"
)

// BenchmarkRBACValueAllocation benchmarks the cost of allocating a rego input
// value. By default, `ast.InterfaceToValue` is used to convert the input,
// which uses json marshaling under the hood.
//
// Currently ast.Object.insert() is the slowest part of the process and allocates
// the most amount of bytes. This general approach copies all of our struct
// data and uses a lot of extra memory for handling things like sort order.
// A possible large improvement would be to implement the ast.Value interface directly.
func BenchmarkRBACValueAllocation(b *testing.B) {
	actor := Subject{
		Roles:  RoleNames{RoleOrgMember(uuid.New()), RoleOrgAdmin(uuid.New()), RoleMember()},
		ID:     uuid.NewString(),
		Scope:  ScopeAll,
		Groups: []string{uuid.NewString(), uuid.NewString(), uuid.NewString()},
	}
	obj := ResourceTemplate.
		WithID(uuid.New()).
		InOrg(uuid.New()).
		WithOwner(uuid.NewString()).
		WithGroupACL(map[string][]Action{
			uuid.NewString(): {ActionRead, ActionCreate},
			uuid.NewString(): {ActionRead, ActionCreate},
			uuid.NewString(): {ActionRead, ActionCreate},
		}).WithACLUserList(map[string][]Action{
		uuid.NewString(): {ActionRead, ActionCreate},
		uuid.NewString(): {ActionRead, ActionCreate},
	})

	jsonSubject := authSubject{
		ID:     actor.ID,
		Roles:  must(actor.Roles.Expand()),
		Groups: actor.Groups,
		Scope:  must(actor.Scope.Expand()),
	}

	b.Run("ManualRegoValue", func(b *testing.B) {
		for i := 0; i < b.N; i++ {
			_, err := regoInputValue(actor, ActionRead, obj)
			require.NoError(b, err)
		}
	})
	b.Run("JSONRegoValue", func(b *testing.B) {
		for i := 0; i < b.N; i++ {
			_, err := ast.InterfaceToValue(map[string]interface{}{
				"subject": jsonSubject,
				"action":  ActionRead,
				"object":  obj,
			})
			require.NoError(b, err)
		}
	})
}

// TestRegoInputValue ensures the custom rego input parser returns the
// same value as the default json parser. The json parser is always correct,
// and the custom parser is used to reduce allocations. This optimization
// should yield the same results. Anything different is a bug.
func TestRegoInputValue(t *testing.T) {
	t.Parallel()

	// Expand all roles and make sure we have a good copy.
	// This is because these tests modify the roles, and we don't want to
	// modify the original roles.
	roles, err := RoleNames{RoleOrgMember(uuid.New()), RoleOrgAdmin(uuid.New()), RoleMember()}.Expand()
	require.NoError(t, err, "failed to expand roles")
	for i := range roles {
		// If all cached values are nil, then the role will not use
		// the shared cached value.
		roles[i].cachedRegoValue = nil
	}

	actor := Subject{
		Roles:  Roles(roles),
		ID:     uuid.NewString(),
		Scope:  ScopeAll,
		Groups: []string{uuid.NewString(), uuid.NewString(), uuid.NewString()},
	}

	obj := ResourceTemplate.
		WithID(uuid.New()).
		InOrg(uuid.New()).
		WithOwner(uuid.NewString()).
		WithGroupACL(map[string][]Action{
			uuid.NewString(): {ActionRead, ActionCreate},
			uuid.NewString(): {ActionRead, ActionCreate},
			uuid.NewString(): {ActionRead, ActionCreate},
		}).WithACLUserList(map[string][]Action{
		uuid.NewString(): {ActionRead, ActionCreate},
		uuid.NewString(): {ActionRead, ActionCreate},
	})

	action := ActionRead

	t.Run("InputValue", func(t *testing.T) {
		t.Parallel()

		// This is the input that would be passed to the rego policy.
		jsonInput := map[string]interface{}{
			"subject": authSubject{
				ID:     actor.ID,
				Roles:  must(actor.Roles.Expand()),
				Groups: actor.Groups,
				Scope:  must(actor.Scope.Expand()),
			},
			"action": action,
			"object": obj,
		}

		manual, err := regoInputValue(actor, action, obj)
		require.NoError(t, err)

		general, err := ast.InterfaceToValue(jsonInput)
		require.NoError(t, err)

		// The custom parser does not set these fields because they are not needed.
		// To ensure the outputs are identical, intentionally overwrite all names
		// to the same values.
		ignoreNames(t, manual)
		ignoreNames(t, general)

		cmp := manual.Compare(general)
		require.Equal(t, 0, cmp, "manual and general input values should be equal")
	})

	t.Run("PartialInputValue", func(t *testing.T) {
		t.Parallel()

		// This is the input that would be passed to the rego policy.
		jsonInput := map[string]interface{}{
			"subject": authSubject{
				ID:     actor.ID,
				Roles:  must(actor.Roles.Expand()),
				Groups: actor.Groups,
				Scope:  must(actor.Scope.Expand()),
			},
			"action": action,
			"object": map[string]interface{}{
				"type": obj.Type,
			},
		}

		manual, err := regoPartialInputValue(actor, action, obj.Type)
		require.NoError(t, err)

		general, err := ast.InterfaceToValue(jsonInput)
		require.NoError(t, err)

		// The custom parser does not set these fields because they are not needed.
		// To ensure the outputs are identical, intentionally overwrite all names
		// to the same values.
		ignoreNames(t, manual)
		ignoreNames(t, general)

		cmp := manual.Compare(general)
		require.Equal(t, 0, cmp, "manual and general input values should be equal")
	})
}

// ignoreNames sets all names to "ignore" to ensure the values are identical.
func ignoreNames(t *testing.T, value ast.Value) {
	t.Helper()

	// Override the names of the roles
	ref := ast.Ref{
		ast.StringTerm("subject"),
		ast.StringTerm("roles"),
	}
	roles, err := value.Find(ref)
	require.NoError(t, err)

	rolesArray, ok := roles.(*ast.Array)
	require.True(t, ok, "roles is expected to be an array")

	rolesArray.Foreach(func(term *ast.Term) {
		obj, _ := term.Value.(ast.Object)
		// Ignore all names
		obj.Insert(ast.StringTerm("name"), ast.StringTerm("ignore"))
		obj.Insert(ast.StringTerm("display_name"), ast.StringTerm("ignore"))
	})

	// Override the names of the scope role
	ref = ast.Ref{
		ast.StringTerm("subject"),
		ast.StringTerm("scope"),
	}
	scope, err := value.Find(ref)
	require.NoError(t, err)

	scopeObj, ok := scope.(ast.Object)
	require.True(t, ok, "scope is expected to be an object")

	scopeObj.Insert(ast.StringTerm("name"), ast.StringTerm("ignore"))
	scopeObj.Insert(ast.StringTerm("display_name"), ast.StringTerm("ignore"))
}

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

	t.Run("BuiltIns", func(t *testing.T) {
		t.Parallel()
		testCases := []struct {
			Role Role
		}{
			{Role: builtInRoles[owner]("")},
			{Role: builtInRoles[member]("")},
			{Role: builtInRoles[templateAdmin]("")},
			{Role: builtInRoles[userAdmin]("")},
			{Role: builtInRoles[auditor]("")},

			{Role: builtInRoles[orgAdmin]("4592dac5-0945-42fd-828d-a903957d3dbb")},
			{Role: builtInRoles[orgAdmin]("24c100c5-1920-49c0-8c38-1b640ac4b38c")},
			{Role: builtInRoles[orgAdmin]("4a00f697-0040-4079-b3ce-d24470281a62")},

			{Role: builtInRoles[orgMember]("3293c50e-fa5d-414f-a461-01112a4dfb6f")},
			{Role: builtInRoles[orgMember]("f88dd23d-bdbd-469d-b82e-36ee06c3d1e1")},
			{Role: builtInRoles[orgMember]("02cfd2a5-016c-4d8d-8290-301f5f18023d")},
		}

		for _, c := range testCases {
			c := c
			t.Run(c.Role.Name, func(t *testing.T) {
				role, err := RoleByName(c.Role.Name)
				require.NoError(t, err, "role exists")
				equalRoles(t, c.Role, role)
			})
		}
	})

	// nolint:paralleltest
	t.Run("Errors", func(t *testing.T) {
		var err error

		_, err = RoleByName("")
		require.Error(t, err, "empty role")

		_, err = RoleByName("too:many:colons")
		require.Error(t, err, "too many colons")

		_, err = RoleByName(orgMember)
		require.Error(t, err, "expect orgID")
	})
}

// SameAs compares 2 roles for equality.
func equalRoles(t *testing.T, a, b Role) {
	require.Equal(t, a.Name, b.Name, "role names")
	require.Equal(t, a.DisplayName, b.DisplayName, "role display names")
	require.ElementsMatch(t, a.Site, b.Site, "site permissions")
	require.ElementsMatch(t, a.User, b.User, "user permissions")
	require.Equal(t, len(a.Org), len(b.Org), "same number of org roles")

	for ak, av := range a.Org {
		bv, ok := b.Org[ak]
		require.True(t, ok, "org permissions missing: %s", ak)
		require.ElementsMatchf(t, av, bv, "org %s permissions", ak)
	}
}
