package s3api

import (
	"context"
	"encoding/json"
	"encoding/xml"
	"fmt"
	"net/http"
	"net/http/httptest"
	"net/url"
	"regexp"
	"strings"
	"sync"
	"testing"
	"time"

	"github.com/aws/aws-sdk-go/aws"
	"github.com/aws/aws-sdk-go/aws/arn"
	"github.com/aws/aws-sdk-go/aws/session"
	"github.com/aws/aws-sdk-go/service/iam"
	"github.com/gorilla/mux"
	"github.com/seaweedfs/seaweedfs/weed/credential"
	"github.com/seaweedfs/seaweedfs/weed/credential/memory"
	"github.com/seaweedfs/seaweedfs/weed/pb/iam_pb"
	"github.com/seaweedfs/seaweedfs/weed/s3api/policy_engine"
	. "github.com/seaweedfs/seaweedfs/weed/s3api/s3_constants"
	"github.com/seaweedfs/seaweedfs/weed/s3api/s3err"
	"github.com/seaweedfs/seaweedfs/weed/util/request_id"
	"github.com/stretchr/testify/assert"
	"github.com/stretchr/testify/require"
	"google.golang.org/protobuf/proto"
)

// EmbeddedIamApiForTest is a testable version of EmbeddedIamApi
type EmbeddedIamApiForTest struct {
	*EmbeddedIamApi
	mockConfig *iam_pb.S3ApiConfiguration
}

func NewEmbeddedIamApiForTest() *EmbeddedIamApiForTest {
	store := &memory.MemoryStore{}
	store.Initialize(nil, "")
	cm := &credential.CredentialManager{Store: store}
	e := &EmbeddedIamApiForTest{
		EmbeddedIamApi: &EmbeddedIamApi{
			iam:               &IdentityAccessManagement{credentialManager: cm},
			credentialManager: cm,
		},
		mockConfig: &iam_pb.S3ApiConfiguration{},
	}
	var syncOnce sync.Once
	e.getS3ApiConfigurationFunc = func(s3cfg *iam_pb.S3ApiConfiguration) error {
		// If mockConfig was set directly in test, sync it to store first (only once)
		var syncErr error
		syncOnce.Do(func() {
			if e.mockConfig != nil {
				syncErr = cm.SaveConfiguration(context.Background(), e.mockConfig)
			}
		})
		if syncErr != nil {
			return syncErr
		}
		config, err := cm.LoadConfiguration(context.Background())
		if err == nil {
			e.mockConfig = config
			proto.Reset(s3cfg)
			// Manually copy identities and other fields to avoid Merge issues with slices
			s3cfg.Identities = make([]*iam_pb.Identity, len(config.Identities))
			for i, ident := range config.Identities {
				s3cfg.Identities[i] = proto.Clone(ident).(*iam_pb.Identity)
			}
			s3cfg.Policies = make([]*iam_pb.Policy, len(config.Policies))
			for i, p := range config.Policies {
				s3cfg.Policies[i] = proto.Clone(p).(*iam_pb.Policy)
			}
			s3cfg.Groups = make([]*iam_pb.Group, len(config.Groups))
			for i, g := range config.Groups {
				s3cfg.Groups[i] = proto.Clone(g).(*iam_pb.Group)
			}
		}
		return err
	}
	e.putS3ApiConfigurationFunc = func(s3cfg *iam_pb.S3ApiConfiguration) error {
		e.mockConfig = proto.Clone(s3cfg).(*iam_pb.S3ApiConfiguration)
		return cm.SaveConfiguration(context.Background(), s3cfg)
	}
	e.reloadConfigurationFunc = func() error {
		config, err := cm.LoadConfiguration(context.Background())
		if err != nil {
			return err
		}
		e.mockConfig = config
		// Also refresh the IAM state so lookup functions see the updated configuration
		if err := e.iam.LoadS3ApiConfigurationFromCredentialManager(); err != nil {
			return err
		}
		return nil
	}
	return e
}

// DoActions handles IAM API actions for testing
func (e *EmbeddedIamApiForTest) DoActions(w http.ResponseWriter, r *http.Request) {
	// Call the real DoActions
	e.EmbeddedIamApi.DoActions(w, r)
}

// executeEmbeddedIamRequest executes an IAM request against the given API instance.
// If v is non-nil, the response body is unmarshalled into it.
func executeEmbeddedIamRequest(api *EmbeddedIamApiForTest, req *http.Request, v interface{}) (*httptest.ResponseRecorder, error) {
	rr := httptest.NewRecorder()
	apiRouter := mux.NewRouter().SkipClean(true)
	apiRouter.Path("/").Methods(http.MethodPost).HandlerFunc(api.DoActions)
	apiRouter.ServeHTTP(rr, req)
	if v != nil {
		if err := xml.Unmarshal(rr.Body.Bytes(), v); err != nil {
			return rr, err
		}
	}
	return rr, nil
}

// embeddedIamErrorResponseForTest is used for parsing IAM error responses in tests
type embeddedIamErrorResponseForTest struct {
	Error struct {
		Code    string `xml:"Code"`
		Message string `xml:"Message"`
	} `xml:"Error"`
}

func extractEmbeddedIamErrorCodeAndMessage(response *httptest.ResponseRecorder) (string, string) {
	body := response.Body.Bytes()
	// Try parsing with ErrorResponse root
	type localError struct {
		Code    string `xml:"Code"`
		Message string `xml:"Message"`
	}
	type localResponse struct {
		XMLName xml.Name   `xml:"ErrorResponse"`
		Error   localError `xml:"Error"`
	}
	var lr localResponse
	if err := xml.Unmarshal(body, &lr); err == nil && lr.Error.Code != "" {
		return lr.Error.Code, lr.Error.Message
	}

	// Try parsing with Error root
	type simpleError struct {
		XMLName xml.Name `xml:"Error"`
		Code    string   `xml:"Code"`
		Message string   `xml:"Message"`
	}
	var se simpleError
	if err := xml.Unmarshal(body, &se); err == nil && se.Code != "" {
		return se.Code, se.Message
	}

	var er embeddedIamErrorResponseForTest
	if err := xml.Unmarshal(body, &er); err == nil {
		return er.Error.Code, er.Error.Message
	}

	return "", ""
}

func extractEmbeddedIamRequestID(response *httptest.ResponseRecorder) string {
	re := regexp.MustCompile(`<RequestId>([^<]+)</RequestId>`)
	matches := re.FindStringSubmatch(response.Body.String())
	if len(matches) < 2 {
		return ""
	}
	return matches[1]
}

// TestEmbeddedIamCreateUser tests creating a user via the embedded IAM API
func TestEmbeddedIamCreateUser(t *testing.T) {
	api := NewEmbeddedIamApiForTest()
	api.mockConfig = &iam_pb.S3ApiConfiguration{}

	userName := aws.String("TestUser")
	params := &iam.CreateUserInput{UserName: userName}
	req, _ := iam.New(session.New()).CreateUserRequest(params)
	_ = req.Build()
	out := iamCreateUserResponse{}
	response, err := executeEmbeddedIamRequest(api, req.HTTPRequest, &out)
	assert.NoError(t, err)
	assert.Equal(t, http.StatusOK, response.Code)

	// Verify response contains correct username
	assert.NotNil(t, out.CreateUserResult.User.UserName)
	assert.Equal(t, "TestUser", *out.CreateUserResult.User.UserName)

	// Issue #9786: terraform aws provider >= 6.41 blocks on a valid ARN.
	assert.True(t, arn.IsARN(aws.StringValue(out.CreateUserResult.User.Arn)),
		"CreateUser must return a valid ARN, got %q", aws.StringValue(out.CreateUserResult.User.Arn))

	// Verify user was persisted in config
	assert.Len(t, api.mockConfig.Identities, 1)
	assert.Equal(t, "TestUser", api.mockConfig.Identities[0].Name)
}

// TestEmbeddedIamCreateUserDoesNotSaveAllUsers is a regression test for the bug
// where creating a new user triggered a full SaveConfiguration over all existing
// identities (causing N file writes + reload cycles in the filer_etc store).
// The fix uses credentialManager.CreateUser for a targeted single-file write.
func TestEmbeddedIamCreateUserDoesNotSaveAllUsers(t *testing.T) {
	api := NewEmbeddedIamApiForTest()
	ctx := context.Background()

	// Seed both mockConfig and the credential store directly so the test does
	// not rely on the getS3ApiConfigurationFunc fixture's syncOnce side effect
	// to populate the store. Pre-call assertions below fail loudly if seeding
	// breaks for any reason.
	existing := []string{"existing-user-1", "existing-user-2", "existing-user-3"}
	api.mockConfig = &iam_pb.S3ApiConfiguration{}
	for _, name := range existing {
		api.mockConfig.Identities = append(api.mockConfig.Identities, &iam_pb.Identity{Name: name})
	}
	require.NoError(t, api.credentialManager.SaveConfiguration(ctx, api.mockConfig))
	for _, name := range existing {
		u, err := api.credentialManager.GetUser(ctx, name)
		require.NoError(t, err, "seed precondition: %s must exist", name)
		require.Equal(t, name, u.Name)
	}

	// Track calls to PutS3ApiConfiguration (the full-config write path).
	putConfigCalls := 0
	api.putS3ApiConfigurationFunc = func(s3cfg *iam_pb.S3ApiConfiguration) error {
		putConfigCalls++
		api.mockConfig = proto.Clone(s3cfg).(*iam_pb.S3ApiConfiguration)
		return api.credentialManager.SaveConfiguration(ctx, s3cfg)
	}

	params := &iam.CreateUserInput{UserName: aws.String("new-user")}
	req, _ := iam.New(session.New()).CreateUserRequest(params)
	_ = req.Build()
	out := iamCreateUserResponse{}
	response, err := executeEmbeddedIamRequest(api, req.HTTPRequest, &out)
	require.NoError(t, err)
	require.Equal(t, http.StatusOK, response.Code)

	// The new user must be visible.
	newUser, userErr := api.credentialManager.GetUser(ctx, "new-user")
	require.NoError(t, userErr)
	require.Equal(t, "new-user", newUser.Name)

	// The critical assertion: full SaveConfiguration must NOT have been called.
	// Previously this was called once for every CreateUser, re-writing all N user
	// files and triggering N reload loops.
	assert.Equal(t, 0, putConfigCalls, "CreateUser must not call PutS3ApiConfiguration (full-config save)")

	// Pre-existing users must be untouched.
	for _, name := range existing {
		u, err := api.credentialManager.GetUser(ctx, name)
		require.NoError(t, err, "pre-existing user %s should still exist", name)
		assert.Equal(t, name, u.Name)
	}
}

// TestEmbeddedIamCreateUserSkipPersist asserts that ExecuteAction("CreateUser",
// skipPersist=true) performs no persistent write to the credential store. The
// targeted-create optimization must respect the skipPersist contract documented
// on ExecuteAction; otherwise no-persist callers would silently leak writes.
func TestEmbeddedIamCreateUserSkipPersist(t *testing.T) {
	api := NewEmbeddedIamApiForTest()
	ctx := context.Background()

	api.putS3ApiConfigurationFunc = func(s3cfg *iam_pb.S3ApiConfiguration) error {
		t.Fatalf("PutS3ApiConfiguration must not be called when skipPersist=true")
		return nil
	}

	vals := url.Values{}
	vals.Set("Action", "CreateUser")
	vals.Set("UserName", "no-persist-user")

	resp, iamErr := api.ExecuteAction(ctx, vals, true /*skipPersist*/, "")
	require.Nil(t, iamErr)
	require.NotNil(t, resp)

	// The credential store must not have the user — skipPersist contract.
	_, err := api.credentialManager.GetUser(ctx, "no-persist-user")
	require.ErrorIs(t, err, credential.ErrUserNotFound,
		"skipPersist=true must not write the new user to the credential store")
}

// TestEmbeddedIamListUsers tests listing users via the embedded IAM API
func TestEmbeddedIamListUsers(t *testing.T) {
	api := NewEmbeddedIamApiForTest()
	api.mockConfig = &iam_pb.S3ApiConfiguration{
		Identities: []*iam_pb.Identity{
			{Name: "User1"},
			{Name: "User2"},
		},
	}

	params := &iam.ListUsersInput{}
	req, _ := iam.New(session.New()).ListUsersRequest(params)
	_ = req.Build()
	out := iamListUsersResponse{}
	response, err := executeEmbeddedIamRequest(api, req.HTTPRequest, &out)
	assert.NoError(t, err)
	assert.Equal(t, http.StatusOK, response.Code)

	// Verify response contains the users
	assert.Len(t, out.ListUsersResult.Users, 2)
	assert.NotEmpty(t, response.Header().Get(request_id.AmzRequestIDHeader))
	assert.Equal(t, response.Header().Get(request_id.AmzRequestIDHeader), out.ResponseMetadata.RequestId)
}

// TestEmbeddedIamListAccessKeys tests listing access keys via the embedded IAM API
func TestEmbeddedIamListAccessKeys(t *testing.T) {
	api := NewEmbeddedIamApiForTest()
	svc := iam.New(session.New())
	params := &iam.ListAccessKeysInput{}
	req, _ := svc.ListAccessKeysRequest(params)
	_ = req.Build()
	out := iamListAccessKeysResponse{}
	response, err := executeEmbeddedIamRequest(api, req.HTTPRequest, &out)
	assert.NoError(t, err)
	assert.Equal(t, http.StatusOK, response.Code)
}

// TestEmbeddedIamGetUser tests getting a user via the embedded IAM API
func TestEmbeddedIamGetUser(t *testing.T) {
	api := NewEmbeddedIamApiForTest()
	api.mockConfig = &iam_pb.S3ApiConfiguration{
		Identities: []*iam_pb.Identity{
			{Name: "TestUser"},
		},
	}

	userName := aws.String("TestUser")
	params := &iam.GetUserInput{UserName: userName}
	req, _ := iam.New(session.New()).GetUserRequest(params)
	_ = req.Build()
	out := iamGetUserResponse{}
	response, err := executeEmbeddedIamRequest(api, req.HTTPRequest, &out)
	assert.NoError(t, err)
	assert.Equal(t, http.StatusOK, response.Code)

	// Verify response contains correct username
	assert.NotNil(t, out.GetUserResult.User.UserName)
	assert.Equal(t, "TestUser", *out.GetUserResult.User.UserName)

	// Issue #9786: the terraform aws provider reads the user back after creating
	// it and waits until GetUser returns a value that passes arn.IsARN.
	assert.True(t, arn.IsARN(aws.StringValue(out.GetUserResult.User.Arn)),
		"GetUser must return a valid ARN, got %q", aws.StringValue(out.GetUserResult.User.Arn))
}

