//! Integration tests for hypen-server SDK.
//!
//! These tests exercise end-to-end flows: build module -> mount -> dispatch
//! actions -> verify state & patches.

use std::sync::{Arc, Mutex};

use hypen_server::prelude::*;
use serde::{Deserialize, Serialize};
use serde_json::json;

// ---------------------------------------------------------------------------
// Shared state types
// ---------------------------------------------------------------------------

#[derive(Clone, Default, Serialize, Deserialize, Debug, PartialEq)]
struct CounterState {
    count: i32,
}

#[derive(Clone, Default, Serialize, Deserialize, Debug, PartialEq)]
struct TodoState {
    items: Vec<TodoItem>,
    filter: String,
}

#[derive(Clone, Default, Serialize, Deserialize, Debug, PartialEq)]
struct TodoItem {
    id: String,
    text: String,
    done: bool,
}

// ---------------------------------------------------------------------------
// Basic flow: build, mount, dispatch, verify state
// ---------------------------------------------------------------------------

#[test]
fn test_counter_full_lifecycle() {
    let def = ModuleBuilder::<CounterState>::new("Counter")
        .state(CounterState { count: 0 })
        .ui(r#"Column { Text("Count: ${state.count}") }"#)
        .on_action::<()>("increment", |state, _, _| {
            state.count += 1;
        })
        .on_action::<()>("decrement", |state, _, _| {
            state.count -= 1;
        })
        .build();

    let instance = ModuleInstance::new(Arc::new(def), None).unwrap();
    instance.mount();
    assert!(instance.is_mounted());

    // Increment 3 times
    for _ in 0..3 {
        instance.dispatch_action("increment", None).unwrap();
    }
    assert_eq!(instance.get_state().count, 3);

    // Decrement once
    instance.dispatch_action("decrement", None).unwrap();
    assert_eq!(instance.get_state().count, 2);

    instance.unmount();
    assert!(!instance.is_mounted());
}

// ---------------------------------------------------------------------------
// Typed payloads across multiple action types
// ---------------------------------------------------------------------------

#[test]
fn test_todo_app_flow() {
    #[derive(Deserialize)]
    struct AddItem {
        id: String,
        text: String,
    }

    #[derive(Deserialize)]
    struct ToggleItem {
        id: String,
    }

    #[derive(Deserialize)]
    struct SetFilter {
        filter: String,
    }

    let def = ModuleBuilder::<TodoState>::new("TodoApp")
        .state(TodoState {
            items: vec![],
            filter: "all".into(),
        })
        .on_action::<AddItem>("add", |state, payload, _| {
            state.items.push(TodoItem {
                id: payload.id,
                text: payload.text,
                done: false,
            });
        })
        .on_action::<ToggleItem>("toggle", |state, payload, _| {
            if let Some(item) = state.items.iter_mut().find(|i| i.id == payload.id) {
                item.done = !item.done;
            }
        })
        .on_action::<SetFilter>("setFilter", |state, payload, _| {
            state.filter = payload.filter;
        })
        .on_action::<()>("clearCompleted", |state, _, _| {
            state.items.retain(|i| !i.done);
        })
        .build();

    let instance = ModuleInstance::new(Arc::new(def), None).unwrap();
    instance.mount();

    // Add items
    instance
        .dispatch_action("add", Some(json!({"id": "1", "text": "Buy milk"})))
        .unwrap();
    instance
        .dispatch_action("add", Some(json!({"id": "2", "text": "Write tests"})))
        .unwrap();
    assert_eq!(instance.get_state().items.len(), 2);

    // Toggle item
    instance
        .dispatch_action("toggle", Some(json!({"id": "1"})))
        .unwrap();
    assert!(instance.get_state().items[0].done);
    assert!(!instance.get_state().items[1].done);

    // Set filter
    instance
        .dispatch_action("setFilter", Some(json!({"filter": "active"})))
        .unwrap();
    assert_eq!(instance.get_state().filter, "active");

    // Clear completed
    instance.dispatch_action("clearCompleted", None).unwrap();
    assert_eq!(instance.get_state().items.len(), 1);
    assert_eq!(instance.get_state().items[0].id, "2");
}

// ---------------------------------------------------------------------------
// Patch collection: verify engine emits patches on state change
// ---------------------------------------------------------------------------

#[test]
fn test_patches_emitted_on_state_change() {
    let patches: Arc<Mutex<Vec<Vec<Patch>>>> = Arc::new(Mutex::new(vec![]));
    let patches_clone = patches.clone();

    let def = ModuleBuilder::<CounterState>::new("PatchTest")
        .state(CounterState { count: 0 })
        .ui(r#"Text("${state.count}")"#)
        .on_action::<()>("increment", |state, _, _| {
            state.count += 1;
        })
        .build();

    let instance = ModuleInstance::new(Arc::new(def), None).unwrap();

    // Register patch callback (after construction, so initial render is not captured)
    instance.on_patches(move |p| {
        patches_clone.lock().unwrap().push(p.to_vec());
    });

    instance.mount();

    // Dispatch action — this should produce patches (setProp for updated text)
    instance.dispatch_action("increment", None).unwrap();
    let after_dispatch = patches.lock().unwrap().len();
    assert!(after_dispatch > 0, "State change should produce patches");
}

// ---------------------------------------------------------------------------
// Global context: share data between modules
// ---------------------------------------------------------------------------

#[test]
fn test_global_context_shared_between_modules() {
    let ctx = Arc::new(GlobalContext::new());
    ctx.register_module_state("theme_config", json!({"theme": "dark"}));

    let def1 = ModuleBuilder::<CounterState>::new("ModuleA")
        .state(CounterState { count: 0 })
        .on_created(|_state, ctx| {
            if let Some(ctx) = ctx {
                let state = ctx.get_module_state("theme_config").unwrap();
                assert_eq!(state["theme"], "dark");
            }
        })
        .build();

    let def2 = ModuleBuilder::<CounterState>::new("ModuleB")
        .state(CounterState { count: 0 })
        .on_created(|_state, ctx| {
            if let Some(ctx) = ctx {
                assert!(ctx.has_module("theme_config"));
            }
        })
        .build();

    let inst1 = ModuleInstance::new(Arc::new(def1), Some(ctx.clone())).unwrap();
    let inst2 = ModuleInstance::new(Arc::new(def2), Some(ctx.clone())).unwrap();

    inst1.mount();
    inst2.mount();
}

// ---------------------------------------------------------------------------
// Lifecycle ordering: on_created before dispatch, on_destroyed after
// ---------------------------------------------------------------------------

#[test]
fn test_lifecycle_ordering() {
    let events: Arc<Mutex<Vec<String>>> = Arc::new(Mutex::new(vec![]));
    let e1 = events.clone();
    let e2 = events.clone();
    let e3 = events.clone();

    let def = ModuleBuilder::<CounterState>::new("LifecycleOrder")
        .state(CounterState { count: 0 })
        .on_created(move |_, _| {
            e1.lock().unwrap().push("created".into());
        })
        .on_action::<()>("act", move |state, _, _| {
            e2.lock().unwrap().push("action".into());
            state.count += 1;
        })
        .on_destroyed(move |_, _| {
            e3.lock().unwrap().push("destroyed".into());
        })
        .build();

    let instance = ModuleInstance::new(Arc::new(def), None).unwrap();
    instance.mount();
    instance.dispatch_action("act", None).unwrap();
    instance.unmount();

    let log = events.lock().unwrap();
    assert_eq!(*log, vec!["created", "action", "destroyed"]);
}

// ---------------------------------------------------------------------------
// State JSON roundtrip
// ---------------------------------------------------------------------------

#[test]
fn test_state_json_roundtrip() {
    let def = ModuleBuilder::<TodoState>::new("JsonRT")
        .state(TodoState {
            items: vec![TodoItem {
                id: "1".into(),
                text: "Test".into(),
                done: false,
            }],
            filter: "all".into(),
        })
        .build();

    let instance = ModuleInstance::new(Arc::new(def), None).unwrap();
    let json = instance.get_state_json().unwrap();

    assert_eq!(json["filter"], "all");
    assert_eq!(json["items"][0]["text"], "Test");
    assert_eq!(json["items"][0]["done"], false);
}

// ---------------------------------------------------------------------------
// Error: unknown action
// ---------------------------------------------------------------------------

#[test]
fn test_dispatch_unknown_action_returns_error() {
    let def = ModuleBuilder::<CounterState>::new("ErrTest")
        .state(CounterState { count: 0 })
        .build();

    let instance = ModuleInstance::new(Arc::new(def), None).unwrap();
    let result = instance.dispatch_action("nope", None);
    assert!(result.is_err());
}

// ---------------------------------------------------------------------------
// Router integration
// ---------------------------------------------------------------------------

#[test]
fn test_router_pattern_matching() {
    let router = HypenRouter::new();

    // Navigate and check state
    router.push("/counter");
    assert_eq!(router.current_path(), "/counter");

    // Match patterns
    let m = router.match_path("/counter", "/counter");
    assert!(m.is_some());

    let m = router.match_path("/profile/:id", "/profile/42").unwrap();
    assert_eq!(m.params["id"], "42");

    // Wildcard
    assert!(router.match_path("/api/*", "/api/users/list").is_some());
    assert!(router.match_path("/api/*", "/other").is_none());
}

// ---------------------------------------------------------------------------
// Component discovery
// ---------------------------------------------------------------------------

#[test]
fn test_component_discovery() {
    let dir = std::env::temp_dir().join("hypen_integration_test_discovery");
    let _ = std::fs::remove_dir_all(&dir);
    std::fs::create_dir_all(&dir).unwrap();
    std::fs::write(dir.join("Header.hypen"), r#"Text("Header")"#).unwrap();
    std::fs::write(dir.join("Footer.hypen"), r#"Text("Footer")"#).unwrap();

    let mut registry = ComponentRegistry::new();
    let loaded = registry.load_dir(dir.to_str().unwrap()).unwrap();
    assert_eq!(loaded.len(), 2);
    assert!(registry.get("Header").is_some());
    assert!(registry.get("Footer").is_some());

    let _ = std::fs::remove_dir_all(&dir);
}

// ---------------------------------------------------------------------------
// HypenApp builder: full app assembly
// ---------------------------------------------------------------------------

#[test]
fn test_hypen_app_builder() {
    let counter_def = ModuleBuilder::<CounterState>::new("Counter")
        .state(CounterState { count: 0 })
        .on_action::<()>("increment", |state, _, _| {
            state.count += 1;
        })
        .build();

    let app = HypenApp::builder().route("/", counter_def).build();

    assert!(app.match_route("/").is_some());
}

// ---------------------------------------------------------------------------
// HypenApp instantiate + dispatch
// ---------------------------------------------------------------------------

#[test]
fn test_app_instantiate_and_dispatch() {
    let app = HypenApp::default();

    let def = ModuleBuilder::<CounterState>::new("Counter")
        .state(CounterState { count: 0 })
        .on_action::<()>("increment", |state, _, _| {
            state.count += 1;
        })
        .build();

    let instance = app.instantiate(Arc::new(def)).unwrap();
    instance.mount();

    instance.dispatch_action("increment", None).unwrap();
    instance.dispatch_action("increment", None).unwrap();
    assert_eq!(instance.get_state().count, 2);
}

// ---------------------------------------------------------------------------
// Multiple modules sharing global context
// ---------------------------------------------------------------------------

#[test]
fn test_multi_module_via_global_context() {
    let ctx = Arc::new(GlobalContext::new());

    let def_a = ModuleBuilder::<CounterState>::new("A")
        .state(CounterState { count: 0 })
        .on_action::<()>("increment_and_sync", |state, _, ctx| {
            state.count += 1;
            if let Some(ctx) = ctx {
                ctx.register_module_state("A", json!({"count": state.count}));
            }
        })
        .build();

    let def_b = ModuleBuilder::<CounterState>::new("B")
        .state(CounterState { count: 0 })
        .on_action::<()>("read_global", |state, _, ctx| {
            if let Some(ctx) = ctx {
                if let Some(val) = ctx.get_module_state("A") {
                    state.count = val["count"].as_i64().unwrap_or(0) as i32;
                }
            }
        })
        .build();

    let inst_a = ModuleInstance::new(Arc::new(def_a), Some(ctx.clone())).unwrap();
    let inst_b = ModuleInstance::new(Arc::new(def_b), Some(ctx.clone())).unwrap();
    inst_a.mount();
    inst_b.mount();

    // Module A increments 5 times, syncing to global each time
    for _ in 0..5 {
        inst_a.dispatch_action("increment_and_sync", None).unwrap();
    }

    // Module B reads from global
    inst_b.dispatch_action("read_global", None).unwrap();
    assert_eq!(inst_b.get_state().count, 5);
}

// ---------------------------------------------------------------------------
// UI file loading from disk
// ---------------------------------------------------------------------------

#[test]
fn test_ui_file_integration() {
    let dir = std::env::temp_dir().join("hypen_integration_ui_file");
    let _ = std::fs::remove_dir_all(&dir);
    std::fs::create_dir_all(&dir).unwrap();

    let path = dir.join("counter.hypen");
    std::fs::write(&path, r#"Column { Text("Count: ${state.count}") }"#).unwrap();

    let def = ModuleBuilder::<CounterState>::new("FileModule")
        .state(CounterState { count: 0 })
        .ui_file(path.to_str().unwrap())
        .on_action::<()>("increment", |state, _, _| {
            state.count += 1;
        })
        .build();

    let instance = ModuleInstance::new(Arc::new(def), None).unwrap();
    instance.mount();
    instance.dispatch_action("increment", None).unwrap();
    assert_eq!(instance.get_state().count, 1);

    let _ = std::fs::remove_dir_all(&dir);
}

// ---------------------------------------------------------------------------
// Persist flag
// ---------------------------------------------------------------------------

#[test]
fn test_persist_flag_propagates() {
    let def = ModuleBuilder::<CounterState>::new("Persistent")
        .state(CounterState { count: 0 })
        .persist()
        .build();

    assert!(def.is_persistent());

    let non_persist = ModuleBuilder::<CounterState>::new("Ephemeral")
        .state(CounterState { count: 0 })
        .build();

    assert!(!non_persist.is_persistent());
}

// ---------------------------------------------------------------------------
// Event emitter
// ---------------------------------------------------------------------------

#[test]
fn test_event_emitter_cross_module() {
    let emitter = EventEmitter::new();
    let received = Arc::new(Mutex::new(vec![]));
    let r = received.clone();

    emitter.on("user:login", move |payload| {
        r.lock().unwrap().push(payload.clone());
    });

    emitter.emit("user:login", &json!({"user": "alice"}));
    emitter.emit("user:login", &json!({"user": "bob"}));

    let msgs = received.lock().unwrap();
    assert_eq!(msgs.len(), 2);
    assert_eq!(msgs[0]["user"], "alice");
    assert_eq!(msgs[1]["user"], "bob");
}
