use dioprism_native_port::core::{
    CapabilityReport, ConversionChange, ConversionPlan, CssPortabilityIssue,
    DioxusNativePortArtifactOptimizer, DioxusNativePortBaselineReport,
    DioxusNativePortBatchOptions, DioxusNativePortDoctorReport, DioxusNativePortIntegrationOptions,
    DioxusNativePortManifestFragment, DioxusNativePortPrecomputedFragments,
    DioxusNativePortTraceSink, GeneratedAsset, NativeStyleRule, PortabilityCapability, RewriteKind,
    RouteConversion, RoutePortability, RoutePortabilityStatus, Severity, SourceSpan,
    UnsupportedUse, builtin_visual_compatibility_manifest,
    dioprism_native_port_batch_report_fragments, dioprism_native_port_doctor_report_for_plan,
    dioprism_native_port_doctor_report_for_report, dioprism_native_port_integration_options,
    dioprism_native_port_manifest_fragment_from_plan,
    dioprism_native_port_manifest_fragment_from_report,
    dioprism_native_port_manifest_fragment_from_report_traced,
    dioprism_native_port_optimized_artifacts, dioprism_native_port_precomputed_fragments,
    dioprism_native_port_report_baseline,
};
use std::collections::{BTreeMap, BTreeSet};
use std::ffi::OsStr;
use std::fs;
use std::path::{Path, PathBuf};
use std::time::{SystemTime, UNIX_EPOCH};

pub type AuditOpts = AuditOptions;
pub type ConvertOpts = ConvertOptions;

pub fn audit_opts() -> AuditOptions {
    AuditOptions::default()
}

pub fn convert_opts() -> ConvertOptions {
    ConvertOptions::default()
}

pub fn run_from<I, S>(args: I) -> Result<(), String>
where
    I: IntoIterator<Item = S>,
    S: Into<String>,
{
    let mut args = args.into_iter().map(Into::into);
    let command = args.next().unwrap_or_else(|| "audit".to_string());
    if command == "-h" || command == "--help" || command == "help" {
        println!("{}", help_text());
        return Ok(());
    }
    if command == "doctor" || command == "self-check" || command == "d" {
        return run_doctor(args.collect());
    }
    if command == "convert" || command == "c" {
        return run_convert(args.collect());
    }
    if command != "audit" && command != "analyze" && command != "a" {
        return Err(format!("unknown command `{command}`\n\n{}", help_text()));
    }

    run_audit(args.collect())
}

fn run_audit(args: Vec<String>) -> Result<(), String> {
    let mut args = args.into_iter();
    let mut options = AuditOptions::default();
    let mut root: Option<PathBuf> = None;

    while let Some(arg) = args.next() {
        match arg.as_str() {
            "--json" => {
                options.json_output = args.next().map(PathBuf::from);
                if options.json_output.is_none() {
                    return Err("--json requires an output path".to_string());
                }
            }
            "--markdown" | "--md" => {
                options.markdown_output = args.next().map(PathBuf::from);
                if options.markdown_output.is_none() {
                    return Err("--markdown requires an output path".to_string());
                }
            }
            "--format" => {
                options.stdout_format = args
                    .next()
                    .ok_or_else(|| "--format requires `markdown` or `json`".to_string())?;
            }
            "--fail-on-error" => {
                options.fail_on_error = true;
            }
            other if other.starts_with('-') => {
                return Err(format!("unknown option `{other}`\n\n{}", help_text()));
            }
            path => {
                if root.is_some() {
                    return Err("audit accepts at most one project path".to_string());
                }
                root = Some(PathBuf::from(path));
            }
        }
    }

    let root = root.unwrap_or_else(|| PathBuf::from("."));
    let report = audit_project(&root)?;
    write_report_outputs(&report, &options)?;

    if options.fail_on_error && !report.summary.native_ready {
        return Err("native portability audit found blocking issues".to_string());
    }

    Ok(())
}

fn run_convert(args: Vec<String>) -> Result<(), String> {
    let mut args = args.into_iter();
    let mut options = ConvertOptions::default();
    let mut root: Option<PathBuf> = None;

    while let Some(arg) = args.next() {
        match arg.as_str() {
            "--format" => {
                options.stdout_format = args
                    .next()
                    .ok_or_else(|| "--format requires `markdown` or `json`".to_string())?;
            }
            "--out-dir" => {
                options.output_dir = args
                    .next()
                    .map(PathBuf::from)
                    .ok_or_else(|| "--out-dir requires a path".to_string())?;
            }
            "--native-css" => {
                options.native_css_output = args
                    .next()
                    .map(PathBuf::from)
                    .ok_or_else(|| "--native-css requires a path".to_string())?;
            }
            "--route" => {
                options.routes.push(
                    args.next()
                        .ok_or_else(|| "--route requires a path".to_string())?,
                );
            }
            "--write" => {
                options.write = true;
            }
            other if other.starts_with('-') => {
                return Err(format!("unknown option `{other}`\n\n{}", help_text()));
            }
            path => {
                if root.is_some() {
                    return Err("convert accepts at most one project path".to_string());
                }
                root = Some(PathBuf::from(path));
            }
        }
    }

    let root = root.unwrap_or_else(|| PathBuf::from("."));
    let plan = convert_project(&root, &options)?;
    write_conversion_outputs(&root, &plan, &options)?;
    Ok(())
}

fn run_doctor(args: Vec<String>) -> Result<(), String> {
    let mut args = args.into_iter();
    let mut root: Option<PathBuf> = None;

    while let Some(arg) = args.next() {
        match arg.as_str() {
            other if other.starts_with('-') => {
                return Err(format!("unknown option `{other}`\n\n{}", help_text()));
            }
            path => {
                if root.is_some() {
                    return Err("doctor accepts at most one project path".to_string());
                }
                root = Some(PathBuf::from(path));
            }
        }
    }

    let root = root.unwrap_or_else(|| PathBuf::from("."));
    let report = audit_project(&root)?;
    let doctor = doctor_report_for_audit(&report);
    println!("{}", doctor.to_markdown());
    if !doctor.ok() {
        return Err("dioxus native-port doctor found blocking issues".to_string());
    }
    Ok(())
}

pub fn help_text() -> &'static str {
    "Usage:\n  dioprism-native-port audit|analyze|a [path] [--format markdown|json] [--json path] [--markdown path] [--fail-on-error]\n  dioprism-native-port convert|c [path] [--format markdown|json] [--out-dir path] [--native-css path] [--route path] [--write]\n  dioprism-native-port doctor|self-check|d [path]"
}

#[derive(Clone, Debug)]
pub struct AuditOptions {
    pub stdout_format: String,
    pub json_output: Option<PathBuf>,
    pub markdown_output: Option<PathBuf>,
    pub fail_on_error: bool,
}

impl Default for AuditOptions {
    fn default() -> Self {
        Self {
            stdout_format: "markdown".to_string(),
            json_output: None,
            markdown_output: None,
            fail_on_error: false,
        }
    }
}

impl AuditOptions {
    pub fn format(mut self, stdout_format: impl Into<String>) -> Self {
        self.stdout_format = stdout_format.into();
        self
    }

    pub fn json(mut self, path: impl Into<PathBuf>) -> Self {
        self.json_output = Some(path.into());
        self
    }

    pub fn markdown(mut self, path: impl Into<PathBuf>) -> Self {
        self.markdown_output = Some(path.into());
        self
    }

    pub fn fail(mut self, fail_on_error: bool) -> Self {
        self.fail_on_error = fail_on_error;
        self
    }
}

#[derive(Clone, Debug)]
pub struct ConvertOptions {
    pub stdout_format: String,
    pub output_dir: PathBuf,
    pub native_css_output: PathBuf,
    pub write: bool,
    pub routes: Vec<String>,
}

impl Default for ConvertOptions {
    fn default() -> Self {
        Self {
            stdout_format: "markdown".to_string(),
            output_dir: PathBuf::from("tmp/native-port-convert"),
            native_css_output: PathBuf::from("public/assets/native-port.css"),
            write: false,
            routes: Vec::new(),
        }
    }
}

impl ConvertOptions {
    pub fn format(mut self, stdout_format: impl Into<String>) -> Self {
        self.stdout_format = stdout_format.into();
        self
    }

    pub fn out_dir(mut self, output_dir: impl Into<PathBuf>) -> Self {
        self.output_dir = output_dir.into();
        self
    }

    pub fn css(mut self, native_css_output: impl Into<PathBuf>) -> Self {
        self.native_css_output = native_css_output.into();
        self
    }

    pub fn write(mut self, write: bool) -> Self {
        self.write = write;
        self
    }

    pub fn route(mut self, route: impl Into<String>) -> Self {
        self.routes.push(route.into());
        self
    }
}

#[derive(Clone)]
struct ScanRule {
    pattern: &'static str,
    capability: PortabilityCapability,
    severity: Severity,
    message: &'static str,
    suggestion: &'static str,
}

#[derive(Clone, Debug)]
struct ManifestFeature {
    name: String,
    entries: Vec<String>,
    line: usize,
    snippet: String,
}

pub fn audit_project(root: &Path) -> Result<CapabilityReport, String> {
    let root = root
        .canonicalize()
        .map_err(|error| format!("failed to resolve `{}`: {error}", root.display()))?;
    let mut report = CapabilityReport::new(path_string(&root));
    report.generated_at = generated_timestamp();
    report.package_name = read_package_name(&root.join("Cargo.toml"));

    let files = collect_audited_files(&root)?;
    for file in &files {
        let source = fs::read_to_string(file)
            .map_err(|error| format!("failed to read `{}`: {error}", file.display()))?;
        if file.file_name() == Some(OsStr::new("Cargo.toml")) {
            scan_manifest(&root, file, &source, &mut report);
        }
        if is_rust_like(file) {
            scan_source_file(&root, file, &source, &mut report);
            scan_routes(&root, file, &source, &mut report.routes);
        }
        if is_css_like(file) || is_rust_like(file) {
            scan_css_file(&root, file, &source, &mut report);
        }
    }

    attach_route_counts(&mut report);
    report.refresh_summary();
    Ok(report)
}

pub fn audit_projects_batch(
    roots: &[PathBuf],
    batch: DioxusNativePortBatchOptions,
) -> Result<Vec<CapabilityReport>, String> {
    let chunk_size = batch.chunk_size.max(1);
    if !batch.deterministic_parallel || roots.len() <= chunk_size {
        let mut reports = Vec::with_capacity(roots.len());
        for chunk in roots.chunks(chunk_size) {
            for root in chunk {
                reports.push(audit_project(root)?);
            }
        }
        return Ok(reports);
    }

    std::thread::scope(|scope| {
        let mut handles = Vec::new();
        for (chunk_index, chunk) in roots.chunks(chunk_size).enumerate() {
            handles.push(scope.spawn(move || {
                let reports = chunk
                    .iter()
                    .map(|root| audit_project(root))
                    .collect::<Result<Vec<_>, _>>()?;
                Ok::<_, String>((chunk_index, reports))
            }));
        }
        let mut chunks = handles
            .into_iter()
            .map(|handle| {
                handle
                    .join()
                    .map_err(|_| "dioxus native-port cli audit worker panicked".to_string())?
            })
            .collect::<Result<Vec<_>, _>>()?;
        chunks.sort_by_key(|(chunk_index, _)| *chunk_index);
        Ok(chunks
            .into_iter()
            .flat_map(|(_, reports)| reports)
            .collect())
    })
}

