use std::fs;
use std::path::Path;
use tempfile::TempDir;

// ── Test helpers ──────────────────────────────────────────────────────────────

fn touch(root: &Path, rel_path: &str) {
    let full = root.join(rel_path);
    if let Some(parent) = full.parent() {
        fs::create_dir_all(parent).unwrap();
    }
    fs::write(full, "").unwrap();
}

fn write_manifest(root: &Path, content: &str) {
    fs::write(root.join("services.yaml"), content).unwrap();
}

fn load(
    root: &Path,
) -> (
    svccat::manifest::Manifest,
    Vec<svccat::discovery::DiscoveredService>,
) {
    let m = svccat::manifest::Manifest::load(&root.join("services.yaml")).unwrap();
    let d = svccat::discovery::discover_services(root, &m);
    (m, d)
}

// ── Tests ─────────────────────────────────────────────────────────────────────

#[test]
fn no_drift_when_all_services_found() {
    let dir = TempDir::new().unwrap();
    let root = dir.path();

    touch(root, "services/api-gateway/Cargo.toml");
    touch(root, "services/auth-service/Dockerfile");

    write_manifest(
        root,
        r#"
discovery:
  paths:
    - "services/*"
services:
  - name: api-gateway
    language: Rust
    role: API gateway
    platform: Cloud Run
  - name: auth-service
    language: Python
    role: Authentication
    platform: Cloud Run
"#,
    );

    let (m, d) = load(root);
    let report = svccat::drift::analyze(&m, &d, root);
    assert_eq!(
        report.drifts.len(),
        0,
        "unexpected drift: {:?}",
        report.drifts
    );
}

#[test]
fn detects_declared_service_missing_from_repo() {
    let dir = TempDir::new().unwrap();
    let root = dir.path();

    touch(root, "services/api-gateway/Cargo.toml");
    // auth-service directory intentionally absent

    write_manifest(
        root,
        r#"
discovery:
  paths:
    - "services/*"
services:
  - name: api-gateway
    language: Rust
    role: API gateway
    platform: Cloud Run
  - name: auth-service
    language: Python
    role: Authentication
    platform: Cloud Run
"#,
    );

    let (m, d) = load(root);
    let report = svccat::drift::analyze(&m, &d, root);

    let missing: Vec<_> = report
        .drifts
        .iter()
        .filter(|item| item.kind == svccat::drift::DriftKind::DeclaredMissingFromRepo)
        .collect();
    assert_eq!(missing.len(), 1, "expected exactly one missing service");
    assert_eq!(missing[0].service, "auth-service");
    assert_eq!(missing[0].severity, svccat::drift::Severity::Error);
}

#[test]
fn detects_undeclared_service_in_repo() {
    let dir = TempDir::new().unwrap();
    let root = dir.path();

    touch(root, "services/api-gateway/Cargo.toml");
    touch(root, "services/auth-service/Dockerfile");
    touch(root, "services/new-feature/go.mod"); // not in manifest

    write_manifest(
        root,
        r#"
discovery:
  paths:
    - "services/*"
services:
  - name: api-gateway
    language: Rust
    role: API gateway
    platform: Cloud Run
  - name: auth-service
    language: Python
    role: Authentication
    platform: Cloud Run
"#,
    );

    let (m, d) = load(root);
    let report = svccat::drift::analyze(&m, &d, root);

    let undeclared: Vec<_> = report
        .drifts
        .iter()
        .filter(|item| item.kind == svccat::drift::DriftKind::UndeclaredInRepo)
        .collect();
    assert_eq!(
        undeclared.len(),
        1,
        "expected exactly one undeclared service"
    );
    assert_eq!(undeclared[0].service, "new-feature");
    assert_eq!(undeclared[0].severity, svccat::drift::Severity::Warning);
}

#[test]
fn detects_missing_role_field() {
    let dir = TempDir::new().unwrap();
    let root = dir.path();

    touch(root, "services/api-gateway/Cargo.toml");

    write_manifest(
        root,
        r#"
discovery:
  paths:
    - "services/*"
services:
  - name: api-gateway
    language: Rust
    platform: Cloud Run
    # role intentionally omitted
"#,
    );

    let (m, d) = load(root);
    let report = svccat::drift::analyze(&m, &d, root);

    let field_drifts: Vec<_> = report
        .drifts
        .iter()
        .filter(|item| item.kind == svccat::drift::DriftKind::MissingField)
        .collect();
    assert_eq!(field_drifts.len(), 1, "expected one missing-field drift");
    assert_eq!(field_drifts[0].detail.as_deref(), Some("role"));
    assert_eq!(field_drifts[0].severity, svccat::drift::Severity::Error);
}

#[test]
fn detects_missing_referenced_docs_file() {
    let dir = TempDir::new().unwrap();
    let root = dir.path();

    touch(root, "services/api-gateway/Cargo.toml");
    // docs/api-gateway.md intentionally absent

    write_manifest(
        root,
        r#"
discovery:
  paths:
    - "services/*"
services:
  - name: api-gateway
    language: Rust
    role: API gateway
    platform: Cloud Run
    docs: docs/api-gateway.md
"#,
    );

    let (m, d) = load(root);
    let report = svccat::drift::analyze(&m, &d, root);

    let ref_drifts: Vec<_> = report
        .drifts
        .iter()
        .filter(|item| item.kind == svccat::drift::DriftKind::MissingReferencedFile)
        .collect();
    assert_eq!(ref_drifts.len(), 1);
    assert!(ref_drifts[0].message.contains("docs"));
}

#[test]
fn explicit_path_overrides_name_matching() {
    let dir = TempDir::new().unwrap();
    let root = dir.path();

    // Service directory is nested, not top-level services/
    touch(root, "infra/gateway/Cargo.toml");

    write_manifest(
        root,
        r#"
discovery:
  paths:
    - "services/*"
services:
  - name: api-gateway
    language: Rust
    role: API gateway
    platform: Cloud Run
    path: infra/gateway
"#,
    );

    let (m, d) = load(root);
    let report = svccat::drift::analyze(&m, &d, root);

    // Should be no "missing" drift (explicit path matched), but api-gateway
    // is not in services/* discovery so no undeclared entries either.
    let missing: Vec<_> = report
        .drifts
        .iter()
        .filter(|i| i.kind == svccat::drift::DriftKind::DeclaredMissingFromRepo)
        .collect();
    assert_eq!(missing.len(), 0, "explicit path should resolve correctly");
}

#[test]
fn json_output_is_valid() {
    let dir = TempDir::new().unwrap();
    let root = dir.path();

    touch(root, "services/api-gateway/Cargo.toml");

    write_manifest(
        root,
        r#"
discovery:
  paths:
    - "services/*"
services:
  - name: api-gateway
    language: Rust
    role: API gateway
    platform: Cloud Run
"#,
    );

    let (m, d) = load(root);
    let report = svccat::drift::analyze(&m, &d, root);

    // Serialise to JSON — must not panic and must round-trip cleanly.
    let json_str = serde_json::to_string_pretty(&report).unwrap();
    let _: serde_json::Value = serde_json::from_str(&json_str).unwrap();
}

