package chain

import (
	"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 TestReturnFirstMatch(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)
	})
}

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,
			Object: ObjectRequest,
			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,
					Object: ObjectRequest,
					Key:    propKey,
					Value:  "100",
				},
				{
					Op:     CondNumericGreaterThan,
					Object: ObjectRequest,
					Key:    propKey,
					Value:  "80",
				},
				{
					Op:     CondNumericNotEquals,
					Object: ObjectRequest,
					Key:    propKey,
					Value:  "91",
				},
			},
			value:  "90",
			status: Allow,
		},
		{
			name: "border value",
			conditions: []Condition{
				{
					Op:     CondNumericEquals,
					Object: ObjectRequest,
					Key:    propKey,
					Value:  "50",
				},
				{
					Op:     CondNumericLessThanEquals,
					Object: ObjectRequest,
					Key:    propKey,
					Value:  "50",
				},
				{
					Op:     CondNumericGreaterThanEquals,
					Object: ObjectRequest,
					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 TestInvalidNumericValues(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,
				Object: ObjectRequest,
				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)
			}
		})
	}
}