package policy

import (
	"context"
	"testing"

	"github.com/stretchr/testify/assert"
	"github.com/stretchr/testify/require"
)

// TestPolicyEngineInitialization tests policy engine initialization
func TestPolicyEngineInitialization(t *testing.T) {
	tests := []struct {
		name    string
		config  *PolicyEngineConfig
		wantErr bool
	}{
		{
			name: "valid config",
			config: &PolicyEngineConfig{
				DefaultEffect: "Deny",
				StoreType:     "memory",
			},
			wantErr: false,
		},
		{
			name: "invalid default effect",
			config: &PolicyEngineConfig{
				DefaultEffect: "Invalid",
				StoreType:     "memory",
			},
			wantErr: true,
		},
		{
			name:    "nil config",
			config:  nil,
			wantErr: true,
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			engine := NewPolicyEngine()

			err := engine.Initialize(tt.config)

			if tt.wantErr {
				assert.Error(t, err)
			} else {
				assert.NoError(t, err)
				assert.True(t, engine.IsInitialized())
			}
		})
	}
}

// TestPolicyDocumentValidation tests policy document structure validation
func TestPolicyDocumentValidation(t *testing.T) {
	tests := []struct {
		name     string
		policy   *PolicyDocument
		wantErr  bool
		errorMsg string
	}{
		{
			name: "valid policy document",
			policy: &PolicyDocument{
				Version: "2012-10-17",
				Statement: []Statement{
					{
						Sid:      "AllowS3Read",
						Effect:   "Allow",
						Action:   []string{"s3:GetObject", "s3:ListBucket"},
						Resource: []string{"arn:seaweed:s3:::mybucket/*"},
					},
				},
			},
			wantErr: false,
		},
		{
			name: "missing version",
			policy: &PolicyDocument{
				Statement: []Statement{
					{
						Effect:   "Allow",
						Action:   []string{"s3:GetObject"},
						Resource: []string{"arn:seaweed:s3:::mybucket/*"},
					},
				},
			},
			wantErr:  true,
			errorMsg: "version is required",
		},
		{
			name: "empty statements",
			policy: &PolicyDocument{
				Version:   "2012-10-17",
				Statement: []Statement{},
			},
			wantErr:  true,
			errorMsg: "at least one statement is required",
		},
		{
			name: "invalid effect",
			policy: &PolicyDocument{
				Version: "2012-10-17",
				Statement: []Statement{
					{
						Effect:   "Maybe",
						Action:   []string{"s3:GetObject"},
						Resource: []string{"arn:seaweed:s3:::mybucket/*"},
					},
				},
			},
			wantErr:  true,
			errorMsg: "invalid effect",
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			err := ValidatePolicyDocument(tt.policy)

			if tt.wantErr {
				assert.Error(t, err)
				if tt.errorMsg != "" {
					assert.Contains(t, err.Error(), tt.errorMsg)
				}
			} else {
				assert.NoError(t, err)
			}
		})
	}
}

// TestPolicyEvaluation tests policy evaluation logic
func TestPolicyEvaluation(t *testing.T) {
	engine := setupTestPolicyEngine(t)

	// Add test policies
	readPolicy := &PolicyDocument{
		Version: "2012-10-17",
		Statement: []Statement{
			{
				Sid:    "AllowS3Read",
				Effect: "Allow",
				Action: []string{"s3:GetObject", "s3:ListBucket"},
				Resource: []string{
					"arn:seaweed:s3:::public-bucket/*", // For object operations
					"arn:seaweed:s3:::public-bucket",   // For bucket operations
				},
			},
		},
	}

	err := engine.AddPolicy("", "read-policy", readPolicy)
	require.NoError(t, err)

	denyPolicy := &PolicyDocument{
		Version: "2012-10-17",
		Statement: []Statement{
			{
				Sid:      "DenyS3Delete",
				Effect:   "Deny",
				Action:   []string{"s3:DeleteObject"},
				Resource: []string{"arn:seaweed:s3:::*"},
			},
		},
	}

	err = engine.AddPolicy("", "deny-policy", denyPolicy)
	require.NoError(t, err)

	tests := []struct {
		name     string
		context  *EvaluationContext
		policies []string
		want     Effect
	}{
		{
			name: "allow read access",
			context: &EvaluationContext{
				Principal: "user:alice",
				Action:    "s3:GetObject",
				Resource:  "arn:seaweed:s3:::public-bucket/file.txt",
				RequestContext: map[string]interface{}{
					"sourceIP": "192.168.1.100",
				},
			},
			policies: []string{"read-policy"},
			want:     EffectAllow,
		},
		{
			name: "deny delete access (explicit deny)",
			context: &EvaluationContext{
				Principal: "user:alice",
				Action:    "s3:DeleteObject",
				Resource:  "arn:seaweed:s3:::public-bucket/file.txt",
			},
			policies: []string{"read-policy", "deny-policy"},
			want:     EffectDeny,
		},
		{
			name: "deny by default (no matching policy)",
			context: &EvaluationContext{
				Principal: "user:alice",
				Action:    "s3:PutObject",
				Resource:  "arn:seaweed:s3:::public-bucket/file.txt",
			},
			policies: []string{"read-policy"},
			want:     EffectDeny,
		},
		{
			name: "allow with wildcard action",
			context: &EvaluationContext{
				Principal: "user:admin",
				Action:    "s3:ListBucket",
				Resource:  "arn:seaweed:s3:::public-bucket",
			},
			policies: []string{"read-policy"},
			want:     EffectAllow,
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			result, err := engine.Evaluate(context.Background(), "", tt.context, tt.policies)

			assert.NoError(t, err)
			assert.Equal(t, tt.want, result.Effect)

			// Verify evaluation details
			assert.NotNil(t, result.EvaluationDetails)
			assert.Equal(t, tt.context.Action, result.EvaluationDetails.Action)
			assert.Equal(t, tt.context.Resource, result.EvaluationDetails.Resource)
		})
	}
}