// ── depends_on graph tests ────────────────────────────────────────────────────

#[test]
fn graph_renders_depends_on_edges() {
    use std::io::Write;

    let dir = TempDir::new().unwrap();
    let root = dir.path();

    write_manifest(
        root,
        r#"
discovery:
  paths:
    - "services/*"
services:
  - name: payment-service
    language: Rust
    role: payments
    platform: Cloud Run
    depends_on:
      - auth-service
      - postgres
  - name: auth-service
    language: Go
    role: authentication
    platform: Cloud Run
  - name: postgres
    language: SQL
    role: database
    platform: Cloud SQL
"#,
    );

    let m = svccat::manifest::Manifest::load(&root.join("services.yaml")).unwrap();

    // Capture stdout
    let mut output = Vec::new();
    {
        // Render to a string by using the public render function
        // We test the manifest parses depends_on correctly
        assert_eq!(m.services[0].depends_on, vec!["auth-service", "postgres"]);
        assert!(m.services[1].depends_on.is_empty());
    }

    // Verify depends_on survives a YAML round-trip
    let yaml = serde_yaml::to_string(&m).unwrap();
    let m2: svccat::manifest::Manifest = serde_yaml::from_str(&yaml).unwrap();
    assert_eq!(m2.services[0].depends_on, vec!["auth-service", "postgres"]);
    let _ = output.flush();
}

// ── ping tests (unit-level, no real HTTP) ────────────────────────────────────

#[test]
fn ping_result_is_ok_for_reachable() {
    use svccat::ping::{PingResult, PingStatus};

    let r = PingResult {
        service: "api".to_string(),
        url: "https://example.com".to_string(),
        ping: PingStatus::Reachable { code: 200 },
    };
    assert!(r.is_ok());

    let r2 = PingResult {
        service: "api".to_string(),
        url: "https://example.com".to_string(),
        ping: PingStatus::Reachable { code: 404 },
    };
    assert!(r2.is_ok()); // reachable even if 404

    let r3 = PingResult {
        service: "api".to_string(),
        url: "https://example.com".to_string(),
        ping: PingStatus::Unreachable {
            reason: "connection refused".to_string(),
        },
    };
    assert!(!r3.is_ok());
}

#[test]
fn ping_skips_services_without_url() {
    let dir = TempDir::new().unwrap();
    let root = dir.path();

    write_manifest(
        root,
        r#"
services:
  - name: no-url-service
    language: Rust
    role: api
    platform: Cloud Run
"#,
    );

    let m = svccat::manifest::Manifest::load(&root.join("services.yaml")).unwrap();
    // ping_services returns empty because no url is set
    // We test the filter logic: services without url are skipped
    let services_with_url: Vec<_> = m.services.iter().filter(|s| s.url.is_some()).collect();
    assert_eq!(services_with_url.len(), 0);
}

#[test]
fn init_creates_services_yaml_from_discovered_services() {
    let dir = TempDir::new().unwrap();
    let root = dir.path();

    touch(root, "services/api-gateway/Cargo.toml");
    touch(root, "services/worker/go.mod");

    let output = root.join("services.yaml");
    svccat::init::run(root, output.clone(), false).unwrap();

    assert!(output.exists(), "services.yaml should be created");
    let contents = fs::read_to_string(&output).unwrap();
    assert!(
        contents.contains("api-gateway"),
        "should include api-gateway"
    );
    assert!(contents.contains("worker"), "should include worker");
    assert!(
        contents.contains("Rust"),
        "should infer Rust from Cargo.toml"
    );
    assert!(contents.contains("Go"), "should infer Go from go.mod");
}

#[test]
fn init_infers_platform_from_deploy_descriptor() {
    let dir = TempDir::new().unwrap();
    let root = dir.path();

    touch(root, "services/web/Cargo.toml");
    touch(root, "services/web/fly.toml");

    let output = root.join("services.yaml");
    svccat::init::run(root, output.clone(), false).unwrap();

    let contents = fs::read_to_string(&output).unwrap();
    assert!(
        contents.contains("platform: fly.io"),
        "should infer fly.io platform from fly.toml, got:\n{contents}"
    );
}

#[test]
fn init_leaves_platform_placeholder_without_descriptor() {
    let dir = TempDir::new().unwrap();
    let root = dir.path();

    // Only a Dockerfile: not a platform signal, so platform stays a placeholder.
    touch(root, "services/api/Dockerfile");

    let output = root.join("services.yaml");
    svccat::init::run(root, output.clone(), false).unwrap();

    let contents = fs::read_to_string(&output).unwrap();
    assert!(
        contents.contains("platform: ~"),
        "platform should remain a placeholder, got:\n{contents}"
    );
}

#[test]
fn fix_infers_language_and_platform_for_added_services() {
    let dir = TempDir::new().unwrap();
    let root = dir.path();

    write_manifest(
        root,
        "version: \"1\"\ndiscovery:\n  paths:\n    - services/*\nservices: []\n",
    );

    touch(root, "services/worker/go.mod");
    fs::create_dir_all(root.join("services/worker/k8s")).unwrap();

    let manifest_path = root.join("services.yaml");
    svccat::fix::run(&manifest_path, root, &[], 1, false, false).unwrap();

    let m = svccat::manifest::Manifest::load(&manifest_path).unwrap();
    let worker = m
        .services
        .iter()
        .find(|s| s.name == "worker")
        .expect("worker should be added by fix");
    assert_eq!(worker.language.as_deref(), Some("Go"));
    assert_eq!(worker.platform.as_deref(), Some("kubernetes"));
}

#[test]
fn init_refuses_to_overwrite_without_force() {
    let dir = TempDir::new().unwrap();
    let root = dir.path();
    let output = root.join("services.yaml");
    fs::write(&output, "existing content").unwrap();

    let result = svccat::init::run(root, output, false);
    assert!(
        result.is_err(),
        "should error when file exists and --force not set"
    );
}

#[test]
fn init_overwrites_with_force() {
    let dir = TempDir::new().unwrap();
    let root = dir.path();
    let output = root.join("services.yaml");
    fs::write(&output, "old content").unwrap();

    touch(root, "services/my-svc/Dockerfile");
    svccat::init::run(root, output.clone(), true).unwrap();

    let contents = fs::read_to_string(&output).unwrap();
    assert!(
        contents.contains("my-svc"),
        "should contain discovered service"
    );
    assert!(
        !contents.contains("old content"),
        "should overwrite old content"
    );
}

