//! Plugin and resource registration system
//!
//! This module provides a framework-level registration system that allows:
//! - Implementation-generated code to register resources dynamically
//! - Config-driven resource loading from TOML files
//! - Multiple contributors (implementation, plugins) to coexist
//! - No hardcoding or conditional compilation in framework
//!
//! The key insight: Framework provides EMPTY registries + registration API.
//! Implementation/plugins include THEIR OWN generated code and register it.

use std::collections::HashMap;
use std::sync::RwLock;

use once_cell::sync::Lazy;
use rustelo_core_types::rendering::RenderingProfile;
use serde::Deserialize;

// ============================================================================
// REGISTRATION TRAITS
// ============================================================================

/// Error type for registration operations
#[derive(Debug, Clone)]
pub enum RegistrationError {
    /// Resource already registered
    DuplicateResource(String),
    /// Required resource not found
    NotFound(String),
    /// Configuration error
    ConfigError(String),
    /// IO error
    IoError(String),
}

impl std::fmt::Display for RegistrationError {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        match self {
            Self::DuplicateResource(key) => write!(f, "Resource already registered: {}", key),
            Self::NotFound(key) => write!(f, "Resource not found: {}", key),
            Self::ConfigError(msg) => write!(f, "Configuration error: {}", msg),
            Self::IoError(msg) => write!(f, "IO error: {}", msg),
        }
    }
}

impl std::error::Error for RegistrationError {}

/// Trait for types that contribute resources to the system
///
/// Implementations include: framework defaults, generated code from build.rs,
/// plugins
pub trait ResourceContributor: Send + Sync {
    /// Contribute menu resources
    /// Returns: HashMap<language, TOML content>
    fn contribute_menus(&self) -> HashMap<String, String> {
        HashMap::new()
    }

    /// Contribute footer resources
    /// Returns: HashMap<name, TOML content>
    fn contribute_footers(&self) -> HashMap<String, String> {
        HashMap::new()
    }

    /// Contribute theme resources
    /// Returns: HashMap<theme_name, TOML content>
    fn contribute_themes(&self) -> HashMap<String, String> {
        HashMap::new()
    }

    /// Contribute FTL (Fluent) translation resources
    /// Returns: HashMap<language, FTL content>
    fn contribute_ftl(&self) -> HashMap<String, String> {
        HashMap::new()
    }

    /// Contribute route configurations
    /// Returns: HashMap<language, TOML content in RoutesConfig format>
    ///
    /// When non-empty, runtime route loading skips filesystem scanning and uses
    /// this embedded data instead.  Build scripts generate this from
    /// site/config.ncl.
    fn contribute_routes(&self) -> HashMap<String, String> {
        HashMap::new()
    }

    /// Contributor name (for debugging)
    fn name(&self) -> &str {
        "unknown"
    }

    /// Rendering profiles this contributor's resources are compatible with.
    ///
    /// Boot-time validation warns when a route's effective profile is not in
    /// this set. Default is `[LeptosHydration]` for backward compatibility with
    /// contributors written before the htmx-ssr profile existed.
    fn supported_rendering_profiles(&self) -> Vec<RenderingProfile> {
        vec![RenderingProfile::LeptosHydration]
    }
}

/// Trait for types that contribute pages/components
pub trait PageContributor: Send + Sync {
    /// List of page component names this contributor provides
    fn provided_pages(&self) -> Vec<String>;

    /// Contributor name (for debugging)
    fn name(&self) -> &str {
        "unknown"
    }

    /// Rendering profiles this contributor's pages are compatible with.
    ///
    /// Same semantics as [`ResourceContributor::supported_rendering_profiles`].
    fn supported_rendering_profiles(&self) -> Vec<RenderingProfile> {
        vec![RenderingProfile::LeptosHydration]
    }
}

// ============================================================================
// RESOURCE REGISTRIES (EMPTY - to be populated by contributors)
// ============================================================================

/// Global menu registry: language -> TOML content string
pub static MENU_REGISTRY: Lazy<RwLock<HashMap<String, String>>> =
    Lazy::new(|| RwLock::new(HashMap::new()));

/// Global footer registry: name -> TOML content string
pub static FOOTER_REGISTRY: Lazy<RwLock<HashMap<String, String>>> =
    Lazy::new(|| RwLock::new(HashMap::new()));