pub fn write_report_outputs(
    report: &CapabilityReport,
    options: &AuditOptions,
) -> Result<(), String> {
    let markdown = report.to_markdown();
    let json = report
        .to_json_pretty()
        .map_err(|error| format!("failed to encode report json: {error}"))?;

    match options.stdout_format.as_str() {
        "json" => println!("{json}"),
        "markdown" | "md" => println!("{markdown}"),
        other => return Err(format!("unknown output format `{other}`")),
    }

    if let Some(path) = &options.json_output {
        write_output(path, &json)?;
    }
    if let Some(path) = &options.markdown_output {
        write_output(path, &markdown)?;
    }

    Ok(())
}

pub fn convert_project(root: &Path, options: &ConvertOptions) -> Result<ConversionPlan, String> {
    let root = root
        .canonicalize()
        .map_err(|error| format!("failed to resolve `{}`: {error}", root.display()))?;
    let report = audit_project(&root)?;
    let mut plan = ConversionPlan::from_report(&report);

    add_base_native_style_rules(&mut plan);
    add_layout_class_conversions(&root, &mut plan)?;
    add_route_gap_conversions(&root, &report, &mut plan)?;
    add_route_link_conversions(&root, options, &report, &mut plan)?;
    add_asset_bundle_conversion(options, &mut plan)?;
    add_renderer_dependency_doctoring(&root, &mut plan);
    plan.refresh_summary();
    Ok(plan)
}

pub fn write_conversion_outputs(
    root: &Path,
    plan: &ConversionPlan,
    options: &ConvertOptions,
) -> Result<(), String> {
    let root = root
        .canonicalize()
        .map_err(|error| format!("failed to resolve `{}`: {error}", root.display()))?;
    let output_dir = resolve_output_path(&root, &options.output_dir);
    fs::create_dir_all(&output_dir)
        .map_err(|error| format!("failed to create `{}`: {error}", output_dir.display()))?;

    let markdown = plan.to_markdown();
    let json = plan
        .to_json_pretty()
        .map_err(|error| format!("failed to encode conversion json: {error}"))?;
    let native_css = plan.to_native_css();
    let route_manifest = conversion_route_manifest_json(plan)?;
    let patch = conversion_patch_preview(plan, &options.native_css_output, &native_css);

    match options.stdout_format.as_str() {
        "json" => println!("{json}"),
        "markdown" | "md" => println!("{markdown}"),
        other => return Err(format!("unknown output format `{other}`")),
    }

    write_output(&output_dir.join("conversion-report.json"), &json)?;
    write_output(&output_dir.join("conversion-report.md"), &markdown)?;
    write_output(&output_dir.join("native-port.css"), &native_css)?;
    write_output(
        &output_dir.join("native-port-manifest.json"),
        &route_manifest,
    )?;
    write_output(&output_dir.join("native-port.patch"), &patch)?;

    if options.write {
        let native_css_output = resolve_output_path(&root, &options.native_css_output);
        write_output(&native_css_output, &native_css)?;
    }

    Ok(())
}

pub fn cli_integration_options() -> DioxusNativePortIntegrationOptions {
    let mut options = dioprism_native_port_integration_options();
    options.package_name = "dioprism-cli".to_string();
    options.cache_namespace = "dioprism-cli".to_string();
    options.labels.push("cli".to_string());
    options
        .tags
        .insert("commands".to_string(), "audit,convert,doctor".to_string());
    options
}

pub fn manifest_fragment_for_audit(report: &CapabilityReport) -> DioxusNativePortManifestFragment {
    dioprism_native_port_manifest_fragment_from_report(report, &cli_integration_options())
}

pub fn manifest_fragment_for_audit_traced(
    report: &CapabilityReport,
    trace: &impl DioxusNativePortTraceSink,
) -> DioxusNativePortManifestFragment {
    dioprism_native_port_manifest_fragment_from_report_traced(
        report,
        &cli_integration_options(),
        trace,
    )
}

pub fn manifest_fragments_for_audits_batch(
    reports: &[CapabilityReport],
    batch: DioxusNativePortBatchOptions,
) -> Vec<DioxusNativePortManifestFragment> {
    dioprism_native_port_batch_report_fragments(reports, &cli_integration_options(), batch)
}

pub fn cli_precomputed_fragments() -> DioxusNativePortPrecomputedFragments {
    dioprism_native_port_precomputed_fragments(&cli_integration_options())
}

pub fn audit_optimized_artifacts(
    report: &CapabilityReport,
    optimizer: &dyn DioxusNativePortArtifactOptimizer,
) -> serde_json::Result<Vec<String>> {
    let options = cli_integration_options();
    let fragment = dioprism_native_port_manifest_fragment_from_report(report, &options);
    dioprism_native_port_optimized_artifacts(&fragment, &options, optimizer)
}

pub fn audit_baseline_report(
    scenario: impl Into<String>,
    report: &CapabilityReport,
) -> DioxusNativePortBaselineReport {
    dioprism_native_port_report_baseline(scenario, report, &cli_integration_options())
}

pub fn manifest_fragment_for_plan(plan: &ConversionPlan) -> DioxusNativePortManifestFragment {
    dioprism_native_port_manifest_fragment_from_plan(plan, &cli_integration_options())
}

pub fn doctor_report_for_audit(report: &CapabilityReport) -> DioxusNativePortDoctorReport {
    dioprism_native_port_doctor_report_for_report(report, &cli_integration_options())
}

pub fn doctor_report_for_plan(plan: &ConversionPlan) -> DioxusNativePortDoctorReport {
    dioprism_native_port_doctor_report_for_plan(plan, &cli_integration_options())
}

fn add_base_native_style_rules(plan: &mut ConversionPlan) {
    for rule in [
        NativeStyleRule::new(".np-shell")
            .declaration("min-height", "100vh")
            .declaration("overflow-x", "hidden")
            .declaration("width", "100%")
            .source("native shell viewport"),
        NativeStyleRule::new(".np-header")
            .declaration("overflow", "visible")
            .declaration("position", "sticky")
            .declaration("top", "0")
            .declaration("width", "100%")
            .declaration("z-index", "30")
            .source("sticky header fallback"),
        NativeStyleRule::new(".np-header-inner")
            .declaration("align-items", "flex-start")
            .declaration("box-sizing", "border-box")
            .declaration("display", "flex")
            .declaration("flex-direction", "column")
            .declaration("gap", "10px")
            .declaration("justify-content", "flex-start")
            .declaration("margin-left", "auto")
            .declaration("margin-right", "auto")
            .declaration("max-width", "1280px")
            .declaration("width", "100%")
            .source("browser responsive header classes"),
        NativeStyleRule::new(".np-brand")
            .declaration("flex", "0 0 auto")
            .declaration("min-width", "0")
            .source("header brand item"),
        NativeStyleRule::new(".np-nav")
            .declaration("box-sizing", "border-box")
            .declaration("display", "block")
            .declaration("max-width", "100%")
            .declaration("overflow", "visible")
            .declaration("white-space", "normal")
            .declaration("width", "100%")
            .source("native-safe compact stacked navigation"),
        NativeStyleRule::new(".np-nav-item")
            .declaration("box-sizing", "border-box")
            .declaration("display", "block")
            .declaration("font-size", "12px")
            .declaration("line-height", "1.1")
            .declaration("margin-bottom", "3px")
            .declaration("margin-right", "0")
            .declaration("min-height", "26px")
            .declaration("overflow", "visible")
            .declaration("padding", "4px 8px")
            .declaration("text-align", "center")
            .declaration("white-space", "normal")
            .source("native-safe nav link sizing"),
        NativeStyleRule::new(".np-locale-switcher")
            .declaration("align-items", "center")
            .declaration("display", "grid")
            .declaration("gap", "6px")
            .declaration("grid-template-columns", "auto repeat(3, 48px)")
            .declaration("margin-top", "4px")
            .declaration("overflow", "visible")
            .declaration("white-space", "normal")
            .source("native-safe locale controls"),
        NativeStyleRule::new(".np-locale-button")
            .declaration("display", "block")
            .declaration("font-size", "12px")
            .declaration("margin-bottom", "0")
            .declaration("margin-right", "0")
            .declaration("text-align", "center")
            .declaration("white-space", "nowrap")
            .source("native-safe locale button sizing"),
        NativeStyleRule::new(".np-route-card")
            .declaration("min-width", "0")
            .declaration("overflow", "visible")
            .source("native-safe repeated route card"),
    ] {
        plan.add_style_rule(rule);
    }
}

fn add_layout_class_conversions(root: &Path, plan: &mut ConversionPlan) -> Result<(), String> {
    for file in collect_audited_files(root)? {
        if !is_rust_like(&file) {
            continue;
        }
        let source = fs::read_to_string(&file)
            .map_err(|error| format!("failed to read `{}`: {error}", file.display()))?;
        for (line_index, line) in source.lines().enumerate() {
            let line_number = line_index + 1;
            let relative = relative_path(root, &file);
            if line.contains("theme-header") {
                push_conversion_once(
                    plan,
                    ConversionChange::new(
                        RewriteKind::ClassAugmentation,
                        PortabilityCapability::StickyPosition,
                        "add native-safe header class for sticky/overflow behavior",
                        "append `np-header` to the header class list",
                    )
                    .with_span(span(
                        root,
                        &file,
                        line_number,
                        line,
                        "theme-header",
                    )),
                );
            }
            if line.contains("sm:flex-row") || line.contains("sm:items-center") {
                push_conversion_once(
                    plan,
                    ConversionChange::new(
                        RewriteKind::ClassAugmentation,
                        PortabilityCapability::ResponsiveLayout,
                        "add native-safe header inner layout class for responsive flex rows",
                        "append `np-header-inner` to the header container class list",
                    )
                    .with_span(span(
                        root,
                        &file,
                        line_number,
                        line,
                        "sm:flex-row",
                    )),
                );
            }
            if line.contains("flex-wrap") && line.contains("gap-2") {
                push_conversion_once(
                    plan,
                    ConversionChange::new(
                        RewriteKind::ClassAugmentation,
                        PortabilityCapability::FlexWrap,
                        "add native-safe wrapping nav class so all top links stay visible",
                        "append `np-nav` to the nav class list",
                    )
                    .with_span(span(
                        root,
                        &file,
                        line_number,
                        line,
                        "flex-wrap",
                    )),
                );
            }
            if (line.contains("hover:") || line.contains("focus:"))
                && line.contains("px-3")
                && line.contains("py-2")
            {
                push_conversion_once(
                        plan,
                        ConversionChange::new(
                            RewriteKind::ClassAugmentation,
                            PortabilityCapability::PseudoState,
                            "add native-safe nav item sizing because hover/focus states may not affect layout in Blitz",
                            "append `np-nav-item` to each nav link class list",
                        )
                        .with_span(SourceSpan::new(
                            relative.clone(),
                            line_number,
                            line.find("hover:").or_else(|| line.find("focus:")).map(|column| column + 1),
                            line.trim().to_string(),
                        )),
                    );
            }
        }
    }
    Ok(())
}

