use athena_auth_core::adapters::DatabaseAdapter;
use athena_auth_core::entity::{
    AuthInvitation, AuthMember, AuthOrganization, AuthSession, AuthUser,
};
use athena_auth_core::error::{AuthError, AuthResult};
use athena_auth_core::plugin::AuthContext;
use athena_auth_core::types::{
    AuthRequest, AuthResponse, CreateEmailSendFailure, CreateInvitation, CreateMember,
    InvitationStatus,
};
use athena_auth_core::{
    AuthEmailRenderContext, build_auth_email_message, merge_email_metadata,
    organization_invitation_email_for_context,
};
use url::Url;

use super::{
    collect_member_user_ids_for_organization, refresh_organization_list_cache_for_users,
    require_session, resolve_organization_id,
};
use crate::plugins::organization::rbac::{
    CANCEL_ACTION, CREATE_ACTION, INVITATION_RESOURCE, has_permission_any,
};
use crate::plugins::organization::types::{
    AcceptInvitationRequest, AcceptInvitationResponse, CancelInvitationRequest, GetInvitationQuery,
    GetInvitationResponse, InviteMemberRequest, ListInvitationsQuery, MemberResponse,
    RejectInvitationRequest, SuccessResponse,
};
use crate::plugins::organization::{InvitationEmailPayload, OrganizationConfig};

fn parse_callback_url<DB: DatabaseAdapter>(
    ctx: &AuthContext<DB>,
    callback_url: &str,
) -> Option<Url> {
    if let Ok(url) = Url::parse(callback_url)
        && url.origin().is_tuple()
    {
        return Some(url);
    }

    if callback_url.starts_with('/') {
        let base = ctx.config.base_url.trim_end_matches('/');
        return Url::parse(&format!("{base}{callback_url}")).ok();
    }

    None
}

fn build_invitation_url<DB: DatabaseAdapter>(
    ctx: &AuthContext<DB>,
    invitation: &DB::Invitation,
    invited_email: &str,
    callback_url: Option<&str>,
) -> String {
    if let Some(callback_url) = callback_url
        && let Some(mut callback) = parse_callback_url(ctx, callback_url)
    {
        callback
            .query_pairs_mut()
            .append_pair("invitationId", invitation.id())
            .append_pair("organizationId", invitation.organization_id())
            .append_pair("email", invited_email);
        return callback.to_string();
    }

    let mut default_url = Url::parse(&ctx.config.public_auth_url("organization/get-invitation"))
        .unwrap_or_else(|_| {
            Url::parse("http://localhost/organization/get-invitation")
                .expect("localhost URL must be valid")
        });
    default_url
        .query_pairs_mut()
        .append_pair("id", invitation.id());
    default_url.to_string()
}

struct InvitationSendFailureContext<'a> {
    user_id: Option<&'a str>,
    recipient_email: &'a str,
    provider: Option<&'a str>,
    error_message: &'a str,
    invitation_url: &'a str,
    extra_metadata: Option<serde_json::Value>,
}

async fn record_invitation_email_send_failure<DB: DatabaseAdapter>(
    ctx: &AuthContext<DB>,
    failure: InvitationSendFailureContext<'_>,
    invitation: &impl AuthInvitation,
    organization: &impl AuthOrganization,
) {
    let metadata = merge_email_metadata(
        Some(serde_json::json!({
            "flow": "organization_invitation",
            "invitation_id": invitation.id(),
            "organization_id": invitation.organization_id(),
            "organization_slug": organization.slug(),
            "invitation_url": failure.invitation_url,
        })),
        failure.extra_metadata,
    );

    let log_row = CreateEmailSendFailure {
        user_id: failure.user_id.map(std::string::ToString::to_string),
        recipient_email: failure.recipient_email.to_string(),
        flow: "organization_invitation".to_string(),
        provider: failure.provider.map(std::string::ToString::to_string),
        error_message: failure.error_message.to_string(),
        metadata,
    };

    if let Err(log_err) = ctx.database.create_email_send_failure(log_row).await {
        tracing::error!(
            recipient_email = %failure.recipient_email,
            invitation_id = %invitation.id(),
            organization_id = %organization.id(),
            failure_log_error = %log_err,
            "Failed to persist invitation email send failure"
        );
    }
}

