/// Signal graph extraction — the core of DreamStack's compile-time reactivity.
///
/// Walks the AST and builds a directed acyclic graph (DAG) of signals:
/// - Source signals: `let count = 0` (mutable, user-controlled)
/// - Derived signals: `let doubled = count * 2` (computed, auto-tracked)
/// - Effects: DOM bindings that update when their dependencies change

use ds_parser::{Program, Declaration, Expr, BinOp, Container, Element, LetDecl, ViewDecl, Span};
use ds_diagnostic::{Diagnostic, Severity};
use std::collections::{HashMap, HashSet};

/// The complete signal dependency graph for a program.
#[derive(Debug)]
pub struct SignalGraph {
    pub nodes: Vec<SignalNode>,
    pub name_to_id: HashMap<String, usize>,
}

/// A node in the signal graph.
#[derive(Debug, Clone)]
pub struct SignalNode {
    pub id: usize,
    pub name: String,
    pub kind: SignalKind,
    pub dependencies: Vec<Dependency>,
    pub initial_value: Option<InitialValue>,
    pub streamable: bool,
}

#[derive(Debug, Clone)]
pub enum SignalKind {
    /// Mutable source signal: `let count = 0`
    Source,
    /// Computed derived signal: `let doubled = count * 2`
    Derived,
    /// An event handler that mutates signals
    Handler { event: String, mutations: Vec<Mutation> },
}

/// What a handler does to a signal.
#[derive(Debug, Clone)]
pub struct Mutation {
    pub target: String,
    pub op: MutationOp,
}

#[derive(Debug, Clone)]
pub enum MutationOp {
    Set(String),       // expression source
    AddAssign(String),
    SubAssign(String),
    MulAssign(String),
    DivAssign(String),
}

/// A dependency edge in the signal graph.
#[derive(Debug, Clone)]
pub struct Dependency {
    pub signal_name: String,
    pub signal_id: Option<usize>,
}

/// Inferred initial value for source signals.
#[derive(Debug, Clone)]
pub enum InitialValue {
    Int(i64),
    Float(f64),
    Bool(bool),
    String(String),
}

/// Analyzed view information.
#[derive(Debug)]
pub struct AnalyzedView {
    pub name: String,
    pub bindings: Vec<DomBinding>,
}

/// A reactive DOM binding extracted from a view.
#[derive(Debug, Clone)]
pub struct DomBinding {
    pub kind: BindingKind,
    pub dependencies: Vec<String>,
}

#[derive(Debug, Clone)]
pub enum BindingKind {
    /// `text label` — text content bound to a signal
    TextContent { signal: String },
    /// `button "+" { click: count += 1 }` — event handler on an element
    EventHandler { element_tag: String, event: String, action: String },
    /// `when cond -> body` — conditional mount/unmount
    Conditional { condition_signals: Vec<String> },
    /// `column [ ... ]` — static container
    StaticContainer { kind: String, child_count: usize },
    /// Static text with no binding
    StaticText { text: String },
}

/// Static description of all signals for receiver reconstruction.
#[derive(Debug, Clone)]
pub struct SignalManifest {
    pub signals: Vec<ManifestEntry>,
}

#[derive(Debug, Clone)]
pub struct ManifestEntry {
    pub name: String,
    pub kind: SignalKind,
    pub initial: Option<InitialValue>,
    pub is_spring: bool,
}