fn add_route_gap_conversions(
    root: &Path,
    report: &CapabilityReport,
    plan: &mut ConversionPlan,
) -> Result<(), String> {
    let known_routes: BTreeMap<String, Option<String>> = report
        .routes
        .iter()
        .map(|route| (route.route.clone(), route.component.clone()))
        .collect();

    for file in collect_audited_files(root)? {
        if !is_rust_like(&file) {
            continue;
        }
        let source = fs::read_to_string(&file)
            .map_err(|error| format!("failed to read `{}`: {error}", file.display()))?;
        for nav_item in extract_website_nav_items(&source) {
            let known = known_routes.contains_key(&nav_item.href);
            let action = native_route_action(&nav_item.href, known).to_string();
            let status = if known {
                RoutePortabilityStatus::NativeReady
            } else {
                RoutePortabilityStatus::NeedsAdapters
            };
            if !known {
                push_conversion_once(
                    plan,
                    ConversionChange::new(
                        RewriteKind::RouteLink,
                        PortabilityCapability::ServerOnlyRoute,
                        "server/SSR navigation exposes a route that is missing from the Dioxus native route inventory",
                        "add a native route component, native static fallback, native task-pool action, or mark it external-only",
                    )
                    .with_span(SourceSpan::new(
                        relative_path(root, &file),
                        nav_item.line,
                        None,
                        nav_item.snippet.clone(),
                    )),
                );
            }
            upsert_route_conversion(
                plan,
                RouteConversion {
                    route: nav_item.href.clone(),
                    component: known_routes
                        .get(&nav_item.href)
                        .cloned()
                        .flatten()
                        .or_else(|| {
                            Some(format!(
                                "{} @ website nav ({})",
                                nav_item.label,
                                relative_path(root, &file)
                            ))
                        }),
                    status,
                    native_action: action,
                    findings: vec![format!("{}:{}", relative_path(root, &file), nav_item.line)],
                },
            );
        }
    }

    dedupe_route_conversions(&mut plan.routes);
    Ok(())
}

fn upsert_route_conversion(plan: &mut ConversionPlan, conversion: RouteConversion) {
    if let Some(existing) = plan
        .routes
        .iter_mut()
        .find(|route| route.route == conversion.route && route.component == conversion.component)
    {
        existing.status = conversion.status;
        existing.native_action = conversion.native_action;
        for finding in conversion.findings {
            if !existing.findings.contains(&finding) {
                existing.findings.push(finding);
            }
        }
        return;
    }

    plan.routes.push(conversion);
}

fn add_route_link_conversions(
    root: &Path,
    options: &ConvertOptions,
    report: &CapabilityReport,
    plan: &mut ConversionPlan,
) -> Result<(), String> {
    let mut known_routes: BTreeSet<String> = report
        .routes
        .iter()
        .map(|route| route.route.clone())
        .collect();
    known_routes.extend(options.routes.iter().cloned());
    if !options.routes.is_empty() {
        plan.routes
            .retain(|route| known_routes.contains(&route.route) || route.route == "*");
    }

    for file in collect_audited_files(root)? {
        if !is_rust_like(&file) {
            continue;
        }
        let source = fs::read_to_string(&file)
            .map_err(|error| format!("failed to read `{}`: {error}", file.display()))?;
        for (line_index, line) in source.lines().enumerate() {
            let Some(href) = extract_href(line) else {
                continue;
            };
            let line_number = line_index + 1;
            if href.starts_with("http://") || href.starts_with("https://") {
                push_conversion_once(
                    plan,
                    ConversionChange::new(
                        RewriteKind::RouteLink,
                        PortabilityCapability::AnchorNavigation,
                        "external anchors need an explicit native browser-open action",
                        "wrap with `PortableLinkKind::External` or a native open-url command",
                    )
                    .with_span(span(root, &file, line_number, line, &href)),
                );
            } else if is_asset_href(&href) {
                push_conversion_once(
                    plan,
                    ConversionChange::new(
                        RewriteKind::AssetBundle,
                        PortabilityCapability::AssetBundle,
                        "plain href points at a browser-served asset that needs native bundle metadata",
                        "copy into the native asset bundle and reference through `portable_asset_path`",
                    )
                    .with_span(span(root, &file, line_number, line, &href)),
                );
            } else if href.starts_with('/') && !known_routes.contains(&href) {
                let span = span(root, &file, line_number, line, &href);
                push_conversion_once(
                    plan,
                    ConversionChange::new(
                        RewriteKind::RouteLink,
                        PortabilityCapability::ServerOnlyRoute,
                        "plain href targets a route that is not in the Dioxus router inventory",
                        "convert to a shared route, disabled native action, or external browser action",
                    )
                    .with_span(span.clone()),
                );
                plan.routes.push(RouteConversion {
                    route: href.clone(),
                    component: Some(format!("plain href @ {}", relative_path(root, &file))),
                    status: RoutePortabilityStatus::NeedsAdapters,
                    native_action: "external-only".to_string(),
                    findings: vec![format!("{}:{}", span.path, span.line)],
                });
            }
        }
    }

    dedupe_route_conversions(&mut plan.routes);
    Ok(())
}

#[derive(Clone, Debug)]
struct WebsiteNavItem {
    href: String,
    label: String,
    line: usize,
    snippet: String,
}

fn extract_website_nav_items(source: &str) -> Vec<WebsiteNavItem> {
    let mut items = Vec::new();
    let mut in_nav = false;
    for (line_index, line) in source.lines().enumerate() {
        if line.contains("class=\"dxr-nav\"") || line.contains("class='dxr-nav'") {
            in_nav = true;
        }
        if in_nav {
            if let Some(href) = extract_href(line)
                && href.starts_with('/')
                && !is_asset_href(&href)
            {
                items.push(WebsiteNavItem {
                    label: website_nav_label(&href).to_string(),
                    href,
                    line: line_index + 1,
                    snippet: line.trim().to_string(),
                });
            }
            if line.contains("</nav>") {
                in_nav = false;
            }
        }
    }
    items
}

fn website_nav_label(route: &str) -> &'static str {
    match route {
        "/" => "Overview",
        "/motion" => "Motion",
        "/browser" => "Browser APIs",
        "/split" => "Split",
        "/shader" => "Shader",
        "/textfx" => "TextFX",
        "/morph" => "Morph",
        "/workertown" => "WorkerTown",
        "/bench" => "Bench",
        "/resume-playground" => "Resume Lab",
        _ => "Website nav route",
    }
}

fn native_route_action(route: &str, known: bool) -> &'static str {
    if !known {
        return "external-only";
    }
    match route {
        "/shader" => "native-gpu-action",
        "/textfx" | "/morph" | "/resume-playground" => "native-action",
        "/workertown" | "/bench" => "native-task-pool",
        _ => "native-route",
    }
}

fn add_asset_bundle_conversion(
    options: &ConvertOptions,
    plan: &mut ConversionPlan,
) -> Result<(), String> {
    let native_css_path = path_string(&options.native_css_output);
    let native_css = plan.to_native_css();
    plan.add_generated_asset(GeneratedAsset::new(
        native_css_path,
        "css",
        native_css,
        "native-safe stylesheet generated from layout conversion rules",
    ));
    plan.add_generated_asset(GeneratedAsset::new(
        "native-port-manifest.json",
        "route-manifest",
        conversion_route_manifest_json(plan)?,
        "route and native action manifest for the converted app",
    ));
    Ok(())
}

fn add_renderer_dependency_doctoring(root: &Path, plan: &mut ConversionPlan) {
    let manifest_path = root.join("Cargo.toml");
    let lock_path = root.join("Cargo.lock");
    let manifest = fs::read_to_string(&manifest_path).unwrap_or_default();
    let lock = fs::read_to_string(&lock_path).unwrap_or_default();
    let native_enabled = manifest.contains("dioxus/native") || manifest.contains("native = [");
    let has_gpu_allocator_patch =
        manifest.contains("[patch.crates-io]") && manifest.contains("gpu-allocator");
    let has_wgpu_hal = lock.contains("name = \"wgpu-hal\"") || manifest.contains("wgpu-hal");
    if native_enabled && has_wgpu_hal && !has_gpu_allocator_patch {
        plan.changes.push(
            ConversionChange::new(
                RewriteKind::RendererPatch,
                PortabilityCapability::RendererDependency,
                "Windows native builds can hit a wgpu-hal DX12 windows-core 0.61/0.62 type mismatch",
                "add a `[patch.crates-io]` gpu-allocator patch aligned to windows 0.62, or choose a supported renderer dependency set",
            )
            .with_span(SourceSpan::new(
                "Cargo.toml",
                1,
                None,
                "missing gpu-allocator windows-core compatibility patch",
            ))
            .preview_only(),
        );
    }
}

fn push_conversion_once(plan: &mut ConversionPlan, change: ConversionChange) {
    let exists = plan.changes.iter().any(|existing| {
        existing.kind == change.kind
            && existing.capability == change.capability
            && existing.span.as_ref().map(|span| (&span.path, span.line))
                == change.span.as_ref().map(|span| (&span.path, span.line))
    });
    if !exists {
        plan.changes.push(change);
    }
}

fn dedupe_route_conversions(routes: &mut Vec<RouteConversion>) {
    let mut seen = BTreeSet::new();
    routes.retain(|route| seen.insert((route.route.clone(), route.component.clone())));
}

fn conversion_route_manifest_json(plan: &ConversionPlan) -> Result<String, String> {
    serde_json::to_string_pretty(&plan.routes)
        .map_err(|error| format!("failed to encode route manifest: {error}"))
}

fn conversion_patch_preview(
    plan: &ConversionPlan,
    native_css_output: &Path,
    native_css: &str,
) -> String {
    let mut patch = String::new();
    let css_path = path_string(native_css_output);
    patch.push_str(&format!("diff --git a/{css_path} b/{css_path}\n"));
    patch.push_str("new file mode 100644\n");
    patch.push_str("--- /dev/null\n");
    patch.push_str(&format!("+++ b/{css_path}\n"));
    patch.push_str("@@ -0,0 +1,");
    patch.push_str(&native_css.lines().count().to_string());
    patch.push_str(" @@\n");
    for line in native_css.lines() {
        patch.push('+');
        patch.push_str(line);
        patch.push('\n');
    }
    patch.push('\n');
    patch.push_str(&plan.to_patch_preview());
    patch
}