async fn send_invitation_email<DB: DatabaseAdapter>(
    body: &InviteMemberRequest,
    invitation: &DB::Invitation,
    inviter: &DB::User,
    config: &OrganizationConfig,
    ctx: &AuthContext<DB>,
) -> AuthResult<()> {
    if !config.send_email_notifications && config.send_invitation_email.is_none() {
        tracing::debug!(
            invitation_id = %invitation.id(),
            "organization invitation email notifications are disabled"
        );
        return Ok(());
    }

    let organization = ctx
        .database
        .get_organization_by_id(invitation.organization_id())
        .await?
        .ok_or_else(|| AuthError::not_found("Organization not found"))?;

    let invitation_url =
        build_invitation_url(ctx, invitation, &body.email, body.callback_url.as_deref());

    if let Some(custom_sender) = config.send_invitation_email.as_ref() {
        let payload = InvitationEmailPayload {
            invitation_id: invitation.id().to_string(),
            organization_id: invitation.organization_id().to_string(),
            organization_name: organization.name().to_string(),
            organization_slug: organization.slug().to_string(),
            invited_email: body.email.clone(),
            role: invitation.role().to_string(),
            inviter_user_id: inviter.id().to_string(),
            inviter_name: inviter.name().map(std::string::ToString::to_string),
            inviter_email: inviter.email().map(std::string::ToString::to_string),
            invitation_url: invitation_url.clone(),
        };

        if let Err(err) = custom_sender.send(payload).await {
            let error_message = err.to_string();
            record_invitation_email_send_failure(
                ctx,
                InvitationSendFailureContext {
                    user_id: Some(inviter.id()),
                    recipient_email: &body.email,
                    provider: Some("custom_sender"),
                    error_message: &error_message,
                    invitation_url: &invitation_url,
                    extra_metadata: None,
                },
                invitation,
                &organization,
            )
            .await;
            return Err(err);
        }

        tracing::info!(
            invitation_id = %invitation.id(),
            organization_id = %organization.id(),
            recipient_email = %body.email,
            provider = "custom_sender",
            "invitation email sent"
        );
        return Ok(());
    }

    if !config.send_email_notifications {
        return Ok(());
    }

    let inviter_identity = inviter
        .name()
        .or_else(|| inviter.email())
        .unwrap_or(inviter.id());

    let bodies = organization_invitation_email_for_context(
        ctx,
        organization.name(),
        invitation.role(),
        inviter_identity,
        invitation_url.as_str(),
        AuthEmailRenderContext::new()
            .with_user_id(inviter.id())
            .with_organization_id(organization.id()),
    )
    .await;

    match ctx.email_provider() {
        Ok(provider) => {
            let message = match build_auth_email_message(ctx, &body.email, &bodies).await {
                Ok(message) => message,
                Err(err) => {
                    let error_message = err.to_string();
                    record_invitation_email_send_failure(
                        ctx,
                        InvitationSendFailureContext {
                            user_id: Some(inviter.id()),
                            recipient_email: &body.email,
                            provider: Some("email_provider"),
                            error_message: &error_message,
                            invitation_url: &invitation_url,
                            extra_metadata: err.metadata().cloned(),
                        },
                        invitation,
                        &organization,
                    )
                    .await;
                    tracing::warn!(
                        invitation_id = %invitation.id(),
                        organization_id = %organization.id(),
                        recipient_email = %body.email,
                        error = %err,
                        metadata = ?err.metadata(),
                        "invitation email preparation failed; continuing without blocking invitation creation"
                    );
                    return Ok(());
                }
            };
            match provider.send_message(&message).await {
                Ok(()) => {
                    tracing::info!(
                        invitation_id = %invitation.id(),
                        organization_id = %organization.id(),
                        recipient_email = %body.email,
                        provider = "email_provider",
                        "invitation email sent"
                    );
                    Ok(())
                }
                Err(err) => {
                    let error_message = err.to_string();
                    record_invitation_email_send_failure(
                        ctx,
                        InvitationSendFailureContext {
                            user_id: Some(inviter.id()),
                            recipient_email: &body.email,
                            provider: Some("email_provider"),
                            error_message: &error_message,
                            invitation_url: &invitation_url,
                            extra_metadata: message.metadata.clone(),
                        },
                        invitation,
                        &organization,
                    )
                    .await;
                    tracing::warn!(
                        invitation_id = %invitation.id(),
                        organization_id = %organization.id(),
                        recipient_email = %body.email,
                        error = %err,
                        "invitation email delivery failed; continuing without blocking invitation creation"
                    );
                    Ok(())
                }
            }
        }
        Err(err) => {
            let error_message = err.to_string();
            record_invitation_email_send_failure(
                ctx,
                InvitationSendFailureContext {
                    user_id: Some(inviter.id()),
                    recipient_email: &body.email,
                    provider: None,
                    error_message: &error_message,
                    invitation_url: &invitation_url,
                    extra_metadata: None,
                },
                invitation,
                &organization,
            )
            .await;
            tracing::warn!(
                invitation_id = %invitation.id(),
                organization_id = %organization.id(),
                recipient_email = %body.email,
                error = %err,
                "no email provider configured; invitation created without email delivery"
            );
            Ok(())
        }
    }
}

