package iam

import (
	"fmt"
	"strings"
	"testing"
	"time"

	"github.com/aws/aws-sdk-go/aws"
	"github.com/aws/aws-sdk-go/service/s3"
	"github.com/stretchr/testify/assert"
	"github.com/stretchr/testify/require"
)

// TestS3PolicyVariablesUsernameInResource tests ${aws:username} in resource paths
func TestS3PolicyVariablesUsernameInResource(t *testing.T) {
	framework := NewS3IAMTestFramework(t)
	defer framework.Cleanup()

	adminClient, err := framework.CreateS3ClientWithJWT("admin-user", "TestAdminRole")
	require.NoError(t, err)

	bucketName := framework.GenerateUniqueBucketName("test-policy-vars")
	err = framework.CreateBucket(adminClient, bucketName)
	require.NoError(t, err)
	defer adminClient.DeleteBucket(&s3.DeleteBucketInput{Bucket: aws.String(bucketName)})

	// Policy with ${aws:username} in resource
	bucketPolicy := fmt.Sprintf(`{
		"Version": "2012-10-17",
		"Statement": [{
			"Effect": "Allow",
			"Principal": "*",
			"Action": ["s3:GetObject", "s3:PutObject"],
			"Resource": ["arn:aws:s3:::%s/${aws:username}/*"]
		}, {
			"Sid": "DenyOthers",
			"Effect": "Deny",
			"Principal": "*",
			"Action": ["s3:GetObject", "s3:PutObject"],
			"NotResource": ["arn:aws:s3:::%s/${aws:username}/*"]
		}]
	}`, bucketName, bucketName)

	_, err = adminClient.PutBucketPolicy(&s3.PutBucketPolicyInput{
		Bucket: aws.String(bucketName),
		Policy: aws.String(bucketPolicy),
	})
	require.NoError(t, err)

	// Verify policy contains variable
	policyResult, err := adminClient.GetBucketPolicy(&s3.GetBucketPolicyInput{
		Bucket: aws.String(bucketName),
	})
	require.NoError(t, err)
	assert.Contains(t, *policyResult.Policy, "${aws:username}")

	// Test Enforcement: Alice should be able to write to her own folder
	aliceClient, err := framework.CreateS3ClientWithJWT("alice", "TestReadOnlyRole")
	require.NoError(t, err)

	_, err = aliceClient.PutObject(&s3.PutObjectInput{
		Bucket: aws.String(bucketName),
		Key:    aws.String("alice/file.txt"),
		Body:   nil, // Empty body is fine for this test
	})
	assert.NoError(t, err, "Alice should be allowed to put to alice/file.txt")

	// Test Enforcement: Alice should NOT be able to write to bob's folder
	_, err = aliceClient.PutObject(&s3.PutObjectInput{
		Bucket: aws.String(bucketName),
		Key:    aws.String("bob/file.txt"),
		Body:   nil,
	})
	assert.Error(t, err, "Alice should be denied put to bob/file.txt")
}

// TestS3PolicyVariablesUsernameInResourcePath tests ${aws:username} in Resource/NotResource
// This validates that policy variables are correctly substituted in resource ARNs
func TestS3PolicyVariablesUsernameInResourcePath(t *testing.T) {
	framework := NewS3IAMTestFramework(t)
	defer framework.Cleanup()

	adminClient, err := framework.CreateS3ClientWithJWT("admin-user", "TestAdminRole")
	require.NoError(t, err)

	bucketName := framework.GenerateUniqueBucketName("test-policy-resource")
	err = framework.CreateBucket(adminClient, bucketName)
	require.NoError(t, err)
	defer adminClient.DeleteBucket(&s3.DeleteBucketInput{Bucket: aws.String(bucketName)})

	// Policy with variable in resource ARN
	bucketPolicy := fmt.Sprintf(`{
		"Version": "2012-10-17",
		"Statement": [{
			"Effect": "Allow",
			"Principal": "*",
			"Action": ["s3:GetObject", "s3:PutObject"],
			"Resource": ["arn:aws:s3:::%s/${aws:username}/*"]
		}, {
			"Sid": "DenyOthersFolders",
			"Effect": "Deny",
			"Principal": "*",
			"Action": ["s3:GetObject", "s3:PutObject"],
			"NotResource": ["arn:aws:s3:::%s/${aws:username}/*"]
		}]
	}`, bucketName, bucketName)

	_, err = adminClient.PutBucketPolicy(&s3.PutBucketPolicyInput{
		Bucket: aws.String(bucketName),
		Policy: aws.String(bucketPolicy),
	})
	require.NoError(t, err)

	policyResult, err := adminClient.GetBucketPolicy(&s3.GetBucketPolicyInput{
		Bucket: aws.String(bucketName),
	})
	require.NoError(t, err)
	assert.Contains(t, *policyResult.Policy, "${aws:username}")

	// Test Enforcement: Alice should be able to write to her own folder
	aliceClient, err := framework.CreateS3ClientWithJWT("alice", "TestReadOnlyRole")
	require.NoError(t, err)

	_, err = aliceClient.PutObject(&s3.PutObjectInput{
		Bucket: aws.String(bucketName),
		Key:    aws.String("alice/file.txt"),
		Body:   nil, // Empty body is fine for this test
	})
	assert.NoError(t, err, "Alice should be allowed to put to alice/file.txt")

	// Test Enforcement: Alice should NOT be able to write to bob's folder
	_, err = aliceClient.PutObject(&s3.PutObjectInput{
		Bucket: aws.String(bucketName),
		Key:    aws.String("bob/file.txt"),
		Body:   nil,
	})
	assert.Error(t, err, "Alice should be denied put to bob/file.txt")
}

