// Tests for mecha10_behavior_runtime::hot_reload::*;

use async_trait::async_trait;
use mecha10_behavior_runtime::{hot_reload::*, prelude::*};
use std::collections::HashMap;
use std::fs;
use std::time::Duration;
use tempfile::TempDir;

#[derive(Debug)]
struct TestBehavior;

#[async_trait]
impl BehaviorNode for TestBehavior {
    async fn tick(&mut self, _ctx: &Context) -> anyhow::Result<NodeStatus> {
        Ok(NodeStatus::Success)
    }

    fn name(&self) -> &str {
        "test_behavior"
    }
}

fn create_test_registry() -> NodeRegistry {
    let mut registry = NodeRegistry::new();
    registry.register("test_behavior", |_| Ok(Box::new(TestBehavior)));
    registry
}

#[test]
fn test_hot_reload_config_defaults() {
    let config = HotReloadConfig::default();
    assert!(!config.enabled);
    assert_eq!(config.watch_paths.len(), 1);
    assert_eq!(config.debounce_duration_ms, 500);
}

#[tokio::test]
async fn test_hot_reload_disabled() {
    let config = HotReloadConfig {
        enabled: false,
        watch_paths: vec![],
        debounce_duration_ms: 500,
    };

    let registry = create_test_registry();
    let loader = BehaviorLoader::new(registry);
    let watcher = HotReloadWatcher::new(config, loader);

    assert!(!watcher.is_enabled(), "Watcher should be disabled");

    let mut rx = watcher.start().await.unwrap();

    // When disabled, the channel is created but nothing is sent
    // We expect either a timeout or None (channel closed)
    let result = tokio::time::timeout(Duration::from_millis(100), rx.recv()).await;
    assert!(
        result.is_err() || result.unwrap().is_none(),
        "Should timeout or receive None when disabled"
    );
}

#[tokio::test]
async fn test_hot_reload_detects_changes() {
    let temp_dir = TempDir::new().unwrap();
    let behavior_path = temp_dir.path().join("test.json");

    // Write initial behavior tree
    let initial_config = BehaviorConfig {
        schema: None,
        name: "test".to_string(),
        description: None,
        root: crate::CompositionConfig::Node {
            node: "test_behavior".to_string(),
            config_ref: None,
            config: None,
            name: None,
        },
        configs: HashMap::new(),
    };
    fs::write(&behavior_path, initial_config.to_json().unwrap()).unwrap();

    // Canonicalize the path after writing to ensure it exists
    let canonical_behavior_path = behavior_path.canonicalize().unwrap();

    // Set up hot-reload
    let config = HotReloadConfig {
        enabled: true,
        watch_paths: vec![temp_dir.path().to_path_buf()],
        debounce_duration_ms: 100,
    };

    let registry = create_test_registry();
    let loader = BehaviorLoader::new(registry);
    let watcher = HotReloadWatcher::new(config, loader);

    let mut rx = watcher.start().await.unwrap();

    // Give the watcher time to set up
    tokio::time::sleep(Duration::from_millis(100)).await;

    // Modify the file
    fs::write(&behavior_path, initial_config.to_json().unwrap()).unwrap();

    // Should receive a reload event
    let event = tokio::time::timeout(Duration::from_secs(2), rx.recv())
        .await
        .expect("Should receive event")
        .expect("Should have event");

    match event {
        ReloadEvent::Success { path, .. } => {
            // Canonicalize both paths for comparison (handles symlinks on macOS)
            let received_canonical = path.canonicalize().unwrap_or(path);
            assert_eq!(
                received_canonical, canonical_behavior_path,
                "Path mismatch after canonicalization"
            );
        }
        ReloadEvent::Error { error, .. } => {
            panic!("Expected success, got error: {}", error);
        }
    }
}

#[tokio::test]
async fn test_hot_reload_detects_invalid_tree() {
    let temp_dir = TempDir::new().unwrap();
    let behavior_path = temp_dir.path().join("invalid.json");

    // Write invalid JSON
    fs::write(&behavior_path, "{ invalid json }").unwrap();

    // Canonicalize the path after writing
    let canonical_behavior_path = behavior_path.canonicalize().unwrap();

    // Set up hot-reload
    let config = HotReloadConfig {
        enabled: true,
        watch_paths: vec![temp_dir.path().to_path_buf()],
        debounce_duration_ms: 100,
    };

    let registry = create_test_registry();
    let loader = BehaviorLoader::new(registry);
    let watcher = HotReloadWatcher::new(config, loader);

    let mut rx = watcher.start().await.unwrap();

    // Give the watcher time to set up
    tokio::time::sleep(Duration::from_millis(100)).await;

    // Modify the file to trigger reload
    fs::write(&behavior_path, "{ still invalid }").unwrap();

    // Should receive an error event
    let event = tokio::time::timeout(Duration::from_secs(2), rx.recv())
        .await
        .expect("Should receive event")
        .expect("Should have event");

    match event {
        ReloadEvent::Error { path, error } => {
            // Canonicalize both paths for comparison (handles symlinks on macOS)
            let received_canonical = path.canonicalize().unwrap_or(path);
            assert_eq!(
                received_canonical, canonical_behavior_path,
                "Path mismatch after canonicalization"
            );
            // Check for common JSON parsing error keywords
            let error_lower = error.to_lowercase();
            assert!(
                error_lower.contains("expected")
                    || error_lower.contains("invalid")
                    || error_lower.contains("eof")
                    || error_lower.contains("parse")
                    || error_lower.contains("json")
                    || error_lower.contains("key must be")
                    || error_lower.contains("syntax"),
                "Expected JSON parsing error, got: {}",
                error
            );
        }
        ReloadEvent::Success { .. } => {
            panic!("Expected error event for invalid JSON");
        }
    }
}