// ---------------------------------------------------------------------------
// Core functions
// ---------------------------------------------------------------------------

pub(crate) async fn invite_member_core<DB: DatabaseAdapter>(
    body: &InviteMemberRequest,
    user: &DB::User,
    session: &DB::Session,
    config: &OrganizationConfig,
    ctx: &AuthContext<DB>,
) -> AuthResult<DB::Invitation> {
    let effective_grant_catalog = config.effective_grant_catalog();
    let org_id =
        resolve_organization_id(body.organization_id.as_deref(), None, session, ctx).await?;

    let member = ctx
        .database
        .get_member(&org_id, user.id())
        .await?
        .ok_or_else(|| AuthError::forbidden("Not a member of this organization"))?;

    if !has_permission_any(
        member.role(),
        INVITATION_RESOURCE,
        CREATE_ACTION,
        &config.roles,
        &effective_grant_catalog,
    ) {
        return Err(AuthError::forbidden(
            "You don't have permission to invite members",
        ));
    }

    if let Some(limit) = config.membership_limit {
        let members = ctx.database.list_organization_members(&org_id).await?;
        if members.len() >= limit {
            return Err(AuthError::bad_request(format!(
                "Membership limit of {} reached",
                limit
            )));
        }
    }

    if let Some(limit) = config.invitation_limit {
        let invitations = ctx.database.list_organization_invitations(&org_id).await?;
        let pending_count = invitations.iter().filter(|i| i.is_pending()).count();
        if pending_count >= limit {
            return Err(AuthError::bad_request(format!(
                "Pending invitation limit of {} reached",
                limit
            )));
        }
    }

    if let Some(existing_user) = ctx.database.get_user_by_email(&body.email).await?
        && ctx
            .database
            .get_member(&org_id, existing_user.id())
            .await?
            .is_some()
    {
        return Err(AuthError::bad_request("User is already a member"));
    }

    // Return existing pending invitation if one exists
    if let Some(existing) = ctx
        .database
        .get_pending_invitation(&org_id, &body.email)
        .await?
    {
        send_invitation_email(body, &existing, user, config, ctx).await?;
        return Ok(existing);
    }

    let expires_at =
        chrono::Utc::now() + chrono::Duration::seconds(config.invitation_expires_in as i64);

    let invitation_data = CreateInvitation {
        organization_id: org_id.clone(),
        email: body.email.clone(),
        role: body.role.clone(),
        inviter_id: user.id().to_string(),
        expires_at,
    };

    let invitation = ctx.database.create_invitation(invitation_data).await?;
    send_invitation_email(body, &invitation, user, config, ctx).await?;

    Ok(invitation)
}

