package chain

import (
	"fmt"
	"strings"
	"testing"

	"git.frostfs.info/TrueCloudLab/policy-engine/pkg/resource/testutil"
	"git.frostfs.info/TrueCloudLab/policy-engine/schema/common"
	"git.frostfs.info/TrueCloudLab/policy-engine/schema/native"
	"git.frostfs.info/TrueCloudLab/policy-engine/schema/s3"
	"github.com/stretchr/testify/require"
)

func TestChainIDSerialization(t *testing.T) {
	chainIDBytes := []byte{93, 236, 80, 138, 168, 3, 144, 92, 173, 141, 16, 42, 249, 90, 97, 109, 211, 169, 54, 163}

	chain1 := &Chain{ID: ID(chainIDBytes)}
	data := chain1.Bytes()

	var chain2 Chain
	err := chain2.DecodeBytes(data)
	require.NoError(t, err)

	require.Equal(t, chain1.ID, chain2.ID)
}

func TestEncodeDecode(t *testing.T) {
	expected := Chain{
		MatchType: MatchTypeFirstMatch,
		Rules: []Rule{
			{
				Status: Allow,
				Actions: Actions{Names: []string{
					"native::PutObject",
				}},
				Resources: Resources{Names: []string{"*"}},
				Condition: []Condition{
					{
						Op:    CondStringEquals,
						Key:   "Name",
						Value: "NNS",
					},
				},
			},
		},
	}
	data := expected.Bytes()

	var actual Chain
	require.NoError(t, actual.DecodeBytes(data))
	require.Equal(t, expected, actual)
}

func TestChainMatch(t *testing.T) {
	ch := Chain{
		Rules: []Rule{
			{
				Status: Allow,
				Actions: Actions{Names: []string{
					native.MethodPutObject,
				}},
				Resources: Resources{Names: []string{native.ResourceFormatRootContainers}},
				Condition: []Condition{},
			},
			{
				Status: AccessDenied,
				Actions: Actions{Names: []string{
					native.MethodPutObject,
				}},
				Resources: Resources{Names: []string{native.ResourceFormatRootContainers}},
				Condition: []Condition{},
			},
		},
	}

	resource := testutil.NewResource(native.ResourceFormatRootContainers, nil)
	request := testutil.NewRequest(native.MethodPutObject, resource, nil)

	t.Run("default match", func(t *testing.T) {
		st, found := ch.Match(request)
		require.True(t, found)
		require.Equal(t, AccessDenied, st)
	})

	t.Run("return first match", func(t *testing.T) {
		ch.MatchType = MatchTypeFirstMatch
		st, found := ch.Match(request)
		require.True(t, found)
		require.Equal(t, Allow, st)
	})

	t.Run("unknown match", func(t *testing.T) {
		ch.MatchType = MatchType(255)
		request := testutil.NewRequest(native.MethodGetObject, resource, nil)
		require.PanicsWithValue(t, "unknown MatchType 255", func() {
			ch.Match(request)
		})
	})

	t.Run("no rule found", func(t *testing.T) {
		ch.MatchType = MatchTypeFirstMatch
		request := testutil.NewRequest(native.MethodGetObject, resource, nil)
		st, found := ch.Match(request)
		require.False(t, found)
		require.Equal(t, NoRuleFound, st)
	})
}

