package s3api

import (
	"fmt"
	"testing"

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

func TestS3PolicyTemplates(t *testing.T) {
	templates := NewS3PolicyTemplates()

	t.Run("S3ReadOnlyPolicy", func(t *testing.T) {
		policy := templates.GetS3ReadOnlyPolicy()

		require.NotNil(t, policy)
		assert.Equal(t, "2012-10-17", policy.Version)
		assert.Len(t, policy.Statement, 1)

		stmt := policy.Statement[0]
		assert.Equal(t, "Allow", stmt.Effect)
		assert.Equal(t, "S3ReadOnlyAccess", stmt.Sid)
		assert.Contains(t, stmt.Action, "s3:GetObject")
		assert.Contains(t, stmt.Action, "s3:ListBucket")
		assert.NotContains(t, stmt.Action, "s3:PutObject")
		assert.NotContains(t, stmt.Action, "s3:DeleteObject")

		assert.Contains(t, stmt.Resource, "arn:seaweed:s3:::*")
		assert.Contains(t, stmt.Resource, "arn:seaweed:s3:::*/*")
	})

	t.Run("S3WriteOnlyPolicy", func(t *testing.T) {
		policy := templates.GetS3WriteOnlyPolicy()

		require.NotNil(t, policy)
		assert.Equal(t, "2012-10-17", policy.Version)
		assert.Len(t, policy.Statement, 1)

		stmt := policy.Statement[0]
		assert.Equal(t, "Allow", stmt.Effect)
		assert.Equal(t, "S3WriteOnlyAccess", stmt.Sid)
		assert.Contains(t, stmt.Action, "s3:PutObject")
		assert.Contains(t, stmt.Action, "s3:CreateMultipartUpload")
		assert.NotContains(t, stmt.Action, "s3:GetObject")
		assert.NotContains(t, stmt.Action, "s3:DeleteObject")

		assert.Contains(t, stmt.Resource, "arn:seaweed:s3:::*")
		assert.Contains(t, stmt.Resource, "arn:seaweed:s3:::*/*")
	})

	t.Run("S3AdminPolicy", func(t *testing.T) {
		policy := templates.GetS3AdminPolicy()

		require.NotNil(t, policy)
		assert.Equal(t, "2012-10-17", policy.Version)
		assert.Len(t, policy.Statement, 1)

		stmt := policy.Statement[0]
		assert.Equal(t, "Allow", stmt.Effect)
		assert.Equal(t, "S3FullAccess", stmt.Sid)
		assert.Contains(t, stmt.Action, "s3:*")

		assert.Contains(t, stmt.Resource, "arn:seaweed:s3:::*")
		assert.Contains(t, stmt.Resource, "arn:seaweed:s3:::*/*")
	})
}

func TestBucketSpecificPolicies(t *testing.T) {
	templates := NewS3PolicyTemplates()
	bucketName := "test-bucket"

	t.Run("BucketSpecificReadPolicy", func(t *testing.T) {
		policy := templates.GetBucketSpecificReadPolicy(bucketName)

		require.NotNil(t, policy)
		assert.Equal(t, "2012-10-17", policy.Version)
		assert.Len(t, policy.Statement, 1)

		stmt := policy.Statement[0]
		assert.Equal(t, "Allow", stmt.Effect)
		assert.Equal(t, "BucketSpecificReadAccess", stmt.Sid)
		assert.Contains(t, stmt.Action, "s3:GetObject")
		assert.Contains(t, stmt.Action, "s3:ListBucket")
		assert.NotContains(t, stmt.Action, "s3:PutObject")

		expectedBucketArn := "arn:seaweed:s3:::" + bucketName
		expectedObjectArn := "arn:seaweed:s3:::" + bucketName + "/*"
		assert.Contains(t, stmt.Resource, expectedBucketArn)
		assert.Contains(t, stmt.Resource, expectedObjectArn)
	})

	t.Run("BucketSpecificWritePolicy", func(t *testing.T) {
		policy := templates.GetBucketSpecificWritePolicy(bucketName)

		require.NotNil(t, policy)
		assert.Equal(t, "2012-10-17", policy.Version)
		assert.Len(t, policy.Statement, 1)

		stmt := policy.Statement[0]
		assert.Equal(t, "Allow", stmt.Effect)
		assert.Equal(t, "BucketSpecificWriteAccess", stmt.Sid)
		assert.Contains(t, stmt.Action, "s3:PutObject")
		assert.Contains(t, stmt.Action, "s3:CreateMultipartUpload")
		assert.NotContains(t, stmt.Action, "s3:GetObject")

		expectedBucketArn := "arn:seaweed:s3:::" + bucketName
		expectedObjectArn := "arn:seaweed:s3:::" + bucketName + "/*"
		assert.Contains(t, stmt.Resource, expectedBucketArn)
		assert.Contains(t, stmt.Resource, expectedObjectArn)
	})
}

func TestPathBasedAccessPolicy(t *testing.T) {
	templates := NewS3PolicyTemplates()
	bucketName := "shared-bucket"
	pathPrefix := "user123/documents"

	policy := templates.GetPathBasedAccessPolicy(bucketName, pathPrefix)

	require.NotNil(t, policy)
	assert.Equal(t, "2012-10-17", policy.Version)
	assert.Len(t, policy.Statement, 2)

	// First statement: List bucket with prefix condition
	listStmt := policy.Statement[0]
	assert.Equal(t, "Allow", listStmt.Effect)
	assert.Equal(t, "ListBucketPermission", listStmt.Sid)
	assert.Contains(t, listStmt.Action, "s3:ListBucket")
	assert.Contains(t, listStmt.Resource, "arn:seaweed:s3:::"+bucketName)
	assert.NotNil(t, listStmt.Condition)

	// Second statement: Object operations on path
	objectStmt := policy.Statement[1]
	assert.Equal(t, "Allow", objectStmt.Effect)
	assert.Equal(t, "PathBasedObjectAccess", objectStmt.Sid)
	assert.Contains(t, objectStmt.Action, "s3:GetObject")
	assert.Contains(t, objectStmt.Action, "s3:PutObject")
	assert.Contains(t, objectStmt.Action, "s3:DeleteObject")

	expectedObjectArn := "arn:seaweed:s3:::" + bucketName + "/" + pathPrefix + "/*"
	assert.Contains(t, objectStmt.Resource, expectedObjectArn)
}

func TestIPRestrictedPolicy(t *testing.T) {
	templates := NewS3PolicyTemplates()
	allowedCIDRs := []string{"192.168.1.0/24", "10.0.0.0/8"}

	policy := templates.GetIPRestrictedPolicy(allowedCIDRs)

	require.NotNil(t, policy)
	assert.Equal(t, "2012-10-17", policy.Version)
	assert.Len(t, policy.Statement, 1)

	stmt := policy.Statement[0]
	assert.Equal(t, "Allow", stmt.Effect)
	assert.Equal(t, "IPRestrictedS3Access", stmt.Sid)
	assert.Contains(t, stmt.Action, "s3:*")
	assert.NotNil(t, stmt.Condition)

	// Check IP condition structure
	condition := stmt.Condition
	ipAddress, exists := condition["IpAddress"]
	assert.True(t, exists)

	sourceIp, exists := ipAddress["aws:SourceIp"]
	assert.True(t, exists)
	assert.Equal(t, allowedCIDRs, sourceIp)
}

func TestTimeBasedAccessPolicy(t *testing.T) {
	templates := NewS3PolicyTemplates()
	startHour := 9 // 9 AM
	endHour := 17  // 5 PM

	policy := templates.GetTimeBasedAccessPolicy(startHour, endHour)

	require.NotNil(t, policy)
	assert.Equal(t, "2012-10-17", policy.Version)
	assert.Len(t, policy.Statement, 1)

	stmt := policy.Statement[0]
	assert.Equal(t, "Allow", stmt.Effect)
	assert.Equal(t, "TimeBasedS3Access", stmt.Sid)
	assert.Contains(t, stmt.Action, "s3:GetObject")
	assert.Contains(t, stmt.Action, "s3:PutObject")
	assert.Contains(t, stmt.Action, "s3:ListBucket")
	assert.NotNil(t, stmt.Condition)

	// Check time condition structure
	condition := stmt.Condition
	_, hasGreater := condition["DateGreaterThan"]
	_, hasLess := condition["DateLessThan"]
	assert.True(t, hasGreater)
	assert.True(t, hasLess)
}

func TestMultipartUploadPolicyTemplate(t *testing.T) {
	templates := NewS3PolicyTemplates()
	bucketName := "large-files"

	policy := templates.GetMultipartUploadPolicy(bucketName)

	require.NotNil(t, policy)
	assert.Equal(t, "2012-10-17", policy.Version)
	assert.Len(t, policy.Statement, 2)

	// First statement: Multipart operations
	multipartStmt := policy.Statement[0]
	assert.Equal(t, "Allow", multipartStmt.Effect)
	assert.Equal(t, "MultipartUploadOperations", multipartStmt.Sid)
	assert.Contains(t, multipartStmt.Action, "s3:CreateMultipartUpload")
	assert.Contains(t, multipartStmt.Action, "s3:UploadPart")
	assert.Contains(t, multipartStmt.Action, "s3:CompleteMultipartUpload")
	assert.Contains(t, multipartStmt.Action, "s3:AbortMultipartUpload")
	assert.Contains(t, multipartStmt.Action, "s3:ListMultipartUploads")
	assert.Contains(t, multipartStmt.Action, "s3:ListParts")

	expectedObjectArn := "arn:seaweed:s3:::" + bucketName + "/*"
	assert.Contains(t, multipartStmt.Resource, expectedObjectArn)

	// Second statement: List bucket
	listStmt := policy.Statement[1]
	assert.Equal(t, "Allow", listStmt.Effect)
	assert.Equal(t, "ListBucketForMultipart", listStmt.Sid)
	assert.Contains(t, listStmt.Action, "s3:ListBucket")

	expectedBucketArn := "arn:seaweed:s3:::" + bucketName
	assert.Contains(t, listStmt.Resource, expectedBucketArn)
}

func TestPresignedURLPolicy(t *testing.T) {
	templates := NewS3PolicyTemplates()
	bucketName := "shared-files"

	policy := templates.GetPresignedURLPolicy(bucketName)

	require.NotNil(t, policy)
	assert.Equal(t, "2012-10-17", policy.Version)
	assert.Len(t, policy.Statement, 1)

	stmt := policy.Statement[0]
	assert.Equal(t, "Allow", stmt.Effect)
	assert.Equal(t, "PresignedURLAccess", stmt.Sid)
	assert.Contains(t, stmt.Action, "s3:GetObject")
	assert.Contains(t, stmt.Action, "s3:PutObject")
	assert.NotNil(t, stmt.Condition)

	expectedObjectArn := "arn:seaweed:s3:::" + bucketName + "/*"
	assert.Contains(t, stmt.Resource, expectedObjectArn)

	// Check signature version condition
	condition := stmt.Condition
	stringEquals, exists := condition["StringEquals"]
	assert.True(t, exists)

	signatureVersion, exists := stringEquals["s3:x-amz-signature-version"]
	assert.True(t, exists)
	assert.Equal(t, "AWS4-HMAC-SHA256", signatureVersion)
}

func TestTemporaryAccessPolicy(t *testing.T) {
	templates := NewS3PolicyTemplates()
	bucketName := "temp-bucket"
	expirationHours := 24

	policy := templates.GetTemporaryAccessPolicy(bucketName, expirationHours)

	require.NotNil(t, policy)
	assert.Equal(t, "2012-10-17", policy.Version)
	assert.Len(t, policy.Statement, 1)

	stmt := policy.Statement[0]
	assert.Equal(t, "Allow", stmt.Effect)
	assert.Equal(t, "TemporaryS3Access", stmt.Sid)
	assert.Contains(t, stmt.Action, "s3:GetObject")
	assert.Contains(t, stmt.Action, "s3:PutObject")
	assert.Contains(t, stmt.Action, "s3:ListBucket")
	assert.NotNil(t, stmt.Condition)

	// Check expiration condition
	condition := stmt.Condition
	dateLessThan, exists := condition["DateLessThan"]
	assert.True(t, exists)

	currentTime, exists := dateLessThan["aws:CurrentTime"]
	assert.True(t, exists)
	assert.IsType(t, "", currentTime) // Should be a string timestamp
}

func TestContentTypeRestrictedPolicy(t *testing.T) {
	templates := NewS3PolicyTemplates()
	bucketName := "media-bucket"
	allowedTypes := []string{"image/jpeg", "image/png", "video/mp4"}

	policy := templates.GetContentTypeRestrictedPolicy(bucketName, allowedTypes)

	require.NotNil(t, policy)
	assert.Equal(t, "2012-10-17", policy.Version)
	assert.Len(t, policy.Statement, 2)

	// First statement: Upload with content type restriction
	uploadStmt := policy.Statement[0]
	assert.Equal(t, "Allow", uploadStmt.Effect)
	assert.Equal(t, "ContentTypeRestrictedUpload", uploadStmt.Sid)
	assert.Contains(t, uploadStmt.Action, "s3:PutObject")
	assert.Contains(t, uploadStmt.Action, "s3:CreateMultipartUpload")
	assert.NotNil(t, uploadStmt.Condition)

	// Check content type condition
	condition := uploadStmt.Condition
	stringEquals, exists := condition["StringEquals"]
	assert.True(t, exists)

	contentType, exists := stringEquals["s3:content-type"]
	assert.True(t, exists)
	assert.Equal(t, allowedTypes, contentType)

	// Second statement: Read access without restrictions
	readStmt := policy.Statement[1]
	assert.Equal(t, "Allow", readStmt.Effect)
	assert.Equal(t, "ReadAccess", readStmt.Sid)
	assert.Contains(t, readStmt.Action, "s3:GetObject")
	assert.Contains(t, readStmt.Action, "s3:ListBucket")
	assert.Nil(t, readStmt.Condition) // No conditions for read access
}

func TestDenyDeletePolicy(t *testing.T) {
	templates := NewS3PolicyTemplates()

	policy := templates.GetDenyDeletePolicy()

	require.NotNil(t, policy)
	assert.Equal(t, "2012-10-17", policy.Version)
	assert.Len(t, policy.Statement, 2)

	// First statement: Allow everything except delete
	allowStmt := policy.Statement[0]
	assert.Equal(t, "Allow", allowStmt.Effect)
	assert.Equal(t, "AllowAllExceptDelete", allowStmt.Sid)
	assert.Contains(t, allowStmt.Action, "s3:GetObject")
	assert.Contains(t, allowStmt.Action, "s3:PutObject")
	assert.Contains(t, allowStmt.Action, "s3:ListBucket")
	assert.NotContains(t, allowStmt.Action, "s3:DeleteObject")
	assert.NotContains(t, allowStmt.Action, "s3:DeleteBucket")

	// Second statement: Explicitly deny delete operations
	denyStmt := policy.Statement[1]
	assert.Equal(t, "Deny", denyStmt.Effect)
	assert.Equal(t, "DenyDeleteOperations", denyStmt.Sid)
	assert.Contains(t, denyStmt.Action, "s3:DeleteObject")
	assert.Contains(t, denyStmt.Action, "s3:DeleteObjectVersion")
	assert.Contains(t, denyStmt.Action, "s3:DeleteBucket")
}

func TestPolicyTemplateMetadata(t *testing.T) {
	templates := NewS3PolicyTemplates()

	t.Run("GetAllPolicyTemplates", func(t *testing.T) {
		allTemplates := templates.GetAllPolicyTemplates()

		assert.Greater(t, len(allTemplates), 10) // Should have many templates

		// Check that each template has required fields
		for _, template := range allTemplates {
			assert.NotEmpty(t, template.Name)
			assert.NotEmpty(t, template.Description)
			assert.NotEmpty(t, template.Category)
			assert.NotEmpty(t, template.UseCase)
			assert.NotNil(t, template.Policy)
			assert.Equal(t, "2012-10-17", template.Policy.Version)
		}
	})

	t.Run("GetPolicyTemplateByName", func(t *testing.T) {
		// Test existing template
		template := templates.GetPolicyTemplateByName("S3ReadOnlyAccess")
		require.NotNil(t, template)
		assert.Equal(t, "S3ReadOnlyAccess", template.Name)
		assert.Equal(t, "Basic Access", template.Category)

		// Test non-existing template
		nonExistent := templates.GetPolicyTemplateByName("NonExistentTemplate")
		assert.Nil(t, nonExistent)
	})

	t.Run("GetPolicyTemplatesByCategory", func(t *testing.T) {
		basicAccessTemplates := templates.GetPolicyTemplatesByCategory("Basic Access")
		assert.GreaterOrEqual(t, len(basicAccessTemplates), 2)

		for _, template := range basicAccessTemplates {
			assert.Equal(t, "Basic Access", template.Category)
		}

		// Test non-existing category
		emptyCategory := templates.GetPolicyTemplatesByCategory("NonExistentCategory")
		assert.Empty(t, emptyCategory)
	})

	t.Run("PolicyTemplateParameters", func(t *testing.T) {
		allTemplates := templates.GetAllPolicyTemplates()

		// Find a template with parameters (like BucketSpecificRead)
		var templateWithParams *PolicyTemplateDefinition
		for _, template := range allTemplates {
			if template.Name == "BucketSpecificRead" {
				templateWithParams = &template
				break
			}
		}

		require.NotNil(t, templateWithParams)
		assert.Greater(t, len(templateWithParams.Parameters), 0)

		param := templateWithParams.Parameters[0]
		assert.Equal(t, "bucketName", param.Name)
		assert.Equal(t, "string", param.Type)
		assert.True(t, param.Required)
		assert.NotEmpty(t, param.Description)
		assert.NotEmpty(t, param.Example)
	})
}

func TestFormatHourHelper(t *testing.T) {
	tests := []struct {
		hour     int
		expected string
	}{
		{0, "00"},
		{5, "05"},
		{9, "09"},
		{10, "10"},
		{15, "15"},
		{23, "23"},
	}

	for _, tt := range tests {
		t.Run(fmt.Sprintf("Hour_%d", tt.hour), func(t *testing.T) {
			result := formatHour(tt.hour)
			assert.Equal(t, tt.expected, result)
		})
	}
}

func TestPolicyTemplateCategories(t *testing.T) {
	templates := NewS3PolicyTemplates()
	allTemplates := templates.GetAllPolicyTemplates()

	// Extract all categories
	categoryMap := make(map[string]int)
	for _, template := range allTemplates {
		categoryMap[template.Category]++
	}

	// Expected categories
	expectedCategories := []string{
		"Basic Access",
		"Administrative",
		"Bucket-Specific",
		"Path-Restricted",
		"Security",
		"Upload-Specific",
		"Presigned URLs",
		"Content Control",
		"Data Protection",
	}

	for _, expectedCategory := range expectedCategories {
		count, exists := categoryMap[expectedCategory]
		assert.True(t, exists, "Category %s should exist", expectedCategory)
		assert.Greater(t, count, 0, "Category %s should have at least one template", expectedCategory)
	}
}

func TestPolicyValidation(t *testing.T) {
	templates := NewS3PolicyTemplates()
	allTemplates := templates.GetAllPolicyTemplates()

	// Test that all policies have valid structure
	for _, template := range allTemplates {
		t.Run("Policy_"+template.Name, func(t *testing.T) {
			policy := template.Policy

			// Basic validation
			assert.Equal(t, "2012-10-17", policy.Version)
			assert.Greater(t, len(policy.Statement), 0)

			// Validate each statement
			for i, stmt := range policy.Statement {
				assert.NotEmpty(t, stmt.Effect, "Statement %d should have effect", i)
				assert.Contains(t, []string{"Allow", "Deny"}, stmt.Effect, "Statement %d effect should be Allow or Deny", i)
				assert.Greater(t, len(stmt.Action), 0, "Statement %d should have actions", i)
				assert.Greater(t, len(stmt.Resource), 0, "Statement %d should have resources", i)

				// Check resource format
				for _, resource := range stmt.Resource {
					if resource != "*" {
						assert.Contains(t, resource, "arn:seaweed:s3:::", "Resource should be valid SeaweedFS S3 ARN: %s", resource)
					}
				}
			}
		})
	}
}