// TestEmbeddedIamGetUserImplicitUsername verifies GetUser without a UserName defaults
// to the user that signed the request rather than returning NoSuchEntity for an empty
// username, matching documented AWS IAM behavior.
func TestEmbeddedIamGetUserImplicitUsername(t *testing.T) {
	const accessKey = UserAccessKeyPrefix + "TESTFAKEKEY000001"

	api := NewEmbeddedIamApiForTest()
	api.mockConfig = &iam_pb.S3ApiConfiguration{
		Identities: []*iam_pb.Identity{
			{
				Name: "TestUser",
				Credentials: []*iam_pb.Credential{
					{AccessKey: accessKey, SecretKey: "testsecretfake"},
				},
			},
		},
	}
	// handleImplicitUsername resolves the username via iam.LookupByAccessKey,
	// so the iam state must know about the credential.
	if err := api.iam.LoadS3ApiConfigurationFromBytes(mustMarshalJSON(api.mockConfig)); err != nil {
		t.Fatalf("failed to load iam config: %v", err)
	}

	// GetUser request with NO UserName, but signed by accessKey.
	form := url.Values{}
	form.Set("Action", "GetUser")
	req, _ := http.NewRequest(http.MethodPost, "/", strings.NewReader(form.Encode()))
	req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
	req.Header.Set("Authorization", "AWS4-HMAC-SHA256 Credential="+accessKey+
		"/20220420/us-east-1/iam/aws4_request, SignedHeaders=content-type;host;x-amz-date, Signature=fakesig")

	out := iamGetUserResponse{}
	rr, err := executeEmbeddedIamRequest(api, req, &out)
	require.NoError(t, err)
	require.Equal(t, http.StatusOK, rr.Code, "body=%s", rr.Body.String())

	require.NotNil(t, out.GetUserResult.User.UserName)
	assert.Equal(t, "TestUser", *out.GetUserResult.User.UserName)
}

// TestEmbeddedIamCreatePolicy tests creating a policy via the embedded IAM API
func TestEmbeddedIamCreatePolicy(t *testing.T) {
	api := NewEmbeddedIamApiForTest()
	params := &iam.CreatePolicyInput{
		PolicyName: aws.String("S3-read-only-example-bucket"),
		PolicyDocument: aws.String(`
			{
			  "Version": "2012-10-17",
			  "Statement": [
				{
				  "Effect": "Allow",
				  "Action": [
					"s3:Get*",
					"s3:List*"
				  ],
				  "Resource": [
					"arn:aws:s3:::EXAMPLE-BUCKET",
					"arn:aws:s3:::EXAMPLE-BUCKET/*"
				  ]
				}
			  ]
			}`),
	}
	req, _ := iam.New(session.New()).CreatePolicyRequest(params)
	_ = req.Build()
	out := iamCreatePolicyResponse{}
	response, err := executeEmbeddedIamRequest(api, req.HTTPRequest, &out)
	assert.NoError(t, err)
	assert.Equal(t, http.StatusOK, response.Code)

	// Verify response contains policy metadata
	assert.NotNil(t, out.CreatePolicyResult.Policy.PolicyName)
	assert.Equal(t, "S3-read-only-example-bucket", *out.CreatePolicyResult.Policy.PolicyName)
	assert.NotNil(t, out.CreatePolicyResult.Policy.Arn)
	assert.NotNil(t, out.CreatePolicyResult.Policy.PolicyId)
}

// TestEmbeddedIamCreatePolicyVersion covers the embedded IAM path for issue
// #9785: updating a managed policy in place via CreatePolicyVersion (used by the
// AWS Terraform provider) instead of returning 501 NotImplemented.
func TestEmbeddedIamCreatePolicyVersion(t *testing.T) {
	api := NewEmbeddedIamApiForTest()
	svc := iam.New(session.New())
	policyArn := aws.String("arn:aws:iam:::policy/tf-managed")

	// Create the managed policy.
	createReq, _ := svc.CreatePolicyRequest(&iam.CreatePolicyInput{
		PolicyName:     aws.String("tf-managed"),
		PolicyDocument: aws.String(`{"Version":"2012-10-17","Statement":[{"Effect":"Allow","Action":["s3:Get*","s3:List*"],"Resource":["arn:aws:s3:::EXAMPLE-BUCKET"]}]}`),
	})
	_ = createReq.Build()
	resp, err := executeEmbeddedIamRequest(api, createReq.HTTPRequest, nil)
	require.NoError(t, err)
	require.Equal(t, http.StatusOK, resp.Code)

	// Update it in place. This used to return 501 NotImplemented.
	cpvReq, _ := svc.CreatePolicyVersionRequest(&iam.CreatePolicyVersionInput{
		PolicyArn:      policyArn,
		PolicyDocument: aws.String(`{"Version":"2012-10-17","Statement":[{"Effect":"Allow","Action":["s3:Get*"],"Resource":["arn:aws:s3:::EXAMPLE-BUCKET"]}]}`),
		SetAsDefault:   aws.Bool(true),
	})
	_ = cpvReq.Build()
	out := iamCreatePolicyVersionResponse{}
	resp, err = executeEmbeddedIamRequest(api, cpvReq.HTTPRequest, &out)
	require.NoError(t, err)
	require.Equal(t, http.StatusOK, resp.Code)
	require.NotNil(t, out.CreatePolicyVersionResult.PolicyVersion.VersionId)
	assert.Equal(t, "v1", *out.CreatePolicyVersionResult.PolicyVersion.VersionId)
	require.NotNil(t, out.CreatePolicyVersionResult.PolicyVersion.IsDefaultVersion)
	assert.True(t, *out.CreatePolicyVersionResult.PolicyVersion.IsDefaultVersion)

	// The stored document must reflect the update.
	gpvReq, _ := svc.GetPolicyVersionRequest(&iam.GetPolicyVersionInput{PolicyArn: policyArn, VersionId: aws.String("v1")})
	_ = gpvReq.Build()
	gpvOut := iamGetPolicyVersionResponse{}
	resp, err = executeEmbeddedIamRequest(api, gpvReq.HTTPRequest, &gpvOut)
	require.NoError(t, err)
	require.Equal(t, http.StatusOK, resp.Code)
	require.NotNil(t, gpvOut.GetPolicyVersionResult.PolicyVersion.Document)
	assert.Contains(t, *gpvOut.GetPolicyVersionResult.PolicyVersion.Document, "s3:Get*")
	assert.NotContains(t, *gpvOut.GetPolicyVersionResult.PolicyVersion.Document, "s3:List*")
}

// TestEmbeddedIamCreatePolicyVersionMissingPolicy verifies NoSuchEntity when the
// target policy does not exist.
func TestEmbeddedIamCreatePolicyVersionMissingPolicy(t *testing.T) {
	api := NewEmbeddedIamApiForTest()
	req, _ := iam.New(session.New()).CreatePolicyVersionRequest(&iam.CreatePolicyVersionInput{
		PolicyArn:      aws.String("arn:aws:iam:::policy/does-not-exist"),
		PolicyDocument: aws.String(`{"Version":"2012-10-17","Statement":[{"Effect":"Allow","Action":["s3:Get*"],"Resource":["arn:aws:s3:::EXAMPLE-BUCKET"]}]}`),
		SetAsDefault:   aws.Bool(true),
	})
	_ = req.Build()
	resp, err := executeEmbeddedIamRequest(api, req.HTTPRequest, nil)
	require.NoError(t, err)
	assert.Equal(t, http.StatusNotFound, resp.Code)
	code, _ := extractEmbeddedIamErrorCodeAndMessage(resp)
	assert.Equal(t, "NoSuchEntity", code)
}

// TestEmbeddedIamCreatePolicyVersionRequiresSetAsDefault verifies the embedded
// path also rejects a non-default version request given the single-version model.
func TestEmbeddedIamCreatePolicyVersionRequiresSetAsDefault(t *testing.T) {
	api := NewEmbeddedIamApiForTest()
	svc := iam.New(session.New())
	policyArn := aws.String("arn:aws:iam:::policy/tf-managed")

	createReq, _ := svc.CreatePolicyRequest(&iam.CreatePolicyInput{
		PolicyName:     aws.String("tf-managed"),
		PolicyDocument: aws.String(`{"Version":"2012-10-17","Statement":[{"Effect":"Allow","Action":["s3:Get*"],"Resource":["arn:aws:s3:::EXAMPLE-BUCKET"]}]}`),
	})
	_ = createReq.Build()
	resp, err := executeEmbeddedIamRequest(api, createReq.HTTPRequest, nil)
	require.NoError(t, err)
	require.Equal(t, http.StatusOK, resp.Code)

	// SetAsDefault omitted must be rejected.
	cpvReq, _ := svc.CreatePolicyVersionRequest(&iam.CreatePolicyVersionInput{
		PolicyArn:      policyArn,
		PolicyDocument: aws.String(`{"Version":"2012-10-17","Statement":[{"Effect":"Allow","Action":["s3:Put*"],"Resource":["arn:aws:s3:::EXAMPLE-BUCKET"]}]}`),
	})
	_ = cpvReq.Build()
	resp, err = executeEmbeddedIamRequest(api, cpvReq.HTTPRequest, nil)
	require.NoError(t, err)
	assert.Equal(t, http.StatusBadRequest, resp.Code)
	code, _ := extractEmbeddedIamErrorCodeAndMessage(resp)
	assert.Equal(t, "InvalidInput", code)
}

// TestEmbeddedIamPutUserPolicy tests attaching a policy to a user
func TestEmbeddedIamPutUserPolicy(t *testing.T) {
	api := NewEmbeddedIamApiForTest()
	api.mockConfig = &iam_pb.S3ApiConfiguration{
		Identities: []*iam_pb.Identity{
			{Name: "TestUser"},
		},
	}

	userName := aws.String("TestUser")
	params := &iam.PutUserPolicyInput{
		UserName:   userName,
		PolicyName: aws.String("S3-read-only-example-bucket"),
		PolicyDocument: aws.String(
			`{
				  "Version": "2012-10-17",
				  "Statement": [
					{
					  "Effect": "Allow",
					  "Action": [
						"s3:Get*",
						"s3:List*"
					  ],
					  "Resource": [
						"arn:aws:s3:::EXAMPLE-BUCKET",
						"arn:aws:s3:::EXAMPLE-BUCKET/*"
					  ]
					}
				  ]
			}`),
	}
	req, _ := iam.New(session.New()).PutUserPolicyRequest(params)
	_ = req.Build()
	out := iamPutUserPolicyResponse{}
	response, err := executeEmbeddedIamRequest(api, req.HTTPRequest, &out)
	assert.NoError(t, err)
	assert.Equal(t, http.StatusOK, response.Code)

	// Verify policy was attached to the user (actions should be set)
	assert.Len(t, api.mockConfig.Identities, 1)
	assert.NotEmpty(t, api.mockConfig.Identities[0].Actions)
}

// TestEmbeddedIamPutUserPolicyError tests error handling when user doesn't exist
func TestEmbeddedIamPutUserPolicyError(t *testing.T) {
	api := NewEmbeddedIamApiForTest()
	api.mockConfig = &iam_pb.S3ApiConfiguration{}

	userName := aws.String("InvalidUser")
	params := &iam.PutUserPolicyInput{
		UserName:   userName,
		PolicyName: aws.String("S3-read-only-example-bucket"),
		PolicyDocument: aws.String(
			`{
				  "Version": "2012-10-17",
				  "Statement": [
					{
					  "Effect": "Allow",
					  "Action": [
						"s3:Get*",
						"s3:List*"
					  ],
					  "Resource": [
						"arn:aws:s3:::EXAMPLE-BUCKET",
						"arn:aws:s3:::EXAMPLE-BUCKET/*"
					  ]
					}
				  ]
			}`),
	}
	req, _ := iam.New(session.New()).PutUserPolicyRequest(params)
	_ = req.Build()
	response, err := executeEmbeddedIamRequest(api, req.HTTPRequest, nil)
	assert.NoError(t, err)
	assert.Equal(t, http.StatusNotFound, response.Code)

	expectedCode := "NoSuchEntity"
	code, _ := extractEmbeddedIamErrorCodeAndMessage(response)
	assert.Equal(t, expectedCode, code)
}

// TestEmbeddedIamGetUserPolicy tests getting a user's policy
func TestEmbeddedIamGetUserPolicy(t *testing.T) {
	api := NewEmbeddedIamApiForTest()
	api.mockConfig = &iam_pb.S3ApiConfiguration{
		Identities: []*iam_pb.Identity{
			{
				Name:    "TestUser",
				Actions: []string{"Read", "List"},
			},
		},
	}

	userName := aws.String("TestUser")
	params := &iam.GetUserPolicyInput{
		UserName:   userName,
		PolicyName: aws.String("S3-read-only-example-bucket"),
	}
	req, _ := iam.New(session.New()).GetUserPolicyRequest(params)
	_ = req.Build()
	out := iamGetUserPolicyResponse{}
	response, err := executeEmbeddedIamRequest(api, req.HTTPRequest, &out)
	assert.NoError(t, err)
	assert.Equal(t, http.StatusOK, response.Code)
}

// TestEmbeddedIamDeleteUserPolicy tests deleting a user's policy (clears actions)
func TestEmbeddedIamDeleteUserPolicy(t *testing.T) {
	api := NewEmbeddedIamApiForTest()
	api.mockConfig = &iam_pb.S3ApiConfiguration{
		Identities: []*iam_pb.Identity{
			{
				Name:    "TestUser",
				Actions: []string{"Read", "Write", "List"},
				Credentials: []*iam_pb.Credential{
					{AccessKey: UserAccessKeyPrefix + "TEST12345", SecretKey: "secret"},
				},
			},
		},
	}

	// Use direct form post for DeleteUserPolicy
	form := url.Values{}
	form.Set("Action", "DeleteUserPolicy")
	form.Set("UserName", "TestUser")
	form.Set("PolicyName", "TestPolicy")

	req, _ := http.NewRequest("POST", "/", nil)
	req.PostForm = form
	req.Form = form
	req.Header.Set("Content-Type", "application/x-www-form-urlencoded")

	rr := httptest.NewRecorder()
	apiRouter := mux.NewRouter().SkipClean(true)
	apiRouter.Path("/").Methods(http.MethodPost).HandlerFunc(api.DoActions)
	apiRouter.ServeHTTP(rr, req)

	assert.Equal(t, http.StatusOK, rr.Code)

	// CRITICAL: Verify user still exists (was NOT deleted)
	assert.Len(t, api.mockConfig.Identities, 1, "User should NOT be deleted")
	assert.Equal(t, "TestUser", api.mockConfig.Identities[0].Name)

	// Verify credentials are still intact
	assert.Len(t, api.mockConfig.Identities[0].Credentials, 1, "Credentials should NOT be deleted")
	assert.Equal(t, UserAccessKeyPrefix+"TEST12345", api.mockConfig.Identities[0].Credentials[0].AccessKey)

	// Verify actions/policy was cleared
	assert.Nil(t, api.mockConfig.Identities[0].Actions, "Actions should be cleared")
}

// TestEmbeddedIamDeleteUserPolicyUserNotFound tests error when user doesn't exist
func TestEmbeddedIamDeleteUserPolicyUserNotFound(t *testing.T) {
	api := NewEmbeddedIamApiForTest()
	api.mockConfig = &iam_pb.S3ApiConfiguration{}

	form := url.Values{}
	form.Set("Action", "DeleteUserPolicy")
	form.Set("UserName", "NonExistentUser")
	form.Set("PolicyName", "TestPolicy")

	req, _ := http.NewRequest("POST", "/", nil)
	req.PostForm = form
	req.Form = form
	req.Header.Set("Content-Type", "application/x-www-form-urlencoded")

	rr := httptest.NewRecorder()
	apiRouter := mux.NewRouter().SkipClean(true)
	apiRouter.Path("/").Methods(http.MethodPost).HandlerFunc(api.DoActions)
	apiRouter.ServeHTTP(rr, req)

	assert.Equal(t, http.StatusNotFound, rr.Code)
}

