package policy_engine

import (
	"net/http"
	"testing"
)

// requiresSSEPolicy is a bucket policy that denies PutObject when the
// x-amz-server-side-encryption header is absent (Null == true).
const requiresSSEPolicy = `{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Sid": "DenyUnencryptedUploads",
      "Effect": "Deny",
      "Principal": "*",
      "Action": "s3:PutObject",
      "Resource": "arn:aws:s3:::test-bucket/*",
      "Condition": {
        "Null": {
          "s3:x-amz-server-side-encryption": "true"
        }
      }
    }
  ]
}`

// requiresAES256Policy denies PutObject unless AES256 is explicitly requested.
const requiresAES256Policy = `{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Sid": "AllowAES256Only",
      "Effect": "Allow",
      "Principal": "*",
      "Action": "s3:PutObject",
      "Resource": "arn:aws:s3:::test-bucket/*",
      "Condition": {
        "StringEquals": {
          "s3:x-amz-server-side-encryption": "AES256"
        }
      }
    }
  ]
}`

// requiresKMSPolicy allows PutObject only when aws:kms encryption is requested.
const requiresKMSPolicy = `{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Sid": "AllowKMSOnly",
      "Effect": "Allow",
      "Principal": "*",
      "Action": "s3:PutObject",
      "Resource": "arn:aws:s3:::test-bucket/*",
      "Condition": {
        "StringEquals": {
          "s3:x-amz-server-side-encryption": "aws:kms"
        }
      }
    }
  ]
}`

// multipartPolicy denies PutObject when SSE is absent but should NOT block UploadPart.
const multipartPolicy = `{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Sid": "DenyUnencryptedUploads",
      "Effect": "Deny",
      "Principal": "*",
      "Action": ["s3:PutObject", "s3:UploadPart", "s3:UploadPartCopy"],
      "Resource": "arn:aws:s3:::test-bucket/*",
      "Condition": {
        "Null": {
          "s3:x-amz-server-side-encryption": "true"
        }
      }
    }
  ]
}`

func newEngineWithPolicy(t *testing.T, policy string) *PolicyEngine {
	t.Helper()
	engine := NewPolicyEngine()
	if err := engine.SetBucketPolicy("test-bucket", policy); err != nil {
		t.Fatalf("SetBucketPolicy: %v", err)
	}
	return engine
}

func evalArgs(action string, conditions map[string][]string) *PolicyEvaluationArgs {
	return &PolicyEvaluationArgs{
		Action:     action,
		Resource:   "arn:aws:s3:::test-bucket/object.txt",
		Principal:  "*",
		Conditions: conditions,
	}
}

func evalArgsWithSSE(action, inheritedSSE string) *PolicyEvaluationArgs {
	return &PolicyEvaluationArgs{
		Action:                action,
		Resource:              "arn:aws:s3:::test-bucket/object.txt",
		Principal:             "*",
		Conditions:            map[string][]string{},
		InheritedSSEAlgorithm: inheritedSSE,
	}
}

// TestSSEStringEqualsPresent – StringEquals with AES256 header present should Allow.
func TestSSEStringEqualsPresent(t *testing.T) {
	engine := newEngineWithPolicy(t, requiresAES256Policy)

	conditions := map[string][]string{
		"s3:x-amz-server-side-encryption": {"AES256"},
	}
	result := engine.EvaluatePolicy("test-bucket", evalArgs("s3:PutObject", conditions))
	if result != PolicyResultAllow {
		t.Errorf("expected Allow, got %v", result)
	}
}

// TestSSEStringEqualsWrongValue – StringEquals with wrong SSE value should not Allow.
func TestSSEStringEqualsWrongValue(t *testing.T) {
	engine := newEngineWithPolicy(t, requiresAES256Policy)

	conditions := map[string][]string{
		"s3:x-amz-server-side-encryption": {"aws:kms"},
	}
	result := engine.EvaluatePolicy("test-bucket", evalArgs("s3:PutObject", conditions))
	if result == PolicyResultAllow {
		t.Errorf("expected non-Allow, got %v", result)
	}
}

// TestSSENullConditionAbsent – Null("true") matches when header is absent → Deny.
func TestSSENullConditionAbsent(t *testing.T) {
	engine := newEngineWithPolicy(t, requiresSSEPolicy)

	// No SSE header → condition "Null == true" matches → Deny statement fires
	result := engine.EvaluatePolicy("test-bucket", evalArgs("s3:PutObject", map[string][]string{}))
	if result != PolicyResultDeny {
		t.Errorf("expected Deny (no SSE header), got %v", result)
	}
}

