//! Tests for ProjectService

use anyhow::Result;
use mecha10_cli::services::project::*;
use std::fs;
use std::path::Path;
use tempfile::TempDir;

fn create_test_project(dir: &Path, with_mecha10_json: bool) -> Result<()> {
    // Create basic structure
    fs::create_dir_all(dir.join("nodes"))?;
    fs::create_dir_all(dir.join("drivers"))?;
    fs::create_dir_all(dir.join("types"))?;

    if with_mecha10_json {
        fs::write(
            dir.join("mecha10.json"),
            r#"{
                "name": "test-project",
                "version": "0.1.0",
                "robot": {
                    "id": "test-robot"
                }
            }"#,
        )?;
    }

    Ok(())
}

#[test]
fn test_detect_project_in_current_dir() -> Result<()> {
    let temp = TempDir::new()?;
    create_test_project(temp.path(), true)?;

    let project = ProjectService::detect(temp.path())?;
    // Use canonicalized paths for comparison (handles macOS symlinks)
    let expected = temp.path().canonicalize()?;
    let actual = project.root().canonicalize()?;
    assert_eq!(actual, expected);

    Ok(())
}

#[test]
fn test_detect_project_in_parent_dir() -> Result<()> {
    let temp = TempDir::new()?;
    create_test_project(temp.path(), true)?;

    let subdir = temp.path().join("nodes").join("my_node");
    fs::create_dir_all(&subdir)?;

    let project = ProjectService::detect(&subdir)?;
    // Use canonicalized paths for comparison (handles macOS symlinks)
    let expected = temp.path().canonicalize()?;
    let actual = project.root().canonicalize()?;
    assert_eq!(actual, expected);

    Ok(())
}

#[test]
fn test_detect_project_fails_when_not_found() {
    let temp = TempDir::new().unwrap();
    create_test_project(temp.path(), false).unwrap();

    let result = ProjectService::detect(temp.path());
    assert!(result.is_err());
}

#[test]
fn test_project_name_from_mecha10_json() -> Result<()> {
    let temp = TempDir::new()?;
    create_test_project(temp.path(), true)?;

    let project = ProjectService::new(temp.path().to_path_buf());
    assert_eq!(project.name()?, "test-project");

    Ok(())
}

#[test]
fn test_project_version_from_mecha10_json() -> Result<()> {
    let temp = TempDir::new()?;
    create_test_project(temp.path(), true)?;

    let project = ProjectService::new(temp.path().to_path_buf());
    assert_eq!(project.version()?, "0.1.0");

    Ok(())
}

#[test]
fn test_validate_succeeds_for_valid_project() -> Result<()> {
    let temp = TempDir::new()?;
    create_test_project(temp.path(), true)?;

    let project = ProjectService::new(temp.path().to_path_buf());
    project.validate()?;

    Ok(())
}

#[test]
fn test_validate_fails_without_mecha10_json() {
    let temp = TempDir::new().unwrap();
    create_test_project(temp.path(), false).unwrap();

    let project = ProjectService::new(temp.path().to_path_buf());
    let result = project.validate();
    assert!(result.is_err());
}

#[test]
fn test_list_nodes() -> Result<()> {
    let temp = TempDir::new()?;
    create_test_project(temp.path(), true)?;

    fs::create_dir_all(temp.path().join("nodes").join("node_a"))?;
    fs::create_dir_all(temp.path().join("nodes").join("node_b"))?;

    let project = ProjectService::new(temp.path().to_path_buf());
    let nodes = project.list_nodes()?;

    assert_eq!(nodes, vec!["node_a", "node_b"]);

    Ok(())
}

#[test]
fn test_list_drivers() -> Result<()> {
    let temp = TempDir::new()?;
    create_test_project(temp.path(), true)?;

    fs::create_dir_all(temp.path().join("drivers").join("camera"))?;
    fs::create_dir_all(temp.path().join("drivers").join("motor"))?;

    let project = ProjectService::new(temp.path().to_path_buf());
    let drivers = project.list_drivers()?;

    assert_eq!(drivers, vec!["camera", "motor"]);

    Ok(())
}

