use super::*;
use parking_lot::RwLock;
use std::collections::HashMap;
use std::sync::Arc;

fn make_service() -> IamService {
    let state: SharedIamState = Arc::new(RwLock::new(
        fakecloud_core::multi_account::MultiAccountState::new("123456789012", "us-east-1", ""),
    ));
    IamService::new(state)
}

fn make_request(action: &str, params: Vec<(&str, &str)>) -> AwsRequest {
    let mut query_params = HashMap::new();
    query_params.insert("Action".to_string(), action.to_string());
    for (k, v) in params {
        query_params.insert(k.to_string(), v.to_string());
    }
    AwsRequest {
        service: "iam".to_string(),
        action: action.to_string(),
        region: "us-east-1".to_string(),
        account_id: "123456789012".to_string(),
        request_id: "test-id".to_string(),
        headers: http::HeaderMap::new(),
        query_params,
        body: bytes::Bytes::new(),
        body_stream: parking_lot::Mutex::new(None),
        path_segments: vec![],
        raw_path: "/".to_string(),
        raw_query: String::new(),
        method: http::Method::POST,
        is_query_protocol: true,
        access_key_id: None,
        principal: None,
    }
}

/// Pull the text inside the first ``<tag>...</tag>`` element of ``body``.
/// Used by IAM tests to fish an ARN out of a create-* response without
/// pulling in a real XML parser.
fn extract_xml_tag<'a>(body: &'a str, tag: &str) -> &'a str {
    let open = format!("<{tag}>");
    let close = format!("</{tag}>");
    let start = body
        .find(&open)
        .unwrap_or_else(|| panic!("tag <{tag}> not found"))
        + open.len();
    let end = body
        .find(&close)
        .unwrap_or_else(|| panic!("tag </{tag}> not found"));
    &body[start..end]
}

#[test]
fn list_access_keys_max_items_zero_returns_error() {
    let svc = make_service();

    // Create a user first
    let req = make_request("CreateUser", vec![("UserName", "testuser")]);
    svc.create_user(&req).unwrap();

    // Try listing access keys with MaxItems=0
    let req = make_request(
        "ListAccessKeys",
        vec![("UserName", "testuser"), ("MaxItems", "0")],
    );
    let result = svc.list_access_keys(&req);
    assert!(result.is_err(), "MaxItems=0 should return an error");
}

#[test]
fn list_users_rejects_non_numeric_max_items() {
    let svc = make_service();
    let req = make_request("ListUsers", vec![("MaxItems", "abc")]);
    let result = svc.list_users(&req);
    assert!(
        result.is_err(),
        "non-numeric MaxItems should return an error"
    );
}

#[test]
fn list_roles_rejects_non_numeric_max_items() {
    let svc = make_service();
    let req = make_request("ListRoles", vec![("MaxItems", "xyz")]);
    let result = svc.list_roles(&req);
    assert!(
        result.is_err(),
        "non-numeric MaxItems should return an error"
    );
}

#[test]
fn list_policies_rejects_non_numeric_max_items() {
    let svc = make_service();
    let req = make_request("ListPolicies", vec![("MaxItems", "notanumber")]);
    let result = svc.list_policies(&req);
    assert!(
        result.is_err(),
        "non-numeric MaxItems should return an error"
    );
}

// ---- Group inline policy tests ----

#[test]
fn put_and_get_group_policy() {
    let svc = make_service();
    let policy_doc = r#"{"Version":"2012-10-17","Statement":[{"Effect":"Allow","Action":"s3:*","Resource":"*"}]}"#;

    // Create group
    svc.handle_sync("CreateGroup", vec![("GroupName", "devs")]);

    // Put inline policy
    svc.handle_sync(
        "PutGroupPolicy",
        vec![
            ("GroupName", "devs"),
            ("PolicyName", "s3-access"),
            ("PolicyDocument", policy_doc),
        ],
    );

    // Get inline policy
    let resp = svc.handle_sync(
        "GetGroupPolicy",
        vec![("GroupName", "devs"), ("PolicyName", "s3-access")],
    );
    assert!(resp.contains("s3-access"));
    assert!(resp.contains("devs"));
}

#[test]
fn list_group_policies() {
    let svc = make_service();
    let doc =
        r#"{"Version":"2012-10-17","Statement":[{"Effect":"Allow","Action":"*","Resource":"*"}]}"#;

    svc.handle_sync("CreateGroup", vec![("GroupName", "ops")]);
    svc.handle_sync(
        "PutGroupPolicy",
        vec![
            ("GroupName", "ops"),
            ("PolicyName", "pol-a"),
            ("PolicyDocument", doc),
        ],
    );
    svc.handle_sync(
        "PutGroupPolicy",
        vec![
            ("GroupName", "ops"),
            ("PolicyName", "pol-b"),
            ("PolicyDocument", doc),
        ],
    );

    let resp = svc.handle_sync("ListGroupPolicies", vec![("GroupName", "ops")]);
    assert!(resp.contains("pol-a"));
    assert!(resp.contains("pol-b"));
}

#[test]
fn delete_group_policy() {
    let svc = make_service();
    let doc =
        r#"{"Version":"2012-10-17","Statement":[{"Effect":"Allow","Action":"*","Resource":"*"}]}"#;

    svc.handle_sync("CreateGroup", vec![("GroupName", "team")]);
    svc.handle_sync(
        "PutGroupPolicy",
        vec![
            ("GroupName", "team"),
            ("PolicyName", "temp"),
            ("PolicyDocument", doc),
        ],
    );

    // Delete
    svc.handle_sync(
        "DeleteGroupPolicy",
        vec![("GroupName", "team"), ("PolicyName", "temp")],
    );

    // List should be empty
    let resp = svc.handle_sync("ListGroupPolicies", vec![("GroupName", "team")]);
    assert!(!resp.contains("temp"));
}

#[test]
fn get_group_policy_not_found() {
    let svc = make_service();
    svc.handle_sync("CreateGroup", vec![("GroupName", "g1")]);

    let req = make_request(
        "GetGroupPolicy",
        vec![("GroupName", "g1"), ("PolicyName", "nope")],
    );
    let result = svc.get_group_policy(&req);
    assert!(result.is_err());
}

// ---- Group managed policy attachment tests ----

#[test]
fn attach_and_list_group_policies_managed() {
    let svc = make_service();
    let doc =
        r#"{"Version":"2012-10-17","Statement":[{"Effect":"Allow","Action":"*","Resource":"*"}]}"#;

    svc.handle_sync("CreateGroup", vec![("GroupName", "eng")]);
    svc.handle_sync(
        "CreatePolicy",
        vec![("PolicyName", "read-policy"), ("PolicyDocument", doc)],
    );

    let create_resp = svc.handle_sync(
        "CreatePolicy",
        vec![("PolicyName", "write-policy"), ("PolicyDocument", doc)],
    );
    // Extract the ARN from the second policy
    let arn_start = create_resp.find("<Arn>").unwrap() + 5;
    let arn_end = create_resp.find("</Arn>").unwrap();
    let write_arn = &create_resp[arn_start..arn_end];

    // Attach both policies - for the first one, extract its ARN too
    // Just use the write_arn which we already have
    svc.handle_sync(
        "AttachGroupPolicy",
        vec![("GroupName", "eng"), ("PolicyArn", write_arn)],
    );

    let list = svc.handle_sync("ListAttachedGroupPolicies", vec![("GroupName", "eng")]);
    assert!(list.contains("write-policy"));
}

#[test]
fn detach_group_policy() {
    let svc = make_service();
    let doc =
        r#"{"Version":"2012-10-17","Statement":[{"Effect":"Allow","Action":"*","Resource":"*"}]}"#;

    svc.handle_sync("CreateGroup", vec![("GroupName", "detach-grp")]);
    let resp = svc.handle_sync(
        "CreatePolicy",
        vec![("PolicyName", "detach-pol"), ("PolicyDocument", doc)],
    );
    let arn = extract_xml_value(&resp, "Arn");

    svc.handle_sync(
        "AttachGroupPolicy",
        vec![("GroupName", "detach-grp"), ("PolicyArn", &arn)],
    );

    // Detach
    svc.handle_sync(
        "DetachGroupPolicy",
        vec![("GroupName", "detach-grp"), ("PolicyArn", &arn)],
    );

    let list = svc.handle_sync(
        "ListAttachedGroupPolicies",
        vec![("GroupName", "detach-grp")],
    );
    assert!(!list.contains("detach-pol"));
}

#[test]
fn detach_group_policy_not_attached_fails() {
    let svc = make_service();
    svc.handle_sync("CreateGroup", vec![("GroupName", "grp-err")]);

    let req = make_request(
        "DetachGroupPolicy",
        vec![
            ("GroupName", "grp-err"),
            ("PolicyArn", "arn:aws:iam::123456789012:policy/nope"),
        ],
    );
    let result = svc.detach_group_policy(&req);
    assert!(result.is_err());
}

// ---- User inline policy tests ----

#[test]
fn put_get_delete_user_inline_policy() {
    let svc = make_service();
    let doc = r#"{"Version":"2012-10-17","Statement":[{"Effect":"Allow","Action":"sqs:*","Resource":"*"}]}"#;

    svc.handle_sync("CreateUser", vec![("UserName", "alice")]);

    // Put
    svc.handle_sync(
        "PutUserPolicy",
        vec![
            ("UserName", "alice"),
            ("PolicyName", "sqs-access"),
            ("PolicyDocument", doc),
        ],
    );

    // Get
    let resp = svc.handle_sync(
        "GetUserPolicy",
        vec![("UserName", "alice"), ("PolicyName", "sqs-access")],
    );
    assert!(resp.contains("sqs-access"));
    assert!(resp.contains("alice"));

    // List
    let list = svc.handle_sync("ListUserPolicies", vec![("UserName", "alice")]);
    assert!(list.contains("sqs-access"));

    // Delete
    svc.handle_sync(
        "DeleteUserPolicy",
        vec![("UserName", "alice"), ("PolicyName", "sqs-access")],
    );

    let list = svc.handle_sync("ListUserPolicies", vec![("UserName", "alice")]);
    assert!(!list.contains("sqs-access"));
}

#[test]
fn get_user_policy_not_found() {
    let svc = make_service();
    svc.handle_sync("CreateUser", vec![("UserName", "bob")]);

    let req = make_request(
        "GetUserPolicy",
        vec![("UserName", "bob"), ("PolicyName", "ghost")],
    );
    let result = svc.get_user_policy(&req);
    assert!(result.is_err());
}

// ---- User managed policy attachment tests ----

#[test]
fn attach_detach_list_user_policies_managed() {
    let svc = make_service();
    let doc =
        r#"{"Version":"2012-10-17","Statement":[{"Effect":"Allow","Action":"*","Resource":"*"}]}"#;

    svc.handle_sync("CreateUser", vec![("UserName", "carol")]);
    let resp = svc.handle_sync(
        "CreatePolicy",
        vec![("PolicyName", "user-pol"), ("PolicyDocument", doc)],
    );
    let arn = extract_xml_value(&resp, "Arn");

    // Attach
    svc.handle_sync(
        "AttachUserPolicy",
        vec![("UserName", "carol"), ("PolicyArn", &arn)],
    );

    // List attached
    let list = svc.handle_sync("ListAttachedUserPolicies", vec![("UserName", "carol")]);
    assert!(list.contains("user-pol"));

    // Detach
    svc.handle_sync(
        "DetachUserPolicy",
        vec![("UserName", "carol"), ("PolicyArn", &arn)],
    );

    let list = svc.handle_sync("ListAttachedUserPolicies", vec![("UserName", "carol")]);
    assert!(!list.contains("user-pol"));
}

#[test]
fn attach_user_policy_nonexistent_user_fails() {
    let svc = make_service();
    let req = make_request(
        "AttachUserPolicy",
        vec![
            ("UserName", "nobody"),
            ("PolicyArn", "arn:aws:iam::123456789012:policy/x"),
        ],
    );
    let result = svc.attach_user_policy(&req);
    assert!(result.is_err());
}

#[test]
fn detach_user_policy_not_attached_fails() {
    let svc = make_service();
    svc.handle_sync("CreateUser", vec![("UserName", "dave")]);

    let req = make_request(
        "DetachUserPolicy",
        vec![
            ("UserName", "dave"),
            ("PolicyArn", "arn:aws:iam::123456789012:policy/nope"),
        ],
    );
    let result = svc.detach_user_policy(&req);
    assert!(result.is_err());
}

// ---- Login profile tests ----

#[test]
fn login_profile_lifecycle() {
    let svc = make_service();
    svc.handle_sync("CreateUser", vec![("UserName", "loginuser")]);

    // Create login profile
    let resp = svc.handle_sync(
        "CreateLoginProfile",
        vec![
            ("UserName", "loginuser"),
            ("Password", "S3cureP@ss!"),
            ("PasswordResetRequired", "true"),
        ],
    );
    assert!(resp.contains("loginuser"));
    assert!(resp.contains("<PasswordResetRequired>true</PasswordResetRequired>"));

    // Get login profile
    let resp = svc.handle_sync("GetLoginProfile", vec![("UserName", "loginuser")]);
    assert!(resp.contains("loginuser"));
    assert!(resp.contains("<PasswordResetRequired>true</PasswordResetRequired>"));

    // Update login profile
    svc.handle_sync(
        "UpdateLoginProfile",
        vec![
            ("UserName", "loginuser"),
            ("PasswordResetRequired", "false"),
        ],
    );

    let resp = svc.handle_sync("GetLoginProfile", vec![("UserName", "loginuser")]);
    assert!(resp.contains("<PasswordResetRequired>false</PasswordResetRequired>"));

    // Delete login profile
    svc.handle_sync("DeleteLoginProfile", vec![("UserName", "loginuser")]);

    // Should fail now
    let req = make_request("GetLoginProfile", vec![("UserName", "loginuser")]);
    assert!(svc.get_login_profile(&req).is_err());
}

#[test]
fn update_login_profile_rotates_password() {
    let svc = make_service();
    svc.handle_sync("CreateUser", vec![("UserName", "rotuser")]);
    svc.handle_sync(
        "CreateLoginProfile",
        vec![("UserName", "rotuser"), ("Password", "OldP@ss1")],
    );
    svc.handle_sync(
        "UpdateLoginProfile",
        vec![("UserName", "rotuser"), ("Password", "NewP@ss2")],
    );
    // The Password param must actually update the stored profile (previously
    // dropped, so a rotate silently kept the old password).
    let accounts = svc.state.read();
    let stored = accounts
        .get("123456789012")
        .unwrap()
        .login_profiles
        .get("rotuser")
        .unwrap();
    assert_eq!(stored.password, "NewP@ss2");
}

#[test]
fn create_login_profile_duplicate_fails() {
    let svc = make_service();
    svc.handle_sync("CreateUser", vec![("UserName", "dupuser")]);
    svc.handle_sync(
        "CreateLoginProfile",
        vec![("UserName", "dupuser"), ("Password", "pass1")],
    );

    let req = make_request(
        "CreateLoginProfile",
        vec![("UserName", "dupuser"), ("Password", "pass2")],
    );
    assert!(svc.create_login_profile(&req).is_err());
}

#[test]
fn delete_login_profile_nonexistent_fails() {
    let svc = make_service();
    svc.handle_sync("CreateUser", vec![("UserName", "nologin")]);

    let req = make_request("DeleteLoginProfile", vec![("UserName", "nologin")]);
    assert!(svc.delete_login_profile(&req).is_err());
}

// ---- MFA tests ----

#[test]
fn virtual_mfa_device_lifecycle() {
    let svc = make_service();

    // Create virtual MFA device
    let resp = svc.handle_sync(
        "CreateVirtualMFADevice",
        vec![("VirtualMFADeviceName", "my-mfa")],
    );
    assert!(resp.contains("my-mfa"));
    assert!(resp.contains("<Base32StringSeed>"));
    assert!(resp.contains("<QRCodePNG>"));
    let serial = extract_xml_value(&resp, "SerialNumber");

    // List should include it
    let list = svc.handle_sync("ListVirtualMFADevices", vec![]);
    assert!(list.contains("my-mfa"));

    // Delete
    svc.handle_sync("DeleteVirtualMFADevice", vec![("SerialNumber", &serial)]);

    // List should be empty
    let list = svc.handle_sync("ListVirtualMFADevices", vec![]);
    assert!(!list.contains("my-mfa"));
}

#[test]
fn delete_virtual_mfa_device_not_found() {
    let svc = make_service();
    let req = make_request(
        "DeleteVirtualMFADevice",
        vec![("SerialNumber", "arn:aws:iam::123456789012:mfa/ghost")],
    );
    assert!(svc.delete_virtual_mfa_device(&req).is_err());
}

#[test]
fn enable_and_list_mfa_devices() {
    let svc = make_service();
    svc.handle_sync("CreateUser", vec![("UserName", "mfauser")]);

    // Create virtual MFA
    let resp = svc.handle_sync(
        "CreateVirtualMFADevice",
        vec![("VirtualMFADeviceName", "dev-mfa")],
    );
    let serial = extract_xml_value(&resp, "SerialNumber");

    // Enable MFA device for user
    svc.handle_sync(
        "EnableMFADevice",
        vec![
            ("UserName", "mfauser"),
            ("SerialNumber", &serial),
            ("AuthenticationCode1", "123456"),
            ("AuthenticationCode2", "654321"),
        ],
    );

    // List MFA devices for user
    let list = svc.handle_sync("ListMFADevices", vec![("UserName", "mfauser")]);
    assert!(list.contains(&serial));
    assert!(list.contains("mfauser"));
}

#[test]
fn deactivate_mfa_device() {
    let svc = make_service();
    svc.handle_sync("CreateUser", vec![("UserName", "deactuser")]);

    let resp = svc.handle_sync(
        "CreateVirtualMFADevice",
        vec![("VirtualMFADeviceName", "deact-mfa")],
    );
    let serial = extract_xml_value(&resp, "SerialNumber");

    svc.handle_sync(
        "EnableMFADevice",
        vec![
            ("UserName", "deactuser"),
            ("SerialNumber", &serial),
            ("AuthenticationCode1", "111111"),
            ("AuthenticationCode2", "222222"),
        ],
    );

    // Deactivate
    svc.handle_sync(
        "DeactivateMFADevice",
        vec![("UserName", "deactuser"), ("SerialNumber", &serial)],
    );

    // Should no longer appear in user's MFA device list
    let list = svc.handle_sync("ListMFADevices", vec![("UserName", "deactuser")]);
    assert!(!list.contains(&serial));
}

#[test]
fn list_virtual_mfa_devices_assignment_filter() {
    let svc = make_service();
    svc.handle_sync("CreateUser", vec![("UserName", "filteruser")]);

    // Create two MFA devices with distinct names
    let resp1 = svc.handle_sync(
        "CreateVirtualMFADevice",
        vec![("VirtualMFADeviceName", "enabled-device")],
    );
    let serial1 = extract_xml_value(&resp1, "SerialNumber");
    svc.handle_sync(
        "CreateVirtualMFADevice",
        vec![("VirtualMFADeviceName", "spare-device")],
    );

    // Enable only the first
    svc.handle_sync(
        "EnableMFADevice",
        vec![
            ("UserName", "filteruser"),
            ("SerialNumber", &serial1),
            ("AuthenticationCode1", "123456"),
            ("AuthenticationCode2", "654321"),
        ],
    );

    // Filter by Assigned
    let assigned = svc.handle_sync(
        "ListVirtualMFADevices",
        vec![("AssignmentStatus", "Assigned")],
    );
    assert!(assigned.contains("enabled-device"));
    assert!(!assigned.contains("spare-device"));

    // Filter by Unassigned
    let unassigned = svc.handle_sync(
        "ListVirtualMFADevices",
        vec![("AssignmentStatus", "Unassigned")],
    );
    assert!(!unassigned.contains("enabled-device"));
    assert!(unassigned.contains("spare-device"));
}

// ---- Account tests ----

#[test]
fn get_account_summary() {
    let svc = make_service();

    // Create some resources to verify counts
    svc.handle_sync("CreateUser", vec![("UserName", "u1")]);
    svc.handle_sync("CreateUser", vec![("UserName", "u2")]);
    svc.handle_sync("CreateGroup", vec![("GroupName", "g1")]);

    let resp = svc.handle_sync("GetAccountSummary", vec![]);
    assert!(resp.contains("<key>Users</key><value>2</value>"));
    assert!(resp.contains("<key>Groups</key><value>1</value>"));
    assert!(resp.contains("<key>UsersQuota</key><value>5000</value>"));
}