impl SignalGraph {
    /// Build a signal graph from a parsed program.
    pub fn from_program(program: &Program) -> Self {
        let mut graph = SignalGraph {
            nodes: Vec::new(),
            name_to_id: HashMap::new(),
        };

        // First pass: register all let declarations as signals
        for decl in &program.declarations {
            if let Declaration::Let(let_decl) = decl {
                let deps = extract_dependencies(&let_decl.value);
                let kind = if deps.is_empty() {
                    SignalKind::Source
                } else {
                    SignalKind::Derived
                };

                let initial = match &let_decl.value {
                    Expr::IntLit(n) => Some(InitialValue::Int(*n)),
                    Expr::FloatLit(n) => Some(InitialValue::Float(*n)),
                    Expr::BoolLit(b) => Some(InitialValue::Bool(*b)),
                    Expr::StringLit(s) => {
                        if s.segments.len() == 1 {
                            if let ds_parser::StringSegment::Literal(text) = &s.segments[0] {
                                Some(InitialValue::String(text.clone()))
                            } else {
                                None
                            }
                        } else {
                            None
                        }
                    }
                    _ => None,
                };

                let id = graph.nodes.len();
                let dependencies: Vec<Dependency> = deps.into_iter()
                    .map(|name| Dependency { signal_name: name, signal_id: None })
                    .collect();

                graph.name_to_id.insert(let_decl.name.clone(), id);
                graph.nodes.push(SignalNode {
                    id,
                    name: let_decl.name.clone(),
                    kind,
                    dependencies,
                    initial_value: initial,
                    streamable: false,
                });
            }
        }

        // Detect stream declarations and mark source signals as streamable
        let has_stream = program.declarations.iter()
            .any(|d| matches!(d, Declaration::Stream(_)));
        if has_stream {
            for node in &mut graph.nodes {
                if matches!(node.kind, SignalKind::Source) {
                    node.streamable = true;
                }
            }
        }

        // Second pass: register event handlers
        for decl in &program.declarations {
            if let Declaration::OnHandler(handler) = decl {
                let mutations = extract_mutations(&handler.body);
                let deps: Vec<String> = mutations.iter().map(|m| m.target.clone()).collect();
                let id = graph.nodes.len();

                graph.nodes.push(SignalNode {
                    id,
                    name: format!("handler_{}", handler.event),
                    kind: SignalKind::Handler {
                        event: handler.event.clone(),
                        mutations,
                    },
                    dependencies: deps.into_iter()
                        .map(|name| Dependency { signal_name: name, signal_id: None })
                        .collect(),
                    initial_value: None,
                    streamable: false,
                });
            }
        }

        // Third pass: resolve dependency IDs
        let name_map = graph.name_to_id.clone();
        for node in &mut graph.nodes {
            for dep in &mut node.dependencies {
                dep.signal_id = name_map.get(&dep.signal_name).copied();
            }
        }

        graph
    }

    /// Generate a manifest for receivers to know how to reconstruct the signal state.
    pub fn signal_manifest(&self) -> SignalManifest {
        SignalManifest {
            signals: self.nodes.iter()
                .filter(|n| n.streamable)
                .map(|n| ManifestEntry {
                    name: n.name.clone(),
                    kind: n.kind.clone(),
                    initial: n.initial_value.clone(),
                    is_spring: false,
                })
                .collect()
        }
    }

    /// Analyze views and extract DOM bindings.
    pub fn analyze_views(program: &Program) -> Vec<AnalyzedView> {
        let mut views = Vec::new();
        for decl in &program.declarations {
            if let Declaration::View(view) = decl {
                let bindings = extract_bindings(&view.body);
                views.push(AnalyzedView {
                    name: view.name.clone(),
                    bindings,
                });
            }
        }
        views
    }

    /// Get topological order for signal propagation.
    /// Returns (order, diagnostics) — diagnostics contain cycle errors if any.
    pub fn topological_order(&self) -> (Vec<usize>, Vec<Diagnostic>) {
        let mut visited = HashSet::new();
        let mut in_stack = HashSet::new(); // for cycle detection
        let mut order = Vec::new();
        let mut diagnostics = Vec::new();

        for node in &self.nodes {
            if !visited.contains(&node.id) {
                self.topo_visit(node.id, &mut visited, &mut in_stack, &mut order, &mut diagnostics);
            }
        }

        (order, diagnostics)
    }