// TestSSENullConditionPresent – Null("true") does NOT match when header is present → not Deny.
func TestSSENullConditionPresent(t *testing.T) {
	engine := newEngineWithPolicy(t, requiresSSEPolicy)

	conditions := map[string][]string{
		"s3:x-amz-server-side-encryption": {"AES256"},
	}
	result := engine.EvaluatePolicy("test-bucket", evalArgs("s3:PutObject", conditions))
	// Deny condition not matched; no explicit Allow → Indeterminate
	if result == PolicyResultDeny {
		t.Errorf("expected non-Deny when SSE header present, got Deny")
	}
}

// TestSSECaseInsensitiveNormalizationAES256 drives the AES256 normalisation
// through EvaluatePolicyForRequest so that a regression in the production
// code path would be caught. The request carries the header in lowercase
// ("aes256"); after normalisation it must match the policy's "AES256" value.
func TestSSECaseInsensitiveNormalizationAES256(t *testing.T) {
	engine := newEngineWithPolicy(t, requiresAES256Policy)

	req, _ := http.NewRequest(http.MethodPut, "/", nil)
	req.RemoteAddr = "1.2.3.4:1234"
	req.Header.Set("X-Amz-Server-Side-Encryption", "aes256") // lowercase

	result := engine.EvaluatePolicyForRequest("test-bucket", "object.txt", "PutObject", "*", req)
	if result != PolicyResultAllow {
		t.Errorf("expected Allow after AES256 case normalisation, got %v", result)
	}
}

// TestSSECaseInsensitiveNormalizationKMS drives the aws:kms branch of the
// normalisation through the production code path. The request carries
// "AWS:KMS" (mixed case); after normalisation it must match "aws:kms".
func TestSSECaseInsensitiveNormalizationKMS(t *testing.T) {
	engine := newEngineWithPolicy(t, requiresKMSPolicy)

	req, _ := http.NewRequest(http.MethodPut, "/", nil)
	req.RemoteAddr = "1.2.3.4:1234"
	req.Header.Set("X-Amz-Server-Side-Encryption", "AWS:KMS") // mixed case

	result := engine.EvaluatePolicyForRequest("test-bucket", "object.txt", "PutObject", "*", req)
	if result != PolicyResultAllow {
		t.Errorf("expected Allow after aws:kms case normalisation, got %v", result)
	}
}

// TestSSEMultipartAES256Exempt – UploadPart with AES256 inherited from
// CreateMultipartUpload is not blocked by the Null("true") deny condition.
func TestSSEMultipartAES256Exempt(t *testing.T) {
	engine := newEngineWithPolicy(t, multipartPolicy)

	result := engine.EvaluatePolicy("test-bucket", evalArgsWithSSE("s3:UploadPart", "AES256"))
	if result == PolicyResultDeny {
		t.Errorf("UploadPart with inherited AES256 should not be Deny, got Deny")
	}
}

// TestSSEMultipartKMSExempt – UploadPart with aws:kms inherited from
// CreateMultipartUpload is not blocked by the Null("true") deny condition.
func TestSSEMultipartKMSExempt(t *testing.T) {
	engine := newEngineWithPolicy(t, multipartPolicy)

	result := engine.EvaluatePolicy("test-bucket", evalArgsWithSSE("s3:UploadPart", "aws:kms"))
	if result == PolicyResultDeny {
		t.Errorf("UploadPart with inherited aws:kms should not be Deny, got Deny")
	}
}

// TestSSEMultipartNoSSEDenied – UploadPart for an upload that had no SSE
// must still be denied by the Null("true") deny condition.
func TestSSEMultipartNoSSEDenied(t *testing.T) {
	engine := newEngineWithPolicy(t, multipartPolicy)

	// inheritedSSE="" means CreateMultipartUpload was sent without SSE
	result := engine.EvaluatePolicy("test-bucket", evalArgsWithSSE("s3:UploadPart", ""))
	if result != PolicyResultDeny {
		t.Errorf("UploadPart with no inherited SSE should be Deny, got %v", result)
	}
}

// TestSSEUploadPartCopyKMSExempt – UploadPartCopy with aws:kms is also exempt.
func TestSSEUploadPartCopyKMSExempt(t *testing.T) {
	engine := newEngineWithPolicy(t, multipartPolicy)

	result := engine.EvaluatePolicy("test-bucket", evalArgsWithSSE("s3:UploadPartCopy", "aws:kms"))
	if result == PolicyResultDeny {
		t.Errorf("UploadPartCopy with inherited aws:kms should not be Deny, got Deny")
	}
}

// TestSSEPutObjectStillBlockedWithoutHeader – regular PutObject still denied without SSE.
func TestSSEPutObjectStillBlockedWithoutHeader(t *testing.T) {
	engine := newEngineWithPolicy(t, multipartPolicy)

	result := engine.EvaluatePolicy("test-bucket", evalArgs("s3:PutObject", map[string][]string{}))
	if result != PolicyResultDeny {
		t.Errorf("PutObject without SSE should be Deny, got %v", result)
	}
}
