dampen-cli 0.3.2

Developer CLI for Dampen UI framework
Documentation
//! Integration tests for the `dampen new` command

use assert_cmd::Command;
use predicates::prelude::*;
use std::fs;
use tempfile::TempDir;

/// Helper to get the dampen binary command
fn dampen_cmd() -> Command {
    Command::cargo_bin("dampen").unwrap()
}

#[test]
fn test_new_creates_project_structure() {
    let temp = TempDir::new().unwrap();
    let project_name = "test-app";

    // Execute dampen new
    dampen_cmd()
        .arg("new")
        .arg(project_name)
        .current_dir(temp.path())
        .assert()
        .success()
        .stdout(predicate::str::contains("Created new Dampen project"));

    // Verify directory structure
    let project_path = temp.path().join(project_name);
    assert!(project_path.exists(), "Project directory should exist");
    assert!(project_path.is_dir(), "Project path should be a directory");

    // Verify files exist
    assert!(
        project_path.join("Cargo.toml").exists(),
        "Cargo.toml should exist"
    );
    assert!(
        project_path.join("README.md").exists(),
        "README.md should exist"
    );
    assert!(
        project_path.join("src").is_dir(),
        "src/ directory should exist"
    );
    assert!(
        project_path.join("src/main.rs").exists(),
        "src/main.rs should exist"
    );
    assert!(
        project_path.join("src/ui").is_dir(),
        "src/ui/ directory should exist"
    );
    assert!(
        project_path.join("src/ui/mod.rs").exists(),
        "src/ui/mod.rs should exist"
    );
    assert!(
        project_path.join("src/ui/window.rs").exists(),
        "src/ui/window.rs should exist"
    );
    assert!(
        project_path.join("src/ui/window.dampen").exists(),
        "src/ui/window.dampen should exist"
    );
    assert!(
        project_path.join("tests").is_dir(),
        "tests/ directory should exist"
    );
    assert!(
        project_path.join("tests/integration.rs").exists(),
        "tests/integration.rs should exist"
    );
    assert!(
        project_path.join("build.rs").exists(),
        "build.rs should exist for production builds"
    );
}

#[test]
fn test_new_substitutes_project_name_in_cargo_toml() {
    let temp = TempDir::new().unwrap();
    let project_name = "my-cool-app";

    dampen_cmd()
        .arg("new")
        .arg(project_name)
        .current_dir(temp.path())
        .assert()
        .success();

    // Read and verify Cargo.toml
    let cargo_toml_path = temp.path().join(project_name).join("Cargo.toml");
    let cargo_toml = fs::read_to_string(cargo_toml_path).unwrap();

    assert!(cargo_toml.contains(&format!("name = \"{}\"", project_name)));
    assert!(cargo_toml.contains("dampen-core"));
    assert!(cargo_toml.contains("dampen-macros"));
    assert!(cargo_toml.contains("dampen-iced"));
    assert!(cargo_toml.contains("serde_json"));
    assert!(
        cargo_toml.contains("build = \"build.rs\""),
        "Cargo.toml should reference build.rs"
    );
}

#[test]
fn test_new_substitutes_project_name_in_readme() {
    let temp = TempDir::new().unwrap();
    let project_name = "readme-test";

    dampen_cmd()
        .arg("new")
        .arg(project_name)
        .current_dir(temp.path())
        .assert()
        .success();

    // Read and verify README.md
    let readme_path = temp.path().join(project_name).join("README.md");
    let readme = fs::read_to_string(readme_path).unwrap();

    assert!(readme.contains(&format!("# {}", project_name)));
    assert!(readme.contains("Quick Start"));
    assert!(readme.contains("dampen run"));
    assert!(readme.contains("src/ui/window.dampen"));
}