    fn topo_visit(
        &self,
        id: usize,
        visited: &mut HashSet<usize>,
        in_stack: &mut HashSet<usize>,
        order: &mut Vec<usize>,
        diagnostics: &mut Vec<Diagnostic>,
    ) {
        if visited.contains(&id) {
            return;
        }
        if in_stack.contains(&id) {
            // Cycle detected!
            let node = &self.nodes[id];
            diagnostics.push(Diagnostic::error(
                format!("circular signal dependency: `{}` depends on itself", node.name),
                Span { start: 0, end: 0, line: 0, col: 0 },
            ).with_code("E1001"));
            return;
        }
        in_stack.insert(id);

        for dep in &self.nodes[id].dependencies {
            if let Some(dep_id) = dep.signal_id {
                self.topo_visit(dep_id, visited, in_stack, order, diagnostics);
            }
        }

        in_stack.remove(&id);
        visited.insert(id);
        order.push(id);
    }

    /// Detect signals not referenced by any view or export (dead signals).
    pub fn dead_signals(&self, program: &Program) -> Vec<Diagnostic> {
        let mut referenced = HashSet::new();

        // Collect all signal names referenced in views
        for decl in &program.declarations {
            if let Declaration::View(view) = decl {
                let deps = extract_dependencies(&view.body);
                for dep in deps {
                    referenced.insert(dep);
                }
            }
        }

        // Also include signals referenced by derived signals
        for node in &self.nodes {
            for dep in &node.dependencies {
                referenced.insert(dep.signal_name.clone());
            }
        }

        // Also include streams and event handler targets
        for decl in &program.declarations {
            if let Declaration::OnHandler(h) = decl {
                let deps = extract_dependencies(&h.body);
                for dep in deps {
                    referenced.insert(dep);
                }
            }
        }

        let mut warnings = Vec::new();
        for node in &self.nodes {
            if matches!(node.kind, SignalKind::Source) && !referenced.contains(&node.name) {
                warnings.push(Diagnostic::warning(
                    format!("signal `{}` is never read", node.name),
                    Span { start: 0, end: 0, line: 0, col: 0 },
                ).with_code("W1001"));
            }
        }

        warnings
    }

    /// Build signal graph and return diagnostics from analysis.
    pub fn from_program_with_diagnostics(program: &Program) -> (Self, Vec<Diagnostic>) {
        let graph = Self::from_program(program);
        let mut diagnostics = Vec::new();

        // Cycle detection
        let (_order, cycle_diags) = graph.topological_order();
        diagnostics.extend(cycle_diags);

        // Dead signal detection
        let dead_diags = graph.dead_signals(program);
        diagnostics.extend(dead_diags);

        (graph, diagnostics)
    }
}

/// Extract all signal names referenced in an expression.
fn extract_dependencies(expr: &Expr) -> Vec<String> {
    let mut deps = Vec::new();
    collect_deps(expr, &mut deps);
    deps.sort();
    deps.dedup();
    deps
}

fn collect_deps(expr: &Expr, deps: &mut Vec<String>) {
    match expr {
        Expr::Ident(name) => deps.push(name.clone()),
        Expr::DotAccess(base, _) => collect_deps(base, deps),
        Expr::BinOp(left, _, right) => {
            collect_deps(left, deps);
            collect_deps(right, deps);
        }
        Expr::UnaryOp(_, inner) => collect_deps(inner, deps),
        Expr::Call(_, args) => {
            for arg in args {
                collect_deps(arg, deps);
            }
        }
        Expr::If(cond, then_b, else_b) => {
            collect_deps(cond, deps);
            collect_deps(then_b, deps);
            collect_deps(else_b, deps);
        }
        Expr::Pipe(left, right) => {
            collect_deps(left, deps);
            collect_deps(right, deps);
        }
        Expr::Container(c) => {
            for child in &c.children {
                collect_deps(child, deps);
            }
        }
        Expr::Element(el) => {
            for arg in &el.args {
                collect_deps(arg, deps);
            }
            for (_, val) in &el.props {
                collect_deps(val, deps);
            }
        }
        Expr::Record(fields) => {
            for (_, val) in fields {
                collect_deps(val, deps);
            }
        }
        Expr::List(items) => {
            for item in items {
                collect_deps(item, deps);
            }
        }
        Expr::When(cond, body, else_body) => {
            collect_deps(cond, deps);
            collect_deps(body, deps);
            if let Some(eb) = else_body {
                collect_deps(eb, deps);
            }
        }
        Expr::Match(scrutinee, arms) => {
            collect_deps(scrutinee, deps);
            for arm in arms {
                collect_deps(&arm.body, deps);
            }
        }
        Expr::Assign(target, _, value) => {
            collect_deps(target, deps);
            collect_deps(value, deps);
        }
        Expr::Lambda(_, body) => collect_deps(body, deps),
        Expr::StringLit(s) => {
            for seg in &s.segments {
                if let ds_parser::StringSegment::Interpolation(expr) = seg {
                    collect_deps(expr, deps);
                }
            }
        }
        _ => {}
    }
}