// TestEmbeddedIamListUserPolicies tests listing inline policies for a user.
func TestEmbeddedIamListUserPolicies(t *testing.T) {
	api := NewEmbeddedIamApiForTest()
	api.mockConfig = &iam_pb.S3ApiConfiguration{
		Identities: []*iam_pb.Identity{
			{
				Name:    "UserWithPolicy",
				Actions: []string{"Read", "Write"},
				Credentials: []*iam_pb.Credential{
					{AccessKey: UserAccessKeyPrefix + "TEST12345", SecretKey: "secret"},
				},
			},
			{
				Name: "UserWithoutPolicy",
				Credentials: []*iam_pb.Credential{
					{AccessKey: UserAccessKeyPrefix + "TEST67890", SecretKey: "secret"},
				},
			},
		},
	}

	// List policies for user with actions
	form := url.Values{}
	form.Set("Action", "ListUserPolicies")
	form.Set("UserName", "UserWithPolicy")

	req, _ := http.NewRequest("POST", "/", nil)
	req.PostForm = form
	req.Form = form
	req.Header.Set("Content-Type", "application/x-www-form-urlencoded")

	rr := httptest.NewRecorder()
	apiRouter := mux.NewRouter().SkipClean(true)
	apiRouter.Path("/").Methods(http.MethodPost).HandlerFunc(api.DoActions)
	apiRouter.ServeHTTP(rr, req)

	assert.Equal(t, http.StatusOK, rr.Code)
	assert.Contains(t, rr.Body.String(), "ListUserPoliciesResponse")
	assert.Contains(t, rr.Body.String(), "PolicyNames")
	assert.Contains(t, rr.Body.String(), "UserWithPolicy_policy")

	// List policies for user without actions
	form2 := url.Values{}
	form2.Set("Action", "ListUserPolicies")
	form2.Set("UserName", "UserWithoutPolicy")

	req2, _ := http.NewRequest("POST", "/", nil)
	req2.PostForm = form2
	req2.Form = form2
	req2.Header.Set("Content-Type", "application/x-www-form-urlencoded")

	rr2 := httptest.NewRecorder()
	apiRouter.ServeHTTP(rr2, req2)

	assert.Equal(t, http.StatusOK, rr2.Code)
	assert.Contains(t, rr2.Body.String(), "ListUserPoliciesResponse")

	// List policies for nonexistent user
	form3 := url.Values{}
	form3.Set("Action", "ListUserPolicies")
	form3.Set("UserName", "NonExistentUser")

	req3, _ := http.NewRequest("POST", "/", nil)
	req3.PostForm = form3
	req3.Form = form3
	req3.Header.Set("Content-Type", "application/x-www-form-urlencoded")

	rr3 := httptest.NewRecorder()
	apiRouter.ServeHTTP(rr3, req3)

	assert.Equal(t, http.StatusNotFound, rr3.Code)
}

// TestEmbeddedIamGroupInlinePolicies_CRUD exercises the Put/Get/List/Delete
// round-trip for group inline policies and verifies that an unknown policy
// surfaces as NoSuchEntity (not the legacy NotImplemented).
func TestEmbeddedIamGroupInlinePolicies_CRUD(t *testing.T) {
	api := NewEmbeddedIamApiForTest()
	s3cfg := &iam_pb.S3ApiConfiguration{
		Groups: []*iam_pb.Group{
			{Name: "developers", Members: []string{"alice"}},
		},
	}
	api.mockConfig = s3cfg

	policyDoc := `{"Version":"2012-10-17","Statement":[{"Effect":"Allow","Action":"s3:GetObject","Resource":"arn:aws:s3:::*"}]}`

	_, iamErr := api.PutGroupPolicy(s3cfg, url.Values{
		"GroupName":      {"developers"},
		"PolicyName":     {"DevPolicy"},
		"PolicyDocument": {policyDoc},
	})
	require.Nil(t, iamErr, "PutGroupPolicy must succeed; got: %+v", iamErr)

	listResp, iamErr := api.ListGroupPolicies(s3cfg, url.Values{"GroupName": {"developers"}})
	require.Nil(t, iamErr)
	assert.Equal(t, []string{"DevPolicy"}, listResp.ListGroupPoliciesResult.PolicyNames)

	getResp, iamErr := api.GetGroupPolicy(s3cfg, url.Values{"GroupName": {"developers"}, "PolicyName": {"DevPolicy"}})
	require.Nil(t, iamErr)
	assert.Equal(t, "developers", getResp.GetGroupPolicyResult.GroupName)
	assert.Equal(t, "DevPolicy", getResp.GetGroupPolicyResult.PolicyName)
	assert.Contains(t, getResp.GetGroupPolicyResult.PolicyDocument, "s3:GetObject")

	_, iamErr = api.DeleteGroupPolicy(s3cfg, url.Values{"GroupName": {"developers"}, "PolicyName": {"DevPolicy"}})
	require.Nil(t, iamErr)

	_, iamErr = api.GetGroupPolicy(s3cfg, url.Values{"GroupName": {"developers"}, "PolicyName": {"DevPolicy"}})
	require.NotNil(t, iamErr)
	assert.Equal(t, iam.ErrCodeNoSuchEntityException, iamErr.Code)

	_, iamErr = api.GetGroupPolicy(s3cfg, url.Values{"GroupName": {"ghosts"}, "PolicyName": {"DevPolicy"}})
	require.NotNil(t, iamErr, "GetGroupPolicy on a missing group must error")
	assert.Equal(t, iam.ErrCodeNoSuchEntityException, iamErr.Code)
}

// TestEmbeddedIamAttachUserPolicy tests attaching a managed policy to a user.
func TestEmbeddedIamAttachUserPolicy(t *testing.T) {
	api := NewEmbeddedIamApiForTest()
	api.mockConfig = &iam_pb.S3ApiConfiguration{
		Identities: []*iam_pb.Identity{
			{Name: "TestUser"},
		},
		Policies: []*iam_pb.Policy{
			{Name: "TestManagedPolicy", Content: `{"Version":"2012-10-17","Statement":[]}`},
		},
	}

	params := &iam.AttachUserPolicyInput{
		UserName:  aws.String("TestUser"),
		PolicyArn: aws.String("arn:aws:iam:::policy/TestManagedPolicy"),
	}
	req, _ := iam.New(session.New()).AttachUserPolicyRequest(params)
	_ = req.Build()

	out := iamAttachUserPolicyResponse{}
	response, err := executeEmbeddedIamRequest(api, req.HTTPRequest, &out)
	assert.NoError(t, err)
	assert.Equal(t, http.StatusOK, response.Code)
	assert.Equal(t, []string{"TestManagedPolicy"}, api.mockConfig.Identities[0].PolicyNames)
}

func TestEmbeddedIamAttachUserPolicyRefreshesIAM(t *testing.T) {
	api := NewEmbeddedIamApiForTest()
	ctx := context.Background()
	cm := api.credentialManager
	user := &iam_pb.Identity{
		Name: "policyRefreshUser",
		Credentials: []*iam_pb.Credential{
			{AccessKey: "REFRESHACCESS", SecretKey: "REFRESHSECRET"},
		},
	}
	require.NoError(t, cm.CreateUser(ctx, user))
	policy := policy_engine.PolicyDocument{
		Version: policy_engine.PolicyVersion2012_10_17,
		Statement: []policy_engine.PolicyStatement{
			{
				Effect:   policy_engine.PolicyEffectAllow,
				Action:   policy_engine.NewStringOrStringSlice("s3:GetObject"),
				Resource: policy_engine.NewStringOrStringSlicePtr("arn:aws:s3:::bucket/*"),
			},
		},
	}
	require.NoError(t, cm.PutPolicy(ctx, "RefreshPolicy", policy))
	require.NoError(t, api.iam.LoadS3ApiConfigurationFromCredentialManager())

	identity := api.iam.lookupByIdentityName("policyRefreshUser")
	require.NotNil(t, identity)
	assert.Empty(t, identity.PolicyNames)

	values := url.Values{}
	values.Set("UserName", "policyRefreshUser")
	values.Set("PolicyArn", "arn:aws:iam:::policy/RefreshPolicy")

	_, iamErr := api.AttachUserPolicy(ctx, values)
	require.Nil(t, iamErr)

	identity = api.iam.lookupByIdentityName("policyRefreshUser")
	require.NotNil(t, identity)
	assert.Equal(t, []string{"RefreshPolicy"}, identity.PolicyNames)
}

// TestEmbeddedIamAttachUserPolicyNoSuchPolicy tests attach failure when managed policy does not exist.
func TestEmbeddedIamAttachUserPolicyNoSuchPolicy(t *testing.T) {
	api := NewEmbeddedIamApiForTest()
	api.mockConfig = &iam_pb.S3ApiConfiguration{
		Identities: []*iam_pb.Identity{
			{Name: "TestUser"},
		},
	}

	params := &iam.AttachUserPolicyInput{
		UserName:  aws.String("TestUser"),
		PolicyArn: aws.String("arn:aws:iam:::policy/DoesNotExist"),
	}
	req, _ := iam.New(session.New()).AttachUserPolicyRequest(params)
	_ = req.Build()

	response, err := executeEmbeddedIamRequest(api, req.HTTPRequest, nil)
	assert.NoError(t, err)
	assert.Equal(t, http.StatusNotFound, response.Code)
	code, _ := extractEmbeddedIamErrorCodeAndMessage(response)
	assert.Equal(t, "NoSuchEntity", code)
}

// TestEmbeddedIamDetachUserPolicy tests detaching a managed policy from a user.
func TestEmbeddedIamDetachUserPolicy(t *testing.T) {
	api := NewEmbeddedIamApiForTest()
	api.mockConfig = &iam_pb.S3ApiConfiguration{
		Identities: []*iam_pb.Identity{
			{Name: "TestUser", PolicyNames: []string{"TestManagedPolicy", "KeepPolicy"}},
		},
		Policies: []*iam_pb.Policy{
			{Name: "TestManagedPolicy", Content: `{"Version":"2012-10-17","Statement":[]}`},
			{Name: "KeepPolicy", Content: `{"Version":"2012-10-17","Statement":[]}`},
		},
	}

	params := &iam.DetachUserPolicyInput{
		UserName:  aws.String("TestUser"),
		PolicyArn: aws.String("arn:aws:iam:::policy/TestManagedPolicy"),
	}
	req, _ := iam.New(session.New()).DetachUserPolicyRequest(params)
	_ = req.Build()

	out := iamDetachUserPolicyResponse{}
	response, err := executeEmbeddedIamRequest(api, req.HTTPRequest, &out)
	assert.NoError(t, err)
	assert.Equal(t, http.StatusOK, response.Code)
	assert.Equal(t, []string{"KeepPolicy"}, api.mockConfig.Identities[0].PolicyNames)
}

// TestEmbeddedIamDeletePolicyInUse ensures deleting a policy that is still attached returns conflict.
func TestEmbeddedIamDeletePolicyInUse(t *testing.T) {
	api := NewEmbeddedIamApiForTest()
	api.mockConfig = &iam_pb.S3ApiConfiguration{
		Identities: []*iam_pb.Identity{
			{Name: "TestUser", PolicyNames: []string{"TestPolicy"}},
		},
		Policies: []*iam_pb.Policy{
			{Name: "TestPolicy", Content: `{"Version":"2012-10-17","Statement":[]}`},
		},
	}

	params := &iam.DeletePolicyInput{
		PolicyArn: aws.String("arn:aws:iam:::policy/TestPolicy"),
	}
	req, _ := iam.New(session.New()).DeletePolicyRequest(params)
	_ = req.Build()

	response, err := executeEmbeddedIamRequest(api, req.HTTPRequest, nil)
	assert.NoError(t, err)
	assert.Equal(t, http.StatusConflict, response.Code)
	code, _ := extractEmbeddedIamErrorCodeAndMessage(response)
	assert.Equal(t, iam.ErrCodeDeleteConflictException, code)

	assert.Len(t, api.mockConfig.Policies, 1)
	assert.Equal(t, "TestPolicy", api.mockConfig.Policies[0].Name)
	assert.Len(t, api.mockConfig.Identities, 1)
	assert.Equal(t, "TestUser", api.mockConfig.Identities[0].Name)
	assert.Contains(t, api.mockConfig.Identities[0].PolicyNames, "TestPolicy")
}

// TestEmbeddedIamAttachAlreadyAttachedPolicy ensures attaching a policy already
// present on the user is idempotent.
func TestEmbeddedIamAttachAlreadyAttachedPolicy(t *testing.T) {
	api := NewEmbeddedIamApiForTest()
	api.mockConfig = &iam_pb.S3ApiConfiguration{
		Identities: []*iam_pb.Identity{
			{Name: "TestUser", PolicyNames: []string{"TestManagedPolicy"}},
		},
		Policies: []*iam_pb.Policy{
			{Name: "TestManagedPolicy", Content: `{"Version":"2012-10-17","Statement":[]}`},
		},
	}

	params := &iam.AttachUserPolicyInput{
		UserName:  aws.String("TestUser"),
		PolicyArn: aws.String("arn:aws:iam:::policy/TestManagedPolicy"),
	}
	req, _ := iam.New(session.New()).AttachUserPolicyRequest(params)
	_ = req.Build()

	out := iamAttachUserPolicyResponse{}
	response, err := executeEmbeddedIamRequest(api, req.HTTPRequest, &out)
	assert.NoError(t, err)
	assert.Equal(t, http.StatusOK, response.Code)
	assert.Equal(t, []string{"TestManagedPolicy"}, api.mockConfig.Identities[0].PolicyNames)
}

// TestEmbeddedIamDetachNotAttachedPolicy verifies detaching a policy that's not
// attached returns NoSuchEntity.
func TestEmbeddedIamDetachNotAttachedPolicy(t *testing.T) {
	api := NewEmbeddedIamApiForTest()
	api.mockConfig = &iam_pb.S3ApiConfiguration{
		Identities: []*iam_pb.Identity{
			{Name: "TestUser"},
		},
		Policies: []*iam_pb.Policy{
			{Name: "MissingPolicy", Content: `{"Version":"2012-10-17","Statement":[]}`},
		},
	}

	params := &iam.DetachUserPolicyInput{
		UserName:  aws.String("TestUser"),
		PolicyArn: aws.String("arn:aws:iam:::policy/MissingPolicy"),
	}
	req, _ := iam.New(session.New()).DetachUserPolicyRequest(params)
	_ = req.Build()

	response, err := executeEmbeddedIamRequest(api, req.HTTPRequest, nil)
	assert.NoError(t, err)
	assert.Equal(t, http.StatusNotFound, response.Code)
	code, _ := extractEmbeddedIamErrorCodeAndMessage(response)
	assert.Equal(t, "NoSuchEntity", code)
}