func TestAnyAllConditionMatch(t *testing.T) {
	ch := Chain{
		Rules: []Rule{
			{
				Status:    Allow,
				Actions:   Actions{Names: []string{native.MethodPutObject}},
				Resources: Resources{Names: []string{native.ResourceFormatRootContainers}},
				Condition: []Condition{
					{
						Op:    CondIPAddress,
						Kind:  KindRequest,
						Key:   common.PropertyKeyFrostFSSourceIP,
						Value: "192.92.1.1/20",
					},
					{
						Op:    CondStringEquals,
						Kind:  KindRequest,
						Key:   native.PropertyKeyActorRole,
						Value: "owner",
					},
					{
						Op:    CondStringEquals,
						Kind:  KindResource,
						Key:   native.PropertyKeyObjectID,
						Value: "79xGoKYwhyJQhrDNb7bHhY1WCvN6trHJPTjKkw24c6W9",
					},
				},
			},
		}}

	t.Run("match by all conditions", func(t *testing.T) {
		ch.Rules[0].Any = false

		resource := testutil.NewResource(native.ResourceFormatRootContainers, map[string]string{
			native.PropertyKeyObjectID: "79xGoKYwhyJQhrDNb7bHhY1WCvN6trHJPTjKkw24c6W9",
		})
		request := testutil.NewRequest(native.MethodPutObject, resource, map[string]string{
			common.PropertyKeyFrostFSSourceIP: "192.92.1.91",
			native.PropertyKeyActorRole:       "owner",
		})

		st, found := ch.Match(request)
		require.True(t, found)
		require.Equal(t, Allow, st)

		request = testutil.NewRequest(native.MethodPutObject, resource, map[string]string{
			common.PropertyKeyFrostFSSourceIP: "192.93.1.91",
		})

		st, found = ch.Match(request)
		require.False(t, found)
		require.Equal(t, NoRuleFound, st)
	})

	t.Run("match by any condition", func(t *testing.T) {
		ch.Rules[0].Any = true

		resource := testutil.NewResource(native.ResourceFormatRootContainers, map[string]string{
			native.PropertyKeyObjectID: "79xGoKYwhyJQhrDNb7bHhY1WCvN6trHJPTjKkw24c6W9",
		})
		request := testutil.NewRequest(native.MethodPutObject, resource, nil)

		st, found := ch.Match(request)
		require.True(t, found)
		require.Equal(t, Allow, st)

		request = testutil.NewRequest(native.MethodPutObject, resource, map[string]string{
			native.PropertyKeyActorRole: "owner",
		})

		st, found = ch.Match(request)
		require.True(t, found)
		require.Equal(t, Allow, st)

		resource = testutil.NewResource(native.ResourceFormatRootContainers, map[string]string{})
		request = testutil.NewRequest(native.MethodPutObject, resource, map[string]string{
			common.PropertyKeyFrostFSSourceIP: "192.92.1.91",
		})

		st, found = ch.Match(request)
		require.True(t, found)
		require.Equal(t, Allow, st)

		request = testutil.NewRequest(native.MethodPutObject, resource, map[string]string{
			common.PropertyKeyFrostFSSourceIP: "192.93.1.91",
		})

		st, found = ch.Match(request)
		require.False(t, found)
		require.Equal(t, NoRuleFound, st)
	})
}

func TestConditionMatch(t *testing.T) {
	t.Run("condition types", func(t *testing.T) {
		t.Run("slice condition type", testCondSliceContainsMatch)
		t.Run("numeric condition types", testNumericConditionsMatch)
		t.Run("string condition types", testStringConiditionsMatch)
		t.Run("ip conidition types", testIPConditionMatch)
		t.Run("unknown condition type", func(t *testing.T) {
			ch := Chain{Rules: []Rule{{
				Status:    Allow,
				Actions:   Actions{Names: []string{native.MethodPutObject}},
				Resources: Resources{Names: []string{native.ResourceFormatRootContainers}},
				Condition: []Condition{{
					Op:    ConditionType(255),
					Kind:  KindRequest,
					Key:   common.PropertyKeyFrostFSSourceIP,
					Value: "192.92.1.1/20",
				}},
			}}}
			resource := testutil.NewResource(native.ResourceFormatRootContainers, map[string]string{
				native.PropertyKeyObjectID: "79xGoKYwhyJQhrDNb7bHhY1WCvN6trHJPTjKkw24c6W9",
			})
			request := testutil.NewRequest(native.MethodPutObject, resource, map[string]string{
				common.PropertyKeyFrostFSSourceIP: "192.92.1.91",
				native.PropertyKeyActorRole:       "owner",
			})
			require.PanicsWithValue(t, "unimplemented: 255", func() {
				ch.Match(request)
			})
		})
	})

	t.Run("kind", func(t *testing.T) {
		resource := testutil.NewResource(native.ResourceFormatRootContainers, map[string]string{
			native.PropertyKeyObjectID: "79xGoKYwhyJQhrDNb7bHhY1WCvN6trHJPTjKkw24c6W9",
		})
		request := testutil.NewRequest(native.MethodPutObject, resource, map[string]string{
			common.PropertyKeyFrostFSSourceIP: "192.92.1.91",
			native.PropertyKeyActorRole:       "owner",
		})

		t.Run("resource", func(t *testing.T) {
			cond := Condition{
				Op:    CondStringEquals,
				Kind:  KindResource,
				Key:   native.PropertyKeyObjectID,
				Value: "79xGoKYwhyJQhrDNb7bHhY1WCvN6trHJPTjKkw24c6W9",
			}

			found := cond.Match(request)
			require.True(t, found)
		})
		t.Run("request", func(t *testing.T) {
			cond := Condition{
				Op:    CondStringEquals,
				Kind:  KindRequest,
				Key:   native.PropertyKeyActorRole,
				Value: "owner",
			}

			found := cond.Match(request)
			require.True(t, found)
		})
		t.Run("unknown", func(t *testing.T) {
			cond := Condition{
				Op:    CondStringEquals,
				Kind:  ConditionKindType(255),
				Key:   native.PropertyKeyActorRole,
				Value: "owner",
			}

			require.PanicsWithValue(t, "unknown condition type: 255", func() {
				cond.Match(request)
			})
		})
	})
}