#[test]
fn test_list_types() -> Result<()> {
    let temp = TempDir::new()?;
    create_test_project(temp.path(), true)?;

    fs::create_dir_all(temp.path().join("types").join("sensor_data"))?;
    fs::create_dir_all(temp.path().join("types").join("control_msg"))?;

    let project = ProjectService::new(temp.path().to_path_buf());
    let types = project.list_types()?;

    assert_eq!(types, vec!["control_msg", "sensor_data"]);

    Ok(())
}

#[test]
fn test_list_nodes_empty_directory() -> Result<()> {
    let temp = TempDir::new()?;
    create_test_project(temp.path(), true)?;

    let project = ProjectService::new(temp.path().to_path_buf());
    let nodes = project.list_nodes()?;

    assert_eq!(nodes, Vec::<String>::new());

    Ok(())
}

#[test]
fn test_list_nodes_skips_files() -> Result<()> {
    let temp = TempDir::new()?;
    create_test_project(temp.path(), true)?;

    fs::create_dir_all(temp.path().join("nodes").join("node_a"))?;
    fs::write(temp.path().join("nodes").join("readme.txt"), "readme")?;

    let project = ProjectService::new(temp.path().to_path_buf());
    let nodes = project.list_nodes()?;

    assert_eq!(nodes, vec!["node_a"]);

    Ok(())
}

#[test]
fn test_list_nodes_sorted() -> Result<()> {
    let temp = TempDir::new()?;
    create_test_project(temp.path(), true)?;

    // Create in non-alphabetical order
    fs::create_dir_all(temp.path().join("nodes").join("zulu"))?;
    fs::create_dir_all(temp.path().join("nodes").join("alpha"))?;
    fs::create_dir_all(temp.path().join("nodes").join("bravo"))?;

    let project = ProjectService::new(temp.path().to_path_buf());
    let nodes = project.list_nodes()?;

    assert_eq!(nodes, vec!["alpha", "bravo", "zulu"]);

    Ok(())
}

#[test]
fn test_project_metadata_from_cargo_toml() -> Result<()> {
    let temp = TempDir::new()?;
    create_test_project(temp.path(), false)?; // No mecha10.json

    // Create Cargo.toml
    fs::write(
        temp.path().join("Cargo.toml"),
        r#"[package]
name = "my-robot"
version = "1.2.3"
edition = "2021"
"#,
    )?;

    let project = ProjectService::new(temp.path().to_path_buf());
    let name = project.name()?;
    let version = project.version()?;

    assert_eq!(name, "my-robot");
    assert_eq!(version, "1.2.3");

    Ok(())
}

#[test]
fn test_project_metadata_prefers_mecha10_json() -> Result<()> {
    let temp = TempDir::new()?;
    create_test_project(temp.path(), true)?; // Has mecha10.json

    // Also create Cargo.toml with different values
    fs::write(
        temp.path().join("Cargo.toml"),
        r#"[package]
name = "different-name"
version = "9.9.9"
"#,
    )?;

    let project = ProjectService::new(temp.path().to_path_buf());
    let name = project.name()?;
    let version = project.version()?;

    // Should use mecha10.json values
    assert_eq!(name, "test-project");
    assert_eq!(version, "0.1.0");

    Ok(())
}

#[test]
fn test_project_metadata_fails_without_config() {
    let temp = TempDir::new().unwrap();
    create_test_project(temp.path(), false).unwrap(); // No mecha10.json, no Cargo.toml

    let project = ProjectService::new(temp.path().to_path_buf());
    let result = project.name();

    assert!(result.is_err());
    assert!(result
        .unwrap_err()
        .to_string()
        .contains("No mecha10.json or Cargo.toml"));
}

#[test]
fn test_project_metadata_invalid_json() {
    let temp = TempDir::new().unwrap();
    create_test_project(temp.path(), false).unwrap();

    fs::write(temp.path().join("mecha10.json"), "not valid json {{{").unwrap();

    let project = ProjectService::new(temp.path().to_path_buf());
    let result = project.name();

    assert!(result.is_err());
    assert!(result.unwrap_err().to_string().contains("parse"));
}

