// Ready-to-use Identity Provider (IdP) handlers for actix-web.
//
// Provides handlers for the standard SAML IdP endpoints:
// - SSO endpoint (receive AuthnRequest and authenticate user)
// - Single Logout Service (receive and process LogoutRequest/Response)
// - Artifact Resolution Service (resolve artifacts over SOAP back-channel)
// - Metadata generation
//
// Register all routes at once with `configure_idp()`.

use std::sync::Arc;

use actix_web::{web, HttpRequest, HttpResponse};
use chrono::{DateTime, Utc};

use gamlastan::bindings::relay_state::RelayState;
use gamlastan::core::assertion::name_id::NameId;
use gamlastan::crypto::signer::SamlSigner;
use gamlastan::profiles::artifact_resolution;
use gamlastan::profiles::logout;
use gamlastan::profiles::sso::idp as idp_profile;
use gamlastan::profiles::sso::web_browser::{ResponseOptions, ResponseTimes};
use gamlastan::xml::serialize::SamlSerialize;
use gamlastan::xml::uppsala;

use crate::config::IdpConfig;
use crate::error::SamlActixError;
use crate::extractors::SamlMessage;
use crate::responders::MetadataXml;

/// IdP signing context for signing responses, assertions, and metadata.
///
/// Separate from `IdpConfig` so it can be shared via `Arc` and contains
/// the actual signer (which holds the private key).
pub struct IdpSigningContext {
    /// The SAML signer (wraps bergshamra).
    pub signer: SamlSigner,
    /// Base64-encoded DER certificate for KeyInfo elements.
    pub cert_b64: String,
}

impl IdpSigningContext {
    /// Build a signing context from an already-constructed [`SamlSigner`].
    ///
    /// `cert_b64` is the base64 DER of the signing certificate — the same value
    /// that populates `<ds:X509Certificate>` in signed messages and metadata.
    pub fn new(signer: SamlSigner, cert_b64: impl Into<String>) -> Self {
        Self {
            signer,
            cert_b64: cert_b64.into(),
        }
    }

    /// Build an HSM / PKCS#11-backed signing context.
    ///
    /// `signer` is any [`kryptering::Signer`] — typically a
    /// `kryptering::pkcs11::Pkcs11Signer` bound to a private key on a token. The
    /// private key never leaves the token; signing happens on the HSM.
    ///
    /// The XML `SignatureMethod` placed into response/assertion/metadata
    /// templates is derived from the signer's configured algorithm.
    ///
    /// The IdP handlers already embed `cert_b64` into the `<ds:KeyInfo>` of the
    /// signature template (see [`signature_template`]), which is exactly what
    /// the HSM signing path needs — bergshamra-dsig does not auto-populate
    /// `<ds:KeyInfo>` when an HSM signer is in use. No `KeysManager` plumbing is
    /// required: an empty one is created internally.
    ///
    /// ```no_run
    /// # use std::sync::Arc;
    /// # use std::path::Path;
    /// use gamlastan_actix::IdpSigningContext;
    /// use gamlastan::crypto::kryptering::pkcs11::{Pkcs11Provider, Pkcs11Signer};
    /// use gamlastan::crypto::kryptering::{HashAlgorithm, SignatureAlgorithm};
    ///
    /// # let cert_b64 = String::new(); // base64 DER of the signing certificate
    /// let provider = Pkcs11Provider::new(Path::new("/usr/lib/softhsm/libsofthsm2.so"))?;
    /// let session = provider.open_session("1234")?;
    /// let pkcs11_signer = Pkcs11Signer::new(
    ///     &session,
    ///     "saml-signing-key",
    ///     SignatureAlgorithm::RsaPkcs1v15(HashAlgorithm::Sha256),
    /// )?;
    ///
    /// let signing_ctx = IdpSigningContext::from_hsm(Arc::new(pkcs11_signer), cert_b64);
    /// # Ok::<(), Box<dyn std::error::Error>>(())
    /// ```
    pub fn from_hsm(
        signer: Arc<dyn gamlastan::crypto::kryptering::Signer>,
        cert_b64: impl Into<String>,
    ) -> Self {
        let keys_manager = gamlastan::crypto::KeysManager::new();
        Self {
            signer: SamlSigner::with_hsm_signer(keys_manager, signer),
            cert_b64: cert_b64.into(),
        }
    }
}