func testCondSliceContainsMatch(t *testing.T) {
	propKey := common.PropertyKeyFrostFSIDGroupID
	groupID := "1"

	ch := Chain{Rules: []Rule{{
		Status:    Allow,
		Actions:   Actions{Names: []string{native.MethodPutObject}},
		Resources: Resources{Names: []string{native.ResourceFormatRootContainers}},
		Condition: []Condition{{
			Op:    CondSliceContains,
			Kind:  KindRequest,
			Key:   propKey,
			Value: groupID,
		}},
	}}}

	for _, tc := range []struct {
		name   string
		value  string
		status Status
	}{
		{
			name:   "simple value",
			value:  groupID,
			status: Allow,
		},
		{
			name:   "simple value by func",
			value:  FormCondSliceContainsValue([]string{groupID}),
			status: Allow,
		},
		{
			name:   "multiple values by func",
			value:  FormCondSliceContainsValue([]string{groupID, "2", "3"}),
			status: Allow,
		},
		{
			name:   "simple mismatched",
			value:  "3",
			status: NoRuleFound,
		},
		{
			name:   "multiple mismatched",
			value:  FormCondSliceContainsValue([]string{"11", "12"}),
			status: NoRuleFound,
		},
		{
			name:   "comma correct handling mismatched",
			value:  "1,11",
			status: NoRuleFound,
		},
	} {
		t.Run(tc.name, func(t *testing.T) {
			resource := testutil.NewResource(native.ResourceFormatRootContainers, nil)
			request := testutil.NewRequest(native.MethodPutObject, resource, map[string]string{propKey: tc.value})

			st, _ := ch.Match(request)
			require.Equal(t, tc.status.String(), st.String())
		})
	}
}

func testNumericConditionsMatch(t *testing.T) {
	propKey := s3.PropertyKeyMaxKeys

	for _, tc := range []struct {
		name       string
		conditions []Condition
		value      string
		status     Status
	}{
		{
			name: "value from interval",
			conditions: []Condition{
				{
					Op:    CondNumericLessThan,
					Kind:  KindRequest,
					Key:   propKey,
					Value: "100",
				},
				{
					Op:    CondNumericGreaterThan,
					Kind:  KindRequest,
					Key:   propKey,
					Value: "80",
				},
				{
					Op:    CondNumericNotEquals,
					Kind:  KindRequest,
					Key:   propKey,
					Value: "91",
				},
			},
			value:  "90",
			status: Allow,
		},
		{
			name: "border value",
			conditions: []Condition{
				{
					Op:    CondNumericEquals,
					Kind:  KindRequest,
					Key:   propKey,
					Value: "50",
				},
				{
					Op:    CondNumericLessThanEquals,
					Kind:  KindRequest,
					Key:   propKey,
					Value: "50",
				},
				{
					Op:    CondNumericGreaterThanEquals,
					Kind:  KindRequest,
					Key:   propKey,
					Value: "50",
				},
			},
			value:  "50",
			status: Allow,
		},
	} {
		t.Run(tc.name, func(t *testing.T) {
			resource := testutil.NewResource(native.ResourceFormatRootContainers, nil)
			request := testutil.NewRequest(native.MethodPutObject, resource, map[string]string{propKey: tc.value})

			ch := Chain{Rules: []Rule{{
				Status:    Allow,
				Actions:   Actions{Names: []string{native.MethodPutObject}},
				Resources: Resources{Names: []string{native.ResourceFormatRootContainers}},
				Condition: tc.conditions,
			}}}
			st, _ := ch.Match(request)
			require.Equal(t, tc.status.String(), st.String())
		})
	}
}