fn resolve_output_path(root: &Path, path: &Path) -> PathBuf {
    if path.is_absolute() {
        path.to_path_buf()
    } else {
        root.join(path)
    }
}

fn scan_manifest(root: &Path, file: &Path, source: &str, report: &mut CapabilityReport) {
    scan_visual_compatibility_manifests(source, report);
    let features = manifest_features(source);
    if !features.is_empty() {
        scan_manifest_features(root, file, source, &features, report);
        scan_native_desktop_alias_lines(root, file, source, report);
        return;
    }

    scan_manifest_lines(root, file, source, report);
    scan_native_desktop_alias_lines(root, file, source, report);
}

fn scan_visual_compatibility_manifests(source: &str, report: &mut CapabilityReport) {
    let mut package_names = BTreeSet::new();
    if let Ok(manifest) = source.parse::<toml::Value>() {
        if let Some(package_name) = manifest
            .get("package")
            .and_then(|package| package.get("name"))
            .and_then(toml::Value::as_str)
        {
            package_names.insert(package_name.to_string());
        }
        package_names.extend(manifest_dependency_package_names(&manifest));
    }
    if let Some(package_name) = manifest_package_name_from_lines(source) {
        package_names.insert(package_name);
    }
    package_names.extend(manifest_dependency_package_names_from_lines(source));

    for package_name in package_names {
        if let Some(visual_manifest) = builtin_visual_compatibility_manifest(&package_name) {
            report.add_visual_manifest(visual_manifest);
        }
    }
}

fn manifest_dependency_package_names(manifest: &toml::Value) -> BTreeSet<String> {
    let mut names = BTreeSet::new();
    for table_path in [
        &["dependencies"][..],
        &["dev-dependencies"][..],
        &["build-dependencies"][..],
        &["workspace", "dependencies"][..],
    ] {
        let Some(table) = toml_table_at_path(manifest, table_path) else {
            continue;
        };
        for (key, value) in table {
            let name = value
                .as_table()
                .and_then(|table| table.get("package"))
                .and_then(toml::Value::as_str)
                .unwrap_or(key)
                .to_string();
            names.insert(name);
        }
    }
    names
}

fn toml_table_at_path<'a>(value: &'a toml::Value, path: &[&str]) -> Option<&'a toml::Table> {
    let mut current = value;
    for segment in path {
        current = current.get(*segment)?;
    }
    current.as_table()
}

fn manifest_package_name_from_lines(source: &str) -> Option<String> {
    let mut in_package = false;
    for line in source.lines() {
        let trimmed = line.trim();
        if trimmed.starts_with('[') {
            in_package = trimmed == "[package]";
            continue;
        }
        if !in_package || trimmed.is_empty() || trimmed.starts_with('#') {
            continue;
        }
        let Some((key, value)) = trimmed.split_once('=') else {
            continue;
        };
        if key.trim() == "name" {
            return parse_toml_string_literal(value.trim()).map(str::to_string);
        }
    }
    None
}

fn manifest_dependency_package_names_from_lines(source: &str) -> BTreeSet<String> {
    let mut names = BTreeSet::new();
    let mut in_dependencies = false;
    for line in source.lines() {
        let trimmed = line.trim();
        if trimmed.starts_with('[') {
            in_dependencies = matches!(
                trimmed,
                "[dependencies]"
                    | "[dev-dependencies]"
                    | "[build-dependencies]"
                    | "[workspace.dependencies]"
            );
            continue;
        }
        if !in_dependencies || trimmed.is_empty() || trimmed.starts_with('#') {
            continue;
        }
        let Some((key, value)) = trimmed.split_once('=') else {
            continue;
        };
        let key = key.trim().trim_matches('"').trim_matches('\'');
        if key.is_empty() {
            continue;
        }
        let package = value
            .split(',')
            .find_map(|entry| {
                let (field, value) = entry.split_once('=')?;
                if field
                    .trim()
                    .trim_start_matches('{')
                    .trim()
                    .trim_matches('"')
                    == "package"
                {
                    parse_toml_string_literal(value.trim().trim_end_matches('}'))
                        .map(str::to_string)
                } else {
                    None
                }
            })
            .unwrap_or_else(|| key.to_string());
        names.insert(package);
    }
    names
}

fn parse_toml_string_literal(value: &str) -> Option<&str> {
    let value = value.trim().trim_end_matches(',').trim();
    if value.len() < 2 {
        return None;
    }
    if let Some(stripped) = value
        .strip_prefix('"')
        .and_then(|rest| rest.strip_suffix('"'))
    {
        return Some(stripped);
    }
    value
        .strip_prefix('\'')
        .and_then(|rest| rest.strip_suffix('\''))
}

fn scan_manifest_features(
    root: &Path,
    file: &Path,
    source: &str,
    features: &[ManifestFeature],
    report: &mut CapabilityReport,
) {
    for feature in features {
        let has_native = feature.entries.iter().any(|entry| entry == "dioxus/native");
        let has_desktop = feature
            .entries
            .iter()
            .any(|entry| entry == "dioxus/desktop");
        let mixed_renderers = feature.entries.iter().any(|entry| {
            matches!(
                entry.as_str(),
                "dioxus/desktop" | "dioxus/web" | "dioxus/server" | "dioxus/mobile"
            )
        });

        if has_native && mixed_renderers {
            report.renderer_conflicts.push(UnsupportedUse::new(
                PortabilityCapability::RendererFeatureConflict,
                "mixed dioxus renderer features",
                "native renderer features are mixed with another first-party renderer in the same feature expression.",
                "Split native into its own Cargo feature or binary so `dioxus::launch` selects the intended renderer.",
                Severity::Error,
                feature_span(root, file, source, feature, "dioxus/native"),
            ));
            continue;
        }

        let native_feature_name = is_native_renderer_feature_name(&feature.name)
            || feature.snippet.trim_start().starts_with("native =")
            || feature.snippet.trim_start().starts_with("native=");

        if has_desktop && native_feature_name {
            report.renderer_conflicts.push(UnsupportedUse::new(
                PortabilityCapability::RendererFeatureConflict,
                "dioxus/desktop",
                "`native`/Blitz-oriented features must not alias the desktop WebView renderer.",
                "Use `dioxus/native` for the native renderer and keep `desktop` as a separate explicit fallback feature.",
                Severity::Error,
                feature_span(root, file, source, feature, "dioxus/desktop"),
            ));
        }
    }
}

fn scan_manifest_lines(root: &Path, file: &Path, source: &str, report: &mut CapabilityReport) {
    for (line_index, line) in source.lines().enumerate() {
        let line_number = line_index + 1;
        let compact = line.replace(' ', "");
        if compact.contains("dioxus/native")
            && (compact.contains("dioxus/desktop")
                || compact.contains("dioxus/web")
                || compact.contains("dioxus/server")
                || compact.contains("dioxus/mobile"))
        {
            report.renderer_conflicts.push(UnsupportedUse::new(
                PortabilityCapability::RendererFeatureConflict,
                "mixed dioxus renderer features",
                "native renderer features are mixed with another first-party renderer in the same feature expression.",
                "Split native into its own Cargo feature or binary so `dioxus::launch` selects the intended renderer.",
                Severity::Error,
                span(root, file, line_number, line, "dioxus/native"),
            ));
        }
    }
}

fn scan_native_desktop_alias_lines(
    root: &Path,
    file: &Path,
    source: &str,
    report: &mut CapabilityReport,
) {
    let mut in_features = false;
    for (line_index, line) in source.lines().enumerate() {
        let trimmed = line.trim();
        if trimmed.starts_with('[') {
            in_features = trimmed == "[features]";
            continue;
        }
        if !in_features || trimmed.is_empty() || trimmed.starts_with('#') {
            continue;
        }

        let line_number = line_index + 1;
        let compact = trimmed.replace(' ', "");
        let native_feature = compact.starts_with("native=")
            || compact.starts_with("blitz=")
            || compact.starts_with("dioxus-native=");
        if native_feature
            && compact.contains("dioxus/desktop")
            && !compact.contains("dioxus/native")
            && !report
                .renderer_conflicts
                .iter()
                .any(|item| item.pattern == "dioxus/desktop" && item.span.line == line_number)
        {
            report.renderer_conflicts.push(UnsupportedUse::new(
                PortabilityCapability::RendererFeatureConflict,
                "dioxus/desktop",
                "`native`/Blitz-oriented features must not alias the desktop WebView renderer.",
                "Use `dioxus/native` for the native renderer and keep `desktop` as a separate explicit fallback feature.",
                Severity::Error,
                span(root, file, line_number, line, "dioxus/desktop"),
            ));
        }
    }
}

fn manifest_features(source: &str) -> Vec<ManifestFeature> {
    let Ok(manifest) = source.parse::<toml::Value>() else {
        return Vec::new();
    };
    let Some(features) = manifest.get("features").and_then(toml::Value::as_table) else {
        return Vec::new();
    };
    let definition_lines = feature_definition_lines(source);

    features
        .iter()
        .filter_map(|(name, value)| {
            let entries: Vec<String> = value
                .as_array()?
                .iter()
                .filter_map(|entry| entry.as_str().map(str::to_string))
                .collect();
            let (line, snippet) = definition_lines
                .get(name)
                .cloned()
                .unwrap_or_else(|| (1, format!("{name} = [...]")));
            Some(ManifestFeature {
                name: name.to_string(),
                entries,
                line,
                snippet,
            })
        })
        .collect()
}

fn feature_definition_lines(source: &str) -> BTreeMap<String, (usize, String)> {
    let mut definitions = BTreeMap::new();
    let mut in_features = false;
    for (line_index, line) in source.lines().enumerate() {
        let trimmed = line.trim();
        if trimmed.starts_with('[') {
            in_features = trimmed == "[features]";
            continue;
        }
        if !in_features || trimmed.is_empty() || trimmed.starts_with('#') {
            continue;
        }
        if let Some((name, _)) = trimmed.split_once('=') {
            definitions.insert(
                name.trim().trim_matches('"').to_string(),
                (line_index + 1, trimmed.to_string()),
            );
        }
    }
    definitions
}

fn is_native_renderer_feature_name(name: &str) -> bool {
    matches!(name, "native" | "blitz" | "dioxus-native")
}