// TestEmbeddedIamAttachPolicyLimitExceeded ensures we honor the managed policy limit.
func TestEmbeddedIamAttachPolicyLimitExceeded(t *testing.T) {
	api := NewEmbeddedIamApiForTest()
	existingPolicies := make([]string, 0, MaxManagedPoliciesPerUser)
	configPolicies := make([]*iam_pb.Policy, 0, MaxManagedPoliciesPerUser+1)
	for i := 0; i < MaxManagedPoliciesPerUser; i++ {
		name := fmt.Sprintf("ManagedPolicy%d", i)
		existingPolicies = append(existingPolicies, name)
		configPolicies = append(configPolicies, &iam_pb.Policy{
			Name:    name,
			Content: `{"Version":"2012-10-17","Statement":[]}`,
		})
	}
	configPolicies = append(configPolicies, &iam_pb.Policy{
		Name:    "NewPolicy",
		Content: `{"Version":"2012-10-17","Statement":[]}`,
	})

	api.mockConfig = &iam_pb.S3ApiConfiguration{
		Identities: []*iam_pb.Identity{
			{Name: "TestUser", PolicyNames: existingPolicies},
		},
		Policies: configPolicies,
	}

	params := &iam.AttachUserPolicyInput{
		UserName:  aws.String("TestUser"),
		PolicyArn: aws.String("arn:aws:iam:::policy/NewPolicy"),
	}
	req, _ := iam.New(session.New()).AttachUserPolicyRequest(params)
	_ = req.Build()

	response, err := executeEmbeddedIamRequest(api, req.HTTPRequest, nil)
	assert.NoError(t, err)
	assert.Equal(t, http.StatusForbidden, response.Code)
	code, _ := extractEmbeddedIamErrorCodeAndMessage(response)
	assert.Equal(t, iam.ErrCodeLimitExceededException, code)
	assert.Len(t, api.mockConfig.Identities[0].PolicyNames, MaxManagedPoliciesPerUser)
}

// TestEmbeddedIamListAttachedUserPolicies tests listing managed policies attached to a user.
func TestEmbeddedIamListAttachedUserPolicies(t *testing.T) {
	api := NewEmbeddedIamApiForTest()
	api.mockConfig = &iam_pb.S3ApiConfiguration{
		Identities: []*iam_pb.Identity{
			{Name: "TestUser", PolicyNames: []string{"PolicyA", "PolicyB"}},
		},
		Policies: []*iam_pb.Policy{
			{Name: "PolicyA", Content: `{"Version":"2012-10-17","Statement":[]}`},
			{Name: "PolicyB", Content: `{"Version":"2012-10-17","Statement":[]}`},
		},
	}

	params := &iam.ListAttachedUserPoliciesInput{
		UserName: aws.String("TestUser"),
	}
	req, _ := iam.New(session.New()).ListAttachedUserPoliciesRequest(params)
	_ = req.Build()

	out := iamListAttachedUserPoliciesResponse{}
	response, err := executeEmbeddedIamRequest(api, req.HTTPRequest, &out)
	assert.NoError(t, err)
	assert.Equal(t, http.StatusOK, response.Code)
	assert.False(t, out.ListAttachedUserPoliciesResult.IsTruncated)
	assert.Len(t, out.ListAttachedUserPoliciesResult.AttachedPolicies, 2)

	got := map[string]string{}
	for _, attached := range out.ListAttachedUserPoliciesResult.AttachedPolicies {
		got[aws.StringValue(attached.PolicyName)] = aws.StringValue(attached.PolicyArn)
	}
	assert.Equal(t, "arn:aws:iam:::policy/PolicyA", got["PolicyA"])
	assert.Equal(t, "arn:aws:iam:::policy/PolicyB", got["PolicyB"])
}

// TestEmbeddedIamUpdateUser tests updating a user
func TestEmbeddedIamUpdateUser(t *testing.T) {
	api := NewEmbeddedIamApiForTest()
	api.mockConfig = &iam_pb.S3ApiConfiguration{
		Identities: []*iam_pb.Identity{
			{Name: "TestUser"},
		},
	}

	userName := aws.String("TestUser")
	newUserName := aws.String("TestUser-New")
	params := &iam.UpdateUserInput{NewUserName: newUserName, UserName: userName}
	req, _ := iam.New(session.New()).UpdateUserRequest(params)
	_ = req.Build()
	out := iamUpdateUserResponse{}
	response, err := executeEmbeddedIamRequest(api, req.HTTPRequest, &out)
	assert.NoError(t, err)
	assert.Equal(t, http.StatusOK, response.Code)
}

// TestEmbeddedIamUpdateUserPreservesInlinePolicies makes sure renaming a
// user keeps that user's stored inline policies under the new name.
// Without the explicit policy migration in UpdateUser, SaveConfiguration
// prunes the old name and CASCADE drops the policies.
func TestEmbeddedIamUpdateUserPreservesInlinePolicies(t *testing.T) {
	api := NewEmbeddedIamApiForTest()
	const (
		oldName    = "RenameMe"
		newName    = "Renamed"
		policyName = "ReadBucket"
	)
	api.mockConfig = &iam_pb.S3ApiConfiguration{
		Identities: []*iam_pb.Identity{{Name: oldName}},
	}
	doc := policy_engine.PolicyDocument{
		Version: "2012-10-17",
		Statement: []policy_engine.PolicyStatement{{
			Effect:   policy_engine.PolicyEffectAllow,
			Action:   policy_engine.NewStringOrStringSlice("s3:GetObject"),
			Resource: policy_engine.NewStringOrStringSlicePtr("arn:aws:s3:::bucket/*"),
		}},
	}
	require.NoError(t, api.credentialManager.PutUserInlinePolicy(context.Background(), oldName, policyName, doc))

	params := &iam.UpdateUserInput{NewUserName: aws.String(newName), UserName: aws.String(oldName)}
	req, _ := iam.New(session.New()).UpdateUserRequest(params)
	_ = req.Build()
	out := iamUpdateUserResponse{}
	response, err := executeEmbeddedIamRequest(api, req.HTTPRequest, &out)
	assert.NoError(t, err)
	assert.Equal(t, http.StatusOK, response.Code)

	got, err := api.credentialManager.GetUserInlinePolicy(context.Background(), newName, policyName)
	require.NoError(t, err)
	require.NotNil(t, got, "inline policy should have been migrated to the new username")
	assert.Equal(t, doc.Version, got.Version)
	require.Len(t, got.Statement, 1)
	assert.Equal(t, policy_engine.PolicyEffectAllow, got.Statement[0].Effect)
}

// TestEmbeddedIamDeleteUser tests deleting a user
func TestEmbeddedIamDeleteUser(t *testing.T) {
	api := NewEmbeddedIamApiForTest()
	api.mockConfig = &iam_pb.S3ApiConfiguration{
		Identities: []*iam_pb.Identity{
			{Name: "TestUser-New"},
		},
	}

	userName := aws.String("TestUser-New")
	params := &iam.DeleteUserInput{UserName: userName}
	req, _ := iam.New(session.New()).DeleteUserRequest(params)
	_ = req.Build()
	out := iamDeleteUserResponse{}
	response, err := executeEmbeddedIamRequest(api, req.HTTPRequest, &out)
	assert.NoError(t, err)
	assert.Equal(t, http.StatusOK, response.Code)
}

// TestEmbeddedIamCreateAccessKey tests creating an access key
func TestEmbeddedIamCreateAccessKey(t *testing.T) {
	api := NewEmbeddedIamApiForTest()
	api.mockConfig = &iam_pb.S3ApiConfiguration{
		Identities: []*iam_pb.Identity{
			{Name: "TestUser"},
		},
	}

	userName := aws.String("TestUser")
	params := &iam.CreateAccessKeyInput{UserName: userName}
	req, _ := iam.New(session.New()).CreateAccessKeyRequest(params)
	_ = req.Build()
	out := iamCreateAccessKeyResponse{}
	response, err := executeEmbeddedIamRequest(api, req.HTTPRequest, &out)
	assert.NoError(t, err)
	assert.Equal(t, http.StatusOK, response.Code)

	// Verify response contains access key credentials
	assert.NotNil(t, out.CreateAccessKeyResult.AccessKey.AccessKeyId)
	assert.NotEmpty(t, *out.CreateAccessKeyResult.AccessKey.AccessKeyId)
	assert.NotNil(t, out.CreateAccessKeyResult.AccessKey.SecretAccessKey)
	assert.NotEmpty(t, *out.CreateAccessKeyResult.AccessKey.SecretAccessKey)
	assert.NotNil(t, out.CreateAccessKeyResult.AccessKey.UserName)
	assert.Equal(t, "TestUser", *out.CreateAccessKeyResult.AccessKey.UserName)

	// Verify credentials were persisted
	assert.Len(t, api.mockConfig.Identities[0].Credentials, 1)
}

// TestEmbeddedIamCreateAccessKeyRejectsMissingUser verifies CreateAccessKey
// returns NoSuchEntity for an unknown user without mutating the config.
func TestEmbeddedIamCreateAccessKeyRejectsMissingUser(t *testing.T) {
	api := NewEmbeddedIamApiForTest()
	api.mockConfig = &iam_pb.S3ApiConfiguration{
		Identities: []*iam_pb.Identity{{Name: "ExistingUser"}},
	}

	form := url.Values{}
	form.Set("Action", "CreateAccessKey")
	form.Set("UserName", "GhostUser")

	req, _ := http.NewRequest("POST", "/", nil)
	req.PostForm = form
	req.Form = form
	req.Header.Set("Content-Type", "application/x-www-form-urlencoded")

	rr := httptest.NewRecorder()
	apiRouter := mux.NewRouter().SkipClean(true)
	apiRouter.Path("/").Methods(http.MethodPost).HandlerFunc(api.DoActions)
	apiRouter.ServeHTTP(rr, req)

	assert.NotEqual(t, http.StatusOK, rr.Code)
	// No new identity and no credential appended to the existing one.
	assert.Len(t, api.mockConfig.Identities, 1)
	assert.Len(t, api.mockConfig.Identities[0].Credentials, 0)
}

// TestEmbeddedIamCreateAccessKeyWithCallerSuppliedKeys tests creating an access key with caller-supplied credentials
func TestEmbeddedIamCreateAccessKeyWithCallerSuppliedKeys(t *testing.T) {
	api := NewEmbeddedIamApiForTest()
	api.mockConfig = &iam_pb.S3ApiConfiguration{
		Identities: []*iam_pb.Identity{
			{Name: "TestUser"},
		},
	}

	form := url.Values{}
	form.Set("Action", "CreateAccessKey")
	form.Set("UserName", "TestUser")
	form.Set("AccessKeyId", "myapp")
	form.Set("SecretAccessKey", "mysecret123")

	req, _ := http.NewRequest("POST", "/", nil)
	req.PostForm = form
	req.Form = form
	req.Header.Set("Content-Type", "application/x-www-form-urlencoded")

	rr := httptest.NewRecorder()
	apiRouter := mux.NewRouter().SkipClean(true)
	apiRouter.Path("/").Methods(http.MethodPost).HandlerFunc(api.DoActions)
	apiRouter.ServeHTTP(rr, req)

	assert.Equal(t, http.StatusOK, rr.Code)

	// Verify caller-supplied keys were used, not random ones
	var out iamCreateAccessKeyResponse
	err := xml.Unmarshal(rr.Body.Bytes(), &out)
	require.NoError(t, err, "failed to unmarshal CreateAccessKey response")
	require.NotNil(t, out.CreateAccessKeyResult.AccessKey.AccessKeyId)
	require.NotNil(t, out.CreateAccessKeyResult.AccessKey.SecretAccessKey)
	require.NotNil(t, out.CreateAccessKeyResult.AccessKey.UserName)
	assert.Equal(t, "myapp", *out.CreateAccessKeyResult.AccessKey.AccessKeyId)
	assert.Equal(t, "mysecret123", *out.CreateAccessKeyResult.AccessKey.SecretAccessKey)
	assert.Equal(t, "TestUser", *out.CreateAccessKeyResult.AccessKey.UserName)

	// Verify credentials were persisted with caller-supplied keys
	require.Len(t, api.mockConfig.Identities, 1)
	require.Len(t, api.mockConfig.Identities[0].Credentials, 1)
	assert.Equal(t, "myapp", api.mockConfig.Identities[0].Credentials[0].AccessKey)
	assert.Equal(t, "mysecret123", api.mockConfig.Identities[0].Credentials[0].SecretKey)
}

// TestEmbeddedIamCreateAccessKeyRejectsWeakKeys tests that weak caller-supplied keys are rejected
func TestEmbeddedIamCreateAccessKeyRejectsWeakKeys(t *testing.T) {
	api := NewEmbeddedIamApiForTest()
	api.mockConfig = &iam_pb.S3ApiConfiguration{
		Identities: []*iam_pb.Identity{
			{Name: "TestUser"},
		},
	}

	// AccessKeyId too short
	form := url.Values{}
	form.Set("Action", "CreateAccessKey")
	form.Set("UserName", "TestUser")
	form.Set("AccessKeyId", "ab")
	form.Set("SecretAccessKey", "validsecret123")

	req, _ := http.NewRequest("POST", "/", nil)
	req.PostForm = form
	req.Form = form
	req.Header.Set("Content-Type", "application/x-www-form-urlencoded")

	rr := httptest.NewRecorder()
	apiRouter := mux.NewRouter().SkipClean(true)
	apiRouter.Path("/").Methods(http.MethodPost).HandlerFunc(api.DoActions)
	apiRouter.ServeHTTP(rr, req)

	assert.NotEqual(t, http.StatusOK, rr.Code)
	assert.Contains(t, rr.Body.String(), "AccessKeyId must be 4 to 128 alphanumeric characters")

	// SecretAccessKey too short
	form.Set("AccessKeyId", "validkey")
	form.Set("SecretAccessKey", "short")

	req, _ = http.NewRequest("POST", "/", nil)
	req.PostForm = form
	req.Form = form
	req.Header.Set("Content-Type", "application/x-www-form-urlencoded")

	rr = httptest.NewRecorder()
	apiRouter.ServeHTTP(rr, req)

	assert.NotEqual(t, http.StatusOK, rr.Code)
	assert.Contains(t, rr.Body.String(), "SecretAccessKey must be between 8 and 128 characters")
	// AccessKeyId with SigV4 delimiters
	form.Set("AccessKeyId", "foo/bar=baz")
	form.Set("SecretAccessKey", "validsecret123")

	req, _ = http.NewRequest("POST", "/", nil)
	req.PostForm = form
	req.Form = form
	req.Header.Set("Content-Type", "application/x-www-form-urlencoded")

	rr = httptest.NewRecorder()
	apiRouter.ServeHTTP(rr, req)

	assert.NotEqual(t, http.StatusOK, rr.Code)
	assert.Contains(t, rr.Body.String(), "AccessKeyId must be 4 to 128 alphanumeric characters")
}

// TestEmbeddedIamCreateAccessKeyRejectsCollision tests that duplicate access keys are rejected
func TestEmbeddedIamCreateAccessKeyRejectsCollision(t *testing.T) {
	api := NewEmbeddedIamApiForTest()
	// Use a distinctive owner name ("ownerAlpha") so the leak assertion
	// cannot accidentally match a word embedded in the error body.
	const ownerName = "ownerAlpha"
	api.mockConfig = &iam_pb.S3ApiConfiguration{
		Identities: []*iam_pb.Identity{
			{
				Name: ownerName,
				Credentials: []*iam_pb.Credential{
					{AccessKey: "takenkey", SecretKey: "existingsecret"},
				},
			},
			{Name: "NewUser"},
		},
	}

	form := url.Values{}
	form.Set("Action", "CreateAccessKey")
	form.Set("UserName", "NewUser")
	form.Set("AccessKeyId", "takenkey")
	form.Set("SecretAccessKey", "newsecret123")

	req, _ := http.NewRequest("POST", "/", nil)
	req.PostForm = form
	req.Form = form
	req.Header.Set("Content-Type", "application/x-www-form-urlencoded")

	rr := httptest.NewRecorder()
	apiRouter := mux.NewRouter().SkipClean(true)
	apiRouter.Path("/").Methods(http.MethodPost).HandlerFunc(api.DoActions)
	apiRouter.ServeHTTP(rr, req)

	assert.NotEqual(t, http.StatusOK, rr.Code)
	assert.Contains(t, rr.Body.String(), "already in use")
	assert.NotContains(t, rr.Body.String(), ownerName, "should not leak owner name")

	// Verify no credentials were added to NewUser. Look up by name because the
	// memory store backs LoadConfiguration with a map, so Identities order is
	// not stable across a save/load round trip.
	var newUser *iam_pb.Identity
	for _, ident := range api.mockConfig.Identities {
		if ident.Name == "NewUser" {
			newUser = ident
			break
		}
	}
	require.NotNil(t, newUser, "NewUser identity should still exist")
	assert.Len(t, newUser.Credentials, 0)
}

