use std::{env, str::FromStr, time::Duration};

use chrono::Utc;
use defguard_common::{
    config::{DefGuardConfig, SERVER_CONFIG},
    db::{
        models::{
            MFAMethod, Settings,
            settings::{
                initialize_current_settings, set_settings,
                smtp::{SmtpAuthentication, SmtpEncryption},
            },
        },
        setup_pool,
    },
    secret::SecretStringWrapper,
};
use reqwest::Url;
use sqlx::{
    PgPool,
    postgres::{PgConnectOptions, PgPoolOptions},
};
use tera::Context;
use tokio::time::sleep;

use super::{
    templates, {Attachment, MailMessage},
};

#[test]
fn dg25_8_server_side_template_injection() {
    let mut tera = templates::safe_tera();
    tera.add_raw_template("text", "PATH={{ get_env(name=\"PATH\") }}")
        .unwrap();
    assert!(tera.render("text", &Context::new()).is_err());
}

/// Delay, so send_and_forget() can process the message.
async fn delay() {
    sleep(Duration::from_secs(2)).await;
}

/// Set SMTP settings from environment variables.
async fn set_smtp_settings(pool: &PgPool) {
    let config = DefGuardConfig::new_test_config();
    let _ = SERVER_CONFIG.set(config);
    initialize_current_settings(pool).await.unwrap();

    let mut settings = Settings::get_current_settings();
    settings.smtp.server = env::var("SMTP_SERVER").ok();
    settings.smtp.port = Some(env::var("SMTP_PORT").map_or(587, |s| s.parse().unwrap()));

    if let Ok(refresh_token) = env::var("SMTP_OAUTH_REFRESH_TOKEN") {
        settings.smtp.oauth_issuer_url = env::var("SMTP_OAUTH_ISSUER_URL").ok();
        settings.smtp.oauth_client_id = env::var("SMTP_OAUTH_CLIENT_ID").ok();
        settings.smtp.oauth_client_secret = Some(
            SecretStringWrapper::from_str(&env::var("SMTP_OAUTH_CLIENT_SECRET").unwrap()).unwrap(),
        );
        settings.smtp.oauth_refresh_token = Some(refresh_token);
        settings.smtp.authentication = SmtpAuthentication::XOAuth2;
    } else {
        settings.smtp.user = env::var("SMTP_USER").ok();
        settings.smtp.password =
            Some(SecretStringWrapper::from_str(&env::var("SMTP_PASSWORD").unwrap()).unwrap());
        settings.smtp.encryption = SmtpEncryption::StartTls;
        settings.smtp.authentication = SmtpAuthentication::Login;
    }
    settings.smtp.sender = env::var("SMTP_FROM").ok();
    set_settings(Some(settings));
}

#[ignore = "requires SMTP server"]
#[sqlx::test]
fn send_desktop_start(_: PgPoolOptions, options: PgConnectOptions) {
    let pool = setup_pool(options).await;
    set_smtp_settings(&pool).await;

    let mut conn = pool.begin().await.unwrap();
    let context = Context::new();
    let url = Url::parse("http://localhost:8001").unwrap();
    let token = "zXc6N1ndXpWFeyBuogiFp1bD1UomAbZc";
    templates::desktop_start_mail(
        &env::var("SMTP_TO").unwrap(),
        &mut conn,
        context,
        &url,
        token,
    )
    .await
    .unwrap();

    delay().await;
}

#[ignore = "requires SMTP server"]
#[sqlx::test]
fn send_new_device_added(_: PgPoolOptions, options: PgConnectOptions) {
    let pool = setup_pool(options).await;
    set_smtp_settings(&pool).await;

    let mut conn = pool.begin().await.unwrap();
    let device_name = "My beloved machine";
    let public_key = "6N8h7HILMcQ6nqEfQMBAYQH26X+y3t/WdWSOW4bNNxw=";
    let locations = &[
        templates::TemplateLocation {
            name: String::from("Location 1"),
            assigned_ips: String::from("192.168.1.42"),
        },
        templates::TemplateLocation {
            name: String::from("Location 2"),
            assigned_ips: String::from("192.168.2.69"),
        },
    ];
    templates::new_device_added_mail(
        &env::var("SMTP_TO").unwrap(),
        &mut conn,
        device_name,
        public_key,
        locations,
        Some("1.2.3.4"),
        Some("unknown device"),
    )
    .await
    .unwrap();

    delay().await;
}