/// Callback type for IdP authentication.
///
/// When the IdP receives an AuthnRequest, the application must authenticate the user.
/// This callback receives the processed request and returns the authenticated
/// user's NameId and attributes (or an error to reject the request).
pub type AuthnCallback = Box<
    dyn Fn(
            &idp_profile::ProcessedAuthnRequest,
            &HttpRequest,
        ) -> Result<AuthnCallbackResult, SamlActixError>
        + Send
        + Sync
        + 'static,
>;

/// Result from the authentication callback.
#[derive(Debug, Clone)]
pub struct AuthnCallbackResult {
    /// The authenticated user's NameId.
    pub name_id: NameId,
    /// User attributes to include in the assertion.
    pub attributes: Vec<gamlastan::core::assertion::attribute::Attribute>,
    /// Authentication context class reference (e.g., Password, PasswordProtectedTransport).
    pub authn_context_class_ref: Option<String>,
    /// Session index (generated by IdP, used for SLO).
    pub session_index: Option<String>,
    /// When the principal actually authenticated to the IdP.
    ///
    /// Set this to the SSO session's authentication time when reusing an
    /// existing session, so `AuthnStatement/@AuthnInstant` reflects the real
    /// authentication moment rather than response-generation time. `None`
    /// means "authenticated now" (a fresh login). See ADR 0025.
    pub authn_instant: Option<DateTime<Utc>>,
}

/// Register all IdP routes on the given service configuration.
///
/// Routes:
/// - `GET|POST /saml/sso`               - SSO endpoint (receive AuthnRequest)
/// - `GET|POST /saml/slo`               - Single Logout Service
/// - `POST     /saml/artifact-resolve`   - Artifact Resolution Service (SOAP)
/// - `GET      /saml/metadata`           - IdP Metadata
pub fn configure_idp(cfg: &mut web::ServiceConfig) {
    cfg.service(
        web::resource("/saml/sso")
            .route(web::get().to(idp_sso))
            .route(web::post().to(idp_sso)),
    )
    .service(
        web::resource("/saml/slo")
            .route(web::get().to(idp_slo))
            .route(web::post().to(idp_slo)),
    )
    .service(web::resource("/saml/artifact-resolve").route(web::post().to(idp_artifact_resolve)))
    .service(web::resource("/saml/metadata").route(web::get().to(idp_metadata)));
}

/// Build a ds:Signature template for enveloped signing.
///
/// The template contains empty DigestValue and SignatureValue placeholders
/// that are filled in by `signer.sign_enveloped()`.
pub fn signature_template(
    reference_id: &str,
    cert_b64: &str,
    signature_method_uri: &str,
) -> String {
    format!(
        r##"<ds:Signature xmlns:ds="http://www.w3.org/2000/09/xmldsig#"><ds:SignedInfo><ds:CanonicalizationMethod Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#"/><ds:SignatureMethod Algorithm="{sig_alg}"/><ds:Reference URI="#{id}"><ds:Transforms><ds:Transform Algorithm="http://www.w3.org/2000/09/xmldsig#enveloped-signature"/><ds:Transform Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#"/></ds:Transforms><ds:DigestMethod Algorithm="http://www.w3.org/2001/04/xmlenc#sha256"/><ds:DigestValue/></ds:Reference></ds:SignedInfo><ds:SignatureValue/><ds:KeyInfo><ds:X509Data><ds:X509Certificate>{cert}</ds:X509Certificate></ds:X509Data></ds:KeyInfo></ds:Signature>"##,
        id = reference_id,
        cert = cert_b64,
        sig_alg = signature_method_uri,
    )
}

