//! Integration-level security regression tests.
//!
//! These tests exercise the full Axum router using `tower::ServiceExt::oneshot`.
//! No real TCP connection is made — the router is called in-process.
//!
//! # Coverage
//!
//! | ID      | What                                    | Covered here |
//! |---------|-----------------------------------------|-------------|
//! | SEC-001 | No auth header → 401                    | ✓ |
//! | SEC-002 | Wrong token → 403                       | ✓ |
//! | SEC-003 | Disabled key → 403                      | ✓ |
//! | SEC-004 | CR/LF in subject → 400                  | validation::tests |
//! | SEC-005 | CR/LF in from_name → 400                | validation::tests |
//! | SEC-006 | CR/LF in reply_to → 400                 | validation::tests |
//! | SEC-007 | CR/LF in to → 400                       | validation::tests |
//! | SEC-008 | Unknown field "from" → 400              | ✓ |
//! | SEC-009 | Unknown field "bcc" → 400               | ✓ |
//! | SEC-010 | Unknown field "headers" → 400           | ✓ |
//! | SEC-011 | Body too large → 413                    | ✓ |
//! | SEC-012 | Disallowed domain → 400                 | validation::tests |
//! | SEC-013 | Rate limit exceeded → 429               | rate_limit::tests |
//! | SEC-014 | Forged X-Forwarded-For from untrusted   | auth::tests (unit) |
//! | SEC-015 | Auth log has no token value             | structural (no log sink in unit) |
//! | SEC-016 | Send log has no body value              | structural (skip(payload) enforced) |
//! | SEC-017 | SecretString Debug is redacted          | validation::tests + config::tests |

use axum::{
    body::Body,
    http::{header, Request, StatusCode},
};
use serde_json::{json, Value};
use tower::ServiceExt;

use crate::{
    api,
    config::{
        ApiKeyConfig, AppConfig, LoggingConfig, MailConfig, RateLimitConfig, SecretString,
        SecurityConfig, ServerConfig, SmtpConfig,
    },
    AppState,
};

// ---------------------------------------------------------------------------
// Test helpers
// ---------------------------------------------------------------------------

/// Build a minimal, fully-functional AppConfig for tests.
///
/// Uses port 1 for SMTP — connections will always fail, but the transport
/// object is constructed without error (no connection at init time).
fn test_config() -> AppConfig {
    AppConfig {
        server: ServerConfig {
            bind_address: "127.0.0.1:0".into(),
            max_request_body_bytes: 256,  // intentionally small for SEC-011
            request_timeout_seconds: 5,
            shutdown_timeout_seconds: 5,
            concurrency_limit: 0,
            monitoring_cidrs: vec!["127.0.0.1/32".into()],
            tls_cert: None,
            tls_key: None,
        },
        security: SecurityConfig {
            trust_proxy_headers: false,
            trusted_source_cidrs: vec![],
            allowed_source_cidrs: vec![],
            api_keys: vec![
                ApiKeyConfig {
                    id: "enabled-key".into(),
                    secret: SecretString::new("valid-secret-padded-to-32bytes-xx"),
                    enabled: true,
                    description: None,
                    allowed_recipient_domains: vec!["example.com".into()],
                    rate_limit_per_min: None,
                    allowed_recipients: vec![],
                    burst: 0,
                    mask_recipient: None,
                },
                ApiKeyConfig {
                    id: "disabled-key".into(),
                    secret: SecretString::new("disabled-secret"),
                    enabled: false,
                    description: None,
                    allowed_recipient_domains: vec![],
                    rate_limit_per_min: None,
                    allowed_recipients: vec![],
                    burst: 0,
                    mask_recipient: None,
                },
            ],
        },
        mail: MailConfig {
            default_from: "relay@example.com".into(),
            default_from_name: None,
            allowed_recipient_domains: vec!["example.com".into()],
            max_subject_chars: 255,
            max_body_bytes: 200,  // intentionally small for SEC-011
            max_recipients: 10,
            max_attachments: 5,
            max_attachment_bytes: 10 * 1024 * 1024,
            max_bulk_messages: 10,
            allow_html_body: true,
            allow_attachments: true,
            allow_bulk_send: true,
            max_total_attachment_bytes: None,
        },
        smtp: SmtpConfig {
            mode: "smtp".into(),
            host: "127.0.0.1".into(),
            port: 1,  // no listener — SMTP submit will fail, but that's after validation
            connect_timeout_seconds: 1,
            submission_timeout_seconds: 1,
            auth_user: None,
            auth_password: None,
            pipe_command: "/usr/sbin/sendmail".into(),
            tls: "none".into(),
            bulk_concurrency: 5,
        },
        rate_limit: RateLimitConfig {
            global_per_min: 60,
            per_ip_per_min: 20,
            per_key_per_min: 30,
            global_burst: 5,
            per_ip_burst: 5,
            per_key_burst: 5,
            burst_size: 0,
            ip_table_size: 100,
        },
        logging: LoggingConfig {
            format: "text".into(),
            level: "error".into(),  // suppress output during tests
            mask_recipient: true,
        },
        status: Default::default(),
    }
}