func testStringConiditionsMatch(t *testing.T) {
	propKey := fmt.Sprintf(common.PropertyKeyFormatFrostFSIDUserClaim, "some-tag")
	val := "tag-value"

	t.Run(CondStringEquals.String(), func(t *testing.T) {
		ch := Chain{Rules: []Rule{{
			Status:    Allow,
			Actions:   Actions{Names: []string{native.MethodPutObject}},
			Resources: Resources{Names: []string{native.ResourceFormatRootContainers}},
			Condition: []Condition{{
				Op:    CondStringEquals,
				Kind:  KindRequest,
				Key:   propKey,
				Value: val,
			}},
		}}}

		resource := testutil.NewResource(native.ResourceFormatRootContainers, nil)
		request := testutil.NewRequest(native.MethodPutObject, resource, map[string]string{
			propKey: val,
		})

		st, found := ch.Match(request)
		require.True(t, found)
		require.Equal(t, Allow, st)

		request = testutil.NewRequest(native.MethodPutObject, resource, map[string]string{
			propKey: "distort_tag_value" + val,
		})

		st, found = ch.Match(request)
		require.False(t, found)
		require.Equal(t, NoRuleFound, st)
	})

	t.Run(CondStringNotEquals.String(), func(t *testing.T) {
		ch := Chain{Rules: []Rule{{
			Status:    Allow,
			Actions:   Actions{Names: []string{native.MethodPutObject}},
			Resources: Resources{Names: []string{native.ResourceFormatRootContainers}},
			Condition: []Condition{{
				Op:    CondStringNotEquals,
				Kind:  KindRequest,
				Key:   propKey,
				Value: val,
			}},
		}}}

		resource := testutil.NewResource(native.ResourceFormatRootContainers, nil)
		request := testutil.NewRequest(native.MethodPutObject, resource, map[string]string{
			propKey: "distort_tag_value" + val,
		})

		st, found := ch.Match(request)
		require.True(t, found)
		require.Equal(t, Allow, st)

		request = testutil.NewRequest(native.MethodPutObject, resource, map[string]string{
			propKey: val,
		})

		st, found = ch.Match(request)
		require.False(t, found)
		require.Equal(t, NoRuleFound, st)
	})

	t.Run(CondStringEqualsIgnoreCase.String(), func(t *testing.T) {
		ch := Chain{Rules: []Rule{{
			Status:    Allow,
			Actions:   Actions{Names: []string{native.MethodPutObject}},
			Resources: Resources{Names: []string{native.ResourceFormatRootContainers}},
			Condition: []Condition{{
				Op:    CondStringEqualsIgnoreCase,
				Kind:  KindRequest,
				Key:   propKey,
				Value: val,
			}},
		}}}

		resource := testutil.NewResource(native.ResourceFormatRootContainers, nil)
		request := testutil.NewRequest(native.MethodPutObject, resource, map[string]string{
			propKey: strings.ToUpper(val),
		})

		st, found := ch.Match(request)
		require.True(t, found)
		require.Equal(t, Allow, st)

		request = testutil.NewRequest(native.MethodPutObject, resource, map[string]string{
			propKey: strings.ToUpper("distort_tag_value" + val),
		})

		st, found = ch.Match(request)
		require.False(t, found)
		require.Equal(t, NoRuleFound, st)
	})

	t.Run(CondStringNotEqualsIgnoreCase.String(), func(t *testing.T) {
		ch := Chain{Rules: []Rule{{
			Status:    Allow,
			Actions:   Actions{Names: []string{native.MethodPutObject}},
			Resources: Resources{Names: []string{native.ResourceFormatRootContainers}},
			Condition: []Condition{{
				Op:    CondStringNotEqualsIgnoreCase,
				Kind:  KindRequest,
				Key:   propKey,
				Value: val,
			}},
		}}}

		resource := testutil.NewResource(native.ResourceFormatRootContainers, nil)
		request := testutil.NewRequest(native.MethodPutObject, resource, map[string]string{
			propKey: strings.ToUpper("distort_tag_value" + val),
		})

		st, found := ch.Match(request)
		require.True(t, found)
		require.Equal(t, Allow, st)

		request = testutil.NewRequest(native.MethodPutObject, resource, map[string]string{
			propKey: strings.ToUpper(val),
		})

		st, found = ch.Match(request)
		require.False(t, found)
		require.Equal(t, NoRuleFound, st)
	})

	t.Run(CondStringLike.String(), func(t *testing.T) {
		ch := Chain{Rules: []Rule{{
			Status:    Allow,
			Actions:   Actions{Names: []string{native.MethodPutObject}},
			Resources: Resources{Names: []string{native.ResourceFormatRootContainers}},
			Condition: []Condition{{
				Op:    CondStringLike,
				Kind:  KindRequest,
				Key:   propKey,
				Value: val + "*",
			}},
		}}}

		resource := testutil.NewResource(native.ResourceFormatRootContainers, nil)
		request := testutil.NewRequest(native.MethodPutObject, resource, map[string]string{
			propKey: val + "suffix",
		})

		st, found := ch.Match(request)
		require.True(t, found)
		require.Equal(t, Allow, st)

		request = testutil.NewRequest(native.MethodPutObject, resource, map[string]string{
			propKey: string([]byte(val)[:len(val)-1]), //cut last letter
		})

		st, found = ch.Match(request)
		require.False(t, found)
		require.Equal(t, NoRuleFound, st)
	})

	t.Run(CondStringNotLike.String(), func(t *testing.T) {
		ch := Chain{Rules: []Rule{{
			Status:    Allow,
			Actions:   Actions{Names: []string{native.MethodPutObject}},
			Resources: Resources{Names: []string{native.ResourceFormatRootContainers}},
			Condition: []Condition{{
				Op:    CondStringNotLike,
				Kind:  KindRequest,
				Key:   propKey,
				Value: "prefix" + val + "*",
			}},
		}}}

		resource := testutil.NewResource(native.ResourceFormatRootContainers, nil)
		request := testutil.NewRequest(native.MethodPutObject, resource, map[string]string{
			propKey: val + "suffix",
		})

		st, found := ch.Match(request)
		require.True(t, found)
		require.Equal(t, Allow, st)

		request = testutil.NewRequest(native.MethodPutObject, resource, map[string]string{
			propKey: "prefix" + val,
		})

		st, found = ch.Match(request)
		require.False(t, found)
		require.Equal(t, NoRuleFound, st)
	})

	t.Run(CondStringLessThan.String(), func(t *testing.T) {
		ch := Chain{Rules: []Rule{{
			Status:    Allow,
			Actions:   Actions{Names: []string{native.MethodPutObject}},
			Resources: Resources{Names: []string{native.ResourceFormatRootContainers}},
			Condition: []Condition{{
				Op:    CondStringLessThan,
				Kind:  KindRequest,
				Key:   propKey,
				Value: val + "b",
			}},
		}}}

		resource := testutil.NewResource(native.ResourceFormatRootContainers, nil)
		request := testutil.NewRequest(native.MethodPutObject, resource, map[string]string{
			propKey: val + "a",
		})

		st, found := ch.Match(request)
		require.True(t, found)
		require.Equal(t, Allow, st)

		request = testutil.NewRequest(native.MethodPutObject, resource, map[string]string{
			propKey: val + "c",
		})

		st, found = ch.Match(request)
		require.False(t, found)
		require.Equal(t, NoRuleFound, st)
	})

	t.Run(CondStringLessThanEquals.String(), func(t *testing.T) {
		ch := Chain{Rules: []Rule{{
			Status:    Allow,
			Actions:   Actions{Names: []string{native.MethodPutObject}},
			Resources: Resources{Names: []string{native.ResourceFormatRootContainers}},
			Condition: []Condition{{
				Op:    CondStringLessThanEquals,
				Kind:  KindRequest,
				Key:   propKey,
				Value: val + "b",
			}},
		}}}

		resource := testutil.NewResource(native.ResourceFormatRootContainers, nil)
		request := testutil.NewRequest(native.MethodPutObject, resource, map[string]string{
			propKey: val + "a",
		})

		st, found := ch.Match(request)
		require.True(t, found)
		require.Equal(t, Allow, st)

		request = testutil.NewRequest(native.MethodPutObject, resource, map[string]string{
			propKey: val + "b",
		})

		st, found = ch.Match(request)
		require.True(t, found)
		require.Equal(t, Allow, st)
	})

	t.Run(CondStringGreaterThan.String(), func(t *testing.T) {
		ch := Chain{Rules: []Rule{{
			Status:    Allow,
			Actions:   Actions{Names: []string{native.MethodPutObject}},
			Resources: Resources{Names: []string{native.ResourceFormatRootContainers}},
			Condition: []Condition{{
				Op:    CondStringGreaterThan,
				Kind:  KindRequest,
				Key:   propKey,
				Value: val + "b",
			}},
		}}}

		resource := testutil.NewResource(native.ResourceFormatRootContainers, nil)
		request := testutil.NewRequest(native.MethodPutObject, resource, map[string]string{
			propKey: val + "c",
		})

		st, found := ch.Match(request)
		require.True(t, found)
		require.Equal(t, Allow, st)

		request = testutil.NewRequest(native.MethodPutObject, resource, map[string]string{
			propKey: val + "b",
		})

		st, found = ch.Match(request)
		require.False(t, found)
		require.Equal(t, NoRuleFound, st)
	})

	t.Run(CondStringGreaterThanEquals.String(), func(t *testing.T) {
		ch := Chain{Rules: []Rule{{
			Status:    Allow,
			Actions:   Actions{Names: []string{native.MethodPutObject}},
			Resources: Resources{Names: []string{native.ResourceFormatRootContainers}},
			Condition: []Condition{{
				Op:    CondStringGreaterThanEquals,
				Kind:  KindRequest,
				Key:   propKey,
				Value: val + "b",
			}},
		}}}

		resource := testutil.NewResource(native.ResourceFormatRootContainers, nil)
		request := testutil.NewRequest(native.MethodPutObject, resource, map[string]string{
			propKey: val + "c",
		})

		st, found := ch.Match(request)
		require.True(t, found)
		require.Equal(t, Allow, st)

		request = testutil.NewRequest(native.MethodPutObject, resource, map[string]string{
			propKey: val + "b",
		})

		st, found = ch.Match(request)
		require.True(t, found)
		require.Equal(t, Allow, st)
	})
}