#[test]
fn init_empty_repo_writes_skeleton() {
    let dir = TempDir::new().unwrap();
    let root = dir.path();
    let output = root.join("services.yaml");

    svccat::init::run(root, output.clone(), false).unwrap();

    let contents = fs::read_to_string(&output).unwrap();
    assert!(
        contents.contains("version"),
        "skeleton should contain version key"
    );
    assert!(
        contents.contains("services:"),
        "skeleton should contain services key"
    );
}

// ── ignore pattern tests ──────────────────────────────────────────────────────

#[test]
fn ignore_patterns_exclude_matching_directories() {
    let dir = TempDir::new().unwrap();
    let root = dir.path();

    touch(root, "services/api/Cargo.toml");
    touch(root, "services/examples/Cargo.toml"); // should be ignored
    touch(root, "services/vendor/go.mod"); // should be ignored

    write_manifest(
        root,
        r#"
discovery:
  paths:
    - "services/*"
  ignore:
    - "services/examples"
    - "services/vendor"
services:
  - name: api
    language: Rust
    role: api
    platform: Cloud Run
"#,
    );

    let m = svccat::manifest::Manifest::load(&root.join("services.yaml")).unwrap();
    let discovered = svccat::discovery::discover_services(root, &m);

    let names: Vec<&str> = discovered.iter().map(|d| d.name.as_str()).collect();
    assert!(names.contains(&"api"), "api should be discovered");
    assert!(!names.contains(&"examples"), "examples should be ignored");
    assert!(!names.contains(&"vendor"), "vendor should be ignored");
}

#[test]
fn extra_ignore_from_cli_merges_with_manifest() {
    let dir = TempDir::new().unwrap();
    let root = dir.path();

    touch(root, "services/api/Cargo.toml");
    touch(root, "services/scratch/Cargo.toml");

    write_manifest(
        root,
        r#"
discovery:
  paths:
    - "services/*"
services:
  - name: api
    language: Rust
    role: api
    platform: Cloud Run
"#,
    );

    let m = svccat::manifest::Manifest::load(&root.join("services.yaml")).unwrap();
    // Pass "services/scratch" as an extra CLI ignore
    let discovered = svccat::discovery::discover_services_with_ignore(
        root,
        &m,
        &["services/scratch".to_string()],
    );

    let names: Vec<&str> = discovered.iter().map(|d| d.name.as_str()).collect();
    assert!(names.contains(&"api"));
    assert!(
        !names.contains(&"scratch"),
        "scratch should be excluded by CLI ignore"
    );
}

// ── svccat.toml config tests ──────────────────────────────────────────────────

#[test]
fn config_loads_from_svccat_toml() {
    let dir = TempDir::new().unwrap();
    let root = dir.path();

    fs::write(
        root.join("svccat.toml"),
        r#"
fail_on_drift = true
ignore = ["examples/*", "vendor/*"]
"#,
    )
    .unwrap();

    let cfg = svccat::config::SvccatConfig::load(root).unwrap();
    assert!(cfg.fail_on_drift);
    assert_eq!(cfg.ignore, vec!["examples/*", "vendor/*"]);
}

#[test]
fn config_returns_defaults_when_no_file() {
    let dir = TempDir::new().unwrap();
    let root = dir.path();

    let cfg = svccat::config::SvccatConfig::load(root).unwrap();
    assert!(!cfg.fail_on_drift);
    assert!(cfg.ignore.is_empty());
}

// ── completions test ──────────────────────────────────────────────────────────

#[test]
fn completions_produces_output_for_bash() {
    use clap::CommandFactory;
    use clap_complete::{generate, Shell};
    use svccat::cli::Cli;

    let mut cmd = Cli::command();
    let mut buf = Vec::new();
    generate(Shell::Bash, &mut cmd, "svccat", &mut buf);
    assert!(!buf.is_empty(), "bash completions should produce output");
    let script = String::from_utf8(buf).unwrap();
    assert!(
        script.contains("svccat"),
        "completions should mention the binary name"
    );
}

// ── policy tests ──────────────────────────────────────────────────────────────

#[test]
fn policy_require_fields_flags_missing_url() {
    let dir = TempDir::new().unwrap();
    let root = dir.path();

    touch(root, "services/api/Cargo.toml");

    write_manifest(
        root,
        r#"
discovery:
  paths:
    - "services/*"
policy:
  require_fields: [url, language]
services:
  - name: api
    language: Rust
    role: api
    platform: Cloud Run
"#,
    );

    let (m, d) = load(root);
    let report = svccat::drift::analyze(&m, &d, root);

    let policy_violations: Vec<_> = report
        .drifts
        .iter()
        .filter(|i| i.kind == svccat::drift::DriftKind::PolicyViolation)
        .collect();

    assert_eq!(
        policy_violations.len(),
        1,
        "url should be flagged by policy"
    );
    assert!(policy_violations[0].message.contains("url"));
}

#[test]
fn policy_no_violations_when_fields_present() {
    let dir = TempDir::new().unwrap();
    let root = dir.path();

    touch(root, "services/api/Cargo.toml");

    write_manifest(
        root,
        r#"
discovery:
  paths:
    - "services/*"
policy:
  require_fields: [url, language]
services:
  - name: api
    language: Rust
    role: api
    platform: Cloud Run
    url: https://api.example.com
"#,
    );

    let (m, d) = load(root);
    let report = svccat::drift::analyze(&m, &d, root);

    let violations: Vec<_> = report
        .drifts
        .iter()
        .filter(|i| i.kind == svccat::drift::DriftKind::PolicyViolation)
        .collect();
    assert!(violations.is_empty(), "no policy violations expected");
}

// ── diff tests ────────────────────────────────────────────────────────────────

#[test]
fn diff_detects_added_and_removed_services() {
    let dir = TempDir::new().unwrap();
    let root = dir.path();

    let before_json = r#"{
      "version": "1",
      "manifest": "services.yaml",
      "summary": {"declared": 2, "discovered": 2, "drift_count": 0, "errors": 0, "warnings": 0},
      "services": [
        {"name": "api", "language": "Rust", "platform": "Cloud Run", "role": "api", "depends_on": []},
        {"name": "legacy", "language": "Go", "platform": "Fly.io", "role": "worker", "depends_on": []}
      ],
      "drift": []
    }"#;

    let after_json = r#"{
      "version": "1",
      "manifest": "services.yaml",
      "summary": {"declared": 2, "discovered": 2, "drift_count": 0, "errors": 0, "warnings": 0},
      "services": [
        {"name": "api", "language": "Rust", "platform": "Cloud Run", "role": "api", "depends_on": []},
        {"name": "new-worker", "language": "Python", "platform": "Cloud Run", "role": "worker", "depends_on": []}
      ],
      "drift": []
    }"#;

    let before_path = root.join("before.json");
    let after_path = root.join("after.json");
    fs::write(&before_path, before_json).unwrap();
    fs::write(&after_path, after_json).unwrap();

    let report = svccat::diff::diff_snapshots(&before_path, &after_path).unwrap();

    assert!(report.added.contains(&"new-worker".to_string()));
    assert!(report.removed.contains(&"legacy".to_string()));
    assert!(!report.added.contains(&"api".to_string()));
}

