mod appendix;
mod attack_chain;
mod cover;
mod executive_summary;
mod findings;
mod scope;
mod styles;

use super::ReportContext;

#[allow(clippy::format_in_format_args)]
pub(super) fn build_html_report(ctx: &ReportContext) -> String {
    let session = &ctx.session;
    let session_id = session
        .id
        .map(|oid| oid.to_hex())
        .unwrap_or_else(|| "-".to_string());
    let date_str = session
        .started_at
        .format("%B %d, %Y at %H:%M UTC")
        .to_string();
    let date_short = session.started_at.format("%B %d, %Y").to_string();
    let completed_str = session
        .completed_at
        .map(|d| d.format("%B %d, %Y at %H:%M UTC").to_string())
        .unwrap_or_else(|| "In Progress".to_string());

    // Collect unique tool names used
    let tool_names: Vec<String> = {
        let mut names: Vec<String> = ctx
            .attack_chain
            .iter()
            .map(|n| n.tool_name.clone())
            .collect();
        names.sort();
        names.dedup();
        names
    };

    // Find the best app screenshot for the cover page:
    // prefer the first navigate to the target URL that has a screenshot,
    // falling back to any navigate with a screenshot
    let app_screenshot: Option<String> = ctx
        .attack_chain
        .iter()
        .filter(|n| n.tool_name == "browser")
        .filter_map(|n| {
            n.tool_output
                .as_ref()?
                .get("screenshot_base64")?
                .as_str()
                .filter(|s| !s.is_empty())
                .map(|s| s.to_string())
        })
        // Skip the Keycloak login page screenshots — prefer one that shows the actual app
        .find(|_| {
            ctx.attack_chain
                .iter()
                .filter(|n| n.tool_name == "browser")
                .any(|n| {
                    n.tool_output
                        .as_ref()
                        .and_then(|o| o.get("title"))
                        .and_then(|t| t.as_str())
                        .is_some_and(|t| t.contains("Compliance") || t.contains("Dashboard"))
                })
        })
        .or_else(|| {
            // Fallback: any screenshot
            ctx.attack_chain
                .iter()
                .filter(|n| n.tool_name == "browser")
                .filter_map(|n| {
                    n.tool_output
                        .as_ref()?
                        .get("screenshot_base64")?
                        .as_str()
                        .filter(|s| !s.is_empty())
                        .map(|s| s.to_string())
                })
                .next()
        });

    let styles_html = styles::styles();
    let cover_html = cover::cover(
        &ctx.target_name,
        &session_id,
        &date_short,
        &ctx.target_url,
        &ctx.requester_name,
        &ctx.requester_email,
        app_screenshot.as_deref(),
    );
    let exec_html = executive_summary::executive_summary(
        &ctx.findings,
        &ctx.target_name,
        &ctx.target_url,
        tool_names.len(),
        session.tool_invocations,
        session.success_rate(),
    );
    let scope_html = scope::scope(
        session,
        &ctx.target_name,
        &ctx.target_url,
        &date_str,
        &completed_str,
        &tool_names,
        ctx.config.as_ref(),
    );
    let findings_html = findings::findings(
        &ctx.findings,
        &ctx.sast_findings,
        &ctx.code_context,
        &ctx.sbom_entries,
    );
    let chain_html = attack_chain::attack_chain(&ctx.attack_chain);
    let appendix_html = appendix::appendix(&session_id);

    format!(
        r#"<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Penetration Test Report — {target_name}</title>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Libre+Baskerville:ital,wght@0,400;0,700;1,400&family=Source+Sans+3:ital,wght@0,300;0,400;0,600;0,700&family=JetBrains+Mono:wght@400;500&display=swap" rel="stylesheet">
{styles_html}
</head>
<body>

{cover_html}

{exec_html}

{scope_html}

{findings_html}

{chain_html}

{appendix_html}
"#,
        target_name = html_escape(&ctx.target_name),
    )
}

fn tool_category(tool_name: &str) -> &'static str {
    let name = tool_name.to_lowercase();
    if name.contains("nmap") || name.contains("port") {
        return "Network Reconnaissance";
    }
    if name.contains("nikto") || name.contains("header") {
        return "Web Server Analysis";
    }
    if name.contains("zap") || name.contains("spider") || name.contains("crawl") {
        return "Web Application Scanning";
    }
    if name.contains("sqlmap") || name.contains("sqli") || name.contains("sql") {
        return "SQL Injection Testing";
    }
    if name.contains("xss") || name.contains("cross-site") {
        return "Cross-Site Scripting Testing";
    }
    if name.contains("dir")
        || name.contains("brute")
        || name.contains("fuzz")
        || name.contains("gobuster")
    {
        return "Directory Enumeration";
    }
    if name.contains("ssl") || name.contains("tls") || name.contains("cert") {
        return "SSL/TLS Analysis";
    }
    if name.contains("api") || name.contains("endpoint") {
        return "API Security Testing";
    }
    if name.contains("auth") || name.contains("login") || name.contains("credential") {
        return "Authentication Testing";
    }
    if name.contains("cors") {
        return "CORS Testing";
    }
    if name.contains("csrf") {
        return "CSRF Testing";
    }
    if name.contains("nuclei") || name.contains("template") {
        return "Vulnerability Scanning";
    }
    if name.contains("whatweb") || name.contains("tech") || name.contains("wappalyzer") {
        return "Technology Fingerprinting";
    }
    "Security Testing"
}