#[ignore = "requires SMTP server"]
#[sqlx::test]
fn send_mfa_code(_: PgPoolOptions, options: PgConnectOptions) {
    let pool = setup_pool(options).await;
    set_smtp_settings(&pool).await;

    let mut conn = pool.begin().await.unwrap();
    let first_name = "Nebuchadnezzar";
    let code = "123456";
    templates::mfa_code_mail(
        &env::var("SMTP_TO").unwrap(),
        &mut conn,
        first_name,
        code,
        None,
    )
    .await
    .unwrap();

    delay().await;
}

#[ignore = "requires SMTP server"]
#[sqlx::test]
fn send_new_account(_: PgPoolOptions, options: PgConnectOptions) {
    let pool = setup_pool(options).await;
    set_smtp_settings(&pool).await;

    let mut conn = pool.begin().await.unwrap();
    let url = Url::parse("http://localhost:8001").unwrap();
    let context = Context::new();
    let token = "zXc6N1ndXpWFeyBuogiFp1bD1UomAbZc";
    templates::new_account_mail(
        &env::var("SMTP_TO").unwrap(),
        &mut conn,
        context,
        url,
        token,
    )
    .await
    .unwrap();

    delay().await;
}

#[ignore = "requires SMTP server"]
#[sqlx::test]
fn send_mfa_activation(_: PgPoolOptions, options: PgConnectOptions) {
    let pool = setup_pool(options).await;
    set_smtp_settings(&pool).await;

    let mut conn = pool.begin().await.unwrap();
    let first_name = "Nebuchadnezzar";
    let code = "123456";
    templates::mfa_activation_mail(
        &env::var("SMTP_TO").unwrap(),
        &mut conn,
        first_name,
        code,
        None,
    )
    .await
    .unwrap();

    delay().await;
}

#[ignore = "requires SMTP server"]
#[sqlx::test]
fn send_enrollment_admin_notification(_: PgPoolOptions, options: PgConnectOptions) {
    let pool = setup_pool(options).await;
    set_smtp_settings(&pool).await;

    let mut conn = pool.begin().await.unwrap();
    let user_name = "Nebuchadnezzar the Great";
    let admin_name = "Nabopolassar the Admin";
    let ip_address = "1.2.3.4";
    templates::enrollment_admin_notification(
        &env::var("SMTP_TO").unwrap(),
        &mut conn,
        user_name,
        admin_name,
        ip_address,
        None,
    )
    .await
    .unwrap();

    delay().await;
}

#[ignore = "requires SMTP server"]
#[sqlx::test]
fn send_gateway_disconnected_mail(_: PgPoolOptions, options: PgConnectOptions) {
    let pool = setup_pool(options).await;
    set_smtp_settings(&pool).await;

    let mut conn = pool.begin().await.unwrap();
    let gateway_name = "Portal";
    let ip_address = "1.2.3.4";
    let location_name = "Somewhere";
    templates::gateway_disconnected_mail(
        &env::var("SMTP_TO").unwrap(),
        &mut conn,
        gateway_name,
        ip_address,
        location_name,
    )
    .await
    .unwrap();

    delay().await;
}

#[ignore = "requires SMTP server"]
#[sqlx::test]
fn send_gateway_reconnected_mail(_: PgPoolOptions, options: PgConnectOptions) {
    let pool = setup_pool(options).await;
    set_smtp_settings(&pool).await;

    let mut conn = pool.begin().await.unwrap();
    let gateway_name = "Portal";
    let ip_address = "1.2.3.4";
    let location_name = "Somewhere";
    templates::gateway_reconnected_mail(
        &env::var("SMTP_TO").unwrap(),
        &mut conn,
        gateway_name,
        ip_address,
        location_name,
    )
    .await
    .unwrap();

    delay().await;
}

#[ignore = "requires SMTP server"]
#[sqlx::test]
fn send_mfa_configured_mail(_: PgPoolOptions, options: PgConnectOptions) {
    let pool = setup_pool(options).await;
    set_smtp_settings(&pool).await;

    let mut conn = pool.begin().await.unwrap();
    templates::mfa_configured_mail(
        &env::var("SMTP_TO").unwrap(),
        &mut conn,
        None,
        &MFAMethod::Email,
        "Test",
    )
    .await
    .unwrap();

    delay().await;
}

#[ignore = "requires SMTP server"]
#[sqlx::test]
fn send_new_device_login_mail(_: PgPoolOptions, options: PgConnectOptions) {
    let pool = setup_pool(options).await;
    set_smtp_settings(&pool).await;

    let mut conn = pool.begin().await.unwrap();
    let created = Utc::now().naive_utc();
    templates::new_device_login_mail(&env::var("SMTP_TO").unwrap(), &mut conn, None, created)
        .await
        .unwrap();

    delay().await;
}