// TestEmbeddedIamCreateAccessKeyRejectsPartialSupply tests that supplying only
// one of AccessKeyId / SecretAccessKey is rejected.
func TestEmbeddedIamCreateAccessKeyRejectsPartialSupply(t *testing.T) {
	api := NewEmbeddedIamApiForTest()
	api.mockConfig = &iam_pb.S3ApiConfiguration{
		Identities: []*iam_pb.Identity{
			{Name: "TestUser"},
		},
	}

	apiRouter := mux.NewRouter().SkipClean(true)
	apiRouter.Path("/").Methods(http.MethodPost).HandlerFunc(api.DoActions)

	// AccessKeyId supplied, SecretAccessKey omitted
	form := url.Values{}
	form.Set("Action", "CreateAccessKey")
	form.Set("UserName", "TestUser")
	form.Set("AccessKeyId", "myappkey")

	req, _ := http.NewRequest("POST", "/", nil)
	req.PostForm = form
	req.Form = form
	req.Header.Set("Content-Type", "application/x-www-form-urlencoded")

	rr := httptest.NewRecorder()
	apiRouter.ServeHTTP(rr, req)
	assert.NotEqual(t, http.StatusOK, rr.Code)
	assert.Contains(t, rr.Body.String(), "AccessKeyId and SecretAccessKey must be supplied together")
	assert.Len(t, api.mockConfig.Identities[0].Credentials, 0)

	// SecretAccessKey supplied, AccessKeyId omitted
	form = url.Values{}
	form.Set("Action", "CreateAccessKey")
	form.Set("UserName", "TestUser")
	form.Set("SecretAccessKey", "validsecret1")

	req, _ = http.NewRequest("POST", "/", nil)
	req.PostForm = form
	req.Form = form
	req.Header.Set("Content-Type", "application/x-www-form-urlencoded")

	rr = httptest.NewRecorder()
	apiRouter.ServeHTTP(rr, req)
	assert.NotEqual(t, http.StatusOK, rr.Code)
	assert.Contains(t, rr.Body.String(), "AccessKeyId and SecretAccessKey must be supplied together")
	assert.Len(t, api.mockConfig.Identities[0].Credentials, 0)
}

// TestEmbeddedIamCreateAccessKeyBoundary tests key length boundaries
func TestEmbeddedIamCreateAccessKeyBoundary(t *testing.T) {
	api := NewEmbeddedIamApiForTest()
	api.mockConfig = &iam_pb.S3ApiConfiguration{
		Identities: []*iam_pb.Identity{
			{Name: "TestUser"},
		},
	}

	apiRouter := mux.NewRouter().SkipClean(true)
	apiRouter.Path("/").Methods(http.MethodPost).HandlerFunc(api.DoActions)

	// Exactly 4 chars — should pass
	form := url.Values{}
	form.Set("Action", "CreateAccessKey")
	form.Set("UserName", "TestUser")
	form.Set("AccessKeyId", "abcd")
	form.Set("SecretAccessKey", "validsecret1")

	req, _ := http.NewRequest("POST", "/", nil)
	req.PostForm = form
	req.Form = form
	req.Header.Set("Content-Type", "application/x-www-form-urlencoded")

	rr := httptest.NewRecorder()
	apiRouter.ServeHTTP(rr, req)
	assert.Equal(t, http.StatusOK, rr.Code)

	// Exactly 3 chars — should fail
	api.mockConfig.Identities[0].Credentials = nil
	form.Set("AccessKeyId", "abc")

	req, _ = http.NewRequest("POST", "/", nil)
	req.PostForm = form
	req.Form = form
	req.Header.Set("Content-Type", "application/x-www-form-urlencoded")

	rr = httptest.NewRecorder()
	apiRouter.ServeHTTP(rr, req)
	assert.NotEqual(t, http.StatusOK, rr.Code)
	assert.Contains(t, rr.Body.String(), "alphanumeric")

	// Exactly 128 chars — should pass
	api.mockConfig.Identities[0].Credentials = nil
	ak128 := strings.Repeat("a", 128)
	sk128 := strings.Repeat("s", 128)
	form.Set("AccessKeyId", ak128)
	form.Set("SecretAccessKey", sk128)

	req, _ = http.NewRequest("POST", "/", nil)
	req.PostForm = form
	req.Form = form
	req.Header.Set("Content-Type", "application/x-www-form-urlencoded")

	rr = httptest.NewRecorder()
	apiRouter.ServeHTTP(rr, req)
	assert.Equal(t, http.StatusOK, rr.Code)

	// 129 chars AccessKeyId — should fail
	api.mockConfig.Identities[0].Credentials = nil
	form.Set("AccessKeyId", strings.Repeat("a", 129))
	form.Set("SecretAccessKey", sk128)

	req, _ = http.NewRequest("POST", "/", nil)
	req.PostForm = form
	req.Form = form
	req.Header.Set("Content-Type", "application/x-www-form-urlencoded")

	rr = httptest.NewRecorder()
	apiRouter.ServeHTTP(rr, req)
	assert.NotEqual(t, http.StatusOK, rr.Code)
	assert.Contains(t, rr.Body.String(), "alphanumeric")

	// 7-char SecretAccessKey — should fail
	api.mockConfig.Identities[0].Credentials = nil
	form.Set("AccessKeyId", "validkey")
	form.Set("SecretAccessKey", "1234567")

	req, _ = http.NewRequest("POST", "/", nil)
	req.PostForm = form
	req.Form = form
	req.Header.Set("Content-Type", "application/x-www-form-urlencoded")

	rr = httptest.NewRecorder()
	apiRouter.ServeHTTP(rr, req)
	assert.NotEqual(t, http.StatusOK, rr.Code)
	assert.Contains(t, rr.Body.String(), "SecretAccessKey must be between 8 and 128 characters")

	// Exactly 8-char SecretAccessKey — should pass (lower boundary)
	api.mockConfig.Identities[0].Credentials = nil
	form.Set("AccessKeyId", "validkey")
	form.Set("SecretAccessKey", "12345678")

	req, _ = http.NewRequest("POST", "/", nil)
	req.PostForm = form
	req.Form = form
	req.Header.Set("Content-Type", "application/x-www-form-urlencoded")

	rr = httptest.NewRecorder()
	apiRouter.ServeHTTP(rr, req)
	assert.Equal(t, http.StatusOK, rr.Code)

	// 129-char SecretAccessKey — should fail
	api.mockConfig.Identities[0].Credentials = nil
	form.Set("AccessKeyId", "validkey")
	form.Set("SecretAccessKey", strings.Repeat("s", 129))

	req, _ = http.NewRequest("POST", "/", nil)
	req.PostForm = form
	req.Form = form
	req.Header.Set("Content-Type", "application/x-www-form-urlencoded")

	rr = httptest.NewRecorder()
	apiRouter.ServeHTTP(rr, req)
	assert.NotEqual(t, http.StatusOK, rr.Code)
	assert.Contains(t, rr.Body.String(), "SecretAccessKey must be between 8 and 128 characters")
}

// TestEmbeddedIamDeleteAccessKey tests deleting an access key via direct form post
func TestEmbeddedIamDeleteAccessKey(t *testing.T) {
	api := NewEmbeddedIamApiForTest()
	api.mockConfig = &iam_pb.S3ApiConfiguration{
		Identities: []*iam_pb.Identity{
			{
				Name: "TestUser",
				Credentials: []*iam_pb.Credential{
					{AccessKey: UserAccessKeyPrefix + "TEST12345", SecretKey: "secret"},
				},
			},
		},
	}

	// Use direct form post since AWS SDK may format differently
	form := url.Values{}
	form.Set("Action", "DeleteAccessKey")
	form.Set("UserName", "TestUser")
	form.Set("AccessKeyId", UserAccessKeyPrefix+"TEST12345")

	req, _ := http.NewRequest("POST", "/", nil)
	req.PostForm = form
	req.Form = form
	req.Header.Set("Content-Type", "application/x-www-form-urlencoded")

	rr := httptest.NewRecorder()
	apiRouter := mux.NewRouter().SkipClean(true)
	apiRouter.Path("/").Methods(http.MethodPost).HandlerFunc(api.DoActions)
	apiRouter.ServeHTTP(rr, req)

	assert.Equal(t, http.StatusOK, rr.Code)

	// Verify the access key was deleted
	assert.Len(t, api.mockConfig.Identities[0].Credentials, 0)
}

// TestEmbeddedIamHandleImplicitUsername tests implicit username extraction from authorization header
func TestEmbeddedIamHandleImplicitUsername(t *testing.T) {
	// Create IAM with test credentials - the handleImplicitUsername function now looks
	// up the username from the credential store based on AccessKeyId
	// Note: Using obviously fake access keys to avoid secret scanner false positives
	iam := &IdentityAccessManagement{}
	testConfig := &iam_pb.S3ApiConfiguration{
		Identities: []*iam_pb.Identity{
			{
				Name: "testuser1",
				Credentials: []*iam_pb.Credential{
					{AccessKey: UserAccessKeyPrefix + "TESTFAKEKEY000001", SecretKey: "testsecretfake"},
				},
			},
		},
	}
	err := iam.LoadS3ApiConfigurationFromBytes(mustMarshalJSON(testConfig))
	if err != nil {
		t.Fatalf("Failed to load test config: %v", err)
	}

	embeddedApi := &EmbeddedIamApi{
		iam: iam,
	}

	var tests = []struct {
		r        *http.Request
		values   url.Values
		userName string
	}{
		// No authorization header - should not set username
		{&http.Request{}, url.Values{}, ""},
		// Valid auth header with known access key - should look up and find "testuser1"
		{&http.Request{Header: http.Header{"Authorization": []string{"AWS4-HMAC-SHA256 Credential=" + UserAccessKeyPrefix + "TESTFAKEKEY000001/20220420/us-east-1/iam/aws4_request, SignedHeaders=content-type;host;x-amz-date, Signature=fakesignature0123456789abcdef"}}}, url.Values{}, "testuser1"},
		// Malformed auth header (no Credential=) - should not set username
		{&http.Request{Header: http.Header{"Authorization": []string{"AWS4-HMAC-SHA256 =" + UserAccessKeyPrefix + "TESTFAKEKEY000001/20220420/test1/iam/aws4_request, SignedHeaders=content-type;host;x-amz-date, Signature=fakesignature0123456789abcdef"}}}, url.Values{}, ""},
		// Unknown access key - should not set username
		{&http.Request{Header: http.Header{"Authorization": []string{"AWS4-HMAC-SHA256 Credential=" + UserAccessKeyPrefix + "TESTUNKNOWN000000/20220420/us-east-1/iam/aws4_request, SignedHeaders=content-type;host;x-amz-date, Signature=fakesignature0123456789abcdef"}}}, url.Values{}, ""},
	}

	for i, test := range tests {
		embeddedApi.handleImplicitUsername(test.r, test.values)
		if un := test.values.Get("UserName"); un != test.userName {
			t.Errorf("No.%d: Got: %v, Expected: %v", i, un, test.userName)
		}
	}
}

func mustMarshalJSON(v interface{}) []byte {
	data, err := json.Marshal(v)
	if err != nil {
		panic(err)
	}
	return data
}

// TestEmbeddedIamFullWorkflow tests a complete user lifecycle
func TestEmbeddedIamFullWorkflow(t *testing.T) {
	api := NewEmbeddedIamApiForTest()
	api.mockConfig = &iam_pb.S3ApiConfiguration{}

	// 1. Create user
	t.Run("CreateUser", func(t *testing.T) {
		userName := aws.String("WorkflowUser")
		params := &iam.CreateUserInput{UserName: userName}
		req, _ := iam.New(session.New()).CreateUserRequest(params)
		_ = req.Build()
		response, err := executeEmbeddedIamRequest(api, req.HTTPRequest, nil)
		assert.NoError(t, err)
		assert.Equal(t, http.StatusOK, response.Code)
	})

	// 2. Create access key for user
	t.Run("CreateAccessKey", func(t *testing.T) {
		userName := aws.String("WorkflowUser")
		params := &iam.CreateAccessKeyInput{UserName: userName}
		req, _ := iam.New(session.New()).CreateAccessKeyRequest(params)
		_ = req.Build()
		response, err := executeEmbeddedIamRequest(api, req.HTTPRequest, nil)
		assert.NoError(t, err)
		assert.Equal(t, http.StatusOK, response.Code)
	})

	// 3. Attach policy to user
	t.Run("PutUserPolicy", func(t *testing.T) {
		params := &iam.PutUserPolicyInput{
			UserName:   aws.String("WorkflowUser"),
			PolicyName: aws.String("ReadWritePolicy"),
			PolicyDocument: aws.String(`{
				"Version": "2012-10-17",
				"Statement": [{
					"Effect": "Allow",
					"Action": ["s3:Get*", "s3:Put*"],
					"Resource": ["arn:aws:s3:::*"]
				}]
			}`),
		}
		req, _ := iam.New(session.New()).PutUserPolicyRequest(params)
		_ = req.Build()
		response, err := executeEmbeddedIamRequest(api, req.HTTPRequest, nil)
		assert.NoError(t, err)
		assert.Equal(t, http.StatusOK, response.Code)
	})

	// 4. List users to verify
	t.Run("ListUsers", func(t *testing.T) {
		params := &iam.ListUsersInput{}
		req, _ := iam.New(session.New()).ListUsersRequest(params)
		_ = req.Build()
		response, err := executeEmbeddedIamRequest(api, req.HTTPRequest, nil)
		assert.NoError(t, err)
		assert.Equal(t, http.StatusOK, response.Code)
	})

	// 5. Delete user
	t.Run("DeleteUser", func(t *testing.T) {
		params := &iam.DeleteUserInput{UserName: aws.String("WorkflowUser")}
		req, _ := iam.New(session.New()).DeleteUserRequest(params)
		_ = req.Build()
		response, err := executeEmbeddedIamRequest(api, req.HTTPRequest, nil)
		assert.NoError(t, err)
		assert.Equal(t, http.StatusOK, response.Code)
	})
}

