//! MIME type detection and asset path validation for serving product bundles.
//!
//! Platform implementations use these helpers when responding to WebView
//! resource requests (e.g. `WKURLSchemeHandler` on iOS/macOS, `WebViewClient`
//! on Android, or a custom fetch handler in WASM).

/// Determine the MIME type for a product asset based on file extension.
///
/// Extension matching is case-insensitive (`Logo.PNG` → `image/png`).
/// Returns a default of `application/octet-stream` for unknown extensions.
pub fn mime_for_path(path: &str) -> &'static str {
    let ext = path.rsplit('.').next().unwrap_or("");
    let ext_lower = ext.to_ascii_lowercase();
    match ext_lower.as_str() {
        "html" | "htm" => "text/html; charset=utf-8",
        "js" | "mjs" => "text/javascript; charset=utf-8",
        "css" => "text/css; charset=utf-8",
        "json" => "application/json; charset=utf-8",
        "wasm" => "application/wasm",
        "svg" => "image/svg+xml",
        "png" => "image/png",
        "jpg" | "jpeg" => "image/jpeg",
        "gif" => "image/gif",
        "webp" => "image/webp",
        "ico" => "image/x-icon",
        "woff" => "font/woff",
        "woff2" => "font/woff2",
        "ttf" => "font/ttf",
        "txt" => "text/plain; charset=utf-8",
        "xml" => "application/xml; charset=utf-8",
        "toml" => "application/toml; charset=utf-8",
        "mp3" => "audio/mpeg",
        "mp4" => "video/mp4",
        "webm" => "video/webm",
        _ => "application/octet-stream",
    }
}

