use crate::manifest::{OsFamily, RunnerManifest, canonical_arch};
use crate::model::{Check, CheckReport, ReceiptSummary};

pub fn validate(manifest: &RunnerManifest, manifest_path: &str) -> CheckReport {
    let mut checks = Vec::new();

    if manifest.image.name.trim().is_empty() {
        checks.push(Check::fail(
            "manifest.image_name_empty",
            "image.name must be a non-empty string",
        ));
    } else {
        checks.push(Check::pass(
            "manifest.image_name_present",
            format!("image name = {}", manifest.image.name),
        ));
    }

    let arch = manifest.image.os.arch.clone();
    let canonical = canonical_arch(&arch);
    if canonical == "unknown" {
        checks.push(Check::fail(
            "manifest.arch_unknown",
            format!("image.os.arch '{arch}' is not a recognized runner arch"),
        ));
    } else {
        checks.push(Check::pass(
            "manifest.arch_recognized",
            format!("image arch = {canonical} (from '{arch}')"),
        ));
    }

    validate_labels(manifest, canonical, &mut checks);
    validate_toolcache_tools(manifest, &mut checks);
    validate_environment(manifest, canonical, &mut checks);
    validate_package_managers(manifest, &mut checks);
    validate_capabilities(manifest, &mut checks);
    validate_evidence(manifest, &mut checks);

    let summary = ReceiptSummary::from_checks(&checks);
    CheckReport {
        manifest_path: manifest_path.to_owned(),
        schema_version: manifest.schema_version,
        image_name: manifest.image.name.clone(),
        summary,
        checks,
    }
}

fn validate_labels(manifest: &RunnerManifest, canonical_arch: &str, checks: &mut Vec<Check>) {
    if manifest.labels.is_empty() {
        checks.push(Check::warn(
            "manifest.labels_empty",
            "labels list is empty (no GitHub-Actions-style routing labels)",
        ));
        return;
    }

    let mentions_arch = manifest.labels.iter().any(|l| {
        let lower = l.to_lowercase();
        lower == canonical_arch
            || lower == manifest.image.os.arch.to_lowercase()
            || (canonical_arch == "x64" && (lower == "x86_64" || lower == "amd64"))
            || (canonical_arch == "arm64" && lower == "aarch64")
    });
    if !mentions_arch {
        checks.push(Check::warn(
            "manifest.labels_arch_missing",
            format!(
                "no label mentions the image arch '{canonical_arch}'; runner routing usually \
                 expects an arch label"
            ),
        ));
    } else {
        checks.push(Check::pass(
            "manifest.labels_arch_present",
            format!("labels include the image arch '{canonical_arch}'"),
        ));
    }

    let expected_family = manifest.image.os.family.as_str();
    let mentions_family = manifest
        .labels
        .iter()
        .any(|l| l.to_lowercase() == expected_family);
    if !mentions_family {
        checks.push(Check::warn(
            "manifest.labels_family_missing",
            format!(
                "no label mentions the OS family '{expected_family}'; runner routing usually \
                 expects '{expected_family}' as a label"
            ),
        ));
    } else {
        checks.push(Check::pass(
            "manifest.labels_family_present",
            format!("labels include OS family '{expected_family}'"),
        ));
    }
}

fn validate_toolcache_tools(manifest: &RunnerManifest, checks: &mut Vec<Check>) {
    let toolcache_root = manifest.toolcache_root();
    let cached_tools: Vec<&_> = manifest.tools.iter().filter(|t| t.in_toolcache).collect();
    match (toolcache_root, cached_tools.is_empty()) {
        (None, false) => checks.push(Check::fail(
            "manifest.toolcache_root_missing",
            "tools list marks entries as in_toolcache but toolcache.root is unset",
        )),
        (Some(root), _) => {
            checks.push(Check::pass(
                "manifest.toolcache_root_present",
                format!("toolcache.root = {root}"),
            ));
            for tool in cached_tools {
                if let Some(path) = &tool.path
                    && !path.starts_with(root)
                {
                    checks.push(Check::warn(
                        "manifest.toolcache_path_outside_root",
                        format!(
                            "tool '{name}@{version}' has path '{path}' outside toolcache.root '{root}'",
                            name = tool.name,
                            version = tool.version
                        ),
                    ));
                }
            }
        }
        (None, true) => {
            checks.push(Check::skip(
                "manifest.toolcache_root_optional",
                "toolcache.root not declared; no tool is marked in_toolcache",
            ));
        }
    }

    for tool in &manifest.tools {
        if tool.name.trim().is_empty() {
            checks.push(Check::fail(
                "manifest.tool_name_empty",
                "a tool entry has an empty name",
            ));
        }
        if tool.version.trim().is_empty() {
            checks.push(Check::fail(
                "manifest.tool_version_empty",
                format!("tool '{}' has an empty version", tool.name),
            ));
        }
    }
}