fn feature_span(
    root: &Path,
    file: &Path,
    source: &str,
    feature: &ManifestFeature,
    pattern: &str,
) -> SourceSpan {
    let lines: Vec<&str> = source.lines().collect();
    let start = feature.line.saturating_sub(1);
    let mut in_array = feature.snippet.contains('[') && !feature.snippet.contains(']');
    for (index, line) in lines.iter().enumerate().skip(start) {
        let trimmed = line.trim();
        if index > start {
            if trimmed.starts_with('[') {
                break;
            }
            if !in_array && is_feature_assignment(trimmed) {
                break;
            }
        }
        if line.contains(pattern) {
            return span(root, file, index + 1, line, pattern);
        }
        if trimmed.contains('[') && !trimmed.contains(']') {
            in_array = true;
        }
        if in_array && trimmed.contains(']') {
            break;
        }
    }

    SourceSpan::new(
        relative_path(root, file),
        feature.line,
        feature.snippet.find(pattern).map(|column| column + 1),
        feature.snippet.clone(),
    )
}

fn is_feature_assignment(trimmed: &str) -> bool {
    trimmed
        .chars()
        .next()
        .map(|ch| ch.is_alphanumeric() || ch == '_' || ch == '"' || ch == '\'')
        .unwrap_or(false)
        && trimmed.contains('=')
}

fn scan_source_file(root: &Path, file: &Path, source: &str, report: &mut CapabilityReport) {
    let supported = supported_capabilities(root, file, source);
    let lines: Vec<&str> = source.lines().collect();
    for (line_index, line) in lines.iter().enumerate() {
        let line_number = line_index + 1;
        for rule in source_rules() {
            if supported.contains(&rule.capability) || wasm_export_is_cfg_gated(&lines, line_index)
            {
                continue;
            }
            if line.contains(rule.pattern) {
                report.unsupported_uses.push(UnsupportedUse::new(
                    rule.capability.clone(),
                    rule.pattern,
                    rule.message,
                    rule.suggestion,
                    rule.severity,
                    span(root, file, line_number, line, rule.pattern),
                ));
            }
        }
    }
}

fn scan_css_file(root: &Path, file: &Path, source: &str, report: &mut CapabilityReport) {
    let supported = supported_capabilities(root, file, source);
    for (line_index, line) in source.lines().enumerate() {
        let line_number = line_index + 1;
        for rule in css_rules() {
            if supported.contains(&rule.capability) {
                continue;
            }
            if line.contains(rule.pattern) {
                report.css_issues.push(CssPortabilityIssue::new(
                    rule.capability.clone(),
                    rule.pattern,
                    rule.message,
                    rule.suggestion,
                    rule.severity,
                    span(root, file, line_number, line, rule.pattern),
                ));
            }
        }
    }
}

fn supported_capabilities(
    root: &Path,
    file: &Path,
    source: &str,
) -> BTreeSet<PortabilityCapability> {
    let mut supported = BTreeSet::new();
    supported.extend(path_supported_capabilities(&relative_path(root, file)));
    for line in source.lines().take(64) {
        let Some((_, marker)) = line.split_once("native-port: supports") else {
            continue;
        };
        for name in marker.split([',', ' ', '\t', ';']) {
            if let Some(capability) = parse_capability_marker(name.trim()) {
                supported.insert(capability);
            }
        }
    }
    supported
}

fn path_supported_capabilities(path: &str) -> BTreeSet<PortabilityCapability> {
    let mut supported = BTreeSet::new();
    if path == "public/sw.js" {
        supported.insert(PortabilityCapability::CssLayout);
    }
    supported
}

fn parse_capability_marker(name: &str) -> Option<PortabilityCapability> {
    match name {
        "anchor-navigation" => Some(PortabilityCapability::AnchorNavigation),
        "asset-bundle" => Some(PortabilityCapability::AssetBundle),
        "browser-dom" => Some(PortabilityCapability::BrowserDom),
        "browser-storage" => Some(PortabilityCapability::BrowserStorage),
        "browser-performance" => Some(PortabilityCapability::BrowserPerformance),
        "browser-navigator" => Some(PortabilityCapability::BrowserNavigator),
        "clock" => Some(PortabilityCapability::Clock),
        "css-animation" => Some(PortabilityCapability::CssAnimation),
        "css-filter" => Some(PortabilityCapability::CssFilter),
        "css-gradient" => Some(PortabilityCapability::CssGradient),
        "css-layout" => Some(PortabilityCapability::CssLayout),
        "canvas-effects" => Some(PortabilityCapability::CanvasEffects),
        "document-eval" => Some(PortabilityCapability::DocumentEval),
        "dom-snapshot" => Some(PortabilityCapability::DomSnapshot),
        "fetch" => Some(PortabilityCapability::Fetch),
        "flex-wrap" => Some(PortabilityCapability::FlexWrap),
        "morph-transition" => Some(PortabilityCapability::MorphTransition),
        "offscreen-canvas" => Some(PortabilityCapability::OffscreenCanvas),
        "pointer-proximity" => Some(PortabilityCapability::PointerProximity),
        "random" => Some(PortabilityCapability::Random),
        "reduced-motion-policy" => Some(PortabilityCapability::ReducedMotionPolicy),
        "renderer-dependency" => Some(PortabilityCapability::RendererDependency),
        "renderer-feature-conflict" => Some(PortabilityCapability::RendererFeatureConflict),
        "responsive-layout" => Some(PortabilityCapability::ResponsiveLayout),
        "route-asset" => Some(PortabilityCapability::RouteAsset),
        "service-worker" => Some(PortabilityCapability::ServiceWorker),
        "server-only-route" => Some(PortabilityCapability::ServerOnlyRoute),
        "storage" => Some(PortabilityCapability::Storage),
        "sticky-position" => Some(PortabilityCapability::StickyPosition),
        "shader-runtime" => Some(PortabilityCapability::ShaderRuntime),
        "system-info" => Some(PortabilityCapability::SystemInfo),
        "task-pool" => Some(PortabilityCapability::TaskPool),
        "pseudo-state" => Some(PortabilityCapability::PseudoState),
        "text-animation" => Some(PortabilityCapability::TextAnimation),
        "theme-tokens" => Some(PortabilityCapability::ThemeTokens),
        "view-transition" => Some(PortabilityCapability::ViewTransition),
        "wasm-interop" => Some(PortabilityCapability::WasmInterop),
        "web-gl" => Some(PortabilityCapability::WebGl),
        "worker" => Some(PortabilityCapability::Worker),
        _ => None,
    }
}

fn wasm_export_is_cfg_gated(lines: &[&str], line_index: usize) -> bool {
    let line = lines.get(line_index).copied().unwrap_or_default();
    if !line.contains("wasm_bindgen") {
        return false;
    }
    if line.contains("cfg_attr(feature = \"wasm-bindgen\"") {
        return true;
    }
    lines
        .get(line_index.saturating_sub(1))
        .map(|previous| previous.contains("#[cfg(feature = \"wasm-bindgen\")]"))
        .unwrap_or(false)
}

fn scan_routes(root: &Path, file: &Path, source: &str, routes: &mut Vec<RoutePortability>) {
    let lines: Vec<&str> = source.lines().collect();
    for (line_index, line) in lines.iter().enumerate() {
        let Some(route) = extract_route(line) else {
            continue;
        };
        let component = lines
            .iter()
            .skip(line_index + 1)
            .take(6)
            .find_map(|candidate| extract_route_component(candidate))
            .map(|component| format!("{component} @ {}", relative_path(root, file)));
        let mut route_entry = RoutePortability::new(route, component);
        if route_entry.component.is_none() {
            route_entry.component = Some(format!(
                "{}:{}",
                relative_path(root, file),
                line_index.saturating_add(1)
            ));
        }
        let already_seen = routes.iter().any(|existing| {
            existing.route == route_entry.route && existing.component == route_entry.component
        });
        if !already_seen {
            routes.push(route_entry);
        }
    }
}

fn attach_route_counts(report: &mut CapabilityReport) {
    let unsupported_by_path = count_by_path(
        report
            .renderer_conflicts
            .iter()
            .chain(report.unsupported_uses.iter())
            .map(|item| item.span.path.as_str()),
    );
    let css_by_path = count_by_path(report.css_issues.iter().map(|item| item.span.path.as_str()));
    for route in &mut report.routes {
        let Some(component) = &route.component else {
            continue;
        };
        let mut unsupported = 0;
        let mut css = 0;
        for (path, count) in &unsupported_by_path {
            if component.contains(path) || route.route == *path {
                unsupported += count;
            }
        }
        for (path, count) in &css_by_path {
            if component.contains(path) || route.route == *path {
                css += count;
            }
        }
        *route = route.clone().with_counts(unsupported, css);
    }
}

fn count_by_path<'a>(paths: impl Iterator<Item = &'a str>) -> BTreeMap<String, usize> {
    let mut counts = BTreeMap::new();
    for path in paths {
        *counts.entry(path.to_string()).or_insert(0) += 1;
    }
    counts
}