pub(crate) async fn get_invitation_core<DB: DatabaseAdapter>(
    query: &GetInvitationQuery,
    ctx: &AuthContext<DB>,
) -> AuthResult<GetInvitationResponse<DB::Invitation>> {
    if query.id.is_empty() {
        return Err(AuthError::bad_request("Missing invitation id"));
    }

    let invitation = ctx
        .database
        .get_invitation_by_id(&query.id)
        .await?
        .ok_or_else(|| AuthError::not_found("Invitation not found"))?;

    let organization = ctx
        .database
        .get_organization_by_id(invitation.organization_id())
        .await?
        .ok_or_else(|| AuthError::not_found("Organization not found"))?;

    let inviter_email =
        if let Some(inviter) = ctx.database.get_user_by_id(invitation.inviter_id()).await? {
            inviter.email().map(|s| s.to_string())
        } else {
            None
        };

    Ok(GetInvitationResponse {
        invitation,
        organization_name: organization.name().to_string(),
        organization_slug: organization.slug().to_string(),
        inviter_email,
    })
}

pub(crate) async fn list_invitations_core<DB: DatabaseAdapter>(
    query: &ListInvitationsQuery,
    user: &DB::User,
    session: &DB::Session,
    ctx: &AuthContext<DB>,
) -> AuthResult<Vec<DB::Invitation>> {
    let org_id =
        resolve_organization_id(query.organization_id.as_deref(), None, session, ctx).await?;

    ctx.database
        .get_member(&org_id, user.id())
        .await?
        .ok_or_else(|| AuthError::forbidden("Not a member of this organization"))?;

    let invitations = ctx.database.list_organization_invitations(&org_id).await?;

    Ok(invitations)
}

pub(crate) async fn list_user_invitations_core<DB: DatabaseAdapter>(
    user: &DB::User,
    ctx: &AuthContext<DB>,
) -> AuthResult<Vec<DB::Invitation>> {
    let user_email = user
        .email()
        .ok_or_else(|| AuthError::bad_request("User has no email"))?;

    let all_invitations = ctx.database.list_user_invitations(user_email).await?;

    let pending: Vec<_> = all_invitations
        .into_iter()
        .filter(|i| i.is_pending() && !i.is_expired())
        .collect();

    Ok(pending)
}