// TestIamStringSlicesEqual tests the iamStringSlicesEqual helper function
func TestIamStringSlicesEqual(t *testing.T) {
	tests := []struct {
		name     string
		a        []string
		b        []string
		expected bool
	}{
		{"both empty", []string{}, []string{}, true},
		{"both nil", nil, nil, true},
		{"same elements same order", []string{"a", "b", "c"}, []string{"a", "b", "c"}, true},
		{"same elements different order", []string{"c", "a", "b"}, []string{"a", "b", "c"}, true},
		{"different lengths", []string{"a", "b"}, []string{"a", "b", "c"}, false},
		{"different elements", []string{"a", "b", "c"}, []string{"a", "b", "d"}, false},
		{"one empty one not", []string{}, []string{"a"}, false},
		{"duplicates same", []string{"a", "a", "b"}, []string{"a", "b", "a"}, true},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			result := iamStringSlicesEqual(tt.a, tt.b)
			assert.Equal(t, tt.expected, result)
		})
	}
}

// TestIamHash tests the iamHash function
func TestIamHash(t *testing.T) {
	input := "test-policy-document"
	hash := iamHash(&input)

	// Hash should be non-empty
	assert.NotEmpty(t, hash)

	// Same input should produce same hash
	hash2 := iamHash(&input)
	assert.Equal(t, hash, hash2)

	// Different input should produce different hash
	input2 := "different-policy"
	hash3 := iamHash(&input2)
	assert.NotEqual(t, hash, hash3)
}

// TestIamStringWithCharset tests the cryptographically secure random string generator
func TestIamStringWithCharset(t *testing.T) {
	charset := "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"
	length := 20

	str, err := iamStringWithCharset(length, charset)
	assert.NoError(t, err)
	assert.Len(t, str, length)

	// All characters should be from the charset
	for _, c := range str {
		assert.Contains(t, charset, string(c))
	}

	// Two calls should produce different strings (with very high probability)
	str2, err := iamStringWithCharset(length, charset)
	assert.NoError(t, err)
	assert.NotEqual(t, str, str2)
}

// TestIamMapToStatementAction tests action mapping
func TestIamMapToStatementAction(t *testing.T) {
	// iamMapToStatementAction maps IAM statement action patterns to internal action names
	tests := []struct {
		input    string
		expected string
	}{
		{"*", "Admin"},
		{"Get*", "Read"},
		{"Put*", "Write"},
		{"List*", "List"},
		{"Tagging*", "Tagging"},
		{"DeleteBucket*", "DeleteBucket"},
		{"PutBucketAcl", "WriteAcp"},
		{"GetBucketAcl", "ReadAcp"},
		{"InvalidAction", ""},
	}

	for _, tt := range tests {
		t.Run(tt.input, func(t *testing.T) {
			result := iamMapToStatementAction(tt.input)
			assert.Equal(t, tt.expected, result)
		})
	}
}

// TestIamMapToIdentitiesAction tests reverse action mapping
func TestIamMapToIdentitiesAction(t *testing.T) {
	// iamMapToIdentitiesAction maps internal action names to IAM statement action patterns
	tests := []struct {
		input    string
		expected string
	}{
		{"Admin", "*"},
		{"Read", "Get*"},
		{"Write", "Put*"},
		{"List", "List*"},
		{"Tagging", "Tagging*"},
		{"Unknown", ""},
	}

	for _, tt := range tests {
		t.Run(tt.input, func(t *testing.T) {
			result := iamMapToIdentitiesAction(tt.input)
			assert.Equal(t, tt.expected, result)
		})
	}
}

// TestEmbeddedIamGetUserNotFound tests GetUser with non-existent user
func TestEmbeddedIamGetUserNotFound(t *testing.T) {
	api := NewEmbeddedIamApiForTest()
	api.mockConfig = &iam_pb.S3ApiConfiguration{
		Identities: []*iam_pb.Identity{
			{Name: "ExistingUser"},
		},
	}

	userName := aws.String("NonExistentUser")
	params := &iam.GetUserInput{UserName: userName}
	req, _ := iam.New(session.New()).GetUserRequest(params)
	_ = req.Build()
	response, _ := executeEmbeddedIamRequest(api, req.HTTPRequest, nil)
	assert.Equal(t, http.StatusNotFound, response.Code)
}

// TestEmbeddedIamDeleteUserNotFound tests DeleteUser with non-existent user
func TestEmbeddedIamDeleteUserNotFound(t *testing.T) {
	api := NewEmbeddedIamApiForTest()
	api.mockConfig = &iam_pb.S3ApiConfiguration{}

	userName := aws.String("NonExistentUser")
	params := &iam.DeleteUserInput{UserName: userName}
	req, _ := iam.New(session.New()).DeleteUserRequest(params)
	_ = req.Build()
	response, _ := executeEmbeddedIamRequest(api, req.HTTPRequest, nil)
	assert.Equal(t, http.StatusNotFound, response.Code)
}

// TestEmbeddedIamUpdateUserNotFound tests UpdateUser with non-existent user
func TestEmbeddedIamUpdateUserNotFound(t *testing.T) {
	api := NewEmbeddedIamApiForTest()
	api.mockConfig = &iam_pb.S3ApiConfiguration{}

	params := &iam.UpdateUserInput{
		UserName:    aws.String("NonExistentUser"),
		NewUserName: aws.String("NewName"),
	}
	req, _ := iam.New(session.New()).UpdateUserRequest(params)
	_ = req.Build()
	response, _ := executeEmbeddedIamRequest(api, req.HTTPRequest, nil)
	assert.Equal(t, http.StatusNotFound, response.Code)
}

// TestEmbeddedIamCreateAccessKeyForExistingUser tests CreateAccessKey creates credentials for existing user
func TestEmbeddedIamCreateAccessKeyForExistingUser(t *testing.T) {
	api := NewEmbeddedIamApiForTest()
	api.mockConfig = &iam_pb.S3ApiConfiguration{
		Identities: []*iam_pb.Identity{
			{Name: "ExistingUser"},
		},
	}

	// Use direct form post
	form := url.Values{}
	form.Set("Action", "CreateAccessKey")
	form.Set("UserName", "ExistingUser")

	req, _ := http.NewRequest("POST", "/", nil)
	req.PostForm = form
	req.Form = form
	req.Header.Set("Content-Type", "application/x-www-form-urlencoded")

	rr := httptest.NewRecorder()
	apiRouter := mux.NewRouter().SkipClean(true)
	apiRouter.Path("/").Methods(http.MethodPost).HandlerFunc(api.DoActions)
	apiRouter.ServeHTTP(rr, req)

	assert.Equal(t, http.StatusOK, rr.Code)
	// Verify credentials were created
	assert.Len(t, api.mockConfig.Identities[0].Credentials, 1)
}

// TestEmbeddedIamPutGetUserPolicyRoundTrip is a regression test for
// https://github.com/seaweedfs/seaweedfs/issues/9008: put-user-policy followed
// by get-user-policy must return the same policy document, with Action and Resource
// lists intact (no wildcard expansion, no duplication, no collapsing).
func TestEmbeddedIamPutGetUserPolicyRoundTrip(t *testing.T) {
	api := NewEmbeddedIamApiForTest()
	s3cfg := &iam_pb.S3ApiConfiguration{
		Identities: []*iam_pb.Identity{{Name: "steward"}},
	}
	api.mockConfig = s3cfg

	policyJSON := `{
    "Version": "2012-10-17",
    "Statement": [{
      "Effect": "Allow",
      "Action": ["s3:GetObject", "s3:PutObject", "s3:ListBucket"],
      "Resource": ["arn:aws:s3:::b-le*", "arn:aws:s3:::b-le*/*"]
    }]
  }`

	// PutUserPolicy
	_, iamErr := api.PutUserPolicy(s3cfg, url.Values{
		"UserName":       []string{"steward"},
		"PolicyName":     []string{"steward_policy"},
		"PolicyDocument": []string{policyJSON},
	})
	assert.Nil(t, iamErr)

	// GetUserPolicy should return the exact document
	resp, iamErr := api.GetUserPolicy(s3cfg, url.Values{
		"UserName":   []string{"steward"},
		"PolicyName": []string{"steward_policy"},
	})
	assert.Nil(t, iamErr)

	var got policy_engine.PolicyDocument
	assert.NoError(t, json.Unmarshal([]byte(resp.GetUserPolicyResult.PolicyDocument), &got))

	assert.Equal(t, "2012-10-17", got.Version)
	require.Equal(t, 1, len(got.Statement))
	stmt := got.Statement[0]
	assert.Equal(t, policy_engine.PolicyEffectAllow, stmt.Effect)
	assert.ElementsMatch(t, []string{"s3:GetObject", "s3:PutObject", "s3:ListBucket"}, stmt.Action.Strings())
	assert.ElementsMatch(t, []string{"arn:aws:s3:::b-le*", "arn:aws:s3:::b-le*/*"}, stmt.Resource.Strings())
}

// TestEmbeddedIamGetUserPolicyFallback tests the lossy fallback reconstruction
// when no stored inline policy is available (pre-existing ident.Actions only).
func TestEmbeddedIamGetUserPolicyFallback(t *testing.T) {
	api := NewEmbeddedIamApiForTest()
	s3cfg := &iam_pb.S3ApiConfiguration{
		Identities: []*iam_pb.Identity{{
			Name:    "steward",
			Actions: []string{"Read:b-le*", "Write:b-le*", "List:b-le*", "Read:b-le*/*", "Write:b-le*/*", "List:b-le*/*"},
		}},
	}
	api.mockConfig = s3cfg

	// No stored inline policy — GetUserPolicy must reconstruct from ident.Actions
	resp, iamErr := api.GetUserPolicy(s3cfg, url.Values{
		"UserName":   []string{"steward"},
		"PolicyName": []string{"steward_policy"},
	})
	assert.Nil(t, iamErr)

	var got policy_engine.PolicyDocument
	assert.NoError(t, json.Unmarshal([]byte(resp.GetUserPolicyResult.PolicyDocument), &got))

	// Fallback is lossy but must not duplicate actions
	require.Equal(t, 1, len(got.Statement), "fallback should merge equal-action statements")
	stmt := got.Statement[0]
	assert.ElementsMatch(t, []string{"s3:Get*", "s3:Put*", "s3:List*"}, stmt.Action.Strings())
	// Bucket-level and object-level resources stay distinct
	assert.ElementsMatch(t, []string{"arn:aws:s3:::b-le*", "arn:aws:s3:::b-le*/*"}, stmt.Resource.Strings())
}

// TestEmbeddedIamGetUserPolicyUserNotFound tests GetUserPolicy with non-existent user
func TestEmbeddedIamGetUserPolicyUserNotFound(t *testing.T) {
	api := NewEmbeddedIamApiForTest()
	api.mockConfig = &iam_pb.S3ApiConfiguration{}

	params := &iam.GetUserPolicyInput{
		UserName:   aws.String("NonExistentUser"),
		PolicyName: aws.String("TestPolicy"),
	}
	req, _ := iam.New(session.New()).GetUserPolicyRequest(params)
	_ = req.Build()
	response, _ := executeEmbeddedIamRequest(api, req.HTTPRequest, nil)
	assert.Equal(t, http.StatusNotFound, response.Code)
}

// TestEmbeddedIamCreatePolicyMalformed tests CreatePolicy with invalid policy document
func TestEmbeddedIamCreatePolicyMalformed(t *testing.T) {
	api := NewEmbeddedIamApiForTest()

	params := &iam.CreatePolicyInput{
		PolicyName:     aws.String("TestPolicy"),
		PolicyDocument: aws.String("invalid json"),
	}
	req, _ := iam.New(session.New()).CreatePolicyRequest(params)
	_ = req.Build()
	response, _ := executeEmbeddedIamRequest(api, req.HTTPRequest, nil)
	assert.Equal(t, http.StatusBadRequest, response.Code)
}

// TestEmbeddedIamListAccessKeysForUser tests listing access keys for a specific user
func TestEmbeddedIamListAccessKeysForUser(t *testing.T) {
	api := NewEmbeddedIamApiForTest()
	api.mockConfig = &iam_pb.S3ApiConfiguration{
		Identities: []*iam_pb.Identity{
			{
				Name: "TestUser",
				Credentials: []*iam_pb.Credential{
					{AccessKey: UserAccessKeyPrefix + "TEST1", SecretKey: "secret1"},
					{AccessKey: UserAccessKeyPrefix + "TEST2", SecretKey: "secret2"},
				},
			},
		},
	}

	params := &iam.ListAccessKeysInput{UserName: aws.String("TestUser")}
	req, _ := iam.New(session.New()).ListAccessKeysRequest(params)
	_ = req.Build()
	out := iamListAccessKeysResponse{}
	response, err := executeEmbeddedIamRequest(api, req.HTTPRequest, &out)
	assert.NoError(t, err)
	assert.Equal(t, http.StatusOK, response.Code)

	// Verify both access keys are listed
	assert.Len(t, out.ListAccessKeysResult.AccessKeyMetadata, 2)
}

// TestEmbeddedIamNotImplementedAction tests handling of unimplemented actions
func TestEmbeddedIamNotImplementedAction(t *testing.T) {
	api := NewEmbeddedIamApiForTest()

	form := url.Values{}
	form.Set("Action", "SomeUnknownAction")

	req, _ := http.NewRequest("POST", "/", nil)
	req.PostForm = form
	req.Form = form
	req.Header.Set("Content-Type", "application/x-www-form-urlencoded")

	rr := httptest.NewRecorder()
	apiRouter := mux.NewRouter().SkipClean(true)
	apiRouter.Path("/").Methods(http.MethodPost).HandlerFunc(api.DoActions)
	apiRouter.ServeHTTP(rr, req)

	assert.Equal(t, http.StatusNotImplemented, rr.Code)
	assert.Contains(t, rr.Body.String(), "<RequestId>")
	assert.NotContains(t, rr.Body.String(), "<ResponseMetadata>")
	assert.Equal(t, rr.Header().Get(request_id.AmzRequestIDHeader), extractEmbeddedIamRequestID(rr))
}

// TestGetPolicyDocument tests parsing of policy documents
func TestGetPolicyDocument(t *testing.T) {
	api := NewEmbeddedIamApiForTest()

	validPolicy := `{
		"Version": "2012-10-17",
		"Statement": [{
			"Effect": "Allow",
			"Action": ["s3:GetObject"],
			"Resource": ["arn:aws:s3:::bucket/*"]
		}]
	}`

	doc, err := api.GetPolicyDocument(&validPolicy)
	assert.NoError(t, err)
	assert.Equal(t, "2012-10-17", doc.Version)
	assert.Len(t, doc.Statement, 1)

	// Test invalid JSON
	invalidPolicy := "not valid json"
	_, err = api.GetPolicyDocument(&invalidPolicy)
	assert.Error(t, err)
}

// TestEmbeddedIamGetActionsFromPolicy tests action extraction from policy documents
func TestEmbeddedIamGetActionsFromPolicy(t *testing.T) {
	api := NewEmbeddedIamApiForTest()

	// Actions must use wildcards (Get*, Put*, List*, etc.) as expected by the mapper
	policyDoc := `{
		"Version": "2012-10-17",
		"Statement": [{
			"Effect": "Allow",
			"Action": ["s3:Get*", "s3:Put*"],
			"Resource": ["arn:aws:s3:::mybucket/*"]
		}]
	}`

	policy, err := api.GetPolicyDocument(&policyDoc)
	assert.NoError(t, err)

	actions, err := api.getActions(&policy)
	assert.NoError(t, err)
	assert.NotEmpty(t, actions)
	// Should have Read and Write actions for the bucket
	// arn:aws:s3:::mybucket/* means all objects in mybucket, represented as "Action:mybucket"
	assert.Contains(t, actions, "Read:mybucket")
	assert.Contains(t, actions, "Write:mybucket")
}