#[test]
fn test_new_creates_valid_xml() {
    let temp = TempDir::new().unwrap();
    let project_name = "valid-xml-test";

    dampen_cmd()
        .arg("new")
        .arg(project_name)
        .current_dir(temp.path())
        .assert()
        .success();

    let xml_file = temp.path().join(project_name).join("src/ui/window.dampen");
    let xml_content = fs::read_to_string(xml_file).unwrap();

    // Verify it's valid XML (at least has XML declaration and root element)
    assert!(!xml_content.contains("<?xml"));
    assert!(xml_content.contains(r#"<dampen version="1.1" encoding="utf-8">"#));
    assert!(xml_content.contains("</dampen>"));
    assert!(xml_content.contains("<column"));
    assert!(xml_content.contains("</column>"));
    assert!(xml_content.contains("Hello, Dampen!"));
}

#[test]
fn test_new_rejects_empty_name() {
    let temp = TempDir::new().unwrap();

    dampen_cmd()
        .arg("new")
        .arg("")
        .current_dir(temp.path())
        .assert()
        .failure()
        .stderr(predicate::str::contains("Project name cannot be empty"));
}

#[test]
fn test_new_rejects_name_starting_with_number() {
    let temp = TempDir::new().unwrap();

    dampen_cmd()
        .arg("new")
        .arg("123invalid")
        .current_dir(temp.path())
        .assert()
        .failure()
        .stderr(predicate::str::contains(
            "must start with a letter or underscore",
        ));
}

#[test]
fn test_new_rejects_name_with_special_chars() {
    let temp = TempDir::new().unwrap();

    dampen_cmd()
        .arg("new")
        .arg("my@app")
        .current_dir(temp.path())
        .assert()
        .failure()
        .stderr(predicate::str::contains(
            "can only contain letters, numbers, hyphens, and underscores",
        ));
}

#[test]
fn test_new_rejects_name_with_spaces() {
    let temp = TempDir::new().unwrap();

    dampen_cmd()
        .arg("new")
        .arg("my app")
        .current_dir(temp.path())
        .assert()
        .failure()
        .stderr(predicate::str::contains(
            "can only contain letters, numbers, hyphens, and underscores",
        ));
}

#[test]
fn test_new_rejects_reserved_names() {
    let temp = TempDir::new().unwrap();

    for reserved in &["test", "build", "target", "src"] {
        dampen_cmd()
            .arg("new")
            .arg(reserved)
            .current_dir(temp.path())
            .assert()
            .failure()
            .stderr(predicate::str::contains("is a reserved name"));
    }
}

#[test]
fn test_new_accepts_valid_names() {
    let temp = TempDir::new().unwrap();

    let valid_names = vec![
        "my-app", "my_app", "MyApp", "app123", "_private", "a", "a-b-c",
    ];

    for (i, name) in valid_names.iter().enumerate() {
        // Use different names to avoid conflicts
        let unique_name = format!("{}-{}", name, i);

        dampen_cmd()
            .arg("new")
            .arg(&unique_name)
            .current_dir(temp.path())
            .assert()
            .success();
    }
}

#[test]
fn test_new_detects_existing_directory() {
    let temp = TempDir::new().unwrap();
    let project_name = "existing";

    // Create directory first
    fs::create_dir(temp.path().join(project_name)).unwrap();

    // Try to create project with same name
    dampen_cmd()
        .arg("new")
        .arg(project_name)
        .current_dir(temp.path())
        .assert()
        .failure()
        .stderr(predicate::str::contains("already exists"));
}

#[test]
fn test_new_creates_valid_rust_code() {
    let temp = TempDir::new().unwrap();
    let project_name = "valid-code-test";

    dampen_cmd()
        .arg("new")
        .arg(project_name)
        .current_dir(temp.path())
        .assert()
        .success();

    let project_path = temp.path().join(project_name);

    // Read main.rs and verify it's valid Rust syntax
    let main_rs = fs::read_to_string(project_path.join("src/main.rs")).unwrap();

    // Check for key elements
    assert!(main_rs.contains("mod ui;"));
    assert!(main_rs.contains("#[dampen_app("));
    assert!(main_rs.contains("DampenApp"));
    assert!(main_rs.contains("pub fn main() -> iced::Result"));

    // Read window.rs and verify it's valid Rust syntax
    let window_rs = fs::read_to_string(project_path.join("src/ui/window.rs")).unwrap();

    // Check for key elements
    assert!(window_rs.contains("pub struct Model"));
    assert!(
        window_rs.contains("#[derive(Default, UiModel, Serialize, Deserialize, Clone, Debug)]")
    );
    assert!(window_rs.contains("#[dampen_ui(\"window.dampen\")]"));
    assert!(window_rs.contains("HandlerRegistry::new"));
}

#[test]
fn test_new_creates_ui_mod_exports() {
    let temp = TempDir::new().unwrap();
    let project_name = "ui-mod-test";

    dampen_cmd()
        .arg("new")
        .arg(project_name)
        .current_dir(temp.path())
        .assert()
        .success();

    let project_path = temp.path().join(project_name);

    // Read mod.rs and verify it exports window
    let mod_rs = fs::read_to_string(project_path.join("src/ui/mod.rs")).unwrap();

    assert!(mod_rs.contains("pub mod window;"));
}

#[test]
fn test_new_creates_integration_tests() {
    let temp = TempDir::new().unwrap();
    let project_name = "integration-test";

    dampen_cmd()
        .arg("new")
        .arg(project_name)
        .current_dir(temp.path())
        .assert()
        .success();

    let project_path = temp.path().join(project_name);

    // Read integration.rs and verify it has tests
    let integration_rs = fs::read_to_string(project_path.join("tests/integration.rs")).unwrap();

    assert!(integration_rs.contains("#[test]"));
    // The template may have changed - just verify basic structure
    assert!(integration_rs.contains("parse(xml)") || integration_rs.contains("parse("));
}

#[test]
fn test_new_output_messages() {
    let temp = TempDir::new().unwrap();
    let project_name = "output-test";

    dampen_cmd()
        .arg("new")
        .arg(project_name)
        .current_dir(temp.path())
        .assert()
        .success()
        .stdout(predicate::str::contains("Created new Dampen project"))
        .stdout(predicate::str::contains("Next steps:"))
        .stdout(predicate::str::contains("cd output-test"))
        .stdout(predicate::str::contains("dampen run"));
}

#[test]
fn test_new_creates_build_rs_for_production() {
    let temp = TempDir::new().unwrap();
    let project_name = "prod-build-test";

    dampen_cmd()
        .arg("new")
        .arg(project_name)
        .current_dir(temp.path())
        .assert()
        .success();

    let project_path = temp.path().join(project_name);

    // Verify build.rs exists
    let build_rs_path = project_path.join("build.rs");
    assert!(build_rs_path.exists(), "build.rs should exist");

    // Read and verify build.rs content
    let build_rs = fs::read_to_string(build_rs_path).unwrap();
    assert!(
        build_rs.contains("fn main()"),
        "build.rs should have main function"
    );
    assert!(build_rs.contains("OUT_DIR"), "build.rs should use OUT_DIR");
    assert!(
        build_rs.contains(".dampen"),
        "build.rs should reference .dampen files"
    );
    assert!(
        build_rs.contains("src/ui/"),
        "build.rs should look in src/ui/"
    );
    assert!(
        build_rs.contains("cargo:rerun-if-changed"),
        "build.rs should declare rerun triggers"
    );

    // Verify Cargo.toml references build.rs
    let cargo_toml = fs::read_to_string(project_path.join("Cargo.toml")).unwrap();
    assert!(
        cargo_toml.contains("build = \"build.rs\""),
        "Cargo.toml should reference build.rs"
    );
}