//! Integration tests for the `#[node]` attribute macro.
//!
//! Verifies that `#[node]` generates correct `NodeFn` implementations
//! from simple async functions, with and without `NodeContext`.

use pe_core::PeError;
use pe_core::node::{NodeContext, NodeFn, NodeResult};
use pe_core::state::{State, StateUpdate};
use pe_core::types::{END, START};
use pe_graph::{ExecutionOutcome, GraphConfig, StateGraph};
use pe_macros::node;
use serde::{Deserialize, Serialize};

fn test_ctx(name: &str) -> NodeContext {
    NodeContext {
        step: 1,
        recursion_limit: 10,
        node_name: name.into(),
        activation: pe_core::node::ActivationReason::EntryPoint,
        metadata: Default::default(),
        phase_store: Default::default(),
        stream_sender: None,
        tool_observer: None,
        lobe_runtime_service_factory: None,
    }
}

// ── Test State ───────────────────────────────────────────────────────

#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
struct TestState {
    value: String,
    counter: u32,
}

#[derive(Debug, Clone, Serialize, Deserialize, Default)]
struct TestStateUpdate {
    value: Option<String>,
    counter: Option<u32>,
}

impl StateUpdate for TestStateUpdate {}

impl State for TestState {
    type Update = TestStateUpdate;
    fn apply(&mut self, update: TestStateUpdate) {
        if let Some(v) = update.value {
            self.value = v;
        }
        if let Some(c) = update.counter {
            self.counter = c;
        }
    }
}

impl TestState {
    fn new() -> Self {
        Self {
            value: String::new(),
            counter: 0,
        }
    }
}

// ── Mode 1: Simple async function (no NodeContext) ───────────────────

#[node]
async fn prepare(state: &TestState) -> NodeResult<TestStateUpdate> {
    NodeResult::Update(TestStateUpdate {
        value: Some(format!("prepared:{}", state.counter)),
        counter: Some(state.counter + 1),
    })
}

#[tokio::test]
async fn test_node_attr_simple_function_produces_update() {
    // `prepare` should be a variable of type PrepareNode that implements NodeFn
    let result = prepare.call(&TestState::new(), &test_ctx("prepare")).await;

    match result {
        NodeResult::Update(update) => {
            assert_eq!(update.value, Some("prepared:0".into()));
            assert_eq!(update.counter, Some(1));
        }
        other => panic!("Expected Update, got: {other:?}"),
    }
}

#[tokio::test]
async fn test_node_attr_name_matches_function_name() {
    assert_eq!(prepare.name(), "prepare");
}

// ── Mode 2: With NodeContext access ──────────────────────────────────

#[node]
async fn chat(state: &TestState, ctx: &NodeContext) -> NodeResult<TestStateUpdate> {
    let remaining = ctx.remaining_steps();
    NodeResult::Update(TestStateUpdate {
        value: Some(format!("chat:remaining={}", remaining)),
        counter: Some(state.counter + 1),
    })
}

#[tokio::test]
async fn test_node_attr_with_context_accesses_ctx() {
    let mut ctx = test_ctx("chat");
    ctx.step = 3;

    let result = chat.call(&TestState::new(), &ctx).await;

    match result {
        NodeResult::Update(update) => {
            assert_eq!(update.value, Some("chat:remaining=7".into()));
            assert_eq!(update.counter, Some(1));
        }
        other => panic!("Expected Update, got: {other:?}"),
    }
}

#[tokio::test]
async fn test_node_attr_with_context_name_matches() {
    assert_eq!(chat.name(), "chat");
}

// ── Test: Interrupt variant works ────────────────────────────────────

#[node]
async fn review(_state: &TestState) -> NodeResult<TestStateUpdate> {
    NodeResult::Interrupt(pe_core::node::InterruptRequest {
        reason: "needs review".into(),
        partial_update: None,
        resume_point: "review_gate".into(),
    })
}

#[tokio::test]
async fn test_node_attr_interrupt_variant() {
    let result = review.call(&TestState::new(), &test_ctx("review")).await;

    match result {
        NodeResult::Interrupt(req) => {
            assert_eq!(req.reason, "needs review");
            assert_eq!(req.resume_point, "review_gate");
        }
        other => panic!("Expected Interrupt, got: {other:?}"),
    }
}

// ── Test: Works with StateGraph::add_node ────────────────────────────

#[node]
async fn step_a(state: &TestState) -> NodeResult<TestStateUpdate> {
    NodeResult::Update(TestStateUpdate {
        value: Some(format!("a:{}", state.counter)),
        counter: Some(state.counter + 1),
    })
}

#[node]
async fn step_b(_state: &TestState, ctx: &NodeContext) -> NodeResult<TestStateUpdate> {
    NodeResult::Update(TestStateUpdate {
        value: Some(format!("b:step={}", ctx.step)),
        counter: None,
    })
}

#[tokio::test]
async fn test_node_attr_integrates_with_state_graph() {
    let graph = StateGraph::new()
        .add_node("step_a", step_a)
        .add_node("step_b", step_b)
        .add_edge(START, "step_a")
        .add_edge("step_a", "step_b")
        .add_edge("step_b", END)
        .compile()
        .unwrap();

    let outcome = graph
        .invoke(TestState::new(), GraphConfig::default())
        .await
        .unwrap();

    match outcome {
        ExecutionOutcome::Completed(state) => {
            // step_b runs last, so value = "b:step=2"
            assert_eq!(state.counter, 1);
            assert!(
                state.value.starts_with("b:"),
                "Expected step_b output, got: {}",
                state.value
            );
        }
        other => panic!("Expected Completed, got: {other:?}"),
    }
}

// ── Mode: Result<NodeResult<U>, PeError> return type ─────────────────

#[node]
async fn fallible_ok(state: &TestState) -> Result<NodeResult<TestStateUpdate>, PeError> {
    let val = Some(format!("ok:{}", state.counter));
    Ok(NodeResult::Update(TestStateUpdate {
        value: val,
        counter: Some(state.counter + 10),
    }))
}

#[tokio::test]
async fn test_node_attr_result_return_ok_produces_update() {
    let result = fallible_ok
        .call(&TestState::new(), &test_ctx("fallible_ok"))
        .await;

    match result {
        NodeResult::Update(update) => {
            assert_eq!(update.value, Some("ok:0".into()));
            assert_eq!(update.counter, Some(10));
        }
        other => panic!("Expected Update, got: {other:?}"),
    }
}

#[node]
async fn fallible_err(_state: &TestState) -> Result<NodeResult<TestStateUpdate>, PeError> {
    Err(PeError::InvalidUpdate {
        details: "something went wrong".into(),
    })
}

#[tokio::test]
async fn test_node_attr_result_return_err_becomes_node_error() {
    let result = fallible_err
        .call(&TestState::new(), &test_ctx("fallible_err"))
        .await;

    match result {
        NodeResult::Error(e) => {
            let msg = e.to_string();
            assert!(
                msg.contains("something went wrong"),
                "Expected error message, got: {msg}"
            );
        }
        other => panic!("Expected Error, got: {other:?}"),
    }
}

#[tokio::test]
async fn test_node_attr_result_return_name_matches() {
    assert_eq!(fallible_ok.name(), "fallible_ok");
    assert_eq!(fallible_err.name(), "fallible_err");
}