/// Global theme registry: theme_name -> TOML content string
pub static THEME_REGISTRY: Lazy<RwLock<HashMap<String, String>>> =
    Lazy::new(|| RwLock::new(HashMap::new()));

/// Global FTL (translation) registry: language -> FTL content string
pub static FTL_REGISTRY: Lazy<RwLock<HashMap<String, String>>> =
    Lazy::new(|| RwLock::new(HashMap::new()));

/// Global page registry: component_name -> page definition
pub static PAGE_REGISTRY: Lazy<RwLock<HashMap<String, String>>> =
    Lazy::new(|| RwLock::new(HashMap::new()));

/// Global routes registry: language -> TOML content string (RoutesConfig
/// format)
///
/// Populated by `WebsiteResourceContributor::contribute_routes()` at startup.
/// When non-empty, `routing::config::load_routes_config()` uses this instead
/// of scanning the filesystem.
pub static ROUTES_REGISTRY: Lazy<RwLock<HashMap<String, String>>> =
    Lazy::new(|| RwLock::new(HashMap::new()));

// ============================================================================
// REGISTRATION API
// ============================================================================

/// Register a menu resource
pub fn register_menu(language: String, content: String) -> Result<(), RegistrationError> {
    let mut registry = MENU_REGISTRY
        .write()
        .map_err(|e| RegistrationError::ConfigError(format!("Failed to acquire lock: {}", e)))?;

    if registry.contains_key(&language) {
        return Err(RegistrationError::DuplicateResource(format!(
            "Menu for language: {}",
            language
        )));
    }

    registry.insert(language, content);
    Ok(())
}

/// Register a footer resource
pub fn register_footer(name: String, content: String) -> Result<(), RegistrationError> {
    let mut registry = FOOTER_REGISTRY
        .write()
        .map_err(|e| RegistrationError::ConfigError(format!("Failed to acquire lock: {}", e)))?;

    if registry.contains_key(&name) {
        return Err(RegistrationError::DuplicateResource(format!(
            "Footer: {}",
            name
        )));
    }

    registry.insert(name, content);
    Ok(())
}

/// Register a theme resource
pub fn register_theme(name: String, content: String) -> Result<(), RegistrationError> {
    let mut registry = THEME_REGISTRY
        .write()
        .map_err(|e| RegistrationError::ConfigError(format!("Failed to acquire lock: {}", e)))?;

    if registry.contains_key(&name) {
        return Err(RegistrationError::DuplicateResource(format!(
            "Theme: {}",
            name
        )));
    }

    registry.insert(name, content);
    Ok(())
}

/// Register a routes resource (per language, RoutesConfig TOML format)
pub fn register_routes(language: String, content: String) -> Result<(), RegistrationError> {
    let mut registry = ROUTES_REGISTRY
        .write()
        .map_err(|e| RegistrationError::ConfigError(format!("Failed to acquire lock: {}", e)))?;

    if registry.contains_key(&language) {
        return Err(RegistrationError::DuplicateResource(format!(
            "Routes for language: {}",
            language
        )));
    }

    registry.insert(language, content);
    Ok(())
}

/// Register a FTL (translation) resource
pub fn register_ftl(language: String, content: String) -> Result<(), RegistrationError> {
    let mut registry = FTL_REGISTRY
        .write()
        .map_err(|e| RegistrationError::ConfigError(format!("Failed to acquire lock: {}", e)))?;

    if registry.contains_key(&language) {
        return Err(RegistrationError::DuplicateResource(format!(
            "FTL for language: {}",
            language
        )));
    }

    registry.insert(language, content);
    Ok(())
}

/// Warning emitted when a route's resolved rendering profile is not declared
/// as supported by the contributor that provides the route.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ProfileSupportWarning {
    pub contributor: String,
    pub route_path: String,
    pub effective_profile: RenderingProfile,
    pub supported: Vec<RenderingProfile>,
}

impl std::fmt::Display for ProfileSupportWarning {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        write!(
            f,
            "contributor `{}` does not declare support for `{}` on route `{}` (supported: {:?})",
            self.contributor, self.effective_profile, self.route_path, self.supported,
        )
    }
}