fn metadata_signing_cert_b64<'a>(
    config: &'a IdpConfig,
    signing_ctx: Option<&'a IdpSigningContext>,
) -> Option<&'a str> {
    signing_ctx
        .map(|ctx| ctx.cert_b64.as_str())
        .or(config.signing_cert_b64.as_deref())
}

/// Insert a signature template after the first occurrence of a given element's opening tag.
///
/// Finds `<element_name ` or `<element_name>` and inserts the template after the `>`.
pub fn insert_signature_after_element(
    xml: &str,
    element_name: &str,
    sig_template: &str,
) -> Result<String, SamlActixError> {
    // Find the opening tag for the element
    let search_with_space = format!("<{element_name} ");
    let search_with_close = format!("<{element_name}>");

    let tag_start = xml
        .find(&search_with_space)
        .or_else(|| xml.find(&search_with_close))
        .ok_or_else(|| SamlActixError::Internal(format!("cannot find <{element_name}> in XML")))?;

    // Find the closing '>' of this opening tag
    let insert_pos = xml[tag_start..]
        .find('>')
        .ok_or_else(|| SamlActixError::Internal(format!("malformed <{element_name}> tag")))?;

    let absolute_pos = tag_start + insert_pos;
    Ok(format!(
        "{}{}{}",
        &xml[..=absolute_pos],
        sig_template,
        &xml[absolute_pos + 1..]
    ))
}

/// Sign a SAML Response XML, optionally signing both the Assertion and Response.
///
/// Signing order: Assertion first (inner), then Response (outer).
pub fn sign_response_xml(
    response_xml: &str,
    signing_ctx: &IdpSigningContext,
    response_id: &str,
    assertion_id: Option<&str>,
    sign_assertions: bool,
    sign_responses: bool,
) -> Result<String, SamlActixError> {
    let mut xml = response_xml.to_string();

    // Sign Assertion first (inner-to-outer order)
    if sign_assertions {
        if let Some(assertion_id) = assertion_id {
            let signature_method_uri = signing_ctx.signer.signature_method_uri().map_err(|e| {
                SamlActixError::Internal(format!("unsupported signing algorithm: {e}"))
            })?;
            let sig = signature_template(assertion_id, &signing_ctx.cert_b64, signature_method_uri);
            xml = insert_signature_after_element(&xml, "saml:Assertion", &sig)?;
            xml = signing_ctx
                .signer
                .sign_enveloped(&xml)
                .map_err(|e| SamlActixError::Internal(format!("assertion signing failed: {e}")))?;
        }
    }

    // Then sign Response (outer)
    if sign_responses {
        let signature_method_uri = signing_ctx
            .signer
            .signature_method_uri()
            .map_err(|e| SamlActixError::Internal(format!("unsupported signing algorithm: {e}")))?;
        let sig = signature_template(response_id, &signing_ctx.cert_b64, signature_method_uri);
        xml = insert_signature_after_element(&xml, "samlp:Response", &sig)?;
        xml = signing_ctx
            .signer
            .sign_enveloped(&xml)
            .map_err(|e| SamlActixError::Internal(format!("response signing failed: {e}")))?;
    }

    Ok(xml)
}

