projd 0.1.3

Scan software projects and generate structured reports.
use assert_cmd::Command;
use predicates::prelude::*;
use std::fs;
use tempfile::TempDir;

#[test]
fn prints_help() {
    let mut cmd = Command::cargo_bin("projd").expect("binary exists");

    cmd.arg("--help")
        .assert()
        .success()
        .stdout(predicate::str::contains("Scan software projects"));
}

#[test]
fn scan_prints_markdown_to_stdout_by_default() {
    let fixture = ProjectFixture::new();

    let mut cmd = Command::cargo_bin("projd").expect("binary exists");

    cmd.arg("scan")
        .arg(fixture.path())
        .assert()
        .success()
        .stdout(predicate::str::contains("# Project Report"));
}

#[test]
fn scan_prints_json_to_stdout() {
    let fixture = ProjectFixture::new();

    let mut cmd = Command::cargo_bin("projd").expect("binary exists");

    cmd.arg("scan")
        .arg(fixture.path())
        .arg("--format")
        .arg("json")
        .assert()
        .success()
        .stdout(predicate::str::contains("\"identity\""));
}

#[test]
fn scan_prints_terminal_dashboard_when_requested() {
    let fixture = ProjectFixture::new();

    let mut cmd = Command::cargo_bin("projd").expect("binary exists");

    cmd.arg("scan")
        .arg(fixture.path())
        .arg("--format")
        .arg("terminal")
        .assert()
        .success()
        .stdout(
            predicate::str::contains("Projd Scan Report")
                .and(predicate::str::contains("Health"))
                .and(predicate::str::contains("Languages"))
                .and(predicate::str::contains("Rust"))
                .and(predicate::str::contains("files scanned"))
                .and(predicate::str::contains(""))
                .and(predicate::str::contains("Risks")),
        );
}

#[test]
fn scan_terminal_dashboard_supports_ascii_output() {
    let fixture = ProjectFixture::new();

    let mut cmd = Command::cargo_bin("projd").expect("binary exists");

    cmd.arg("scan")
        .arg(fixture.path())
        .arg("--format")
        .arg("terminal")
        .arg("--no-unicode")
        .assert()
        .success()
        .stdout(predicate::str::contains("Projd Scan Report"))
        .stdout(predicate::str::contains("#"))
        .stdout(predicate::str::contains("OK"))
        .stdout(predicate::str::contains("").not());
}

#[test]
fn scan_terminal_dashboard_aggregates_repeated_sections() {
    let fixture = ProjectFixture::workspace();

    let mut cmd = Command::cargo_bin("projd").expect("binary exists");

    cmd.arg("scan")
        .arg(fixture.path())
        .arg("--format")
        .arg("terminal")
        .arg("--no-unicode")
        .assert()
        .success()
        .stdout(predicate::str::contains("Cargo"))
        .stdout(predicate::str::contains("Rust"))
        .stdout(predicate::str::contains("3 manifest(s)"))
        .stdout(predicate::str::contains("cargo test"))
        .stdout(predicate::str::contains("3 source(s)"))
        .stdout(predicate::str::contains("crate_a/Cargo.toml").not())
        .stdout(predicate::str::contains("crate_b/Cargo.toml").not());
}

#[test]
fn scan_terminal_dashboard_respects_width_option() {
    let fixture = ProjectFixture::new();

    let mut cmd = Command::cargo_bin("projd").expect("binary exists");

    cmd.arg("scan")
        .arg(fixture.path())
        .arg("--format")
        .arg("terminal")
        .arg("--no-unicode")
        .arg("--width")
        .arg("44")
        .assert()
        .success()
        .stdout(predicate::str::contains("########## 100%"));
}

#[test]
fn scan_terminal_dashboard_supports_color_control() {
    let fixture = ProjectFixture::new();

    let mut colored = Command::cargo_bin("projd").expect("binary exists");
    colored
        .arg("scan")
        .arg(fixture.path())
        .arg("--format")
        .arg("terminal")
        .arg("--color")
        .arg("always")
        .assert()
        .success()
        .stdout(predicate::str::contains("\u{1b}["));

    let mut plain = Command::cargo_bin("projd").expect("binary exists");
    plain
        .arg("scan")
        .arg(fixture.path())
        .arg("--format")
        .arg("terminal")
        .arg("--color")
        .arg("never")
        .assert()
        .success()
        .stdout(predicate::str::contains("\u{1b}[").not());
}

#[test]
fn scan_terminal_dashboard_uses_semantic_colors() {
    let fixture = ProjectFixture::new();

    let mut cmd = Command::cargo_bin("projd").expect("binary exists");

    cmd.arg("scan")
        .arg(fixture.path())
        .arg("--format")
        .arg("terminal")
        .arg("--color")
        .arg("always")
        .assert()
        .success()
        .stdout(predicate::str::contains("\u{1b}[1;36m"))
        .stdout(predicate::str::contains("\u{1b}[32mOK\u{1b}[0m"))
        .stdout(predicate::str::contains("\u{1b}[34m"))
        .stdout(predicate::str::contains("\u{1b}[33mMissing\u{1b}[0m"));
}

