//! Security constants and scripts for sandboxing product WebViews.
//!
//! These constants are platform-agnostic building blocks. Each platform
//! implementation (iOS/macOS WKWebView, Android WebView, desktop, WASM iframe)
//! is responsible for applying them at the correct injection point.
//!
//! # Injection order for native WebViews (WKWebView / Android)
//!
//! 1. Install `BLOCK_ALL_CONTENT_RULE_JSON` as a `WKContentRuleList` *before*
//!    the view navigates to the product bundle.
//! 2. Inject `host-api`'s `HOST_API_BRIDGE_SCRIPT` at `document_start`.
//! 3. Inject `SANDBOX_LOCKDOWN_SCRIPT` at `document_start` *after* the bridge.
//!
//! # Injection order for WASM iframe products
//!
//! 1. Set the `sandbox` attribute to `IFRAME_SANDBOX_ATTR` on the `<iframe>`.
//! 2. Insert a `<meta http-equiv="Content-Security-Policy">` with `IFRAME_CSP`
//!    as the very first element in `<head>`.

/// WKContentRuleList JSON that blocks all HTTP, HTTPS, WS, and WSS requests.
/// Install this on WKWebView (iOS/macOS) to prevent all outbound network.
pub const BLOCK_ALL_CONTENT_RULE_JSON: &str =
    r#"[{"trigger":{"url-filter":"^(https?|wss?)://"},"action":{"type":"block"}}]"#;

/// JavaScript that must be injected at `document_start` **after** the bridge script
/// to remove dangerous Web APIs from the product's scope.
///
/// # Blocked APIs
///
/// - WebSocket / Worker / ServiceWorker (network and background exfiltration)
/// - WebRTC peer connections (P2P data exfiltration)
/// - EventSource (Server-Sent Events)
/// - SharedWorker / BroadcastChannel (cross-product communication)
/// - Direct storage access: localStorage, sessionStorage, IndexedDB, CacheStorage
/// - Cookies (read/write via document.cookie)
/// - Wallet extension sniffing (window.injectedWeb3, window.polkadot, window.ethereum)
/// - sendBeacon (fire-and-forget network exfiltration)
/// - Nested iframes (CSP bypass via child frames without inherited CSP)
///
/// # Execution ordering (load-bearing)
///
/// This script **must** run at `document_start`, **after** the host-api bridge
/// script and **before** any product code executes. If product code runs first,
/// it can cache references to native APIs (e.g. `const ws = WebSocket`) before
/// the lockdown nullifies them. The `window.X = undefined` assignments are not
/// tamper-proof — they rely entirely on correct injection ordering. Additionally,
/// `= undefined` assignments can be reverted by product code via
/// `Object.defineProperty`; they defend against accidental API use but not
/// deliberate circumvention. The `configurable: false` pattern (used for
/// storage, serviceWorker, cookies, wallet keys, and sendBeacon) is the
/// mitigation for deliberate circumvention.
///
/// # Defense-in-depth patterns
///
/// - **`configurable: false`**: High-value overrides (storage, serviceWorker,
///   cookies, wallet extension keys) use `Object.defineProperty` with
///   `configurable: false` so product code cannot revert them via
///   `Object.defineProperty`. Simple `= undefined` assignments (WebSocket,
///   Worker, etc.) do not have this protection and rely on ordering.
/// - **Iframe blocking**: `document.createElement` is wrapped to reject
///   `<iframe>` creation because `<meta>` CSP tags do not propagate to child
///   frames. A product-created iframe could bypass the document-level CSP
///   restrictions entirely.
/// - **`sendBeacon`**: Locked with `Object.defineProperty` and
///   `configurable: false`, consistent with the other high-value overrides.
pub const SANDBOX_LOCKDOWN_SCRIPT: &str = r#"
(function() {
    'use strict';

    // ── Network APIs ────────────────────────────────────────────────────────
    // All network I/O must go through the host bridge.
    window.WebSocket = undefined;
    window.EventSource = undefined;

    // Workers can open their own network connections independent of the page.
    window.Worker = undefined;

    // ServiceWorker can intercept network requests and run in the background.
    if (navigator.serviceWorker) {
        Object.defineProperty(navigator, 'serviceWorker', {
            get: function() {
                throw new DOMException(
                    'serviceWorker is blocked in sandboxed products', 'SecurityError'
                );
            },
            configurable: false,
        });
    }

    // sendBeacon bypasses many CSP checks and fires regardless of page unload.
    if (navigator.sendBeacon) {
        Object.defineProperty(navigator, 'sendBeacon', {
            value: function() { return false; },
            writable: false,
            configurable: false,
        });
    }

    // ── WebRTC ──────────────────────────────────────────────────────────────
    // The host provides media and data channels through the extension bridge.
    window.RTCPeerConnection = undefined;
    window.RTCSessionDescription = undefined;
    window.RTCIceCandidate = undefined;
    window.RTCDataChannel = undefined;

    // ── Cross-product communication ─────────────────────────────────────────
    // Each product is isolated; cross-origin messaging channels are blocked.
    window.SharedWorker = undefined;
    window.BroadcastChannel = undefined;

    // ── Storage — products must use the host bridge for persistence ─────────
    // Override getters to throw so any access (read or write) is blocked.
    ['localStorage', 'sessionStorage'].forEach(function(key) {
        try {
            Object.defineProperty(window, key, {
                get: function() {
                    throw new DOMException(
                        key + ' is blocked in sandboxed products; use the host bridge', 'SecurityError'
                    );
                },
                configurable: false,
            });
        } catch (e) { /* already non-configurable on this engine — silently accept */ }
    });

    // IndexedDB — override open() and deleteDatabase() to block all access.
    if (window.indexedDB) {
        window.indexedDB.open = function() {
            throw new DOMException(
                'indexedDB.open is blocked in sandboxed products; use the host bridge', 'SecurityError'
            );
        };
        window.indexedDB.deleteDatabase = function() {
            throw new DOMException(
                'indexedDB.deleteDatabase is blocked in sandboxed products', 'SecurityError'
            );
        };
    }

    // CacheStorage — block caches.open and caches.delete.
    if (window.caches) {
        window.caches.open = function() {
            throw new DOMException(
                'caches.open is blocked in sandboxed products', 'SecurityError'
            );
        };
        window.caches.delete = function() {
            throw new DOMException(
                'caches.delete is blocked in sandboxed products', 'SecurityError'
            );
        };
    }

    // ── Cookies ─────────────────────────────────────────────────────────────
    // Override document.cookie so products cannot read or write cookies.
    try {
        Object.defineProperty(document, 'cookie', {
            get: function() { return ''; },
            set: function() { /* no-op: silently discard cookie writes */ },
            configurable: false,
        });
    } catch (e) { /* non-configurable environment — silently accept */ }

    // ── Wallet extension sniffing ────────────────────────────────────────────
    // Freeze these as undefined so products cannot detect or hijack extensions.
    ['injectedWeb3', 'polkadot', 'ethereum'].forEach(function(key) {
        try {
            Object.defineProperty(window, key, {
                value: undefined,
                writable: false,
                configurable: false,
            });
        } catch (e) { /* property already non-configurable — silently accept */ }
    });

    // ── Nested iframes ───────────────────────────────────────────────────────
    // CSP meta tags do not propagate to child frames. A product-created iframe
    // could bypass network restrictions. Block creation entirely; products that
    // need sub-frames must use the host's extension bridge instead.
    var _origCreateElement = document.createElement.bind(document);
    document.createElement = function(tag) {
        if (tag && tag.toLowerCase() === 'iframe') {
            throw new DOMException(
                'iframe creation is blocked in sandboxed products', 'SecurityError'
            );
        }
        return _origCreateElement.apply(this, arguments);
    };
})();
"#;