fn test_router() -> axum::Router {
    let state = AppState::new(test_config());
    api::build_router(state)
}

/// POST /v1/send with a full valid auth header and JSON body.
async fn send_request(
    router: &axum::Router,
    auth: Option<&str>,
    body: Value,
) -> (StatusCode, Value) {
    let mut builder = Request::builder()
        .method("POST")
        .uri("/v1/send")
        .header(header::CONTENT_TYPE, "application/json");
    if let Some(token) = auth {
        builder = builder.header(header::AUTHORIZATION, format!("Bearer {token}"));
    }
    let req = builder.body(Body::from(body.to_string())).unwrap();
    let resp = router.clone().oneshot(req).await.unwrap();
    let status = resp.status();
    let bytes = axum::body::to_bytes(resp.into_body(), 8192).await.unwrap();
    let json: Value = serde_json::from_slice(&bytes).unwrap_or(json!({}));
    (status, json)
}

fn valid_body() -> Value {
    json!({
        "to": "user@example.com",
        "subject": "Test",
        "body": "Hello."
    })
}

// ---------------------------------------------------------------------------
// SEC-001: No Authorization header → 401 unauthorized
// ---------------------------------------------------------------------------

#[tokio::test]
async fn sec_001_no_auth_header_returns_401() {
    let router = test_router();
    let (status, body) = send_request(&router, None, valid_body()).await;
    assert_eq!(status, StatusCode::UNAUTHORIZED, "body={body}");
    assert_eq!(body["code"], "unauthorized");
}

// ---------------------------------------------------------------------------
// SEC-002: Wrong token → 403 forbidden
// ---------------------------------------------------------------------------

#[tokio::test]
async fn sec_002_wrong_token_returns_403() {
    let router = test_router();
    let (status, body) = send_request(&router, Some("completely-wrong"), valid_body()).await;
    assert_eq!(status, StatusCode::FORBIDDEN, "body={body}");
    assert_eq!(body["code"], "forbidden");
}

// ---------------------------------------------------------------------------
// SEC-003: Disabled key with correct secret → 403 (not 200)
// ---------------------------------------------------------------------------

#[tokio::test]
async fn sec_003_disabled_key_returns_403() {
    let router = test_router();
    let (status, body) = send_request(&router, Some("disabled-secret"), valid_body()).await;
    assert_eq!(status, StatusCode::FORBIDDEN, "body={body}");
    assert_eq!(body["code"], "forbidden");
}

// ---------------------------------------------------------------------------
// SEC-008: Unknown field "from" → 400 validation_failed
// ---------------------------------------------------------------------------

#[tokio::test]
async fn sec_008_unknown_field_from_rejected() {
    let router = test_router();
    let bad = json!({
        "to": "user@example.com",
        "subject": "Test",
        "body": "Hello.",
        "from": "evil@evil.com"
    });
    let (status, body) = send_request(&router, Some("valid-secret-padded-to-32bytes-xx"), bad).await;
    assert_eq!(status, StatusCode::BAD_REQUEST, "body={body}");
}

