// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: MPL-2.0

package auth

import (
	"testing"

	"github.com/shoenig/test/must"

	"github.com/hashicorp/nomad/ci"
	"github.com/hashicorp/nomad/helper/uuid"
	"github.com/hashicorp/nomad/nomad/mock"
	"github.com/hashicorp/nomad/nomad/state"
	"github.com/hashicorp/nomad/nomad/structs"
)

func TestBinder_Bind(t *testing.T) {
	ci.Parallel(t)

	testStore := state.TestStateStore(t)
	testBind := NewBinder(testStore)

	// create an authMethod method and insert into the state store
	authMethod := mock.ACLOIDCAuthMethod()
	must.NoError(t, testStore.UpsertACLAuthMethods(0, []*structs.ACLAuthMethod{authMethod}))

	// create some roles and insert into the state store
	targetRole := &structs.ACLRole{
		ID:   uuid.Generate(),
		Name: "vim-role",
	}
	otherRole := &structs.ACLRole{
		ID:   uuid.Generate(),
		Name: "frontend-engineers",
	}
	must.NoError(t, testStore.UpsertACLRoles(
		structs.MsgTypeTestSetup, 0, []*structs.ACLRole{targetRole, otherRole}, true,
	))

	// create binding rules and insert into the state store
	bindingRules := []*structs.ACLBindingRule{
		{
			ID:         uuid.Generate(),
			Selector:   "role==engineer",
			BindType:   structs.ACLBindingRuleBindTypeRole,
			BindName:   "${editor}-role",
			AuthMethod: authMethod.Name,
		},
		{
			ID:         uuid.Generate(),
			Selector:   "role==engineer",
			BindType:   structs.ACLBindingRuleBindTypeRole,
			BindName:   "this-role-does-not-exist",
			AuthMethod: authMethod.Name,
		},
		{
			ID:         uuid.Generate(),
			Selector:   "language==js",
			BindType:   structs.ACLBindingRuleBindTypeRole,
			BindName:   otherRole.Name,
			AuthMethod: authMethod.Name,
		},
		{
			ID:         uuid.Generate(),
			Selector:   "role==admin",
			BindType:   structs.ACLBindingRuleBindTypeManagement,
			BindName:   "",
			AuthMethod: authMethod.Name,
		},
	}
	must.NoError(t, testStore.UpsertACLBindingRules(0, bindingRules, true))

	tests := []struct {
		name       string
		authMethod *structs.ACLAuthMethod
		identity   *Identity
		want       *Bindings
		wantErr    bool
	}{
		{
			name:       "empty identity",
			authMethod: authMethod,
			identity:   &Identity{},
			want:       &Bindings{},
			wantErr:    false,
		},
		{
			name:       "role",
			authMethod: authMethod,
			identity: &Identity{
				Claims: map[string]string{
					"role":     "engineer",
					"language": "go",
				},
				ClaimMappings: map[string]string{
					"editor": "vim",
				},
			},
			want:    &Bindings{Roles: []*structs.ACLTokenRoleLink{{ID: targetRole.ID}}},
			wantErr: false,
		},
		{
			name:       "management",
			authMethod: authMethod,
			identity: &Identity{
				Claims: map[string]string{
					"role": "admin",
				},
				ClaimMappings: map[string]string{},
			},
			want:    &Bindings{Management: true},
			wantErr: false,
		},
	}
	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			got, err := testBind.Bind(tt.authMethod, tt.identity)
			if tt.wantErr {
				must.Error(t, err)
			} else {
				must.NoError(t, err)
			}
			must.Eq(t, got, tt.want)
		})
	}
}

func Test_computeBindName(t *testing.T) {
	ci.Parallel(t)
	tests := []struct {
		name          string
		bindType      string
		bindName      string
		claimMappings map[string]string
		wantName      string
		wantTrue      bool
		wantErr       bool
	}{
		{
			name:          "valid bind name and type",
			bindType:      structs.ACLBindingRuleBindTypeRole,
			bindName:      "cluster-admin",
			claimMappings: map[string]string{"cluster-admin": "root"},
			wantName:      "cluster-admin",
			wantTrue:      true,
			wantErr:       false,
		},
		{
			name:          "valid management",
			bindType:      structs.ACLBindingRuleBindTypeManagement,
			bindName:      "",
			claimMappings: map[string]string{"cluster-admin": "root"},
			wantName:      "",
			wantTrue:      true,
			wantErr:       false,
		},
		{
			name:          "invalid type",
			bindType:      "amazing",
			bindName:      "cluster-admin",
			claimMappings: map[string]string{"cluster-admin": "root"},
			wantName:      "",
			wantTrue:      false,
			wantErr:       true,
		},
	}
	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			got, got1, err := computeBindName(tt.bindType, tt.bindName, tt.claimMappings)
			if tt.wantErr {
				must.NotNil(t, err)
			}
			must.Eq(t, got, tt.wantName)
			must.Eq(t, got1, tt.wantTrue)
		})
	}
}

func Test_doesSelectorMatch(t *testing.T) {
	ci.Parallel(t)
	tests := []struct {
		name           string
		selector       string
		selectableVars interface{}
		want           bool
	}{
		{
			"catch-all",
			"",
			nil,
			true,
		},
		{
			"valid selector but no selectable vars",
			"nomad_engineering_team in Groups",
			"",
			false,
		},
		{
			"valid selector and successful evaluation",
			"nomad_engineering_team in Groups",
			map[string][]string{"Groups": {"nomad_sales_team", "nomad_engineering_team"}},
			true,
		},
	}
	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			must.Eq(t, doesSelectorMatch(tt.selector, tt.selectableVars), tt.want)
		})
	}
}
