//! End-to-end tests for `provisioning-tool` library surface.
//!
//! These tests exercise the registry + tool dispatch without requiring a running
//! orchestrator, vault, or hcloud CLI — only pure-Rust code paths.

use provisioning_core::{
    Environment,
    sources::{NclCache, OrchestratorClient},
    providers::hcloud::HcloudClient,
    Registry,
    tool::Context,
};
use provisioning_tool::{build_env, print_json};
use serde_json::json;
use std::sync::Arc;

fn make_registry() -> Registry {
    let cache  = Arc::new(NclCache::new());
    let orch   = Arc::new(OrchestratorClient::new("http://localhost:19999"));
    let hcloud = Arc::new(HcloudClient::new());
    Registry::with_all_tools(cache, orch, hcloud, None).expect("registry init")
}

fn make_ctx() -> Context {
    Context::new(Arc::new(Environment::default()))
}

// ── Registry shape ────────────────────────────────────────────────────────────

#[test]
fn list_returns_52_tools_without_vault() {
    let reg = make_registry();
    assert_eq!(reg.len(), 52);
}

#[test]
fn list_is_sorted_and_non_empty() {
    let reg = make_registry();
    let names: Vec<&str> = reg.list().iter().map(|m| m.name).collect();
    assert!(!names.is_empty());
    let mut sorted = names.clone();
    sorted.sort_unstable();
    assert_eq!(names, sorted);
}

#[test]
fn schema_of_known_tool_has_object_type() {
    let reg = make_registry();
    let schema = reg.schema_of("workspace_list").expect("workspace_list must exist");
    assert_eq!(schema["type"], "object");
}

#[test]
fn schema_of_unknown_tool_returns_none() {
    let reg = make_registry();
    assert!(reg.schema_of("nonexistent_tool").is_none());
}

#[test]
fn get_known_tool_returns_some() {
    let reg = make_registry();
    assert!(reg.get("installer_settings_defaults").is_some());
}

#[test]
fn get_unknown_tool_returns_none() {
    let reg = make_registry();
    assert!(reg.get("no_such_tool").is_none());
}

// ── Pure-Rust tool dispatch ───────────────────────────────────────────────────

#[tokio::test]
async fn installer_defaults_solo_returns_valid_json() {
    let reg = make_registry();
    let ctx = make_ctx();
    let result = reg
        .invoke("installer_settings_defaults", json!({"mode": "solo"}), &ctx)
        .await
        .expect("invoke failed");
    assert_eq!(result["mode"], "solo");
    assert!(result["orchestrator"]["port"].is_number());
}

#[tokio::test]
async fn installer_defaults_enterprise_returns_valid_json() {
    let reg = make_registry();
    let ctx = make_ctx();
    let result = reg
        .invoke("installer_settings_defaults", json!({"mode": "enterprise"}), &ctx)
        .await
        .expect("invoke failed");
    assert_eq!(result["mode"], "enterprise");
    assert_eq!(result["nats"]["cluster"], true);
}

#[tokio::test]
async fn installer_defaults_unknown_mode_returns_error() {
    let reg = make_registry();
    let ctx = make_ctx();
    let err = reg
        .invoke("installer_settings_defaults", json!({"mode": "unknown_xyz"}), &ctx)
        .await
        .unwrap_err();
    assert!(matches!(err, provisioning_core::ToolError::InvalidParam { .. }));
}

#[tokio::test]
async fn installer_settings_validate_without_config_returns_valid_json() {
    let reg = make_registry();
    let ctx = make_ctx();
    let result = reg
        .invoke("installer_settings_validate", json!({}), &ctx)
        .await
        .expect("invoke failed");
    // Should return a json object with 'valid' and 'issues' fields
    assert!(result["issues"].is_array());
}

#[tokio::test]
async fn invoke_missing_tool_returns_not_found() {
    let reg = make_registry();
    let ctx = make_ctx();
    let err = reg
        .invoke("nonexistent_tool", json!({}), &ctx)
        .await
        .unwrap_err();
    assert!(matches!(err, provisioning_core::ToolError::NotFound(_)));
}

#[tokio::test]
async fn infra_detect_with_project_root_returns_detections() {
    let reg = make_registry();
    let mut env = Environment::default();
    // Point to the platform dir which has Cargo.toml, justfile, etc.
    env.workspaces_root = std::path::PathBuf::from(
        std::env::var("CARGO_MANIFEST_DIR").unwrap_or_else(|_| ".".into()),
    )
    .join("../..");
    let ctx = Context::new(Arc::new(env));

    let result = reg
        .invoke(
            "infra_detect",
            json!({ "path": env::manifest_dir_parent() }),
            &ctx,
        )
        .await
        .expect("infra_detect failed");
    assert!(result["detections"].is_array());
}

#[tokio::test]
async fn ontology_graph_with_missing_root_returns_error() {
    let reg = make_registry();
    let mut env = Environment::default();
    env.ontology_root = std::path::PathBuf::from("/tmp/no_such_ontology_dir_xyz");
    let ctx = Context::new(Arc::new(env));

    let err = reg
        .invoke("ontology_graph", json!({}), &ctx)
        .await
        .unwrap_err();
    assert!(matches!(
        err,
        provisioning_core::ToolError::NotFound(_) | provisioning_core::ToolError::Invocation(_)
    ));
}

// ── Output helpers ────────────────────────────────────────────────────────────

#[test]
fn print_json_compact_does_not_panic() {
    let v = json!({"key": "value", "n": 42});
    // Just verify it doesn't panic — output goes to stdout
    print_json(&v, true);
    print_json(&v, false);
}

// ── build_env structure ───────────────────────────────────────────────────────

#[test]
fn build_env_returns_valid_struct() {
    // Verify build_env produces a structurally valid Environment without asserting
    // specific values (env vars may be set by CI or other tests in parallel).
    let env = build_env();
    assert!(!env.orchestrator_url.is_empty());
    assert!(env.orchestrator_url.starts_with("http"));
}

// ── helpers ───────────────────────────────────────────────────────────────────

mod env {
    pub fn manifest_dir_parent() -> String {
        let manifest = std::env::var("CARGO_MANIFEST_DIR").unwrap_or_else(|_| ".".into());
        std::path::PathBuf::from(manifest)
            .parent()
            .and_then(|p| p.parent())
            .map(|p| p.to_string_lossy().into_owned())
            .unwrap_or_else(|| ".".into())
    }
}