func testIPConditionMatch(t *testing.T) {
	t.Run(CondIPAddress.String(), func(t *testing.T) {
		ch := Chain{Rules: []Rule{{
			Status:    Allow,
			Actions:   Actions{Names: []string{native.MethodPutObject}},
			Resources: Resources{Names: []string{native.ResourceFormatRootContainers}},
			Condition: []Condition{{
				Op:    CondIPAddress,
				Kind:  KindRequest,
				Key:   common.PropertyKeyFrostFSSourceIP,
				Value: "192.92.1.1/20",
			}},
		}}}

		resource := testutil.NewResource(native.ResourceFormatRootContainers, nil)
		request := testutil.NewRequest(native.MethodPutObject, resource, map[string]string{
			common.PropertyKeyFrostFSSourceIP: "192.92.1.91",
		})

		st, found := ch.Match(request)
		require.True(t, found)
		require.Equal(t, Allow, st)

		request = testutil.NewRequest(native.MethodPutObject, resource, map[string]string{
			common.PropertyKeyFrostFSSourceIP: "192.93.1.91",
		})

		st, found = ch.Match(request)
		require.False(t, found)
		require.Equal(t, NoRuleFound, st)
	})

	t.Run(CondNotIPAddress.String(), func(t *testing.T) {
		ch := Chain{Rules: []Rule{{
			Status:    Allow,
			Actions:   Actions{Names: []string{native.MethodPutObject}},
			Resources: Resources{Names: []string{native.ResourceFormatRootContainers}},
			Condition: []Condition{{
				Op:    CondNotIPAddress,
				Kind:  KindRequest,
				Key:   common.PropertyKeyFrostFSSourceIP,
				Value: "192.92.1.1/20",
			}},
		}}}

		resource := testutil.NewResource(native.ResourceFormatRootContainers, nil)
		request := testutil.NewRequest(native.MethodPutObject, resource, map[string]string{
			common.PropertyKeyFrostFSSourceIP: "192.93.1.91",
		})

		st, found := ch.Match(request)
		require.True(t, found)
		require.Equal(t, Allow, st)

		request = testutil.NewRequest(native.MethodPutObject, resource, map[string]string{
			common.PropertyKeyFrostFSSourceIP: "192.92.1.91",
		})

		st, found = ch.Match(request)
		require.False(t, found)
		require.Equal(t, NoRuleFound, st)
	})

	t.Run("invalid ip address condition value", func(t *testing.T) {
		ch := Chain{Rules: []Rule{{
			Status:    Allow,
			Actions:   Actions{Names: []string{native.MethodPutObject}},
			Resources: Resources{Names: []string{native.ResourceFormatRootContainers}},
			Condition: []Condition{{
				Op:    CondIPAddress,
				Kind:  KindRequest,
				Key:   common.PropertyKeyFrostFSSourceIP,
				Value: "192.92.1.1:33333",
			}},
		}}}

		resource := testutil.NewResource(native.ResourceFormatRootContainers, nil)
		request := testutil.NewRequest(native.MethodPutObject, resource, map[string]string{
			common.PropertyKeyFrostFSSourceIP: "192.92.1.91",
		})

		st, found := ch.Match(request)
		require.False(t, found)
		require.Equal(t, NoRuleFound, st)
	})

	t.Run("match ip", func(t *testing.T) {
		cond := Condition{
			Op:    CondIPAddress,
			Kind:  KindRequest,
			Key:   common.PropertyKeyFrostFSSourceIP,
			Value: "192.92.1.1/10",
		}

		require.NotPanics(t, func() {
			cond.matchIP("192.92.1.91")
		})

		require.PanicsWithValue(t, "unimplemented: 10", func() {
			cond.Op = CondNumericEquals
			cond.matchIP("192.92.1.91")
		})
	})
}