#[test]
fn account_alias_lifecycle() {
    let svc = make_service();

    // Create alias
    svc.handle_sync("CreateAccountAlias", vec![("AccountAlias", "my-org")]);

    // List aliases
    let list = svc.handle_sync("ListAccountAliases", vec![]);
    assert!(list.contains("my-org"));

    // Delete alias
    svc.handle_sync("DeleteAccountAlias", vec![("AccountAlias", "my-org")]);

    let list = svc.handle_sync("ListAccountAliases", vec![]);
    assert!(!list.contains("my-org"));
}

#[test]
fn create_account_alias_idempotent() {
    let svc = make_service();
    svc.handle_sync("CreateAccountAlias", vec![("AccountAlias", "test-alias")]);
    svc.handle_sync("CreateAccountAlias", vec![("AccountAlias", "test-alias")]);

    let list = svc.handle_sync("ListAccountAliases", vec![]);
    // Should only appear once
    let count = list.matches("test-alias").count();
    assert_eq!(count, 1, "alias should appear exactly once");
}

// ---- Helper methods for tests ----

fn extract_xml_value(xml: &str, tag: &str) -> String {
    let open = format!("<{tag}>");
    let close = format!("</{tag}>");
    let start = xml.find(&open).unwrap() + open.len();
    let end = xml.find(&close).unwrap();
    xml[start..end].to_string()
}

impl IamService {
    /// Synchronous helper for unit tests: dispatches to the correct method
    fn handle_sync(&self, action: &str, params: Vec<(&str, &str)>) -> String {
        let req = make_request(action, params);
        let resp = match action {
            "CreateUser" => self.create_user(&req),
            "CreateGroup" => self.create_group(&req),
            "CreatePolicy" => self.create_policy(&req),
            "ListPolicies" => self.list_policies(&req),
            "PutGroupPolicy" => self.put_group_policy(&req),
            "GetGroupPolicy" => self.get_group_policy(&req),
            "DeleteGroupPolicy" => self.delete_group_policy(&req),
            "ListGroupPolicies" => self.list_group_policies(&req),
            "AttachGroupPolicy" => self.attach_group_policy(&req),
            "DetachGroupPolicy" => self.detach_group_policy(&req),
            "ListAttachedGroupPolicies" => self.list_attached_group_policies(&req),
            "PutUserPolicy" => self.put_user_policy(&req),
            "GetUserPolicy" => self.get_user_policy(&req),
            "DeleteUserPolicy" => self.delete_user_policy(&req),
            "ListUserPolicies" => self.list_user_policies(&req),
            "AttachUserPolicy" => self.attach_user_policy(&req),
            "DetachUserPolicy" => self.detach_user_policy(&req),
            "ListAttachedUserPolicies" => self.list_attached_user_policies(&req),
            "CreateLoginProfile" => self.create_login_profile(&req),
            "GetLoginProfile" => self.get_login_profile(&req),
            "UpdateLoginProfile" => self.update_login_profile(&req),
            "DeleteLoginProfile" => self.delete_login_profile(&req),
            "CreateVirtualMFADevice" => self.create_virtual_mfa_device(&req),
            "DeleteVirtualMFADevice" => self.delete_virtual_mfa_device(&req),
            "ListVirtualMFADevices" => self.list_virtual_mfa_devices(&req),
            "EnableMFADevice" => self.enable_mfa_device(&req),
            "DeactivateMFADevice" => self.deactivate_mfa_device(&req),
            "ListMFADevices" => self.list_mfa_devices(&req),
            "GetAccountSummary" => self.get_account_summary(&req),
            "CreateAccountAlias" => self.create_account_alias(&req),
            "DeleteAccountAlias" => self.delete_account_alias(&req),
            "ListAccountAliases" => self.list_account_aliases(&req),
            other => panic!("handle_sync: unhandled action {other}"),
        }
        .unwrap();
        String::from_utf8(resp.body.expect_bytes().to_vec()).unwrap()
    }
}

// ---- Policy Version Tests ----

#[test]
fn create_and_get_policy_version() {
    let svc = make_service();
    let policy_doc = r#"{"Version":"2012-10-17","Statement":[{"Effect":"Allow","Action":"s3:GetObject","Resource":"*"}]}"#;
    let req = make_request(
        "CreatePolicy",
        vec![("PolicyName", "test-pol"), ("PolicyDocument", policy_doc)],
    );
    let resp = svc.create_policy(&req).unwrap();
    let body = String::from_utf8_lossy(resp.body.expect_bytes());
    // Extract policy ARN
    let policy_arn = extract_xml_tag(&body, "Arn");

    // Create v2
    let new_doc = r#"{"Version":"2012-10-17","Statement":[{"Effect":"Allow","Action":["s3:GetObject","s3:PutObject"],"Resource":"*"}]}"#;
    let req = make_request(
        "CreatePolicyVersion",
        vec![
            ("PolicyArn", policy_arn),
            ("PolicyDocument", new_doc),
            ("SetAsDefault", "true"),
        ],
    );
    let resp = svc.create_policy_version(&req).unwrap();
    let body = String::from_utf8_lossy(resp.body.expect_bytes());
    assert!(body.contains("<VersionId>v2</VersionId>"));
    assert!(body.contains("<IsDefaultVersion>true</IsDefaultVersion>"));

    // Get v2
    let req = make_request(
        "GetPolicyVersion",
        vec![("PolicyArn", policy_arn), ("VersionId", "v2")],
    );
    let resp = svc.get_policy_version(&req).unwrap();
    let body = String::from_utf8_lossy(resp.body.expect_bytes());
    assert!(body.contains("<VersionId>v2</VersionId>"));
    assert!(body.contains("<IsDefaultVersion>true</IsDefaultVersion>"));
}

#[test]
fn list_policy_versions() {
    let svc = make_service();
    let policy_doc = r#"{"Version":"2012-10-17","Statement":[{"Effect":"Allow","Action":"s3:*","Resource":"*"}]}"#;
    let req = make_request(
        "CreatePolicy",
        vec![("PolicyName", "ver-pol"), ("PolicyDocument", policy_doc)],
    );
    let resp = svc.create_policy(&req).unwrap();
    let body = String::from_utf8_lossy(resp.body.expect_bytes());
    let policy_arn = extract_xml_tag(&body, "Arn");

    // Create v2
    let req = make_request(
        "CreatePolicyVersion",
        vec![("PolicyArn", policy_arn), ("PolicyDocument", policy_doc)],
    );
    svc.create_policy_version(&req).unwrap();

    let req = make_request("ListPolicyVersions", vec![("PolicyArn", policy_arn)]);
    let resp = svc.list_policy_versions(&req).unwrap();
    let body = String::from_utf8_lossy(resp.body.expect_bytes());
    // Should have v1 and v2
    assert!(body.contains("<VersionId>v1</VersionId>"));
    assert!(body.contains("<VersionId>v2</VersionId>"));
    // AWS omits the Document from ListPolicyVersions; only GetPolicyVersion
    // returns it.
    assert!(
        !body.contains("<Document>"),
        "ListPolicyVersions must not include Document, got: {body}"
    );
}

#[test]
fn create_policy_version_omits_document_but_get_returns_it() {
    let svc = make_service();
    let policy_doc = r#"{"Version":"2012-10-17","Statement":[{"Effect":"Allow","Action":"s3:*","Resource":"*"}]}"#;
    let resp = svc
        .create_policy(&make_request(
            "CreatePolicy",
            vec![("PolicyName", "doc-pol"), ("PolicyDocument", policy_doc)],
        ))
        .unwrap();
    let body = String::from_utf8_lossy(resp.body.expect_bytes());
    let policy_arn = extract_xml_tag(&body, "Arn").to_string();

    // CreatePolicyVersion must NOT echo the Document (AWS omits it).
    let resp = svc
        .create_policy_version(&make_request(
            "CreatePolicyVersion",
            vec![("PolicyArn", &policy_arn), ("PolicyDocument", policy_doc)],
        ))
        .unwrap();
    let body = String::from_utf8_lossy(resp.body.expect_bytes());
    assert!(body.contains("<VersionId>v2</VersionId>"));
    assert!(
        !body.contains("<Document>"),
        "CreatePolicyVersion must not include Document, got: {body}"
    );

    // GetPolicyVersion still returns the (URL-encoded) Document.
    let resp = svc
        .get_policy_version(&make_request(
            "GetPolicyVersion",
            vec![("PolicyArn", &policy_arn), ("VersionId", "v2")],
        ))
        .unwrap();
    let body = String::from_utf8_lossy(resp.body.expect_bytes());
    assert!(
        body.contains("<Document>"),
        "GetPolicyVersion must include Document"
    );
}

#[test]
fn delete_default_policy_version_fails() {
    let svc = make_service();
    let policy_doc = r#"{"Version":"2012-10-17","Statement":[{"Effect":"Allow","Action":"s3:*","Resource":"*"}]}"#;
    let req = make_request(
        "CreatePolicy",
        vec![("PolicyName", "def-pol"), ("PolicyDocument", policy_doc)],
    );
    let resp = svc.create_policy(&req).unwrap();
    let body = String::from_utf8_lossy(resp.body.expect_bytes());
    let policy_arn = extract_xml_tag(&body, "Arn");

    // v1 is the default; deleting it should fail
    let req = make_request(
        "DeletePolicyVersion",
        vec![("PolicyArn", policy_arn), ("VersionId", "v1")],
    );
    let result = svc.delete_policy_version(&req);
    assert!(result.is_err(), "deleting default version should fail");
}

#[test]
fn set_default_policy_version() {
    let svc = make_service();
    let policy_doc = r#"{"Version":"2012-10-17","Statement":[{"Effect":"Allow","Action":"s3:*","Resource":"*"}]}"#;
    let req = make_request(
        "CreatePolicy",
        vec![("PolicyName", "sd-pol"), ("PolicyDocument", policy_doc)],
    );
    let resp = svc.create_policy(&req).unwrap();
    let body = String::from_utf8_lossy(resp.body.expect_bytes());
    let policy_arn = extract_xml_tag(&body, "Arn");

    // Create v2
    let req = make_request(
        "CreatePolicyVersion",
        vec![("PolicyArn", policy_arn), ("PolicyDocument", policy_doc)],
    );
    svc.create_policy_version(&req).unwrap();

    // Set v2 as default
    let req = make_request(
        "SetDefaultPolicyVersion",
        vec![("PolicyArn", policy_arn), ("VersionId", "v2")],
    );
    svc.set_default_policy_version(&req).unwrap();

    // Verify v2 is now default
    let req = make_request(
        "GetPolicyVersion",
        vec![("PolicyArn", policy_arn), ("VersionId", "v2")],
    );
    let resp = svc.get_policy_version(&req).unwrap();
    let body = String::from_utf8_lossy(resp.body.expect_bytes());
    assert!(body.contains("<IsDefaultVersion>true</IsDefaultVersion>"));

    // v1 should no longer be default
    let req = make_request(
        "GetPolicyVersion",
        vec![("PolicyArn", policy_arn), ("VersionId", "v1")],
    );
    let resp = svc.get_policy_version(&req).unwrap();
    let body = String::from_utf8_lossy(resp.body.expect_bytes());
    assert!(body.contains("<IsDefaultVersion>false</IsDefaultVersion>"));
}

// ---- Server Certificate Tests ----

#[test]
fn server_certificate_lifecycle() {
    let svc = make_service();

    // Upload
    let req = make_request(
        "UploadServerCertificate",
        vec![
            ("ServerCertificateName", "my-cert"),
            (
                "CertificateBody",
                "-----BEGIN CERTIFICATE-----\ntest\n-----END CERTIFICATE-----",
            ),
            (
                "PrivateKey",
                "-----BEGIN RSA PRIVATE KEY-----\ntest\n-----END RSA PRIVATE KEY-----",
            ),
        ],
    );
    let resp = svc.upload_server_certificate(&req).unwrap();
    let body = String::from_utf8_lossy(resp.body.expect_bytes());
    assert!(body.contains("<ServerCertificateName>my-cert</ServerCertificateName>"));
    assert!(body.contains("ASCA"));

    // Get
    let req = make_request(
        "GetServerCertificate",
        vec![("ServerCertificateName", "my-cert")],
    );
    let resp = svc.get_server_certificate(&req).unwrap();
    let body = String::from_utf8_lossy(resp.body.expect_bytes());
    assert!(body.contains("<ServerCertificateName>my-cert</ServerCertificateName>"));

    // List
    let req = make_request("ListServerCertificates", vec![]);
    let resp = svc.list_server_certificates(&req).unwrap();
    let body = String::from_utf8_lossy(resp.body.expect_bytes());
    assert!(body.contains("my-cert"));

    // Delete
    let req = make_request(
        "DeleteServerCertificate",
        vec![("ServerCertificateName", "my-cert")],
    );
    svc.delete_server_certificate(&req).unwrap();

    // Should be gone
    let req = make_request(
        "GetServerCertificate",
        vec![("ServerCertificateName", "my-cert")],
    );
    assert!(svc.get_server_certificate(&req).is_err());
}

#[test]
fn server_certificate_duplicate_fails() {
    let svc = make_service();
    let req = make_request(
        "UploadServerCertificate",
        vec![
            ("ServerCertificateName", "dup-cert"),
            ("CertificateBody", "cert-body"),
            ("PrivateKey", "key-body"),
        ],
    );
    svc.upload_server_certificate(&req).unwrap();

    let req = make_request(
        "UploadServerCertificate",
        vec![
            ("ServerCertificateName", "dup-cert"),
            ("CertificateBody", "cert-body"),
            ("PrivateKey", "key-body"),
        ],
    );
    assert!(svc.upload_server_certificate(&req).is_err());
}

// ---- SSH Public Key Tests ----

#[test]
fn ssh_public_key_lifecycle() {
    let svc = make_service();
    svc.create_user(&make_request("CreateUser", vec![("UserName", "sshuser")]))
        .unwrap();

    // Upload
    let req = make_request(
        "UploadSSHPublicKey",
        vec![
            ("UserName", "sshuser"),
            (
                "SSHPublicKeyBody",
                "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQ test@example",
            ),
        ],
    );
    let resp = svc.upload_ssh_public_key(&req).unwrap();
    let body = String::from_utf8_lossy(resp.body.expect_bytes());
    assert!(body.contains("<Status>Active</Status>"));
    assert!(body.contains("APKA"));
    // Extract key ID
    let kid_start = body.find("<SSHPublicKeyId>").unwrap() + 16;
    let kid_end = body.find("</SSHPublicKeyId>").unwrap();
    let key_id = &body[kid_start..kid_end];

    // Get
    let req = make_request(
        "GetSSHPublicKey",
        vec![("UserName", "sshuser"), ("SSHPublicKeyId", key_id)],
    );
    let resp = svc.get_ssh_public_key(&req).unwrap();
    let body = String::from_utf8_lossy(resp.body.expect_bytes());
    assert!(body.contains(key_id));
    assert!(body.contains("<Status>Active</Status>"));

    // List
    let req = make_request("ListSSHPublicKeys", vec![("UserName", "sshuser")]);
    let resp = svc.list_ssh_public_keys(&req).unwrap();
    let body = String::from_utf8_lossy(resp.body.expect_bytes());
    assert!(body.contains(key_id));

    // Update status to Inactive
    let req = make_request(
        "UpdateSSHPublicKey",
        vec![
            ("UserName", "sshuser"),
            ("SSHPublicKeyId", key_id),
            ("Status", "Inactive"),
        ],
    );
    svc.update_ssh_public_key(&req).unwrap();

    // Verify status changed
    let req = make_request(
        "GetSSHPublicKey",
        vec![("UserName", "sshuser"), ("SSHPublicKeyId", key_id)],
    );
    let resp = svc.get_ssh_public_key(&req).unwrap();
    let body = String::from_utf8_lossy(resp.body.expect_bytes());
    assert!(body.contains("<Status>Inactive</Status>"));

    // Delete
    let req = make_request(
        "DeleteSSHPublicKey",
        vec![("UserName", "sshuser"), ("SSHPublicKeyId", key_id)],
    );
    svc.delete_ssh_public_key(&req).unwrap();

    // Should be empty now
    let req = make_request("ListSSHPublicKeys", vec![("UserName", "sshuser")]);
    let resp = svc.list_ssh_public_keys(&req).unwrap();
    let body = String::from_utf8_lossy(resp.body.expect_bytes());
    assert!(!body.contains(key_id));
}

// ---- Signing Certificate Tests ----

#[test]
fn signing_certificate_lifecycle() {
    let svc = make_service();
    svc.create_user(&make_request("CreateUser", vec![("UserName", "certuser")]))
        .unwrap();

    let pem = "-----BEGIN CERTIFICATE-----\nMIIBxTCCAW4=\n-----END CERTIFICATE-----";

    // Upload
    let req = make_request(
        "UploadSigningCertificate",
        vec![("UserName", "certuser"), ("CertificateBody", pem)],
    );
    let resp = svc.upload_signing_certificate(&req).unwrap();
    let body = String::from_utf8_lossy(resp.body.expect_bytes());
    assert!(body.contains("<Status>Active</Status>"));
    assert!(body.contains("<UserName>certuser</UserName>"));
    let cid_start = body.find("<CertificateId>").unwrap() + 15;
    let cid_end = body.find("</CertificateId>").unwrap();
    let cert_id = &body[cid_start..cid_end];

    // List
    let req = make_request("ListSigningCertificates", vec![("UserName", "certuser")]);
    let resp = svc.list_signing_certificates(&req).unwrap();
    let body = String::from_utf8_lossy(resp.body.expect_bytes());
    assert!(body.contains(cert_id));

    // Update to Inactive
    let req = make_request(
        "UpdateSigningCertificate",
        vec![
            ("UserName", "certuser"),
            ("CertificateId", cert_id),
            ("Status", "Inactive"),
        ],
    );
    svc.update_signing_certificate(&req).unwrap();

    // Delete
    let req = make_request(
        "DeleteSigningCertificate",
        vec![("UserName", "certuser"), ("CertificateId", cert_id)],
    );
    svc.delete_signing_certificate(&req).unwrap();

    // Should be empty
    let req = make_request("ListSigningCertificates", vec![("UserName", "certuser")]);
    let resp = svc.list_signing_certificates(&req).unwrap();
    let body = String::from_utf8_lossy(resp.body.expect_bytes());
    assert!(!body.contains(cert_id));
}

#[test]
fn signing_certificate_malformed_pem_fails() {
    let svc = make_service();
    svc.create_user(&make_request("CreateUser", vec![("UserName", "badcert")]))
        .unwrap();

    let req = make_request(
        "UploadSigningCertificate",
        vec![
            ("UserName", "badcert"),
            ("CertificateBody", "not-a-pem-cert"),
        ],
    );
    assert!(svc.upload_signing_certificate(&req).is_err());
}

// ---- Credential Report Tests ----

#[test]
fn credential_report_lifecycle() {
    let svc = make_service();

    // GetCredentialReport without generating first should fail
    let req = make_request("GetCredentialReport", vec![]);
    assert!(svc.get_credential_report(&req).is_err());

    // Generate
    let req = make_request("GenerateCredentialReport", vec![]);
    let resp = svc.generate_credential_report(&req).unwrap();
    let body = String::from_utf8_lossy(resp.body.expect_bytes());
    assert!(body.contains("<State>STARTED</State>"));

    // Generate again returns COMPLETE
    let req = make_request("GenerateCredentialReport", vec![]);
    let resp = svc.generate_credential_report(&req).unwrap();
    let body = String::from_utf8_lossy(resp.body.expect_bytes());
    assert!(body.contains("<State>COMPLETE</State>"));

    // Get credential report
    let req = make_request("GetCredentialReport", vec![]);
    let resp = svc.get_credential_report(&req).unwrap();
    let body = String::from_utf8_lossy(resp.body.expect_bytes());
    assert!(body.contains("<ReportFormat>text/csv</ReportFormat>"));
    assert!(body.contains("<Content>"));
}

// ---- Service Linked Role Tests ----

#[test]
fn service_linked_role_lifecycle() {
    let svc = make_service();

    // Create
    let req = make_request(
        "CreateServiceLinkedRole",
        vec![("AWSServiceName", "elasticloadbalancing.amazonaws.com")],
    );
    let resp = svc.create_service_linked_role(&req).unwrap();
    let body = String::from_utf8_lossy(resp.body.expect_bytes());
    assert!(body.contains("AWSServiceRoleForElasticLoadBalancing"));
    assert!(body.contains("/aws-service-role/elasticloadbalancing.amazonaws.com/"));

    // Delete
    let req = make_request(
        "DeleteServiceLinkedRole",
        vec![("RoleName", "AWSServiceRoleForElasticLoadBalancing")],
    );
    let resp = svc.delete_service_linked_role(&req).unwrap();
    let body = String::from_utf8_lossy(resp.body.expect_bytes());
    assert!(body.contains("<DeletionTaskId>"));
    let tid_start = body.find("<DeletionTaskId>").unwrap() + 16;
    let tid_end = body.find("</DeletionTaskId>").unwrap();
    let task_id = &body[tid_start..tid_end];

    // Check deletion status
    let req = make_request(
        "GetServiceLinkedRoleDeletionStatus",
        vec![("DeletionTaskId", task_id)],
    );
    let resp = svc.get_service_linked_role_deletion_status(&req).unwrap();
    let body = String::from_utf8_lossy(resp.body.expect_bytes());
    assert!(body.contains("<Status>SUCCEEDED</Status>"));
}

