package util

import (
	"fmt"
	"testing"

	policyengine "git.frostfs.info/TrueCloudLab/policy-engine/pkg/chain"
	nativeschema "git.frostfs.info/TrueCloudLab/policy-engine/schema/native"
	"github.com/stretchr/testify/require"
)

func TestParseAPERule(t *testing.T) {
	tests := [...]struct {
		name       string
		rule       string
		expectErr  error
		expectRule policyengine.Rule
	}{
		{
			name: "Valid allow rule for all objects",
			rule: "allow Object.Put *",
			expectRule: policyengine.Rule{
				Status:    policyengine.Allow,
				Actions:   policyengine.Actions{Names: []string{nativeschema.MethodPutObject}},
				Resources: policyengine.Resources{Names: []string{nativeschema.ResourceFormatAllObjects}},
			},
		},
		{
			name: "Valid rule for all objects in implicit root namespace",
			rule: "allow Object.Put /*",
			expectRule: policyengine.Rule{
				Status:    policyengine.Allow,
				Actions:   policyengine.Actions{Names: []string{nativeschema.MethodPutObject}},
				Resources: policyengine.Resources{Names: []string{nativeschema.ResourceFormatRootObjects}},
			},
		},
		{
			name: "Valid rule for all objects in explicit root namespace",
			rule: "allow Object.Put root/*",
			expectRule: policyengine.Rule{
				Status:    policyengine.Allow,
				Actions:   policyengine.Actions{Names: []string{nativeschema.MethodPutObject}},
				Resources: policyengine.Resources{Names: []string{nativeschema.ResourceFormatRootObjects}},
			},
		},
		{
			name: "Valid rule for all objects in root namespace and container",
			rule: "allow Object.Put /cid/*",
			expectRule: policyengine.Rule{
				Status:  policyengine.Allow,
				Actions: policyengine.Actions{Names: []string{nativeschema.MethodPutObject}},
				Resources: policyengine.Resources{Names: []string{
					fmt.Sprintf(nativeschema.ResourceFormatRootContainerObjects, "cid"),
				}},
			},
		},
		{
			name: "Valid rule for object in root namespace and container",
			rule: "allow Object.Put /cid/oid",
			expectRule: policyengine.Rule{
				Status:  policyengine.Allow,
				Actions: policyengine.Actions{Names: []string{nativeschema.MethodPutObject}},
				Resources: policyengine.Resources{Names: []string{
					fmt.Sprintf(nativeschema.ResourceFormatRootContainerObject, "cid", "oid"),
				}},
			},
		},
		{
			name: "Valid rule for all objects in namespace",
			rule: "allow Object.Put ns/*",
			expectRule: policyengine.Rule{
				Status:  policyengine.Allow,
				Actions: policyengine.Actions{Names: []string{nativeschema.MethodPutObject}},
				Resources: policyengine.Resources{Names: []string{
					fmt.Sprintf(nativeschema.ResourceFormatNamespaceObjects, "ns"),
				}},
			},
		},
		{
			name: "Valid rule for all objects in namespace and container",
			rule: "allow Object.Put ns/cid/*",
			expectRule: policyengine.Rule{
				Status:  policyengine.Allow,
				Actions: policyengine.Actions{Names: []string{nativeschema.MethodPutObject}},
				Resources: policyengine.Resources{Names: []string{
					fmt.Sprintf(nativeschema.ResourceFormatNamespaceContainerObjects, "ns", "cid"),
				}},
			},
		},
		{
			name: "Valid rule for object in namespace and container",
			rule: "allow Object.Put ns/cid/oid",
			expectRule: policyengine.Rule{
				Status:  policyengine.Allow,
				Actions: policyengine.Actions{Names: []string{nativeschema.MethodPutObject}},
				Resources: policyengine.Resources{Names: []string{
					fmt.Sprintf(nativeschema.ResourceFormatNamespaceContainerObject, "ns", "cid", "oid"),
				}},
			},
		},
		{
			name: "Valid deny rule",
			rule: "deny Object.Put *",
			expectRule: policyengine.Rule{
				Status:    policyengine.AccessDenied,
				Actions:   policyengine.Actions{Names: []string{nativeschema.MethodPutObject}},
				Resources: policyengine.Resources{Names: []string{nativeschema.ResourceFormatAllObjects}},
			},
		},
		{
			name: "Valid deny rule with action detail",
			rule: "deny:QuotaLimitReached Object.Put *",
			expectRule: policyengine.Rule{
				Status:    policyengine.QuotaLimitReached,
				Actions:   policyengine.Actions{Names: []string{nativeschema.MethodPutObject}},
				Resources: policyengine.Resources{Names: []string{nativeschema.ResourceFormatAllObjects}},
			},
		},
		{
			name: "Valid allow rule with conditions",
			rule: "allow Object.Get ResourceCondition:Department=HR RequestCondition:Actor!=ownerA *",
			expectRule: policyengine.Rule{
				Status:    policyengine.Allow,
				Actions:   policyengine.Actions{Names: []string{nativeschema.MethodGetObject}},
				Resources: policyengine.Resources{Names: []string{nativeschema.ResourceFormatAllObjects}},
				Condition: []policyengine.Condition{
					{
						Op:    policyengine.CondStringEquals,
						Kind:  policyengine.KindResource,
						Key:   "Department",
						Value: "HR",
					},
					{
						Op:    policyengine.CondStringNotEquals,
						Kind:  policyengine.KindRequest,
						Key:   "Actor",
						Value: "ownerA",
					},
				},
			},
		},
		{
			name: "Valid rule for object with conditions with action detail",
			rule: "deny:QuotaLimitReached Object.Get ResourceCondition:Department=HR RequestCondition:Actor!=ownerA *",
			expectRule: policyengine.Rule{
				Status:    policyengine.QuotaLimitReached,
				Actions:   policyengine.Actions{Names: []string{nativeschema.MethodGetObject}},
				Resources: policyengine.Resources{Names: []string{nativeschema.ResourceFormatAllObjects}},
				Condition: []policyengine.Condition{
					{
						Op:    policyengine.CondStringEquals,
						Kind:  policyengine.KindResource,
						Key:   "Department",
						Value: "HR",
					},
					{
						Op:    policyengine.CondStringNotEquals,
						Kind:  policyengine.KindRequest,
						Key:   "Actor",
						Value: "ownerA",
					},
				},
			},
		},
		{
			name:      "Invalid rule with unknown status",
			rule:      "permit Object.Put *",
			expectErr: errUnknownStatus,
		},
		{
			name:      "Invalid rule with unknown action",
			rule:      "allow Object.PutOut *",
			expectErr: errUnknownAction,
		},
		{
			name:      "Invalid rule with unknown status detail",
			rule:      "deny:UnknownActionDetail Object.Put *",
			expectErr: errUnknownStatusDetail,
		},
		{
			name:      "Invalid rule with unknown condition binary operator",
			rule:      "deny Object.Put ResourceCondition:Department<HR *",
			expectErr: errUnknownBinaryOperator,
		},
		{
			name:      "Invalid rule with unknown condition object type",
			rule:      "deny Object.Put ResourSeCondiDion:Department=HR *",
			expectErr: errUnknownCondObjectType,
		},
		{
			name:      "Invalid rule with mixed types of actions",
			rule:      "allow Object.Put Container.Put *",
			expectErr: errMixedTypesInRule,
		},
		{
			name:      "Invalid rule with no actions",
			rule:      "allow ResourceCondition:A=B  *",
			expectErr: errNoActionsInRule,
		},
		{
			name:      "Invalid rule with invalid resource for object nm/cnt/obj/err",
			rule:      "allow Object.Put nm/cnt/obj/err",
			expectErr: errUnsupportedResourceFormat,
		},
		{
			name:      "Invalid rule with invalid resource for container nm/cnt/err",
			rule:      "allow Container.Put nm/cnt/err",
			expectErr: errUnsupportedResourceFormat,
		},
		{
			name:      "Invalid rule with invalid resource for container /nm/cnt/err",
			rule:      "allow Container.Put /nm/cnt/err",
			expectErr: errUnsupportedResourceFormat,
		},
		{
			name:      "Invalid rule with invalid resource for container /nm/cnt/",
			rule:      "allow Container.Put /nm/cnt/",
			expectErr: errUnsupportedResourceFormat,
		},
		{
			name:      "Invalid rule with invalid resource for container /nm/cnt",
			rule:      "allow Container.Put /nm/cnt",
			expectErr: errUnsupportedResourceFormat,
		},
		{
			name:      "Invalid rule with invalid resource for container /nm/",
			rule:      "allow Container.Put /nm/",
			expectErr: errUnsupportedResourceFormat,
		},
		{
			name: "Valid rule for all containers",
			rule: "allow Container.Put *",
			expectRule: policyengine.Rule{
				Status:    policyengine.Allow,
				Actions:   policyengine.Actions{Names: []string{nativeschema.MethodPutContainer}},
				Resources: policyengine.Resources{Names: []string{nativeschema.ResourceFormatAllContainers}},
			},
		},
		{
			name: "Valid rule for all containers in root namespace",
			rule: "allow Container.Put /*",
			expectRule: policyengine.Rule{
				Status:    policyengine.Allow,
				Actions:   policyengine.Actions{Names: []string{nativeschema.MethodPutContainer}},
				Resources: policyengine.Resources{Names: []string{nativeschema.ResourceFormatRootContainers}},
			},
		},
		{
			name: "Valid rule for container in root namespace",
			rule: "allow Container.Put /cid",
			expectRule: policyengine.Rule{
				Status:  policyengine.Allow,
				Actions: policyengine.Actions{Names: []string{nativeschema.MethodPutContainer}},
				Resources: policyengine.Resources{Names: []string{
					fmt.Sprintf(nativeschema.ResourceFormatRootContainer, "cid"),
				}},
			},
		},
		{
			name: "Valid rule for all container in namespace",
			rule: "allow Container.Put ns/*",
			expectRule: policyengine.Rule{
				Status:  policyengine.Allow,
				Actions: policyengine.Actions{Names: []string{nativeschema.MethodPutContainer}},
				Resources: policyengine.Resources{Names: []string{
					fmt.Sprintf(nativeschema.ResourceFormatNamespaceContainers, "ns"),
				}},
			},
		},
		{
			name: "Valid rule for container in namespace",
			rule: "allow Container.Put ns/cid",
			expectRule: policyengine.Rule{
				Status:  policyengine.Allow,
				Actions: policyengine.Actions{Names: []string{nativeschema.MethodPutContainer}},
				Resources: policyengine.Resources{Names: []string{
					fmt.Sprintf(nativeschema.ResourceFormatNamespaceContainer, "ns", "cid"),
				}},
			},
		},
		{
			name: "Valid rule for container with conditions with action detail",
			rule: "allow Container.Get ResourceCondition:A=B Container.Put RequestCondition:C!=D " +
				"* /cnt_id",
			expectRule: policyengine.Rule{
				Status:  policyengine.Allow,
				Actions: policyengine.Actions{Names: []string{nativeschema.MethodGetContainer, nativeschema.MethodPutContainer}},
				Resources: policyengine.Resources{Names: []string{
					nativeschema.ResourceFormatAllContainers,
					fmt.Sprintf(nativeschema.ResourceFormatRootContainer, "cnt_id"),
				}},
				Condition: []policyengine.Condition{
					{
						Op:    policyengine.CondStringEquals,
						Kind:  policyengine.KindResource,
						Key:   "A",
						Value: "B",
					},
					{
						Op:    policyengine.CondStringNotEquals,
						Kind:  policyengine.KindRequest,
						Key:   "C",
						Value: "D",
					},
				},
			},
		},
	}
	for _, test := range tests {
		t.Run(test.name, func(t *testing.T) {
			r := new(policyengine.Rule)
			err := ParseAPERule(r, test.rule)
			require.ErrorIs(t, err, test.expectErr)
			if test.expectErr == nil {
				require.Equal(t, test.expectRule, *r)
			}
		})
	}
}