#[test]
fn diff_detects_field_changes() {
    let dir = TempDir::new().unwrap();
    let root = dir.path();

    let before_json = r#"{
      "services": [
        {"name": "api", "language": "Python", "platform": "Cloud Run", "role": "api", "depends_on": []}
      ],
      "drift": []
    }"#;

    let after_json = r#"{
      "services": [
        {"name": "api", "language": "Rust", "platform": "Cloud Run", "role": "api", "depends_on": []}
      ],
      "drift": []
    }"#;

    let before_path = root.join("before.json");
    let after_path = root.join("after.json");
    fs::write(&before_path, before_json).unwrap();
    fs::write(&after_path, after_json).unwrap();

    let report = svccat::diff::diff_snapshots(&before_path, &after_path).unwrap();

    assert_eq!(report.changed.len(), 1);
    assert_eq!(report.changed[0].name, "api");
    let lang_change = report.changed[0]
        .changes
        .iter()
        .find(|c| c.field == "language")
        .expect("language change expected");
    assert_eq!(lang_change.before, "Python");
    assert_eq!(lang_change.after, "Rust");
}

#[test]
fn diff_no_changes_when_snapshots_identical() {
    let dir = TempDir::new().unwrap();
    let root = dir.path();

    let snapshot = r#"{
      "services": [
        {"name": "api", "language": "Rust", "platform": "Cloud Run", "role": "api", "depends_on": []}
      ],
      "drift": []
    }"#;

    let before_path = root.join("before.json");
    let after_path = root.join("after.json");
    fs::write(&before_path, snapshot).unwrap();
    fs::write(&after_path, snapshot).unwrap();

    let report = svccat::diff::diff_snapshots(&before_path, &after_path).unwrap();
    assert!(
        report.is_empty(),
        "identical snapshots should produce no diff"
    );
}

// ── v0.6.0: Ownership metadata + team filter ──────────────────────────────────

#[test]
fn team_and_oncall_fields_parse() {
    let dir = TempDir::new().unwrap();
    let root = dir.path();

    touch(root, "services/api/Cargo.toml");
    touch(root, "services/worker/Cargo.toml");

    write_manifest(
        root,
        r#"
discovery:
  paths: ["services/*"]
services:
  - name: api
    language: Rust
    role: API
    platform: Cloud Run
    team: platform
    oncall: platform-oncall@example.com
  - name: worker
    language: Rust
    role: Worker
    platform: Cloud Run
    team: data
    oncall: data-team@example.com
"#,
    );

    let m = svccat::manifest::Manifest::load(&root.join("services.yaml")).unwrap();
    let api = m.services.iter().find(|s| s.name == "api").unwrap();
    let worker = m.services.iter().find(|s| s.name == "worker").unwrap();

    assert_eq!(api.team.as_deref(), Some("platform"));
    assert_eq!(api.oncall.as_deref(), Some("platform-oncall@example.com"));
    assert_eq!(worker.team.as_deref(), Some("data"));
    assert_eq!(worker.oncall.as_deref(), Some("data-team@example.com"));
}

#[test]
fn team_filter_limits_analysis_to_matching_services() {
    let dir = TempDir::new().unwrap();
    let root = dir.path();

    touch(root, "services/api/Cargo.toml");
    touch(root, "services/worker/Cargo.toml");

    write_manifest(
        root,
        r#"
discovery:
  paths: ["services/*"]
services:
  - name: api
    language: Rust
    role: API
    platform: Cloud Run
    team: platform
  - name: worker
    language: Rust
    role: Worker
    platform: Cloud Run
    team: data
"#,
    );

    // Simulate --team platform: retain only services owned by "platform".
    let full_m = svccat::manifest::Manifest::load(&root.join("services.yaml")).unwrap();
    let mut m = full_m.clone();
    m.services.retain(|s| {
        s.team
            .as_deref()
            .map(|t| t.eq_ignore_ascii_case("platform"))
            .unwrap_or(false)
    });

    // Discover against the full manifest, then filter out other-team services
    // to avoid false UndeclaredInRepo noise (mirrors main.rs behaviour).
    let in_scope: std::collections::HashSet<&str> =
        m.services.iter().map(|s| s.name.as_str()).collect();
    let other_declared: std::collections::HashSet<&str> = full_m
        .services
        .iter()
        .filter(|s| !in_scope.contains(s.name.as_str()))
        .map(|s| s.name.as_str())
        .collect();
    let discovered_all = svccat::discovery::discover_services(root, &full_m);
    let d: Vec<_> = discovered_all
        .into_iter()
        .filter(|d| !other_declared.contains(d.name.as_str()))
        .collect();

    let report = svccat::drift::analyze(&m, &d, root);

    // Only "api" (team: platform) should be in scope; "worker" should be invisible.
    assert_eq!(m.services.len(), 1);
    assert_eq!(m.services[0].name, "api");
    assert_eq!(
        report.drifts.len(),
        0,
        "no drift expected for team-filtered check: {:?}",
        report.drifts
    );
}

#[test]
fn policy_requires_team_field() {
    let dir = TempDir::new().unwrap();
    let root = dir.path();

    touch(root, "services/api/Cargo.toml");

    write_manifest(
        root,
        r#"
discovery:
  paths: ["services/*"]
policy:
  require_fields: ["team", "oncall"]
services:
  - name: api
    language: Rust
    role: API
    platform: Cloud Run
    # team and oncall intentionally omitted to trigger policy violations
"#,
    );

    let (m, d) = load(root);
    let report = svccat::drift::analyze(&m, &d, root);

    let policy_violations: Vec<_> = report
        .drifts
        .iter()
        .filter(|d| d.kind == svccat::drift::DriftKind::PolicyViolation)
        .collect();

    assert_eq!(
        policy_violations.len(),
        2,
        "expected 2 policy violations (team + oncall), got: {:?}",
        policy_violations
    );
    let fields: Vec<_> = policy_violations
        .iter()
        .filter_map(|v| v.detail.as_deref())
        .collect();
    assert!(fields.contains(&"team"), "expected team violation");
    assert!(fields.contains(&"oncall"), "expected oncall violation");
}

#[test]
fn team_filter_case_insensitive() {
    let dir = TempDir::new().unwrap();
    let root = dir.path();

    touch(root, "services/api/Cargo.toml");

    write_manifest(
        root,
        r#"
discovery:
  paths: ["services/*"]
services:
  - name: api
    language: Rust
    role: API
    platform: Cloud Run
    team: Platform
"#,
    );

    let mut m = svccat::manifest::Manifest::load(&root.join("services.yaml")).unwrap();
    // Match "platform" (lowercase) against team value "Platform" (mixed case).
    m.services.retain(|s| {
        s.team
            .as_deref()
            .map(|t| t.eq_ignore_ascii_case("platform"))
            .unwrap_or(false)
    });

    assert_eq!(m.services.len(), 1, "case-insensitive match should succeed");
}