#[ignore = "requires SMTP server"]
#[sqlx::test]
fn send_new_device_oidc_login_mail(_: PgPoolOptions, options: PgConnectOptions) {
    let pool = setup_pool(options).await;
    set_smtp_settings(&pool).await;

    let mut conn = pool.begin().await.unwrap();
    let client_name = "RemoteApp";
    let username = "testuser";
    templates::new_device_oidc_login_mail(
        &env::var("SMTP_TO").unwrap(),
        &mut conn,
        None,
        client_name,
        username,
    )
    .await
    .unwrap();

    delay().await;
}

#[ignore = "requires SMTP server"]
#[sqlx::test]
fn send_password_reset_mail(_: PgPoolOptions, options: PgConnectOptions) {
    let pool = setup_pool(options).await;
    set_smtp_settings(&pool).await;

    let mut conn = pool.begin().await.unwrap();
    let proxy_url = Url::parse("http://localhost:8000").unwrap();
    let token = "blablabla";
    templates::password_reset_mail(
        &env::var("SMTP_TO").unwrap(),
        &mut conn,
        proxy_url,
        token,
        None,
        None,
    )
    .await
    .unwrap();

    delay().await;
}

#[ignore = "requires SMTP server"]
#[sqlx::test]
fn send_password_reset_success_mail(_: PgPoolOptions, options: PgConnectOptions) {
    let pool = setup_pool(options).await;
    set_smtp_settings(&pool).await;

    let mut conn = pool.begin().await.unwrap();
    templates::password_reset_success_mail(&env::var("SMTP_TO").unwrap(), &mut conn, None, None)
        .await
        .unwrap();

    delay().await;
}

#[ignore = "requires SMTP server"]
#[sqlx::test]
fn send_test_mail(_: PgPoolOptions, options: PgConnectOptions) {
    let pool = setup_pool(options).await;
    set_smtp_settings(&pool).await;

    let mut conn = pool.begin().await.unwrap();
    templates::test_mail(&env::var("SMTP_TO").unwrap(), &mut conn, None)
        .await
        .unwrap();

    delay().await;
}

#[ignore = "requires SMTP server"]
#[sqlx::test]
fn send_support_data_mail(_: PgPoolOptions, options: PgConnectOptions) {
    let pool = setup_pool(options).await;
    set_smtp_settings(&pool).await;

    let mut conn = pool.begin().await.unwrap();
    let config = Attachment::new(
        "defguard-support-data-test.json".into(),
        b"{\"key\":\"value\"}".into(),
    );
    templates::support_data_mail(&env::var("SMTP_TO").unwrap(), &mut conn, vec![config])
        .await
        .unwrap();

    delay().await;
}

#[ignore = "requires SMTP server"]
#[sqlx::test]
fn send_enrollment_welcome_mail(_: PgPoolOptions, options: PgConnectOptions) {
    let pool = setup_pool(options).await;
    set_smtp_settings(&pool).await;

    let markdown = "Paragraph **bold** _italic_.";
    templates::enrollment_welcome_mail(&env::var("SMTP_TO").unwrap(), markdown, None, None)
        .unwrap();

    delay().await;
}

#[ignore = "requires SMTP server"]
#[sqlx::test]
fn send_certificate_expiration(_: PgPoolOptions, options: PgConnectOptions) {
    let pool = setup_pool(options).await;
    set_smtp_settings(&pool).await;

    let expiration = Utc::now().naive_utc();
    let mut conn = pool.begin().await.unwrap();
    templates::certificate_expiration_mail(
        &env::var("SMTP_TO").unwrap(),
        &mut conn,
        "Dummy",
        expiration,
    )
    .await
    .unwrap();

    delay().await;
}

#[ignore = "requires SMTP server"]
#[sqlx::test]
fn send_certificate_expired(_: PgPoolOptions, options: PgConnectOptions) {
    let pool = setup_pool(options).await;
    set_smtp_settings(&pool).await;

    let expiration = Utc::now().naive_utc();
    let mut conn = pool.begin().await.unwrap();
    templates::certificate_expired_mail(
        &env::var("SMTP_TO").unwrap(),
        &mut conn,
        "Dummy",
        expiration,
    )
    .await
    .unwrap();

    delay().await;
}

mod markdown_to_html {
    use super::templates::markdown_to_html;

    fn has_tag(html: &str, tag: &str) -> bool {
        html.contains(&format!("<{tag}"))
    }