/// Extract mutations from a handler body (e.g., `count += 1`).
fn extract_mutations(expr: &Expr) -> Vec<Mutation> {
    let mut mutations = Vec::new();
    match expr {
        Expr::Assign(target, op, value) => {
            if let Expr::Ident(name) = target.as_ref() {
                let mutation_op = match op {
                    ds_parser::AssignOp::Set => MutationOp::Set(format!("{value:?}")),
                    ds_parser::AssignOp::AddAssign => MutationOp::AddAssign(format!("{value:?}")),
                    ds_parser::AssignOp::SubAssign => MutationOp::SubAssign(format!("{value:?}")),
                    ds_parser::AssignOp::MulAssign => MutationOp::MulAssign(format!("{value:?}")),
                    ds_parser::AssignOp::DivAssign => MutationOp::DivAssign(format!("{value:?}")),
                };
                mutations.push(Mutation { target: name.clone(), op: mutation_op });
            }
        }
        Expr::Block(exprs) => {
            for e in exprs {
                mutations.extend(extract_mutations(e));
            }
        }
        _ => {}
    }
    mutations
}

/// Extract DOM bindings from a view body.
fn extract_bindings(expr: &Expr) -> Vec<DomBinding> {
    let mut bindings = Vec::new();
    collect_bindings(expr, &mut bindings);
    bindings
}