fn html_escape(s: &str) -> String {
    s.replace('&', "&amp;")
        .replace('<', "&lt;")
        .replace('>', "&gt;")
        .replace('"', "&quot;")
}

#[cfg(test)]
mod tests {
    use super::*;
    use compliance_core::models::dast::{DastFinding, DastVulnType};
    use compliance_core::models::finding::Severity;
    use compliance_core::models::pentest::{
        AttackChainNode, AttackNodeStatus, PentestSession, PentestStrategy,
    };

    // ── html_escape ──────────────────────────────────────────────────

    #[test]
    fn html_escape_handles_ampersand() {
        assert_eq!(html_escape("a & b"), "a &amp; b");
    }

    #[test]
    fn html_escape_handles_angle_brackets() {
        assert_eq!(html_escape("<script>"), "&lt;script&gt;");
    }

    #[test]
    fn html_escape_handles_quotes() {
        assert_eq!(html_escape(r#"key="val""#), "key=&quot;val&quot;");
    }

    #[test]
    fn html_escape_handles_all_special_chars() {
        assert_eq!(
            html_escape(r#"<a href="x">&y</a>"#),
            "&lt;a href=&quot;x&quot;&gt;&amp;y&lt;/a&gt;"
        );
    }

    #[test]
    fn html_escape_no_change_for_plain_text() {
        assert_eq!(html_escape("hello world"), "hello world");
    }

    #[test]
    fn html_escape_empty_string() {
        assert_eq!(html_escape(""), "");
    }

    // ── tool_category ────────────────────────────────────────────────

    #[test]
    fn tool_category_nmap() {
        assert_eq!(tool_category("nmap_scan"), "Network Reconnaissance");
    }

    #[test]
    fn tool_category_port_scanner() {
        assert_eq!(tool_category("port_scanner"), "Network Reconnaissance");
    }

    #[test]
    fn tool_category_nikto() {
        assert_eq!(tool_category("nikto"), "Web Server Analysis");
    }

    #[test]
    fn tool_category_header_check() {
        assert_eq!(
            tool_category("security_header_check"),
            "Web Server Analysis"
        );
    }

    #[test]
    fn tool_category_zap_spider() {
        assert_eq!(tool_category("zap_spider"), "Web Application Scanning");
    }

    #[test]
    fn tool_category_sqlmap() {
        assert_eq!(tool_category("sqlmap"), "SQL Injection Testing");
    }

    #[test]
    fn tool_category_xss_scanner() {
        assert_eq!(tool_category("xss_scanner"), "Cross-Site Scripting Testing");
    }

    #[test]
    fn tool_category_dir_bruteforce() {
        assert_eq!(tool_category("dir_bruteforce"), "Directory Enumeration");
    }

    #[test]
    fn tool_category_gobuster() {
        assert_eq!(tool_category("gobuster"), "Directory Enumeration");
    }

    #[test]
    fn tool_category_ssl_check() {
        assert_eq!(tool_category("ssl_check"), "SSL/TLS Analysis");
    }

    #[test]
    fn tool_category_tls_scan() {
        assert_eq!(tool_category("tls_scan"), "SSL/TLS Analysis");
    }

    #[test]
    fn tool_category_api_test() {
        assert_eq!(tool_category("api_endpoint_test"), "API Security Testing");
    }

    #[test]
    fn tool_category_auth_bypass() {
        assert_eq!(tool_category("auth_bypass_check"), "Authentication Testing");
    }

    #[test]
    fn tool_category_cors() {
        assert_eq!(tool_category("cors_check"), "CORS Testing");
    }

    #[test]
    fn tool_category_csrf() {
        assert_eq!(tool_category("csrf_scanner"), "CSRF Testing");
    }

    #[test]
    fn tool_category_nuclei() {
        assert_eq!(tool_category("nuclei"), "Vulnerability Scanning");
    }

    #[test]
    fn tool_category_whatweb() {
        assert_eq!(tool_category("whatweb"), "Technology Fingerprinting");
    }

    #[test]
    fn tool_category_unknown_defaults_to_security_testing() {
        assert_eq!(tool_category("custom_tool"), "Security Testing");
    }

    #[test]
    fn tool_category_is_case_insensitive() {
        assert_eq!(tool_category("NMAP_Scanner"), "Network Reconnaissance");
        assert_eq!(tool_category("SQLMap"), "SQL Injection Testing");
    }

    // ── build_html_report ────────────────────────────────────────────

    fn make_session(strategy: PentestStrategy) -> PentestSession {
        let mut s = PentestSession::new("target-1".into(), strategy);
        s.tool_invocations = 5;
        s.tool_successes = 4;
        s.findings_count = 2;
        s.exploitable_count = 1;
        s
    }

    fn make_finding(severity: Severity, title: &str, exploitable: bool) -> DastFinding {
        let mut f = DastFinding::new(
            "run-1".into(),
            "target-1".into(),
            DastVulnType::Xss,
            title.into(),
            "description".into(),
            severity,
            "https://example.com/test".into(),
            "GET".into(),
        );
        f.exploitable = exploitable;
        f
    }

    fn make_attack_node(tool_name: &str) -> AttackChainNode {
        let mut node = AttackChainNode::new(
            "session-1".into(),
            "node-1".into(),
            tool_name.into(),
            serde_json::json!({}),
            "Testing this tool".into(),
        );
        node.status = AttackNodeStatus::Completed;
        node
    }

    fn make_report_context(
        findings: Vec<DastFinding>,
        chain: Vec<AttackChainNode>,
    ) -> ReportContext {
        ReportContext {
            session: make_session(PentestStrategy::Comprehensive),
            target_name: "Test App".into(),
            target_url: "https://example.com".into(),
            findings,
            attack_chain: chain,
            requester_name: "Alice".into(),
            requester_email: "alice@example.com".into(),
            config: None,
            sast_findings: Vec::new(),
            sbom_entries: Vec::new(),
            code_context: Vec::new(),
        }
    }

    #[test]
    fn report_contains_target_info() {
        let ctx = make_report_context(vec![], vec![]);
        let html = build_html_report(&ctx);
        assert!(html.contains("Test App"));
        assert!(html.contains("https://example.com"));
    }

    #[test]
    fn report_contains_requester_info() {
        let ctx = make_report_context(vec![], vec![]);
        let html = build_html_report(&ctx);
        assert!(html.contains("Alice"));
        assert!(html.contains("alice@example.com"));
    }

    #[test]
    fn report_shows_informational_risk_when_no_findings() {
        let ctx = make_report_context(vec![], vec![]);
        let html = build_html_report(&ctx);
        assert!(html.contains("INFORMATIONAL"));
    }

    #[test]
    fn report_shows_critical_risk_with_critical_finding() {
        let findings = vec![make_finding(Severity::Critical, "Critical XSS", true)];
        let ctx = make_report_context(findings, vec![]);
        let html = build_html_report(&ctx);
        assert!(html.contains("CRITICAL"));
    }

    #[test]
    fn report_shows_high_risk_without_critical() {
        let findings = vec![make_finding(Severity::High, "High SQLi", false)];
        let ctx = make_report_context(findings, vec![]);
        let html = build_html_report(&ctx);
        // Should show HIGH, not CRITICAL
        assert!(html.contains("HIGH"));
    }

    #[test]
    fn report_shows_medium_risk_level() {
        let findings = vec![make_finding(Severity::Medium, "Medium Issue", false)];
        let ctx = make_report_context(findings, vec![]);
        let html = build_html_report(&ctx);
        assert!(html.contains("MEDIUM"));
    }

    #[test]
    fn report_includes_finding_title() {
        let findings = vec![make_finding(
            Severity::High,
            "Reflected XSS in /search",
            true,
        )];
        let ctx = make_report_context(findings, vec![]);
        let html = build_html_report(&ctx);
        assert!(html.contains("Reflected XSS in /search"));
    }

    #[test]
    fn report_shows_exploitable_badge() {
        let findings = vec![make_finding(Severity::Critical, "SQLi", true)];
        let ctx = make_report_context(findings, vec![]);
        let html = build_html_report(&ctx);
        // The report should mark exploitable findings
        assert!(html.contains("EXPLOITABLE"));
    }

    #[test]
    fn report_includes_attack_chain_tool_names() {
        let chain = vec![make_attack_node("nmap_scan"), make_attack_node("sqlmap")];
        let ctx = make_report_context(vec![], chain);
        let html = build_html_report(&ctx);
        assert!(html.contains("nmap_scan"));
        assert!(html.contains("sqlmap"));
    }

    #[test]
    fn report_is_valid_html_structure() {
        let ctx = make_report_context(vec![], vec![]);
        let html = build_html_report(&ctx);
        assert!(html.contains("<!DOCTYPE html>") || html.contains("<html"));
        assert!(html.contains("</html>"));
    }

    #[test]
    fn report_strategy_appears() {
        let ctx = make_report_context(vec![], vec![]);
        let html = build_html_report(&ctx);
        // PentestStrategy::Comprehensive => "comprehensive"
        assert!(html.contains("comprehensive") || html.contains("Comprehensive"));
    }

    #[test]
    fn report_finding_count_is_correct() {
        let findings = vec![
            make_finding(Severity::Critical, "F1", true),
            make_finding(Severity::High, "F2", false),
            make_finding(Severity::Low, "F3", false),
        ];
        let ctx = make_report_context(findings, vec![]);
        let html = build_html_report(&ctx);
        // The total count "3" should appear somewhere
        assert!(
            html.contains(">3<")
                || html.contains(">3 ")
                || html.contains("3 findings")
                || html.contains("3 Total")
        );
    }
}