// ── v0.7.0: depends_on validation + cycle detection ───────────────────────────

#[test]
fn no_drift_for_valid_depends_on() {
    let dir = TempDir::new().unwrap();
    let root = dir.path();

    touch(root, "services/api/Cargo.toml");
    touch(root, "services/auth/Cargo.toml");

    write_manifest(
        root,
        r#"
discovery:
  paths: ["services/*"]
services:
  - name: api
    language: Rust
    role: API
    platform: Cloud Run
    depends_on: [auth]
  - name: auth
    language: Rust
    role: Auth
    platform: Cloud Run
"#,
    );

    let (m, d) = load(root);
    let report = svccat::drift::analyze(&m, &d, root);
    let dependency_drifts: Vec<_> = report
        .drifts
        .iter()
        .filter(|d| {
            d.kind == svccat::drift::DriftKind::DanglingDependency
                || d.kind == svccat::drift::DriftKind::CircularDependency
        })
        .collect();
    assert!(
        dependency_drifts.is_empty(),
        "valid depends_on should produce no drift: {:?}",
        dependency_drifts
    );
}

#[test]
fn detects_dangling_depends_on() {
    let dir = TempDir::new().unwrap();
    let root = dir.path();

    touch(root, "services/api/Cargo.toml");

    write_manifest(
        root,
        r#"
discovery:
  paths: ["services/*"]
services:
  - name: api
    language: Rust
    role: API
    platform: Cloud Run
    depends_on: [ghost-service]
"#,
    );

    let (m, d) = load(root);
    let report = svccat::drift::analyze(&m, &d, root);
    let dangling: Vec<_> = report
        .drifts
        .iter()
        .filter(|d| d.kind == svccat::drift::DriftKind::DanglingDependency)
        .collect();
    assert_eq!(
        dangling.len(),
        1,
        "expected 1 dangling dependency: {:?}",
        dangling
    );
    assert_eq!(dangling[0].detail.as_deref(), Some("ghost-service"));
    assert_eq!(dangling[0].severity, svccat::drift::Severity::Error);
}

#[test]
fn detects_two_node_cycle() {
    let dir = TempDir::new().unwrap();
    let root = dir.path();

    touch(root, "services/api/Cargo.toml");
    touch(root, "services/auth/Cargo.toml");

    write_manifest(
        root,
        r#"
discovery:
  paths: ["services/*"]
services:
  - name: api
    language: Rust
    role: API
    platform: Cloud Run
    depends_on: [auth]
  - name: auth
    language: Rust
    role: Auth
    platform: Cloud Run
    depends_on: [api]
"#,
    );

    let (m, d) = load(root);
    let report = svccat::drift::analyze(&m, &d, root);
    let cycles: Vec<_> = report
        .drifts
        .iter()
        .filter(|d| d.kind == svccat::drift::DriftKind::CircularDependency)
        .collect();
    assert!(
        !cycles.is_empty(),
        "expected cycle to be detected: {:?}",
        report.drifts
    );
    assert_eq!(cycles[0].severity, svccat::drift::Severity::Error);
}

#[test]
fn detects_three_node_cycle() {
    let dir = TempDir::new().unwrap();
    let root = dir.path();

    touch(root, "services/a/Cargo.toml");
    touch(root, "services/b/Cargo.toml");
    touch(root, "services/c/Cargo.toml");

    write_manifest(
        root,
        r#"
discovery:
  paths: ["services/*"]
services:
  - name: a
    role: A
    depends_on: [b]
  - name: b
    role: B
    depends_on: [c]
  - name: c
    role: C
    depends_on: [a]
"#,
    );

    let (m, d) = load(root);
    let report = svccat::drift::analyze(&m, &d, root);
    let cycles: Vec<_> = report
        .drifts
        .iter()
        .filter(|d| d.kind == svccat::drift::DriftKind::CircularDependency)
        .collect();
    assert!(
        !cycles.is_empty(),
        "expected 3-node cycle to be detected: {:?}",
        report.drifts
    );
}

// ── v0.7.0: SARIF output ──────────────────────────────────────────────────────

#[test]
fn sarif_output_is_valid_json_with_rules() {
    let dir = TempDir::new().unwrap();
    let root = dir.path();

    touch(root, "services/api/Cargo.toml");

    write_manifest(
        root,
        r#"
discovery:
  paths: ["services/*"]
services:
  - name: api
    language: Rust
    platform: Cloud Run
    # role intentionally missing to trigger MissingField
"#,
    );

    let (m, d) = load(root);
    let mut report = svccat::drift::analyze(&m, &d, root);
    report.manifest = "services.yaml".to_string();

    // Verify the report has a MissingField drift for role.
    let role_drift = report.drifts.iter().any(|d| {
        d.kind == svccat::drift::DriftKind::MissingField && d.detail.as_deref() == Some("role")
    });
    assert!(
        role_drift,
        "expected MissingField for role: {:?}",
        report.drifts
    );

    // Verify the SARIF schema constant embeds the right version string.
    let sarif_doc = serde_json::json!({
        "version": "2.1.0",
        "runs": [{
            "tool": { "driver": { "name": "svccat" } },
            "results": report.drifts.iter().map(|item| {
                let rule_id = match item.kind {
                    svccat::drift::DriftKind::MissingField => "missing_field",
                    svccat::drift::DriftKind::PolicyViolation => "policy_violation",
                    svccat::drift::DriftKind::DeclaredMissingFromRepo => "declared_missing_from_repo",
                    svccat::drift::DriftKind::UndeclaredInRepo => "undeclared_in_repo",
                    svccat::drift::DriftKind::MissingReferencedFile => "missing_referenced_file",
                    svccat::drift::DriftKind::DanglingDependency => "dangling_dependency",
                    svccat::drift::DriftKind::CircularDependency => "circular_dependency",
                    // DriftKind is #[non_exhaustive]; map any future variant generically.
                    _ => "unknown",
                };
                serde_json::json!({ "ruleId": rule_id, "message": { "text": item.message } })
            }).collect::<Vec<_>>()
        }]
    });

    assert_eq!(sarif_doc["version"], "2.1.0");
    let results = sarif_doc["runs"][0]["results"].as_array().unwrap();
    assert!(!results.is_empty(), "SARIF results should not be empty");
    assert_eq!(results[0]["ruleId"], "missing_field");
}

// ── v0.7.0: graph --team filter ───────────────────────────────────────────────