// TestConditionEvaluation tests policy conditions
func TestConditionEvaluation(t *testing.T) {
	engine := setupTestPolicyEngine(t)

	// Policy with IP address condition
	conditionalPolicy := &PolicyDocument{
		Version: "2012-10-17",
		Statement: []Statement{
			{
				Sid:      "AllowFromOfficeIP",
				Effect:   "Allow",
				Action:   []string{"s3:*"},
				Resource: []string{"arn:seaweed:s3:::*"},
				Condition: map[string]map[string]interface{}{
					"IpAddress": {
						"seaweed:SourceIP": []string{"192.168.1.0/24", "10.0.0.0/8"},
					},
				},
			},
		},
	}

	err := engine.AddPolicy("", "ip-conditional", conditionalPolicy)
	require.NoError(t, err)

	tests := []struct {
		name    string
		context *EvaluationContext
		want    Effect
	}{
		{
			name: "allow from office IP",
			context: &EvaluationContext{
				Principal: "user:alice",
				Action:    "s3:GetObject",
				Resource:  "arn:seaweed:s3:::mybucket/file.txt",
				RequestContext: map[string]interface{}{
					"sourceIP": "192.168.1.100",
				},
			},
			want: EffectAllow,
		},
		{
			name: "deny from external IP",
			context: &EvaluationContext{
				Principal: "user:alice",
				Action:    "s3:GetObject",
				Resource:  "arn:seaweed:s3:::mybucket/file.txt",
				RequestContext: map[string]interface{}{
					"sourceIP": "8.8.8.8",
				},
			},
			want: EffectDeny,
		},
		{
			name: "allow from internal IP",
			context: &EvaluationContext{
				Principal: "user:alice",
				Action:    "s3:PutObject",
				Resource:  "arn:seaweed:s3:::mybucket/newfile.txt",
				RequestContext: map[string]interface{}{
					"sourceIP": "10.1.2.3",
				},
			},
			want: EffectAllow,
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			result, err := engine.Evaluate(context.Background(), "", tt.context, []string{"ip-conditional"})

			assert.NoError(t, err)
			assert.Equal(t, tt.want, result.Effect)
		})
	}
}

// TestResourceMatching tests resource ARN matching
func TestResourceMatching(t *testing.T) {
	tests := []struct {
		name            string
		policyResource  string
		requestResource string
		want            bool
	}{
		{
			name:            "exact match",
			policyResource:  "arn:seaweed:s3:::mybucket/file.txt",
			requestResource: "arn:seaweed:s3:::mybucket/file.txt",
			want:            true,
		},
		{
			name:            "wildcard match",
			policyResource:  "arn:seaweed:s3:::mybucket/*",
			requestResource: "arn:seaweed:s3:::mybucket/folder/file.txt",
			want:            true,
		},
		{
			name:            "bucket wildcard",
			policyResource:  "arn:seaweed:s3:::*",
			requestResource: "arn:seaweed:s3:::anybucket/file.txt",
			want:            true,
		},
		{
			name:            "no match different bucket",
			policyResource:  "arn:seaweed:s3:::mybucket/*",
			requestResource: "arn:seaweed:s3:::otherbucket/file.txt",
			want:            false,
		},
		{
			name:            "prefix match",
			policyResource:  "arn:seaweed:s3:::mybucket/documents/*",
			requestResource: "arn:seaweed:s3:::mybucket/documents/secret.txt",
			want:            true,
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			result := matchResource(tt.policyResource, tt.requestResource)
			assert.Equal(t, tt.want, result)
		})
	}
}

// TestActionMatching tests action pattern matching
func TestActionMatching(t *testing.T) {
	tests := []struct {
		name          string
		policyAction  string
		requestAction string
		want          bool
	}{
		{
			name:          "exact match",
			policyAction:  "s3:GetObject",
			requestAction: "s3:GetObject",
			want:          true,
		},
		{
			name:          "wildcard service",
			policyAction:  "s3:*",
			requestAction: "s3:PutObject",
			want:          true,
		},
		{
			name:          "wildcard all",
			policyAction:  "*",
			requestAction: "filer:CreateEntry",
			want:          true,
		},
		{
			name:          "prefix match",
			policyAction:  "s3:Get*",
			requestAction: "s3:GetObject",
			want:          true,
		},
		{
			name:          "no match different service",
			policyAction:  "s3:GetObject",
			requestAction: "filer:GetEntry",
			want:          false,
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			result := matchAction(tt.policyAction, tt.requestAction)
			assert.Equal(t, tt.want, result)
		})
	}
}

// Helper function to set up test policy engine
func setupTestPolicyEngine(t *testing.T) *PolicyEngine {
	engine := NewPolicyEngine()
	config := &PolicyEngineConfig{
		DefaultEffect: "Deny",
		StoreType:     "memory",
	}

	err := engine.Initialize(config)
	require.NoError(t, err)

	return engine
}