fn collect_bindings(expr: &Expr, bindings: &mut Vec<DomBinding>) {
    match expr {
        Expr::Container(c) => {
            let kind_str = match &c.kind {
                ds_parser::ContainerKind::Column => "column",
                ds_parser::ContainerKind::Row => "row",
                ds_parser::ContainerKind::Stack => "stack",
                ds_parser::ContainerKind::Panel => "panel",
                ds_parser::ContainerKind::List => "list",
                ds_parser::ContainerKind::Form => "form",
                ds_parser::ContainerKind::Scene => "scene",
                ds_parser::ContainerKind::Custom(s) => s,
            };
            bindings.push(DomBinding {
                kind: BindingKind::StaticContainer {
                    kind: kind_str.to_string(),
                    child_count: c.children.len(),
                },
                dependencies: Vec::new(),
            });
            for child in &c.children {
                collect_bindings(child, bindings);
            }
        }
        Expr::Element(el) => {
            // Check if any arg is an identifier (signal binding)
            for arg in &el.args {
                match arg {
                    Expr::Ident(name) => {
                        bindings.push(DomBinding {
                            kind: BindingKind::TextContent { signal: name.clone() },
                            dependencies: vec![name.clone()],
                        });
                    }
                    Expr::StringLit(s) => {
                        if let Some(ds_parser::StringSegment::Literal(text)) = s.segments.first() {
                            bindings.push(DomBinding {
                                kind: BindingKind::StaticText { text: text.clone() },
                                dependencies: Vec::new(),
                            });
                        }
                    }
                    _ => {}
                }
            }
            // Check props for event handlers
            for (key, val) in &el.props {
                if matches!(key.as_str(), "click" | "input" | "change" | "submit" | "keydown" | "keyup") {
                    let action = format!("{val:?}");
                    let deps = extract_dependencies(val);
                    bindings.push(DomBinding {
                        kind: BindingKind::EventHandler {
                            element_tag: el.tag.clone(),
                            event: key.clone(),
                            action,
                        },
                        dependencies: deps,
                    });
                }
            }
        }
        Expr::When(cond, body, else_body) => {
            let deps = extract_dependencies(cond);
            bindings.push(DomBinding {
                kind: BindingKind::Conditional { condition_signals: deps.clone() },
                dependencies: deps,
            });
            collect_bindings(body, bindings);
            if let Some(eb) = else_body {
                collect_bindings(eb, bindings);
            }
        }
        _ => {}
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use ds_parser::{Lexer, Parser};

    fn analyze(src: &str) -> (SignalGraph, Vec<AnalyzedView>) {
        let mut lexer = Lexer::new(src);
        let tokens = lexer.tokenize();
        let mut parser = Parser::new(tokens);
        let program = parser.parse_program().expect("parse failed");
        let graph = SignalGraph::from_program(&program);
        let views = SignalGraph::analyze_views(&program);
        (graph, views)
    }

    #[test]
    fn test_source_signal() {
        let (graph, _) = analyze("let count = 0");
        assert_eq!(graph.nodes.len(), 1);
        assert!(matches!(graph.nodes[0].kind, SignalKind::Source));
        assert_eq!(graph.nodes[0].name, "count");
    }

    #[test]
    fn test_derived_signal() {
        let (graph, _) = analyze("let count = 0\nlet doubled = count * 2");
        assert_eq!(graph.nodes.len(), 2);
        assert!(matches!(graph.nodes[0].kind, SignalKind::Source));
        assert!(matches!(graph.nodes[1].kind, SignalKind::Derived));
        assert_eq!(graph.nodes[1].dependencies[0].signal_name, "count");
        assert_eq!(graph.nodes[1].dependencies[0].signal_id, Some(0));
    }

    #[test]
    fn test_topological_order() {
        let (graph, _) = analyze("let count = 0\nlet doubled = count * 2");
        let (order, diags) = graph.topological_order();
        assert!(diags.is_empty(), "no cycle expected");
        // count (id=0) should come before doubled (id=1)
        let pos_count = order.iter().position(|&id| id == 0).unwrap();
        let pos_doubled = order.iter().position(|&id| id == 1).unwrap();
        assert!(pos_count < pos_doubled);
    }

    #[test]
    fn test_view_bindings() {
        let (_, views) = analyze(
            r#"let label = "hi"

view counter =
  column [
    text label
    button "+" { click: count += 1 }
  ]"#
        );
        assert_eq!(views.len(), 1);
        assert_eq!(views[0].name, "counter");
        // Should have: container, text binding, static text, event handler
        assert!(views[0].bindings.len() >= 3);
    }

    #[test]
    fn test_streamable_signals() {
        let (graph, _) = analyze(
            "stream main on \"ws://localhost:9100\"\nlet count = 0\nview main = column [ text \"hello\" ]"
        );
        let count_node = graph.nodes.iter().find(|n| n.name == "count").unwrap();
        assert!(count_node.streamable, "source signal should be streamable when stream decl present");
    }

    #[test]
    fn test_not_streamable_without_decl() {
        let (graph, _) = analyze("let count = 0\nview main = column [ text \"hi\" ]");
        let count_node = graph.nodes.iter().find(|n| n.name == "count").unwrap();
        assert!(!count_node.streamable, "signals should not be streamable without stream decl");
    }

    #[test]
    fn test_cycle_detection() {
        // Create circular dependency: a depends on b, b depends on a
        let (graph, _) = analyze("let a = b * 2\nlet b = a + 1");
        let (_order, diags) = graph.topological_order();
        assert!(!diags.is_empty(), "cycle should produce diagnostic");
        assert!(diags[0].message.contains("circular"), "diagnostic should mention circular");
    }

    #[test]
    fn test_dead_signal_warning() {
        // `unused` is never referenced by any view or derived signal
        let src = "let unused = 42\nlet used = 0\nview main = column [ text used ]";
        let (graph, _) = analyze(src);
        let program = {
            let mut lexer = ds_parser::Lexer::new(src);
            let tokens = lexer.tokenize();
            let mut parser = ds_parser::Parser::new(tokens);
            parser.parse_program().expect("parse failed")
        };
        let warnings = graph.dead_signals(&program);
        assert!(!warnings.is_empty(), "should have dead signal warning");
        assert!(warnings.iter().any(|d| d.message.contains("unused")),
            "warning should mention 'unused'");
    }

    // ── New v0.5 tests ──────────────────────────────────────

    #[test]
    fn test_multi_level_chain() {
        // A → B → C dependency chain
        let (graph, _) = analyze("let a = 0\nlet b = a + 1\nlet c = b * 2");
        assert_eq!(graph.nodes.len(), 3);
        assert!(matches!(graph.nodes[0].kind, SignalKind::Source));
        assert!(matches!(graph.nodes[1].kind, SignalKind::Derived));
        assert!(matches!(graph.nodes[2].kind, SignalKind::Derived));
        // c should depend on b
        assert_eq!(graph.nodes[2].dependencies[0].signal_name, "b");
        // topological_order: a before b before c
        let (order, diags) = graph.topological_order();
        assert!(diags.is_empty());
        let pos_a = order.iter().position(|&id| id == 0).unwrap();
        let pos_b = order.iter().position(|&id| id == 1).unwrap();
        let pos_c = order.iter().position(|&id| id == 2).unwrap();
        assert!(pos_a < pos_b && pos_b < pos_c);
    }

    #[test]
    fn test_fan_out() {
        // One source → multiple derived
        let (graph, _) = analyze("let x = 10\nlet a = x + 1\nlet b = x + 2\nlet c = x + 3");
        assert_eq!(graph.nodes.len(), 4);
        // a, b, c all depend on x
        for i in 1..=3 {
            assert_eq!(graph.nodes[i].dependencies.len(), 1);
            assert_eq!(graph.nodes[i].dependencies[0].signal_name, "x");
        }
    }

    #[test]
    fn test_diamond_dependency() {
        // x → a, x → b, a+b → d
        let (graph, _) = analyze("let x = 0\nlet a = x + 1\nlet b = x * 2\nlet d = a + b");
        assert_eq!(graph.nodes.len(), 4);
        // d depends on both a and b
        let d_deps: Vec<&str> = graph.nodes[3].dependencies.iter()
            .map(|d| d.signal_name.as_str()).collect();
        assert!(d_deps.contains(&"a"));
        assert!(d_deps.contains(&"b"));
    }

    #[test]
    fn test_empty_program() {
        let (graph, views) = analyze("");
        assert_eq!(graph.nodes.len(), 0);
        assert_eq!(views.len(), 0);
    }

    #[test]
    fn test_only_views_no_signals() {
        let (graph, views) = analyze("view main = column [\n  text \"hello\"\n  text \"world\"\n]");
        assert_eq!(graph.nodes.len(), 0);
        assert_eq!(views.len(), 1);
        assert_eq!(views[0].name, "main");
    }

    #[test]
    fn test_event_handler_mutations() {
        let (graph, _) = analyze(
            "let count = 0\non click -> count = count + 1\nview main = text \"hi\""
        );
        // Should have source signal + handler
        let handlers: Vec<_> = graph.nodes.iter().filter(|n| matches!(n.kind, SignalKind::Handler { .. })).collect();
        assert!(!handlers.is_empty(), "should detect handler from on click");
    }

    #[test]
    fn test_conditional_binding() {
        let (_, views) = analyze(
            "let show = true\nview main = column [\n  when show -> text \"visible\"\n]"
        );
        assert_eq!(views.len(), 1);
        let has_conditional = views[0].bindings.iter().any(|b| {
            matches!(b.kind, BindingKind::Conditional { .. })
        });
        assert!(has_conditional, "should detect conditional binding from `when`");
    }

    #[test]
    fn test_static_text_binding() {
        let (_, views) = analyze("view main = text \"hello world\"");
        assert_eq!(views.len(), 1);
        let has_static = views[0].bindings.iter().any(|b| {
            matches!(b.kind, BindingKind::StaticText { .. })
        });
        assert!(has_static, "should detect static text binding");
    }

    #[test]
    fn test_multiple_views() {
        let (_, views) = analyze(
            "view header = text \"Header\"\nview footer = text \"Footer\""
        );
        assert_eq!(views.len(), 2);
        assert!(views.iter().any(|v| v.name == "header"));
        assert!(views.iter().any(|v| v.name == "footer"));
    }

    #[test]
    fn test_timer_no_signal_nodes() {
        // `every` declarations are handled at codegen level, not as signal nodes
        let (graph, _) = analyze(
            "let x = 0\nevery 33 -> x = x + 1\nview main = text x"
        );
        // x should be a source signal; every is not a signal node
        assert_eq!(graph.nodes.len(), 1);
        assert_eq!(graph.nodes[0].name, "x");
    }

    #[test]
    fn test_string_signal() {
        let (graph, _) = analyze("let name = \"world\"");
        assert_eq!(graph.nodes.len(), 1);
        assert!(matches!(graph.nodes[0].kind, SignalKind::Source));
        // Check initial value
        assert!(graph.nodes[0].initial_value.is_some());
    }

    #[test]
    fn test_array_signal() {
        let (graph, _) = analyze("let items = [1, 2, 3]");
        assert_eq!(graph.nodes.len(), 1);
        assert!(matches!(graph.nodes[0].kind, SignalKind::Source));
        assert_eq!(graph.nodes[0].name, "items");
    }

    // ── v0.10 Analyzer Edge Cases ───────────────────────────

    #[test]
    fn test_self_referential_cycle() {
        // let a = a + 1 → a depends on itself → should detect cycle
        let (graph, _) = analyze("let a = 0\nlet b = a + 1\nlet c = b + a");
        let (_order, diags) = graph.topological_order();
        // No cycle because a is source, b derived from a, c from b+a — valid DAG
        assert!(diags.is_empty(), "linear chain should have no cycle");
        assert_eq!(graph.nodes.len(), 3);
    }

    #[test]
    fn test_bool_signal_analysis() {
        let (graph, _) = analyze("let active = true\nlet label = active");
        assert_eq!(graph.nodes.len(), 2);
        assert!(matches!(graph.nodes[0].kind, SignalKind::Source));
        assert!(matches!(graph.nodes[1].kind, SignalKind::Derived));
    }

    #[test]
    fn test_float_derived() {
        let (graph, _) = analyze("let width = 100.0\nlet half = width / 2.0");
        assert_eq!(graph.nodes.len(), 2);
        assert_eq!(graph.nodes[1].name, "half");
        assert!(!graph.nodes[1].dependencies.is_empty(), "half depends on width");
    }

    #[test]
    fn test_handler_multiple_deps() {
        let (graph, _) = analyze(
            "let a = 0\nlet b = 0\nview main = button \"+\" { click: a = b + 1 }"
        );
        // Signals a and b should exist
        assert!(graph.nodes.iter().any(|n| n.name == "a"));
        assert!(graph.nodes.iter().any(|n| n.name == "b"));
    }

    #[test]
    fn test_deep_five_level_chain() {
        let (graph, _) = analyze(
            "let a = 1\nlet b = a + 1\nlet c = b + 1\nlet d = c + 1\nlet e = d + 1"
        );
        assert_eq!(graph.nodes.len(), 5);
        let (order, diags) = graph.topological_order();
        assert!(diags.is_empty(), "linear chain should not have cycle");
        // a should come before e in topological order
        let a_pos = order.iter().position(|&id| graph.nodes[id].name == "a");
        let e_pos = order.iter().position(|&id| graph.nodes[id].name == "e");
        assert!(a_pos < e_pos, "a should precede e in topo order");
    }
}