// ---------------------------------------------------------------------------
// SEC-009: Unknown field "bcc" → 400 / 422
// ---------------------------------------------------------------------------

#[tokio::test]
async fn sec_009_unknown_field_bcc_rejected() {
    let router = test_router();
    let bad = json!({
        "to": "user@example.com",
        "subject": "Test",
        "body": "Hello.",
        "bcc": "spy@evil.com"
    });
    let (status, _) = send_request(&router, Some("valid-secret-padded-to-32bytes-xx"), bad).await;
    assert!(
        status == StatusCode::BAD_REQUEST || status == StatusCode::BAD_REQUEST,
        "expected 422 or 400, got {status}"
    );
}

// ---------------------------------------------------------------------------
// SEC-010: Unknown field "headers" → 400 / 422
// ---------------------------------------------------------------------------

#[tokio::test]
async fn sec_010_unknown_field_headers_rejected() {
    let router = test_router();
    let bad = json!({
        "to": "user@example.com",
        "subject": "Test",
        "body": "Hello.",
        "headers": {"X-Custom": "injected"}
    });
    let (status, _) = send_request(&router, Some("valid-secret-padded-to-32bytes-xx"), bad).await;
    assert!(
        status == StatusCode::BAD_REQUEST || status == StatusCode::BAD_REQUEST,
        "expected 422 or 400, got {status}"
    );
}

// ---------------------------------------------------------------------------
// SEC-011: Body exceeding max_request_body_bytes → 413
// ---------------------------------------------------------------------------

#[tokio::test]
async fn sec_011_oversized_request_body_returns_413() {
    let router = test_router();
    // test_config sets max_request_body_bytes = 256
    let giant = "x".repeat(300);
    let req = Request::builder()
        .method("POST")
        .uri("/v1/send")
        .header(header::CONTENT_TYPE, "application/json")
        .header(header::AUTHORIZATION, "Bearer valid-secret-padded-to-32bytes-xx")
        .body(Body::from(giant))
        .unwrap();
    let resp = router.oneshot(req).await.unwrap();
    assert_eq!(resp.status(), StatusCode::PAYLOAD_TOO_LARGE);
}

// ---------------------------------------------------------------------------
// Structural: From is always from config (mail::tests also cover this)
// ---------------------------------------------------------------------------

#[tokio::test]
async fn from_address_cannot_be_overridden_via_extra_field() {
    // The "from" field is rejected by deny_unknown_fields (SEC-008).
    // This test double-checks that even with a crafted payload structure,
    // the router does not accept it.
    let router = test_router();
    let with_from = json!({
        "to": "user@example.com",
        "subject": "Hi",
        "body": "Text.",
        "from": "spoofed@attacker.com"
    });
    let (status, _) = send_request(&router, Some("valid-secret-padded-to-32bytes-xx"), with_from).await;
    assert_ne!(
        status,
        StatusCode::ACCEPTED,
        "A request with a 'from' field must never result in 202 Accepted"
    );
}

// --- Tests from src/validation.rs ---
#[cfg(test)]
mod validation_tests {
    use crate::{
        auth::AuthContext,
        config::{
            ApiKeyConfig, AppConfig, LoggingConfig, MailConfig, RateLimitConfig, SecretString,
            SecurityConfig, ServerConfig, SmtpConfig,
        },
        error::AppError,
        validation::{validate_mail_request, MailRequest},
    };
    use std::net::IpAddr;

    fn make_auth(key_id: &str) -> AuthContext {
        AuthContext {
            key_id: key_id.to_string(),
            client_ip: IpAddr::V4(std::net::Ipv4Addr::LOCALHOST),
            key_rate_limit_per_min: None,
            key_burst: 0,
        }
    }