fn validate_environment(manifest: &RunnerManifest, canonical_arch: &str, checks: &mut Vec<Check>) {
    let env = &manifest.environment;
    if let Some(runner_os) = env.get("RUNNER_OS") {
        let expected = match manifest.image.os.family {
            OsFamily::Linux => "Linux",
            OsFamily::Macos => "macOS",
            OsFamily::Windows => "Windows",
        };
        if !runner_os.eq_ignore_ascii_case(expected) {
            checks.push(Check::warn(
                "manifest.env_runner_os_mismatch",
                format!("RUNNER_OS = '{runner_os}' does not match image family '{expected}'"),
            ));
        } else {
            checks.push(Check::pass(
                "manifest.env_runner_os_consistent",
                format!("RUNNER_OS matches image family '{expected}'"),
            ));
        }
    }
    if let Some(runner_arch) = env.get("RUNNER_ARCH") {
        let want = match canonical_arch {
            "x64" => "X64",
            "arm64" => "ARM64",
            "x86" => "X86",
            "arm" => "ARM",
            other => other,
        };
        if !runner_arch.eq_ignore_ascii_case(want) {
            checks.push(Check::warn(
                "manifest.env_runner_arch_mismatch",
                format!("RUNNER_ARCH = '{runner_arch}' does not match image arch '{want}'"),
            ));
        } else {
            checks.push(Check::pass(
                "manifest.env_runner_arch_consistent",
                format!("RUNNER_ARCH matches image arch '{want}'"),
            ));
        }
    }
    if let (Some(runner_tool_cache), Some(root)) =
        (env.get("RUNNER_TOOL_CACHE"), manifest.toolcache_root())
        && runner_tool_cache != root
    {
        checks.push(Check::warn(
            "manifest.env_runner_tool_cache_mismatch",
            format!("RUNNER_TOOL_CACHE = '{runner_tool_cache}' but toolcache.root = '{root}'"),
        ));
    }
}

fn validate_package_managers(manifest: &RunnerManifest, checks: &mut Vec<Check>) {
    if manifest.packages.is_empty() {
        return;
    }
    for pkg in &manifest.packages {
        match pkg.manager.as_str() {
            "apt" | "dpkg" | "dnf" | "yum" | "rpm" | "brew" | "pkg" | "choco" | "winget"
            | "scoop" | "apk" | "pacman" | "zypper" => {}
            other => {
                checks.push(Check::warn(
                    "manifest.package_manager_unknown",
                    format!(
                        "package '{name}' uses unrecognized manager '{other}'",
                        name = pkg.name
                    ),
                ));
            }
        }
    }
}

fn validate_capabilities(manifest: &RunnerManifest, checks: &mut Vec<Check>) {
    // No closed catalog of capabilities in v1 — they are caller-defined strings.
    // But warn on obvious typos for common ones.
    let well_known = [
        "docker",
        "docker-socket",
        "podman",
        "buildx",
        "playwright-chromium",
        "playwright-firefox",
        "playwright-webkit",
        "tauri-linux-build",
        "tauri-windows-build",
        "tauri-macos-build",
        "cargo-advisory-tools",
        "android-sdk",
        "xcode",
    ];
    for cap in &manifest.capabilities {
        let lower = cap.to_lowercase();
        if lower != *cap {
            checks.push(Check::warn(
                "manifest.capability_case",
                format!("capability '{cap}' is not lowercase; convention is kebab-case lowercase"),
            ));
        }
        if !well_known.contains(&lower.as_str()) {
            // Don't fail — just note it. Capabilities are open-ended.
            checks.push(Check::pass(
                "manifest.capability_recognized",
                format!("capability '{cap}' accepted (not in well-known list)"),
            ));
        } else {
            checks.push(Check::pass(
                "manifest.capability_well_known",
                format!("capability '{cap}' is well-known"),
            ));
        }
    }
}

