use super::html_escape;
use compliance_core::models::dast::DastFinding;
use compliance_core::models::finding::Finding;
use compliance_core::models::pentest::CodeContextHint;
use compliance_core::models::sbom::SbomEntry;

/// Render the findings section with code-level correlation.
///
/// For each DAST finding, if a linked SAST finding exists (via `linked_sast_finding_id`)
/// or if we can match the endpoint to a code entry point, we render a "Code-Level
/// Remediation" block showing the exact file, line, code snippet, and suggested fix.
pub(super) fn findings(
    findings_list: &[DastFinding],
    sast_findings: &[Finding],
    code_context: &[CodeContextHint],
    sbom_entries: &[SbomEntry],
) -> String {
    if findings_list.is_empty() {
        return r#"<!-- ═══════════════ 3. FINDINGS ═══════════════ -->
<div class="page-break"></div>
<h2><span class="section-num">3.</span> Findings</h2>

<p style="color: var(--text-muted);">No vulnerabilities were identified during this assessment.</p>"#.to_string();
    }

    let severity_order = ["critical", "high", "medium", "low", "info"];
    let severity_labels = ["Critical", "High", "Medium", "Low", "Informational"];
    let severity_colors = ["#991b1b", "#c2410c", "#a16207", "#1d4ed8", "#4b5563"];

    // Build SAST lookup by ObjectId hex string
    let sast_by_id: std::collections::HashMap<String, &Finding> = sast_findings
        .iter()
        .filter_map(|f| {
            let id = f.id.as_ref()?.to_hex();
            Some((id, f))
        })
        .collect();

    let mut findings_html = String::new();
    let mut finding_num = 0usize;

    for (si, &sev_key) in severity_order.iter().enumerate() {
        let sev_findings: Vec<&DastFinding> = findings_list
            .iter()
            .filter(|f| f.severity.to_string() == sev_key)
            .collect();
        if sev_findings.is_empty() {
            continue;
        }

        findings_html.push_str(&format!(
            r#"<h4 class="sev-group-title" style="border-color: {color}">{label} ({count})</h4>"#,
            color = severity_colors[si],
            label = severity_labels[si],
            count = sev_findings.len(),
        ));

        for f in sev_findings {
            finding_num += 1;
            let sev_color = severity_colors[si];
            let exploitable_badge = if f.exploitable {
                r#"<span class="badge badge-exploit">EXPLOITABLE</span>"#
            } else {
                ""
            };
            let cwe_cell = f
                .cwe
                .as_deref()
                .map(|c| format!("<tr><td>CWE</td><td>{}</td></tr>", html_escape(c)))
                .unwrap_or_default();
            let param_row = f
                .parameter
                .as_deref()
                .map(|p| {
                    format!(
                        "<tr><td>Parameter</td><td><code>{}</code></td></tr>",
                        html_escape(p)
                    )
                })
                .unwrap_or_default();
            let remediation = f
                .remediation
                .as_deref()
                .unwrap_or("Refer to industry best practices for this vulnerability class.");

            let evidence_html = build_evidence_html(f);

            // ── Code-level correlation ──────────────────────────────
            let code_correlation =
                build_code_correlation(f, &sast_by_id, code_context, sbom_entries);

            findings_html.push_str(&format!(
                r#"
                <div class="finding" style="border-left-color: {sev_color}">
                    <div class="finding-header">
                        <span class="finding-id">F-{num:03}</span>
                        <span class="finding-title">{title}</span>
                        {exploitable_badge}
                    </div>
                    <table class="finding-meta">
                        <tr><td>Type</td><td>{vuln_type}</td></tr>
                        <tr><td>Endpoint</td><td><code>{method} {endpoint}</code></td></tr>
                        {param_row}
                        {cwe_cell}
                    </table>
                    <div class="finding-desc">{description}</div>
                    {evidence_html}
                    {code_correlation}
                    <div class="remediation">
                        <div class="remediation-label">Recommendation</div>
                        {remediation}
                    </div>
                </div>
                "#,
                num = finding_num,
                title = html_escape(&f.title),
                vuln_type = f.vuln_type,
                method = f.method,
                endpoint = html_escape(&f.endpoint),
                description = html_escape(&f.description),
            ));
        }
    }

    format!(
        r##"<!-- ═══════════════ 3. FINDINGS ═══════════════ -->
<div class="page-break"></div>
<h2><span class="section-num">3.</span> Findings</h2>

{findings_html}"##
    )
}