/// IdP SSO handler: process an incoming AuthnRequest.
///
/// This handler:
/// 1. Decodes and parses the AuthnRequest
/// 2. Validates it against SP metadata (if available)
/// 3. Calls the AuthnCallback to authenticate the user
/// 4. Creates a SAML Response with assertion
/// 5. Signs the Assertion and Response (if signing context is available)
/// 6. Sends the Response back to the SP's ACS URL via POST binding
/// 7. Forwards RelayState from the original request
async fn idp_sso(
    msg: SamlMessage,
    config: web::Data<IdpConfig>,
    signing_ctx: Option<web::Data<Arc<IdpSigningContext>>>,
    authn_callback: Option<web::Data<AuthnCallback>>,
    req: HttpRequest,
) -> Result<HttpResponse, SamlActixError> {
    // Save relay state before msg is consumed
    let relay_state_str = msg.relay_state.clone();

    // Parse the AuthnRequest
    let xml_str = msg.saml_xml_str()?;
    let doc = gamlastan::xml::parse_secure(xml_str).map_err(|e: uppsala::XmlError| {
        SamlActixError::Xml(gamlastan::xml::error::XmlError::ParseError(e))
    })?;
    let request_ref = gamlastan::xml::deserialize::parse_saml::<
        gamlastan::core::protocol::request::AuthnRequestRef<'_>,
    >(&doc)?;
    let authn_request = request_ref.to_owned();

    // Process the AuthnRequest (validates and extracts parameters)
    let processed = idp_profile::process_authn_request(&authn_request, None)
        .map_err(|e| SamlActixError::Internal(format!("AuthnRequest processing failed: {e}")))?;

    // Call the authentication callback
    let callback = authn_callback.ok_or_else(|| {
        SamlActixError::Configuration("no AuthnCallback registered for IdP SSO".into())
    })?;

    let authn_result = callback(&processed, &req)?;

    let now = Utc::now();
    let session_not_on_or_after = now
        + chrono::TimeDelta::try_seconds(config.session_lifetime_seconds as i64)
            .unwrap_or(chrono::TimeDelta::try_hours(8).unwrap());

    // Create the SAML Response
    let response_options = ResponseOptions {
        idp_entity_id: config.entity_id.clone(),
        in_response_to: Some(processed.request_id.clone()),
        sp_entity_id: processed.sp_entity_id.clone(),
        acs_url: processed.acs_url.clone(),
        assertion_lifetime_seconds: config.assertion_lifetime_seconds,
        session_index: authn_result.session_index.clone(),
        session_not_on_or_after: Some(session_not_on_or_after),
        authn_context_class_ref: authn_result.authn_context_class_ref.clone(),
        client_address: req
            .connection_info()
            .realip_remote_addr()
            .map(|s| s.to_string()),
        attributes: authn_result.attributes.clone(),
    };

    let times = ResponseTimes {
        issue_instant: now,
        authn_instant: authn_result.authn_instant.unwrap_or(now),
    };
    let response = idp_profile::create_response(&response_options, &authn_result.name_id, times);

    let response_xml = response
        .to_xml_string()
        .map_err(|e| SamlActixError::Internal(format!("failed to serialize Response: {e}")))?;

    // Sign the Response and Assertion if signing context is available
    let final_xml = if let Some(ctx) = &signing_ctx {
        // Extract assertion ID from the response for signing
        let assertion_id = response.assertions.first().map(|a| a.id.as_str());
        sign_response_xml(
            &response_xml,
            ctx,
            &response.base.id,
            assertion_id,
            config.sign_assertions,
            config.sign_responses,
        )?
    } else {
        response_xml
    };

    // Build RelayState for forwarding
    let relay_state = relay_state_str.as_deref().map(RelayState::echo);

    // Send via POST binding to the SP's ACS URL
    let html = gamlastan::bindings::post::post_encode(
        final_xml.as_bytes(),
        false, // is_response, not request
        &processed.acs_url,
        relay_state.as_ref(),
    );

    Ok(crate::response_adapter::post_binding_response(&html))
}

