//! Integration tests for `mcptest sbom` (WOR-946).
//!
//! Drives the real `mcptest` binary through assert_cmd to confirm the
//! CycloneDX BOM the build script embeds is reachable from the CLI, that
//! every output format renders, and that `--verify` agrees with the
//! build-time hash on a clean binary.

use std::process::Command;

use assert_cmd::prelude::*;
use predicates::prelude::*;
use serde_json::Value;

fn mcptest() -> Command {
    Command::cargo_bin("mcptest").expect("mcptest binary built")
}

#[test]
fn sbom_default_returns_valid_cyclonedx_1_5_json() {
    let output = mcptest().arg("sbom").output().expect("sbom command runs");
    assert!(output.status.success(), "exit 0");
    let body = String::from_utf8(output.stdout).expect("utf8 stdout");
    let bom: Value = serde_json::from_str(&body).expect("BOM parses as JSON");
    assert_eq!(bom["bomFormat"], "CycloneDX");
    assert_eq!(bom["specVersion"], "1.5");
    let components = bom["components"].as_array().expect("components array");
    assert!(!components.is_empty(), "BOM has at least one component");
    // Sanity: a real Rust binary has dozens of crates. If we accidentally
    // strip the dep walk this drops to zero.
    assert!(
        components.len() > 30,
        "expected many components, found {}",
        components.len()
    );
}

#[test]
fn sbom_licenses_format_lists_every_component() {
    let output = mcptest()
        .args(["sbom", "--format", "licenses"])
        .output()
        .expect("sbom --format licenses runs");
    assert!(output.status.success());
    let body = String::from_utf8(output.stdout).expect("utf8 stdout");
    let lines: Vec<&str> = body.lines().collect();
    assert!(!lines.is_empty(), "at least one license line emitted");
    for line in &lines {
        // Each line is `<name> <version> <spdx>`. The SPDX column is
        // never empty: an unlicensed crate would render UNKNOWN.
        let parts: Vec<&str> = line.split_whitespace().collect();
        assert!(
            parts.len() >= 3,
            "license line `{line}` has fewer than 3 columns"
        );
    }
}

#[test]
fn sbom_names_format_one_line_per_component() {
    let output = mcptest()
        .args(["sbom", "--format", "names"])
        .output()
        .expect("sbom --format names runs");
    assert!(output.status.success());
    let body = String::from_utf8(output.stdout).expect("utf8 stdout");
    for line in body.lines() {
        let parts: Vec<&str> = line.split_whitespace().collect();
        assert_eq!(
            parts.len(),
            2,
            "names line `{line}` is not `<name> <version>`"
        );
    }
}

#[test]
fn sbom_verify_succeeds_on_a_clean_binary() {
    mcptest()
        .args(["sbom", "--verify"])
        .assert()
        .success()
        .stdout(predicate::str::contains("embedded BOM verified"))
        .stdout(predicate::str::contains("sha256"));
}

#[test]
fn sbom_writes_to_out_file_when_given() {
    let tmp = tempfile::NamedTempFile::new().expect("tempfile");
    mcptest()
        .args(["sbom", "--out"])
        .arg(tmp.path())
        .assert()
        .success();
    let body = std::fs::read_to_string(tmp.path()).expect("readback");
    let bom: Value = serde_json::from_str(&body).expect("parses");
    assert_eq!(bom["bomFormat"], "CycloneDX");
}
