//! Phase 08 fleet guard: scanner crates must not depend on `reqwest` directly.
//!
//! HTTP stacks route through `scanclient` (which owns the workspace `reqwest`
//! pin, proxywire integration, redirect SSRF policy, and optional TLS impersonate).
//!
//! ## Allowed exceptions (`ALLOWLIST`)
//!
//! Document each path here when a crate must keep a direct `reqwest` dep:
//!
//! - `libs/scanner/scanclient` - implements the pooled client.
//! - `software/wafrift/**` - deferred to wafrift adoption wave (transport still wraps scanclient for stealth).
//! - `software/secbench/**` - secbench harness adapters (phase 07/15).
//! - `detonation/**` - static analyzers with optional HTTP smoke tests only.
//!
//! Crates migrated in MP-R08 closeout: `gqlprobe`, `gossan-core`, `keyhog-verifier`,
//! `scald-core` (session/multipart documented in `scald-core/SCANCLIENT_GAPS.md`).

use std::fs;
use std::path::{Path, PathBuf};

/// Phase-08 closeout crates that must not list `reqwest` under `[dependencies]`.
const PHASE08_CRATES: &[&str] = &[
    "software/gqlprobe/Cargo.toml",
    "software/gossan/crates/core/Cargo.toml",
    "software/scald/crates/scald-core/Cargo.toml",
];

/// Crates with a deliberate direct `reqwest` dep until scanclient 0.3 + workspace unify.
const PHASE08_REQWEST_EXCEPTIONS: &[(&str, &str)] = &[(
    "software/keyhog/crates/verifier/Cargo.toml",
    "isolated keyhog workspace: HTTP on reqwest; vendor bogon until scanclient 0.3 + workspace unify",
)];

/// Broader fleet roots for advisory reporting (not failing until migrated).
const FLEET_ROOTS_ADVISORY: &[&str] = &["software/wafrift", "software/secbench", "detonation"];

/// Crate manifest paths permitted to list `reqwest` under `[dependencies]`.
const ALLOWLIST: &[&str] = &[
    "libs/scanner/scanclient/Cargo.toml",
    "software/wafrift/crates/transport/Cargo.toml",
    "software/wafrift/crates/detect/Cargo.toml",
    "software/wafrift/crates/cli/Cargo.toml",
    "software/wafrift/crates/pool/Cargo.toml",
    "software/wafrift/crates/proxy/Cargo.toml",
    "software/wafrift/crates/oracle/Cargo.toml",
    "software/wafrift/crates/recon/Cargo.toml",
    "software/wafrift/Cargo.toml",
    "software/secbench/crates/adapters/scald/Cargo.toml",
    "software/secbench/crates/adapters/bolascan/Cargo.toml",
    "software/secbench/crates/adapters/respdiff/Cargo.toml",
    "software/secbench/crates/providers/auth-identity/Cargo.toml",
    "detonation/godet/Cargo.toml",
    "detonation/jsdet/crates/jsdet-cli/Cargo.toml",
    "detonation/httpdet/Cargo.toml",
    "detonation/gemdet/Cargo.toml",
];

fn santh_root() -> PathBuf {
    Path::new(env!("CARGO_MANIFEST_DIR"))
        .join("../../..")
        .canonicalize()
        .unwrap_or_else(|e| panic!("canonicalize Santh root: {e}"))
}

fn walk_cargo_tomls(dir: &Path, out: &mut Vec<PathBuf>) {
    let entries = fs::read_dir(dir).unwrap_or_else(|e| panic!("read_dir {}: {e}", dir.display()));
    for ent in entries.flatten() {
        let path = ent.path();
        if path.is_dir() {
            let name = path.file_name().and_then(|n| n.to_str()).unwrap_or("");
            if name == "target" || name == "vendor" || name.starts_with('.') {
                continue;
            }
            walk_cargo_tomls(&path, out);
            continue;
        }
        if path.file_name().is_some_and(|n| n == "Cargo.toml") {
            out.push(path);
        }
    }
}

fn manifest_lists_reqwest(manifest: &str) -> bool {
    let mut in_deps = false;
    for line in manifest.lines() {
        let trimmed = line.trim();
        if trimmed.starts_with('[') {
            in_deps = trimmed == "[dependencies]" || trimmed.starts_with("[dependencies.");
            continue;
        }
        if !in_deps {
            continue;
        }
        if trimmed.starts_with("reqwest") {
            return true;
        }
    }
    false
}

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

#[test]
fn phase08_closeout_crates_do_not_depend_on_reqwest_directly() {
    let root = santh_root();
    let mut violations = Vec::new();
    for rel in PHASE08_CRATES {
        let path = root.join(rel);
        let text =
            fs::read_to_string(&path).unwrap_or_else(|e| panic!("read {}: {e}", path.display()));
        if manifest_lists_reqwest(&text) {
            violations.push(format!(
                "{rel}: direct `reqwest` dependency - use `scanclient` and `scanclient::reqwest` instead"
            ));
        }
    }
    assert!(
        violations.is_empty(),
        "Phase 08 migrated crates must not depend on reqwest directly:\n{}",
        violations.join("\n")
    );

    for (rel, reason) in PHASE08_REQWEST_EXCEPTIONS {
        let path = root.join(rel);
        let text =
            fs::read_to_string(&path).unwrap_or_else(|e| panic!("read {}: {e}", path.display()));
        assert!(
            manifest_lists_reqwest(&text),
            "exception {rel} should still list reqwest while documented: {reason}"
        );
    }
}

#[test]
fn fleet_reqwest_deps_are_allowlisted_or_phase08_clean() {
    let root = santh_root();
    let mut manifests = Vec::new();
    for fleet in FLEET_ROOTS_ADVISORY {
        let dir = root.join(fleet);
        if dir.is_dir() {
            walk_cargo_tomls(&dir, &mut manifests);
        }
    }

    let mut violations = Vec::new();
    for path in manifests {
        let rel = rel_path(&root, &path);
        if ALLOWLIST.iter().any(|allowed| *allowed == rel) {
            continue;
        }
        let text =
            fs::read_to_string(&path).unwrap_or_else(|e| panic!("read {}: {e}", path.display()));
        if manifest_lists_reqwest(&text) {
            violations.push(format!(
                "{rel}: direct `reqwest` - migrate to scanclient or add to ALLOWLIST in no_reqwest_direct_in_fleet.rs"
            ));
        }
    }

    assert!(
        violations.is_empty(),
        "unallowlisted direct reqwest deps in advisory fleet sweep:\n{}",
        violations.join("\n")
    );
}