func TestInvalidNumericValues(t *testing.T) {
	t.Run("invalid request property", func(t *testing.T) {
		propKey := s3.PropertyKeyMaxKeys
		propValues := []string{"", "invalid"}

		for _, tc := range []struct {
			name          string
			conditionType ConditionType
			match         bool
		}{
			{
				name:          "NumericEquals condition",
				conditionType: CondNumericEquals,
				match:         false,
			},
			{
				name:          "NumericNotEquals condition",
				conditionType: CondNumericNotEquals,
				match:         true,
			},
			{
				name:          "NumericLessThan condition",
				conditionType: CondNumericLessThan,
				match:         false,
			},
			{
				name:          "NumericLessThanEquals condition",
				conditionType: CondNumericLessThanEquals,
				match:         false,
			},
			{
				name:          "NumericGreaterThan condition",
				conditionType: CondNumericGreaterThan,
				match:         false,
			},
			{
				name:          "NumericGreaterThanEquals condition",
				conditionType: CondNumericGreaterThanEquals,
				match:         false,
			},
		} {
			t.Run(tc.name, func(t *testing.T) {
				resource := testutil.NewResource(native.ResourceFormatRootContainers, nil)
				condition := Condition{
					Op:    tc.conditionType,
					Kind:  KindRequest,
					Key:   propKey,
					Value: "50",
				}

				for _, propValue := range propValues {
					request := testutil.NewRequest(native.MethodPutObject, resource, map[string]string{propKey: propValue})

					match := condition.Match(request)
					require.Equal(t, tc.match, match)
				}
			})
		}
	})
	t.Run("invalid condition numeric value", func(t *testing.T) {
		propKey := s3.PropertyKeyMaxKeys

		condition := Condition{
			Kind:  KindRequest,
			Key:   propKey,
			Value: "invalid",
		}

		t.Run("match on CondNumericNotEquals", func(t *testing.T) {
			condition.Op = CondNumericNotEquals

			resource := testutil.NewResource(native.ResourceFormatRootContainers, nil)
			request := testutil.NewRequest(native.MethodPutObject, resource, map[string]string{propKey: "50"})

			match := condition.Match(request)
			require.Equal(t, true, match)
		})

		t.Run("non-match on non-CondNumericNotEquals", func(t *testing.T) {
			condition.Op = CondNumericEquals

			resource := testutil.NewResource(native.ResourceFormatRootContainers, nil)
			request := testutil.NewRequest(native.MethodPutObject, resource, map[string]string{propKey: "50"})

			match := condition.Match(request)
			require.Equal(t, false, match)
		})

		t.Run("match numeric", func(t *testing.T) {
			cond := Condition{
				Op:    CondNumericLessThan,
				Kind:  KindRequest,
				Key:   s3.PropertyKeyMaxKeys,
				Value: "5",
			}

			require.NotPanics(t, func() {
				cond.matchNumeric("6")
			})

			require.PanicsWithValue(t, "unimplemented: 0", func() {
				cond.Op = CondStringEquals
				cond.matchNumeric("10")
			})
		})
	})
}