#[test]
fn create_service_linked_role_uses_region_partition() {
    let svc = make_service();
    // GovCloud region -> aws-us-gov partition in the role ARN.
    let mut req = make_request(
        "CreateServiceLinkedRole",
        vec![("AWSServiceName", "elasticloadbalancing.amazonaws.com")],
    );
    req.region = "us-gov-west-1".to_string();
    let resp = svc.create_service_linked_role(&req).unwrap();
    let body = String::from_utf8_lossy(resp.body.expect_bytes());
    assert!(
        body.contains("arn:aws-us-gov:iam::"),
        "SLR ARN must use the gov partition, got: {body}"
    );
    assert!(!body.contains("arn:aws:iam::"));
}

// ---- Permission Boundary Tests ----

#[test]
fn role_permissions_boundary() {
    let svc = make_service();
    let trust = r#"{"Version":"2012-10-17","Statement":[{"Effect":"Allow","Principal":{"Service":"ec2.amazonaws.com"},"Action":"sts:AssumeRole"}]}"#;
    svc.create_role(&make_request(
        "CreateRole",
        vec![
            ("RoleName", "bound-role"),
            ("AssumeRolePolicyDocument", trust),
        ],
    ))
    .unwrap();

    // Put boundary
    let boundary_arn = "arn:aws:iam::123456789012:policy/boundary-policy";
    let req = make_request(
        "PutRolePermissionsBoundary",
        vec![
            ("RoleName", "bound-role"),
            ("PermissionsBoundary", boundary_arn),
        ],
    );
    svc.put_role_permissions_boundary(&req).unwrap();

    // Verify via GetRole
    let req = make_request("GetRole", vec![("RoleName", "bound-role")]);
    let resp = svc.get_role(&req).unwrap();
    let body = String::from_utf8_lossy(resp.body.expect_bytes());
    assert!(body.contains(boundary_arn));

    // Delete boundary
    let req = make_request(
        "DeleteRolePermissionsBoundary",
        vec![("RoleName", "bound-role")],
    );
    svc.delete_role_permissions_boundary(&req).unwrap();

    // Verify removed
    let req = make_request("GetRole", vec![("RoleName", "bound-role")]);
    let resp = svc.get_role(&req).unwrap();
    let body = String::from_utf8_lossy(resp.body.expect_bytes());
    assert!(!body.contains(boundary_arn));
}

// ---- Tag Role Tests ----

#[test]
fn tag_untag_role() {
    let svc = make_service();
    let trust = r#"{"Version":"2012-10-17","Statement":[{"Effect":"Allow","Principal":{"Service":"ec2.amazonaws.com"},"Action":"sts:AssumeRole"}]}"#;
    svc.create_role(&make_request(
        "CreateRole",
        vec![
            ("RoleName", "tag-role"),
            ("AssumeRolePolicyDocument", trust),
        ],
    ))
    .unwrap();

    // Tag
    let req = make_request(
        "TagRole",
        vec![
            ("RoleName", "tag-role"),
            ("Tags.member.1.Key", "env"),
            ("Tags.member.1.Value", "prod"),
        ],
    );
    svc.tag_role(&req).unwrap();

    // List tags
    let req = make_request("ListRoleTags", vec![("RoleName", "tag-role")]);
    let resp = svc.list_role_tags(&req).unwrap();
    let body = String::from_utf8_lossy(resp.body.expect_bytes());
    assert!(body.contains("<Key>env</Key>"));
    assert!(body.contains("<Value>prod</Value>"));

    // Untag
    let req = make_request(
        "UntagRole",
        vec![("RoleName", "tag-role"), ("TagKeys.member.1", "env")],
    );
    svc.untag_role(&req).unwrap();

    let req = make_request("ListRoleTags", vec![("RoleName", "tag-role")]);
    let resp = svc.list_role_tags(&req).unwrap();
    let body = String::from_utf8_lossy(resp.body.expect_bytes());
    assert!(!body.contains("<Key>env</Key>"));
}

#[test]
fn list_role_tags_rejects_invalid_marker() {
    // bug-audit 2026-05-28, 1.7: a Marker that does not parse as a valid offset
    // must be rejected with "Invalid Marker." (the same ValidationError the
    // other IAM list ops return) rather than silently falling back to the first
    // page.
    let svc = make_service();
    let trust = r#"{"Version":"2012-10-17","Statement":[{"Effect":"Allow","Principal":{"Service":"ec2.amazonaws.com"},"Action":"sts:AssumeRole"}]}"#;
    svc.create_role(&make_request(
        "CreateRole",
        vec![
            ("RoleName", "marker-role"),
            ("AssumeRolePolicyDocument", trust),
        ],
    ))
    .unwrap();

    let req = make_request(
        "ListRoleTags",
        vec![("RoleName", "marker-role"), ("Marker", "not-a-number")],
    );
    match svc.list_role_tags(&req) {
        Ok(_) => panic!("malformed Marker must be rejected"),
        Err(err) => {
            assert_eq!(err.code(), "ValidationError");
            assert!(err.message().contains("Invalid Marker"));
        }
    }
}

// ---- Tag Policy Tests ----

#[test]
fn tag_untag_policy() {
    let svc = make_service();
    let policy_doc = r#"{"Version":"2012-10-17","Statement":[{"Effect":"Allow","Action":"s3:*","Resource":"*"}]}"#;
    let req = make_request(
        "CreatePolicy",
        vec![("PolicyName", "tag-pol"), ("PolicyDocument", policy_doc)],
    );
    let resp = svc.create_policy(&req).unwrap();
    let body = String::from_utf8_lossy(resp.body.expect_bytes());
    let policy_arn = extract_xml_tag(&body, "Arn").to_string();

    let req = make_request(
        "TagPolicy",
        vec![
            ("PolicyArn", &policy_arn),
            ("Tags.member.1.Key", "team"),
            ("Tags.member.1.Value", "platform"),
        ],
    );
    svc.tag_policy(&req).unwrap();

    let req = make_request("ListPolicyTags", vec![("PolicyArn", &policy_arn)]);
    let resp = svc.list_policy_tags(&req).unwrap();
    let body = String::from_utf8_lossy(resp.body.expect_bytes());
    assert!(body.contains("<Key>team</Key>"));

    let req = make_request(
        "UntagPolicy",
        vec![("PolicyArn", &policy_arn), ("TagKeys.member.1", "team")],
    );
    svc.untag_policy(&req).unwrap();

    let req = make_request("ListPolicyTags", vec![("PolicyArn", &policy_arn)]);
    let resp = svc.list_policy_tags(&req).unwrap();
    let body = String::from_utf8_lossy(resp.body.expect_bytes());
    assert!(!body.contains("<Key>team</Key>"));
}

// ---- Tag Instance Profile Tests ----

#[test]
fn tag_untag_instance_profile() {
    let svc = make_service();
    svc.create_instance_profile(&make_request(
        "CreateInstanceProfile",
        vec![("InstanceProfileName", "tag-ip")],
    ))
    .unwrap();

    let req = make_request(
        "TagInstanceProfile",
        vec![
            ("InstanceProfileName", "tag-ip"),
            ("Tags.member.1.Key", "dept"),
            ("Tags.member.1.Value", "eng"),
        ],
    );
    svc.tag_instance_profile(&req).unwrap();

    let req = make_request(
        "ListInstanceProfileTags",
        vec![("InstanceProfileName", "tag-ip")],
    );
    let resp = svc.list_instance_profile_tags(&req).unwrap();
    let body = String::from_utf8_lossy(resp.body.expect_bytes());
    assert!(body.contains("<Key>dept</Key>"));

    let req = make_request(
        "UntagInstanceProfile",
        vec![
            ("InstanceProfileName", "tag-ip"),
            ("TagKeys.member.1", "dept"),
        ],
    );
    svc.untag_instance_profile(&req).unwrap();

    let req = make_request(
        "ListInstanceProfileTags",
        vec![("InstanceProfileName", "tag-ip")],
    );
    let resp = svc.list_instance_profile_tags(&req).unwrap();
    let body = String::from_utf8_lossy(resp.body.expect_bytes());
    assert!(!body.contains("<Key>dept</Key>"));
}

// ---- Tag OIDC Provider Tests ----

#[test]
fn tag_untag_oidc_provider() {
    let svc = make_service();
    let req = make_request(
        "CreateOpenIDConnectProvider",
        vec![
            ("Url", "https://oidc.example.com"),
            (
                "ThumbprintList.member.1",
                "abcdef1234567890abcdef1234567890abcdef12",
            ),
        ],
    );
    let resp = svc.create_oidc_provider(&req).unwrap();
    let body = String::from_utf8_lossy(resp.body.expect_bytes());
    let arn_start =
        body.find("<OpenIDConnectProviderArn>").unwrap() + "<OpenIDConnectProviderArn>".len();
    let arn_end = body.find("</OpenIDConnectProviderArn>").unwrap();
    let oidc_arn = body[arn_start..arn_end].to_string();

    let req = make_request(
        "TagOpenIDConnectProvider",
        vec![
            ("OpenIDConnectProviderArn", &oidc_arn),
            ("Tags.member.1.Key", "stage"),
            ("Tags.member.1.Value", "dev"),
        ],
    );
    svc.tag_oidc_provider(&req).unwrap();

    let req = make_request(
        "ListOpenIDConnectProviderTags",
        vec![("OpenIDConnectProviderArn", &oidc_arn)],
    );
    let resp = svc.list_oidc_provider_tags(&req).unwrap();
    let body = String::from_utf8_lossy(resp.body.expect_bytes());
    assert!(body.contains("<Key>stage</Key>"));

    let req = make_request(
        "UntagOpenIDConnectProvider",
        vec![
            ("OpenIDConnectProviderArn", &oidc_arn),
            ("TagKeys.member.1", "stage"),
        ],
    );
    svc.untag_oidc_provider(&req).unwrap();

    let req = make_request(
        "ListOpenIDConnectProviderTags",
        vec![("OpenIDConnectProviderArn", &oidc_arn)],
    );
    let resp = svc.list_oidc_provider_tags(&req).unwrap();
    let body = String::from_utf8_lossy(resp.body.expect_bytes());
    assert!(!body.contains("<Key>stage</Key>"));
}

// ---- Update Role Tests ----

#[test]
fn update_role_description_and_max_session() {
    let svc = make_service();
    let trust = r#"{"Version":"2012-10-17","Statement":[{"Effect":"Allow","Principal":{"Service":"ec2.amazonaws.com"},"Action":"sts:AssumeRole"}]}"#;
    svc.create_role(&make_request(
        "CreateRole",
        vec![
            ("RoleName", "upd-role"),
            ("AssumeRolePolicyDocument", trust),
        ],
    ))
    .unwrap();

    // UpdateRole with Description and MaxSessionDuration
    let req = make_request(
        "UpdateRole",
        vec![
            ("RoleName", "upd-role"),
            ("Description", "new description"),
            ("MaxSessionDuration", "7200"),
        ],
    );
    svc.update_role(&req).unwrap();

    // UpdateRoleDescription
    let req = make_request(
        "UpdateRoleDescription",
        vec![("RoleName", "upd-role"), ("Description", "updated desc")],
    );
    let resp = svc.update_role_description(&req).unwrap();
    let body = String::from_utf8_lossy(resp.body.expect_bytes());
    assert!(body.contains("<Description>updated desc</Description>"));
}

// ---- UpdateAssumeRolePolicy Tests ----

#[test]
fn update_assume_role_policy() {
    let svc = make_service();
    let trust = r#"{"Version":"2012-10-17","Statement":[{"Effect":"Allow","Principal":{"Service":"ec2.amazonaws.com"},"Action":"sts:AssumeRole"}]}"#;
    svc.create_role(&make_request(
        "CreateRole",
        vec![
            ("RoleName", "arp-role"),
            ("AssumeRolePolicyDocument", trust),
        ],
    ))
    .unwrap();

    let new_trust = r#"{"Version":"2012-10-17","Statement":[{"Effect":"Allow","Principal":{"Service":"lambda.amazonaws.com"},"Action":"sts:AssumeRole"}]}"#;
    let req = make_request(
        "UpdateAssumeRolePolicy",
        vec![("RoleName", "arp-role"), ("PolicyDocument", new_trust)],
    );
    svc.update_assume_role_policy(&req).unwrap();

    // Verify by GetRole
    let req = make_request("GetRole", vec![("RoleName", "arp-role")]);
    let resp = svc.get_role(&req).unwrap();
    let body = String::from_utf8_lossy(resp.body.expect_bytes());
    assert!(body.contains("lambda.amazonaws.com"));
}

// ---- UpdateGroup Tests ----

#[test]
fn update_group_rename() {
    let svc = make_service();
    svc.create_group(&make_request("CreateGroup", vec![("GroupName", "old-grp")]))
        .unwrap();

    let req = make_request(
        "UpdateGroup",
        vec![("GroupName", "old-grp"), ("NewGroupName", "new-grp")],
    );
    svc.update_group(&req).unwrap();

    // Old name should not exist
    assert!(svc
        .get_group(&make_request("GetGroup", vec![("GroupName", "old-grp")]))
        .is_err());

    // New name should exist
    let resp = svc
        .get_group(&make_request("GetGroup", vec![("GroupName", "new-grp")]))
        .unwrap();
    let body = String::from_utf8_lossy(resp.body.expect_bytes());
    assert!(body.contains("<GroupName>new-grp</GroupName>"));
}

// ---- UpdateUser Tests ----

#[test]
fn update_user_rename() {
    let svc = make_service();
    svc.create_user(&make_request("CreateUser", vec![("UserName", "old-user")]))
        .unwrap();

    let req = make_request(
        "UpdateUser",
        vec![("UserName", "old-user"), ("NewUserName", "new-user")],
    );
    svc.update_user(&req).unwrap();

    assert!(svc
        .get_user(&make_request("GetUser", vec![("UserName", "old-user")]))
        .is_err());

    let resp = svc
        .get_user(&make_request("GetUser", vec![("UserName", "new-user")]))
        .unwrap();
    let body = String::from_utf8_lossy(resp.body.expect_bytes());
    assert!(body.contains("<UserName>new-user</UserName>"));
}

/// 1.29: UpdateUser rename must preserve the ARN partition (aws-cn here)
/// rather than hardcoding `arn:aws:`.
#[test]
fn update_user_rename_preserves_partition() {
    let svc = make_service();
    let mut create = make_request("CreateUser", vec![("UserName", "old-cn")]);
    create.region = "cn-north-1".to_string();
    let resp = svc.create_user(&create).unwrap();
    let body = String::from_utf8_lossy(resp.body.expect_bytes());
    assert!(
        extract_xml_tag(&body, "Arn").starts_with("arn:aws-cn:iam::"),
        "create should mint an aws-cn ARN"
    );

    let mut rename = make_request(
        "UpdateUser",
        vec![("UserName", "old-cn"), ("NewUserName", "new-cn")],
    );
    rename.region = "cn-north-1".to_string();
    svc.update_user(&rename).unwrap();

    let mut get = make_request("GetUser", vec![("UserName", "new-cn")]);
    get.region = "cn-north-1".to_string();
    let resp = svc.get_user(&get).unwrap();
    let body = String::from_utf8_lossy(resp.body.expect_bytes());
    let arn = extract_xml_tag(&body, "Arn");
    assert!(
        arn.starts_with("arn:aws-cn:iam::"),
        "rename must keep the aws-cn partition, got {arn}"
    );
    assert!(arn.ends_with(":user/new-cn"));
}

// ---- Account Password Policy Tests ----

#[test]
fn account_password_policy_lifecycle() {
    let svc = make_service();

    // Get before setting returns error
    let req = make_request("GetAccountPasswordPolicy", vec![]);
    assert!(svc.get_account_password_policy(&req).is_err());

    // Update (creates the policy)
    let req = make_request(
        "UpdateAccountPasswordPolicy",
        vec![
            ("MinimumPasswordLength", "12"),
            ("RequireSymbols", "true"),
            ("RequireNumbers", "true"),
        ],
    );
    svc.update_account_password_policy(&req).unwrap();

    // Get
    let req = make_request("GetAccountPasswordPolicy", vec![]);
    let resp = svc.get_account_password_policy(&req).unwrap();
    let body = String::from_utf8_lossy(resp.body.expect_bytes());
    assert!(body.contains("<MinimumPasswordLength>12</MinimumPasswordLength>"));
    assert!(body.contains("<RequireSymbols>true</RequireSymbols>"));

    // Delete
    let req = make_request("DeleteAccountPasswordPolicy", vec![]);
    svc.delete_account_password_policy(&req).unwrap();

    // Should be gone
    let req = make_request("GetAccountPasswordPolicy", vec![]);
    assert!(svc.get_account_password_policy(&req).is_err());
}

// ---- GetAccountAuthorizationDetails Tests ----

#[test]
fn get_account_authorization_details() {
    let svc = make_service();

    // Create a user and role
    svc.create_user(&make_request("CreateUser", vec![("UserName", "auth-user")]))
        .unwrap();
    let trust = r#"{"Version":"2012-10-17","Statement":[{"Effect":"Allow","Principal":{"Service":"ec2.amazonaws.com"},"Action":"sts:AssumeRole"}]}"#;
    svc.create_role(&make_request(
        "CreateRole",
        vec![
            ("RoleName", "auth-role"),
            ("AssumeRolePolicyDocument", trust),
        ],
    ))
    .unwrap();

    let req = make_request("GetAccountAuthorizationDetails", vec![]);
    let resp = svc.get_account_authorization_details(&req).unwrap();
    let body = String::from_utf8_lossy(resp.body.expect_bytes());
    assert!(body.contains("<UserName>auth-user</UserName>"));
    assert!(body.contains("<RoleName>auth-role</RoleName>"));
}

#[test]
fn get_account_authorization_details_paginates_by_marker() {
    // MaxItems/Marker were validated then ignored: everything returned with
    // IsTruncated=false (bug-audit 2026-06-20, 1.14).
    let svc = make_service();
    for n in ["aaa", "bbb", "ccc"] {
        svc.create_user(&make_request("CreateUser", vec![("UserName", n)]))
            .unwrap();
    }

    // First page of 2 users is truncated and carries a Marker. Scope to
    // `Filter=User` so the default service-linked roles every account ships
    // don't interleave with the users this test paginates.
    let resp = svc
        .get_account_authorization_details(&make_request(
            "GetAccountAuthorizationDetails",
            vec![("MaxItems", "2"), ("Filter.member.1", "User")],
        ))
        .unwrap();
    let body = String::from_utf8_lossy(resp.body.expect_bytes()).to_string();
    assert!(body.contains("<IsTruncated>true</IsTruncated>"), "{body}");
    assert!(body.contains("<UserName>aaa</UserName>"));
    assert!(body.contains("<UserName>bbb</UserName>"));
    assert!(!body.contains("<UserName>ccc</UserName>"));
    let marker = extract_xml_tag(&body, "Marker").to_string();
    assert!(!marker.is_empty());

    // Resume: the last user, not truncated, no Marker.
    let resp = svc
        .get_account_authorization_details(&make_request(
            "GetAccountAuthorizationDetails",
            vec![
                ("MaxItems", "2"),
                ("Marker", &marker),
                ("Filter.member.1", "User"),
            ],
        ))
        .unwrap();
    let body = String::from_utf8_lossy(resp.body.expect_bytes()).to_string();
    assert!(body.contains("<IsTruncated>false</IsTruncated>"), "{body}");
    assert!(body.contains("<UserName>ccc</UserName>"));
    assert!(!body.contains("<UserName>aaa</UserName>"));
}

#[test]
fn get_account_authorization_details_filter_restricts_types() {
    // Filter was ignored; with Filter=Role only roles should appear.
    let svc = make_service();
    svc.create_user(&make_request("CreateUser", vec![("UserName", "f-user")]))
        .unwrap();
    let trust = r#"{"Version":"2012-10-17","Statement":[{"Effect":"Allow","Principal":{"Service":"ec2.amazonaws.com"},"Action":"sts:AssumeRole"}]}"#;
    svc.create_role(&make_request(
        "CreateRole",
        vec![("RoleName", "f-role"), ("AssumeRolePolicyDocument", trust)],
    ))
    .unwrap();

    let resp = svc
        .get_account_authorization_details(&make_request(
            "GetAccountAuthorizationDetails",
            vec![("Filter.member.1", "Role")],
        ))
        .unwrap();
    let body = String::from_utf8_lossy(resp.body.expect_bytes()).to_string();
    assert!(body.contains("<RoleName>f-role</RoleName>"));
    assert!(!body.contains("<UserName>f-user</UserName>"), "{body}");
}