/// Development-only script that wraps blocked APIs with instrumented stubs
/// that report violation attempts to the host via postMessage before blocking.
///
/// Inject this INSTEAD OF `SANDBOX_LOCKDOWN_SCRIPT` during development.
/// In production, use the lockdown script which silently blocks without reporting.
///
/// Each violation sends a message to the parent window in the format:
/// `{type: "SANDBOX_VIOLATION", api: "<ApiName>", details: {...}, timestamp: <ms>}`
pub const SANDBOX_MONITOR_SCRIPT: &str = r#"
(function() {
    'use strict';

    // Report a sandbox violation to the host and then throw.
    // All blocked APIs call this before raising SecurityError.
    function report(api, details) {
        var payload = {
            type: 'SANDBOX_VIOLATION',
            api: api,
            details: details || {},
            timestamp: Date.now(),
        };
        try {
            // Works in both iframe (parent) and WKWebView (window self) contexts.
            var target = (window.parent !== window) ? window.parent : window;
            target.postMessage(payload, '*');
        } catch (e) { /* postMessage failure must never suppress the throw below */ }
        throw new DOMException(
            api + ' is blocked in sandboxed products', 'SecurityError'
        );
    }

    // ── Network APIs ────────────────────────────────────────────────────────
    window.WebSocket = function(url) {
        report('WebSocket', { url: String(url) });
    };

    window.EventSource = function(url) {
        report('EventSource', { url: String(url) });
    };

    window.Worker = function(url) {
        report('Worker', { url: String(url) });
    };

    if (navigator.serviceWorker) {
        Object.defineProperty(navigator, 'serviceWorker', {
            get: function() { report('ServiceWorker', {}); },
            configurable: false,
        });
    }

    // Wrap fetch to detect and report cross-origin requests before blocking.
    var _origFetch = window.fetch;
    if (_origFetch) {
        window.fetch = function(input, init) {
            var url;
            try {
                url = (typeof input === 'string') ? input : (input.url || String(input));
                var parsed = new URL(url, location.href);
                if (parsed.origin !== location.origin) {
                    report('fetch', { url: url, origin: parsed.origin });
                }
            } catch (e) {
                report('fetch', { url: String(url) });
            }
            return _origFetch.apply(this, arguments);
        };
    }

    // Wrap XMLHttpRequest.open to report cross-origin attempts.
    var _origXhrOpen = XMLHttpRequest.prototype.open;
    if (_origXhrOpen) {
        XMLHttpRequest.prototype.open = function(method, url) {
            try {
                var parsed = new URL(String(url), location.href);
                if (parsed.origin !== location.origin) {
                    report('XMLHttpRequest', { method: String(method), url: String(url), origin: parsed.origin });
                }
            } catch (e) {
                report('XMLHttpRequest', { method: String(method), url: String(url) });
            }
            return _origXhrOpen.apply(this, arguments);
        };
    }

    if (navigator.sendBeacon) {
        navigator.sendBeacon = function(url) {
            report('sendBeacon', { url: String(url) });
        };
    }

    // ── WebRTC ──────────────────────────────────────────────────────────────
    window.RTCPeerConnection = function() {
        report('RTCPeerConnection', {});
    };
    window.RTCSessionDescription = function() {
        report('RTCSessionDescription', {});
    };
    window.RTCIceCandidate = function() {
        report('RTCIceCandidate', {});
    };
    window.RTCDataChannel = function() {
        report('RTCDataChannel', {});
    };

    // ── Cross-product communication ─────────────────────────────────────────
    window.SharedWorker = function(url) {
        report('SharedWorker', { url: String(url) });
    };
    window.BroadcastChannel = function(name) {
        report('BroadcastChannel', { name: String(name) });
    };

    // ── Storage ─────────────────────────────────────────────────────────────
    ['localStorage', 'sessionStorage'].forEach(function(key) {
        try {
            Object.defineProperty(window, key, {
                get: function() { report(key, {}); },
                configurable: false,
            });
        } catch (e) { /* already non-configurable — silently accept */ }
    });

    if (window.indexedDB) {
        window.indexedDB.open = function(name, version) {
            report('indexedDB.open', { name: String(name), version: version });
        };
        window.indexedDB.deleteDatabase = function(name) {
            report('indexedDB.deleteDatabase', { name: String(name) });
        };
    }

    if (window.caches) {
        window.caches.open = function(cacheName) {
            report('caches.open', { cacheName: String(cacheName) });
        };
        window.caches.delete = function(cacheName) {
            report('caches.delete', { cacheName: String(cacheName) });
        };
    }

    // ── Cookies ─────────────────────────────────────────────────────────────
    try {
        Object.defineProperty(document, 'cookie', {
            get: function() {
                report('document.cookie (read)', {});
            },
            set: function(value) {
                report('document.cookie (write)', { value: String(value).slice(0, 64) });
            },
            configurable: false,
        });
    } catch (e) { /* non-configurable environment — silently accept */ }

    // ── Wallet extension sniffing ────────────────────────────────────────────
    ['injectedWeb3', 'polkadot', 'ethereum'].forEach(function(key) {
        try {
            Object.defineProperty(window, key, {
                get: function() { report('window.' + key, {}); },
                set: function(v) { report('window.' + key + ' (set)', {}); },
                configurable: false,
            });
        } catch (e) { /* already non-configurable — silently accept */ }
    });

    // ── Nested iframes ───────────────────────────────────────────────────────
    var _origCreateElement = document.createElement.bind(document);
    document.createElement = function(tag) {
        if (tag && tag.toLowerCase() === 'iframe') {
            report('createElement(iframe)', { tag: tag });
        }
        return _origCreateElement.apply(this, arguments);
    };
})();
"#;