/// Validate a product asset path — reject traversal attempts.
///
/// Returns `true` if the path is safe to serve.
///
/// Rejected patterns:
/// - Directory traversal (`..`)
/// - Absolute UNIX paths (leading `/`)
/// - Absolute Windows paths (leading `\` or containing `:`)
/// - Null bytes (may confuse OS path APIs)
/// - Percent-encoded characters (`%`) which could encode traversal sequences
///   such as `%2e%2e` — callers must percent-decode before calling this function
pub fn is_safe_asset_path(path: &str) -> bool {
    !path.contains("..")
        && !path.starts_with('/')
        && !path.starts_with('\\')
        && !path.contains('\0')
        && !path.contains(':')
        && !path.contains('%')
}

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

    // ------------------------------------------------------------------
    // mime_for_path
    // ------------------------------------------------------------------

    #[test]
    fn mime_html_variants() {
        assert_eq!(mime_for_path("index.html"), "text/html; charset=utf-8");
        assert_eq!(mime_for_path("page.htm"), "text/html; charset=utf-8");
    }

    #[test]
    fn mime_javascript_variants() {
        assert_eq!(mime_for_path("app.js"), "text/javascript; charset=utf-8");
        assert_eq!(
            mime_for_path("module.mjs"),
            "text/javascript; charset=utf-8"
        );
    }

    #[test]
    fn mime_css() {
        assert_eq!(mime_for_path("style.css"), "text/css; charset=utf-8");
    }

    #[test]
    fn mime_json() {
        assert_eq!(
            mime_for_path("manifest.json"),
            "application/json; charset=utf-8"
        );
    }

    #[test]
    fn mime_wasm() {
        assert_eq!(mime_for_path("app.wasm"), "application/wasm");
    }

    #[test]
    fn mime_svg() {
        assert_eq!(mime_for_path("icon.svg"), "image/svg+xml");
    }

    #[test]
    fn mime_raster_images() {
        assert_eq!(mime_for_path("photo.png"), "image/png");
        assert_eq!(mime_for_path("photo.jpg"), "image/jpeg");
        assert_eq!(mime_for_path("photo.jpeg"), "image/jpeg");
        assert_eq!(mime_for_path("anim.gif"), "image/gif");
        assert_eq!(mime_for_path("hero.webp"), "image/webp");
        assert_eq!(mime_for_path("favicon.ico"), "image/x-icon");
    }

    #[test]
    fn mime_fonts() {
        assert_eq!(mime_for_path("font.woff"), "font/woff");
        assert_eq!(mime_for_path("font.woff2"), "font/woff2");
        assert_eq!(mime_for_path("font.ttf"), "font/ttf");
    }

    #[test]
    fn mime_text_and_data() {
        assert_eq!(mime_for_path("readme.txt"), "text/plain; charset=utf-8");
        assert_eq!(mime_for_path("feed.xml"), "application/xml; charset=utf-8");
        assert_eq!(
            mime_for_path("config.toml"),
            "application/toml; charset=utf-8"
        );
    }

    #[test]
    fn mime_unknown_extension_is_octet_stream() {
        assert_eq!(mime_for_path("archive.zip"), "application/octet-stream");
        assert_eq!(mime_for_path("data.bin"), "application/octet-stream");
        assert_eq!(mime_for_path("mystery"), "application/octet-stream");
    }

    #[test]
    fn mime_no_extension_is_octet_stream() {
        // rsplit('.').next() returns the whole string when there is no dot,
        // which matches the wildcard arm.
        assert_eq!(mime_for_path("Makefile"), "application/octet-stream");
    }

    #[test]
    fn mime_path_with_directories() {
        // Only the extension matters; directory components are ignored.
        assert_eq!(mime_for_path("assets/images/logo.png"), "image/png");
        assert_eq!(
            mime_for_path("src/worker.js"),
            "text/javascript; charset=utf-8"
        );
    }

    #[test]
    fn mime_empty_path() {
        assert_eq!(mime_for_path(""), "application/octet-stream");
    }

    // ------------------------------------------------------------------
    // is_safe_asset_path
    // ------------------------------------------------------------------

    #[test]
    fn safe_paths_are_accepted() {
        assert!(is_safe_asset_path("index.html"));
        assert!(is_safe_asset_path("assets/logo.png"));
        assert!(is_safe_asset_path("js/app.bundle.js"));
        assert!(is_safe_asset_path("fonts/Inter.woff2"));
    }

    #[test]
    fn traversal_dot_dot_is_rejected() {
        assert!(!is_safe_asset_path("../../etc/passwd"));
        assert!(!is_safe_asset_path("assets/../../../secret"));
        assert!(!is_safe_asset_path(".."));
        assert!(!is_safe_asset_path("a/b/../../../c"));
    }

    #[test]
    fn absolute_unix_path_is_rejected() {
        assert!(!is_safe_asset_path("/etc/passwd"));
        assert!(!is_safe_asset_path("/index.html"));
    }

    #[test]
    fn absolute_windows_path_is_rejected() {
        assert!(!is_safe_asset_path("\\Windows\\System32\\cmd.exe"));
        assert!(!is_safe_asset_path("C:\\secret.txt"));
    }

    #[test]
    fn windows_drive_colon_is_rejected() {
        assert!(!is_safe_asset_path("C:secret.txt"));
        assert!(!is_safe_asset_path("D:/data"));
    }

    #[test]
    fn null_byte_is_rejected() {
        assert!(!is_safe_asset_path("assets/file\0.js"));
        assert!(!is_safe_asset_path("\0"));
    }

    #[test]
    fn empty_path_is_safe() {
        // An empty path triggers no unsafe pattern; callers decide whether to
        // serve a default document or return 404.
        assert!(is_safe_asset_path(""));
    }

    // ------------------------------------------------------------------
    // Additional traversal and encoding edge cases
    // ------------------------------------------------------------------

    #[test]
    fn test_rejects_null_bytes_in_path() {
        // Null bytes can fool OS path APIs into truncating the string,
        // allowing a crafted path like "safe.txt\0../../etc/passwd" to
        // resolve outside the bundle root on some platforms.
        assert!(!is_safe_asset_path("assets/file\0.js"));
        assert!(!is_safe_asset_path("\0"));
        assert!(!is_safe_asset_path("assets/\0index.html"));
    }

    #[test]
    fn test_rejects_windows_drive_letter() {
        // A colon after a single letter indicates a Windows drive
        // (e.g. "C:"), which is an absolute path on Windows hosts.
        assert!(!is_safe_asset_path("C:secret.txt"));
        assert!(!is_safe_asset_path("D:/data/file.js"));
        // Also catches any colon in a path component (not a valid URL-safe
        // file name character for product bundles).
        assert!(!is_safe_asset_path("assets/file:name.js"));
    }

    #[test]
    fn test_rejects_backslash_traversal() {
        // Windows-style path separators must be rejected even on UNIX hosts
        // because some WebView implementations normalise them before the
        // scheme handler sees the path.
        assert!(!is_safe_asset_path("\\Windows\\System32\\cmd.exe"));
        assert!(!is_safe_asset_path("assets\\..\\secret.txt"));
    }

    #[test]
    fn test_accepts_nested_paths() {
        // Multiple directory levels without traversal are safe.
        assert!(is_safe_asset_path("assets/icons/16x16/logo.png"));
        assert!(is_safe_asset_path("js/vendor/polyfill/es5.js"));
        assert!(is_safe_asset_path("data/i18n/en-US/strings.json"));
    }

    #[test]
    fn test_accepts_dotfiles() {
        // Files whose names start with a single dot (e.g. ".well-known") are
        // legitimate product assets and must not be rejected — only double-dot
        // sequences that constitute traversal are blocked.
        assert!(is_safe_asset_path(".well-known/manifest.json"));
        assert!(is_safe_asset_path("assets/.gitkeep"));
        // A single dot component is harmless ("assets/./img.png" does not
        // escape the bundle), but note our validator does not see it because
        // the caller should strip redundant segments before calling us.
        // We ensure a plain dotfile name is accepted regardless.
        assert!(is_safe_asset_path(".hidden"));
    }

    // ------------------------------------------------------------------
    // New: percent-encoded traversal rejection
    // ------------------------------------------------------------------

    #[test]
    fn test_rejects_percent_encoded_path() {
        // %2e%2e is URL-encoded ".." — must be rejected before decode.
        assert!(!is_safe_asset_path("%2e%2e/etc/passwd"));
        assert!(!is_safe_asset_path("assets/%2e%2e/secret"));
        // Any bare percent sign is rejected.
        assert!(!is_safe_asset_path("assets/file%20name.js"));
        assert!(!is_safe_asset_path("%41pplication.wasm"));
    }

    // ------------------------------------------------------------------
    // New: case-insensitive MIME detection
    // ------------------------------------------------------------------

    #[test]
    fn mime_uppercase_extension_matches() {
        assert_eq!(mime_for_path("logo.PNG"), "image/png");
        assert_eq!(mime_for_path("App.JS"), "text/javascript; charset=utf-8");
        assert_eq!(mime_for_path("Style.CSS"), "text/css; charset=utf-8");
        assert_eq!(mime_for_path("index.HTML"), "text/html; charset=utf-8");
        assert_eq!(mime_for_path("font.WOFF2"), "font/woff2");
    }

    #[test]
    fn mime_mixed_case_extension_matches() {
        assert_eq!(
            mime_for_path("module.Mjs"),
            "text/javascript; charset=utf-8"
        );
        assert_eq!(mime_for_path("archive.WaSm"), "application/wasm");
        assert_eq!(mime_for_path("image.JpEg"), "image/jpeg");
    }

    // ------------------------------------------------------------------
    // New: media type extensions
    // ------------------------------------------------------------------

    #[test]
    fn mime_media_types() {
        assert_eq!(mime_for_path("track.mp3"), "audio/mpeg");
        assert_eq!(mime_for_path("video.mp4"), "video/mp4");
        assert_eq!(mime_for_path("clip.webm"), "video/webm");
        // Also case-insensitive
        assert_eq!(mime_for_path("track.MP3"), "audio/mpeg");
        assert_eq!(mime_for_path("video.MP4"), "video/mp4");
    }
}