pub(crate) async fn accept_invitation_core<DB: DatabaseAdapter>(
    body: &AcceptInvitationRequest,
    user: &DB::User,
    session: &DB::Session,
    config: &OrganizationConfig,
    ctx: &AuthContext<DB>,
) -> AuthResult<AcceptInvitationResponse<DB::Invitation>> {
    let invitation = ctx
        .database
        .get_invitation_by_id(&body.invitation_id)
        .await?
        .ok_or_else(|| AuthError::not_found("Invitation not found"))?;

    let user_email = user
        .email()
        .ok_or_else(|| AuthError::bad_request("User has no email"))?;

    if invitation.email().to_lowercase() != user_email.to_lowercase() {
        return Err(AuthError::forbidden("This invitation is not for you"));
    }

    if !invitation.is_pending() {
        return Err(AuthError::bad_request(format!(
            "Invitation is {:?}",
            invitation.status()
        )));
    }

    if invitation.is_expired() {
        return Err(AuthError::bad_request("Invitation has expired"));
    }

    if config.require_email_verification_on_invitation && !user.email_verified() {
        return Err(AuthError::forbidden(
            "Verify your email before accepting invitations",
        ));
    }

    if let Some(limit) = config.membership_limit {
        let members = ctx
            .database
            .list_organization_members(invitation.organization_id())
            .await?;
        if members.len() >= limit {
            return Err(AuthError::bad_request(
                "Organization membership limit reached",
            ));
        }
    }

    if ctx
        .database
        .get_member(invitation.organization_id(), user.id())
        .await?
        .is_some()
    {
        ctx.database
            .update_invitation_status(invitation.id(), InvitationStatus::Accepted)
            .await?;
        return Err(AuthError::bad_request(
            "Already a member of this organization",
        ));
    }

    let member_data = CreateMember {
        organization_id: invitation.organization_id().to_string(),
        user_id: user.id().to_string(),
        role: invitation.role().to_string(),
    };

    let member = ctx.database.create_member(member_data).await?;

    let updated_invitation = ctx
        .database
        .update_invitation_status(invitation.id(), InvitationStatus::Accepted)
        .await?;

    ctx.database
        .update_session_active_organization(session.token(), Some(invitation.organization_id()))
        .await?;

    match collect_member_user_ids_for_organization(invitation.organization_id(), ctx).await {
        Ok(mut affected_user_ids) => {
            affected_user_ids.push(user.id().to_string());
            if let Err(cache_error) =
                refresh_organization_list_cache_for_users(&affected_user_ids, ctx).await
            {
                tracing::warn!(
                    organization_id = %invitation.organization_id(),
                    error = %cache_error,
                    "Failed to refresh organization list cache after accept_invitation"
                );
            }
        }
        Err(cache_error) => {
            tracing::warn!(
                organization_id = %invitation.organization_id(),
                error = %cache_error,
                "Failed to resolve affected users after accept_invitation"
            );
        }
    }

    let member_response = MemberResponse::from_member_and_user(&member, user);

    Ok(AcceptInvitationResponse {
        invitation: updated_invitation,
        member: member_response,
    })
}

pub(crate) async fn reject_invitation_core<DB: DatabaseAdapter>(
    body: &RejectInvitationRequest,
    user: &DB::User,
    ctx: &AuthContext<DB>,
) -> AuthResult<SuccessResponse> {
    let invitation = ctx
        .database
        .get_invitation_by_id(&body.invitation_id)
        .await?
        .ok_or_else(|| AuthError::not_found("Invitation not found"))?;

    let user_email = user
        .email()
        .ok_or_else(|| AuthError::bad_request("User has no email"))?;

    if invitation.email().to_lowercase() != user_email.to_lowercase() {
        return Err(AuthError::forbidden("This invitation is not for you"));
    }

    if !invitation.is_pending() {
        return Err(AuthError::bad_request(format!(
            "Invitation is already {:?}",
            invitation.status()
        )));
    }

    ctx.database
        .update_invitation_status(invitation.id(), InvitationStatus::Rejected)
        .await?;

    Ok(SuccessResponse { success: true })
}

pub(crate) async fn cancel_invitation_core<DB: DatabaseAdapter>(
    body: &CancelInvitationRequest,
    user: &DB::User,
    config: &OrganizationConfig,
    ctx: &AuthContext<DB>,
) -> AuthResult<SuccessResponse> {
    let effective_grant_catalog = config.effective_grant_catalog();
    let invitation = ctx
        .database
        .get_invitation_by_id(&body.invitation_id)
        .await?
        .ok_or_else(|| AuthError::not_found("Invitation not found"))?;

    let member = ctx
        .database
        .get_member(invitation.organization_id(), user.id())
        .await?
        .ok_or_else(|| AuthError::forbidden("Not a member of this organization"))?;

    if !has_permission_any(
        member.role(),
        INVITATION_RESOURCE,
        CANCEL_ACTION,
        &config.roles,
        &effective_grant_catalog,
    ) {
        return Err(AuthError::forbidden(
            "You don't have permission to cancel invitations",
        ));
    }

    if !invitation.is_pending() {
        return Err(AuthError::bad_request(format!(
            "Invitation is already {:?}",
            invitation.status()
        )));
    }

    ctx.database
        .update_invitation_status(invitation.id(), InvitationStatus::Canceled)
        .await?;

    Ok(SuccessResponse { success: true })
}

// ---------------------------------------------------------------------------
// Old handlers (rewritten to call core)
// ---------------------------------------------------------------------------