// TestS3PolicyVariablesJWTClaims tests ${jwt:*} variables
func TestS3PolicyVariablesJWTClaims(t *testing.T) {
	framework := NewS3IAMTestFramework(t)
	defer framework.Cleanup()

	adminClient, err := framework.CreateS3ClientWithJWT("admin-user", "TestAdminRole")
	require.NoError(t, err)

	bucketName := framework.GenerateUniqueBucketName("test-policy-jwt")
	err = framework.CreateBucket(adminClient, bucketName)
	require.NoError(t, err)
	defer adminClient.DeleteBucket(&s3.DeleteBucketInput{Bucket: aws.String(bucketName)})

	// Policy with JWT claim variable
	bucketPolicy := fmt.Sprintf(`{
		"Version": "2012-10-17",
		"Statement": [{
			"Effect": "Allow",
			"Principal": "*",
			"Action": ["s3:GetObject"],
			"Resource": ["arn:aws:s3:::%s/${jwt:preferred_username}/*"]
		}]
	}`, bucketName)

	_, err = adminClient.PutBucketPolicy(&s3.PutBucketPolicyInput{
		Bucket: aws.String(bucketName),
		Policy: aws.String(bucketPolicy),
	})
	require.NoError(t, err)

	policyResult, err := adminClient.GetBucketPolicy(&s3.GetBucketPolicyInput{
		Bucket: aws.String(bucketName),
	})
	require.NoError(t, err)
	assert.Contains(t, *policyResult.Policy, "jwt:preferred_username")
}