    #[test]
    fn h1_has_inline_style() {
        let html = markdown_to_html("# Title");
        assert!(has_tag(&html, "h1"), "h1 tag missing: {html}");
        assert!(html.contains("font-size: 24px"), "h1 font-size: {html}");
        assert!(html.contains("#141517"), "h1 color: {html}");
        assert!(html.contains("font-weight: 600"), "h1 font-weight: {html}");
        assert!(html.contains("Title"), "h1 text missing: {html}");
    }

    #[test]
    fn h2_has_inline_style() {
        let html = markdown_to_html("## Subtitle");
        assert!(has_tag(&html, "h2"), "h2 tag missing: {html}");
        assert!(html.contains("font-size: 16px"), "h2 font-size: {html}");
        assert!(html.contains("#4A5059"), "h2 color: {html}");
        assert!(html.contains("Subtitle"), "h2 text missing: {html}");
    }

    #[test]
    fn paragraph_has_inline_style() {
        let html = markdown_to_html("Hello world");
        assert!(has_tag(&html, "p"), "p tag missing: {html}");
        assert!(html.contains("font-size: 14px"), "p font-size: {html}");
        assert!(html.contains("#4A5059"), "p color: {html}");
        assert!(html.contains("Hello world"), "p text missing: {html}");
    }

    #[test]
    fn bold_has_inline_style() {
        let html = markdown_to_html("**important**");
        assert!(has_tag(&html, "strong"), "strong tag missing: {html}");
        assert!(
            html.contains("font-weight: 600"),
            "strong font-weight: {html}"
        );
        assert!(html.contains("important"), "bold text missing: {html}");
    }

    #[test]
    fn link_has_inline_style_and_href() {
        let html = markdown_to_html("[click here](https://example.com)");
        assert!(has_tag(&html, "a"), "a tag missing: {html}");
        assert!(html.contains("https://example.com"), "href missing: {html}");
        assert!(html.contains("#3961DB"), "a color: {html}");
        assert!(html.contains("click here"), "link text missing: {html}");
    }

    #[test]
    fn unordered_list_has_inline_styles() {
        let html = markdown_to_html("- Alpha\n- Beta");
        assert!(has_tag(&html, "ul"), "ul tag missing: {html}");
        assert!(has_tag(&html, "li"), "li tag missing: {html}");
        assert!(html.contains("Alpha"), "first item text missing: {html}");
        assert!(html.contains("Beta"), "second item text missing: {html}");
        assert!(html.contains("font-size: 14px"), "li font-size: {html}");
    }

    #[test]
    fn h3_through_h6_are_demoted_to_h2() {
        for (level, marker) in [(3, "###"), (4, "####"), (5, "#####"), (6, "######")] {
            let html = markdown_to_html(&format!("{marker} Heading {level}"));
            assert!(
                has_tag(&html, "h2"),
                "h{level} must be demoted to h2: {html}"
            );
            assert!(
                !has_tag(&html, &format!("h{level}")),
                "h{level} must not appear: {html}"
            );
            assert!(
                html.contains(&format!("Heading {level}")),
                "h{level} text must be preserved: {html}"
            );
        }
    }

    #[test]
    fn horizontal_rule_is_stripped() {
        let html = markdown_to_html("---");
        assert!(!has_tag(&html, "hr"), "hr must be stripped: {html}");
    }

    #[test]
    fn blockquote_is_stripped() {
        let html = markdown_to_html("> confidential");
        assert!(
            !has_tag(&html, "blockquote"),
            "blockquote tag must be stripped: {html}"
        );
        assert!(
            !html.contains("confidential"),
            "blockquote content must not appear: {html}"
        );
    }

    #[test]
    fn ordered_list_is_stripped() {
        let html = markdown_to_html("1. First\n2. Second");
        assert!(!has_tag(&html, "ol"), "ol tag must be stripped: {html}");
        assert!(
            !html.contains("First"),
            "ordered list content must not appear: {html}"
        );
    }

    #[test]
    fn fenced_code_block_is_stripped() {
        let html = markdown_to_html("```rust\nlet x = 1;\n```");
        assert!(!has_tag(&html, "pre"), "pre tag must be stripped: {html}");
        assert!(!has_tag(&html, "code"), "code tag must be stripped: {html}");
        assert!(
            !html.contains("let x"),
            "code block content must not appear: {html}"
        );
    }

    #[test]
    fn indented_code_block_is_stripped() {
        let html = markdown_to_html("    indented block");
        assert!(!has_tag(&html, "pre"), "pre tag must be stripped: {html}");
        assert!(
            !html.contains("indented block"),
            "indented code block content must not appear: {html}"
        );
    }

    #[test]
    fn inline_code_rendered_as_plain_text() {
        let html = markdown_to_html("`fn main()`");
        assert!(!has_tag(&html, "code"), "code tag must be stripped: {html}");
        assert!(
            html.contains("fn main()"),
            "inline code text must be preserved: {html}"
        );
    }