func TestCondTypeStringification(t *testing.T) {
	for _, pair := range []struct {
		cond           ConditionType
		expectedString string
	}{
		{CondStringEquals, "StringEquals"},
		{CondStringNotEquals, "StringNotEquals"},
		{CondStringEqualsIgnoreCase, "StringEqualsIgnoreCase"},
		{CondStringNotEqualsIgnoreCase, "StringNotEqualsIgnoreCase"},
		{CondStringLike, "StringLike"},
		{CondStringNotLike, "StringNotLike"},
		{CondStringLessThan, "StringLessThan"},
		{CondStringLessThanEquals, "StringLessThanEquals"},
		{CondStringGreaterThan, "StringGreaterThan"},
		{CondStringGreaterThanEquals, "StringGreaterThanEquals"},
		{CondNumericEquals, "NumericEquals"},
		{CondNumericNotEquals, "NumericNotEquals"},
		{CondNumericLessThan, "NumericLessThan"},
		{CondNumericLessThanEquals, "NumericLessThanEquals"},
		{CondNumericGreaterThan, "NumericGreaterThan"},
		{CondNumericGreaterThanEquals, "NumericGreaterThanEquals"},
		{CondSliceContains, "SliceContains"},
		{CondIPAddress, "IPAddress"},
		{CondNotIPAddress, "NotIPAddress"},
		{ConditionType(255), "unknown condition type"},
	} {
		require.Equal(t, pair.expectedString, pair.cond.String())
	}
}