// ---- ListEntitiesForPolicy Tests ----

#[test]
fn list_entities_for_policy() {
    let svc = make_service();

    // Create policy
    let policy_doc = r#"{"Version":"2012-10-17","Statement":[{"Effect":"Allow","Action":"s3:*","Resource":"*"}]}"#;
    let req = make_request(
        "CreatePolicy",
        vec![("PolicyName", "ent-pol"), ("PolicyDocument", policy_doc)],
    );
    let resp = svc.create_policy(&req).unwrap();
    let body = String::from_utf8_lossy(resp.body.expect_bytes());
    let policy_arn = extract_xml_tag(&body, "Arn").to_string();

    // Create role and attach policy
    let trust = r#"{"Version":"2012-10-17","Statement":[{"Effect":"Allow","Principal":{"Service":"ec2.amazonaws.com"},"Action":"sts:AssumeRole"}]}"#;
    svc.create_role(&make_request(
        "CreateRole",
        vec![
            ("RoleName", "ent-role"),
            ("AssumeRolePolicyDocument", trust),
        ],
    ))
    .unwrap();
    svc.attach_role_policy(&make_request(
        "AttachRolePolicy",
        vec![("RoleName", "ent-role"), ("PolicyArn", &policy_arn)],
    ))
    .unwrap();

    // Create user and attach policy
    svc.create_user(&make_request("CreateUser", vec![("UserName", "ent-user")]))
        .unwrap();
    svc.attach_user_policy(&make_request(
        "AttachUserPolicy",
        vec![("UserName", "ent-user"), ("PolicyArn", &policy_arn)],
    ))
    .unwrap();

    let req = make_request("ListEntitiesForPolicy", vec![("PolicyArn", &policy_arn)]);
    let resp = svc.list_entities_for_policy(&req).unwrap();
    let body = String::from_utf8_lossy(resp.body.expect_bytes());
    assert!(body.contains("<RoleName>ent-role</RoleName>"));
    assert!(body.contains("<UserName>ent-user</UserName>"));
}

// ---- GetAccessKeyLastUsed Tests ----

#[test]
fn get_access_key_last_used() {
    let svc = make_service();
    svc.create_user(&make_request("CreateUser", vec![("UserName", "keyuser")]))
        .unwrap();

    let req = make_request("CreateAccessKey", vec![("UserName", "keyuser")]);
    let resp = svc.create_access_key(&req).unwrap();
    let body = String::from_utf8_lossy(resp.body.expect_bytes());
    let kid_start = body.find("<AccessKeyId>").unwrap() + 13;
    let kid_end = body.find("</AccessKeyId>").unwrap();
    let key_id = body[kid_start..kid_end].to_string();

    let req = make_request("GetAccessKeyLastUsed", vec![("AccessKeyId", &key_id)]);
    let resp = svc.get_access_key_last_used(&req).unwrap();
    let body = String::from_utf8_lossy(resp.body.expect_bytes());
    assert!(body.contains("<UserName>keyuser</UserName>"));
    // No last used info yet -- should show N/A
    assert!(body.contains("<ServiceName>N/A</ServiceName>"));
}

fn expect_err(result: Result<AwsResponse, AwsServiceError>) -> AwsServiceError {
    match result {
        Err(e) => e,
        Ok(_) => panic!("expected error, got Ok"),
    }
}

// ── User CRUD ──

#[test]
fn create_get_delete_user() {
    let svc = make_service();

    let req = make_request("CreateUser", vec![("UserName", "alice")]);
    let resp = svc.create_user(&req).unwrap();
    let body = String::from_utf8_lossy(resp.body.expect_bytes()).to_string();
    assert!(body.contains("<UserName>alice</UserName>"));
    assert!(body.contains("<Arn>"));

    // Get
    let req = make_request("GetUser", vec![("UserName", "alice")]);
    let resp = svc.get_user(&req).unwrap();
    let body = String::from_utf8_lossy(resp.body.expect_bytes()).to_string();
    assert!(body.contains("<UserName>alice</UserName>"));

    // Delete
    let req = make_request("DeleteUser", vec![("UserName", "alice")]);
    svc.delete_user(&req).unwrap();

    // Get should fail
    let req = make_request("GetUser", vec![("UserName", "alice")]);
    let err = expect_err(svc.get_user(&req));
    assert!(err.to_string().contains("NoSuchEntity"));
}

#[test]
fn create_user_duplicate_fails() {
    let svc = make_service();
    svc.create_user(&make_request("CreateUser", vec![("UserName", "dup")]))
        .unwrap();

    let err = expect_err(svc.create_user(&make_request("CreateUser", vec![("UserName", "dup")])));
    assert!(err.to_string().contains("EntityAlreadyExists"));
}

#[test]
fn list_users_basic() {
    let svc = make_service();
    svc.create_user(&make_request("CreateUser", vec![("UserName", "u1")]))
        .unwrap();
    svc.create_user(&make_request("CreateUser", vec![("UserName", "u2")]))
        .unwrap();

    let req = make_request("ListUsers", vec![]);
    let resp = svc.list_users(&req).unwrap();
    let body = String::from_utf8_lossy(resp.body.expect_bytes()).to_string();
    assert!(body.contains("<UserName>u1</UserName>"));
    assert!(body.contains("<UserName>u2</UserName>"));
}

fn count_occurrences(haystack: &str, needle: &str) -> usize {
    haystack.matches(needle).count()
}

fn extract_marker(body: &str) -> Option<String> {
    let start = body.find("<Marker>")? + "<Marker>".len();
    let end = body[start..].find("</Marker>")? + start;
    Some(body[start..end].to_string())
}

#[test]
fn list_users_paginates_via_marker() {
    let svc = make_service();
    for i in 0..5 {
        svc.create_user(&make_request(
            "CreateUser",
            vec![("UserName", &format!("user-{i:02}"))],
        ))
        .unwrap();
    }

    // Page 1: MaxItems=2 -> 2 users, IsTruncated true, a Marker.
    let body1 = String::from_utf8_lossy(
        svc.list_users(&make_request("ListUsers", vec![("MaxItems", "2")]))
            .unwrap()
            .body
            .expect_bytes(),
    )
    .to_string();
    assert_eq!(count_occurrences(&body1, "<member>"), 2);
    assert!(body1.contains("<IsTruncated>true</IsTruncated>"));
    let marker = extract_marker(&body1).expect("Marker on page 1");

    // Page 2: feed the Marker back -> next 2 users.
    let body2 = String::from_utf8_lossy(
        svc.list_users(&make_request(
            "ListUsers",
            vec![("MaxItems", "2"), ("Marker", &marker)],
        ))
        .unwrap()
        .body
        .expect_bytes(),
    )
    .to_string();
    assert_eq!(count_occurrences(&body2, "<member>"), 2);
    assert!(body2.contains("<IsTruncated>true</IsTruncated>"));
    let marker = extract_marker(&body2).expect("Marker on page 2");

    // Page 3: last user, not truncated, no Marker.
    let body3 = String::from_utf8_lossy(
        svc.list_users(&make_request(
            "ListUsers",
            vec![("MaxItems", "2"), ("Marker", &marker)],
        ))
        .unwrap()
        .body
        .expect_bytes(),
    )
    .to_string();
    assert_eq!(count_occurrences(&body3, "<member>"), 1);
    assert!(body3.contains("<IsTruncated>false</IsTruncated>"));
    assert!(extract_marker(&body3).is_none());
}

#[test]
fn list_groups_paginates_via_marker() {
    let svc = make_service();
    for i in 0..3 {
        svc.create_group(&make_request(
            "CreateGroup",
            vec![("GroupName", &format!("grp-{i}"))],
        ))
        .unwrap();
    }
    let body1 = String::from_utf8_lossy(
        svc.list_groups(&make_request("ListGroups", vec![("MaxItems", "2")]))
            .unwrap()
            .body
            .expect_bytes(),
    )
    .to_string();
    assert_eq!(count_occurrences(&body1, "<member>"), 2);
    assert!(body1.contains("<IsTruncated>true</IsTruncated>"));
    let marker = extract_marker(&body1).expect("Marker");

    let body2 = String::from_utf8_lossy(
        svc.list_groups(&make_request(
            "ListGroups",
            vec![("MaxItems", "2"), ("Marker", &marker)],
        ))
        .unwrap()
        .body
        .expect_bytes(),
    )
    .to_string();
    assert_eq!(count_occurrences(&body2, "<member>"), 1);
    assert!(body2.contains("<IsTruncated>false</IsTruncated>"));
}

#[test]
fn list_policies_paginates_via_marker() {
    let svc = make_service();
    let doc = r#"{"Version":"2012-10-17","Statement":[{"Effect":"Allow","Action":"s3:*","Resource":"*"}]}"#;
    for i in 0..3 {
        svc.create_policy(&make_request(
            "CreatePolicy",
            vec![("PolicyName", &format!("pol-{i}")), ("PolicyDocument", doc)],
        ))
        .unwrap();
    }
    // Scope=Local isolates the customer-managed policies created above from the
    // seeded AWS-managed catalog (which Scope=All now also returns).
    let body1 = String::from_utf8_lossy(
        svc.list_policies(&make_request(
            "ListPolicies",
            vec![("MaxItems", "2"), ("Scope", "Local")],
        ))
        .unwrap()
        .body
        .expect_bytes(),
    )
    .to_string();
    assert_eq!(count_occurrences(&body1, "<member>"), 2);
    assert!(body1.contains("<IsTruncated>true</IsTruncated>"));
    let marker = extract_marker(&body1).expect("Marker");

    let body2 = String::from_utf8_lossy(
        svc.list_policies(&make_request(
            "ListPolicies",
            vec![("MaxItems", "2"), ("Marker", &marker), ("Scope", "Local")],
        ))
        .unwrap()
        .body
        .expect_bytes(),
    )
    .to_string();
    assert_eq!(count_occurrences(&body2, "<member>"), 1);
    assert!(body2.contains("<IsTruncated>false</IsTruncated>"));
}

#[test]
fn create_user_with_path_and_tags() {
    let svc = make_service();
    let req = make_request(
        "CreateUser",
        vec![
            ("UserName", "tagged-user"),
            ("Path", "/engineering/"),
            ("Tags.member.1.Key", "team"),
            ("Tags.member.1.Value", "backend"),
        ],
    );
    let resp = svc.create_user(&req).unwrap();
    let body = String::from_utf8_lossy(resp.body.expect_bytes()).to_string();
    assert!(body.contains("/engineering/"));
}

#[test]
fn user_permissions_boundary() {
    let svc = make_service();
    svc.create_user(&make_request("CreateUser", vec![("UserName", "bounded")]))
        .unwrap();

    let policy_arn = "arn:aws:iam::123456789012:policy/boundary";
    let req = make_request(
        "PutUserPermissionsBoundary",
        vec![("UserName", "bounded"), ("PermissionsBoundary", policy_arn)],
    );
    svc.put_user_permissions_boundary(&req).unwrap();

    let req = make_request("GetUser", vec![("UserName", "bounded")]);
    let resp = svc.get_user(&req).unwrap();
    let body = String::from_utf8_lossy(resp.body.expect_bytes()).to_string();
    assert!(body.contains(policy_arn));

    let req = make_request(
        "DeleteUserPermissionsBoundary",
        vec![("UserName", "bounded")],
    );
    svc.delete_user_permissions_boundary(&req).unwrap();
}

// ── Role CRUD ──

#[test]
fn create_get_delete_role() {
    let svc = make_service();

    let trust = r#"{"Version":"2012-10-17","Statement":[{"Effect":"Allow","Principal":{"Service":"ec2.amazonaws.com"},"Action":"sts:AssumeRole"}]}"#;
    let req = make_request(
        "CreateRole",
        vec![
            ("RoleName", "test-role"),
            ("AssumeRolePolicyDocument", trust),
        ],
    );
    let resp = svc.create_role(&req).unwrap();
    let body = String::from_utf8_lossy(resp.body.expect_bytes()).to_string();
    assert!(body.contains("<RoleName>test-role</RoleName>"));

    // Get
    let req = make_request("GetRole", vec![("RoleName", "test-role")]);
    let resp = svc.get_role(&req).unwrap();
    let body = String::from_utf8_lossy(resp.body.expect_bytes()).to_string();
    assert!(body.contains("<RoleName>test-role</RoleName>"));

    // Delete
    let req = make_request("DeleteRole", vec![("RoleName", "test-role")]);
    svc.delete_role(&req).unwrap();

    let err = expect_err(svc.get_role(&make_request("GetRole", vec![("RoleName", "test-role")])));
    assert!(err.to_string().contains("NoSuchEntity"));
}

#[test]
fn list_roles_basic() {
    let svc = make_service();
    let trust = r#"{"Version":"2012-10-17"}"#;
    svc.create_role(&make_request(
        "CreateRole",
        vec![("RoleName", "r1"), ("AssumeRolePolicyDocument", trust)],
    ))
    .unwrap();
    svc.create_role(&make_request(
        "CreateRole",
        vec![("RoleName", "r2"), ("AssumeRolePolicyDocument", trust)],
    ))
    .unwrap();

    let resp = svc.list_roles(&make_request("ListRoles", vec![])).unwrap();
    let body = String::from_utf8_lossy(resp.body.expect_bytes()).to_string();
    assert!(body.contains("<RoleName>r1</RoleName>"));
    assert!(body.contains("<RoleName>r2</RoleName>"));
}

#[test]
fn role_inline_policy_lifecycle() {
    let svc = make_service();
    let trust = r#"{"Version":"2012-10-17"}"#;
    svc.create_role(&make_request(
        "CreateRole",
        vec![("RoleName", "ip-role"), ("AssumeRolePolicyDocument", trust)],
    ))
    .unwrap();

    let policy = r#"{"Version":"2012-10-17","Statement":[{"Effect":"Allow","Action":"s3:*","Resource":"*"}]}"#;
    svc.put_role_policy(&make_request(
        "PutRolePolicy",
        vec![
            ("RoleName", "ip-role"),
            ("PolicyName", "s3-access"),
            ("PolicyDocument", policy),
        ],
    ))
    .unwrap();

    let resp = svc
        .get_role_policy(&make_request(
            "GetRolePolicy",
            vec![("RoleName", "ip-role"), ("PolicyName", "s3-access")],
        ))
        .unwrap();
    let body = String::from_utf8_lossy(resp.body.expect_bytes()).to_string();
    assert!(body.contains("<PolicyName>s3-access</PolicyName>"));

    let resp = svc
        .list_role_policies(&make_request(
            "ListRolePolicies",
            vec![("RoleName", "ip-role")],
        ))
        .unwrap();
    let body = String::from_utf8_lossy(resp.body.expect_bytes()).to_string();
    assert!(body.contains("s3-access"));

    svc.delete_role_policy(&make_request(
        "DeleteRolePolicy",
        vec![("RoleName", "ip-role"), ("PolicyName", "s3-access")],
    ))
    .unwrap();
}

// ── Policy CRUD ──

#[test]
fn create_get_delete_policy() {
    let svc = make_service();

    let doc = r#"{"Version":"2012-10-17","Statement":[{"Effect":"Allow","Action":"s3:GetObject","Resource":"*"}]}"#;
    let req = make_request(
        "CreatePolicy",
        vec![("PolicyName", "test-pol"), ("PolicyDocument", doc)],
    );
    let resp = svc.create_policy(&req).unwrap();
    let body = String::from_utf8_lossy(resp.body.expect_bytes()).to_string();
    assert!(body.contains("<PolicyName>test-pol</PolicyName>"));
    let arn = extract_xml_tag(&body, "Arn");

    // Get
    let resp = svc
        .get_policy(&make_request("GetPolicy", vec![("PolicyArn", arn)]))
        .unwrap();
    let body = String::from_utf8_lossy(resp.body.expect_bytes()).to_string();
    assert!(body.contains("<PolicyName>test-pol</PolicyName>"));

    // Delete
    svc.delete_policy(&make_request("DeletePolicy", vec![("PolicyArn", arn)]))
        .unwrap();

    let err = expect_err(svc.get_policy(&make_request("GetPolicy", vec![("PolicyArn", arn)])));
    assert!(err.to_string().contains("NoSuchEntity"));
}

#[test]
fn list_policies_basic() {
    let svc = make_service();
    let doc = r#"{"Version":"2012-10-17","Statement":[{"Effect":"Allow","Action":"s3:GetObject","Resource":"*"}]}"#;
    svc.create_policy(&make_request(
        "CreatePolicy",
        vec![("PolicyName", "p1"), ("PolicyDocument", doc)],
    ))
    .unwrap();

    let resp = svc
        .list_policies(&make_request("ListPolicies", vec![("Scope", "Local")]))
        .unwrap();
    let body = String::from_utf8_lossy(resp.body.expect_bytes()).to_string();
    assert!(body.contains("<PolicyName>p1</PolicyName>"));
}

// ── Group CRUD ──

#[test]
fn create_get_delete_group() {
    let svc = make_service();

    svc.create_group(&make_request("CreateGroup", vec![("GroupName", "devs")]))
        .unwrap();

    let resp = svc
        .get_group(&make_request("GetGroup", vec![("GroupName", "devs")]))
        .unwrap();
    let body = String::from_utf8_lossy(resp.body.expect_bytes()).to_string();
    assert!(body.contains("<GroupName>devs</GroupName>"));

    svc.delete_group(&make_request("DeleteGroup", vec![("GroupName", "devs")]))
        .unwrap();

    let err = expect_err(svc.get_group(&make_request("GetGroup", vec![("GroupName", "devs")])));
    assert!(err.to_string().contains("NoSuchEntity"));
}

#[test]
fn group_user_membership() {
    let svc = make_service();
    svc.create_group(&make_request("CreateGroup", vec![("GroupName", "team")]))
        .unwrap();
    svc.create_user(&make_request("CreateUser", vec![("UserName", "member")]))
        .unwrap();

    svc.add_user_to_group(&make_request(
        "AddUserToGroup",
        vec![("GroupName", "team"), ("UserName", "member")],
    ))
    .unwrap();

    let resp = svc
        .get_group(&make_request("GetGroup", vec![("GroupName", "team")]))
        .unwrap();
    let body = String::from_utf8_lossy(resp.body.expect_bytes()).to_string();
    assert!(body.contains("<UserName>member</UserName>"));

    svc.remove_user_from_group(&make_request(
        "RemoveUserFromGroup",
        vec![("GroupName", "team"), ("UserName", "member")],
    ))
    .unwrap();
}

#[test]
fn list_groups_basic() {
    let svc = make_service();
    svc.create_group(&make_request("CreateGroup", vec![("GroupName", "g1")]))
        .unwrap();
    svc.create_group(&make_request("CreateGroup", vec![("GroupName", "g2")]))
        .unwrap();

    let resp = svc
        .list_groups(&make_request("ListGroups", vec![]))
        .unwrap();
    let body = String::from_utf8_lossy(resp.body.expect_bytes()).to_string();
    assert!(body.contains("g1"));
    assert!(body.contains("g2"));
}

// ── Instance Profile CRUD ──

#[test]
fn instance_profile_lifecycle() {
    let svc = make_service();

    svc.create_instance_profile(&make_request(
        "CreateInstanceProfile",
        vec![("InstanceProfileName", "web-ip")],
    ))
    .unwrap();

    let resp = svc
        .get_instance_profile(&make_request(
            "GetInstanceProfile",
            vec![("InstanceProfileName", "web-ip")],
        ))
        .unwrap();
    let body = String::from_utf8_lossy(resp.body.expect_bytes()).to_string();
    assert!(body.contains("<InstanceProfileName>web-ip</InstanceProfileName>"));

    // Add role
    let trust = r#"{"Version":"2012-10-17"}"#;
    svc.create_role(&make_request(
        "CreateRole",
        vec![
            ("RoleName", "ip-role2"),
            ("AssumeRolePolicyDocument", trust),
        ],
    ))
    .unwrap();
    svc.add_role_to_instance_profile(&make_request(
        "AddRoleToInstanceProfile",
        vec![("InstanceProfileName", "web-ip"), ("RoleName", "ip-role2")],
    ))
    .unwrap();

    // List
    let resp = svc
        .list_instance_profiles(&make_request("ListInstanceProfiles", vec![]))
        .unwrap();
    let body = String::from_utf8_lossy(resp.body.expect_bytes()).to_string();
    assert!(body.contains("web-ip"));

    // Remove role and delete
    svc.remove_role_from_instance_profile(&make_request(
        "RemoveRoleFromInstanceProfile",
        vec![("InstanceProfileName", "web-ip"), ("RoleName", "ip-role2")],
    ))
    .unwrap();
    svc.delete_instance_profile(&make_request(
        "DeleteInstanceProfile",
        vec![("InstanceProfileName", "web-ip")],
    ))
    .unwrap();
}