// TestEmbeddedIamGetActionsPrefixScopedResource verifies that a prefix-scoped
// resource keeps its trailing wildcard so CanDo authorizes objects under the
// prefix. Stripping the wildcard produced a non-wildcard action that only
// matched at bucket level, denying every PutObject under the prefix.
func TestEmbeddedIamGetActionsPrefixScopedResource(t *testing.T) {
	api := NewEmbeddedIamApiForTest()

	policyDoc := `{
		"Version": "2012-10-17",
		"Statement": [{
			"Effect": "Allow",
			"Action": ["s3:Get*", "s3:Put*", "s3:DeleteObject"],
			"Resource": ["arn:aws:s3:::mcp-upload/mcp/TMS/api_keys/example/*"]
		}]
	}`

	policy, err := api.GetPolicyDocument(&policyDoc)
	require.NoError(t, err)

	actions, err := api.getActions(&policy)
	require.NoError(t, err)
	assert.Contains(t, actions, "Read:mcp-upload/mcp/TMS/api_keys/example/*")
	assert.Contains(t, actions, "Write:mcp-upload/mcp/TMS/api_keys/example/*")

	identity := &Identity{Name: "prefix-user"}
	for _, a := range actions {
		identity.Actions = append(identity.Actions, Action(a))
	}

	assert.True(t, identity.CanDo(ACTION_WRITE, "mcp-upload", "mcp/TMS/api_keys/example/logo.png"),
		"PutObject under the allowed prefix should be authorized")
	assert.True(t, identity.CanDo(ACTION_READ, "mcp-upload", "mcp/TMS/api_keys/example/logo.png"),
		"GetObject under the allowed prefix should be authorized")
	assert.False(t, identity.CanDo(ACTION_WRITE, "mcp-upload", "other/logo.png"),
		"PutObject outside the allowed prefix should be denied")
}

// TestEmbeddedIamPutUserPolicyAllResourceWildcard reproduces issue #9209:
// an AWS-style policy using "Action":"s3:*" with a bare "Resource":"*" is
// valid AWS IAM syntax (meaning "any resource") but was rejected with
// "no valid actions found in policy document" because the resource parser
// required a full ARN.
func TestEmbeddedIamPutUserPolicyAllResourceWildcard(t *testing.T) {
	api := NewEmbeddedIamApiForTest()

	// From issue #9209: bare "*" resource and bare "s3:*" action.
	policyDoc := `{
		"Version": "2012-10-17",
		"Statement": [{
			"Sid": "AllowS3Admin",
			"Effect": "Allow",
			"Action": "s3:*",
			"Resource": "*"
		}]
	}`

	policy, err := api.GetPolicyDocument(&policyDoc)
	require.NoError(t, err)

	actions, err := api.getActions(&policy)
	require.NoError(t, err, "getActions must accept bare \"*\" resource")
	assert.Contains(t, actions, ACTION_ADMIN, "s3:* should map to admin action")
}

// TestEmbeddedIamSetUserStatus tests enabling/disabling a user
func TestEmbeddedIamSetUserStatus(t *testing.T) {
	api := NewEmbeddedIamApiForTest()

	t.Run("DisableUser", func(t *testing.T) {
		// Reset state for test isolation
		api.mockConfig = &iam_pb.S3ApiConfiguration{
			Identities: []*iam_pb.Identity{
				{Name: "TestUser", Disabled: false},
			},
		}

		form := url.Values{}
		form.Set("Action", "SetUserStatus")
		form.Set("UserName", "TestUser")
		form.Set("Status", "Inactive")

		req, _ := http.NewRequest("POST", "/", nil)
		req.PostForm = form
		req.Form = form
		req.Header.Set("Content-Type", "application/x-www-form-urlencoded")

		rr := httptest.NewRecorder()
		apiRouter := mux.NewRouter().SkipClean(true)
		apiRouter.Path("/").Methods(http.MethodPost).HandlerFunc(api.DoActions)
		apiRouter.ServeHTTP(rr, req)

		assert.Equal(t, http.StatusOK, rr.Code)
		// Verify user is now disabled
		assert.True(t, api.mockConfig.Identities[0].Disabled)
	})

	t.Run("EnableUser", func(t *testing.T) {
		// Reset state for test isolation - start with disabled user
		api.mockConfig = &iam_pb.S3ApiConfiguration{
			Identities: []*iam_pb.Identity{
				{Name: "TestUser", Disabled: true},
			},
		}

		form := url.Values{}
		form.Set("Action", "SetUserStatus")
		form.Set("UserName", "TestUser")
		form.Set("Status", "Active")

		req, _ := http.NewRequest("POST", "/", nil)
		req.PostForm = form
		req.Form = form
		req.Header.Set("Content-Type", "application/x-www-form-urlencoded")

		rr := httptest.NewRecorder()
		apiRouter := mux.NewRouter().SkipClean(true)
		apiRouter.Path("/").Methods(http.MethodPost).HandlerFunc(api.DoActions)
		apiRouter.ServeHTTP(rr, req)

		assert.Equal(t, http.StatusOK, rr.Code)
		// Verify user is now enabled
		assert.False(t, api.mockConfig.Identities[0].Disabled)
	})
}

// TestEmbeddedIamSetUserStatusErrors tests error handling for SetUserStatus
func TestEmbeddedIamSetUserStatusErrors(t *testing.T) {
	api := NewEmbeddedIamApiForTest()
	api.mockConfig = &iam_pb.S3ApiConfiguration{
		Identities: []*iam_pb.Identity{
			{Name: "TestUser"},
		},
	}

	t.Run("UserNotFound", func(t *testing.T) {
		form := url.Values{}
		form.Set("Action", "SetUserStatus")
		form.Set("UserName", "NonExistentUser")
		form.Set("Status", "Inactive")

		req, _ := http.NewRequest("POST", "/", nil)
		req.PostForm = form
		req.Form = form
		req.Header.Set("Content-Type", "application/x-www-form-urlencoded")

		rr := httptest.NewRecorder()
		apiRouter := mux.NewRouter().SkipClean(true)
		apiRouter.Path("/").Methods(http.MethodPost).HandlerFunc(api.DoActions)
		apiRouter.ServeHTTP(rr, req)

		assert.Equal(t, http.StatusNotFound, rr.Code)
	})

	t.Run("InvalidStatus", func(t *testing.T) {
		form := url.Values{}
		form.Set("Action", "SetUserStatus")
		form.Set("UserName", "TestUser")
		form.Set("Status", "InvalidStatus")

		req, _ := http.NewRequest("POST", "/", nil)
		req.PostForm = form
		req.Form = form
		req.Header.Set("Content-Type", "application/x-www-form-urlencoded")

		rr := httptest.NewRecorder()
		apiRouter := mux.NewRouter().SkipClean(true)
		apiRouter.Path("/").Methods(http.MethodPost).HandlerFunc(api.DoActions)
		apiRouter.ServeHTTP(rr, req)

		assert.Equal(t, http.StatusBadRequest, rr.Code)
	})

	t.Run("MissingUserName", func(t *testing.T) {
		form := url.Values{}
		form.Set("Action", "SetUserStatus")
		form.Set("Status", "Inactive")

		req, _ := http.NewRequest("POST", "/", nil)
		req.PostForm = form
		req.Form = form
		req.Header.Set("Content-Type", "application/x-www-form-urlencoded")

		rr := httptest.NewRecorder()
		apiRouter := mux.NewRouter().SkipClean(true)
		apiRouter.Path("/").Methods(http.MethodPost).HandlerFunc(api.DoActions)
		apiRouter.ServeHTTP(rr, req)

		assert.Equal(t, http.StatusBadRequest, rr.Code)
	})

	t.Run("MissingStatus", func(t *testing.T) {
		form := url.Values{}
		form.Set("Action", "SetUserStatus")
		form.Set("UserName", "TestUser")

		req, _ := http.NewRequest("POST", "/", nil)
		req.PostForm = form
		req.Form = form
		req.Header.Set("Content-Type", "application/x-www-form-urlencoded")

		rr := httptest.NewRecorder()
		apiRouter := mux.NewRouter().SkipClean(true)
		apiRouter.Path("/").Methods(http.MethodPost).HandlerFunc(api.DoActions)
		apiRouter.ServeHTTP(rr, req)

		assert.Equal(t, http.StatusBadRequest, rr.Code)
	})
}

// TestEmbeddedIamUpdateAccessKey tests updating access key status
func TestEmbeddedIamUpdateAccessKey(t *testing.T) {
	api := NewEmbeddedIamApiForTest()

	t.Run("DeactivateAccessKey", func(t *testing.T) {
		// Reset state for test isolation
		api.mockConfig = &iam_pb.S3ApiConfiguration{
			Identities: []*iam_pb.Identity{
				{
					Name: "TestUser",
					Credentials: []*iam_pb.Credential{
						{AccessKey: UserAccessKeyPrefix + "TEST12345", SecretKey: "secret", Status: "Active"},
					},
				},
			},
		}

		form := url.Values{}
		form.Set("Action", "UpdateAccessKey")
		form.Set("UserName", "TestUser")
		form.Set("AccessKeyId", UserAccessKeyPrefix+"TEST12345")
		form.Set("Status", "Inactive")

		req, _ := http.NewRequest("POST", "/", nil)
		req.PostForm = form
		req.Form = form
		req.Header.Set("Content-Type", "application/x-www-form-urlencoded")

		rr := httptest.NewRecorder()
		apiRouter := mux.NewRouter().SkipClean(true)
		apiRouter.Path("/").Methods(http.MethodPost).HandlerFunc(api.DoActions)
		apiRouter.ServeHTTP(rr, req)

		assert.Equal(t, http.StatusOK, rr.Code)
		// Verify access key is now inactive
		assert.Equal(t, "Inactive", api.mockConfig.Identities[0].Credentials[0].Status)
	})

	t.Run("ActivateAccessKey", func(t *testing.T) {
		// Reset state for test isolation - start with inactive key
		api.mockConfig = &iam_pb.S3ApiConfiguration{
			Identities: []*iam_pb.Identity{
				{
					Name: "TestUser",
					Credentials: []*iam_pb.Credential{
						{AccessKey: UserAccessKeyPrefix + "TEST12345", SecretKey: "secret", Status: "Inactive"},
					},
				},
			},
		}

		form := url.Values{}
		form.Set("Action", "UpdateAccessKey")
		form.Set("UserName", "TestUser")
		form.Set("AccessKeyId", UserAccessKeyPrefix+"TEST12345")
		form.Set("Status", "Active")

		req, _ := http.NewRequest("POST", "/", nil)
		req.PostForm = form
		req.Form = form
		req.Header.Set("Content-Type", "application/x-www-form-urlencoded")

		rr := httptest.NewRecorder()
		apiRouter := mux.NewRouter().SkipClean(true)
		apiRouter.Path("/").Methods(http.MethodPost).HandlerFunc(api.DoActions)
		apiRouter.ServeHTTP(rr, req)

		assert.Equal(t, http.StatusOK, rr.Code)
		// Verify access key is now active
		assert.Equal(t, "Active", api.mockConfig.Identities[0].Credentials[0].Status)
	})
}

// TestEmbeddedIamUpdateAccessKeyErrors tests error handling for UpdateAccessKey
func TestEmbeddedIamUpdateAccessKeyErrors(t *testing.T) {
	api := NewEmbeddedIamApiForTest()
	api.mockConfig = &iam_pb.S3ApiConfiguration{
		Identities: []*iam_pb.Identity{
			{
				Name: "TestUser",
				Credentials: []*iam_pb.Credential{
					{AccessKey: UserAccessKeyPrefix + "TEST12345", SecretKey: "secret"},
				},
			},
		},
	}

	t.Run("AccessKeyNotFound", func(t *testing.T) {
		form := url.Values{}
		form.Set("Action", "UpdateAccessKey")
		form.Set("UserName", "TestUser")
		form.Set("AccessKeyId", "NONEXISTENT123")
		form.Set("Status", "Inactive")

		req, _ := http.NewRequest("POST", "/", nil)
		req.PostForm = form
		req.Form = form
		req.Header.Set("Content-Type", "application/x-www-form-urlencoded")

		rr := httptest.NewRecorder()
		apiRouter := mux.NewRouter().SkipClean(true)
		apiRouter.Path("/").Methods(http.MethodPost).HandlerFunc(api.DoActions)
		apiRouter.ServeHTTP(rr, req)

		assert.Equal(t, http.StatusNotFound, rr.Code)
	})

	t.Run("InvalidStatus", func(t *testing.T) {
		form := url.Values{}
		form.Set("Action", "UpdateAccessKey")
		form.Set("UserName", "TestUser")
		form.Set("AccessKeyId", UserAccessKeyPrefix+"TEST12345")
		form.Set("Status", "InvalidStatus")

		req, _ := http.NewRequest("POST", "/", nil)
		req.PostForm = form
		req.Form = form
		req.Header.Set("Content-Type", "application/x-www-form-urlencoded")

		rr := httptest.NewRecorder()
		apiRouter := mux.NewRouter().SkipClean(true)
		apiRouter.Path("/").Methods(http.MethodPost).HandlerFunc(api.DoActions)
		apiRouter.ServeHTTP(rr, req)

		assert.Equal(t, http.StatusBadRequest, rr.Code)
	})

	t.Run("MissingUserName", func(t *testing.T) {
		form := url.Values{}
		form.Set("Action", "UpdateAccessKey")
		form.Set("AccessKeyId", UserAccessKeyPrefix+"TEST12345")
		form.Set("Status", "Inactive")

		req, _ := http.NewRequest("POST", "/", nil)
		req.PostForm = form
		req.Form = form
		req.Header.Set("Content-Type", "application/x-www-form-urlencoded")

		rr := httptest.NewRecorder()
		apiRouter := mux.NewRouter().SkipClean(true)
		apiRouter.Path("/").Methods(http.MethodPost).HandlerFunc(api.DoActions)
		apiRouter.ServeHTTP(rr, req)

		assert.Equal(t, http.StatusBadRequest, rr.Code)
	})

	t.Run("MissingAccessKeyId", func(t *testing.T) {
		form := url.Values{}
		form.Set("Action", "UpdateAccessKey")
		form.Set("UserName", "TestUser")
		form.Set("Status", "Inactive")

		req, _ := http.NewRequest("POST", "/", nil)
		req.PostForm = form
		req.Form = form
		req.Header.Set("Content-Type", "application/x-www-form-urlencoded")

		rr := httptest.NewRecorder()
		apiRouter := mux.NewRouter().SkipClean(true)
		apiRouter.Path("/").Methods(http.MethodPost).HandlerFunc(api.DoActions)
		apiRouter.ServeHTTP(rr, req)

		assert.Equal(t, http.StatusBadRequest, rr.Code)
	})

	t.Run("UserNotFound", func(t *testing.T) {
		form := url.Values{}
		form.Set("Action", "UpdateAccessKey")
		form.Set("UserName", "NonExistentUser")
		form.Set("AccessKeyId", UserAccessKeyPrefix+"TEST12345")
		form.Set("Status", "Inactive")

		req, _ := http.NewRequest("POST", "/", nil)
		req.PostForm = form
		req.Form = form
		req.Header.Set("Content-Type", "application/x-www-form-urlencoded")

		rr := httptest.NewRecorder()
		apiRouter := mux.NewRouter().SkipClean(true)
		apiRouter.Path("/").Methods(http.MethodPost).HandlerFunc(api.DoActions)
		apiRouter.ServeHTTP(rr, req)

		assert.Equal(t, http.StatusNotFound, rr.Code)
	})

	t.Run("MissingStatus", func(t *testing.T) {
		form := url.Values{}
		form.Set("Action", "UpdateAccessKey")
		form.Set("UserName", "TestUser")
		form.Set("AccessKeyId", UserAccessKeyPrefix+"TEST12345")

		req, _ := http.NewRequest("POST", "/", nil)
		req.PostForm = form
		req.Form = form
		req.Header.Set("Content-Type", "application/x-www-form-urlencoded")

		rr := httptest.NewRecorder()
		apiRouter := mux.NewRouter().SkipClean(true)
		apiRouter.Path("/").Methods(http.MethodPost).HandlerFunc(api.DoActions)
		apiRouter.ServeHTTP(rr, req)

		assert.Equal(t, http.StatusBadRequest, rr.Code)
	})
}