func TestRuleMatch(t *testing.T) {
	rule := Rule{
		Status:    Allow,
		Actions:   Actions{Names: []string{native.MethodPutObject}},
		Resources: Resources{Names: []string{native.ResourceFormatRootContainers}},
		Condition: []Condition{{
			Op:    CondIPAddress,
			Kind:  KindRequest,
			Key:   common.PropertyKeyFrostFSSourceIP,
			Value: "192.92.1.1/20",
		}},
	}

	t.Run("match", func(t *testing.T) {
		resource := testutil.NewResource(native.ResourceFormatRootContainers, nil)
		request := testutil.NewRequest(native.MethodPutObject, resource, map[string]string{
			common.PropertyKeyFrostFSSourceIP: "192.92.1.91",
		})
		st, found := rule.Match(request)
		require.True(t, found)
		require.Equal(t, Allow, st)
	})

	t.Run("not matching resource name", func(t *testing.T) {
		resource := testutil.NewResource(fmt.Sprintf(native.ResourceFormatNamespaceContainers, "namespicy"), nil)
		request := testutil.NewRequest(native.MethodPutObject, resource, map[string]string{
			common.PropertyKeyFrostFSSourceIP: "192.92.1.91",
		})
		st, found := rule.Match(request)
		require.False(t, found)
		require.Equal(t, NoRuleFound, st)
	})

	t.Run("not matching action", func(t *testing.T) {
		resource := testutil.NewResource(native.ResourceFormatRootContainers, nil)
		request := testutil.NewRequest(native.MethodGetObject, resource, map[string]string{
			common.PropertyKeyFrostFSSourceIP: "192.92.1.91",
		})
		st, found := rule.Match(request)
		require.False(t, found)
		require.Equal(t, NoRuleFound, st)
	})

	t.Run("not matching condition", func(t *testing.T) {
		resource := testutil.NewResource(native.ResourceFormatRootContainers, nil)
		request := testutil.NewRequest(native.MethodGetObject, resource, map[string]string{
			common.PropertyKeyFrostFSSourceIP: "192.93.1.91",
		})
		st, found := rule.Match(request)
		require.False(t, found)
		require.Equal(t, NoRuleFound, st)
	})
}