// ── OIDC Provider CRUD ──

#[test]
fn oidc_provider_lifecycle() {
    let svc = make_service();

    let req = make_request(
        "CreateOpenIDConnectProvider",
        vec![
            ("Url", "https://oidc.example.com"),
            (
                "ThumbprintList.member.1",
                "aabbccddeeff00112233aabbccddeeff00112233",
            ),
            ("ClientIDList.member.1", "my-client"),
        ],
    );
    let resp = svc.create_oidc_provider(&req).unwrap();
    let body = String::from_utf8_lossy(resp.body.expect_bytes()).to_string();
    let arn = extract_xml_tag(&body, "OpenIDConnectProviderArn");

    let resp = svc
        .get_oidc_provider(&make_request(
            "GetOpenIDConnectProvider",
            vec![("OpenIDConnectProviderArn", arn)],
        ))
        .unwrap();
    let body = String::from_utf8_lossy(resp.body.expect_bytes()).to_string();
    assert!(body.contains("oidc.example.com"));
    assert!(body.contains("my-client"));

    let resp = svc
        .list_oidc_providers(&make_request("ListOpenIDConnectProviders", vec![]))
        .unwrap();
    let body = String::from_utf8_lossy(resp.body.expect_bytes()).to_string();
    assert!(body.contains(arn));

    svc.delete_oidc_provider(&make_request(
        "DeleteOpenIDConnectProvider",
        vec![("OpenIDConnectProviderArn", arn)],
    ))
    .unwrap();
}

// ── Access Key lifecycle ──

#[test]
fn access_key_create_update_delete() {
    let svc = make_service();
    svc.create_user(&make_request("CreateUser", vec![("UserName", "akuser")]))
        .unwrap();

    let resp = svc
        .create_access_key(&make_request(
            "CreateAccessKey",
            vec![("UserName", "akuser")],
        ))
        .unwrap();
    let body = String::from_utf8_lossy(resp.body.expect_bytes()).to_string();
    let key_id = extract_xml_tag(&body, "AccessKeyId");
    assert!(body.contains("<Status>Active</Status>"));

    // Update to Inactive
    svc.update_access_key(&make_request(
        "UpdateAccessKey",
        vec![
            ("UserName", "akuser"),
            ("AccessKeyId", key_id),
            ("Status", "Inactive"),
        ],
    ))
    .unwrap();

    // List and verify
    let resp = svc
        .list_access_keys(&make_request(
            "ListAccessKeys",
            vec![("UserName", "akuser")],
        ))
        .unwrap();
    let body = String::from_utf8_lossy(resp.body.expect_bytes()).to_string();
    assert!(body.contains("<Status>Inactive</Status>"));

    // Delete
    svc.delete_access_key(&make_request(
        "DeleteAccessKey",
        vec![("UserName", "akuser"), ("AccessKeyId", key_id)],
    ))
    .unwrap();

    let resp = svc
        .list_access_keys(&make_request(
            "ListAccessKeys",
            vec![("UserName", "akuser")],
        ))
        .unwrap();
    let body = String::from_utf8_lossy(resp.body.expect_bytes()).to_string();
    assert!(!body.contains("<AccessKeyId>"));
}

// ── Tag/Untag User ──

#[test]
fn tag_untag_list_user_tags() {
    let svc = make_service();
    svc.create_user(&make_request("CreateUser", vec![("UserName", "tuser")]))
        .unwrap();

    svc.tag_user(&make_request(
        "TagUser",
        vec![
            ("UserName", "tuser"),
            ("Tags.member.1.Key", "dept"),
            ("Tags.member.1.Value", "eng"),
        ],
    ))
    .unwrap();

    let resp = svc
        .list_user_tags(&make_request("ListUserTags", vec![("UserName", "tuser")]))
        .unwrap();
    let body = String::from_utf8_lossy(resp.body.expect_bytes()).to_string();
    assert!(body.contains("<Key>dept</Key>"));
    assert!(body.contains("<Value>eng</Value>"));

    svc.untag_user(&make_request(
        "UntagUser",
        vec![("UserName", "tuser"), ("TagKeys.member.1", "dept")],
    ))
    .unwrap();

    let resp = svc
        .list_user_tags(&make_request("ListUserTags", vec![("UserName", "tuser")]))
        .unwrap();
    let body = String::from_utf8_lossy(resp.body.expect_bytes()).to_string();
    assert!(!body.contains("<Key>dept</Key>"));
}

// ── Attach/Detach role policy ──

#[test]
fn attach_detach_list_role_policies_managed() {
    let svc = make_service();
    let trust = r#"{"Version":"2012-10-17","Statement":[{"Effect":"Allow","Principal":{"Service":"ec2.amazonaws.com"},"Action":"sts:AssumeRole"}]}"#;
    svc.create_role(&make_request(
        "CreateRole",
        vec![
            ("RoleName", "pol-role"),
            ("AssumeRolePolicyDocument", trust),
        ],
    ))
    .unwrap();

    // Create a local policy to attach
    let doc = r#"{"Version":"2012-10-17","Statement":[{"Effect":"Allow","Action":"s3:*","Resource":"*"}]}"#;
    let resp = svc
        .create_policy(&make_request(
            "CreatePolicy",
            vec![("PolicyName", "role-pol"), ("PolicyDocument", doc)],
        ))
        .unwrap();
    let body = String::from_utf8_lossy(resp.body.expect_bytes()).to_string();
    let policy_arn = extract_xml_tag(&body, "Arn").to_string();

    svc.attach_role_policy(&make_request(
        "AttachRolePolicy",
        vec![("RoleName", "pol-role"), ("PolicyArn", &policy_arn)],
    ))
    .unwrap();

    let resp = svc
        .list_attached_role_policies(&make_request(
            "ListAttachedRolePolicies",
            vec![("RoleName", "pol-role")],
        ))
        .unwrap();
    let body = String::from_utf8_lossy(resp.body.expect_bytes()).to_string();
    assert!(body.contains("role-pol"));

    svc.detach_role_policy(&make_request(
        "DetachRolePolicy",
        vec![("RoleName", "pol-role"), ("PolicyArn", &policy_arn)],
    ))
    .unwrap();
}

#[test]
fn list_attached_role_policies_includes_aws_managed() {
    let svc = make_service();
    let trust = r#"{"Version":"2012-10-17","Statement":[{"Effect":"Allow","Principal":{"Service":"ecs-tasks.amazonaws.com"},"Action":"sts:AssumeRole"}]}"#;
    svc.create_role(&make_request(
        "CreateRole",
        vec![
            ("RoleName", "task-exec"),
            ("AssumeRolePolicyDocument", trust),
        ],
    ))
    .unwrap();

    let managed_arn = "arn:aws:iam::aws:policy/service-role/AmazonECSTaskExecutionRolePolicy";
    svc.attach_role_policy(&make_request(
        "AttachRolePolicy",
        vec![("RoleName", "task-exec"), ("PolicyArn", managed_arn)],
    ))
    .unwrap();

    let resp = svc
        .list_attached_role_policies(&make_request(
            "ListAttachedRolePolicies",
            vec![("RoleName", "task-exec")],
        ))
        .unwrap();
    let body = String::from_utf8_lossy(resp.body.expect_bytes()).to_string();
    assert!(
        body.contains(managed_arn),
        "managed policy ARN missing from response: {body}"
    );
    assert!(
        body.contains("<PolicyName>AmazonECSTaskExecutionRolePolicy</PolicyName>"),
        "managed policy name missing from response: {body}"
    );
}

#[test]
fn list_attached_user_policies_includes_aws_managed() {
    let svc = make_service();
    svc.create_user(&make_request(
        "CreateUser",
        vec![("UserName", "managed-user")],
    ))
    .unwrap();

    let managed_arn = "arn:aws:iam::aws:policy/AdministratorAccess";
    svc.attach_user_policy(&make_request(
        "AttachUserPolicy",
        vec![("UserName", "managed-user"), ("PolicyArn", managed_arn)],
    ))
    .unwrap();

    let resp = svc
        .list_attached_user_policies(&make_request(
            "ListAttachedUserPolicies",
            vec![("UserName", "managed-user")],
        ))
        .unwrap();
    let body = String::from_utf8_lossy(resp.body.expect_bytes()).to_string();
    assert!(
        body.contains(managed_arn),
        "managed policy ARN missing from response: {body}"
    );
    assert!(
        body.contains("<PolicyName>AdministratorAccess</PolicyName>"),
        "managed policy name missing from response: {body}"
    );
}

#[test]
fn list_attached_group_policies_includes_aws_managed() {
    let svc = make_service();
    svc.create_group(&make_request(
        "CreateGroup",
        vec![("GroupName", "managed-grp")],
    ))
    .unwrap();

    let managed_arn = "arn:aws:iam::aws:policy/ReadOnlyAccess";
    svc.attach_group_policy(&make_request(
        "AttachGroupPolicy",
        vec![("GroupName", "managed-grp"), ("PolicyArn", managed_arn)],
    ))
    .unwrap();

    let resp = svc
        .list_attached_group_policies(&make_request(
            "ListAttachedGroupPolicies",
            vec![("GroupName", "managed-grp")],
        ))
        .unwrap();
    let body = String::from_utf8_lossy(resp.body.expect_bytes()).to_string();
    assert!(
        body.contains(managed_arn),
        "managed policy ARN missing from response: {body}"
    );
    assert!(
        body.contains("<PolicyName>ReadOnlyAccess</PolicyName>"),
        "managed policy name missing from response: {body}"
    );
}

// ── users.rs additional coverage ──

#[test]
fn create_user_missing_name_errors() {
    let svc = make_service();
    let req = make_request("CreateUser", vec![]);
    assert!(svc.create_user(&req).is_err());
}

#[test]
fn create_user_duplicate_errors() {
    let svc = make_service();
    let req = make_request("CreateUser", vec![("UserName", "dup")]);
    svc.create_user(&req).unwrap();
    assert!(svc.create_user(&req).is_err());
}

#[test]
fn get_user_unknown_errors() {
    let svc = make_service();
    let req = make_request("GetUser", vec![("UserName", "ghost")]);
    assert!(svc.get_user(&req).is_err());
}

#[test]
fn delete_user_unknown_errors() {
    let svc = make_service();
    let req = make_request("DeleteUser", vec![("UserName", "ghost")]);
    assert!(svc.delete_user(&req).is_err());
}

#[test]
fn update_user_unknown_errors() {
    let svc = make_service();
    let req = make_request(
        "UpdateUser",
        vec![("UserName", "ghost"), ("NewUserName", "new")],
    );
    assert!(svc.update_user(&req).is_err());
}

#[test]
fn tag_user_unknown_errors() {
    let svc = make_service();
    let req = make_request(
        "TagUser",
        vec![
            ("UserName", "ghost"),
            ("Tags.member.1.Key", "k"),
            ("Tags.member.1.Value", "v"),
        ],
    );
    assert!(svc.tag_user(&req).is_err());
}

#[test]
fn list_user_tags_unknown_errors() {
    let svc = make_service();
    let req = make_request("ListUserTags", vec![("UserName", "ghost")]);
    assert!(svc.list_user_tags(&req).is_err());
}

#[test]
fn create_access_key_unknown_user_errors() {
    let svc = make_service();
    let req = make_request("CreateAccessKey", vec![("UserName", "ghost")]);
    assert!(svc.create_access_key(&req).is_err());
}

#[test]
fn delete_access_key_unknown_user_errors() {
    let svc = make_service();
    let req = make_request(
        "DeleteAccessKey",
        vec![("UserName", "ghost"), ("AccessKeyId", "AKIAEXAMPLE")],
    );
    assert!(svc.delete_access_key(&req).is_err());
}

#[test]
fn create_login_profile_unknown_user_errors() {
    let svc = make_service();
    let req = make_request(
        "CreateLoginProfile",
        vec![("UserName", "ghost"), ("Password", "Pass123!")],
    );
    assert!(svc.create_login_profile(&req).is_err());
}

#[test]
fn get_login_profile_not_found() {
    let svc = make_service();
    svc.create_user(&make_request("CreateUser", vec![("UserName", "lp")]))
        .unwrap();
    let req = make_request("GetLoginProfile", vec![("UserName", "lp")]);
    assert!(svc.get_login_profile(&req).is_err());
}

#[test]
fn delete_login_profile_not_found() {
    let svc = make_service();
    let req = make_request("DeleteLoginProfile", vec![("UserName", "ghost")]);
    assert!(svc.delete_login_profile(&req).is_err());
}

#[test]
fn upload_signing_cert_unknown_user_errors() {
    let svc = make_service();
    let req = make_request(
        "UploadSigningCertificate",
        vec![
            ("UserName", "ghost"),
            ("CertificateBody", "-----BEGIN CERT-----"),
        ],
    );
    assert!(svc.upload_signing_certificate(&req).is_err());
}

#[test]
fn upload_ssh_public_key_unknown_user_errors() {
    let svc = make_service();
    let req = make_request(
        "UploadSSHPublicKey",
        vec![("UserName", "ghost"), ("SSHPublicKeyBody", "ssh-rsa AAA")],
    );
    assert!(svc.upload_ssh_public_key(&req).is_err());
}

#[test]
fn get_ssh_public_key_not_found() {
    let svc = make_service();
    let req = make_request(
        "GetSSHPublicKey",
        vec![
            ("UserName", "ghost"),
            ("SSHPublicKeyId", "ASDF"),
            ("Encoding", "SSH"),
        ],
    );
    assert!(svc.get_ssh_public_key(&req).is_err());
}

#[test]
fn delete_ssh_public_key_not_found() {
    let svc = make_service();
    let req = make_request(
        "DeleteSSHPublicKey",
        vec![("UserName", "ghost"), ("SSHPublicKeyId", "ASDF")],
    );
    assert!(svc.delete_ssh_public_key(&req).is_err());
}

#[test]
fn attach_user_policy_unknown_user_errors() {
    let svc = make_service();
    let req = make_request(
        "AttachUserPolicy",
        vec![
            ("UserName", "ghost"),
            ("PolicyArn", "arn:aws:iam::aws:policy/ReadOnlyAccess"),
        ],
    );
    assert!(svc.attach_user_policy(&req).is_err());
}

#[test]
fn detach_user_policy_unknown_user_errors() {
    let svc = make_service();
    let req = make_request(
        "DetachUserPolicy",
        vec![
            ("UserName", "ghost"),
            ("PolicyArn", "arn:aws:iam::aws:policy/ReadOnlyAccess"),
        ],
    );
    assert!(svc.detach_user_policy(&req).is_err());
}

#[test]
fn list_attached_user_policies_unknown_user_errors() {
    let svc = make_service();
    let req = make_request("ListAttachedUserPolicies", vec![("UserName", "ghost")]);
    assert!(svc.list_attached_user_policies(&req).is_err());
}

// ── roles.rs additional ──

#[test]
fn create_role_missing_trust_errors() {
    let svc = make_service();
    let req = make_request("CreateRole", vec![("RoleName", "r1")]);
    assert!(svc.create_role(&req).is_err());
}

#[test]
fn create_role_duplicate_errors() {
    let svc = make_service();
    let trust = r#"{"Version":"2012-10-17","Statement":[{"Effect":"Allow","Principal":{"Service":"ec2.amazonaws.com"},"Action":"sts:AssumeRole"}]}"#;
    let req = make_request(
        "CreateRole",
        vec![("RoleName", "rd"), ("AssumeRolePolicyDocument", trust)],
    );
    svc.create_role(&req).unwrap();
    assert!(svc.create_role(&req).is_err());
}

#[test]
fn get_role_unknown_errors() {
    let svc = make_service();
    let req = make_request("GetRole", vec![("RoleName", "ghost")]);
    assert!(svc.get_role(&req).is_err());
}

#[test]
fn delete_role_unknown_errors() {
    let svc = make_service();
    let req = make_request("DeleteRole", vec![("RoleName", "ghost")]);
    assert!(svc.delete_role(&req).is_err());
}

#[test]
fn update_assume_role_policy_unknown_role_errors() {
    let svc = make_service();
    let req = make_request(
        "UpdateAssumeRolePolicy",
        vec![("RoleName", "ghost"), ("PolicyDocument", "{}")],
    );
    assert!(svc.update_assume_role_policy(&req).is_err());
}

#[test]
fn tag_role_unknown_errors() {
    let svc = make_service();
    let req = make_request(
        "TagRole",
        vec![
            ("RoleName", "ghost"),
            ("Tags.member.1.Key", "k"),
            ("Tags.member.1.Value", "v"),
        ],
    );
    assert!(svc.tag_role(&req).is_err());
}

// ── groups.rs additional ──

#[test]
fn create_group_duplicate_errors() {
    let svc = make_service();
    let req = make_request("CreateGroup", vec![("GroupName", "g1")]);
    svc.create_group(&req).unwrap();
    assert!(svc.create_group(&req).is_err());
}

#[test]
fn get_group_unknown_errors() {
    let svc = make_service();
    let req = make_request("GetGroup", vec![("GroupName", "ghost")]);
    assert!(svc.get_group(&req).is_err());
}

#[test]
fn delete_group_unknown_errors() {
    let svc = make_service();
    let req = make_request("DeleteGroup", vec![("GroupName", "ghost")]);
    assert!(svc.delete_group(&req).is_err());
}

#[test]
fn add_user_to_unknown_group_errors() {
    let svc = make_service();
    svc.create_user(&make_request("CreateUser", vec![("UserName", "u1")]))
        .unwrap();
    let req = make_request(
        "AddUserToGroup",
        vec![("GroupName", "ghost"), ("UserName", "u1")],
    );
    assert!(svc.add_user_to_group(&req).is_err());
}

#[test]
fn add_unknown_user_to_group_errors() {
    let svc = make_service();
    svc.create_group(&make_request("CreateGroup", vec![("GroupName", "ug")]))
        .unwrap();
    let req = make_request(
        "AddUserToGroup",
        vec![("GroupName", "ug"), ("UserName", "ghost")],
    );
    assert!(svc.add_user_to_group(&req).is_err());
}

// ── policies.rs additional ──

#[test]
fn create_policy_duplicate_errors() {
    let svc = make_service();
    let doc =
        r#"{"Version":"2012-10-17","Statement":[{"Effect":"Allow","Action":"*","Resource":"*"}]}"#;
    let req = make_request(
        "CreatePolicy",
        vec![("PolicyName", "dp"), ("PolicyDocument", doc)],
    );
    svc.create_policy(&req).unwrap();
    assert!(svc.create_policy(&req).is_err());
}

#[test]
fn delete_policy_unknown_errors() {
    let svc = make_service();
    let req = make_request(
        "DeletePolicy",
        vec![("PolicyArn", "arn:aws:iam::123456789012:policy/ghost")],
    );
    assert!(svc.delete_policy(&req).is_err());
}

#[test]
fn get_policy_unknown_errors() {
    let svc = make_service();
    let req = make_request(
        "GetPolicy",
        vec![("PolicyArn", "arn:aws:iam::123456789012:policy/ghost")],
    );
    assert!(svc.get_policy(&req).is_err());
}

// ── instance profiles ──

#[test]
fn create_instance_profile_duplicate() {
    let svc = make_service();
    let req = make_request(
        "CreateInstanceProfile",
        vec![("InstanceProfileName", "ip1")],
    );
    svc.create_instance_profile(&req).unwrap();
    assert!(svc.create_instance_profile(&req).is_err());
}

#[test]
fn delete_instance_profile_unknown_errors() {
    let svc = make_service();
    let req = make_request(
        "DeleteInstanceProfile",
        vec![("InstanceProfileName", "ghost")],
    );
    assert!(svc.delete_instance_profile(&req).is_err());
}

// ── instance_profiles additional ──

#[test]
fn get_instance_profile_unknown_errors() {
    let svc = make_service();
    let req = make_request("GetInstanceProfile", vec![("InstanceProfileName", "ghost")]);
    assert!(svc.get_instance_profile(&req).is_err());
}

#[test]
fn list_instance_profiles_empty_returns_ok() {
    let svc = make_service();
    let req = make_request("ListInstanceProfiles", vec![]);
    let resp = svc.list_instance_profiles(&req).unwrap();
    assert_eq!(resp.status, http::StatusCode::OK);
}

#[test]
fn add_role_to_instance_profile_unknown_profile_errors() {
    let svc = make_service();
    let req = make_request(
        "AddRoleToInstanceProfile",
        vec![("InstanceProfileName", "ghost"), ("RoleName", "r")],
    );
    assert!(svc.add_role_to_instance_profile(&req).is_err());
}