// TestEmbeddedIamListAccessKeysShowsStatus tests that ListAccessKeys returns the access key status
func TestEmbeddedIamListAccessKeysShowsStatus(t *testing.T) {
	api := NewEmbeddedIamApiForTest()
	api.mockConfig = &iam_pb.S3ApiConfiguration{
		Identities: []*iam_pb.Identity{
			{
				Name: "TestUser",
				Credentials: []*iam_pb.Credential{
					{AccessKey: UserAccessKeyPrefix + "ACTIVE123", SecretKey: "secret1", Status: "Active"},
					{AccessKey: UserAccessKeyPrefix + "INACTIVE1", SecretKey: "secret2", Status: "Inactive"},
					{AccessKey: UserAccessKeyPrefix + "DEFAULT12", SecretKey: "secret3"}, // No status set, should default to Active
				},
			},
		},
	}

	params := &iam.ListAccessKeysInput{UserName: aws.String("TestUser")}
	req, _ := iam.New(session.New()).ListAccessKeysRequest(params)
	_ = req.Build()
	out := iamListAccessKeysResponse{}
	response, err := executeEmbeddedIamRequest(api, req.HTTPRequest, &out)
	assert.NoError(t, err)
	assert.Equal(t, http.StatusOK, response.Code)

	// Verify all three access keys are listed with correct status
	assert.Len(t, out.ListAccessKeysResult.AccessKeyMetadata, 3)

	// Find each key and verify status
	statusMap := make(map[string]string)
	for _, meta := range out.ListAccessKeysResult.AccessKeyMetadata {
		statusMap[*meta.AccessKeyId] = *meta.Status
	}

	assert.Equal(t, "Active", statusMap[UserAccessKeyPrefix+"ACTIVE123"])
	assert.Equal(t, "Inactive", statusMap[UserAccessKeyPrefix+"INACTIVE1"])
	assert.Equal(t, "Active", statusMap[UserAccessKeyPrefix+"DEFAULT12"]) // Default to Active
}

// TestDisabledUserLookupFails tests that disabled users cannot authenticate
func TestDisabledUserLookupFails(t *testing.T) {
	iam := &IdentityAccessManagement{}
	testConfig := &iam_pb.S3ApiConfiguration{
		Identities: []*iam_pb.Identity{
			{
				Name:     "enabledUser",
				Disabled: false,
				Credentials: []*iam_pb.Credential{
					{AccessKey: UserAccessKeyPrefix + "ENABLED123", SecretKey: "secret1"},
				},
			},
			{
				Name:     "disabledUser",
				Disabled: true,
				Credentials: []*iam_pb.Credential{
					{AccessKey: UserAccessKeyPrefix + "DISABLED12", SecretKey: "secret2"},
				},
			},
		},
	}
	err := iam.LoadS3ApiConfigurationFromBytes(mustMarshalJSON(testConfig))
	assert.NoError(t, err)

	// Enabled user should be found
	identity, cred, found := iam.LookupByAccessKey(UserAccessKeyPrefix + "ENABLED123")
	assert.True(t, found)
	assert.NotNil(t, identity)
	assert.NotNil(t, cred)
	assert.Equal(t, "enabledUser", identity.Name)

	// Disabled user should NOT be found
	identity, cred, found = iam.LookupByAccessKey(UserAccessKeyPrefix + "DISABLED12")
	assert.False(t, found)
	assert.Nil(t, identity)
	assert.Nil(t, cred)
}

// TestInactiveAccessKeyLookupFails tests that inactive access keys cannot authenticate
func TestInactiveAccessKeyLookupFails(t *testing.T) {
	iam := &IdentityAccessManagement{}
	testConfig := &iam_pb.S3ApiConfiguration{
		Identities: []*iam_pb.Identity{
			{
				Name: "testUser",
				Credentials: []*iam_pb.Credential{
					{AccessKey: UserAccessKeyPrefix + "ACTIVE123", SecretKey: "secret1", Status: "Active"},
					{AccessKey: UserAccessKeyPrefix + "INACTIVE1", SecretKey: "secret2", Status: "Inactive"},
					{AccessKey: UserAccessKeyPrefix + "DEFAULT12", SecretKey: "secret3"}, // No status = Active
				},
			},
		},
	}
	err := iam.LoadS3ApiConfigurationFromBytes(mustMarshalJSON(testConfig))
	assert.NoError(t, err)

	// Active key should be found
	identity, cred, found := iam.LookupByAccessKey(UserAccessKeyPrefix + "ACTIVE123")
	assert.True(t, found)
	assert.NotNil(t, identity)
	assert.NotNil(t, cred)

	// Inactive key should NOT be found
	identity, cred, found = iam.LookupByAccessKey(UserAccessKeyPrefix + "INACTIVE1")
	assert.False(t, found)
	assert.Nil(t, identity)
	assert.Nil(t, cred)

	// Key with no status (default Active) should be found
	identity, cred, found = iam.LookupByAccessKey(UserAccessKeyPrefix + "DEFAULT12")
	assert.True(t, found)
	assert.NotNil(t, identity)
	assert.NotNil(t, cred)
}

// TestAuthIamAuthenticatesBeforeParseForm verifies that AuthIam authenticates the request
// BEFORE parsing the form. This is critical because ParseForm() consumes the request body,
// but IAM signature verification needs to hash the body.
// This test reproduces the bug described in GitHub issue #7802.
func TestAuthIamAuthenticatesBeforeParseForm(t *testing.T) {
	// Create IAM with test credentials
	iam := &IdentityAccessManagement{
		hashes:       make(map[string]*sync.Pool),
		hashCounters: make(map[string]*int32),
	}

	testConfig := &iam_pb.S3ApiConfiguration{
		Identities: []*iam_pb.Identity{
			{
				Name: "admin",
				Credentials: []*iam_pb.Credential{
					{
						AccessKey: "admin_access_key",
						SecretKey: "admin_secret_key",
						Status:    "Active",
					},
				},
				Actions: []string{"Admin"},
			},
		},
	}
	err := iam.loadS3ApiConfiguration(testConfig)
	assert.NoError(t, err)

	embeddedApi := &EmbeddedIamApi{
		iam: iam,
	}

	// Create a properly signed IAM request
	payload := "Action=CreateUser&Version=2010-05-08&UserName=bob"

	// Use current time to avoid clock skew
	now := time.Now().UTC()
	amzDate := now.Format(iso8601Format)
	dateStamp := now.Format(yyyymmdd)
	credentialScope := dateStamp + "/us-east-1/iam/aws4_request"

	req, err := http.NewRequest("POST", "http://localhost:8333/", strings.NewReader(payload))
	assert.NoError(t, err)

	req.Header.Set("Content-Type", "application/x-www-form-urlencoded; charset=utf-8")
	req.Header.Set("Host", "localhost:8333")
	req.Header.Set("X-Amz-Date", amzDate)

	// Calculate the correct signature using IAM service
	payloadHash := getSHA256Hash([]byte(payload))
	canonicalRequest := fmt.Sprintf("POST\n/\n\ncontent-type:application/x-www-form-urlencoded; charset=utf-8\nhost:localhost:8333\nx-amz-date:%s\n\ncontent-type;host;x-amz-date\n%s", amzDate, payloadHash)
	canonicalRequestHash := getSHA256Hash([]byte(canonicalRequest))
	stringToSign := fmt.Sprintf("AWS4-HMAC-SHA256\n%s\n%s\n%s", amzDate, credentialScope, canonicalRequestHash)
	signingKey := getSigningKey("admin_secret_key", dateStamp, "us-east-1", "iam")
	signature := getSignature(signingKey, stringToSign)

	authHeader := fmt.Sprintf("AWS4-HMAC-SHA256 Credential=admin_access_key/%s, SignedHeaders=content-type;host;x-amz-date, Signature=%s",
		credentialScope, signature)
	req.Header.Set("Authorization", authHeader)

	// Create a test handler that just returns OK
	handlerCalled := false
	testHandler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		handlerCalled = true
		w.WriteHeader(http.StatusOK)
	})

	// Wrap with AuthIam
	authHandler := embeddedApi.AuthIam(testHandler, ACTION_WRITE)

	// Execute the request
	rr := httptest.NewRecorder()
	authHandler.ServeHTTP(rr, req)

	// The handler should be called (authentication succeeded)
	// Before the fix, this would fail with SignatureDoesNotMatch because
	// ParseForm was called before authentication, consuming the body
	assert.True(t, handlerCalled, "Handler was not called - authentication failed")
	assert.Equal(t, http.StatusOK, rr.Code, "Expected OK status, got %d", rr.Code)
}

// TestOldCodeOrderWouldFail demonstrates why the old code order was broken.
// This test shows that ParseForm() before signature verification causes auth failure.
func TestOldCodeOrderWouldFail(t *testing.T) {
	// Create IAM with test credentials
	iam := &IdentityAccessManagement{
		hashes:       make(map[string]*sync.Pool),
		hashCounters: make(map[string]*int32),
	}

	testConfig := &iam_pb.S3ApiConfiguration{
		Identities: []*iam_pb.Identity{
			{
				Name: "admin",
				Credentials: []*iam_pb.Credential{
					{
						AccessKey: "admin_access_key",
						SecretKey: "admin_secret_key",
						Status:    "Active",
					},
				},
				Actions: []string{"Admin"},
			},
		},
	}
	err := iam.loadS3ApiConfiguration(testConfig)
	assert.NoError(t, err)

	// Create a properly signed IAM request
	payload := "Action=CreateUser&Version=2010-05-08&UserName=bob"

	now := time.Now().UTC()
	amzDate := now.Format(iso8601Format)
	dateStamp := now.Format(yyyymmdd)
	credentialScope := dateStamp + "/us-east-1/iam/aws4_request"

	req, err := http.NewRequest("POST", "http://localhost:8333/", strings.NewReader(payload))
	assert.NoError(t, err)

	req.Header.Set("Content-Type", "application/x-www-form-urlencoded; charset=utf-8")
	req.Header.Set("Host", "localhost:8333")
	req.Header.Set("X-Amz-Date", amzDate)

	// Calculate the correct signature using IAM service
	payloadHash := getSHA256Hash([]byte(payload))
	canonicalRequest := fmt.Sprintf("POST\n/\n\ncontent-type:application/x-www-form-urlencoded; charset=utf-8\nhost:localhost:8333\nx-amz-date:%s\n\ncontent-type;host;x-amz-date\n%s", amzDate, payloadHash)
	canonicalRequestHash := getSHA256Hash([]byte(canonicalRequest))
	stringToSign := fmt.Sprintf("AWS4-HMAC-SHA256\n%s\n%s\n%s", amzDate, credentialScope, canonicalRequestHash)
	signingKey := getSigningKey("admin_secret_key", dateStamp, "us-east-1", "iam")
	signature := getSignature(signingKey, stringToSign)

	authHeader := fmt.Sprintf("AWS4-HMAC-SHA256 Credential=admin_access_key/%s, SignedHeaders=content-type;host;x-amz-date, Signature=%s",
		credentialScope, signature)
	req.Header.Set("Authorization", authHeader)

	// Simulate OLD buggy code: ParseForm BEFORE authentication
	// This consumes the request body!
	err = req.ParseForm()
	assert.NoError(t, err)
	assert.Equal(t, "CreateUser", req.Form.Get("Action")) // Form parsing works

	// Now try to authenticate - this should FAIL because body is consumed
	identity, errCode := iam.AuthSignatureOnly(req)

	// With old code order, this would fail with SignatureDoesNotMatch
	// because the body is empty when signature verification tries to hash it
	assert.Equal(t, s3err.ErrSignatureDoesNotMatch, errCode,
		"Expected SignatureDoesNotMatch when ParseForm is called before auth")
	assert.Nil(t, identity)

	t.Log("This demonstrates the bug: ParseForm before auth causes SignatureDoesNotMatch")
}

// TestEmbeddedIamExecuteAction tests calling ExecuteAction directly
func TestEmbeddedIamExecuteAction(t *testing.T) {
	api := NewEmbeddedIamApiForTest()
	api.mockConfig = &iam_pb.S3ApiConfiguration{}

	// Test case: CreateUser via ExecuteAction
	vals := url.Values{}
	vals.Set("Action", "CreateUser")
	vals.Set("UserName", "ExecuteActionUser")

	ctx := context.Background()
	resp, iamErr := api.ExecuteAction(ctx, vals, false, "")
	assert.Nil(t, iamErr)

	// Verify response type
	createResp, ok := resp.(*iamCreateUserResponse)
	assert.True(t, ok)
	assert.Equal(t, "ExecuteActionUser", *createResp.CreateUserResult.User.UserName)

	// CreateUser now uses a targeted write (credentialManager.CreateUser) rather than
	// SaveConfiguration, so persistence is checked via the credential manager directly.
	user, err := api.credentialManager.GetUser(ctx, "ExecuteActionUser")
	assert.NoError(t, err)
	assert.Equal(t, "ExecuteActionUser", user.Name)
}

// TestEmbeddedIamReadOnly tests that write operations are blocked when readOnly is true
func TestEmbeddedIamReadOnly(t *testing.T) {
	api := NewEmbeddedIamApiForTest()
	api.readOnly = true

	// Try CreateUser (Write)
	userName := aws.String("ReadOnlyUser")
	params := &iam.CreateUserInput{UserName: userName}
	req, _ := iam.New(session.New()).CreateUserRequest(params)
	_ = req.Build()

	response, err := executeEmbeddedIamRequest(api, req.HTTPRequest, nil)
	assert.NoError(t, err)
	assert.Equal(t, http.StatusForbidden, response.Code)

	code, msg := extractEmbeddedIamErrorCodeAndMessage(response)
	assert.Equal(t, "AccessDenied", code)
	assert.Contains(t, msg, "IAM write operations are disabled")

	// Try ListUsers (Read) - Should succeed
	paramsList := &iam.ListUsersInput{}
	reqList, _ := iam.New(session.New()).ListUsersRequest(paramsList)
	_ = reqList.Build()

	outList := iamListUsersResponse{}
	responseList, err := executeEmbeddedIamRequest(api, reqList.HTTPRequest, &outList)
	assert.NoError(t, err)
	assert.Equal(t, http.StatusOK, responseList.Code)
}