/// Validate that every route's effective rendering profile is in the
/// contributor's declared `supported_rendering_profiles()`. Mismatches are
/// logged via `tracing::warn!` and also returned for callers that want to
/// surface them programmatically (e.g. a CI gate).
pub fn validate_contributor_profiles<'a, I>(
    contributor: &dyn ResourceContributor,
    routes: I,
) -> Vec<ProfileSupportWarning>
where
    I: IntoIterator<Item = (&'a str, RenderingProfile)>,
{
    let supported = contributor.supported_rendering_profiles();
    let name = contributor.name().to_owned();
    let mut warnings = Vec::new();
    for (path, effective) in routes {
        if !supported.contains(&effective) {
            let w = ProfileSupportWarning {
                contributor: name.clone(),
                route_path: path.to_owned(),
                effective_profile: effective,
                supported: supported.clone(),
            };
            tracing::warn!(
                contributor = %w.contributor,
                route = %w.route_path,
                effective = ?w.effective_profile,
                supported = ?w.supported,
                "route rendering profile not declared as supported by contributor",
            );
            warnings.push(w);
        }
    }
    warnings
}

/// Register all resources from a contributor.
///
/// After registering, call [`validate_contributor_profiles`] with the resolved
/// route list to surface compatibility warnings between the contributor's
/// declared [`ResourceContributor::supported_rendering_profiles`] and the
/// effective profile of each route. Validation is opt-in (non-fatal) so legacy
/// contributors keep working until they declare their profile support
/// explicitly.
pub fn register_contributor(
    contributor: &dyn ResourceContributor,
) -> Result<(), RegistrationError> {
    // Register menus
    for (language, content) in contributor.contribute_menus() {
        register_menu(language, content)?;
    }

    // Register footers
    for (name, content) in contributor.contribute_footers() {
        register_footer(name, content)?;
    }

    // Register themes
    for (name, content) in contributor.contribute_themes() {
        register_theme(name, content)?;
    }

    // Register FTL
    for (language, content) in contributor.contribute_ftl() {
        register_ftl(language, content)?;
    }

    // Register routes (per-language RoutesConfig TOML, skips filesystem scan at
    // runtime)
    for (language, content) in contributor.contribute_routes() {
        register_routes(language, content)?;
    }

    Ok(())
}

// ============================================================================
// RESOURCE ACCESS API (reads from registries)
// ============================================================================

/// Get a menu resource by language
pub fn get_menu(language: &str) -> Option<String> {
    MENU_REGISTRY.read().ok()?.get(language).cloned()
}

/// Get a footer resource by name
pub fn get_footer(name: &str) -> Option<String> {
    FOOTER_REGISTRY.read().ok()?.get(name).cloned()
}

/// Get a theme resource by name
pub fn get_theme(name: &str) -> Option<String> {
    THEME_REGISTRY.read().ok()?.get(name).cloned()
}

/// Get a FTL (translation) resource by language
pub fn get_ftl(language: &str) -> Option<String> {
    FTL_REGISTRY.read().ok()?.get(language).cloned()
}

/// Get a routes resource by language (RoutesConfig TOML format)
pub fn get_routes(language: &str) -> Option<String> {
    ROUTES_REGISTRY.read().ok()?.get(language).cloned()
}

/// Returns true if any routes have been registered via `contribute_routes()`
pub fn routes_registered() -> bool {
    ROUTES_REGISTRY
        .read()
        .map(|r| !r.is_empty())
        .unwrap_or(false)
}

/// List all registered languages (menus)
pub fn list_languages() -> Vec<String> {
    MENU_REGISTRY
        .read()
        .map(|r| r.keys().cloned().collect())
        .unwrap_or_default()
}

/// List all registered themes
pub fn list_themes() -> Vec<String> {
    THEME_REGISTRY
        .read()
        .map(|r| r.keys().cloned().collect())
        .unwrap_or_default()
}

/// List all registered pages
pub fn list_pages() -> Vec<String> {
    PAGE_REGISTRY
        .read()
        .map(|r| r.keys().cloned().collect())
        .unwrap_or_default()
}

/// Parse FTL resources for a language and return flat key→value translations.
///
/// Reads all entries from `FTL_REGISTRY` whose key starts with `{language}_`,
/// builds a Fluent bundle from their concatenated content, and returns all
/// resolved message values.  Returns `None` when no FTL data is registered for
/// the requested language.
pub fn get_parsed_ftl_for_language(
    language: &str,
) -> Option<std::collections::HashMap<String, String>> {
    use fluent_bundle::{FluentBundle, FluentResource};
    use fluent_syntax::ast;
    use unic_langid::LanguageIdentifier;

    let registry = FTL_REGISTRY.read().ok()?;
    let lang_prefix = format!("{}_", language);

    let langid: LanguageIdentifier = language.parse().ok()?;
    let mut bundle = FluentBundle::new(vec![langid]);
    let mut all_message_ids = std::collections::HashSet::<String>::new();
    let mut found_any = false;

    for (key, content) in registry.iter() {
        if key.starts_with(&lang_prefix) {
            if let Ok(resource) = FluentResource::try_new(content.clone()) {
                for entry in resource.entries() {
                    if let ast::Entry::Message(message) = entry {
                        all_message_ids.insert(message.id.name.to_string());
                    }
                }
                let _ = bundle.add_resource(resource);
                found_any = true;
            }
        }
    }

    if !found_any {
        return None;
    }

    let mut translations = std::collections::HashMap::new();
    for message_id in &all_message_ids {
        if let Some(message) = bundle.get_message(message_id) {
            if let Some(pattern) = message.value() {
                let mut errors = Vec::new();
                let value = bundle.format_pattern(pattern, None, &mut errors);
                // Accept the formatted value even when errors are present: Fluent
                // enters recovery mode for missing variables, producing the
                // placeholder text (e.g. "{ $email }") that call-sites replace
                // manually.  Hard failures (parse errors) are caught earlier at
                // `FluentResource::try_new()`.
                translations.insert(message_id.clone(), value.to_string());
            }
        }
    }

    Some(translations)
}

// ============================================================================
// CONFIG-DRIVEN LOADER (PAP-COMPLIANT)
// ============================================================================

/// Load resources from configuration paths (PAP-compliant)
///
/// This function loads resources from TOML files at runtime, respecting
/// environment variables and the manifest system.
pub fn load_resources_from_config() -> Result<(), RegistrationError> {
    // Get config root from path resolution module (respects SITE_CONFIG_PATH env
    // var)
    let config_root = std::path::PathBuf::from(crate::path_resolution::get_config_path());

    // Load menus from config/menus/*.toml (required)
    load_menus_from_config(&config_root)?;

    // Load footers from config/footers/*.toml (optional)
    // Errors in optional folders are logged but not returned
    let _ = load_footers_from_config(&config_root);

    // Load themes from config/themes/*.toml (optional)
    let _ = load_themes_from_config(&config_root);

    // Load FTL translations from content/i18n/*.ftl (optional)
    let _ = load_ftl_from_config(&config_root);

    Ok(())
}

/// Load menu configurations from site/ui/menus/*.toml or config/menus/*.toml
/// Skips loading if menus are already in the registry (e.g., from build.rs
/// embedding)
fn load_menus_from_config(config_root: &std::path::Path) -> Result<(), RegistrationError> {
    // Check if menus are already registered (contributed via
    // WebsiteResourceContributor). The embedded key is "main" (unified menu),
    // not per-language — so we check non-empty rather than specific language
    // keys.
    if MENU_REGISTRY.read().map(|r| !r.is_empty()).unwrap_or(false) {
        tracing::debug!("menus already registered via contributor; skipping filesystem load");
        return Ok(());
    }

    // Legacy filesystem menu loader. Modern profiles compose navigation from the
    // route table instead: the htmx-ssr shell reads the `[menu]` blocks of
    // `routes.ncl` directly, and the Leptos path derives items via
    // `routing::engine::navigation::generate_navigation_items` from
    // `route.menu_group`/`menu_order`. A missing `site/ui/menus` directory is
    // therefore the normal, expected state — logged at debug, never warned.
    let paths_to_try = [
        // site/ui/menus (legacy preferred location)
        config_root
            .parent()
            .map(|parent| parent.join("ui").join("menus")),
        // ../ui/menus when config is in a subdirectory
        Some(config_root.join("../ui/menus")),
        // config/menus fallback
        Some(config_root.join("menus")),
        // absolute paths from the current working directory
        Some(std::path::PathBuf::from("site/ui/menus")),
        Some(std::path::PathBuf::from("./site/ui/menus")),
    ];

    for path in paths_to_try.into_iter().flatten() {
        if path.exists() {
            tracing::debug!(dir = %path.display(), "loading legacy filesystem menus");
            return load_menus_from_directory(&path);
        }
    }

    tracing::debug!(
        config_root = %config_root.display(),
        "no legacy menu directory found; navigation is route-derived"
    );
    Ok(()) // Directory doesn't exist is not an error
}

/// Helper function to load menus from a given directory
fn load_menus_from_directory(menus_dir: &std::path::Path) -> Result<(), RegistrationError> {
    tracing::debug!(dir = %menus_dir.display(), "scanning legacy menus directory");

    let entries: Vec<_> = std::fs::read_dir(menus_dir)
        .map_err(|e| RegistrationError::IoError(format!("Failed to read menus directory: {}", e)))?
        .collect::<Result<Vec<_>, _>>()
        .map_err(|e| {
            RegistrationError::IoError(format!("Failed to read directory entry: {}", e))
        })?;

    let mut menu_count = 0;
    for entry in entries {
        let path = entry.path();

        if path.extension().and_then(|s| s.to_str()) == Some("toml") {
            if let Some(language) = path.file_stem().and_then(|s| s.to_str()) {
                let content = std::fs::read_to_string(&path).map_err(|e| {
                    RegistrationError::IoError(format!(
                        "Failed to read menu file {:?}: {}",
                        path, e
                    ))
                })?;

                tracing::debug!(
                    language = %language,
                    bytes = content.len(),
                    "registering legacy menu"
                );
                register_menu(language.to_string(), content)?;
                menu_count += 1;
            }
        }
    }

    tracing::debug!(count = menu_count, "loaded legacy menus");
    Ok(())
}

/// Load footer configurations from config/footers/*.toml (optional)
fn load_footers_from_config(config_root: &std::path::Path) -> Result<(), RegistrationError> {
    let footers_dir = config_root.join("footers");

    if !footers_dir.exists() {
        return Ok(());
    }

    for entry in std::fs::read_dir(&footers_dir).map_err(|e| {
        RegistrationError::IoError(format!("Failed to read footers directory: {}", e))
    })? {
        let entry = entry.map_err(|e| {
            RegistrationError::IoError(format!("Failed to read directory entry: {}", e))
        })?;

        let path = entry.path();
        if path.extension().and_then(|s| s.to_str()) == Some("toml") {
            if let Some(name) = path.file_stem().and_then(|s| s.to_str()) {
                let content = std::fs::read_to_string(&path).map_err(|e| {
                    RegistrationError::IoError(format!(
                        "Failed to read footer file {:?}: {}",
                        path, e
                    ))
                })?;

                register_footer(name.to_string(), content)?;
            }
        }
    }

    Ok(())
}

/// Load theme configurations from config/themes/*.toml (optional)
fn load_themes_from_config(config_root: &std::path::Path) -> Result<(), RegistrationError> {
    let themes_dir = config_root.join("themes");

    if !themes_dir.exists() {
        return Ok(());
    }

    for entry in std::fs::read_dir(&themes_dir).map_err(|e| {
        RegistrationError::IoError(format!("Failed to read themes directory: {}", e))
    })? {
        let entry = entry.map_err(|e| {
            RegistrationError::IoError(format!("Failed to read directory entry: {}", e))
        })?;

        let path = entry.path();
        if path.extension().and_then(|s| s.to_str()) == Some("toml") {
            if let Some(name) = path.file_stem().and_then(|s| s.to_str()) {
                let content = std::fs::read_to_string(&path).map_err(|e| {
                    RegistrationError::IoError(format!(
                        "Failed to read theme file {:?}: {}",
                        path, e
                    ))
                })?;

                register_theme(name.to_string(), content)?;
            }
        }
    }

    Ok(())
}

/// Load FTL (Fluent) translations from site/i18n/locales/ at runtime.
///
/// Walks site/i18n/locales/{lang}/**/*.ftl recursively, producing keys in the
/// same format as the build-time loader: `{lang}_{path_underscored}` (e.g.
/// `en_pages_home`). This mirrors `load_ftl_from_locales` in the server
/// build.rs so htmx-ssr can skip build-time FTL baking entirely.
fn load_ftl_from_config(_config_root: &std::path::Path) -> Result<(), RegistrationError> {
    let i18n_root = std::path::PathBuf::from(crate::path_resolution::get_i18n_path());
    let locales_dir = i18n_root.join("locales");

    if !locales_dir.exists() {
        return Ok(());
    }

    for lang_entry in std::fs::read_dir(&locales_dir)
        .map_err(|e| RegistrationError::IoError(format!("Failed to read locales dir: {e}")))?
        .filter_map(|e| e.ok())
    {
        let lang_path = lang_entry.path();
        if !lang_path.is_dir() {
            continue;
        }
        let lang = match lang_path.file_name().and_then(|n| n.to_str()) {
            Some(l) => l.to_string(),
            None => continue,
        };

        // BFS walk — mirrors build-time load_ftl_from_locales
        let mut stack = vec![lang_path.clone()];
        while let Some(dir) = stack.pop() {
            let entries = match std::fs::read_dir(&dir) {
                Ok(e) => e,
                Err(_) => continue,
            };
            for entry in entries.filter_map(|e| e.ok()) {
                let path = entry.path();
                if path.is_dir() {
                    stack.push(path);
                } else if path.extension().is_some_and(|x| x == "ftl") {
                    let rel = path.strip_prefix(&lang_path).unwrap_or(&path);
                    let key_suffix = rel
                        .with_extension("")
                        .components()
                        .filter_map(|c| {
                            if let std::path::Component::Normal(n) = c {
                                n.to_str().map(|s| s.to_string())
                            } else {
                                None
                            }
                        })
                        .collect::<Vec<_>>()
                        .join("_");
                    let key = format!("{lang}_{key_suffix}");
                    match std::fs::read_to_string(&path) {
                        Ok(content) => {
                            tracing::debug!(key, "FTL loaded from locales at runtime");
                            if let Err(e) = register_ftl(key, content) {
                                tracing::warn!("FTL register error: {e}");
                            }
                        }
                        Err(e) => tracing::warn!("Cannot read FTL {:?}: {e}", path),
                    }
                }
            }
        }
    }

    Ok(())
}

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

    #[test]
    fn test_menu_registration() {
        let menu = r#"
[[menu]]
name = "Home"
path = "/"
"#;
        assert!(register_menu("en".to_string(), menu.to_string()).is_ok());
        assert!(get_menu("en").is_some());
    }

    #[test]
    fn test_duplicate_registration_fails() {
        let menu = "[[menu]]\nname = \"Home\"\npath = \"/\"";
        let _ = register_menu("test".to_string(), menu.to_string());
        // Second registration should fail
        let result = register_menu("test".to_string(), menu.to_string());
        assert!(result.is_err());
    }

    struct StubContributor {
        name_: &'static str,
        profiles: Vec<RenderingProfile>,
    }

    impl ResourceContributor for StubContributor {
        fn name(&self) -> &str {
            self.name_
        }

        fn supported_rendering_profiles(&self) -> Vec<RenderingProfile> {
            self.profiles.clone()
        }
    }

    #[test]
    fn validate_profiles_emits_no_warnings_when_supported() {
        let c = StubContributor {
            name_: "site",
            profiles: vec![RenderingProfile::LeptosHydration, RenderingProfile::HtmxSsr],
        };
        let warnings = validate_contributor_profiles(
            &c,
            vec![
                ("/", RenderingProfile::LeptosHydration),
                ("/blog", RenderingProfile::HtmxSsr),
            ],
        );
        assert!(warnings.is_empty());
    }

    #[test]
    fn validate_profiles_warns_on_mismatch() {
        let c = StubContributor {
            name_: "legacy",
            profiles: vec![RenderingProfile::LeptosHydration],
        };
        let warnings = validate_contributor_profiles(
            &c,
            vec![
                ("/", RenderingProfile::LeptosHydration),
                ("/blog", RenderingProfile::HtmxSsr),
                ("/about", RenderingProfile::HtmxSsr),
            ],
        );
        assert_eq!(warnings.len(), 2);
        assert!(warnings.iter().all(|w| w.contributor == "legacy"));
        assert!(warnings.iter().any(|w| w.route_path == "/blog"));
        assert!(warnings.iter().any(|w| w.route_path == "/about"));
    }
}