#[test]
fn remove_role_from_instance_profile_unknown_errors() {
    let svc = make_service();
    let req = make_request(
        "RemoveRoleFromInstanceProfile",
        vec![("InstanceProfileName", "ghost"), ("RoleName", "r")],
    );
    assert!(svc.remove_role_from_instance_profile(&req).is_err());
}

#[test]
fn list_instance_profiles_for_role_unknown_errors() {
    let svc = make_service();
    let req = make_request("ListInstanceProfilesForRole", vec![("RoleName", "ghost")]);
    assert!(svc.list_instance_profiles_for_role(&req).is_err());
}

#[test]
fn list_instance_profile_tags_unknown_errors() {
    let svc = make_service();
    let req = make_request(
        "ListInstanceProfileTags",
        vec![("InstanceProfileName", "ghost")],
    );
    assert!(svc.list_instance_profile_tags(&req).is_err());
}

// ── OIDC/SAML error branches ──

#[test]
fn get_saml_provider_unknown_errors() {
    let svc = make_service();
    let req = make_request(
        "GetSAMLProvider",
        vec![(
            "SAMLProviderArn",
            "arn:aws:iam::123456789012:saml-provider/ghost",
        )],
    );
    assert!(svc.get_saml_provider(&req).is_err());
}

#[test]
fn delete_saml_provider_unknown_errors() {
    let svc = make_service();
    let req = make_request(
        "DeleteSAMLProvider",
        vec![(
            "SAMLProviderArn",
            "arn:aws:iam::123456789012:saml-provider/ghost",
        )],
    );
    assert!(svc.delete_saml_provider(&req).is_err());
}

#[test]
fn update_saml_provider_unknown_errors() {
    let svc = make_service();
    let req = make_request(
        "UpdateSAMLProvider",
        vec![
            (
                "SAMLProviderArn",
                "arn:aws:iam::123456789012:saml-provider/ghost",
            ),
            ("SAMLMetadataDocument", "<EntityDescriptor>xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx</EntityDescriptor>"),
        ],
    );
    assert!(svc.update_saml_provider(&req).is_err());
}

#[test]
fn get_oidc_provider_unknown_errors() {
    let svc = make_service();
    let req = make_request(
        "GetOpenIDConnectProvider",
        vec![(
            "OpenIDConnectProviderArn",
            "arn:aws:iam::123456789012:oidc-provider/ghost",
        )],
    );
    assert!(svc.get_oidc_provider(&req).is_err());
}

// ── policies: version operations ──

#[test]
fn create_policy_version_unknown_errors() {
    let svc = make_service();
    let doc =
        r#"{"Version":"2012-10-17","Statement":[{"Effect":"Allow","Action":"*","Resource":"*"}]}"#;
    let req = make_request(
        "CreatePolicyVersion",
        vec![
            ("PolicyArn", "arn:aws:iam::123456789012:policy/ghost"),
            ("PolicyDocument", doc),
        ],
    );
    assert!(svc.create_policy_version(&req).is_err());
}

#[test]
fn list_policy_versions_unknown_errors() {
    let svc = make_service();
    let req = make_request(
        "ListPolicyVersions",
        vec![("PolicyArn", "arn:aws:iam::123456789012:policy/ghost")],
    );
    assert!(svc.list_policy_versions(&req).is_err());
}

#[test]
fn get_policy_version_unknown_errors() {
    let svc = make_service();
    let req = make_request(
        "GetPolicyVersion",
        vec![
            ("PolicyArn", "arn:aws:iam::123456789012:policy/ghost"),
            ("VersionId", "v1"),
        ],
    );
    assert!(svc.get_policy_version(&req).is_err());
}

#[test]
fn delete_policy_version_unknown_errors() {
    let svc = make_service();
    let req = make_request(
        "DeletePolicyVersion",
        vec![
            ("PolicyArn", "arn:aws:iam::123456789012:policy/ghost"),
            ("VersionId", "v1"),
        ],
    );
    assert!(svc.delete_policy_version(&req).is_err());
}

// ── roles: put/get/delete_role_policy inline ──

#[test]
fn put_role_policy_unknown_role_errors() {
    let svc = make_service();
    let req = make_request(
        "PutRolePolicy",
        vec![
            ("RoleName", "ghost"),
            ("PolicyName", "p1"),
            ("PolicyDocument", "{}"),
        ],
    );
    assert!(svc.put_role_policy(&req).is_err());
}

#[test]
fn get_role_policy_unknown_errors() {
    let svc = make_service();
    let req = make_request(
        "GetRolePolicy",
        vec![("RoleName", "ghost"), ("PolicyName", "p1")],
    );
    assert!(svc.get_role_policy(&req).is_err());
}

#[test]
fn delete_role_policy_unknown_errors() {
    let svc = make_service();
    let req = make_request(
        "DeleteRolePolicy",
        vec![("RoleName", "ghost"), ("PolicyName", "p1")],
    );
    assert!(svc.delete_role_policy(&req).is_err());
}

#[test]
fn list_role_policies_unknown_errors() {
    let svc = make_service();
    let req = make_request("ListRolePolicies", vec![("RoleName", "ghost")]);
    assert!(svc.list_role_policies(&req).is_err());
}

// ── user inline policies ──

#[test]
fn put_user_policy_unknown_errors() {
    let svc = make_service();
    let req = make_request(
        "PutUserPolicy",
        vec![
            ("UserName", "ghost"),
            ("PolicyName", "p"),
            ("PolicyDocument", "{}"),
        ],
    );
    assert!(svc.put_user_policy(&req).is_err());
}

#[test]
fn list_user_policies_unknown_errors() {
    let svc = make_service();
    let req = make_request("ListUserPolicies", vec![("UserName", "ghost")]);
    assert!(svc.list_user_policies(&req).is_err());
}

#[test]
fn get_user_policy_unknown_errors() {
    let svc = make_service();
    let req = make_request(
        "GetUserPolicy",
        vec![("UserName", "ghost"), ("PolicyName", "p")],
    );
    assert!(svc.get_user_policy(&req).is_err());
}

#[test]
fn delete_user_policy_unknown_errors() {
    let svc = make_service();
    let req = make_request(
        "DeleteUserPolicy",
        vec![("UserName", "ghost"), ("PolicyName", "p")],
    );
    assert!(svc.delete_user_policy(&req).is_err());
}

#[test]
fn put_group_policy_unknown_group_errors() {
    let svc = make_service();
    let req = make_request(
        "PutGroupPolicy",
        vec![
            ("GroupName", "ghost"),
            ("PolicyName", "p"),
            ("PolicyDocument", "{}"),
        ],
    );
    assert!(svc.put_group_policy(&req).is_err());
}

#[test]
fn get_group_policy_unknown_errors() {
    let svc = make_service();
    let req = make_request(
        "GetGroupPolicy",
        vec![("GroupName", "ghost"), ("PolicyName", "p")],
    );
    assert!(svc.get_group_policy(&req).is_err());
}

#[test]
fn delete_group_policy_unknown_errors() {
    let svc = make_service();
    let req = make_request(
        "DeleteGroupPolicy",
        vec![("GroupName", "ghost"), ("PolicyName", "p")],
    );
    assert!(svc.delete_group_policy(&req).is_err());
}

#[test]
fn list_group_policies_unknown_errors() {
    let svc = make_service();
    let req = make_request("ListGroupPolicies", vec![("GroupName", "ghost")]);
    assert!(svc.list_group_policies(&req).is_err());
}

#[test]
fn attach_group_policy_unknown_errors() {
    let svc = make_service();
    let req = make_request(
        "AttachGroupPolicy",
        vec![
            ("GroupName", "ghost"),
            ("PolicyArn", "arn:aws:iam::aws:policy/ReadOnlyAccess"),
        ],
    );
    assert!(svc.attach_group_policy(&req).is_err());
}

#[test]
fn detach_group_policy_unknown_errors() {
    let svc = make_service();
    let req = make_request(
        "DetachGroupPolicy",
        vec![
            ("GroupName", "ghost"),
            ("PolicyArn", "arn:aws:iam::aws:policy/ReadOnlyAccess"),
        ],
    );
    assert!(svc.detach_group_policy(&req).is_err());
}

#[test]
fn list_attached_group_policies_unknown_errors() {
    let svc = make_service();
    let req = make_request("ListAttachedGroupPolicies", vec![("GroupName", "ghost")]);
    assert!(svc.list_attached_group_policies(&req).is_err());
}

#[test]
fn list_groups_for_user_unknown_errors() {
    let svc = make_service();
    let req = make_request("ListGroupsForUser", vec![("UserName", "ghost")]);
    assert!(svc.list_groups_for_user(&req).is_err());
}

#[test]
fn get_account_summary_returns_ok() {
    let svc = make_service();
    let req = make_request("GetAccountSummary", vec![]);
    let resp = svc.get_account_summary(&req).unwrap();
    assert_eq!(resp.status, http::StatusCode::OK);
}

#[test]
fn list_policies_empty_ok() {
    let svc = make_service();
    let req = make_request("ListPolicies", vec![]);
    let resp = svc.list_policies(&req).unwrap();
    assert_eq!(resp.status, http::StatusCode::OK);
}

#[test]
fn list_users_empty_ok() {
    let svc = make_service();
    let req = make_request("ListUsers", vec![]);
    let resp = svc.list_users(&req).unwrap();
    assert_eq!(resp.status, http::StatusCode::OK);
}

#[test]
fn list_roles_empty_ok() {
    let svc = make_service();
    let req = make_request("ListRoles", vec![]);
    let resp = svc.list_roles(&req).unwrap();
    assert_eq!(resp.status, http::StatusCode::OK);
}

#[test]
fn list_groups_empty_ok() {
    let svc = make_service();
    let req = make_request("ListGroups", vec![]);
    let resp = svc.list_groups(&req).unwrap();
    assert_eq!(resp.status, http::StatusCode::OK);
}

#[test]
fn list_attached_role_policies_unknown_errors() {
    let svc = make_service();
    let req = make_request("ListAttachedRolePolicies", vec![("RoleName", "ghost")]);
    assert!(svc.list_attached_role_policies(&req).is_err());
}

#[test]
fn attach_role_policy_unknown_role_errors() {
    let svc = make_service();
    let req = make_request(
        "AttachRolePolicy",
        vec![
            ("RoleName", "ghost"),
            ("PolicyArn", "arn:aws:iam::aws:policy/ReadOnlyAccess"),
        ],
    );
    assert!(svc.attach_role_policy(&req).is_err());
}

#[test]
fn detach_role_policy_unknown_role_errors() {
    let svc = make_service();
    let req = make_request(
        "DetachRolePolicy",
        vec![
            ("RoleName", "ghost"),
            ("PolicyArn", "arn:aws:iam::aws:policy/ReadOnlyAccess"),
        ],
    );
    assert!(svc.detach_role_policy(&req).is_err());
}

#[test]
fn update_role_description_unknown_errors() {
    let svc = make_service();
    let req = make_request(
        "UpdateRole",
        vec![("RoleName", "ghost"), ("Description", "new")],
    );
    assert!(svc.update_role(&req).is_err());
}

// ── SAML / OIDC providers + update paths (oidc.rs) ────────────────

#[test]
fn saml_provider_full_lifecycle() {
    let svc = make_service();

    let create = svc
        .create_saml_provider(&make_request(
            "CreateSAMLProvider",
            vec![
                ("Name", "my-saml"),
                ("SAMLMetadataDocument", "<EntityDescriptor>xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx</EntityDescriptor>"),
            ],
        ))
        .unwrap();
    let body = std::str::from_utf8(create.body.expect_bytes()).unwrap();
    let arn = extract_xml_tag(body, "SAMLProviderArn").to_string();
    assert!(arn.ends_with(":saml-provider/my-saml"));

    let get = svc
        .get_saml_provider(&make_request(
            "GetSAMLProvider",
            vec![("SAMLProviderArn", &arn)],
        ))
        .unwrap();
    let get_body = std::str::from_utf8(get.body.expect_bytes()).unwrap();
    assert!(get_body.contains("&lt;EntityDescriptor&gt;"));

    let list = svc
        .list_saml_providers(&make_request("ListSAMLProviders", vec![]))
        .unwrap();
    let list_body = std::str::from_utf8(list.body.expect_bytes()).unwrap();
    assert!(list_body.contains(&arn));

    let update = svc
        .update_saml_provider(&make_request(
            "UpdateSAMLProvider",
            vec![
                ("SAMLProviderArn", &arn),
                ("SAMLMetadataDocument", "<EntityDescriptor>xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx</EntityDescriptor>"),
            ],
        ))
        .unwrap();
    let update_body = std::str::from_utf8(update.body.expect_bytes()).unwrap();
    assert!(update_body.contains(&arn));

    svc.delete_saml_provider(&make_request(
        "DeleteSAMLProvider",
        vec![("SAMLProviderArn", &arn)],
    ))
    .unwrap();
    assert!(svc
        .get_saml_provider(&make_request(
            "GetSAMLProvider",
            vec![("SAMLProviderArn", &arn)]
        ))
        .is_err());
}

#[test]
fn oidc_provider_duplicate_rejected() {
    let svc = make_service();
    let params = vec![
        ("Url", "https://example.com"),
        (
            "ThumbprintList.member.1",
            "abcdef1234567890abcdef1234567890abcdef12",
        ),
        ("ClientIDList.member.1", "client-1"),
    ];
    svc.create_oidc_provider(&make_request("CreateOpenIDConnectProvider", params.clone()))
        .unwrap();
    let err = svc
        .create_oidc_provider(&make_request("CreateOpenIDConnectProvider", params))
        .err()
        .unwrap();
    assert_eq!(err.status(), StatusCode::CONFLICT);
}

#[test]
fn oidc_add_and_remove_client_id() {
    let svc = make_service();
    let create = svc
        .create_oidc_provider(&make_request(
            "CreateOpenIDConnectProvider",
            vec![
                ("Url", "https://client-ops.example.com"),
                (
                    "ThumbprintList.member.1",
                    "abcdef1234567890abcdef1234567890abcdef12",
                ),
                ("ClientIDList.member.1", "original-client"),
            ],
        ))
        .unwrap();
    let body = std::str::from_utf8(create.body.expect_bytes()).unwrap();
    let arn = extract_xml_tag(body, "OpenIDConnectProviderArn").to_string();

    svc.add_client_id_to_oidc(&make_request(
        "AddClientIDToOpenIDConnectProvider",
        vec![
            ("OpenIDConnectProviderArn", &arn),
            ("ClientID", "new-client"),
        ],
    ))
    .unwrap();

    let get = svc
        .get_oidc_provider(&make_request(
            "GetOpenIDConnectProvider",
            vec![("OpenIDConnectProviderArn", &arn)],
        ))
        .unwrap();
    let get_body = std::str::from_utf8(get.body.expect_bytes()).unwrap();
    assert!(get_body.contains("new-client"));

    svc.remove_client_id_from_oidc(&make_request(
        "RemoveClientIDFromOpenIDConnectProvider",
        vec![
            ("OpenIDConnectProviderArn", &arn),
            ("ClientID", "new-client"),
        ],
    ))
    .unwrap();
    let get2 = svc
        .get_oidc_provider(&make_request(
            "GetOpenIDConnectProvider",
            vec![("OpenIDConnectProviderArn", &arn)],
        ))
        .unwrap();
    let get2_body = std::str::from_utf8(get2.body.expect_bytes()).unwrap();
    assert!(!get2_body.contains("new-client"));
}

#[test]
fn oidc_add_client_id_unknown_arn_errors() {
    let svc = make_service();
    let err = svc
        .add_client_id_to_oidc(&make_request(
            "AddClientIDToOpenIDConnectProvider",
            vec![
                (
                    "OpenIDConnectProviderArn",
                    "arn:aws:iam::123:oidc-provider/ghost",
                ),
                ("ClientID", "c"),
            ],
        ))
        .err()
        .unwrap();
    assert_eq!(err.status(), StatusCode::NOT_FOUND);
}

#[test]
fn oidc_remove_client_id_unknown_arn_errors() {
    let svc = make_service();
    let err = svc
        .remove_client_id_from_oidc(&make_request(
            "RemoveClientIDFromOpenIDConnectProvider",
            vec![
                (
                    "OpenIDConnectProviderArn",
                    "arn:aws:iam::123:oidc-provider/ghost",
                ),
                ("ClientID", "c"),
            ],
        ))
        .err()
        .unwrap();
    assert_eq!(err.status(), StatusCode::NOT_FOUND);
}

#[test]
fn oidc_update_thumbprint_unknown_arn_errors() {
    let svc = make_service();
    let err = svc
        .update_oidc_thumbprint(&make_request(
            "UpdateOpenIDConnectProviderThumbprint",
            vec![
                (
                    "OpenIDConnectProviderArn",
                    "arn:aws:iam::123:oidc-provider/ghost",
                ),
                (
                    "ThumbprintList.member.1",
                    "abcdef1234567890abcdef1234567890abcdef12",
                ),
            ],
        ))
        .err()
        .unwrap();
    assert_eq!(err.status(), StatusCode::NOT_FOUND);
}

#[test]
fn oidc_update_thumbprint_succeeds() {
    let svc = make_service();
    let create = svc
        .create_oidc_provider(&make_request(
            "CreateOpenIDConnectProvider",
            vec![
                ("Url", "https://thumb.example.com"),
                (
                    "ThumbprintList.member.1",
                    "abcdef1234567890abcdef1234567890abcdef12",
                ),
                ("ClientIDList.member.1", "c"),
            ],
        ))
        .unwrap();
    let body = std::str::from_utf8(create.body.expect_bytes()).unwrap();
    let arn = extract_xml_tag(body, "OpenIDConnectProviderArn").to_string();

    svc.update_oidc_thumbprint(&make_request(
        "UpdateOpenIDConnectProviderThumbprint",
        vec![
            ("OpenIDConnectProviderArn", &arn),
            (
                "ThumbprintList.member.1",
                "fedcba0987654321fedcba0987654321fedcba09",
            ),
        ],
    ))
    .unwrap();

    let get = svc
        .get_oidc_provider(&make_request(
            "GetOpenIDConnectProvider",
            vec![("OpenIDConnectProviderArn", &arn)],
        ))
        .unwrap();
    let get_body = std::str::from_utf8(get.body.expect_bytes()).unwrap();
    assert!(get_body.contains("fedcba0987654321"));
}

#[test]
fn list_oidc_providers_includes_created() {
    let svc = make_service();
    svc.create_oidc_provider(&make_request(
        "CreateOpenIDConnectProvider",
        vec![
            ("Url", "https://list.example.com"),
            (
                "ThumbprintList.member.1",
                "abcdef1234567890abcdef1234567890abcdef12",
            ),
            ("ClientIDList.member.1", "cx"),
        ],
    ))
    .unwrap();

    let list = svc
        .list_oidc_providers(&make_request("ListOpenIDConnectProviders", vec![]))
        .unwrap();
    let list_body = std::str::from_utf8(list.body.expect_bytes()).unwrap();
    assert!(list_body.contains("list.example.com"));
}

// ── Tests for extras handlers (new ops added to close conformance gap) ──

#[test]
fn service_specific_credentials_lifecycle() {
    let svc = make_service();
    svc.create_user(&make_request("CreateUser", vec![("UserName", "u1")]))
        .unwrap();
    svc.create_service_specific_credential(&make_request(
        "CreateServiceSpecificCredential",
        vec![
            ("UserName", "u1"),
            ("ServiceName", "codecommit.amazonaws.com"),
        ],
    ))
    .unwrap();
    svc.list_service_specific_credentials(&make_request(
        "ListServiceSpecificCredentials",
        vec![("UserName", "u1")],
    ))
    .unwrap();
}

#[test]
fn organizations_root_creds_and_sessions() {
    let svc = make_service();
    svc.enable_organizations_root_credentials_management(&make_request(
        "EnableOrganizationsRootCredentialsManagement",
        vec![],
    ))
    .unwrap();
    svc.disable_organizations_root_credentials_management(&make_request(
        "DisableOrganizationsRootCredentialsManagement",
        vec![],
    ))
    .unwrap();
    svc.enable_organizations_root_sessions(&make_request(
        "EnableOrganizationsRootSessions",
        vec![],
    ))
    .unwrap();
    svc.disable_organizations_root_sessions(&make_request(
        "DisableOrganizationsRootSessions",
        vec![],
    ))
    .unwrap();
    svc.list_organizations_features(&make_request("ListOrganizationsFeatures", vec![]))
        .unwrap();
}