#[test]
fn scan_writes_markdown_output_file() {
    let fixture = ProjectFixture::new();
    let output = fixture.path().join("report.md");

    let mut cmd = Command::cargo_bin("projd").expect("binary exists");

    cmd.arg("scan")
        .arg(fixture.path())
        .arg("--output")
        .arg(&output)
        .assert()
        .success()
        .stdout(predicate::str::is_empty());

    let content = fs::read_to_string(output).expect("read report");
    assert!(content.contains("# Project Report"));
}

#[test]
fn scan_infers_json_output_from_extension() {
    let fixture = ProjectFixture::new();
    let output = fixture.path().join("scan.json");

    let mut cmd = Command::cargo_bin("projd").expect("binary exists");

    cmd.arg("scan")
        .arg(fixture.path())
        .arg("--output")
        .arg(&output)
        .assert()
        .success()
        .stdout(predicate::str::is_empty());

    let content = fs::read_to_string(output).expect("read json");
    assert!(content.contains("\"identity\""));
}

#[test]
fn scan_refuses_to_overwrite_existing_output_file() {
    let fixture = ProjectFixture::new();
    let output = fixture.path().join("report.md");
    fs::write(&output, "existing").expect("write existing output");

    let mut cmd = Command::cargo_bin("projd").expect("binary exists");

    cmd.arg("scan")
        .arg(fixture.path())
        .arg("--output")
        .arg(&output)
        .assert()
        .failure()
        .stderr(predicate::str::contains("refusing to overwrite"));

    assert_eq!(fs::read_to_string(output).expect("read output"), "existing");
}

#[test]
fn scan_overwrites_existing_output_file_when_requested() {
    let fixture = ProjectFixture::new();
    let output = fixture.path().join("report.md");
    fs::write(&output, "existing").expect("write existing output");

    let mut cmd = Command::cargo_bin("projd").expect("binary exists");

    cmd.arg("scan")
        .arg(fixture.path())
        .arg("--output")
        .arg(&output)
        .arg("--overwrite")
        .assert()
        .success();

    let content = fs::read_to_string(output).expect("read output");
    assert!(content.contains("# Project Report"));
    assert_ne!(content, "existing");
}

#[test]
fn explicit_format_overrides_output_extension() {
    let fixture = ProjectFixture::new();
    let output = fixture.path().join("report.txt");

    let mut cmd = Command::cargo_bin("projd").expect("binary exists");

    cmd.arg("scan")
        .arg(fixture.path())
        .arg("--format")
        .arg("json")
        .arg("--output")
        .arg(&output)
        .assert()
        .success();

    let content = fs::read_to_string(output).expect("read output");
    assert!(content.contains("\"identity\""));
}

struct ProjectFixture {
    temp_dir: TempDir,
}

impl ProjectFixture {
    fn new() -> Self {
        let temp_dir = tempfile::tempdir().expect("create temp dir");
        fs::write(
            temp_dir.path().join("Cargo.toml"),
            "[package]\nname = \"fixture\"\nversion = \"0.1.0\"\n",
        )
        .expect("write manifest");
        fs::create_dir(temp_dir.path().join("src")).expect("create src dir");
        fs::write(temp_dir.path().join("src/lib.rs"), "pub fn fixture() {}\n")
            .expect("write rust source");
        fs::write(temp_dir.path().join("README.md"), "# Fixture\n").expect("write readme");
        fs::write(temp_dir.path().join("LICENSE"), "MIT\n").expect("write license");

        Self { temp_dir }
    }

    fn workspace() -> Self {
        let temp_dir = tempfile::tempdir().expect("create temp dir");
        fs::write(
            temp_dir.path().join("Cargo.toml"),
            "[workspace]\nmembers = [\"crate_a\", \"crate_b\"]\n",
        )
        .expect("write workspace manifest");
        fs::write(temp_dir.path().join("README.md"), "# Workspace\n").expect("write readme");
        fs::write(temp_dir.path().join("LICENSE"), "MIT\n").expect("write license");

        for crate_name in ["crate_a", "crate_b"] {
            let crate_dir = temp_dir.path().join(crate_name);
            fs::create_dir(&crate_dir).expect("create crate dir");
            fs::create_dir(crate_dir.join("src")).expect("create src dir");
            fs::write(
                crate_dir.join("Cargo.toml"),
                format!(
                    "[package]\nname = \"{crate_name}\"\nversion = \"0.1.0\"\n[dependencies]\nanyhow = \"1\"\n"
                ),
            )
            .expect("write crate manifest");
            fs::write(crate_dir.join("src/lib.rs"), "pub fn fixture() {}\n")
                .expect("write rust source");
        }

        Self { temp_dir }
    }

    fn path(&self) -> &std::path::Path {
        self.temp_dir.path()
    }
}