func TestS3PolicyVariablesUsernameIsolation(t *testing.T) {
	framework := NewS3IAMTestFramework(t)
	defer framework.Cleanup()

	adminClient, err := framework.CreateS3ClientWithJWT("admin-user", "TestAdminRole")
	require.NoError(t, err)

	bucketName := framework.GenerateUniqueBucketName("test-isolation")
	err = framework.CreateBucket(adminClient, bucketName)
	require.NoError(t, err)
	defer adminClient.DeleteBucket(&s3.DeleteBucketInput{Bucket: aws.String(bucketName)})

	bucketPolicy := fmt.Sprintf(`{
		"Version": "2012-10-17",
		"Statement": [{
			"Sid": "AllowOwnFolder",
			"Effect": "Allow",
			"Principal": "*",
			"Action": ["s3:GetObject", "s3:PutObject"],
			"Resource": "arn:aws:s3:::%s/${aws:username}/*"
		}, {
			"Sid": "AllowListOwnPrefix",
			"Effect": "Allow",
			"Principal": "*",
			"Action": "s3:ListBucket",
			"Resource": "arn:aws:s3:::%s",
			"Condition": {
				"StringLike": {
					"s3:prefix": ["${aws:username}/*", "${aws:username}"]
				}
			}
		}, {
			"Sid": "DenyOtherFolders",
			"Effect": "Deny",
			"Principal": "*",
			"Action": ["s3:GetObject", "s3:PutObject"],
			"NotResource": "arn:aws:s3:::%s/${aws:username}/*"
		}, {
			"Sid": "DenyListOtherPrefixes",
			"Effect": "Deny",
			"Principal": "*",
			"Action": "s3:ListBucket",
			"Resource": "arn:aws:s3:::%s",
			"Condition": {
				"StringNotLike": {
					"s3:prefix": ["${aws:username}/*", "${aws:username}"]
				}
			}
		}]
	}`, bucketName, bucketName, bucketName, bucketName)

	_, err = adminClient.PutBucketPolicy(&s3.PutBucketPolicyInput{
		Bucket: aws.String(bucketName),
		Policy: aws.String(bucketPolicy),
	})
	require.NoError(t, err)

	// Wait for policy to propagate (fix race condition)
	time.Sleep(2 * time.Second)

	aliceClient, err := framework.CreateS3ClientWithJWT("alice", "TestReadOnlyRole")
	require.NoError(t, err)

	bobClient, err := framework.CreateS3ClientWithJWT("bob", "TestReadOnlyRole")
	require.NoError(t, err)

	_, err = aliceClient.PutObject(&s3.PutObjectInput{
		Bucket: aws.String(bucketName),
		Key:    aws.String("alice/data.txt"),
		Body:   strings.NewReader("Alice Private Data"),
	})
	assert.NoError(t, err, "Alice should be able to upload to her own folder")

	_, err = aliceClient.PutObject(&s3.PutObjectInput{
		Bucket: aws.String(bucketName),
		Key:    aws.String("bob/data.txt"),
		Body:   strings.NewReader("Alice Intrusion"),
	})
	assert.Error(t, err, "Alice should be denied access to Bob's folder")

	_, err = bobClient.PutObject(&s3.PutObjectInput{
		Bucket: aws.String(bucketName),
		Key:    aws.String("bob/data.txt"),
		Body:   strings.NewReader("Bob Private Data"),
	})
	assert.NoError(t, err, "Bob should be able to upload to his own folder")

	_, err = bobClient.GetObject(&s3.GetObjectInput{
		Bucket: aws.String(bucketName),
		Key:    aws.String("alice/data.txt"),
	})
	assert.Error(t, err, "Bob should be denied access to Alice's folder")

	listAlice, err := aliceClient.ListObjects(&s3.ListObjectsInput{
		Bucket: aws.String(bucketName),
		Prefix: aws.String("alice/"),
	})
	assert.NoError(t, err)
	assert.Equal(t, 1, len(listAlice.Contents))

	_, err = aliceClient.ListObjects(&s3.ListObjectsInput{
		Bucket: aws.String(bucketName),
		Prefix: aws.String("bob/"),
	})
	assert.Error(t, err, "Alice should be denied listing Bob's folder")
}

func TestS3PolicyVariablesAccountEnforcement(t *testing.T) {
	framework := NewS3IAMTestFramework(t)
	defer framework.Cleanup()

	adminClient, err := framework.CreateS3ClientWithJWT("admin-user", "TestAdminRole")
	require.NoError(t, err)

	bucketName := framework.GenerateUniqueBucketName("test-account")
	err = framework.CreateBucket(adminClient, bucketName)
	require.NoError(t, err)
	defer adminClient.DeleteBucket(&s3.DeleteBucketInput{Bucket: aws.String(bucketName)})

	bucketPolicy := fmt.Sprintf(`{
		"Version": "2012-10-17",
		"Statement": [{
			"Effect": "Deny",
			"Principal": "*",
			"Action": ["s3:*"],
			"Resource": ["arn:aws:s3:::%s/*"],
			"Condition": {
				"StringNotEquals": {
					"aws:PrincipalAccount": ["999988887777"]
				}
			}
		}, {
			"Effect": "Allow",
			"Principal": "*",
			"Action": ["s3:*"],
			"Resource": ["arn:aws:s3:::%s/*"]
		}]
	}`, bucketName, bucketName)

	_, err = adminClient.PutBucketPolicy(&s3.PutBucketPolicyInput{
		Bucket: aws.String(bucketName),
		Policy: aws.String(bucketPolicy),
	})
	require.NoError(t, err)

	authorizedClient, err := framework.CreateS3ClientWithCustomClaims("user1", "TestAdminRole", "999988887777", nil)
	require.NoError(t, err)

	unauthorizedClient, err := framework.CreateS3ClientWithCustomClaims("user2", "TestAdminRole", "111122223333", nil)
	require.NoError(t, err)

	_, err = authorizedClient.PutObject(&s3.PutObjectInput{
		Bucket: aws.String(bucketName),
		Key:    aws.String("test.txt"),
		Body:   strings.NewReader("Authorized Data"),
	})
	assert.NoError(t, err, "Authorized account should be able to upload")

	_, err = unauthorizedClient.PutObject(&s3.PutObjectInput{
		Bucket: aws.String(bucketName),
		Key:    aws.String("fail.txt"),
		Body:   strings.NewReader("Unauthorized Data"),
	})
	assert.Error(t, err, "Unauthorized account should be denied")
}