fn validate_evidence(manifest: &RunnerManifest, checks: &mut Vec<Check>) {
    if let Some(evidence) = &manifest.evidence {
        if !evidence.source_urls.is_empty() {
            checks.push(Check::pass(
                "manifest.evidence_sources_present",
                format!("{} evidence source URLs", evidence.source_urls.len()),
            ));
        }
    }
}

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

    fn parse(json: &str) -> RunnerManifest {
        serde_json::from_str(json).unwrap()
    }

    #[test]
    fn validate_minimal_manifest_passes() {
        let manifest = parse(
            r#"{
            "schema_version": 1,
            "image": { "name": "ci-forge/ubuntu-22.04",
                       "os": { "family": "linux", "arch": "x64" } },
            "labels": ["linux", "x64"]
        }"#,
        );
        let report = validate(&manifest, "fixture.json");
        assert_eq!(report.summary.failed, 0);
        assert!(report.summary.passed > 0);
    }

    #[test]
    fn empty_image_name_fails() {
        let manifest = parse(
            r#"{
            "schema_version": 1,
            "image": { "name": "", "os": { "family": "linux", "arch": "x64" } }
        }"#,
        );
        let report = validate(&manifest, "fixture.json");
        assert!(
            report
                .checks
                .iter()
                .any(|c| c.id == "manifest.image_name_empty")
        );
        assert!(report.summary.failed >= 1);
    }

    #[test]
    fn unknown_arch_fails() {
        let manifest = parse(
            r#"{
            "schema_version": 1,
            "image": { "name": "img", "os": { "family": "linux", "arch": "weirdarch" } }
        }"#,
        );
        let report = validate(&manifest, "fixture.json");
        assert!(
            report
                .checks
                .iter()
                .any(|c| c.id == "manifest.arch_unknown")
        );
    }

    #[test]
    fn toolcache_path_outside_root_warns() {
        let manifest = parse(
            r#"{
            "schema_version": 1,
            "image": { "name": "img", "os": { "family": "linux", "arch": "x64" } },
            "toolcache": { "root": "/opt/hostedtoolcache" },
            "tools": [
                { "name": "node", "version": "22.11.0",
                  "path": "/usr/local/node/bin", "in_toolcache": true }
            ]
        }"#,
        );
        let report = validate(&manifest, "fixture.json");
        assert!(
            report
                .checks
                .iter()
                .any(|c| c.id == "manifest.toolcache_path_outside_root")
        );
    }

    #[test]
    fn missing_toolcache_root_with_cached_tool_fails() {
        let manifest = parse(
            r#"{
            "schema_version": 1,
            "image": { "name": "img", "os": { "family": "linux", "arch": "x64" } },
            "tools": [
                { "name": "node", "version": "22.11.0", "in_toolcache": true }
            ]
        }"#,
        );
        let report = validate(&manifest, "fixture.json");
        assert!(
            report
                .checks
                .iter()
                .any(|c| c.id == "manifest.toolcache_root_missing")
        );
    }

    #[test]
    fn runner_os_mismatch_warns() {
        let manifest = parse(
            r#"{
            "schema_version": 1,
            "image": { "name": "img", "os": { "family": "linux", "arch": "x64" } },
            "environment": { "RUNNER_OS": "Windows" }
        }"#,
        );
        let report = validate(&manifest, "fixture.json");
        assert!(
            report
                .checks
                .iter()
                .any(|c| c.id == "manifest.env_runner_os_mismatch")
        );
    }

    #[test]
    fn unknown_package_manager_warns() {
        let manifest = parse(
            r#"{
            "schema_version": 1,
            "image": { "name": "img", "os": { "family": "linux", "arch": "x64" } },
            "packages": [{ "manager": "fakepkg", "name": "thing", "version": "1.0" }]
        }"#,
        );
        let report = validate(&manifest, "fixture.json");
        assert!(
            report
                .checks
                .iter()
                .any(|c| c.id == "manifest.package_manager_unknown")
        );
    }
}