/// IdP SLO handler: process incoming LogoutRequest or LogoutResponse.
async fn idp_slo(
    msg: SamlMessage,
    config: web::Data<IdpConfig>,
) -> Result<HttpResponse, SamlActixError> {
    let xml_str = msg.saml_xml_str()?;
    let doc = gamlastan::xml::parse_secure(xml_str).map_err(|e: uppsala::XmlError| {
        SamlActixError::Xml(gamlastan::xml::error::XmlError::ParseError(e))
    })?;

    let root = doc
        .document_element()
        .ok_or_else(|| SamlActixError::Internal("empty XML document".into()))?;
    let root_elem = doc
        .element(root)
        .ok_or_else(|| SamlActixError::Internal("invalid root element".into()))?;

    match root_elem.name.local_name.as_ref() {
        "LogoutRequest" => {
            let req_ref = gamlastan::xml::deserialize::parse_saml::<
                gamlastan::core::protocol::logout::LogoutRequestRef<'_>,
            >(&doc)?;
            let logout_req = req_ref.to_owned();

            let now = Utc::now();
            logout::validate_logout_request(&logout_req, now, config.security.clock_skew_seconds)
                .map_err(|e| SamlActixError::Internal(format!("invalid LogoutRequest: {e}")))?;

            // Propagate logout to session participants via the SessionStore
            if let Some(ref session_store) = config.session_store {
                use gamlastan::core::assertion::name_id::NameIdOrEncryptedId;
                let name_id_value = match &logout_req.name_id {
                    NameIdOrEncryptedId::NameId(nid) => nid.value.as_str(),
                    NameIdOrEncryptedId::EncryptedId(_) => "",
                };

                if !name_id_value.is_empty() {
                    let sessions = session_store.get_sessions_by_name_id(name_id_value);

                    for session in &sessions {
                        // Build LogoutRequest for each participant (except the requester)
                        let requester_entity_id = logout_req
                            .issuer
                            .as_ref()
                            .map(|i| i.value.as_str())
                            .unwrap_or("");

                        for participant in &session.participants {
                            if participant.entity_id == requester_entity_id {
                                continue; // Don't send back to the requester
                            }
                            let propagation_req = logout::create_idp_propagation_request(
                                &config.entity_id,
                                participant,
                            );
                            let _propagation_xml = propagation_req.to_xml_string().ok();
                            // Note: actual HTTP delivery requires an async HTTP client.
                            // The application should implement a SoapTransport or
                            // use the propagation request XML directly. The SessionStore
                            // tracks who needs logout; the application handles delivery.
                        }

                        // Clean up session
                        session_store.destroy_session(&session.session_index);
                    }
                }
            }

            let in_response_to = &logout_req.id;
            let response =
                logout::create_logout_response_success(&config.entity_id, in_response_to, None);

            let response_xml = response.to_xml_string().map_err(|e| {
                SamlActixError::Internal(format!("failed to serialize LogoutResponse: {e}"))
            })?;

            Ok(HttpResponse::Ok()
                .content_type("text/html; charset=utf-8")
                .body(format!(
                    "<html><body><p>Logout completed</p><!-- {} --></body></html>",
                    response_xml.len()
                )))
        }
        "LogoutResponse" => {
            // Process the SP's response to our propagation LogoutRequest
            Ok(HttpResponse::Ok().body("Logout propagation acknowledged"))
        }
        other => Err(SamlActixError::UnsupportedBinding(format!(
            "unexpected SAML element in SLO: {other}"
        ))),
    }
}

/// IdP Artifact Resolution handler: resolve artifacts over SOAP back-channel.
///
/// Expects a SOAP-wrapped ArtifactResolve request.
async fn idp_artifact_resolve(
    msg: SamlMessage,
    config: web::Data<IdpConfig>,
) -> Result<HttpResponse, SamlActixError> {
    let xml_str = msg.saml_xml_str()?;
    let doc = gamlastan::xml::parse_secure(xml_str).map_err(|e: uppsala::XmlError| {
        SamlActixError::Xml(gamlastan::xml::error::XmlError::ParseError(e))
    })?;

    let resolve_ref = gamlastan::xml::deserialize::parse_saml::<
        gamlastan::core::protocol::artifact::ArtifactResolveRef<'_>,
    >(&doc)?;
    let resolve = resolve_ref.to_owned();

    // Look up the artifact in the artifact store
    let response_xml = if let Some(ref artifact_store) = config.artifact_store {
        match artifact_store.resolve_and_consume(&resolve.artifact) {
            Ok(Some(message_xml)) => {
                // Build a successful ArtifactResponse wrapping the resolved SAML message
                let art_response = artifact_resolution::create_artifact_response(
                    &config.entity_id,
                    &resolve.id,
                    Some(message_xml),
                );
                art_response.to_xml_string().map_err(|e| {
                    SamlActixError::Internal(format!("failed to serialize ArtifactResponse: {e}"))
                })?
            }
            Ok(None) => {
                // Artifact not found (or already consumed)
                let error_response = artifact_resolution::create_artifact_response_error(
                    &config.entity_id,
                    &resolve.id,
                    "artifact not found or already consumed",
                );
                error_response.to_xml_string().map_err(|e| {
                    SamlActixError::Internal(format!("failed to serialize ArtifactResponse: {e}"))
                })?
            }
            Err(e) => {
                let error_response = artifact_resolution::create_artifact_response_error(
                    &config.entity_id,
                    &resolve.id,
                    &format!("artifact store error: {e}"),
                );
                error_response.to_xml_string().map_err(|e| {
                    SamlActixError::Internal(format!("failed to serialize ArtifactResponse: {e}"))
                })?
            }
        }
    } else {
        // No artifact store configured
        let error_response = artifact_resolution::create_artifact_response_error(
            &config.entity_id,
            &resolve.id,
            "artifact resolution not configured",
        );
        error_response.to_xml_string().map_err(|e| {
            SamlActixError::Internal(format!("failed to serialize ArtifactResponse: {e}"))
        })?
    };

    // Wrap in SOAP envelope
    let soap_body = gamlastan::bindings::soap::soap_envelope_wrap(&response_xml, None);
    Ok(HttpResponse::Ok()
        .content_type("text/xml; charset=utf-8")
        .body(soap_body))
}