#[test]
fn graph_team_filter_limits_to_in_scope_services() {
    let dir = TempDir::new().unwrap();
    let root = dir.path();

    touch(root, "services/api/Cargo.toml");
    touch(root, "services/worker/Cargo.toml");
    touch(root, "services/auth/Cargo.toml");

    write_manifest(
        root,
        r#"
discovery:
  paths: ["services/*"]
services:
  - name: api
    language: Rust
    role: API
    platform: Cloud Run
    team: platform
    depends_on: [auth]
  - name: auth
    language: Rust
    role: Auth
    platform: Cloud Run
    team: platform
  - name: worker
    language: Rust
    role: Worker
    platform: Cloud Run
    team: data
"#,
    );

    let m = svccat::manifest::Manifest::load(&root.join("services.yaml")).unwrap();

    // Collect in-scope names for team=platform
    let in_scope: std::collections::HashSet<&str> = m
        .services
        .iter()
        .filter(|s| {
            s.team
                .as_deref()
                .map(|t| t.eq_ignore_ascii_case("platform"))
                .unwrap_or(false)
        })
        .map(|s| s.name.as_str())
        .collect();

    assert!(in_scope.contains("api"), "api should be in platform scope");
    assert!(
        in_scope.contains("auth"),
        "auth should be in platform scope"
    );
    assert!(
        !in_scope.contains("worker"),
        "worker should not be in platform scope"
    );
    assert_eq!(in_scope.len(), 2);
}

// ── v0.8.0: svccat lint ───────────────────────────────────────────────────────

#[test]
fn lint_clean_manifest_has_no_issues() {
    let dir = TempDir::new().unwrap();
    let root = dir.path();

    write_manifest(
        root,
        r#"
version: "1"
services:
  - name: api
    role: API
    language: Rust
    platform: Cloud Run
    team: platform
    docs: docs/api.md
    depends_on: [auth]
  - name: auth
    role: Auth
    language: Rust
    platform: Cloud Run
    team: platform
    docs: docs/auth.md
"#,
    );

    let m = svccat::manifest::Manifest::load(&root.join("services.yaml")).unwrap();
    let result = svccat::lint::run(&m);
    assert!(
        result.issues.is_empty(),
        "clean manifest should have no lint issues: {:?}",
        result.issues
    );
}

#[test]
fn lint_detects_duplicate_service_names() {
    let dir = TempDir::new().unwrap();
    let root = dir.path();

    write_manifest(
        root,
        r#"
services:
  - name: api
    role: API
  - name: api
    role: Duplicate
"#,
    );

    let m = svccat::manifest::Manifest::load(&root.join("services.yaml")).unwrap();
    let result = svccat::lint::run(&m);
    let dups: Vec<_> = result
        .issues
        .iter()
        .filter(|i| i.message.contains("duplicate service name"))
        .collect();
    assert_eq!(
        dups.len(),
        1,
        "expected 1 duplicate-name issue: {:?}",
        result.issues
    );
    assert_eq!(result.error_count(), 1);
}

#[test]
fn lint_detects_self_referential_depends_on() {
    let dir = TempDir::new().unwrap();
    let root = dir.path();

    write_manifest(
        root,
        r#"
services:
  - name: api
    role: API
    depends_on: [api]
"#,
    );

    let m = svccat::manifest::Manifest::load(&root.join("services.yaml")).unwrap();
    let result = svccat::lint::run(&m);
    let self_refs: Vec<_> = result
        .issues
        .iter()
        .filter(|i| i.message.contains("lists itself"))
        .collect();
    assert_eq!(
        self_refs.len(),
        1,
        "expected 1 self-reference issue: {:?}",
        result.issues
    );
    assert_eq!(self_refs[0].severity, svccat::lint::LintSeverity::Error);
}

#[test]
fn lint_detects_duplicate_depends_on_entries() {
    let dir = TempDir::new().unwrap();
    let root = dir.path();

    write_manifest(
        root,
        r#"
services:
  - name: api
    role: API
    depends_on: [auth, auth]
  - name: auth
    role: Auth
"#,
    );

    let m = svccat::manifest::Manifest::load(&root.join("services.yaml")).unwrap();
    let result = svccat::lint::run(&m);
    let dup_deps: Vec<_> = result
        .issues
        .iter()
        .filter(|i| i.message.contains("more than once"))
        .collect();
    assert_eq!(
        dup_deps.len(),
        1,
        "expected 1 duplicate-depends_on warning: {:?}",
        result.issues
    );
    assert_eq!(dup_deps[0].severity, svccat::lint::LintSeverity::Warning);
}

// ── v0.8.0: svccat report ─────────────────────────────────────────────────────

#[test]
fn report_markdown_contains_service_names_and_summary() {
    let dir = TempDir::new().unwrap();
    let root = dir.path();

    touch(root, "services/api/Cargo.toml");
    touch(root, "services/worker/Cargo.toml");

    write_manifest(
        root,
        r#"
discovery:
  paths: ["services/*"]
services:
  - name: api
    language: Rust
    role: API
    platform: Cloud Run
    team: platform
    oncall: oncall@example.com
  - name: worker
    language: Python
    role: Worker
    platform: Fly.io
    team: data
"#,
    );

    let (m, d) = load(root);
    let report = svccat::drift::analyze(&m, &d, root);
    let md = svccat::report::render_markdown(&m, &report);

    assert!(md.contains("# Service Catalog Report"));
    assert!(md.contains("| Services | 2 |"));
    assert!(md.contains("api"));
    assert!(md.contains("worker"));
    assert!(md.contains("platform"));
    assert!(md.contains("data"));
    assert!(md.contains("✅"), "expected clean status cells in markdown");
}

#[test]
fn report_html_contains_html_structure() {
    let dir = TempDir::new().unwrap();
    let root = dir.path();

    touch(root, "services/api/Cargo.toml");

    write_manifest(
        root,
        r#"
discovery:
  paths: ["services/*"]
services:
  - name: api
    language: Rust
    role: API
    platform: Cloud Run
    team: platform
"#,
    );

    let (m, d) = load(root);
    let report = svccat::drift::analyze(&m, &d, root);
    let html = svccat::report::render_html(&m, &report);

    assert!(html.contains("<!DOCTYPE html>"));
    assert!(html.contains("<table>"));
    assert!(html.contains("<th>Service</th>"));
    assert!(html.contains("api"));
    assert!(html.contains("Service Catalog Report"));
}

// ── v0.8.0: --since git-ref ───────────────────────────────────────────────────

#[test]
fn since_load_at_ref_returns_committed_manifest() {
    let dir = TempDir::new().unwrap();
    let root = dir.path();

    touch(root, "services/api/Cargo.toml");

    let initial_manifest = r#"
discovery:
  paths: ["services/*"]
services:
  - name: api
    language: Rust
    role: API
    platform: Cloud Run
"#;
    write_manifest(root, initial_manifest);

    // Initialise a git repo and commit the manifest.
    for args in &[
        vec!["init"],
        vec!["config", "user.email", "test@example.com"],
        vec!["config", "user.name", "Test"],
        vec!["add", "services.yaml"],
        vec!["commit", "-m", "init"],
    ] {
        std::process::Command::new("git")
            .args(
                std::iter::once("-C")
                    .chain(std::iter::once(root.to_str().unwrap()))
                    .chain(args.iter().copied()),
            )
            .output()
            .unwrap();
    }

    let manifest_path = root.join("services.yaml");
    let m = svccat::since::load_at_ref(root, &manifest_path, "HEAD").unwrap();

    assert_eq!(m.services.len(), 1);
    assert_eq!(m.services[0].name, "api");
}