#[test]
fn test_project_metadata_missing_name_field() {
    let temp = TempDir::new().unwrap();
    create_test_project(temp.path(), false).unwrap();

    fs::write(temp.path().join("mecha10.json"), r#"{"version": "1.0.0"}"#).unwrap();

    let project = ProjectService::new(temp.path().to_path_buf());
    let result = project.name();

    assert!(result.is_err());
    assert!(result.unwrap_err().to_string().contains("Missing 'name' field"));
}

#[test]
fn test_project_metadata_missing_version_field() {
    let temp = TempDir::new().unwrap();
    create_test_project(temp.path(), false).unwrap();

    fs::write(temp.path().join("mecha10.json"), r#"{"name": "test"}"#).unwrap();

    let project = ProjectService::new(temp.path().to_path_buf());
    let result = project.version();

    assert!(result.is_err());
    assert!(result.unwrap_err().to_string().contains("Missing 'version' field"));
}

#[test]
fn test_project_path_method() -> Result<()> {
    let temp = TempDir::new()?;
    create_test_project(temp.path(), true)?;

    let project = ProjectService::new(temp.path().to_path_buf());

    assert_eq!(project.path("nodes/my_node"), temp.path().join("nodes/my_node"));
    assert_eq!(project.path("config/robot.json"), temp.path().join("config/robot.json"));

    Ok(())
}

#[test]
fn test_project_root_getter() -> Result<()> {
    let temp = TempDir::new()?;
    create_test_project(temp.path(), true)?;

    let project = ProjectService::new(temp.path().to_path_buf());

    assert_eq!(project.root(), temp.path());

    Ok(())
}

#[test]
fn test_project_config_path() -> Result<()> {
    let temp = TempDir::new()?;
    create_test_project(temp.path(), true)?;

    let project = ProjectService::new(temp.path().to_path_buf());

    assert_eq!(project.config_path(), temp.path().join("mecha10.json"));

    Ok(())
}

#[test]
fn test_is_initialized_true() -> Result<()> {
    let temp = TempDir::new()?;
    create_test_project(temp.path(), true)?;

    let project = ProjectService::new(temp.path().to_path_buf());

    assert!(project.is_initialized());

    Ok(())
}

#[test]
fn test_is_initialized_false() -> Result<()> {
    let temp = TempDir::new()?;
    create_test_project(temp.path(), false)?;

    let project = ProjectService::new(temp.path().to_path_buf());

    assert!(!project.is_initialized());

    Ok(())
}

#[test]
fn test_validate_fails_missing_nodes_dir() {
    let temp = TempDir::new().unwrap();
    fs::create_dir_all(temp.path()).unwrap();
    fs::write(temp.path().join("mecha10.json"), "{}").unwrap();
    // Missing nodes/, drivers/, types/

    let project = ProjectService::new(temp.path().to_path_buf());
    let result = project.validate();

    assert!(result.is_err());
    assert!(result.unwrap_err().to_string().contains("missing 'nodes' directory"));
}

#[test]
fn test_validate_fails_missing_drivers_dir() {
    let temp = TempDir::new().unwrap();
    fs::create_dir_all(temp.path().join("nodes")).unwrap();
    fs::write(temp.path().join("mecha10.json"), "{}").unwrap();
    // Has nodes/, missing drivers/ and types/

    let project = ProjectService::new(temp.path().to_path_buf());
    let result = project.validate();

    assert!(result.is_err());
    assert!(result.unwrap_err().to_string().contains("missing 'drivers' directory"));
}

#[test]
fn test_validate_fails_missing_types_dir() {
    let temp = TempDir::new().unwrap();
    fs::create_dir_all(temp.path().join("nodes")).unwrap();
    fs::create_dir_all(temp.path().join("drivers")).unwrap();
    fs::write(temp.path().join("mecha10.json"), "{}").unwrap();
    // Has nodes/ and drivers/, missing types/

    let project = ProjectService::new(temp.path().to_path_buf());
    let result = project.validate();

    assert!(result.is_err());
    assert!(result.unwrap_err().to_string().contains("missing 'types' directory"));
}