fn source_rules() -> &'static [ScanRule] {
    &[
        ScanRule {
            pattern: "web_sys::window",
            capability: PortabilityCapability::BrowserDom,
            severity: Severity::Error,
            message: "direct browser window access will not run under Dioxus Native.",
            suggestion: "Use `NativePortProvider::system_info`, storage, or a narrow platform adapter.",
        },
        ScanRule {
            pattern: "web_sys::",
            capability: PortabilityCapability::BrowserDom,
            severity: Severity::Error,
            message: "`web_sys` is browser-only.",
            suggestion: "Move browser API access behind a portable provider with native and web implementations.",
        },
        ScanRule {
            pattern: "js_sys::Date",
            capability: PortabilityCapability::Clock,
            severity: Severity::Error,
            message: "`js_sys::Date` is browser/JS runtime-specific.",
            suggestion: "Use `use_portable_clock` or `NativePortProvider::now_millis`.",
        },
        ScanRule {
            pattern: "js_sys::Math::random",
            capability: PortabilityCapability::Random,
            severity: Severity::Error,
            message: "`js_sys::Math::random` is browser/JS runtime-specific.",
            suggestion: "Use `use_portable_random` or `NativePortProvider::random_f64`.",
        },
        ScanRule {
            pattern: "js_sys::",
            capability: PortabilityCapability::WasmInterop,
            severity: Severity::Warning,
            message: "`js_sys` assumes a JavaScript runtime.",
            suggestion: "Replace JS interop with Rust code or a provider method.",
        },
        ScanRule {
            pattern: "wasm_bindgen",
            capability: PortabilityCapability::WasmInterop,
            severity: Severity::Warning,
            message: "`wasm_bindgen` code is only portable to browser WASM targets.",
            suggestion: "Gate WASM exports behind `target_arch = \"wasm32\"` and expose a Rust-native equivalent.",
        },
        ScanRule {
            pattern: "serde_wasm_bindgen",
            capability: PortabilityCapability::WasmInterop,
            severity: Severity::Warning,
            message: "`serde_wasm_bindgen` serializes through JS values.",
            suggestion: "Use `serde_json` or a typed Rust model in shared/native code.",
        },
        ScanRule {
            pattern: "document::eval",
            capability: PortabilityCapability::DocumentEval,
            severity: Severity::Error,
            message: "document eval depends on browser/WebView script execution.",
            suggestion: "Move behavior into Rust state, a provider method, or a native renderer primitive.",
        },
        ScanRule {
            pattern: "local_storage",
            capability: PortabilityCapability::BrowserStorage,
            severity: Severity::Error,
            message: "browser localStorage is unavailable in Dioxus Native.",
            suggestion: "Use `use_portable_storage` for browser storage plus filesystem-backed native storage.",
        },
        ScanRule {
            pattern: ".performance()",
            capability: PortabilityCapability::BrowserPerformance,
            severity: Severity::Error,
            message: "browser performance APIs are unavailable in Dioxus Native.",
            suggestion: "Use `NativePortProvider::now_millis` or app-level instrumentation.",
        },
        ScanRule {
            pattern: ".navigator()",
            capability: PortabilityCapability::BrowserNavigator,
            severity: Severity::Error,
            message: "browser navigator APIs are unavailable in Dioxus Native.",
            suggestion: "Use `use_portable_system_info`.",
        },
        ScanRule {
            pattern: "Worker::",
            capability: PortabilityCapability::Worker,
            severity: Severity::Error,
            message: "browser workers do not map directly to native windows.",
            suggestion: "Use `spawn_portable_task` or a Rust task pool for native builds.",
        },
        ScanRule {
            pattern: "OffscreenCanvas",
            capability: PortabilityCapability::OffscreenCanvas,
            severity: Severity::Error,
            message: "OffscreenCanvas is browser-only.",
            suggestion: "Gate OffscreenCanvas behind a browser adapter and provide a static or native renderer fallback.",
        },
        ScanRule {
            pattern: "transferControlToOffscreen",
            capability: PortabilityCapability::OffscreenCanvas,
            severity: Severity::Error,
            message: "transferring HTML canvas control to a worker is browser-only.",
            suggestion: "Use native renderer surfaces, WGPU actions, or static assets for native builds.",
        },
        ScanRule {
            pattern: "ServiceWorker",
            capability: PortabilityCapability::ServiceWorker,
            severity: Severity::Error,
            message: "service workers are browser platform infrastructure.",
            suggestion: "Use native networking/cache layers instead of service-worker scripts.",
        },
        ScanRule {
            pattern: "service_worker",
            capability: PortabilityCapability::ServiceWorker,
            severity: Severity::Error,
            message: "service worker APIs are browser-only.",
            suggestion: "Use native networking/cache layers instead of service-worker scripts.",
        },
        ScanRule {
            pattern: "text/dioxus-worker",
            capability: PortabilityCapability::Worker,
            severity: Severity::Error,
            message: "inert browser worker script markers need a native task replacement.",
            suggestion: "Represent worker behavior through a portable task capability.",
        },
        ScanRule {
            pattern: "data-dxw-src",
            capability: PortabilityCapability::RouteAsset,
            severity: Severity::Warning,
            message: "route-scoped browser runtime assets need a native equivalent or exclusion.",
            suggestion: "Declare native route capabilities explicitly in the app manifest.",
        },
        ScanRule {
            pattern: "startViewTransition",
            capability: PortabilityCapability::ViewTransition,
            severity: Severity::Error,
            message: "browser View Transition APIs do not exist in Dioxus Native.",
            suggestion: "Use route transition metadata or a native-safe transition adapter.",
        },
        ScanRule {
            pattern: "getBoundingClientRect",
            capability: PortabilityCapability::DomSnapshot,
            severity: Severity::Warning,
            message: "DOM geometry snapshots need a native renderer equivalent.",
            suggestion: "Record stable layout metadata or downgrade exact morph/view transitions on native targets.",
        },
        ScanRule {
            pattern: "DOMRect",
            capability: PortabilityCapability::DomSnapshot,
            severity: Severity::Warning,
            message: "DOMRect-based measurement is a browser DOM contract.",
            suggestion: "Use native renderer layout metadata or a static fallback path.",
        },
        ScanRule {
            pattern: "requestAnimationFrame",
            capability: PortabilityCapability::TextAnimation,
            severity: Severity::Warning,
            message: "browser frame callbacks need native timer or scheduler mapping.",
            suggestion: "Use Dioxus state, native timers, or a reduced-motion static fallback.",
        },
        ScanRule {
            pattern: "WebGl",
            capability: PortabilityCapability::WebGl,
            severity: Severity::Error,
            message: "WebGL APIs are browser-specific.",
            suggestion: "Use a renderer abstraction or native WGPU/Vello integration.",
        },
        ScanRule {
            pattern: "webgl",
            capability: PortabilityCapability::WebGl,
            severity: Severity::Warning,
            message: "WebGL assumptions should be replaced for Blitz/native rendering.",
            suggestion: "Use a renderer abstraction or native WGPU/Vello integration.",
        },
    ]
}

fn css_rules() -> &'static [ScanRule] {
    &[
        ScanRule {
            pattern: "sm:flex-row",
            capability: PortabilityCapability::ResponsiveLayout,
            severity: Severity::Warning,
            message: "responsive Tailwind flex breakpoints can differ under Blitz layout.",
            suggestion: "Generate and apply `.np-header-inner` or another stable native layout class.",
        },
        ScanRule {
            pattern: "flex-wrap",
            capability: PortabilityCapability::FlexWrap,
            severity: Severity::Warning,
            message: "native flex wrapping needs explicit width and overflow constraints.",
            suggestion: "Generate and apply `.np-nav` for native-safe wrapping navigation.",
        },
        ScanRule {
            pattern: "sticky",
            capability: PortabilityCapability::StickyPosition,
            severity: Severity::Warning,
            message: "sticky browser positioning should be verified under the native renderer.",
            suggestion: "Generate and apply `.np-header` with explicit width and overflow behavior.",
        },
        ScanRule {
            pattern: "hover:",
            capability: PortabilityCapability::PseudoState,
            severity: Severity::Info,
            message: "hover-only styling does not provide a native layout contract.",
            suggestion: "Generate native-safe default styles for the same interactive element.",
        },
        ScanRule {
            pattern: "focus:",
            capability: PortabilityCapability::PseudoState,
            severity: Severity::Info,
            message: "focus-only styling should have a native-safe default layout.",
            suggestion: "Generate native-safe default styles for the same interactive element.",
        },
        ScanRule {
            pattern: "bg-[",
            capability: PortabilityCapability::CssGradient,
            severity: Severity::Warning,
            message: "Tailwind arbitrary background values often rely on advanced CSS parsing.",
            suggestion: "Verify Blitz support or move the visual into a native-safe component/style token.",
        },
        ScanRule {
            pattern: "grid-cols-[",
            capability: PortabilityCapability::CssLayout,
            severity: Severity::Warning,
            message: "Tailwind arbitrary grid tracks need Blitz layout verification.",
            suggestion: "Prefer fixed utility classes or native-safe layout tokens.",
        },
        ScanRule {
            pattern: "shadow-[",
            capability: PortabilityCapability::CssFilter,
            severity: Severity::Warning,
            message: "arbitrary shadows can exceed current native renderer CSS support.",
            suggestion: "Use conservative shadow tokens and verify visual output.",
        },
        ScanRule {
            pattern: "blur-",
            capability: PortabilityCapability::CssFilter,
            severity: Severity::Warning,
            message: "blur/filter effects need native renderer verification.",
            suggestion: "Use static visual fallbacks for native builds.",
        },
        ScanRule {
            pattern: "backdrop-blur",
            capability: PortabilityCapability::CssFilter,
            severity: Severity::Warning,
            message: "backdrop filters are high-risk in native rendering.",
            suggestion: "Use opaque native-safe surfaces or explicit renderer support checks.",
        },
        ScanRule {
            pattern: "animate-",
            capability: PortabilityCapability::CssAnimation,
            severity: Severity::Warning,
            message: "CSS animation support should be confirmed under Blitz.",
            suggestion: "Prefer Dioxus state-driven animation for native parity.",
        },
        ScanRule {
            pattern: "@keyframes",
            capability: PortabilityCapability::CssAnimation,
            severity: Severity::Warning,
            message: "CSS keyframes need native renderer verification.",
            suggestion: "Prefer Dioxus state-driven animation for native parity.",
        },
        ScanRule {
            pattern: "::view-transition",
            capability: PortabilityCapability::ViewTransition,
            severity: Severity::Error,
            message: "browser View Transition pseudo-elements are not native renderer APIs.",
            suggestion: "Use route state transitions or a native renderer animation abstraction.",
        },
        ScanRule {
            pattern: "view-transition-name",
            capability: PortabilityCapability::ViewTransition,
            severity: Severity::Warning,
            message: "view-transition-name relies on browser View Transition snapshot behavior.",
            suggestion: "Mirror route/component identity into native transition metadata.",
        },
        ScanRule {
            pattern: "backdrop-filter",
            capability: PortabilityCapability::CssFilter,
            severity: Severity::Warning,
            message: "backdrop-filter support must be verified in Blitz.",
            suggestion: "Use native-safe fallback surfaces.",
        },
        ScanRule {
            pattern: "content-visibility",
            capability: PortabilityCapability::CssLayout,
            severity: Severity::Warning,
            message: "content-visibility is a browser rendering optimization.",
            suggestion: "Use native virtualization or route-level lazy rendering instead.",
        },
        ScanRule {
            pattern: "radial-gradient",
            capability: PortabilityCapability::CssGradient,
            severity: Severity::Warning,
            message: "advanced gradients should be visually verified in Blitz.",
            suggestion: "Use conservative CSS or native paint primitives.",
        },
        ScanRule {
            pattern: "linear-gradient",
            capability: PortabilityCapability::CssGradient,
            severity: Severity::Info,
            message: "gradients should be checked against the native renderer CSS matrix.",
            suggestion: "Keep a native screenshot check for important branded visuals.",
        },
    ]
}

fn collect_audited_files(root: &Path) -> Result<Vec<PathBuf>, String> {
    let mut files = Vec::new();
    collect_files_recursive(root, &mut files)?;
    files.sort();
    Ok(files)
}

fn collect_files_recursive(dir: &Path, files: &mut Vec<PathBuf>) -> Result<(), String> {
    if is_skipped_dir(dir) {
        return Ok(());
    }

    for entry in
        fs::read_dir(dir).map_err(|error| format!("failed to read `{}`: {error}", dir.display()))?
    {
        let entry = entry.map_err(|error| format!("failed to read directory entry: {error}"))?;
        let path = entry.path();
        if path.is_dir() {
            collect_files_recursive(&path, files)?;
        } else if is_audited_file(&path) {
            files.push(path);
        }
    }
    Ok(())
}