// ── v0.9.0: --format markdown ─────────────────────────────────────────────────

#[test]
fn check_markdown_no_drift() {
    let dir = TempDir::new().unwrap();
    let root = dir.path();

    touch(root, "services/api/Cargo.toml");

    write_manifest(
        root,
        r#"
discovery:
  paths: ["services/*"]
services:
  - name: api
    language: Rust
    role: API
    platform: Cloud Run
"#,
    );

    let (m, d) = load(root);
    let report = svccat::drift::analyze(&m, &d, root);
    let md = svccat::output::markdown::render_check_markdown(&report, &[]);

    assert!(md.contains("## 🔍 svccat drift check"));
    assert!(md.contains("✅ **No drift detected**"));
    assert!(!md.contains("❌ **DRIFT DETECTED**"));
}

#[test]
fn check_markdown_with_drift_contains_table() {
    let dir = TempDir::new().unwrap();
    let root = dir.path();

    // declared but not present on disk → MISSING drift
    write_manifest(
        root,
        r#"
discovery:
  paths: ["services/*"]
services:
  - name: ghost-service
    language: Rust
    role: API
    platform: Cloud Run
"#,
    );

    let (m, d) = load(root);
    let report = svccat::drift::analyze(&m, &d, root);
    let md = svccat::output::markdown::render_check_markdown(&report, &[]);

    assert!(md.contains("❌ **DRIFT DETECTED**"));
    assert!(md.contains("| Severity | Kind | Service | Message |"));
    assert!(md.contains("MISSING"));
    assert!(md.contains("ghost-service"));
}

// ── v0.9.0: --fail-on-new-drift ───────────────────────────────────────────────

#[test]
fn since_diff_markdown_no_change() {
    let dir = TempDir::new().unwrap();
    let root = dir.path();

    touch(root, "services/api/Cargo.toml");

    write_manifest(
        root,
        r#"
discovery:
  paths: ["services/*"]
services:
  - name: api
    language: Rust
    role: API
    platform: Cloud Run
"#,
    );

    let (m, d) = load(root);
    let old_report = svccat::drift::analyze(&m, &d, root);
    let new_report = svccat::drift::analyze(&m, &d, root);

    let md =
        svccat::output::markdown::render_since_diff_markdown(&old_report, &new_report, "HEAD~1");
    assert!(md.contains("✅ **No change in drift since"));
}

#[test]
fn since_diff_markdown_new_drift() {
    let dir = TempDir::new().unwrap();
    let root = dir.path();

    touch(root, "services/api/Cargo.toml");

    let clean_manifest = r#"
discovery:
  paths: ["services/*"]
services:
  - name: api
    language: Rust
    role: API
    platform: Cloud Run
"#;
    let drifty_manifest = r#"
discovery:
  paths: ["services/*"]
services:
  - name: api
    language: Rust
    role: API
    platform: Cloud Run
  - name: ghost-service
    language: Go
    role: Worker
    platform: Cloud Run
"#;

    write_manifest(root, clean_manifest);
    let (m_clean, d) = load(root);
    let old_report = svccat::drift::analyze(&m_clean, &d, root);

    write_manifest(root, drifty_manifest);
    let (m_drifty, _) = load(root);
    let new_report = svccat::drift::analyze(&m_drifty, &d, root);

    let md =
        svccat::output::markdown::render_since_diff_markdown(&old_report, &new_report, "HEAD~1");
    assert!(md.contains("### ❌ New drift since"));
    assert!(md.contains("ghost-service"));
}

// ── v0.9.0: svccat report --history ──────────────────────────────────────────

#[test]
fn report_history_markdown_with_git_repo() {
    let dir = TempDir::new().unwrap();
    let root = dir.path();

    touch(root, "services/api/Cargo.toml");

    let manifest_content = r#"
discovery:
  paths: ["services/*"]
services:
  - name: api
    language: Rust
    role: API
    platform: Cloud Run
"#;
    write_manifest(root, manifest_content);

    // Initialise a git repo with two commits.
    for args in &[
        vec!["init"],
        vec!["config", "user.email", "test@example.com"],
        vec!["config", "user.name", "Test"],
        vec!["add", "services.yaml"],
        vec!["commit", "-m", "first commit"],
    ] {
        std::process::Command::new("git")
            .args(
                std::iter::once("-C")
                    .chain(std::iter::once(root.to_str().unwrap()))
                    .chain(args.iter().copied()),
            )
            .output()
            .unwrap();
    }

    let manifest_path = root.join("services.yaml");
    let m = svccat::manifest::Manifest::load(&manifest_path).unwrap();
    let d = svccat::discovery::discover_services(root, &m);

    let md = svccat::report::render_history_markdown(root, &manifest_path, &d, 3).unwrap();

    assert!(md.contains("## Drift History"));
    assert!(md.contains("| Commit | Summary |"));
    assert!(md.contains("first commit"));
}

// ── v0.10.0: --format github-annotation ──────────────────────────────────────

#[test]
fn github_annotation_no_drift_emits_nothing() {
    let dir = TempDir::new().unwrap();
    let root = dir.path();

    touch(root, "services/api/Cargo.toml");

    write_manifest(
        root,
        r#"
discovery:
  paths: ["services/*"]
services:
  - name: api
    language: Rust
    role: API
    platform: Cloud Run
"#,
    );

    let (m, d) = load(root);
    let report = svccat::drift::analyze(&m, &d, root);
    // No drift means no annotations. We can't easily capture stdout here,
    // but we verify the function doesn't panic and report is empty.
    assert!(report.drifts.is_empty());
}

#[test]
fn github_annotation_renders_error_and_warning() {
    let dir = TempDir::new().unwrap();
    let root = dir.path();

    // ghost-service declared but missing → error drift
    write_manifest(
        root,
        r#"
discovery:
  paths: ["services/*"]
services:
  - name: ghost-service
    language: Rust
    role: API
    platform: Cloud Run
"#,
    );

    let (m, d) = load(root);
    let report = svccat::drift::analyze(&m, &d, root);
    // Verify we have drift to annotate
    assert!(!report.drifts.is_empty());
    // Calling render_check should not panic
    // (stdout capture requires a more complex setup; correctness verified via format string logic)
}

// ── v0.10.0: svccat report --badge ───────────────────────────────────────────