/// Content Security Policy for iframe-sandboxed products (web platform).
///
/// Blocks ALL network access. Only inline scripts and WASM are allowed.
/// This should be injected as a `<meta>` tag at the very start of `<head>`.
pub const IFRAME_CSP: &str = "default-src 'none'; \
    script-src 'unsafe-inline' 'wasm-unsafe-eval'; \
    style-src 'unsafe-inline'; \
    frame-src 'none'; \
    connect-src 'none'; \
    img-src blob: data:; \
    font-src 'none'; \
    media-src 'none'; \
    object-src 'none'; \
    base-uri 'none'; \
    form-action 'none'";

/// HTML `sandbox` attribute for iframe-sandboxed products.
///
/// `allow-scripts` is the ONLY permission granted. Everything else is blocked:
/// - No network (fetch, XHR, WebSocket)
/// - No same-origin access to parent
/// - No forms submission
/// - No popups
/// - No top-level navigation
pub const IFRAME_SANDBOX_ATTR: &str = "allow-scripts";

/// Build a CSP string for products served from a custom URI scheme.
///
/// - `scheme`: the registered scheme name without trailing colon (e.g. "dotapp")
/// - `chain_access`: if true, adds `wss:` to connect-src for chain RPC
/// - `approved_origins`: additional origins to allow in connect-src (from manifest)
///
/// Returns a complete CSP string suitable for a `Content-Security-Policy` header
/// or `<meta>` tag.
///
/// # CORS: do not add
///
/// Custom URI schemes (`dotapp://`, `myapp://`) are treated as opaque unique
/// origins by WebView engines. They do **not** participate in the CORS protocol.
/// Adding `Access-Control-Allow-Origin` or other CORS headers to custom-scheme
/// responses is meaningless and risks implying a broader cross-origin permission
/// model than actually exists.
///
/// The scheme must also be registered as a true custom scheme at the platform
/// level (e.g. `WKWebViewConfiguration.setURLSchemeHandler`), **not** as an
/// HTTP-equivalent scheme, to preserve opaque-origin isolation.
pub fn csp_for_scheme(scheme: &str, chain_access: bool, approved_origins: &[&str]) -> String {
    // Build connect-src parts; bind the formatted scheme string so it outlives
    // the borrow taken by `connect_parts`.
    let scheme_src = format!("{scheme}:");
    let mut connect_parts = vec!["'self'", &scheme_src];
    if chain_access {
        connect_parts.push("wss:");
    }
    for origin in approved_origins {
        connect_parts.push(origin);
    }
    let connect_src = connect_parts.join(" ");

    format!(
        "default-src 'self' {scheme}:; \
         script-src 'self' 'unsafe-inline' 'wasm-unsafe-eval'; \
         style-src 'self' 'unsafe-inline'; \
         img-src 'self' {scheme}: data: blob:; \
         font-src 'self' {scheme}: data:; \
         connect-src {connect_src}; \
         worker-src 'none'; \
         child-src 'none'; \
         object-src 'none'; \
         frame-src 'none'; \
         base-uri 'none'; \
         form-action 'none'"
    )
}

