package iam

import (
	"encoding/json"
	"testing"

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

func TestUnmarshalIAMPolicy(t *testing.T) {
	t.Run("simple fields", func(t *testing.T) {
		policy := `{
	"Version": "2012-10-17",
	"Id": "PutObjPolicy",
	"Statement": {
		"Sid": "DenyObjectsThatAreNotSSEKMS",
		"Principal": "*",
		"Effect": "Deny",
		"Action": "s3:PutObject",
		"Resource": "arn:aws:s3:::DOC-EXAMPLE-BUCKET/*",
		"Condition": {
			"Null": {
				"s3:x-amz-server-side-encryption-aws-kms-key-id": "true"
			}
		}
	}
}`

		expected := Policy{
			Version: "2012-10-17",
			ID:      "PutObjPolicy",
			Statement: []Statement{{
				SID: "DenyObjectsThatAreNotSSEKMS",
				Principal: map[PrincipalType][]string{
					"*": nil,
				},
				Effect:   DenyEffect,
				Action:   []string{"s3:PutObject"},
				Resource: []string{"arn:aws:s3:::DOC-EXAMPLE-BUCKET/*"},
				Conditions: map[string]Condition{
					"Null": {
						"s3:x-amz-server-side-encryption-aws-kms-key-id": {"true"},
					},
				},
			}},
		}

		var p Policy
		err := json.Unmarshal([]byte(policy), &p)
		require.NoError(t, err)
		require.Equal(t, expected, p)
	})

	t.Run("complex fields", func(t *testing.T) {
		policy := `{
	"Version": "2012-10-17",
	"Statement": [{
		"Principal":{
			"AWS":[
				"arn:aws:iam::111122223333:user/JohnDoe"
			]
		},
		"Effect": "Allow",
		"Action": [
			"s3:PutObject"
		],
		"Resource": [
			"arn:aws:s3:::DOC-EXAMPLE-BUCKET/*"
		],
		"Condition": {
			"StringEquals": {
				"s3:RequestObjectTag/Department": ["Finance"]
			}
		}
	}]
}`

		expected := Policy{
			Version: "2012-10-17",
			Statement: []Statement{{
				Principal: map[PrincipalType][]string{
					AWSPrincipalType: {"arn:aws:iam::111122223333:user/JohnDoe"},
				},
				Effect:   AllowEffect,
				Action:   []string{"s3:PutObject"},
				Resource: []string{"arn:aws:s3:::DOC-EXAMPLE-BUCKET/*"},
				Conditions: map[string]Condition{
					"StringEquals": {
						"s3:RequestObjectTag/Department": {"Finance"},
					},
				},
			}},
		}

		var p Policy
		err := json.Unmarshal([]byte(policy), &p)
		require.NoError(t, err)
		require.Equal(t, expected, p)

		raw, err := json.Marshal(expected)
		require.NoError(t, err)
		require.JSONEq(t, policy, string(raw))
	})

	t.Run("check principal AWS", func(t *testing.T) {
		policy := `{
	"Statement": [{
		"Principal":{
			"AWS":"arn:aws:iam::111122223333:user/JohnDoe"
		}
	}]
}`

		expected := Policy{
			Statement: []Statement{{
				Principal: map[PrincipalType][]string{
					AWSPrincipalType: {"arn:aws:iam::111122223333:user/JohnDoe"},
				},
			}},
		}

		var p Policy
		err := json.Unmarshal([]byte(policy), &p)
		require.NoError(t, err)
		require.Equal(t, expected, p)
	})

	t.Run("native example", func(t *testing.T) {
		policy := `
{
	"Version": "xyz",
	"Statement": [
	  {
		"Effect": "Allow",
		"Action": [
		  "native:*",
		  "s3:PutObject",
		  "s3:GetObject"
		],
		"Resource": ["*"],
		"Principal": {"FrostFS": ["did:frostfs:039e3ee771a223361fe7862f532e9511b57baaae3c3e2622682e99d0e660f7671"]},
		"Condition": {"StringEquals": {"native::object::attribute": "iamuser-admin"}}
	  }
	]
}`

		var p Policy
		err := json.Unmarshal([]byte(policy), &p)
		require.NoError(t, err)
	})

	t.Run("condition array", func(t *testing.T) {
		policy := `
{
	"Statement": [{
		"Condition": {"StringLike": {"ec2:InstanceType": ["t1.*", "t2.*", "m3.*"]}}
	}]
}`

		expected := Policy{
			Statement: []Statement{{
				Conditions: map[string]Condition{
					"StringLike": {"ec2:InstanceType": {"t1.*", "t2.*", "m3.*"}},
				},
			}},
		}

		var p Policy
		err := json.Unmarshal([]byte(policy), &p)
		require.NoError(t, err)
		require.Equal(t, expected, p)
	})

	t.Run("'Not*' fields", func(t *testing.T) {
		policy := `
{
	"Id": "PutObjPolicy",
	"Statement": [{
			"NotPrincipal": {"AWS":["arn:aws:iam::111122223333:user/Alice"]},
			"Effect": "Deny",
			"NotAction": "s3:PutObject",
			"NotResource": "arn:aws:s3:::DOC-EXAMPLE-BUCKET/*"
	}]
}`

		expected := Policy{
			ID: "PutObjPolicy",
			Statement: []Statement{{
				NotPrincipal: map[PrincipalType][]string{
					AWSPrincipalType: {"arn:aws:iam::111122223333:user/Alice"},
				},
				Effect:      DenyEffect,
				NotAction:   []string{"s3:PutObject"},
				NotResource: []string{"arn:aws:s3:::DOC-EXAMPLE-BUCKET/*"},
			}},
		}

		var p Policy
		err := json.Unmarshal([]byte(policy), &p)
		require.NoError(t, err)
		require.Equal(t, expected, p)
	})
}

func TestValidatePolicies(t *testing.T) {
	for _, tc := range []struct {
		name    string
		policy  Policy
		typ     PolicyType
		isValid bool
	}{
		{
			name: "valid permission boundaries",
			policy: Policy{
				Version: policyVersion,
				Statement: []Statement{{
					Effect:   AllowEffect,
					Action:   []string{"s3:*", "cloudwatch:*", "ec2:*"},
					Resource: []string{Wildcard},
				}},
			},
			typ:     GeneralPolicyType,
			isValid: true,
		},
		{
			name: "general invalid effect",
			policy: Policy{
				Version: policyVersion,
				Statement: []Statement{{
					Effect:   "dummy",
					Action:   []string{"s3:*", "cloudwatch:*", "ec2:*"},
					Resource: []string{Wildcard},
				}},
			},
			typ:     GeneralPolicyType,
			isValid: false,
		},
		{
			name: "general invalid principal block",
			policy: Policy{
				Version: policyVersion,
				Statement: []Statement{{
					Effect:       AllowEffect,
					Action:       []string{"s3:*", "cloudwatch:*", "ec2:*"},
					Resource:     []string{Wildcard},
					Principal:    map[PrincipalType][]string{Wildcard: nil},
					NotPrincipal: map[PrincipalType][]string{Wildcard: nil},
				}},
			},
			typ:     GeneralPolicyType,
			isValid: false,
		},
		{
			name: "general invalid not principal",
			policy: Policy{
				Version: policyVersion,
				Statement: []Statement{{
					Effect:       AllowEffect,
					Action:       []string{"s3:*", "cloudwatch:*", "ec2:*"},
					Resource:     []string{Wildcard},
					NotPrincipal: map[PrincipalType][]string{AWSPrincipalType: {"arn:aws:iam::111122223333:user/Alice"}},
				}},
			},
			typ:     GeneralPolicyType,
			isValid: false,
		},
		{
			name: "general invalid principal type",
			policy: Policy{
				Version: policyVersion,
				Statement: []Statement{{
					Effect:       AllowEffect,
					Action:       []string{"s3:*", "cloudwatch:*", "ec2:*"},
					Resource:     []string{Wildcard},
					NotPrincipal: map[PrincipalType][]string{"dummy": {"arn:aws:iam::111122223333:user/Alice"}},
				}},
			},
			typ:     GeneralPolicyType,
			isValid: false,
		},
		{
			name: "general invalid action block",
			policy: Policy{
				Version: policyVersion,
				Statement: []Statement{{
					Effect:    AllowEffect,
					Action:    []string{"s3:*", "cloudwatch:*", "ec2:*"},
					NotAction: []string{"iam:*"},
					Resource:  []string{Wildcard},
				}},
			},
			typ:     GeneralPolicyType,
			isValid: false,
		},
		{
			name: "general invalid resource block",
			policy: Policy{
				Version: policyVersion,
				Statement: []Statement{{
					Effect:      AllowEffect,
					Resource:    []string{Wildcard},
					NotResource: []string{"arn:aws:s3:::DOC-EXAMPLE-BUCKET/*"},
				}},
			},
			typ:     GeneralPolicyType,
			isValid: false,
		},
		{
			name: "invalid resource block",
			policy: Policy{
				Version: policyVersion,
				Statement: []Statement{{
					Effect:      AllowEffect,
					Resource:    []string{},
					NotResource: []string{"arn:aws:s3:::DOC-EXAMPLE-BUCKET/*"},
				}},
			},
			typ:     GeneralPolicyType,
			isValid: false,
		},
		{
			name: "missing resource block",
			policy: Policy{
				Version: policyVersion,
				Statement: []Statement{{
					Effect: AllowEffect,
				}},
			},
			typ:     GeneralPolicyType,
			isValid: false,
		},
		{
			name:    "missing statement block",
			policy:  Policy{},
			typ:     GeneralPolicyType,
			isValid: false,
		},
		{
			name: "duplicate sid",
			policy: Policy{
				Version: policyVersion,
				Statement: []Statement{
					{
						SID:      "sid",
						Effect:   AllowEffect,
						Action:   []string{"s3:*"},
						Resource: []string{Wildcard},
					},
					{
						SID:      "sid",
						Effect:   AllowEffect,
						Action:   []string{"cloudwatch:*"},
						Resource: []string{Wildcard},
					}},
			},
			typ:     GeneralPolicyType,
			isValid: false,
		},
		{
			name: "missing version",
			policy: Policy{
				Statement: []Statement{{
					Effect:   AllowEffect,
					Action:   []string{"s3:*"},
					Resource: []string{Wildcard},
				}},
			},
			typ:     GeneralPolicyType,
			isValid: false,
		},
		{
			name: "identity based valid",
			policy: Policy{
				Version: policyVersion,
				Statement: []Statement{{
					Effect:   AllowEffect,
					Action:   []string{"s3:PutObject"},
					Resource: []string{Wildcard},
				}},
			},
			typ:     IdentityBasedPolicyType,
			isValid: true,
		},
		{
			name: "identity based invalid because of id presence",
			policy: Policy{
				ID:      "some-id",
				Version: policyVersion,
				Statement: []Statement{{
					Effect:   AllowEffect,
					Action:   []string{"s3:PutObject"},
					Resource: []string{Wildcard},
				}},
			},
			typ:     IdentityBasedPolicyType,
			isValid: false,
		},
		{
			name: "identity based invalid because of principal presence",
			policy: Policy{
				Version: policyVersion,
				Statement: []Statement{{
					Effect:    AllowEffect,
					Action:    []string{"s3:PutObject"},
					Resource:  []string{Wildcard},
					Principal: map[PrincipalType][]string{AWSPrincipalType: {"arn:aws:iam::111122223333:user/Alice"}},
				}},
			},
			typ:     IdentityBasedPolicyType,
			isValid: false,
		},
		{
			name: "identity based invalid because of not principal presence",
			policy: Policy{
				Version: policyVersion,
				Statement: []Statement{{
					Effect:       AllowEffect,
					Action:       []string{"s3:PutObject"},
					Resource:     []string{Wildcard},
					NotPrincipal: map[PrincipalType][]string{AWSPrincipalType: {"arn:aws:iam::111122223333:user/Alice"}},
				}},
			},
			typ:     IdentityBasedPolicyType,
			isValid: false,
		},
		{
			name: "resource based valid principal",
			policy: Policy{
				Version: policyVersion,
				Statement: []Statement{{
					Effect:    DenyEffect,
					Action:    []string{"s3:PutObject"},
					Resource:  []string{Wildcard},
					Principal: map[PrincipalType][]string{AWSPrincipalType: {"arn:aws:iam::111122223333:user/Alice"}},
				}},
			},
			typ:     ResourceBasedPolicyType,
			isValid: true,
		},
		{
			name: "resource based valid not principal",
			policy: Policy{
				ID:      "some-id",
				Version: policyVersion,
				Statement: []Statement{{
					Effect:       DenyEffect,
					Action:       []string{"s3:PutObject"},
					Resource:     []string{Wildcard},
					NotPrincipal: map[PrincipalType][]string{AWSPrincipalType: {"arn:aws:iam::111122223333:user/Alice"}},
				}},
			},
			typ:     ResourceBasedPolicyType,
			isValid: true,
		},
		{
			name: "resource based invalid missing principal",
			policy: Policy{
				ID:      "some-id",
				Version: policyVersion,
				Statement: []Statement{{
					Effect:   AllowEffect,
					Action:   []string{"s3:PutObject"},
					Resource: []string{Wildcard},
				}},
			},
			typ:     ResourceBasedPolicyType,
			isValid: false,
		},
	} {
		t.Run(tc.name, func(t *testing.T) {
			err := tc.policy.Validate(tc.typ)
			if tc.isValid {
				require.NoError(t, err)
			} else {
				require.Error(t, err)
			}
		})
	}
}

func TestProcessDenyFirst(t *testing.T) {
	identityBasedPolicyStr := `
{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Effect": "Allow",
			"Principal": {
				"AWS": [ "arn:aws:iam::root:user/user-name" ]
			},
            "Action": ["s3:PutObject" ],
            "Resource": "arn:aws:s3:::*"
        }
    ]
}
`

	resourceBasedPolicyStr := `
{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Effect": "Deny",
            "Principal": "*",
            "Action": "s3:*",
            "Resource": [ "arn:aws:s3:::test-bucket/*" ]
        }
    ]
}
`

	var identityPolicy Policy
	err := json.Unmarshal([]byte(identityBasedPolicyStr), &identityPolicy)
	require.NoError(t, err)

	var resourcePolicy Policy
	err = json.Unmarshal([]byte(resourceBasedPolicyStr), &resourcePolicy)
	require.NoError(t, err)

	mockResolver := newMockUserResolver([]string{"root/user-name"}, []string{"test-bucket"}, "")

	identityNativePolicy, err := ConvertToS3Chain(identityPolicy, mockResolver)
	require.NoError(t, err)
	identityNativePolicy.MatchType = chain.MatchTypeFirstMatch

	resourceNativePolicy, err := ConvertToS3Chain(resourcePolicy, mockResolver)
	require.NoError(t, err)

	s := inmemory.NewInMemory()

	target := engine.NamespaceTarget("ns")

	_, _, err = s.MorphRuleChainStorage().AddMorphRuleChain(chain.S3, target, identityNativePolicy)
	require.NoError(t, err)

	_, _, err = s.MorphRuleChainStorage().AddMorphRuleChain(chain.S3, target, resourceNativePolicy)
	require.NoError(t, err)

	resource := testutil.NewResource("arn:aws:s3:::test-bucket/object", nil)
	request := testutil.NewRequest("s3:PutObject", resource, map[string]string{s3.PropertyKeyOwner: mockResolver.users["root/user-name"]})

	status, found, err := s.IsAllowed(chain.S3, engine.NewRequestTarget("ns", ""), request)
	require.NoError(t, err)
	require.True(t, found)
	require.Equal(t, chain.AccessDenied, status)
}