fn is_skipped_dir(path: &Path) -> bool {
    path.file_name()
        .and_then(OsStr::to_str)
        .map(|name| {
            name.starts_with("dioprism-native-port")
                || matches!(
                    name,
                    ".git" | "target" | "node_modules" | "dist" | "tmp" | "graphify-out"
                )
        })
        .unwrap_or(false)
}

fn is_audited_file(path: &Path) -> bool {
    path.file_name() == Some(OsStr::new("Cargo.toml"))
        || path
            .extension()
            .and_then(OsStr::to_str)
            .map(|ext| matches!(ext, "rs" | "css" | "html" | "js" | "ts" | "tsx" | "jsx"))
            .unwrap_or(false)
}

fn is_rust_like(path: &Path) -> bool {
    path.extension().and_then(OsStr::to_str) == Some("rs")
}

fn is_css_like(path: &Path) -> bool {
    path.extension()
        .and_then(OsStr::to_str)
        .map(|ext| matches!(ext, "css" | "html" | "js" | "ts" | "tsx" | "jsx"))
        .unwrap_or(false)
}

fn extract_route(line: &str) -> Option<String> {
    let marker = "#[route(\"";
    let start = line.find(marker)? + marker.len();
    let rest = &line[start..];
    let end = rest.find('"')?;
    Some(rest[..end].to_string())
}

fn extract_href(line: &str) -> Option<String> {
    for marker in ["href: \"", "href=\"", "href = \""] {
        let Some(start) = line.find(marker).map(|index| index + marker.len()) else {
            continue;
        };
        let rest = &line[start..];
        let end = rest.find('"')?;
        return Some(rest[..end].to_string());
    }
    None
}

fn is_asset_href(href: &str) -> bool {
    href.starts_with("/assets/")
        || href.starts_with("/public/")
        || href.ends_with(".css")
        || href.ends_with(".js")
        || href.ends_with(".wasm")
        || href.ends_with(".png")
        || href.ends_with(".jpg")
        || href.ends_with(".jpeg")
        || href.ends_with(".svg")
        || href.ends_with(".webp")
}

fn extract_route_component(line: &str) -> Option<String> {
    let trimmed = line.trim();
    if trimmed.starts_with("#[") || trimmed.is_empty() {
        return None;
    }
    let name: String = trimmed
        .chars()
        .take_while(|ch| ch.is_alphanumeric() || *ch == '_')
        .collect();
    if name.is_empty() { None } else { Some(name) }
}

fn read_package_name(manifest: &Path) -> Option<String> {
    let source = fs::read_to_string(manifest).ok()?;
    source.lines().find_map(|line| {
        let trimmed = line.trim();
        trimmed
            .strip_prefix("name = ")
            .map(|value| value.trim_matches('"').to_string())
    })
}

fn generated_timestamp() -> String {
    let seconds = SystemTime::now()
        .duration_since(UNIX_EPOCH)
        .map(|duration| duration.as_secs())
        .unwrap_or_default();
    format!("unix:{seconds}")
}

fn span(root: &Path, file: &Path, line: usize, snippet: &str, pattern: &str) -> SourceSpan {
    SourceSpan::new(
        relative_path(root, file),
        line,
        snippet.find(pattern).map(|column| column + 1),
        snippet.trim().to_string(),
    )
}

fn relative_path(root: &Path, file: &Path) -> String {
    file.strip_prefix(root)
        .ok()
        .map(path_string)
        .unwrap_or_else(|| path_string(file))
}

fn path_string(path: &Path) -> String {
    path.to_string_lossy().replace('\\', "/")
}

fn write_output(path: &Path, content: &str) -> Result<(), String> {
    if let Some(parent) = path.parent() {
        fs::create_dir_all(parent)
            .map_err(|error| format!("failed to create `{}`: {error}", parent.display()))?;
    }
    fs::write(path, content)
        .map_err(|error| format!("failed to write `{}`: {error}", path.display()))
}