func TestS3PolicyVariablesJWTPreferredUsername(t *testing.T) {
	framework := NewS3IAMTestFramework(t)
	defer framework.Cleanup()

	adminClient, err := framework.CreateS3ClientWithJWT("admin-user", "TestAdminRole")
	require.NoError(t, err)

	bucketName := framework.GenerateUniqueBucketName("test-jwt-claim")
	err = framework.CreateBucket(adminClient, bucketName)
	require.NoError(t, err)
	defer adminClient.DeleteBucket(&s3.DeleteBucketInput{Bucket: aws.String(bucketName)})

	bucketPolicy := fmt.Sprintf(`{
		"Version": "2012-10-17",
		"Statement": [{
			"Sid": "AllowOwnFolder",
			"Effect": "Allow",
			"Principal": "*",
			"Action": "s3:PutObject",
			"Resource": "arn:aws:s3:::%s/${jwt:preferred_username}/*"
		}, {
			"Sid": "DenyOtherFolders",
			"Effect": "Deny",
			"Principal": "*",
			"Action": "s3:PutObject",
			"NotResource": "arn:aws:s3:::%s/${jwt:preferred_username}/*"
		}]
	}`, bucketName, bucketName)

	_, err = adminClient.PutBucketPolicy(&s3.PutBucketPolicyInput{
		Bucket: aws.String(bucketName),
		Policy: aws.String(bucketPolicy),
	})
	require.NoError(t, err)

	claims := map[string]interface{}{
		"preferred_username": "jdoe",
	}
	client, err := framework.CreateS3ClientWithCustomClaims("jdoe", "TestReadOnlyRole", "", claims)
	require.NoError(t, err)

	_, err = client.PutObject(&s3.PutObjectInput{
		Bucket: aws.String(bucketName),
		Key:    aws.String("jdoe/file.txt"),
		Body:   strings.NewReader("JWT Claim Data"),
	})
	assert.NoError(t, err, "Should allow access based on jwt:preferred_username")

	_, err = client.PutObject(&s3.PutObjectInput{
		Bucket: aws.String(bucketName),
		Key:    aws.String("other/file.txt"),
		Body:   strings.NewReader("JWT Claim Data"),
	})
	assert.Error(t, err, "Should deny access if prefix doesn't match jwt:preferred_username")
}

func TestS3PolicyVariablesLDAPClaims(t *testing.T) {
	framework := NewS3IAMTestFramework(t)
	defer framework.Cleanup()

	adminClient, err := framework.CreateS3ClientWithJWT("admin-user", "TestAdminRole")
	require.NoError(t, err)

	bucketName := framework.GenerateUniqueBucketName("test-ldap-claim")
	err = framework.CreateBucket(adminClient, bucketName)
	require.NoError(t, err)
	defer adminClient.DeleteBucket(&s3.DeleteBucketInput{Bucket: aws.String(bucketName)})

	bucketPolicy := fmt.Sprintf(`{
		"Version": "2012-10-17",
		"Statement": [{
			"Effect": "Allow",
			"Principal": "*",
			"Action": ["s3:PutObject"],
			"Resource": ["arn:aws:s3:::%s/${ldap:username}/*"]
		}, {
			"Effect": "Allow",
			"Principal": "*",
			"Action": ["s3:GetObject"],
			"Resource": ["arn:aws:s3:::%s/*"],
			"Condition": {
				"StringEquals": {
					"ldap:dn": ["cn=manager,dc=example,dc=org"]
				}
			}
		}]
	}`, bucketName, bucketName)

	_, err = adminClient.PutBucketPolicy(&s3.PutBucketPolicyInput{
		Bucket: aws.String(bucketName),
		Policy: aws.String(bucketPolicy),
	})
	require.NoError(t, err)

	claims := map[string]interface{}{
		"ldap:username": "manager",
		"ldap:dn":       "cn=manager,dc=example,dc=org",
	}
	client, err := framework.CreateS3ClientWithCustomClaims("manager", "TestReadOnlyRole", "", claims)
	require.NoError(t, err)

	_, err = client.PutObject(&s3.PutObjectInput{
		Bucket: aws.String(bucketName),
		Key:    aws.String("manager/data.txt"),
		Body:   strings.NewReader("LDAP Upload"),
	})
	assert.NoError(t, err)

	_, err = client.GetObject(&s3.GetObjectInput{
		Bucket: aws.String(bucketName),
		Key:    aws.String("manager/data.txt"),
	})
	assert.NoError(t, err, "Should allow download based on ldap:dn condition")
}