#[test]
fn service_last_accessed_jobs() {
    let svc = make_service();
    let resp = svc
        .generate_service_last_accessed_details(&make_request(
            "GenerateServiceLastAccessedDetails",
            vec![("Arn", "arn:aws:iam::123456789012:user/u1")],
        ))
        .unwrap();
    let body = String::from_utf8(resp.body.expect_bytes().to_vec()).unwrap();
    let job_id = extract_xml_tag(&body, "JobId");
    svc.get_service_last_accessed_details(&make_request(
        "GetServiceLastAccessedDetails",
        vec![("JobId", job_id)],
    ))
    .unwrap();
    svc.get_service_last_accessed_details_with_entities(&make_request(
        "GetServiceLastAccessedDetailsWithEntities",
        vec![("JobId", job_id), ("ServiceNamespace", "s3")],
    ))
    .unwrap();
}

#[test]
fn extra_tagging_surfaces() {
    let svc = make_service();
    // Pre-create entities the tag handlers expect
    svc.create_saml_provider(&make_request(
        "CreateSAMLProvider",
        vec![
            ("Name", "sp1"),
            ("SAMLMetadataDocument", "<EntityDescriptor>xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx</EntityDescriptor>"),
        ],
    ))
    .unwrap();
    svc.tag_saml_provider(&make_request(
        "TagSAMLProvider",
        vec![
            (
                "SAMLProviderArn",
                "arn:aws:iam::123456789012:saml-provider/sp1",
            ),
            ("Tags.member.1.Key", "k"),
            ("Tags.member.1.Value", "v"),
        ],
    ))
    .unwrap();
    svc.list_saml_provider_tags(&make_request(
        "ListSAMLProviderTags",
        vec![(
            "SAMLProviderArn",
            "arn:aws:iam::123456789012:saml-provider/sp1",
        )],
    ))
    .unwrap();
    svc.untag_saml_provider(&make_request(
        "UntagSAMLProvider",
        vec![
            (
                "SAMLProviderArn",
                "arn:aws:iam::123456789012:saml-provider/sp1",
            ),
            ("TagKeys.member.1", "k"),
        ],
    ))
    .unwrap();
}

#[test]
fn policy_simulation_smoke() {
    let svc = make_service();
    let policy = r#"{"Version":"2012-10-17","Statement":[{"Effect":"Allow","Action":"s3:GetObject","Resource":"*"}]}"#;
    svc.simulate_custom_policy(&make_request(
        "SimulateCustomPolicy",
        vec![
            ("PolicyInputList.member.1", policy),
            ("ActionNames.member.1", "s3:GetObject"),
            ("ResourceArns.member.1", "arn:aws:s3:::b/k"),
        ],
    ))
    .unwrap();
    svc.get_context_keys_for_custom_policy(&make_request(
        "GetContextKeysForCustomPolicy",
        vec![("PolicyInputList.member.1", policy)],
    ))
    .unwrap();
}

#[test]
fn simulate_custom_policy_returns_allow_when_action_matches() {
    let svc = make_service();
    let policy = r#"{"Version":"2012-10-17","Statement":[{"Effect":"Allow","Action":"s3:GetObject","Resource":"*"}]}"#;
    let resp = svc
        .simulate_custom_policy(&make_request(
            "SimulateCustomPolicy",
            vec![
                ("PolicyInputList.member.1", policy),
                ("ActionNames.member.1", "s3:GetObject"),
                ("ResourceArns.member.1", "arn:aws:s3:::b/k"),
            ],
        ))
        .unwrap();
    let body = std::str::from_utf8(resp.body.expect_bytes()).unwrap();
    assert!(
        body.contains("<EvalDecision>allowed</EvalDecision>"),
        "expected allowed, body: {body}"
    );
    assert!(body.contains("<EvalActionName>s3:GetObject</EvalActionName>"));
    assert!(body.contains("<EvalResourceName>arn:aws:s3:::b/k</EvalResourceName>"));
}

#[test]
fn simulate_custom_policy_returns_implicit_deny_when_action_unrelated() {
    let svc = make_service();
    let policy = r#"{"Version":"2012-10-17","Statement":[{"Effect":"Allow","Action":"s3:GetObject","Resource":"*"}]}"#;
    let resp = svc
        .simulate_custom_policy(&make_request(
            "SimulateCustomPolicy",
            vec![
                ("PolicyInputList.member.1", policy),
                ("ActionNames.member.1", "ec2:RunInstances"),
            ],
        ))
        .unwrap();
    let body = std::str::from_utf8(resp.body.expect_bytes()).unwrap();
    assert!(
        body.contains("<EvalDecision>implicitDeny</EvalDecision>"),
        "expected implicitDeny, body: {body}"
    );
}

#[test]
fn simulate_custom_policy_returns_explicit_deny_for_matching_deny_statement() {
    let svc = make_service();
    let policy = r#"{"Version":"2012-10-17","Statement":[
        {"Effect":"Allow","Action":"s3:*","Resource":"*"},
        {"Effect":"Deny","Action":"s3:DeleteObject","Resource":"*"}
    ]}"#;
    let resp = svc
        .simulate_custom_policy(&make_request(
            "SimulateCustomPolicy",
            vec![
                ("PolicyInputList.member.1", policy),
                ("ActionNames.member.1", "s3:DeleteObject"),
                ("ResourceArns.member.1", "arn:aws:s3:::b/k"),
            ],
        ))
        .unwrap();
    let body = std::str::from_utf8(resp.body.expect_bytes()).unwrap();
    assert!(
        body.contains("<EvalDecision>explicitDeny</EvalDecision>"),
        "expected explicitDeny, body: {body}"
    );
}

#[test]
fn simulate_custom_policy_boundary_caps_allow() {
    let svc = make_service();
    let identity =
        r#"{"Version":"2012-10-17","Statement":[{"Effect":"Allow","Action":"*","Resource":"*"}]}"#;
    let boundary = r#"{"Version":"2012-10-17","Statement":[{"Effect":"Allow","Action":"s3:GetObject","Resource":"*"}]}"#;
    let resp = svc
        .simulate_custom_policy(&make_request(
            "SimulateCustomPolicy",
            vec![
                ("PolicyInputList.member.1", identity),
                ("PermissionsBoundaryPolicyInputList.member.1", boundary),
                ("ActionNames.member.1", "ec2:RunInstances"),
            ],
        ))
        .unwrap();
    let body = std::str::from_utf8(resp.body.expect_bytes()).unwrap();
    // Boundary doesn't allow ec2:RunInstances, so result is implicitDeny.
    assert!(
        body.contains("<EvalDecision>implicitDeny</EvalDecision>"),
        "expected implicitDeny under boundary, body: {body}"
    );
}

#[test]
fn simulate_principal_policy_uses_principals_attached_managed_policy() {
    let svc = make_service();
    svc.create_user(&make_request("CreateUser", vec![("UserName", "alice")]))
        .unwrap();
    let policy_doc = r#"{"Version":"2012-10-17","Statement":[{"Effect":"Allow","Action":"s3:GetObject","Resource":"*"}]}"#;
    svc.create_policy(&make_request(
        "CreatePolicy",
        vec![("PolicyName", "P"), ("PolicyDocument", policy_doc)],
    ))
    .unwrap();
    let policy_arn = "arn:aws:iam::123456789012:policy/P";
    svc.attach_user_policy(&make_request(
        "AttachUserPolicy",
        vec![("UserName", "alice"), ("PolicyArn", policy_arn)],
    ))
    .unwrap();
    let user_arn = "arn:aws:iam::123456789012:user/alice";
    let resp = svc
        .simulate_principal_policy(&make_request(
            "SimulatePrincipalPolicy",
            vec![
                ("PolicySourceArn", user_arn),
                ("ActionNames.member.1", "s3:GetObject"),
                ("ResourceArns.member.1", "arn:aws:s3:::b/k"),
            ],
        ))
        .unwrap();
    let body = std::str::from_utf8(resp.body.expect_bytes()).unwrap();
    assert!(
        body.contains("<EvalDecision>allowed</EvalDecision>"),
        "expected allowed via attached policy, body: {body}"
    );
}

#[test]
fn simulate_principal_policy_unknown_principal_returns_implicit_deny() {
    let svc = make_service();
    let resp = svc
        .simulate_principal_policy(&make_request(
            "SimulatePrincipalPolicy",
            vec![
                ("PolicySourceArn", "arn:aws:iam::123456789012:user/ghost"),
                ("ActionNames.member.1", "s3:GetObject"),
            ],
        ))
        .unwrap();
    let body = std::str::from_utf8(resp.body.expect_bytes()).unwrap();
    assert!(
        body.contains("<EvalDecision>implicitDeny</EvalDecision>"),
        "expected implicitDeny for unknown principal, body: {body}"
    );
}

#[test]
fn simulate_custom_policy_requires_action_names() {
    let svc = make_service();
    let policy = r#"{"Version":"2012-10-17","Statement":[]}"#;
    let err = match svc.simulate_custom_policy(&make_request(
        "SimulateCustomPolicy",
        vec![("PolicyInputList.member.1", policy)],
    )) {
        Err(e) => e,
        Ok(_) => panic!("expected InvalidInput"),
    };
    assert_eq!(err.code(), "InvalidInput");
}

#[test]
fn simulate_custom_policy_evaluates_condition_keys() {
    let svc = make_service();
    let policy = r#"{"Version":"2012-10-17","Statement":[{"Effect":"Allow","Action":"s3:PutObject","Resource":"*","Condition":{"StringEquals":{"aws:RequestTag/foo":"bar"}}}]}"#;

    // Matching tag value -> allowed.
    let resp = svc
        .simulate_custom_policy(&make_request(
            "SimulateCustomPolicy",
            vec![
                ("PolicyInputList.member.1", policy),
                ("ActionNames.member.1", "s3:PutObject"),
                ("ResourceArns.member.1", "arn:aws:s3:::b/k"),
                (
                    "ContextEntries.member.1.ContextKeyName",
                    "aws:RequestTag/foo",
                ),
                ("ContextEntries.member.1.ContextKeyValues.member.1", "bar"),
                ("ContextEntries.member.1.ContextKeyType", "string"),
            ],
        ))
        .unwrap();
    let body = std::str::from_utf8(resp.body.expect_bytes()).unwrap();
    assert!(
        body.contains("<EvalDecision>allowed</EvalDecision>"),
        "expected allowed when condition matches, body: {body}"
    );

    // Different tag value -> implicit deny (Condition false, no Allow matches).
    let resp = svc
        .simulate_custom_policy(&make_request(
            "SimulateCustomPolicy",
            vec![
                ("PolicyInputList.member.1", policy),
                ("ActionNames.member.1", "s3:PutObject"),
                ("ResourceArns.member.1", "arn:aws:s3:::b/k"),
                (
                    "ContextEntries.member.1.ContextKeyName",
                    "aws:RequestTag/foo",
                ),
                ("ContextEntries.member.1.ContextKeyValues.member.1", "baz"),
                ("ContextEntries.member.1.ContextKeyType", "string"),
            ],
        ))
        .unwrap();
    let body = std::str::from_utf8(resp.body.expect_bytes()).unwrap();
    assert!(
        body.contains("<EvalDecision>implicitDeny</EvalDecision>"),
        "expected implicitDeny when condition fails, body: {body}"
    );
}

#[test]
fn simulate_principal_policy_blocked_by_permission_boundary() {
    let svc = make_service();
    svc.create_user(&make_request("CreateUser", vec![("UserName", "bob")]))
        .unwrap();

    // Identity policy grants s3:GetObject.
    let identity_doc = r#"{"Version":"2012-10-17","Statement":[{"Effect":"Allow","Action":"s3:GetObject","Resource":"*"}]}"#;
    svc.create_policy(&make_request(
        "CreatePolicy",
        vec![
            ("PolicyName", "GrantS3Get"),
            ("PolicyDocument", identity_doc),
        ],
    ))
    .unwrap();
    let identity_arn = "arn:aws:iam::123456789012:policy/GrantS3Get";
    svc.attach_user_policy(&make_request(
        "AttachUserPolicy",
        vec![("UserName", "bob"), ("PolicyArn", identity_arn)],
    ))
    .unwrap();

    // Boundary only allows ec2 actions, so it does not allow s3:GetObject.
    let boundary_doc = r#"{"Version":"2012-10-17","Statement":[{"Effect":"Allow","Action":"ec2:Describe*","Resource":"*"}]}"#;
    svc.create_policy(&make_request(
        "CreatePolicy",
        vec![
            ("PolicyName", "BoundaryEc2Only"),
            ("PolicyDocument", boundary_doc),
        ],
    ))
    .unwrap();
    let boundary_arn = "arn:aws:iam::123456789012:policy/BoundaryEc2Only";
    svc.put_user_permissions_boundary(&make_request(
        "PutUserPermissionsBoundary",
        vec![("UserName", "bob"), ("PermissionsBoundary", boundary_arn)],
    ))
    .unwrap();

    let user_arn = "arn:aws:iam::123456789012:user/bob";
    let resp = svc
        .simulate_principal_policy(&make_request(
            "SimulatePrincipalPolicy",
            vec![
                ("PolicySourceArn", user_arn),
                ("ActionNames.member.1", "s3:GetObject"),
                ("ResourceArns.member.1", "arn:aws:s3:::b/k"),
            ],
        ))
        .unwrap();
    let body = std::str::from_utf8(resp.body.expect_bytes()).unwrap();
    assert!(
        body.contains("<EvalDecision>implicitDeny</EvalDecision>"),
        "expected implicitDeny when boundary blocks, body: {body}"
    );
}

#[test]
fn simulate_principal_policy_unions_group_policies() {
    let svc = make_service();
    svc.create_user(&make_request("CreateUser", vec![("UserName", "carol")]))
        .unwrap();
    svc.create_group(&make_request("CreateGroup", vec![("GroupName", "ops")]))
        .unwrap();
    svc.add_user_to_group(&make_request(
        "AddUserToGroup",
        vec![("UserName", "carol"), ("GroupName", "ops")],
    ))
    .unwrap();
    // Group inline policy grants s3:GetObject — the user has no
    // identity-side policies of their own.
    let inline_doc = r#"{"Version":"2012-10-17","Statement":[{"Effect":"Allow","Action":"s3:GetObject","Resource":"*"}]}"#;
    svc.put_group_policy(&make_request(
        "PutGroupPolicy",
        vec![
            ("GroupName", "ops"),
            ("PolicyName", "InlineRead"),
            ("PolicyDocument", inline_doc),
        ],
    ))
    .unwrap();

    let resp = svc
        .simulate_principal_policy(&make_request(
            "SimulatePrincipalPolicy",
            vec![
                ("PolicySourceArn", "arn:aws:iam::123456789012:user/carol"),
                ("ActionNames.member.1", "s3:GetObject"),
                ("ResourceArns.member.1", "arn:aws:s3:::b/k"),
            ],
        ))
        .unwrap();
    let body = std::str::from_utf8(resp.body.expect_bytes()).unwrap();
    assert!(
        body.contains("<EvalDecision>allowed</EvalDecision>"),
        "expected allowed via group inline policy, body: {body}"
    );
}

#[test]
fn simulate_principal_policy_unions_attached_group_managed_policy() {
    let svc = make_service();
    svc.create_user(&make_request("CreateUser", vec![("UserName", "dave")]))
        .unwrap();
    svc.create_group(&make_request("CreateGroup", vec![("GroupName", "admins")]))
        .unwrap();
    svc.add_user_to_group(&make_request(
        "AddUserToGroup",
        vec![("UserName", "dave"), ("GroupName", "admins")],
    ))
    .unwrap();
    let policy_doc = r#"{"Version":"2012-10-17","Statement":[{"Effect":"Allow","Action":"s3:PutObject","Resource":"*"}]}"#;
    svc.create_policy(&make_request(
        "CreatePolicy",
        vec![("PolicyName", "GroupPut"), ("PolicyDocument", policy_doc)],
    ))
    .unwrap();
    svc.attach_group_policy(&make_request(
        "AttachGroupPolicy",
        vec![
            ("GroupName", "admins"),
            ("PolicyArn", "arn:aws:iam::123456789012:policy/GroupPut"),
        ],
    ))
    .unwrap();

    let resp = svc
        .simulate_principal_policy(&make_request(
            "SimulatePrincipalPolicy",
            vec![
                ("PolicySourceArn", "arn:aws:iam::123456789012:user/dave"),
                ("ActionNames.member.1", "s3:PutObject"),
                ("ResourceArns.member.1", "arn:aws:s3:::b/k"),
            ],
        ))
        .unwrap();
    let body = std::str::from_utf8(resp.body.expect_bytes()).unwrap();
    assert!(
        body.contains("<EvalDecision>allowed</EvalDecision>"),
        "expected allowed via attached group managed policy, body: {body}"
    );
}

#[test]
fn simulate_principal_policy_reports_missing_context_from_resolved_policies() {
    let svc = make_service();
    svc.create_user(&make_request("CreateUser", vec![("UserName", "ed")]))
        .unwrap();
    // Attach a managed policy whose Condition references a key the
    // request never supplies. The simulator must walk the *resolved*
    // policy set, not just PolicyInputList, when reporting missing
    // context values.
    let policy_doc = r#"{"Version":"2012-10-17","Statement":[{"Effect":"Allow","Action":"s3:PutObject","Resource":"*","Condition":{"StringEquals":{"aws:RequestedRegion":"us-east-1"}}}]}"#;
    svc.create_policy(&make_request(
        "CreatePolicy",
        vec![("PolicyName", "RegionGate"), ("PolicyDocument", policy_doc)],
    ))
    .unwrap();
    svc.attach_user_policy(&make_request(
        "AttachUserPolicy",
        vec![
            ("UserName", "ed"),
            ("PolicyArn", "arn:aws:iam::123456789012:policy/RegionGate"),
        ],
    ))
    .unwrap();

    let resp = svc
        .simulate_principal_policy(&make_request(
            "SimulatePrincipalPolicy",
            vec![
                ("PolicySourceArn", "arn:aws:iam::123456789012:user/ed"),
                ("ActionNames.member.1", "s3:PutObject"),
                ("ResourceArns.member.1", "arn:aws:s3:::b/k"),
            ],
        ))
        .unwrap();
    let body = std::str::from_utf8(resp.body.expect_bytes()).unwrap();
    assert!(
        body.contains(
            "<MissingContextValues><member>aws:RequestedRegion</member></MissingContextValues>"
        ),
        "expected resolved-policy condition key reported, body: {body}"
    );
}

#[test]
fn simulate_custom_policy_reports_missing_context_values() {
    let svc = make_service();
    let policy = r#"{"Version":"2012-10-17","Statement":[{"Effect":"Allow","Action":"s3:GetObject","Resource":"*","Condition":{"StringEquals":{"aws:RequestTag/team":"red"}}}]}"#;
    let resp = svc
        .simulate_custom_policy(&make_request(
            "SimulateCustomPolicy",
            vec![
                ("PolicyInputList.member.1", policy),
                ("ActionNames.member.1", "s3:GetObject"),
                ("ResourceArns.member.1", "arn:aws:s3:::b/k"),
            ],
        ))
        .unwrap();
    let body = std::str::from_utf8(resp.body.expect_bytes()).unwrap();
    // Caller didn't pass aws:RequestTag/team — should appear under
    // MissingContextValues, and the eval should implicitly deny.
    assert!(
        body.contains(
            "<MissingContextValues><member>aws:RequestTag/team</member></MissingContextValues>"
        ),
        "expected missing context key reported, body: {body}"
    );
    assert!(body.contains("<EvalDecision>implicitDeny</EvalDecision>"));
}

#[test]
fn misc_extras_smoke() {
    let svc = make_service();
    svc.create_user(&make_request("CreateUser", vec![("UserName", "u1")]))
        .unwrap();
    svc.create_login_profile(&make_request(
        "CreateLoginProfile",
        vec![("UserName", "u1"), ("Password", "p")],
    ))
    .unwrap();
    let mut req = make_request(
        "ChangePassword",
        vec![("OldPassword", "p"), ("NewPassword", "q")],
    );
    req.principal = Some(fakecloud_core::auth::Principal {
        arn: "arn:aws:iam::123456789012:user/u1".to_string(),
        user_id: "AIDAEXAMPLE".to_string(),
        account_id: "123456789012".to_string(),
        principal_type: fakecloud_core::auth::PrincipalType::User,
        source_identity: None,
        tags: None,
    });
    svc.change_password(&req).unwrap();
    svc.set_security_token_service_preferences(&make_request(
        "SetSecurityTokenServicePreferences",
        vec![("GlobalEndpointTokenVersion", "v2Token")],
    ))
    .unwrap();
}

fn change_password_principal(user_name: &str) -> fakecloud_core::auth::Principal {
    fakecloud_core::auth::Principal {
        arn: format!("arn:aws:iam::123456789012:user/{user_name}"),
        user_id: "AIDAEXAMPLE".to_string(),
        account_id: "123456789012".to_string(),
        principal_type: fakecloud_core::auth::PrincipalType::User,
        source_identity: None,
        tags: None,
    }
}