/// Build the evidence table HTML for a finding.
fn build_evidence_html(f: &DastFinding) -> String {
    if f.evidence.is_empty() {
        return String::new();
    }

    let mut eh = String::from(
        r#"<div class="evidence-block"><div class="evidence-title">Evidence</div><table class="evidence-table"><thead><tr><th>Request</th><th>Status</th><th>Details</th></tr></thead><tbody>"#,
    );
    for ev in &f.evidence {
        let payload_info = ev
            .payload
            .as_deref()
            .map(|p| {
                format!(
                    "<br><span class=\"evidence-payload\">Payload: <code>{}</code></span>",
                    html_escape(p)
                )
            })
            .unwrap_or_default();
        eh.push_str(&format!(
            "<tr><td><code>{} {}</code></td><td>{}</td><td>{}{}</td></tr>",
            html_escape(&ev.request_method),
            html_escape(&ev.request_url),
            ev.response_status,
            ev.response_snippet
                .as_deref()
                .map(html_escape)
                .unwrap_or_default(),
            payload_info,
        ));
    }
    eh.push_str("</tbody></table></div>");
    eh
}

/// Build the code-level correlation block for a DAST finding.
///
/// Attempts correlation in priority order:
/// 1. Direct link via `linked_sast_finding_id` → shows exact file, line, snippet, suggested fix
/// 2. Endpoint match via code context → shows handler function, file, known SAST vulns
/// 3. CWE/CVE match to SBOM → shows vulnerable dependency + version to upgrade
fn build_code_correlation(
    dast_finding: &DastFinding,
    sast_by_id: &std::collections::HashMap<String, &Finding>,
    code_context: &[CodeContextHint],
    sbom_entries: &[SbomEntry],
) -> String {
    let mut sections: Vec<String> = Vec::new();

    // 1. Direct SAST link
    if let Some(ref sast_id) = dast_finding.linked_sast_finding_id {
        if let Some(sast) = sast_by_id.get(sast_id) {
            let mut s = String::new();
            s.push_str(r#"<div class="code-correlation-item">"#);
            s.push_str(r#"<div class="code-correlation-badge">SAST Correlation</div>"#);
            s.push_str("<table class=\"code-meta\">");

            if let Some(ref fp) = sast.file_path {
                let line_info = sast
                    .line_number
                    .map(|l| format!(":{l}"))
                    .unwrap_or_default();
                s.push_str(&format!(
                    "<tr><td>Location</td><td><code>{}{}</code></td></tr>",
                    html_escape(fp),
                    line_info,
                ));
            }
            s.push_str(&format!(
                "<tr><td>Scanner</td><td>{} &mdash; {}</td></tr>",
                html_escape(&sast.scanner),
                html_escape(&sast.title),
            ));
            if let Some(ref cwe) = sast.cwe {
                s.push_str(&format!(
                    "<tr><td>CWE</td><td>{}</td></tr>",
                    html_escape(cwe)
                ));
            }
            if let Some(ref rule) = sast.rule_id {
                s.push_str(&format!(
                    "<tr><td>Rule</td><td><code>{}</code></td></tr>",
                    html_escape(rule)
                ));
            }
            s.push_str("</table>");

            // Code snippet
            if let Some(ref snippet) = sast.code_snippet {
                if !snippet.is_empty() {
                    s.push_str(&format!(
                        "<div class=\"code-snippet-block\"><div class=\"code-snippet-label\">Vulnerable Code</div><pre class=\"code-snippet\">{}</pre></div>",
                        html_escape(snippet)
                    ));
                }
            }

            // Suggested fix
            if let Some(ref fix) = sast.suggested_fix {
                if !fix.is_empty() {
                    s.push_str(&format!(
                        "<div class=\"code-fix-block\"><div class=\"code-fix-label\">Suggested Fix</div><pre class=\"code-fix\">{}</pre></div>",
                        html_escape(fix)
                    ));
                }
            }

            // Remediation from SAST
            if let Some(ref rem) = sast.remediation {
                if !rem.is_empty() {
                    s.push_str(&format!(
                        "<div class=\"code-remediation\">{}</div>",
                        html_escape(rem)
                    ));
                }
            }

            s.push_str("</div>");
            sections.push(s);
        }
    }

    // 2. Endpoint match via code context
    let endpoint_lower = dast_finding.endpoint.to_lowercase();
    let matching_hints: Vec<&CodeContextHint> = code_context
        .iter()
        .filter(|hint| {
            // Match by endpoint pattern overlap
            let pattern_lower = hint.endpoint_pattern.to_lowercase();
            endpoint_lower.contains(&pattern_lower)
                || pattern_lower.contains(&endpoint_lower)
                || hint.file_path.to_lowercase().contains(
                    &endpoint_lower
                        .split('/')
                        .next_back()
                        .unwrap_or("")
                        .replace(".html", "")
                        .replace(".php", ""),
                )
        })
        .collect();

    for hint in &matching_hints {
        let mut s = String::new();
        s.push_str(r#"<div class="code-correlation-item">"#);
        s.push_str(r#"<div class="code-correlation-badge">Code Entry Point</div>"#);
        s.push_str("<table class=\"code-meta\">");
        s.push_str(&format!(
            "<tr><td>Handler</td><td><code>{}</code></td></tr>",
            html_escape(&hint.handler_function),
        ));
        s.push_str(&format!(
            "<tr><td>File</td><td><code>{}</code></td></tr>",
            html_escape(&hint.file_path),
        ));
        s.push_str(&format!(
            "<tr><td>Route</td><td><code>{}</code></td></tr>",
            html_escape(&hint.endpoint_pattern),
        ));
        s.push_str("</table>");

        if !hint.known_vulnerabilities.is_empty() {
            s.push_str("<div class=\"code-linked-vulns\"><strong>Known SAST issues in this file:</strong><ul>");
            for vuln in &hint.known_vulnerabilities {
                s.push_str(&format!("<li>{}</li>", html_escape(vuln)));
            }
            s.push_str("</ul></div>");
        }

        s.push_str("</div>");
        sections.push(s);
    }

    // 3. SBOM match — if a linked SAST finding has a CVE, or we can match by CWE
    let linked_cve = dast_finding
        .linked_sast_finding_id
        .as_deref()
        .and_then(|id| sast_by_id.get(id))
        .and_then(|f| f.cve.as_deref());

    if let Some(cve_id) = linked_cve {
        let matching_deps: Vec<&SbomEntry> = sbom_entries
            .iter()
            .filter(|e| e.known_vulnerabilities.iter().any(|v| v.id == cve_id))
            .collect();

        for dep in &matching_deps {
            let mut s = String::new();
            s.push_str(r#"<div class="code-correlation-item">"#);
            s.push_str(r#"<div class="code-correlation-badge">Vulnerable Dependency</div>"#);
            s.push_str("<table class=\"code-meta\">");
            s.push_str(&format!(
                "<tr><td>Package</td><td><code>{} {}</code> ({})</td></tr>",
                html_escape(&dep.name),
                html_escape(&dep.version),
                html_escape(&dep.package_manager),
            ));
            let cve_ids: Vec<&str> = dep
                .known_vulnerabilities
                .iter()
                .map(|v| v.id.as_str())
                .collect();
            s.push_str(&format!(
                "<tr><td>CVEs</td><td>{}</td></tr>",
                cve_ids.join(", "),
            ));
            if let Some(ref purl) = dep.purl {
                s.push_str(&format!(
                    "<tr><td>PURL</td><td><code>{}</code></td></tr>",
                    html_escape(purl),
                ));
            }
            s.push_str("</table>");
            s.push_str(&format!(
                "<div class=\"code-remediation\">Upgrade <code>{}</code> to the latest patched version to resolve {}.</div>",
                html_escape(&dep.name),
                html_escape(cve_id),
            ));
            s.push_str("</div>");
            sections.push(s);
        }
    }

    if sections.is_empty() {
        return String::new();
    }

    format!(
        r#"<div class="code-correlation">
    <div class="code-correlation-title">Code-Level Remediation</div>
    {}
</div>"#,
        sections.join("\n")
    )
}

#[cfg(test)]
mod tests {
    use super::*;
    use compliance_core::models::dast::{DastEvidence, DastVulnType};
    use compliance_core::models::finding::Severity;
    use compliance_core::models::scan::ScanType;

    /// Helper: create a minimal `DastFinding`.
    fn make_dast(title: &str, severity: Severity, endpoint: &str) -> DastFinding {
        DastFinding::new(
            "run1".into(),
            "target1".into(),
            DastVulnType::Xss,
            title.into(),
            "desc".into(),
            severity,
            endpoint.into(),
            "GET".into(),
        )
    }

    /// Helper: create a minimal SAST `Finding` with an ObjectId.
    fn make_sast(title: &str) -> Finding {
        let mut f = Finding::new(
            "repo1".into(),
            "fp1".into(),
            "semgrep".into(),
            ScanType::Sast,
            title.into(),
            "sast desc".into(),
            Severity::High,
        );
        f.id = Some(mongodb::bson::oid::ObjectId::new());
        f
    }

    #[test]
    fn test_findings_empty() {
        let result = findings(&[], &[], &[], &[]);
        assert!(
            result.contains("No vulnerabilities were identified"),
            "Empty findings should contain the no-vulns message"
        );
    }

    #[test]
    fn test_findings_grouped_by_severity() {
        let f_high = make_dast("High vuln", Severity::High, "/a");
        let f_low = make_dast("Low vuln", Severity::Low, "/b");
        let f_critical = make_dast("Crit vuln", Severity::Critical, "/c");

        let result = findings(&[f_high, f_low, f_critical], &[], &[], &[]);

        // All severity group headers should appear
        assert!(
            result.contains("Critical (1)"),
            "should have Critical header"
        );
        assert!(result.contains("High (1)"), "should have High header");
        assert!(result.contains("Low (1)"), "should have Low header");

        // Critical should appear before High, High before Low
        let crit_pos = result.find("Critical (1)");
        let high_pos = result.find("High (1)");
        let low_pos = result.find("Low (1)");
        assert!(crit_pos < high_pos, "Critical should come before High");
        assert!(high_pos < low_pos, "High should come before Low");
    }

    #[test]
    fn test_code_correlation_sast_link() {
        let mut sast = make_sast("SQL Injection in query");
        sast.file_path = Some("src/db/query.rs".into());
        sast.line_number = Some(42);
        sast.code_snippet =
            Some("let q = format!(\"SELECT * FROM {} WHERE id={}\", table, id);".into());

        let sast_id = sast.id.as_ref().map(|oid| oid.to_hex()).unwrap_or_default();

        let mut dast = make_dast("SQLi on /api/users", Severity::High, "/api/users");
        dast.linked_sast_finding_id = Some(sast_id);

        let result = findings(&[dast], &[sast], &[], &[]);

        assert!(
            result.contains("SAST Correlation"),
            "should render SAST Correlation badge"
        );
        assert!(
            result.contains("src/db/query.rs"),
            "should contain the file path"
        );
        assert!(result.contains(":42"), "should contain the line number");
        assert!(
            result.contains("Vulnerable Code"),
            "should render code snippet block"
        );
    }

    #[test]
    fn test_code_correlation_no_match() {
        let dast = make_dast("XSS in search", Severity::Medium, "/search");
        // No linked_sast_finding_id, no code context, no sbom
        let result = findings(&[dast], &[], &[], &[]);

        assert!(
            !result.contains("code-correlation"),
            "should not contain any code-correlation div"
        );
    }

    #[test]
    fn test_evidence_html_empty() {
        let f = make_dast("No evidence", Severity::Low, "/x");
        let result = build_evidence_html(&f);
        assert!(result.is_empty(), "no evidence should yield empty string");
    }

    #[test]
    fn test_evidence_html_with_entries() {
        let mut f = make_dast("Has evidence", Severity::High, "/y");
        f.evidence.push(DastEvidence {
            request_method: "POST".into(),
            request_url: "https://example.com/login".into(),
            request_headers: None,
            request_body: None,
            response_status: 200,
            response_headers: None,
            response_snippet: Some("OK".into()),
            screenshot_path: None,
            payload: Some("<script>alert(1)</script>".into()),
            response_time_ms: None,
        });

        let result = build_evidence_html(&f);

        assert!(
            result.contains("evidence-table"),
            "should render the evidence table"
        );
        assert!(result.contains("POST"), "should contain request method");
        assert!(
            result.contains("https://example.com/login"),
            "should contain request URL"
        );
        assert!(result.contains("200"), "should contain response status");
        assert!(
            result.contains("&lt;script&gt;alert(1)&lt;/script&gt;"),
            "payload should be HTML-escaped"
        );
    }
}