/// IdP metadata handler: generate and return the IdP's SAML metadata.
///
/// If a signing context is available, the metadata will include a KeyDescriptor
/// and will be signed with an enveloped signature.
async fn idp_metadata(
    config: web::Data<IdpConfig>,
    signing_ctx: Option<web::Data<Arc<IdpSigningContext>>>,
) -> Result<MetadataXml, SamlActixError> {
    use gamlastan::core::identifiers::SamlId;
    use gamlastan::metadata::types::endpoint::Endpoint;
    use gamlastan::metadata::types::entity_descriptor::{EntityDescriptor, EntityRoles};
    use gamlastan::metadata::types::idp::IdpSsoDescriptor;
    use gamlastan::metadata::types::key_descriptor::KeyDescriptor;
    use gamlastan::metadata::types::role_descriptor::{RoleDescriptorBase, SsoDescriptorBase};

    // Prefer the active signing context's certificate so metadata stays in sync
    // with the key that actually signs responses and metadata.
    let metadata_cert_b64 = metadata_signing_cert_b64(
        config.get_ref(),
        signing_ctx.as_ref().map(|ctx| ctx.get_ref().as_ref()),
    );

    let key_descriptors = if let Some(cert_b64) = metadata_cert_b64 {
        let key_info_xml = gamlastan::crypto::build_x509_key_info(&[cert_b64]);
        vec![KeyDescriptor::signing(key_info_xml)]
    } else {
        vec![]
    };

    let mut base =
        RoleDescriptorBase::new(vec!["urn:oasis:names:tc:SAML:2.0:protocol".to_string()]);
    base.key_descriptors = key_descriptors;

    let idp_sso_desc = IdpSsoDescriptor {
        sso_base: SsoDescriptorBase {
            base,
            artifact_resolution_services: vec![],
            single_logout_services: if config.slo_url.is_empty() {
                vec![]
            } else {
                vec![
                    Endpoint::new(
                        gamlastan::profiles::sso::web_browser::bindings::HTTP_REDIRECT,
                        &config.slo_url,
                    ),
                    Endpoint::new(
                        gamlastan::profiles::sso::web_browser::bindings::HTTP_POST,
                        &config.slo_url,
                    ),
                ]
            },
            manage_name_id_services: vec![],
            name_id_formats: vec![
                gamlastan::core::constants::NAMEID_TRANSIENT.to_string(),
                gamlastan::core::constants::NAMEID_EMAIL.to_string(),
            ],
        },
        want_authn_requests_signed: Some(false),
        single_sign_on_services: vec![
            Endpoint::new(
                gamlastan::profiles::sso::web_browser::bindings::HTTP_REDIRECT,
                &config.sso_url,
            ),
            Endpoint::new(
                gamlastan::profiles::sso::web_browser::bindings::HTTP_POST,
                &config.sso_url,
            ),
        ],
        name_id_mapping_services: vec![],
        assertion_id_request_services: vec![],
        attribute_profiles: vec![],
        attributes: vec![],
    };

    let metadata_id = SamlId::generate().to_string();
    let entity = EntityDescriptor {
        entity_id: config.entity_id.clone(),
        id: Some(metadata_id.clone()),
        valid_until: None,
        cache_duration: None,
        has_signature: false,
        extensions: None,
        roles: EntityRoles::Roles {
            idp_sso: vec![idp_sso_desc],
            sp_sso: vec![],
            authn_authority: vec![],
            attr_authority: vec![],
            pdp: vec![],
        },
        organization: None,
        contact_persons: vec![],
        additional_metadata_locations: vec![],
    };

    let xml = entity
        .to_xml_string()
        .map_err(|e| SamlActixError::Internal(format!("failed to serialize metadata: {e}")))?;

    // Sign metadata if signing context is available
    let final_xml = if let Some(ctx) = &signing_ctx {
        let signature_method_uri = ctx
            .signer
            .signature_method_uri()
            .map_err(|e| SamlActixError::Internal(format!("unsupported signing algorithm: {e}")))?;
        let sig = signature_template(&metadata_id, &ctx.cert_b64, signature_method_uri);
        let xml_with_sig = insert_signature_after_element(&xml, "md:EntityDescriptor", &sig)?;
        ctx.signer
            .sign_enveloped(&xml_with_sig)
            .map_err(|e| SamlActixError::Internal(format!("metadata signing failed: {e}")))?
    } else {
        xml
    };

    Ok(MetadataXml(final_xml))
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_authn_callback_result_debug() {
        let result = AuthnCallbackResult {
            name_id: NameId {
                value: "user@example.com".to_string(),
                format: None,
                name_qualifier: None,
                sp_name_qualifier: None,
                sp_provided_id: None,
            },
            attributes: vec![],
            authn_context_class_ref: None,
            session_index: Some("_sess_123".to_string()),
            authn_instant: None,
        };
        assert!(format!("{result:?}").contains("user@example.com"));
    }

    #[test]
    fn test_signature_template() {
        let sig = signature_template(
            "_abc123",
            "MIID...",
            "http://www.w3.org/2001/04/xmldsig-more#rsa-sha256",
        );
        assert!(sig.contains("URI=\"#_abc123\""));
        assert!(sig.contains("<ds:X509Certificate>MIID...</ds:X509Certificate>"));
        assert!(sig.contains("rsa-sha256"));
        assert!(sig.contains("exc-c14n"));
    }

    #[test]
    fn test_insert_signature_after_element() {
        let xml = r#"<samlp:Response xmlns:samlp="urn:oasis:names:tc:SAML:2.0:protocol" ID="_resp1"><saml:Assertion ID="_a1"/></samlp:Response>"#;
        let sig = "<SIGNATURE/>";
        let result = insert_signature_after_element(xml, "samlp:Response", sig).unwrap();
        assert!(result.contains(r#"ID="_resp1"><SIGNATURE/><saml:Assertion"#));
    }

    #[test]
    fn test_metadata_signing_cert_prefers_signing_context() {
        let config = IdpConfig::new("https://idp.example.com", "https://idp.example.com/sso")
            .with_signing_cert("CONFIG_CERT");
        let signing_ctx = IdpSigningContext::new(
            SamlSigner::new(gamlastan::crypto::KeysManager::new()),
            "CTX_CERT",
        );

        assert_eq!(
            metadata_signing_cert_b64(&config, Some(&signing_ctx)),
            Some("CTX_CERT")
        );
    }

    #[test]
    fn test_metadata_signing_cert_falls_back_to_config() {
        let config = IdpConfig::new("https://idp.example.com", "https://idp.example.com/sso")
            .with_signing_cert("CONFIG_CERT");

        assert_eq!(
            metadata_signing_cert_b64(&config, None),
            Some("CONFIG_CERT")
        );
    }
}