/// Inject a CSP `<meta>` tag and a `<script>` block into an HTML document's `<head>`.
///
/// - `html`: the raw HTML bytes (must be valid UTF-8)
/// - `csp`: the CSP string (will be HTML-attribute-escaped)
/// - `script_body`: raw JavaScript to inject (will be wrapped in `<script>...</script>`;
///   any `</script>` in the body is escaped as `<\/script>` to prevent tag injection;
///   pass `""` to skip script injection)
///
/// If no `<head>` tag is found, content is prepended to the document.
/// Returns the modified HTML as owned bytes, or an error if the input is not valid UTF-8.
///
/// # Limitations
///
/// A `<meta http-equiv="Content-Security-Policy">` tag only constrains the
/// **document that contains it**. Sub-resource responses (JS, CSS, fonts) are
/// **not** covered by a meta CSP — they are governed by the CSP delivered on
/// the document's response header. For native WebViews, prefer setting the
/// `Content-Security-Policy` header via the platform-level scheme handler
/// (`WKURLSchemeHandler.didReceiveResponse`, Android `shouldInterceptRequest`)
/// which provides a strictly stronger guarantee.
///
/// Additionally, per the CSP spec, `<meta>` CSP tags **cannot** set:
/// - `frame-ancestors` (must be delivered via response header)
/// - `report-uri` / `report-to` (ignored in meta context)
/// - `sandbox` (ignored in meta context)
///
/// Other limitations:
/// - Calling this on non-HTML content will corrupt the output (the CSP meta
///   tag is prepended as raw bytes). Callers must ensure `html` is HTML.
/// - Malformed HTML with an unclosed `<!--` comment may cause `<head>` tags
///   inside the comment to be used as the injection point.
///
/// The meta-injection path exists as a fallback for environments (WASM iframe)
/// where response header injection is not available.
pub fn inject_into_head(
    html: &[u8],
    csp: &str,
    script_body: &str,
) -> Result<Vec<u8>, std::str::Utf8Error> {
    let html_str = std::str::from_utf8(html)?;

    // HTML-attribute-escape the CSP value (& must be first to avoid double-escaping).
    let escaped_csp = csp
        .replace('&', "&amp;")
        .replace('"', "&quot;")
        .replace('<', "&lt;")
        .replace('>', "&gt;");

    let csp_meta =
        format!(r#"<meta http-equiv="Content-Security-Policy" content="{escaped_csp}">"#);

    // Wrap raw JS in <script> tags; skip if empty.
    // Escape "</script>" in the body to prevent early tag closure and HTML injection.
    let script_block = if script_body.is_empty() {
        String::new()
    } else {
        let safe_body = script_body.replace("</script>", r"<\/script>");
        format!("<script>{safe_body}</script>")
    };

    // Find <head> tag, case-insensitive, skipping HTML comments.
    let inject_pos = find_head_tag(html_str);

    match inject_pos {
        Some(pos) => {
            let mut result =
                String::with_capacity(html_str.len() + csp_meta.len() + script_block.len() + 20);
            result.push_str(&html_str[..pos]);
            result.push_str(&csp_meta);
            result.push_str(&script_block);
            result.push_str(&html_str[pos..]);
            Ok(result.into_bytes())
        }
        None => {
            let mut result =
                Vec::with_capacity(html.len() + csp_meta.len() + script_block.len() + 20);
            result.extend_from_slice(csp_meta.as_bytes());
            result.extend_from_slice(script_block.as_bytes());
            result.extend_from_slice(html);
            Ok(result)
        }
    }
}

/// Find the position right after the opening `<head>` tag, skipping HTML comments.
/// Returns None if no `<head>` tag is found.
fn find_head_tag(html: &str) -> Option<usize> {
    let lower = html.to_ascii_lowercase();
    let mut search_from = 0;

    loop {
        // Check if a comment starts before or at our search position.
        if let Some(comment_start) = lower[search_from..].find("<!--") {
            let abs_comment_start = search_from + comment_start;
            if let Some(comment_end) = lower[abs_comment_start..].find("-->") {
                let abs_comment_end = abs_comment_start + comment_end + 3;
                // Check if there's a <head> between search_from and comment_start.
                if let Some(head_pos) = lower[search_from..abs_comment_start].find("<head") {
                    let abs_head = search_from + head_pos;
                    // Find the closing > of the <head> tag.
                    if let Some(close) = html[abs_head..].find('>') {
                        return Some(abs_head + close + 1);
                    }
                }
                // No <head> before this comment — skip past it and continue.
                search_from = abs_comment_end;
                continue;
            }
        }

        // No more comments — search the remainder of the document.
        if let Some(head_pos) = lower[search_from..].find("<head") {
            let abs_head = search_from + head_pos;
            if let Some(close) = html[abs_head..].find('>') {
                return Some(abs_head + close + 1);
            }
        }

        return None;
    }
}

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

    #[test]
    fn block_all_content_rule_json_is_valid_json() {
        // Parsed by serde_json; invalid JSON would panic the test.
        let v: serde_json::Value =
            serde_json::from_str(BLOCK_ALL_CONTENT_RULE_JSON).expect("should be valid JSON");
        // Must be an array with at least one rule object.
        let arr = v.as_array().expect("top-level value must be a JSON array");
        assert!(!arr.is_empty(), "rule list must not be empty");
        let rule = &arr[0];
        assert!(
            rule.get("trigger").is_some(),
            "rule must have a trigger field"
        );
        assert!(
            rule.get("action").is_some(),
            "rule must have an action field"
        );
    }

    #[test]
    fn sandbox_lockdown_script_disables_websocket() {
        assert!(
            SANDBOX_LOCKDOWN_SCRIPT.contains("window.WebSocket = undefined"),
            "must nullify WebSocket"
        );
    }

    #[test]
    fn sandbox_lockdown_script_disables_webrtc() {
        assert!(
            SANDBOX_LOCKDOWN_SCRIPT.contains("window.RTCPeerConnection = undefined"),
            "must nullify RTCPeerConnection"
        );
        assert!(
            SANDBOX_LOCKDOWN_SCRIPT.contains("window.RTCSessionDescription = undefined"),
            "must nullify RTCSessionDescription"
        );
        assert!(
            SANDBOX_LOCKDOWN_SCRIPT.contains("window.RTCIceCandidate = undefined"),
            "must nullify RTCIceCandidate"
        );
        assert!(
            SANDBOX_LOCKDOWN_SCRIPT.contains("window.RTCDataChannel = undefined"),
            "must nullify RTCDataChannel"
        );
    }

    #[test]
    fn sandbox_lockdown_script_disables_cross_product_apis() {
        assert!(
            SANDBOX_LOCKDOWN_SCRIPT.contains("window.SharedWorker = undefined"),
            "must nullify SharedWorker"
        );
        assert!(
            SANDBOX_LOCKDOWN_SCRIPT.contains("window.BroadcastChannel = undefined"),
            "must nullify BroadcastChannel"
        );
    }

    #[test]
    fn sandbox_lockdown_script_disables_eventsource() {
        assert!(
            SANDBOX_LOCKDOWN_SCRIPT.contains("window.EventSource = undefined"),
            "must nullify EventSource"
        );
    }

    #[test]
    fn sandbox_lockdown_script_blocks_iframe_creation() {
        assert!(
            SANDBOX_LOCKDOWN_SCRIPT.contains("createElement"),
            "must override createElement to block iframe creation"
        );
        assert!(
            SANDBOX_LOCKDOWN_SCRIPT.contains("'iframe'"),
            "must specifically check for iframe tag"
        );
    }

    #[test]
    fn iframe_csp_blocks_network() {
        assert!(
            IFRAME_CSP.contains("connect-src 'none'"),
            "CSP must block connect-src"
        );
        assert!(
            IFRAME_CSP.contains("frame-src 'none'"),
            "CSP must block frame-src"
        );
    }

    #[test]
    fn iframe_csp_blocks_objects_and_forms() {
        assert!(
            IFRAME_CSP.contains("object-src 'none'"),
            "CSP must block object-src"
        );
        assert!(
            IFRAME_CSP.contains("form-action 'none'"),
            "CSP must block form-action"
        );
    }

    #[test]
    fn iframe_csp_allows_inline_scripts_and_wasm() {
        assert!(
            IFRAME_CSP.contains("'unsafe-inline'"),
            "CSP must allow inline scripts for product bootstrapping"
        );
        assert!(
            IFRAME_CSP.contains("'wasm-unsafe-eval'"),
            "CSP must allow WASM evaluation"
        );
    }

    #[test]
    fn iframe_sandbox_attr_only_allows_scripts() {
        assert_eq!(
            IFRAME_SANDBOX_ATTR, "allow-scripts",
            "sandbox must grant only allow-scripts"
        );
    }

    // ------------------------------------------------------------------
    // Granular lockdown assertions (one API override per test so a
    // regression pinpoints the exact missing assignment).
    // ------------------------------------------------------------------

    #[test]
    fn test_lockdown_overrides_websocket() {
        assert!(
            SANDBOX_LOCKDOWN_SCRIPT.contains("window.WebSocket = undefined"),
            "lockdown must set window.WebSocket = undefined"
        );
    }

    #[test]
    fn test_lockdown_overrides_rtc_peer_connection() {
        assert!(
            SANDBOX_LOCKDOWN_SCRIPT.contains("window.RTCPeerConnection = undefined"),
            "lockdown must set window.RTCPeerConnection = undefined"
        );
    }

    #[test]
    fn test_lockdown_overrides_rtc_session_description() {
        assert!(
            SANDBOX_LOCKDOWN_SCRIPT.contains("window.RTCSessionDescription = undefined"),
            "lockdown must set window.RTCSessionDescription = undefined"
        );
    }

    #[test]
    fn test_lockdown_overrides_rtc_ice_candidate() {
        assert!(
            SANDBOX_LOCKDOWN_SCRIPT.contains("window.RTCIceCandidate = undefined"),
            "lockdown must set window.RTCIceCandidate = undefined"
        );
    }

    #[test]
    fn test_lockdown_overrides_rtc_data_channel() {
        assert!(
            SANDBOX_LOCKDOWN_SCRIPT.contains("window.RTCDataChannel = undefined"),
            "lockdown must set window.RTCDataChannel = undefined"
        );
    }

    #[test]
    fn test_lockdown_overrides_event_source() {
        assert!(
            SANDBOX_LOCKDOWN_SCRIPT.contains("window.EventSource = undefined"),
            "lockdown must set window.EventSource = undefined"
        );
    }

    #[test]
    fn test_lockdown_overrides_shared_worker() {
        assert!(
            SANDBOX_LOCKDOWN_SCRIPT.contains("window.SharedWorker = undefined"),
            "lockdown must set window.SharedWorker = undefined"
        );
    }

    #[test]
    fn test_lockdown_overrides_broadcast_channel() {
        assert!(
            SANDBOX_LOCKDOWN_SCRIPT.contains("window.BroadcastChannel = undefined"),
            "lockdown must set window.BroadcastChannel = undefined"
        );
    }

    // ------------------------------------------------------------------
    // Granular CSP directive assertions
    // ------------------------------------------------------------------

    #[test]
    fn test_csp_blocks_connect() {
        assert!(
            IFRAME_CSP.contains("connect-src 'none'"),
            "CSP must contain connect-src 'none' to block fetch/XHR/WebSocket"
        );
    }

    #[test]
    fn test_csp_blocks_frames() {
        assert!(
            IFRAME_CSP.contains("frame-src 'none'"),
            "CSP must contain frame-src 'none' to prevent nested iframes"
        );
    }

    #[test]
    fn test_csp_blocks_fonts() {
        assert!(
            IFRAME_CSP.contains("font-src 'none'"),
            "CSP must contain font-src 'none' to block external font exfiltration"
        );
    }

    #[test]
    fn test_csp_blocks_media() {
        assert!(
            IFRAME_CSP.contains("media-src 'none'"),
            "CSP must contain media-src 'none' to block audio/video network requests"
        );
    }

    #[test]
    fn test_csp_blocks_objects() {
        assert!(
            IFRAME_CSP.contains("object-src 'none'"),
            "CSP must contain object-src 'none' to block plugin execution"
        );
    }

    #[test]
    fn test_csp_blocks_forms() {
        assert!(
            IFRAME_CSP.contains("form-action 'none'"),
            "CSP must contain form-action 'none' to prevent form-based data exfiltration"
        );
    }

    // ------------------------------------------------------------------
    // New lockdown additions — one assertion per API
    // ------------------------------------------------------------------

    #[test]
    fn test_lockdown_blocks_worker() {
        assert!(
            SANDBOX_LOCKDOWN_SCRIPT.contains("window.Worker = undefined"),
            "lockdown must set window.Worker = undefined"
        );
    }

    #[test]
    fn test_lockdown_blocks_service_worker() {
        assert!(
            SANDBOX_LOCKDOWN_SCRIPT.contains("'serviceWorker'"),
            "lockdown must override navigator.serviceWorker"
        );
        assert!(
            SANDBOX_LOCKDOWN_SCRIPT.contains("serviceWorker is blocked"),
            "lockdown must throw SecurityError for serviceWorker"
        );
    }

    #[test]
    fn test_lockdown_blocks_local_storage() {
        assert!(
            SANDBOX_LOCKDOWN_SCRIPT.contains("'localStorage'"),
            "lockdown must block localStorage"
        );
        assert!(
            SANDBOX_LOCKDOWN_SCRIPT.contains("'sessionStorage'"),
            "lockdown must block sessionStorage"
        );
    }

    #[test]
    fn test_lockdown_blocks_indexed_db() {
        assert!(
            SANDBOX_LOCKDOWN_SCRIPT.contains("indexedDB.open"),
            "lockdown must block indexedDB.open"
        );
    }

    #[test]
    fn test_lockdown_blocks_cache_storage() {
        assert!(
            SANDBOX_LOCKDOWN_SCRIPT.contains("caches.open"),
            "lockdown must block caches.open"
        );
        assert!(
            SANDBOX_LOCKDOWN_SCRIPT.contains("caches.delete"),
            "lockdown must block caches.delete"
        );
    }

    #[test]
    fn test_lockdown_blocks_cookies() {
        assert!(
            SANDBOX_LOCKDOWN_SCRIPT.contains("document, 'cookie'"),
            "lockdown must override document.cookie"
        );
    }

    #[test]
    fn test_lockdown_blocks_wallet_sniffing() {
        assert!(
            SANDBOX_LOCKDOWN_SCRIPT.contains("'injectedWeb3'"),
            "lockdown must freeze window.injectedWeb3"
        );
        assert!(
            SANDBOX_LOCKDOWN_SCRIPT.contains("'polkadot'"),
            "lockdown must freeze window.polkadot"
        );
        assert!(
            SANDBOX_LOCKDOWN_SCRIPT.contains("'ethereum'"),
            "lockdown must freeze window.ethereum"
        );
    }

    #[test]
    fn test_lockdown_blocks_send_beacon() {
        assert!(
            SANDBOX_LOCKDOWN_SCRIPT.contains("sendBeacon"),
            "lockdown must neutralise navigator.sendBeacon"
        );
    }

    // ------------------------------------------------------------------
    // Monitor script assertions
    // ------------------------------------------------------------------

    #[test]
    fn test_monitor_script_reports_violations() {
        // Every blocked API in the monitor script must call report(), which
        // issues a postMessage with type "SANDBOX_VIOLATION".
        assert!(
            SANDBOX_MONITOR_SCRIPT.contains("SANDBOX_VIOLATION"),
            "monitor script must emit SANDBOX_VIOLATION messages"
        );
        assert!(
            SANDBOX_MONITOR_SCRIPT.contains("postMessage"),
            "monitor script must use postMessage to surface violations"
        );
        // The monitor still throws after reporting — it is not a silent pass-through.
        assert!(
            SANDBOX_MONITOR_SCRIPT.contains("SecurityError"),
            "monitor script must still throw SecurityError after reporting"
        );
    }

    #[test]
    fn test_monitor_script_covers_all_lockdown_apis() {
        // The monitor must instrument every API that the lockdown disables.
        for api in &[
            "WebSocket",
            "Worker",
            "serviceWorker",
            "RTCPeerConnection",
            "RTCSessionDescription",
            "RTCIceCandidate",
            "RTCDataChannel",
            "SharedWorker",
            "BroadcastChannel",
            "EventSource",
            "sendBeacon",
            "indexedDB.open",
            "indexedDB.deleteDatabase",
            "caches.open",
            "caches.delete",
            "localStorage",
            "sessionStorage",
            "document.cookie",
            "injectedWeb3",
            "polkadot",
            "ethereum",
            "createElement(iframe)",
        ] {
            assert!(
                SANDBOX_MONITOR_SCRIPT.contains(api),
                "monitor script is missing coverage for API: {}",
                api
            );
        }
    }

    #[test]
    fn test_monitor_script_wraps_fetch_and_xhr() {
        assert!(
            SANDBOX_MONITOR_SCRIPT.contains("window.fetch"),
            "monitor script must wrap fetch for cross-origin detection"
        );
        assert!(
            SANDBOX_MONITOR_SCRIPT.contains("XMLHttpRequest"),
            "monitor script must wrap XMLHttpRequest.open for cross-origin detection"
        );
    }

    // ------------------------------------------------------------------
    // csp_for_scheme
    // ------------------------------------------------------------------

    #[test]
    fn test_csp_for_scheme_basic() {
        let csp = csp_for_scheme("dotapp", false, &[]);
        assert!(
            csp.contains("default-src 'self' dotapp:;"),
            "must include scheme in default-src"
        );
        assert!(
            csp.contains("connect-src 'self' dotapp:;"),
            "must include self and scheme in connect-src"
        );
        assert!(
            !csp.contains("wss:"),
            "wss: must not appear when chain_access is false"
        );
    }

    #[test]
    fn test_csp_for_scheme_with_chain_access() {
        let csp = csp_for_scheme("dotapp", true, &[]);
        assert!(
            csp.contains("connect-src 'self' dotapp: wss:;"),
            "connect-src must include wss: when chain_access is true"
        );
    }

    #[test]
    fn test_csp_for_scheme_with_approved_origins() {
        let csp = csp_for_scheme("dotapp", false, &["https://api.example.com"]);
        assert!(
            csp.contains("connect-src 'self' dotapp: https://api.example.com;"),
            "approved origin must appear in connect-src"
        );
    }

    #[test]
    fn test_csp_for_scheme_with_chain_access_and_origins() {
        let csp = csp_for_scheme(
            "myapp",
            true,
            &["https://rpc.example.com", "https://cdn.example.com"],
        );
        assert!(csp.contains(
            "connect-src 'self' myapp: wss: https://rpc.example.com https://cdn.example.com;"
        ));
    }

    #[test]
    fn test_csp_for_scheme_includes_required_directives() {
        let csp = csp_for_scheme("dotapp", false, &[]);
        assert!(csp.contains("worker-src 'none'"), "must block worker-src");
        assert!(csp.contains("child-src 'none'"), "must block child-src");
        assert!(csp.contains("object-src 'none'"), "must block object-src");
        assert!(csp.contains("frame-src 'none'"), "must block frame-src");
        assert!(csp.contains("base-uri 'none'"), "must block base-uri");
        assert!(csp.contains("form-action 'none'"), "must block form-action");
        assert!(csp.contains("'unsafe-inline'"), "must allow inline scripts");
        assert!(csp.contains("'wasm-unsafe-eval'"), "must allow WASM eval");
    }

    #[test]
    fn test_csp_for_scheme_img_and_font_include_scheme() {
        let csp = csp_for_scheme("dotapp", false, &[]);
        assert!(
            csp.contains("img-src 'self' dotapp: data: blob:"),
            "img-src must include scheme"
        );
        assert!(
            csp.contains("font-src 'self' dotapp: data:"),
            "font-src must include scheme"
        );
    }

    #[test]
    fn test_csp_for_scheme_custom_scheme_name() {
        let csp = csp_for_scheme("polkadotapp", false, &[]);
        assert!(
            csp.contains("polkadotapp:"),
            "custom scheme name must appear in CSP"
        );
        // Verify the scheme in default-src is the full custom name (not some other scheme).
        assert!(
            csp.contains("default-src 'self' polkadotapp:"),
            "default-src must use the custom scheme"
        );
    }

    // ------------------------------------------------------------------
    // inject_into_head
    // ------------------------------------------------------------------

    #[test]
    fn test_inject_into_head_normal() {
        let html = b"<html><head><title>Test</title></head><body></body></html>";
        let result = inject_into_head(html, "default-src 'self'", "").unwrap();
        let out = String::from_utf8(result).unwrap();
        // CSP meta must appear right after <head>
        assert!(
            out.contains(r#"<head><meta http-equiv="Content-Security-Policy""#),
            "CSP meta must be injected immediately after <head>"
        );
    }

    #[test]
    fn test_inject_into_head_uppercase_head_tag() {
        let html = b"<html><HEAD><title>Test</title></HEAD><body></body></html>";
        let result = inject_into_head(html, "default-src 'self'", "").unwrap();
        let out = String::from_utf8(result).unwrap();
        assert!(
            out.contains(r#"<HEAD><meta http-equiv="Content-Security-Policy""#),
            "must handle uppercase HEAD tag"
        );
    }

    #[test]
    fn test_inject_into_head_skips_commented_head() {
        // The <head> inside the comment must be ignored; the real one must be found.
        let html = b"<!-- <head> fake --><html><head><title>T</title></head></html>";
        let result = inject_into_head(html, "csp", "").unwrap();
        let out = String::from_utf8(result).unwrap();
        // The injection point must be the real <head>, not the one in the comment.
        let fake_head_pos = out.find("<!-- <head>").unwrap();
        let real_inject_pos = out.find(r#"<head><meta"#).unwrap();
        assert!(
            real_inject_pos > fake_head_pos,
            "injection must happen at the real <head>, not the commented one"
        );
        // The comment must be left intact.
        assert!(out.contains("<!-- <head> fake -->"));
    }

    #[test]
    fn test_inject_into_head_no_head_tag_prepends() {
        let html = b"<p>No head here</p>";
        let result = inject_into_head(html, "default-src 'self'", "").unwrap();
        let out = String::from_utf8(result).unwrap();
        assert!(
            out.starts_with(r#"<meta http-equiv="Content-Security-Policy""#),
            "must prepend when no <head> tag is present"
        );
        assert!(out.ends_with("<p>No head here</p>"));
    }

    #[test]
    fn test_inject_into_head_escapes_csp_quotes() {
        let html = b"<html><head></head></html>";
        // A CSP with embedded double-quotes must be HTML-attribute-escaped.
        let csp = r#"default-src "self""#;
        let result = inject_into_head(html, csp, "").unwrap();
        let out = String::from_utf8(result).unwrap();
        assert!(
            out.contains("&quot;self&quot;"),
            "double quotes in CSP must be escaped as &quot;"
        );
        assert!(
            !out.contains(r#"content="default-src "self""#),
            "raw quotes must not appear"
        );
    }

    #[test]
    fn test_inject_into_head_escapes_csp_angle_brackets() {
        let html = b"<html><head></head></html>";
        let csp = "default-src <self>";
        let result = inject_into_head(html, csp, "").unwrap();
        let out = String::from_utf8(result).unwrap();
        assert!(
            out.contains("&lt;self&gt;"),
            "angle brackets in CSP must be escaped"
        );
    }

    #[test]
    fn test_inject_into_head_escapes_csp_ampersand() {
        let html = b"<html><head></head></html>";
        let csp = "default-src 'self' & 'nonce-abc'";
        let result = inject_into_head(html, csp, "").unwrap();
        let out = String::from_utf8(result).unwrap();
        assert!(
            out.contains("&amp;"),
            "ampersand in CSP must be escaped as &amp;"
        );
    }

    #[test]
    fn test_inject_into_head_includes_scripts() {
        let html = b"<html><head></head><body></body></html>";
        let js = "window.__init=true;";
        let result = inject_into_head(html, "default-src 'self'", js).unwrap();
        let out = String::from_utf8(result).unwrap();
        // Raw JS must be wrapped in <script> tags by the function.
        assert!(
            out.contains("<script>window.__init=true;</script>"),
            "raw JS must be wrapped in <script> tags"
        );
        // Script block must appear after the CSP meta, inside <head>.
        let csp_pos = out.find(r#"<meta http-equiv"#).unwrap();
        let script_pos = out.find("<script>").unwrap();
        assert!(
            script_pos > csp_pos,
            "script block must come after the CSP meta tag"
        );
    }

    #[test]
    fn test_inject_into_head_empty_script_body_no_script_tag() {
        let html = b"<html><head></head><body></body></html>";
        let result = inject_into_head(html, "default-src 'self'", "").unwrap();
        let out = String::from_utf8(result).unwrap();
        assert!(
            !out.contains("<script>"),
            "empty script_body must not produce <script> tags"
        );
    }

    #[test]
    fn test_inject_into_head_escapes_close_script_tag() {
        let html = b"<html><head></head></html>";
        let js = r#"var x = "</script><b>injected</b>";"#;
        let result = inject_into_head(html, "default-src 'self'", js).unwrap();
        let out = String::from_utf8(result).unwrap();
        // The literal </script> in the JS must not close the tag early.
        assert!(
            !out.contains("</script><b>"),
            "raw </script> in script_body must not break out of the tag"
        );
        assert!(
            out.contains(r"<\/script>"),
            "</script> must be escaped as <\\/script>"
        );
    }

    #[test]
    fn test_inject_into_head_invalid_utf8_returns_error() {
        // 0xFF is not valid UTF-8.
        let bad_html: &[u8] = &[0xFF, 0xFE, 0x00];
        let result = inject_into_head(bad_html, "csp", "");
        assert!(result.is_err(), "invalid UTF-8 must return Err");
    }
}