pub mod prelude {
    pub use crate::dioprism_native_port::{
        AuditOptions, AuditOpts, ConvertOptions, ConvertOpts, audit_baseline_report,
        audit_optimized_artifacts, audit_opts, audit_project, audit_projects_batch,
        cli_integration_options, cli_precomputed_fragments, convert_opts, convert_project,
        doctor_report_for_audit, doctor_report_for_plan, manifest_fragment_for_audit,
        manifest_fragment_for_audit_traced, manifest_fragment_for_plan,
        manifest_fragments_for_audits_batch, write_conversion_outputs, write_report_outputs,
    };
}

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

    #[test]
    fn fixture_flags_browser_only_uses() {
        let root = create_fixture("browser-only");
        write_fixture_file(
            &root,
            "Cargo.toml",
            r#"[package]
name = "fixture"
version = "0.1.0"
edition = "2024"
"#,
        );
        write_fixture_file(
            &root,
            "src/main.rs",
            r##"
fn app() {
    let _ = web_sys::window();
    let _ = js_sys::Date::new_0();
    document::eval("alert(1)");
    let _ = window.local_storage();
    let worker = r#"<script type="text/dioxus-worker">postMessage(1)</script>"#;
}
"##,
        );

        let report = audit_project(&root).unwrap();
        let patterns: Vec<_> = report
            .unsupported_uses
            .iter()
            .map(|item| item.pattern.as_str())
            .collect();

        assert!(patterns.contains(&"web_sys::window"));
        assert!(patterns.contains(&"js_sys::Date"));
        assert!(patterns.contains(&"document::eval"));
        assert!(patterns.contains(&"local_storage"));
        assert!(patterns.contains(&"text/dioxus-worker"));
        assert!(!report.summary.native_ready);
        remove_fixture(root);
    }

    #[test]
    fn fixture_does_not_flag_portable_adapters() {
        let root = create_fixture("portable");
        write_fixture_file(
            &root,
            "Cargo.toml",
            r#"[package]
name = "portable"
version = "0.1.0"
edition = "2024"
"#,
        );
        write_fixture_file(
            &root,
            "src/main.rs",
            r#"
use dioprism_native_port::{use_portable_clock, use_portable_storage};
fn app() {
    let _clock = use_portable_clock();
    let _storage = use_portable_storage("theme", || "dark".to_string());
}
"#,
        );

        let report = audit_project(&root).unwrap();
        assert!(report.unsupported_uses.is_empty());
        assert!(report.css_issues.is_empty());
        assert!(report.summary.native_ready);
        remove_fixture(root);
    }

    #[test]
    fn fixture_allows_split_native_and_desktop_renderer_features() {
        let root = create_fixture("split-renderers");
        write_fixture_file(
            &root,
            "Cargo.toml",
            r#"[package]
name = "split-renderers"
version = "0.1.0"
edition = "2024"

[features]
default = ["web"]
web = ["dioxus/web"]
native = ["dioxus/native"]
desktop = ["dioxus/desktop"]
server = ["dioxus/server"]
"#,
        );
        write_fixture_file(&root, "src/main.rs", "fn main() {}\n");

        let report = audit_project(&root).unwrap();
        assert!(report.renderer_conflicts.is_empty());
        remove_fixture(root);
    }

    #[test]
    fn fixture_flags_mixed_native_renderer_features() {
        let root = create_fixture("mixed-renderers");
        write_fixture_file(
            &root,
            "Cargo.toml",
            r#"[package]
name = "mixed-renderers"
version = "0.1.0"
edition = "2024"

[features]
native = ["dioxus/native", "dioxus/desktop"]
"#,
        );
        write_fixture_file(&root, "src/main.rs", "fn main() {}\n");

        let report = audit_project(&root).unwrap();
        assert_eq!(report.renderer_conflicts.len(), 1);
        assert_eq!(
            report.renderer_conflicts[0].pattern,
            "mixed dioxus renderer features"
        );
        assert_eq!(report.renderer_conflicts[0].severity, Severity::Error);
        remove_fixture(root);
    }

    #[test]
    fn fixture_flags_native_feature_aliasing_desktop_renderer() {
        let root = create_fixture("native-aliases-desktop");
        write_fixture_file(
            &root,
            "Cargo.toml",
            r#"[package]
name = "native-aliases-desktop"
version = "0.1.0"
edition = "2024"

[features]
native = ["dioxus/desktop"]
"#,
        );
        write_fixture_file(&root, "src/main.rs", "fn main() {}\n");

        let report = audit_project(&root).unwrap();
        assert_eq!(report.renderer_conflicts.len(), 1);
        assert_eq!(report.renderer_conflicts[0].pattern, "dioxus/desktop");
        assert_eq!(report.renderer_conflicts[0].severity, Severity::Error);
        remove_fixture(root);
    }

    #[test]
    fn explicit_support_markers_suppress_provider_implementation_findings() {
        let root = create_fixture("support-marker");
        write_fixture_file(
            &root,
            "Cargo.toml",
            r#"[package]
name = "support_marker"
version = "0.1.0"
edition = "2024"
"#,
        );
        write_fixture_file(
            &root,
            "src/lib.rs",
            r#"
//! native-port: supports worker, route-asset, css-animation
pub const WORKER_SCRIPT_TYPE: &str = "text/dioxus-worker";
pub const ROUTE_ASSET_ATTR: &str = "data-dxw-src";
pub const STYLE: &str = "@keyframes demo { from { opacity: 0; } to { opacity: 1; } }";
"#,
        );

        let report = audit_project(&root).unwrap();
        assert!(report.unsupported_uses.is_empty());
        assert!(report.css_issues.is_empty());
        assert!(report.summary.native_ready);
        remove_fixture(root);
    }

    #[test]
    fn emits_stable_json_and_markdown_shapes() {
        let root = create_fixture("report-shape");
        write_fixture_file(
            &root,
            "Cargo.toml",
            r#"[package]
name = "shape"
version = "0.1.0"
edition = "2024"
"#,
        );
        write_fixture_file(
            &root,
            "src/main.rs",
            r#"
#[derive(Routable)]
enum Route {
    #[route("/")]
    Home {},
}
"#,
        );

        let report = audit_project(&root).unwrap();
        let json = report.to_json_pretty().unwrap();
        let markdown = report.to_markdown();

        assert!(json.contains("\"package_name\": \"shape\""));
        assert!(markdown.contains("# Dioxus Native Portability Report"));
        assert!(markdown.contains("| `/` | `Home @ src/main.rs` |"));
        remove_fixture(root);
    }

    #[test]
    fn audit_registers_visual_package_manifests_from_cargo_dependencies() {
        let root = create_fixture("visual-manifests");
        write_fixture_file(
            &root,
            "Cargo.toml",
            r#"[package]
name = "visual_manifests"
version = "0.1.0"
edition = "2024"

[dependencies]
dioprism-shader = { path = "../dioprism-shader" }
dioprism-theme = { path = "../dioprism-theme" }
"#,
        );
        write_fixture_file(&root, "src/main.rs", "fn main() {}\n");

        let report = audit_project(&root).unwrap();
        let markdown = report.to_markdown();

        assert_eq!(report.summary.visual_manifests, 2);
        assert!(
            report
                .visual_manifests
                .iter()
                .any(|manifest| manifest.package == "dioprism-shader")
        );
        assert!(
            report
                .visual_warnings
                .iter()
                .any(|warning| warning.package == "dioprism-shader")
        );
        assert!(markdown.contains("Visual Compatibility Warnings"));
        assert!(markdown.contains("OffscreenCanvas"));
        remove_fixture(root);
    }

    #[test]
    fn audit_flags_visual_browser_runtime_patterns() {
        let root = create_fixture("visual-runtime-patterns");
        write_fixture_file(
            &root,
            "Cargo.toml",
            r#"[package]
name = "visual_runtime_patterns"
version = "0.1.0"
edition = "2024"
"#,
        );
        write_fixture_file(
            &root,
            "src/main.rs",
            r#"
fn app() {
    document.startViewTransition(|| {});
    let _ = "OffscreenCanvas";
    let _ = element.getBoundingClientRect();
}
"#,
        );

        let report = audit_project(&root).unwrap();
        let capabilities: Vec<_> = report
            .unsupported_uses
            .iter()
            .map(|item| item.capability.clone())
            .collect();

        assert!(capabilities.contains(&PortabilityCapability::ViewTransition));
        assert!(capabilities.contains(&PortabilityCapability::OffscreenCanvas));
        assert!(capabilities.contains(&PortabilityCapability::DomSnapshot));
        remove_fixture(root);
    }

    #[test]
    fn convert_generates_native_nav_css_and_patch_preview() {
        let root = create_fixture("convert-nav");
        write_fixture_file(
            &root,
            "Cargo.toml",
            r#"[package]
name = "convert_nav"
version = "0.1.0"
edition = "2024"
"#,
        );
        write_fixture_file(
            &root,
            "src/main.rs",
            r#"
#[derive(Routable)]
enum Route {
    #[route("/")]
    Home {},
}

fn app() {
    rsx! {
        header { class: "theme-header sticky top-0",
            div { class: "mx-auto flex max-w-7xl flex-col gap-4 sm:flex-row sm:items-center",
                nav { class: "flex flex-wrap items-center gap-2 text-sm",
                    a { class: "rounded-md px-3 py-2 hover:bg-cyan-300/10 focus:ring-2", href: "/", "Overview" }
                }
            }
        }
    }
}
"#,
        );

        let options = ConvertOptions::default();
        let plan = convert_project(&root, &options).unwrap();
        let css = plan.to_native_css();
        let patch = conversion_patch_preview(&plan, &options.native_css_output, &css);

        assert!(css.contains(".np-header-inner"));
        assert!(css.contains(".np-nav"));
        assert!(css.contains("display: block"));
        assert!(css.contains("min-height: 26px"));
        assert!(patch.contains("native-port.css"));
        assert!(
            plan.changes
                .iter()
                .any(|change| change.replacement.contains("np-nav"))
        );
        remove_fixture(root);
    }

    #[test]
    fn convert_reports_plain_href_routes_not_in_router() {
        let root = create_fixture("convert-routes");
        write_fixture_file(
            &root,
            "Cargo.toml",
            r#"[package]
name = "convert_routes"
version = "0.1.0"
edition = "2024"
"#,
        );
        write_fixture_file(
            &root,
            "src/main.rs",
            r#"
#[derive(Routable)]
enum Route {
    #[route("/")]
    Home {},
}

fn app() {
    rsx! {
        a { href: "/resume-playground", "Resume Lab" }
    }
}
"#,
        );

        let options = ConvertOptions::default();
        let plan = convert_project(&root, &options).unwrap();

        assert!(plan.routes.iter().any(|route| {
            route.route == "/resume-playground" && route.native_action == "external-only"
        }));
        assert!(
            plan.changes
                .iter()
                .any(|change| change.capability == PortabilityCapability::ServerOnlyRoute)
        );
        remove_fixture(root);
    }

    #[test]
    fn convert_detects_website_nav_route_gaps_and_actions() {
        let root = create_fixture("convert-route-gaps");
        write_fixture_file(
            &root,
            "Cargo.toml",
            r#"[package]
name = "convert_route_gaps"
version = "0.1.0"
edition = "2024"
"#,
        );
        write_fixture_file(
            &root,
            "src/main.rs",
            r##"
const RESUME_SITE_SHELL: &str = r#"
<nav class="dxr-nav" aria-label="Demo navigation">
  <a href="/">Overview</a>
  <a href="/workertown">WorkerTown</a>
  <a href="/resume-playground">Resume Lab</a>
</nav>
"#;

#[derive(Routable)]
enum Route {
    #[route("/")]
    Home {},
    #[route("/workertown")]
    WorkerTown {},
}
"##,
        );

        let options = ConvertOptions::default();
        let plan = convert_project(&root, &options).unwrap();

        assert!(plan.routes.iter().any(|route| {
            route.route == "/workertown" && route.native_action == "native-task-pool"
        }));
        assert!(plan.routes.iter().any(|route| {
            route.route == "/resume-playground" && route.native_action == "external-only"
        }));
        assert!(
            plan.changes
                .iter()
                .any(|change| change.capability == PortabilityCapability::ServerOnlyRoute)
        );
        remove_fixture(root);
    }

    #[test]
    fn convert_writes_outputs_but_requires_write_for_tracked_css() {
        let root = create_fixture("convert-write");
        write_fixture_file(
            &root,
            "Cargo.toml",
            r#"[package]
name = "convert_write"
version = "0.1.0"
edition = "2024"
"#,
        );
        write_fixture_file(&root, "src/main.rs", "fn main() {}\n");

        let options = ConvertOptions::default();
        let plan = convert_project(&root, &options).unwrap();
        write_conversion_outputs(&root, &plan, &options).unwrap();

        assert!(
            root.join("tmp/native-port-convert/native-port.css")
                .exists()
        );
        assert!(!root.join("public/assets/native-port.css").exists());

        let write_options = ConvertOptions {
            write: true,
            ..Default::default()
        };
        write_conversion_outputs(&root, &plan, &write_options).unwrap();

        assert!(root.join("public/assets/native-port.css").exists());
        remove_fixture(root);
    }

    #[test]
    fn convert_does_not_rewrite_portable_adapters() {
        let root = create_fixture("convert-portable");
        write_fixture_file(
            &root,
            "Cargo.toml",
            r#"[package]
name = "convert_portable"
version = "0.1.0"
edition = "2024"
"#,
        );
        write_fixture_file(
            &root,
            "src/main.rs",
            r#"
use dioprism_native_port::{use_portable_clock, use_portable_storage};
fn app() {
    let _clock = use_portable_clock();
    let _storage = use_portable_storage("theme", || "dark".to_string());
}
"#,
        );

        let options = ConvertOptions::default();
        let plan = convert_project(&root, &options).unwrap();

        assert!(
            !plan
                .changes
                .iter()
                .any(|change| matches!(change.kind, RewriteKind::RuntimeAdapter))
        );
        remove_fixture(root);
    }

    #[test]
    fn cli_manifest_fragment_and_doctor_use_audit_report() {
        let root = create_fixture("doctor");
        write_fixture_file(
            &root,
            "Cargo.toml",
            r#"[package]
name = "doctor"
version = "0.1.0"
edition = "2024"
"#,
        );
        write_fixture_file(
            &root,
            "src/main.rs",
            r#"
#[derive(Routable)]
enum Route {
    #[route("/")]
    Home {},
}
"#,
        );

        let report = audit_project(&root).unwrap();
        let fragment = manifest_fragment_for_audit(&report);
        let doctor = doctor_report_for_audit(&report);

        assert_eq!(fragment.package_name, "dioprism-cli");
        assert_eq!(fragment.routes[0].route, "/");
        assert!(fragment.tags.contains_key("commands"));
        assert!(doctor.ok());
        remove_fixture(root);
    }

    #[test]
    fn cli_batch_trace_and_baseline_helpers_cover_audit_reports() {
        let root_a = create_fixture("batch-a");
        let root_b = create_fixture("batch-b");
        for (root, package, route) in [(&root_a, "batch_a", "/"), (&root_b, "batch_b", "/docs")] {
            write_fixture_file(
                root,
                "Cargo.toml",
                &format!(
                    r#"[package]
name = "{package}"
version = "0.1.0"
edition = "2024"
"#
                ),
            );
            write_fixture_file(
                root,
                "src/main.rs",
                &format!(
                    r#"
#[derive(Routable)]
enum Route {{
    #[route("{route}")]
    Page {{}},
}}
"#
                ),
            );
        }

        let reports = audit_projects_batch(
            &[root_a.clone(), root_b.clone()],
            DioxusNativePortBatchOptions::default()
                .chunk_size(1)
                .deterministic_parallel(true),
        )
        .unwrap();
        let fragments = manifest_fragments_for_audits_batch(
            &reports,
            DioxusNativePortBatchOptions::default()
                .chunk_size(1)
                .deterministic_parallel(true),
        );
        let trace_events = std::cell::RefCell::new(Vec::new());
        let traced = manifest_fragment_for_audit_traced(&reports[0], &|event| {
            trace_events.borrow_mut().push(event);
        });
        let precomputed = cli_precomputed_fragments();
        let optimized = audit_optimized_artifacts(
            &reports[0],
            &dioprism_native_port::core::WhitespaceDioxusNativePortArtifactOptimizer,
        )
        .expect("audit artifacts optimize");
        let baseline = audit_baseline_report("audit-fixture", &reports[0]);

        assert_eq!(reports[0].package_name.as_deref(), Some("batch_a"));
        assert_eq!(reports[1].package_name.as_deref(), Some("batch_b"));
        assert_eq!(fragments.len(), 2);
        assert_eq!(traced.package_name, "dioprism-cli");
        assert_eq!(trace_events.borrow().len(), 2);
        assert!(precomputed.runtime_marker.contains("dioprism-cli"));
        assert!(
            optimized
                .iter()
                .any(|artifact| artifact.contains("dioprism-cli"))
        );
        assert!(baseline.output_bytes > 0);
        assert_eq!(
            baseline.operation_counts.get("routes").copied(),
            Some(reports[0].routes.len())
        );
        remove_fixture(root_a);
        remove_fixture(root_b);
    }

    fn create_fixture(name: &str) -> PathBuf {
        let root = std::env::temp_dir().join(format!(
            "dioprism-native-port-{name}-{}",
            std::process::id()
        ));
        let _ = fs::remove_dir_all(&root);
        fs::create_dir_all(root.join("src")).unwrap();
        root
    }

    fn write_fixture_file(root: &Path, relative: &str, content: &str) {
        let path = root.join(relative);
        if let Some(parent) = path.parent() {
            fs::create_dir_all(parent).unwrap();
        }
        fs::write(path, content).unwrap();
    }

    fn remove_fixture(root: PathBuf) {
        let _ = fs::remove_dir_all(root);
    }
}