    fn minimal_config() -> AppConfig {
        AppConfig {
            server: ServerConfig {
                bind_address: "127.0.0.1:8080".into(),
                max_request_body_bytes: 65536,
                request_timeout_seconds: 30,
                shutdown_timeout_seconds: 30,
                concurrency_limit: 0,
            monitoring_cidrs: vec!["127.0.0.1/32".into()],
                tls_cert: None,
                tls_key: None,
            },
            security: SecurityConfig {
                trust_proxy_headers: false,
                trusted_source_cidrs: vec![],
                    allowed_source_cidrs: vec![],
                api_keys: vec![ApiKeyConfig {
                    id: "test-key".into(),
                    secret: SecretString::new("tok"),
                    enabled: true,
                    description: None,
                    allowed_recipient_domains: vec![],
                    allowed_recipients: vec![],
                    rate_limit_per_min: None,
                    burst: 0,
                    mask_recipient: None,
                }],
            },
            mail: MailConfig {
                default_from: "relay@example.com".into(),
                default_from_name: None,
                allowed_recipient_domains: vec![],
                max_subject_chars: 200,
                max_body_bytes: 1_000_000,
                max_recipients: 10,
                max_attachments: 5,
                max_attachment_bytes: 10 * 1024 * 1024,
            max_bulk_messages: 10,
            allow_html_body: true,
            allow_attachments: true,
            allow_bulk_send: true,
            max_total_attachment_bytes: None,
            },
            smtp: SmtpConfig {
                mode: "smtp".into(),
                host: "127.0.0.1".into(),
                port: 25,
                connect_timeout_seconds: 5,
                submission_timeout_seconds: 30,
                auth_user: None,
                auth_password: None,
                pipe_command: "/usr/sbin/sendmail".into(),
                tls: "none".into(),
                bulk_concurrency: 5,
            },
            rate_limit: RateLimitConfig {
                global_per_min: 60,
                per_ip_per_min: 20,
                per_key_per_min: 30,
                global_burst: 5,
                per_ip_burst: 5,
                per_key_burst: 5,
                burst_size: 0,
                ip_table_size: 100,
            },
            logging: LoggingConfig {
                format: "text".into(),
                level: "info".into(),
                mask_recipient: false,
            },
            status: Default::default(),
        }
    }

    fn minimal_request() -> MailRequest {
        MailRequest {
            to: crate::validation::Recipients(vec!["user@example.com".into()]),
            subject: "Hello".into(),
            body: "Test body".into(),
            from_name: None,
            reply_to: None,
            body_html: None,
            cc: None,
            attachments: None,
            metadata: None,
        }
    }

    #[test]
    fn valid_request_passes() {
        let cfg = minimal_config();
        let auth = make_auth("test-key");
        let req = minimal_request();
        assert!(validate_mail_request(req, &cfg, &auth).is_ok());
    }

    #[test]
    fn invalid_email_rejected() {
        let cfg = minimal_config();
        let auth = make_auth("test-key");
        let req = MailRequest {
            to: crate::validation::Recipients(vec!["not-an-email".into()]),
            ..minimal_request()
        };
        assert!(matches!(
            validate_mail_request(req, &cfg, &auth),
            Err(AppError::Validation(_))
        ));
    }

    #[test]
    fn crlf_in_subject_rejected() {
        let cfg = minimal_config();
        let auth = make_auth("test-key");
        let req = MailRequest {
            subject: "Hello\r\nBcc: evil@x.com".into(),
            ..minimal_request()
        };
        assert!(matches!(
            validate_mail_request(req, &cfg, &auth),
            Err(AppError::Validation(_))
        ));
    }

    #[test]
    fn empty_subject_rejected() {
        let cfg = minimal_config();
        let auth = make_auth("test-key");
        let req = MailRequest {
            subject: "   ".into(),
            ..minimal_request()
        };
        assert!(matches!(
            validate_mail_request(req, &cfg, &auth),
            Err(AppError::Validation(_))
        ));
    }

    #[test]
    fn oversized_subject_rejected() {
        let cfg = minimal_config();
        let auth = make_auth("test-key");
        let req = MailRequest {
            subject: "a".repeat(201),
            ..minimal_request()
        };
        assert!(matches!(
            validate_mail_request(req, &cfg, &auth),
            Err(AppError::Validation(_))
        ));
    }