/// Handle invite member request
pub async fn handle_invite_member<DB: DatabaseAdapter>(
    req: &AuthRequest,
    ctx: &AuthContext<DB>,
    config: &OrganizationConfig,
) -> AuthResult<AuthResponse> {
    let (user, session) = require_session(req, ctx).await?;
    let body: InviteMemberRequest = match athena_auth_core::validate_request_body(req) {
        Ok(v) => v,
        Err(resp) => return Ok(resp),
    };
    let invitation = invite_member_core(&body, &user, &session, config, ctx).await?;
    Ok(AuthResponse::json(200, &invitation)?)
}

/// Handle get invitation request
pub async fn handle_get_invitation<DB: DatabaseAdapter>(
    req: &AuthRequest,
    ctx: &AuthContext<DB>,
) -> AuthResult<AuthResponse> {
    let query = parse_query::<GetInvitationQuery>(&req.query);
    let response = get_invitation_core(&query, ctx).await?;
    Ok(AuthResponse::json(200, &response)?)
}

/// Handle list invitations request
pub async fn handle_list_invitations<DB: DatabaseAdapter>(
    req: &AuthRequest,
    ctx: &AuthContext<DB>,
) -> AuthResult<AuthResponse> {
    let (user, session) = require_session(req, ctx).await?;
    let query = parse_query::<ListInvitationsQuery>(&req.query);
    let invitations = list_invitations_core(&query, &user, &session, ctx).await?;
    Ok(AuthResponse::json(200, &invitations)?)
}

/// Handle list user invitations request
pub async fn handle_list_user_invitations<DB: DatabaseAdapter>(
    req: &AuthRequest,
    ctx: &AuthContext<DB>,
) -> AuthResult<AuthResponse> {
    let (user, _session) = require_session(req, ctx).await?;
    let pending = list_user_invitations_core(&user, ctx).await?;
    Ok(AuthResponse::json(200, &pending)?)
}

/// Handle accept invitation request
pub async fn handle_accept_invitation<DB: DatabaseAdapter>(
    req: &AuthRequest,
    ctx: &AuthContext<DB>,
    config: &OrganizationConfig,
) -> AuthResult<AuthResponse> {
    let (user, session) = require_session(req, ctx).await?;
    let body: AcceptInvitationRequest = match athena_auth_core::validate_request_body(req) {
        Ok(v) => v,
        Err(resp) => return Ok(resp),
    };
    let response = accept_invitation_core(&body, &user, &session, config, ctx).await?;
    Ok(AuthResponse::json(200, &response)?)
}

/// Handle reject invitation request
pub async fn handle_reject_invitation<DB: DatabaseAdapter>(
    req: &AuthRequest,
    ctx: &AuthContext<DB>,
) -> AuthResult<AuthResponse> {
    let (user, _session) = require_session(req, ctx).await?;
    let body: RejectInvitationRequest = match athena_auth_core::validate_request_body(req) {
        Ok(v) => v,
        Err(resp) => return Ok(resp),
    };
    let response = reject_invitation_core(&body, &user, ctx).await?;
    Ok(AuthResponse::json(200, &response)?)
}

/// Handle cancel invitation request
pub async fn handle_cancel_invitation<DB: DatabaseAdapter>(
    req: &AuthRequest,
    ctx: &AuthContext<DB>,
    config: &OrganizationConfig,
) -> AuthResult<AuthResponse> {
    let (user, _session) = require_session(req, ctx).await?;
    let body: CancelInvitationRequest = match athena_auth_core::validate_request_body(req) {
        Ok(v) => v,
        Err(resp) => return Ok(resp),
    };
    let response = cancel_invitation_core(&body, &user, config, ctx).await?;
    Ok(AuthResponse::json(200, &response)?)
}