#[test]
fn change_password_rejects_wrong_old_password() {
    let svc = make_service();
    svc.create_user(&make_request("CreateUser", vec![("UserName", "u1")]))
        .unwrap();
    svc.create_login_profile(&make_request(
        "CreateLoginProfile",
        vec![("UserName", "u1"), ("Password", "correct-old")],
    ))
    .unwrap();
    let mut req = make_request(
        "ChangePassword",
        vec![("OldPassword", "wrong-old"), ("NewPassword", "fresh-new")],
    );
    req.principal = Some(change_password_principal("u1"));
    let err = match svc.change_password(&req) {
        Err(e) => e,
        Ok(_) => panic!("expected mismatch to fail"),
    };
    assert_eq!(err.status(), http::StatusCode::FORBIDDEN);
    assert!(format!("{:?}", err).contains("InvalidUserType"));
}

#[test]
fn change_password_rejects_user_without_login_profile() {
    let svc = make_service();
    svc.create_user(&make_request("CreateUser", vec![("UserName", "noprofile")]))
        .unwrap();
    let mut req = make_request(
        "ChangePassword",
        vec![("OldPassword", "a"), ("NewPassword", "b")],
    );
    req.principal = Some(change_password_principal("noprofile"));
    let err = match svc.change_password(&req) {
        Err(e) => e,
        Ok(_) => panic!("expected user-without-profile to fail"),
    };
    assert!(format!("{:?}", err).contains("InvalidUserType"));
}

#[test]
fn change_password_rejects_deleted_user() {
    let svc = make_service();
    let mut req = make_request(
        "ChangePassword",
        vec![("OldPassword", "a"), ("NewPassword", "b")],
    );
    req.principal = Some(change_password_principal("ghost"));
    let err = match svc.change_password(&req) {
        Err(e) => e,
        Ok(_) => panic!("expected deleted-user to fail"),
    };
    assert!(format!("{:?}", err).contains("InvalidUserType"));
}

#[test]
fn change_password_writes_new_password() {
    let svc = make_service();
    svc.create_user(&make_request("CreateUser", vec![("UserName", "u1")]))
        .unwrap();
    svc.create_login_profile(&make_request(
        "CreateLoginProfile",
        vec![("UserName", "u1"), ("Password", "old")],
    ))
    .unwrap();
    let mut first = make_request(
        "ChangePassword",
        vec![("OldPassword", "old"), ("NewPassword", "fresh")],
    );
    first.principal = Some(change_password_principal("u1"));
    svc.change_password(&first).unwrap();

    // Subsequent ChangePassword with the previous password fails —
    // the rotation actually wrote the new value to state.
    let mut second = make_request(
        "ChangePassword",
        vec![("OldPassword", "old"), ("NewPassword", "another")],
    );
    second.principal = Some(change_password_principal("u1"));
    assert!(svc.change_password(&second).is_err());

    // Same call with the new password succeeds.
    let mut third = make_request(
        "ChangePassword",
        vec![("OldPassword", "fresh"), ("NewPassword", "another")],
    );
    third.principal = Some(change_password_principal("u1"));
    svc.change_password(&third).unwrap();
}

#[test]
fn set_sts_prefs_rejects_invalid_version() {
    let svc = make_service();
    let err = match svc.set_security_token_service_preferences(&make_request(
        "SetSecurityTokenServicePreferences",
        vec![("GlobalEndpointTokenVersion", "v9Token")],
    )) {
        Err(e) => e,
        Ok(_) => panic!("expected invalid version to be rejected"),
    };
    assert!(format!("{:?}", err).contains("InvalidParameterValue"));
}

#[test]
fn get_sts_prefs_returns_stored_version() {
    let svc = make_service();
    svc.set_security_token_service_preferences(&make_request(
        "SetSecurityTokenServicePreferences",
        vec![("GlobalEndpointTokenVersion", "v2Token")],
    ))
    .unwrap();
    let resp = svc
        .get_security_token_service_preferences(&make_request(
            "GetSecurityTokenServicePreferences",
            vec![],
        ))
        .unwrap();
    let body = std::str::from_utf8(resp.body.expect_bytes()).unwrap();
    assert!(
        body.contains("<GlobalEndpointTokenVersion>v2Token</GlobalEndpointTokenVersion>"),
        "expected stored version in response, got: {body}"
    );
}

#[test]
fn get_sts_prefs_defaults_to_v1token_when_unset() {
    let svc = make_service();
    let resp = svc
        .get_security_token_service_preferences(&make_request(
            "GetSecurityTokenServicePreferences",
            vec![],
        ))
        .unwrap();
    let body = std::str::from_utf8(resp.body.expect_bytes()).unwrap();
    assert!(
        body.contains("<GlobalEndpointTokenVersion>v1Token</GlobalEndpointTokenVersion>"),
        "expected default v1Token, got: {body}"
    );
}

#[test]
fn resync_mfa_device_updates_enable_date() {
    use chrono::Utc;
    let svc = make_service();
    svc.create_user(&make_request("CreateUser", vec![("UserName", "u1")]))
        .unwrap();
    let serial = "arn:aws:iam::123456789012:mfa/u1";
    {
        let mut accounts = svc.state.write();
        let state = accounts.get_or_create("123456789012");
        let stale = Utc::now() - chrono::Duration::days(30);
        state.virtual_mfa_devices.insert(
            serial.to_string(),
            crate::state::VirtualMfaDevice {
                serial_number: serial.to_string(),
                user: Some("u1".to_string()),
                enable_date: Some(stale),
                base32_string_seed: String::new(),
                qr_code_png: String::new(),
                tags: Vec::new(),
            },
        );
    }

    svc.resync_mfa_device(&make_request(
        "ResyncMFADevice",
        vec![
            ("UserName", "u1"),
            ("SerialNumber", serial),
            ("AuthenticationCode1", "111111"),
            ("AuthenticationCode2", "222222"),
        ],
    ))
    .unwrap();

    let accounts = svc.state.read();
    let state = accounts.get("123456789012").unwrap();
    let dev = state.virtual_mfa_devices.get(serial).unwrap();
    let enabled = dev.enable_date.expect("enable_date should be set");
    let age = (Utc::now() - enabled).num_seconds();
    assert!(
        (0..60).contains(&age),
        "ResyncMFADevice should freshen EnableDate, got age {age}s"
    );
}

// ---- Smithy-declared wire error code tests ----
//
// Strict-mode conformance probes require every error returned by an op to be
// one of the Smithy-declared shapes. These regression tests pin the wire codes
// for input-validation failures on ops where we previously leaked the framework
// codes `MissingParameter`, `ValidationError`, or `ValidationException`.

fn assert_error_code(err: AwsServiceError, expected: &str) {
    match err {
        AwsServiceError::AwsError { code, .. } => {
            assert_eq!(code, expected, "wrong wire error code");
        }
        other => panic!("expected AwsError, got {other:?}"),
    }
}

#[test]
fn create_role_short_permissions_boundary_emits_invalid_input() {
    let svc = make_service();
    let err = svc
        .create_role(&make_request(
            "CreateRole",
            vec![
                ("RoleName", "r"),
                ("AssumeRolePolicyDocument", "{}"),
                ("PermissionsBoundary", "short"),
            ],
        ))
        .err()
        .unwrap();
    assert_error_code(err, "InvalidInput");
}

#[test]
fn put_role_permissions_boundary_short_emits_invalid_input() {
    let svc = make_service();
    let err = svc
        .put_role_permissions_boundary(&make_request(
            "PutRolePermissionsBoundary",
            vec![("RoleName", "r"), ("PermissionsBoundary", "short")],
        ))
        .err()
        .unwrap();
    assert_error_code(err, "InvalidInput");
}

#[test]
fn put_user_permissions_boundary_short_emits_invalid_input() {
    let svc = make_service();
    let err = svc
        .put_user_permissions_boundary(&make_request(
            "PutUserPermissionsBoundary",
            vec![("UserName", "u"), ("PermissionsBoundary", "short")],
        ))
        .err()
        .unwrap();
    assert_error_code(err, "InvalidInput");
}

#[test]
fn set_default_policy_version_short_arn_emits_invalid_input() {
    let svc = make_service();
    let err = svc
        .set_default_policy_version(&make_request(
            "SetDefaultPolicyVersion",
            vec![("PolicyArn", "short"), ("VersionId", "v1")],
        ))
        .err()
        .unwrap();
    assert_error_code(err, "InvalidInput");
}

#[test]
fn create_login_profile_missing_password_emits_password_policy_violation() {
    let svc = make_service();
    let err = svc
        .create_login_profile(&make_request("CreateLoginProfile", vec![("UserName", "u")]))
        .err()
        .unwrap();
    assert_error_code(err, "PasswordPolicyViolation");
}

#[test]
fn create_access_key_unresolved_user_emits_no_such_entity() {
    let svc = make_service();
    let err = svc
        .create_access_key(&make_request("CreateAccessKey", vec![]))
        .err()
        .unwrap();
    assert_error_code(err, "NoSuchEntity");
}

#[test]
fn delete_user_oversize_name_emits_no_such_entity() {
    let svc = make_service();
    let long = "a".repeat(129);
    let err = svc
        .delete_user(&make_request("DeleteUser", vec![("UserName", &long)]))
        .err()
        .unwrap();
    assert_error_code(err, "NoSuchEntity");
}

#[test]
fn tag_user_oversize_name_emits_invalid_input() {
    let svc = make_service();
    let long = "a".repeat(129);
    let err = svc
        .tag_user(&make_request("TagUser", vec![("UserName", &long)]))
        .err()
        .unwrap();
    assert_error_code(err, "InvalidInput");
}

#[test]
fn list_mfa_devices_no_username_does_not_emit_missing_parameter() {
    // ListMFADevices declares NoSuchEntity + ServiceFailure only. Sending no
    // UserName must not leak `MissingParameter` from the shared helper.
    let svc = make_service();
    let resp = svc.list_mfa_devices(&make_request("ListMFADevices", vec![]));
    match resp {
        Ok(_) => {}
        Err(AwsServiceError::AwsError { code, .. }) => {
            assert_ne!(
                code, "MissingParameter",
                "ListMFADevices leaked MissingParameter"
            );
        }
        Err(other) => panic!("unexpected error variant: {other:?}"),
    }
}

#[test]
fn create_open_id_connect_provider_missing_url_emits_invalid_input() {
    let svc = make_service();
    let err = svc
        .create_oidc_provider(&make_request("CreateOpenIDConnectProvider", vec![]))
        .err()
        .unwrap();
    assert_error_code(err, "InvalidInput");
}

#[test]
fn set_security_token_service_preferences_is_iam_enforceable() {
    // The real AWS action `SetSecurityTokenServicePreferences` is mapped and
    // IAM-enforced. Its read-back sibling `GetSecurityTokenServicePreferences`
    // is a fakecloud-only extension (absent from AWS's iam.json), so it is
    // intentionally NOT in SUPPORTED_ACTIONS — there is no AWS IAM action to
    // write a policy against, and listing it would fail the handwritten-test
    // audit (no Smithy model to checksum).
    let svc = make_service();
    let set_req = make_request("SetSecurityTokenServicePreferences", vec![]);
    assert_eq!(
        svc.iam_action_for(&set_req).map(|a| a.action),
        Some("SetSecurityTokenServicePreferences")
    );

    let get_req = make_request("GetSecurityTokenServicePreferences", vec![]);
    assert!(
        svc.iam_action_for(&get_req).is_none(),
        "Get... is a fakecloud extension, not a mapped AWS IAM action"
    );
}

#[test]
fn create_virtual_mfa_device_bad_path_emits_invalid_input() {
    let svc = make_service();
    let err = svc
        .create_virtual_mfa_device(&make_request(
            "CreateVirtualMFADevice",
            vec![("VirtualMFADeviceName", "d"), ("Path", "x")],
        ))
        .err()
        .unwrap();
    assert_error_code(err, "InvalidInput");
}

/// No snapshot store (memory mode) -> no persist hook for the CFN provisioner.
#[test]
fn snapshot_hook_is_none_without_store() {
    let svc = make_service();
    assert!(svc.snapshot_hook().is_none());
}

#[tokio::test]
async fn snapshot_hook_fires_with_store() {
    let store: Arc<dyn fakecloud_persistence::SnapshotStore> =
        Arc::new(fakecloud_persistence::MemorySnapshotStore::new());
    let svc = make_service().with_snapshot_store(store);
    let hook = svc
        .snapshot_hook()
        .expect("hook present when a store is set");
    hook().await;
}

#[test]
fn upload_server_certificate_strips_trailing_pem_newline() {
    let svc = make_service();
    // Caller sends a PEM with a trailing newline (as Terraform's file()/tls
    // provider produce); AWS stores it trimmed and echoes it back without.
    let pem = "-----BEGIN CERTIFICATE-----\nMIIB\n-----END CERTIFICATE-----\n";
    svc.upload_server_certificate(&make_request(
        "UploadServerCertificate",
        vec![
            ("ServerCertificateName", "test"),
            ("CertificateBody", pem),
            (
                "PrivateKey",
                "-----BEGIN PRIVATE KEY-----\nk\n-----END PRIVATE KEY-----\n",
            ),
        ],
    ))
    .unwrap();
    let resp = svc
        .get_server_certificate(&make_request(
            "GetServerCertificate",
            vec![("ServerCertificateName", "test")],
        ))
        .unwrap();
    let body = String::from_utf8_lossy(resp.body.expect_bytes());
    let stored = extract_xml_tag(&body, "CertificateBody");
    assert!(
        !stored.ends_with('\n') && stored.ends_with("-----END CERTIFICATE-----"),
        "certificate_body should be stored without a trailing newline, got {stored:?}"
    );
}

#[test]
fn account_summary_reflects_set_token_version() {
    let svc = make_service();
    svc.set_security_token_service_preferences(&make_request(
        "SetSecurityTokenServicePreferences",
        vec![("GlobalEndpointTokenVersion", "v2Token")],
    ))
    .unwrap();
    let resp = svc
        .get_account_summary(&make_request("GetAccountSummary", vec![]))
        .unwrap();
    let body = String::from_utf8_lossy(resp.body.expect_bytes());
    // GetAccountSummary reports the token version as a bare 1/2 integer. The
    // Terraform aws_iam_security_token_service_preferences resource reads it
    // here, so v2Token must surface as 2.
    assert!(
        body.contains("<key>GlobalEndpointTokenVersion</key><value>2</value>"),
        "account summary should report token version 2 after setting v2Token"
    );
}

#[test]
fn list_service_specific_credentials_uses_member_framing() {
    let svc = make_service();
    svc.create_user(&make_request("CreateUser", vec![("UserName", "u")]))
        .unwrap();
    svc.create_service_specific_credential(&make_request(
        "CreateServiceSpecificCredential",
        vec![
            ("UserName", "u"),
            ("ServiceName", "codecommit.amazonaws.com"),
        ],
    ))
    .unwrap();
    let resp = svc
        .list_service_specific_credentials(&make_request(
            "ListServiceSpecificCredentials",
            vec![("UserName", "u")],
        ))
        .unwrap();
    let body = String::from_utf8_lossy(resp.body.expect_bytes());
    // The IAM query protocol wraps every list element in <member>; emitting
    // the singular <ServiceSpecificCredential> tag made the AWS SDK parse zero
    // entries, so the provider's Read retried to timeout.
    assert!(
        body.contains("<member>") && body.contains("codecommit.amazonaws.com"),
        "list should wrap credentials in <member>, got {body}"
    );
}

#[test]
fn get_policy_resolves_aws_managed_from_catalog() {
    let svc = make_service();
    let arn = "arn:aws:iam::aws:policy/AdministratorAccess";
    let resp = svc
        .get_policy(&make_request("GetPolicy", vec![("PolicyArn", arn)]))
        .unwrap();
    let body = String::from_utf8_lossy(resp.body.expect_bytes()).to_string();
    assert!(body.contains(arn), "ARN missing: {body}");
    assert!(
        body.contains("<PolicyName>AdministratorAccess</PolicyName>"),
        "name missing: {body}"
    );
    assert!(
        body.contains("<DefaultVersionId>v1</DefaultVersionId>"),
        "default version missing: {body}"
    );
    assert!(
        body.contains("<IsAttachable>true</IsAttachable>"),
        "IsAttachable missing: {body}"
    );
}

#[test]
fn get_policy_unknown_managed_arn_still_not_found() {
    let svc = make_service();
    let result = svc.get_policy(&make_request(
        "GetPolicy",
        vec![("PolicyArn", "arn:aws:iam::aws:policy/TotallyMadeUpPolicy")],
    ));
    assert!(result.is_err());
    if let Err(e) = result {
        assert_eq!(e.code(), "NoSuchEntity");
    }
}

#[test]
fn get_policy_version_resolves_aws_managed_document() {
    let svc = make_service();
    let arn = "arn:aws:iam::aws:policy/AmazonS3FullAccess";
    let resp = svc
        .get_policy_version(&make_request(
            "GetPolicyVersion",
            vec![("PolicyArn", arn), ("VersionId", "v2")],
        ))
        .unwrap();
    let body = String::from_utf8_lossy(resp.body.expect_bytes()).to_string();
    // Document is URL-encoded in the response; check an encoded fragment.
    assert!(body.contains("s3%3A"), "encoded s3 action missing: {body}");
    assert!(body.contains("<IsDefaultVersion>true</IsDefaultVersion>"));
}

#[test]
fn list_policy_versions_resolves_aws_managed() {
    let svc = make_service();
    let arn = "arn:aws:iam::aws:policy/AWSLambda_FullAccess";
    let resp = svc
        .list_policy_versions(&make_request(
            "ListPolicyVersions",
            vec![("PolicyArn", arn)],
        ))
        .unwrap();
    let body = String::from_utf8_lossy(resp.body.expect_bytes()).to_string();
    assert!(
        body.contains("<VersionId>v7</VersionId>"),
        "version missing: {body}"
    );
}

#[test]
fn list_policies_scope_aws_returns_catalog() {
    // The catalog is the full AWS set (~1,500), so Scope=AWS paginates. Walk
    // the Marker and confirm well-known policies across path classes appear.
    let svc = make_service();
    let mut marker: Option<String> = None;
    let mut all = String::new();
    for _ in 0..40 {
        let mut params = vec![("Scope", "AWS"), ("MaxItems", "100")];
        if let Some(m) = marker.as_deref() {
            params.push(("Marker", m));
        }
        let resp = svc
            .list_policies(&make_request("ListPolicies", params))
            .unwrap();
        let body = String::from_utf8_lossy(resp.body.expect_bytes()).to_string();
        all.push_str(&body);
        marker = body
            .split_once("<Marker>")
            .and_then(|(_, r)| r.split_once("</Marker>"))
            .map(|(m, _)| m.to_string());
        if marker.is_none() {
            break;
        }
    }
    assert!(
        all.contains("arn:aws:iam::aws:policy/AdministratorAccess"),
        "AdministratorAccess missing from Scope=AWS"
    );
    assert!(
        all.contains("arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole"),
        "service-role policy missing from Scope=AWS"
    );
}

#[test]
fn list_policies_scope_local_excludes_catalog() {
    let svc = make_service();
    let resp = svc
        .list_policies(&make_request("ListPolicies", vec![("Scope", "Local")]))
        .unwrap();
    let body = String::from_utf8_lossy(resp.body.expect_bytes()).to_string();
    assert!(
        !body.contains("arn:aws:iam::aws:policy/AdministratorAccess"),
        "Scope=Local must not include AWS-managed policies: {body}"
    );
}

#[test]
fn list_entities_for_managed_policy_counts_attachments() {
    let svc = make_service();
    let arn = "arn:aws:iam::aws:policy/AmazonS3ReadOnlyAccess";
    svc.create_role(&make_request(
        "CreateRole",
        vec![("RoleName", "reader"), ("AssumeRolePolicyDocument", "{}")],
    ))
    .unwrap();
    svc.attach_role_policy(&make_request(
        "AttachRolePolicy",
        vec![("RoleName", "reader"), ("PolicyArn", arn)],
    ))
    .unwrap();

    let resp = svc
        .list_entities_for_policy(&make_request(
            "ListEntitiesForPolicy",
            vec![("PolicyArn", arn)],
        ))
        .unwrap();
    let body = String::from_utf8_lossy(resp.body.expect_bytes()).to_string();
    assert!(body.contains("reader"), "attached role missing: {body}");

    // GetPolicy should now report AttachmentCount=1.
    let gp = svc
        .get_policy(&make_request("GetPolicy", vec![("PolicyArn", arn)]))
        .unwrap();
    let gp_body = String::from_utf8_lossy(gp.body.expect_bytes()).to_string();
    assert!(
        gp_body.contains("<AttachmentCount>1</AttachmentCount>"),
        "attachment count not reflected: {gp_body}"
    );
}