    #[test]
    fn nul_in_body_rejected() {
        let cfg = minimal_config();
        let auth = make_auth("test-key");
        let req = MailRequest {
            body: "Hello\0world".into(),
            ..minimal_request()
        };
        assert!(matches!(
            validate_mail_request(req, &cfg, &auth),
            Err(AppError::Validation(_))
        ));
    }

    #[test]
    fn crlf_in_from_name_rejected() {
        let cfg = minimal_config();
        let auth = make_auth("test-key");
        let req = MailRequest {
            from_name: Some("Evil\r\nBcc: attacker@evil.com".into()),
            ..minimal_request()
        };
        assert!(matches!(
            validate_mail_request(req, &cfg, &auth),
            Err(AppError::Validation(_))
        ));
    }

    #[test]
    fn disallowed_domain_rejected() {
        let mut cfg = minimal_config();
        cfg.mail.allowed_recipient_domains = vec!["allowed.com".into()];
        let auth = make_auth("test-key");
        let req = MailRequest {
            to: crate::validation::Recipients(vec!["user@other.com".into()]),
            ..minimal_request()
        };
        assert!(matches!(
            validate_mail_request(req, &cfg, &auth),
            Err(AppError::Validation(_))
        ));
    }

    #[test]
    fn allowed_domain_passes() {
        let mut cfg = minimal_config();
        cfg.mail.allowed_recipient_domains = vec!["example.com".into()];
        let auth = make_auth("test-key");
        let req = minimal_request(); // to = user@example.com
        assert!(validate_mail_request(req, &cfg, &auth).is_ok());
    }

    #[test]
    fn per_key_domain_restriction_works() {
        let mut cfg = minimal_config();
        cfg.security.api_keys[0].allowed_recipient_domains = vec!["allowed.com".into()];
        let auth = make_auth("test-key");
        let req = minimal_request(); // to = user@example.com (not allowed)
        assert!(matches!(
            validate_mail_request(req, &cfg, &auth),
            Err(AppError::Validation(_))
        ));
    }

    #[test]
    fn metadata_client_request_id_extracted() {
        let cfg = minimal_config();
        let auth = make_auth("test-key");
        let req = MailRequest {
            metadata: Some(serde_json::json!({"request_id": "client-123"})),
            ..minimal_request()
        };
        let v = validate_mail_request(req, &cfg, &auth).unwrap();
        assert_eq!(v.client_request_id.as_deref(), Some("client-123"));
    }

    /// SEC-006: CR/LF in `reply_to` is rejected before SMTP.
    #[test]
    fn crlf_in_reply_to_rejected() {
        let cfg = minimal_config();
        let auth = make_auth("test-key");
        for bad in &[
            "user@example.com
Bcc: evil@evil.com",
            "user@example.com
X-Header: injected",
        ] {
            let req = MailRequest {
                reply_to: Some(crate::validation::Recipients(vec![bad.to_string()])),
                ..minimal_request()
            };
            assert!(
                matches!(validate_mail_request(req, &cfg, &auth), Err(AppError::Validation(_))),
                "expected Validation error for reply_to={bad:?}"
            );
        }
    }

    /// SEC-007: CR/LF in `to` is rejected before SMTP.
    #[test]
    fn crlf_in_to_rejected() {
        let cfg = minimal_config();
        let auth = make_auth("test-key");
        let req = MailRequest {
            to: crate::validation::Recipients(vec!["user@example.com\nBcc: attacker@evil.com".to_string()]),
            ..minimal_request()
        };
        assert!(matches!(
            validate_mail_request(req, &cfg, &auth),
            Err(AppError::Validation(_))
        ));
    }

    /// SEC-017 (unit): SecretString never exposes its value through Debug.
    #[test]
    fn secret_string_redacted_in_debug() {
        use crate::config::SecretString;
        let s = SecretString::new("super-secret-token-value");
        let debug = format!("{s:?}");
        assert!(
            !debug.contains("super-secret-token-value"),
            "SecretString Debug must not expose secret; got: {debug}"
        );
    }
}