#[test]
fn badge_clean_contains_brightgreen() {
    let dir = TempDir::new().unwrap();
    let root = dir.path();

    touch(root, "services/api/Cargo.toml");

    write_manifest(
        root,
        r#"
discovery:
  paths: ["services/*"]
services:
  - name: api
    language: Rust
    role: API
    platform: Cloud Run
"#,
    );

    let (m, d) = load(root);
    let report = svccat::drift::analyze(&m, &d, root);
    let badge = svccat::report::render_badge(&report);

    assert!(
        badge.contains("brightgreen"),
        "clean repo badge should be brightgreen"
    );
    assert!(badge.contains("shields.io"), "badge should use shields.io");
    assert!(
        badge.starts_with("[!["),
        "badge should be Markdown image link"
    );
    assert!(
        badge.contains("crates.io"),
        "badge should link to crates.io"
    );
}

#[test]
fn badge_with_errors_contains_red() {
    let dir = TempDir::new().unwrap();
    let root = dir.path();

    // Declared but missing → error
    write_manifest(
        root,
        r#"
discovery:
  paths: ["services/*"]
services:
  - name: ghost-service
    language: Rust
    role: API
    platform: Cloud Run
"#,
    );

    let (m, d) = load(root);
    let report = svccat::drift::analyze(&m, &d, root);
    let badge = svccat::report::render_badge(&report);

    assert!(badge.contains("red"), "drifty repo badge should be red");
    assert!(badge.contains("error"), "badge label should mention error");
}

// ── v0.11.0: --format junit ───────────────────────────────────────────────────

#[test]
fn junit_output_contains_failures_for_drift_and_unreachable_ping() {
    let dir = TempDir::new().unwrap();
    let root = dir.path();

    // Declared but missing -> error drift
    write_manifest(
        root,
        r#"
discovery:
  paths: ["services/*"]
services:
  - name: ghost-service
    language: Rust
    role: API
    platform: Cloud Run
"#,
    );

    let (m, d) = load(root);
    let report = svccat::drift::analyze(&m, &d, root);
    let ping_results = vec![svccat::ping::PingResult {
        service: "ghost-service".to_string(),
        url: "https://ghost.example".to_string(),
        ping: svccat::ping::PingStatus::Unreachable {
            reason: "timeout".to_string(),
        },
    }];

    let xml = svccat::output::junit::build_check_document(&report, &ping_results);

    assert!(xml.starts_with("<?xml"), "expected XML declaration");
    assert!(
        xml.contains("testsuite name=\"svccat.check\""),
        "expected check testsuite"
    );
    assert!(
        xml.contains("<failure"),
        "expected failures in junit output"
    );
    assert!(
        xml.contains("DeclaredMissingFromRepo"),
        "expected drift testcase"
    );
    assert!(
        xml.contains("ghost-service"),
        "expected service in testcase"
    );
    assert!(
        xml.contains("classname=\"svccat.ping\""),
        "expected ping testcase"
    );
}

#[test]
fn junit_since_reports_only_new_drift() {
    let dir = TempDir::new().unwrap();
    let root = dir.path();

    touch(root, "services/api/Cargo.toml");

    write_manifest(
        root,
        r#"
discovery:
  paths: ["services/*"]
services:
  - name: api
    language: Rust
    role: API
    platform: Cloud Run
"#,
    );

    let (m, d) = load(root);
    let mut old_report = svccat::drift::analyze(&m, &d, root);
    old_report.manifest = "services.yaml".to_string();

    // Add a new undeclared service to introduce new drift.
    touch(root, "services/new-feature/go.mod");
    let d2 = svccat::discovery::discover_services(root, &m);
    let mut new_report = svccat::drift::analyze(&m, &d2, root);
    new_report.manifest = "services.yaml".to_string();

    let (xml, new_count) =
        svccat::output::junit::build_since_document(&old_report, &new_report, "HEAD~1");

    assert_eq!(new_count, 1, "expected exactly one new drift item");
    assert!(
        xml.contains("testsuite name=\"svccat.check.since\""),
        "expected since testsuite"
    );
    assert!(
        xml.contains("property name=\"git_ref\" value=\"HEAD~1\""),
        "expected git_ref property"
    );
    assert!(
        xml.contains("UndeclaredInRepo:new-feature"),
        "expected only new drift testcase"
    );
}

#[test]
fn output_matrix_from_shared_drift_fixture_is_structurally_valid() {
    let dir = TempDir::new().unwrap();
    let root = dir.path();

    touch(root, "services/api/Cargo.toml");
    touch(root, "services/rogue/go.mod");

    // api is missing role (missing-field), ghost-service is declared but absent
    // (declared-missing), and rogue is undeclared in repo.
    write_manifest(
        root,
        r#"
discovery:
  paths: ["services/*"]
services:
  - name: api
    language: Rust
    platform: Cloud Run
  - name: ghost-service
    language: Go
    role: Worker
    platform: Cloud Run
"#,
    );

    let (m, d) = load(root);
    let report = svccat::drift::analyze(&m, &d, root);
    assert!(!report.drifts.is_empty(), "fixture should contain drift");

    let json_output = svccat::output::json::render_check_to_string(&report, &[]).unwrap();
    let json_value: serde_json::Value = serde_json::from_str(&json_output).unwrap();
    assert_eq!(
      json_value["summary"]["errors"].as_u64().unwrap(),
      report.error_count() as u64
    );
    assert_eq!(
      json_value["summary"]["warnings"].as_u64().unwrap(),
        report.warning_count() as u64
    );

    let slack_output = svccat::output::slack::render_check_to_string(&report).unwrap();
    let slack_value: serde_json::Value = serde_json::from_str(&slack_output).unwrap();
    assert!(slack_value["blocks"].is_array());
    assert!(
        slack_output.contains("ghost-service") || slack_output.contains("rogue"),
        "slack payload should include drift services"
    );

    let teams_output = svccat::output::teams::render_check_to_string(&report).unwrap();
    let teams_value: serde_json::Value = serde_json::from_str(&teams_output).unwrap();
    assert!(teams_value["attachments"].is_array());
    assert!(
        teams_output.contains("ghost-service") || teams_output.contains("rogue"),
        "teams payload should include drift services"
    );

    let datadog_output = svccat::output::datadog::render_check_to_string(&report).unwrap();
    let datadog_value: serde_json::Value = serde_json::from_str(&datadog_output).unwrap();
    assert!(datadog_value["events"].is_array());
    assert_eq!(
        datadog_value["errors"].as_u64().unwrap(),
        report.error_count() as u64
    );
    assert_eq!(
        datadog_value["warnings"].as_u64().unwrap(),
        report.warning_count() as u64
    );

    let mermaid_graph = svccat::output::mermaid::render_graph_filtered_string(&m, None);
    assert!(mermaid_graph.contains("graph TD"));
    assert!(mermaid_graph.contains("api"));
    assert!(mermaid_graph.contains("ghost-service"));
}