    #[test]
    fn raw_block_html_is_stripped() {
        let html = markdown_to_html("<div class=\"x\">content</div>");
        assert!(
            !html.contains("<div"),
            "raw HTML div must be stripped: {html}"
        );
    }

    #[test]
    fn script_tag_is_stripped() {
        // <script> opens an HTML block consuming its whole line, so "Safe paragraph"
        // must be in a separate paragraph to survive stripping.
        let html = markdown_to_html("<script>alert('xss')</script>\n\nSafe paragraph.");
        assert!(
            !html.contains("<script"),
            "script tag must be stripped: {html}"
        );
        assert!(
            html.contains("Safe paragraph"),
            "surrounding text must survive: {html}"
        );
    }

    #[test]
    fn inline_html_is_stripped() {
        let html = markdown_to_html("before <b>inline</b> after");
        assert!(
            !html.contains("<b>"),
            "inline HTML b tag must be stripped: {html}"
        );
    }

    #[test]
    fn nested_unsupported_blocks_are_fully_stripped() {
        let html = markdown_to_html("> 1. nested item");
        assert!(
            !has_tag(&html, "blockquote"),
            "blockquote must be stripped: {html}"
        );
        assert!(!has_tag(&html, "ol"), "ol must be stripped: {html}");
        assert!(
            !html.contains("nested item"),
            "nested content must not appear: {html}"
        );
    }

    #[test]
    fn bold_and_italic_in_same_paragraph() {
        let html = markdown_to_html("**bold** and *italic* together");
        assert!(has_tag(&html, "strong"), "strong must be present: {html}");
        assert!(!has_tag(&html, "em"), "em must be stripped: {html}");
        assert!(html.contains("bold"), "bold text must survive: {html}");
        assert!(html.contains("italic"), "italic text must survive: {html}");
    }

    #[test]
    fn bold_inside_link() {
        let html = markdown_to_html("[**bold link**](https://example.com)");
        assert!(has_tag(&html, "a"), "a tag missing: {html}");
        assert!(has_tag(&html, "strong"), "strong inside a missing: {html}");
        assert!(html.contains("https://example.com"), "href missing: {html}");
        assert!(html.contains("bold link"), "link text missing: {html}");
    }

    #[test]
    fn multiple_paragraphs_are_each_styled() {
        let html = markdown_to_html("First paragraph.\n\nSecond paragraph.");
        assert!(
            html.matches("<p").count() >= 2,
            "expected at least two p tags: {html}"
        );
        assert!(html.contains("First paragraph"), "first p text: {html}");
        assert!(html.contains("Second paragraph"), "second p text: {html}");
    }

    #[test]
    fn hr_surrounded_by_paragraphs_is_stripped() {
        let html = markdown_to_html("Before.\n\n---\n\nAfter.");
        assert!(!has_tag(&html, "hr"), "hr must be stripped: {html}");
        assert!(
            html.contains("Before"),
            "text before hr must survive: {html}"
        );
        assert!(html.contains("After"), "text after hr must survive: {html}");
    }

    #[test]
    fn empty_input_produces_no_tags() {
        let html = markdown_to_html("");
        assert!(
            html.trim().is_empty(),
            "empty input must produce no output: {html}"
        );
    }

    #[test]
    fn plain_text_is_wrapped_in_paragraph() {
        let html = markdown_to_html("Just some text.");
        assert!(
            has_tag(&html, "p"),
            "plain text must be wrapped in p: {html}"
        );
        assert!(
            html.contains("Just some text"),
            "text must be preserved: {html}"
        );
    }
}

#[test]
fn test_mfa_configured_subject_totp() {
    // TOTP
    let subject = MailMessage::MFAConfigured {
        method: MFAMethod::OneTimePassword,
    }
    .subject();
    assert!(
        subject.contains("TOTP"),
        "MFAConfigured subject for OneTimePassword must mention \"TOTP\", got: {subject:?}"
    );

    // WebAuthn
    let subject = MailMessage::MFAConfigured {
        method: MFAMethod::Webauthn,
    }
    .subject();
    assert!(
        subject.contains("WebAuthn"),
        "MFAConfigured subject for Webauthn must mention \"WebAuthn\", got: {subject:?}"
    );

    // email
    let subject = MailMessage::MFAConfigured {
        method: MFAMethod::Email,
    }
    .subject();
    assert!(
        subject.contains("Email"),
        "MFAConfigured subject for Email must mention \"Email\", got: {subject:?}"
    );
}