/// Helper function to parse query parameters into a struct
fn parse_query<T: Default + serde::de::DeserializeOwned>(
    query: &std::collections::HashMap<String, String>,
) -> T {
    let json_value =
        serde_json::to_value(query).unwrap_or(serde_json::Value::Object(Default::default()));
    serde_json::from_value(json_value).unwrap_or_default()
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::plugins::test_helpers;
    use async_trait::async_trait;
    use athena_auth_core::{
        CreateEmailTemplate, CreateInvitation, CreateOrganization, CreateUser, EmailProvider,
        InvitationOps, OrganizationOps, VerificationOps,
    };
    use chrono::{Duration, Utc};
    use std::sync::{Arc, Mutex};

    type SentEmailRecord = (String, String, String, String);
    type SentEmailSink = Arc<Mutex<Vec<SentEmailRecord>>>;

    struct RecordingEmailProvider {
        sent: SentEmailSink,
    }

    #[async_trait]
    impl EmailProvider for RecordingEmailProvider {
        async fn send(
            &self,
            to: &str,
            subject: &str,
            html: &str,
            text: &str,
        ) -> athena_auth_core::AuthResult<()> {
            self.sent.lock().unwrap().push((
                to.to_string(),
                subject.to_string(),
                html.to_string(),
                text.to_string(),
            ));
            Ok(())
        }
    }

    #[tokio::test]
    async fn test_send_invitation_email_resolves_user_and_organization_bindings() {
        let sent: SentEmailSink = Arc::new(Mutex::new(Vec::new()));
        let mut auth_config = test_helpers::create_test_config();
        auth_config.email_provider = Some(Arc::new(RecordingEmailProvider { sent: sent.clone() }));
        let ctx = test_helpers::create_test_context_with_config(auth_config);

        let inviter = test_helpers::create_user(
            &ctx,
            CreateUser::new()
                .with_email("inviter@test.com")
                .with_name("Inviter Person"),
        )
        .await;
        let organization = ctx
            .database
            .create_organization(CreateOrganization::new("Bound Org", "bound-org"))
            .await
            .expect("organization should be created");
        let invitation = ctx
            .database
            .create_invitation(CreateInvitation::new(
                organization.id().to_string(),
                "invitee@test.com",
                "member",
                inviter.id().to_string(),
                Utc::now() + Duration::hours(24),
            ))
            .await
            .expect("invitation should be created");

        ctx.database
            .create_email_template(CreateEmailTemplate {
                template_key: "organization_invitation_email".to_string(),
                event_type: Some("organization.member.invite".to_string()),
                locale: Some("en".to_string()),
                subject_template: "Invite {{organization_slug}}".to_string(),
                text_template: Some(
                    "slug={{organization_slug}} inviter={{inviter_user_id}} url={{invitation_url}}"
                        .to_string(),
                ),
                html_template: Some(
                    "<p>{{organization_slug}} {{inviter_user_id}} {{invitation_url}}</p>"
                        .to_string(),
                ),
                variables: Some(serde_json::json!(["invitation_url"])),
                variable_bindings: Some(serde_json::json!([
                    {
                        "name": "organization_slug",
                        "source": "athena.organizations.slug",
                        "required": true
                    },
                    {
                        "name": "inviter_user_id",
                        "source": "athena.users.id",
                        "required": true
                    }
                ])),
                attachments: None,
                attachment_failure_mode: None,
                is_active: Some(true),
                metadata: None,
            })
            .await
            .expect("organization invitation template should be created");

        let body = InviteMemberRequest {
            email: "invitee@test.com".to_string(),
            role: "member".to_string(),
            organization_id: Some(organization.id().to_string()),
            callback_url: None,
        };

        send_invitation_email(
            &body,
            &invitation,
            &inviter,
            &OrganizationConfig::default(),
            &ctx,
        )
        .await
        .expect("invitation email should be sent");

        let messages = sent.lock().unwrap();
        assert_eq!(messages.len(), 1);
        assert_eq!(messages[0].0, "invitee@test.com");
        assert!(messages[0].1.contains("bound-org"));
        assert!(messages[0].2.contains(inviter.id()));
        assert!(messages[0].3.contains(inviter.id()));
    }
}
