/// DreamStack Incremental Compiler
///
/// Diffs successive compilations to emit minimal JS patches when only
/// signal values change. Falls back to full recompile when structural
/// changes (new/removed declarations, view changes) are detected.

use ds_parser::{Program, Declaration, LetDecl, Lexer, Parser, TokenKind};
use ds_analyzer::SignalGraph;
use ds_codegen::JsEmitter;

/// Result of an incremental compilation.
pub enum IncrementalResult {
    /// Full HTML — either first compile or structural change detected.
    Full(String),
    /// JS patch — only signal value updates, can be eval'd in-place.
    Patch(String),
    /// Compile error.
    Error(String),
}

/// Delta between two programs.
#[derive(Debug)]
pub struct ProgramDelta {
    /// Signals whose initial value expressions changed.
    pub changed_signals: Vec<ChangedSignal>,
    /// Whether structural changes require a full recompile.
    pub needs_full_recompile: bool,
}

/// A signal with a changed initial value.
#[derive(Debug)]
pub struct ChangedSignal {
    pub name: String,
    pub new_expr_js: String,
}

/// Stateful incremental compiler. Holds the previous program AST
/// to diff against.
pub struct IncrementalCompiler {
    prev_source: Option<String>,
    prev_program: Option<Program>,
}

impl IncrementalCompiler {
    pub fn new() -> Self {
        IncrementalCompiler {
            prev_source: None,
            prev_program: None,
        }
    }

    /// Parse source into a Program, returning an error string on failure.
    fn parse_source(source: &str) -> Result<Program, String> {
        let mut lexer = Lexer::new(source);
        let tokens = lexer.tokenize();

        // Check for lexer errors
        let mut errors = Vec::new();
        for tok in &tokens {
            if let TokenKind::Error(msg) = &tok.kind {
                errors.push(format!("Lexer error at line {}: {}", tok.line, msg));
            }
        }
        if !errors.is_empty() {
            return Err(errors.join("\n"));
        }

        let mut parser = Parser::with_source(tokens, source);
        let result = parser.parse_program_resilient();

        if !result.errors.is_empty() {
            let error_msgs: Vec<String> = result.errors.iter()
                .map(|e| e.to_string())
                .collect();
            return Err(error_msgs.join("\n"));
        }

        Ok(result.program)
    }

    /// Full compilation pipeline.
    fn full_compile(program: &Program) -> String {
        let graph = SignalGraph::from_program(program);
        let views = SignalGraph::analyze_views(program);
        JsEmitter::emit_html(program, &graph, &views, false)
    }

    /// Compile source code, returning either a full HTML recompile or a JS patch.
    pub fn compile(&mut self, source: &str) -> IncrementalResult {
        // Fast path: source unchanged
        if self.prev_source.as_deref() == Some(source) {
            return IncrementalResult::Patch(String::new());
        }

        // Parse
        let program = match Self::parse_source(source) {
            Ok(p) => p,
            Err(e) => return IncrementalResult::Error(e),
        };

        // Check if we can do incremental
        let result = if let Some(prev) = &self.prev_program {
            let delta = Self::diff_programs(prev, &program);
            if delta.needs_full_recompile {
                IncrementalResult::Full(Self::full_compile(&program))
            } else if delta.changed_signals.is_empty() {
                // No changes at all — return empty patch
                IncrementalResult::Patch(String::new())
            } else {
                // Only signal values changed → emit patch
                IncrementalResult::Patch(Self::emit_patch(&delta))
            }
        } else {
            // First compile — always full
            IncrementalResult::Full(Self::full_compile(&program))
        };

        // Save state
        self.prev_source = Some(source.to_string());
        self.prev_program = Some(program);

        result
    }

    /// Diff two programs. Detects signal value changes and structural changes.
    fn diff_programs(old: &Program, new: &Program) -> ProgramDelta {
        // If declaration count differs, structural change
        if old.declarations.len() != new.declarations.len() {
            return ProgramDelta {
                changed_signals: vec![],
                needs_full_recompile: true,
            };
        }

        // Check if declaration types match (structure preserved)
        for (o, n) in old.declarations.iter().zip(new.declarations.iter()) {
            if std::mem::discriminant(o) != std::mem::discriminant(n) {
                return ProgramDelta {
                    changed_signals: vec![],
                    needs_full_recompile: true,
                };
            }
        }

        let old_lets = Self::extract_lets(old);
        let new_lets = Self::extract_lets(new);

        // Compare let declarations for value changes
        let mut changed = Vec::new();

        for (name, old_expr_dbg) in &old_lets {
            // Find matching new let
            if let Some((_, new_expr_dbg)) = new_lets.iter().find(|(n, _)| n == name) {
                if old_expr_dbg != new_expr_dbg {
                    // Signal value changed — find the new expression and emit JS
                    for decl in &new.declarations {
                        if let Declaration::Let(LetDecl { name: n, value, .. }) = decl {
                            if n == name {
                                let emitter = JsEmitter::default();
                                let expr_js = emitter.emit_expr(value);
                                changed.push(ChangedSignal {
                                    name: name.clone(),
                                    new_expr_js: expr_js,
                                });
                                break;
                            }
                        }
                    }
                }
            } else {
                // Signal removed → full recompile
                return ProgramDelta {
                    changed_signals: vec![],
                    needs_full_recompile: true,
                };
            }
        }

        // Check for new signals
        for (name, _) in &new_lets {
            if !old_lets.iter().any(|(n, _)| n == name) {
                return ProgramDelta {
                    changed_signals: vec![],
                    needs_full_recompile: true,
                };
            }
        }

        // Check if any view declarations changed (using Debug repr as cheap diff)
        let old_views: Vec<String> = old.declarations.iter()
            .filter_map(|d| if let Declaration::View(v) = d { Some(format!("{:?}", v)) } else { None })
            .collect();
        let new_views: Vec<String> = new.declarations.iter()
            .filter_map(|d| if let Declaration::View(v) = d { Some(format!("{:?}", v)) } else { None })
            .collect();

        let needs_full = old_views != new_views;

        ProgramDelta {
            changed_signals: changed,
            needs_full_recompile: needs_full,
        }
    }

    /// Extract let declarations as (name, debug_string) pairs for comparison.
    fn extract_lets(program: &Program) -> Vec<(String, String)> {
        program.declarations.iter()
            .filter_map(|d| {
                if let Declaration::Let(LetDecl { name, value, .. }) = d {
                    Some((name.clone(), format!("{:?}", value)))
                } else {
                    None
                }
            })
            .collect()
    }

    /// Emit a JS patch that updates signal values without full page reload.
    fn emit_patch(delta: &ProgramDelta) -> String {
        let mut js = String::from("// DreamStack incremental patch\n");
        for sig in &delta.changed_signals {
            js.push_str(&format!(
                "if (typeof DS !== 'undefined' && DS.signals && DS.signals.{name}) {{ DS.signals.{name}.value = {expr}; }}\n",
                name = sig.name,
                expr = sig.new_expr_js,
            ));
        }
        js
    }
}

impl Default for IncrementalCompiler {
    fn default() -> Self {
        Self::new()
    }
}

// ─── v0.7: Incremental Compilation Extensions ───

pub struct IncrementalExt {
    file_mtimes: Vec<(String, u64)>,
    dep_graph: Vec<(String, Vec<String>)>,
    dirty: Vec<String>,
    cache_hits: u64,
    cache_misses: u64,
    snapshots: Vec<Vec<String>>,
    priorities: Vec<(String, u32)>,
}

impl IncrementalExt {
    pub fn new() -> Self { IncrementalExt { file_mtimes: Vec::new(), dep_graph: Vec::new(), dirty: Vec::new(), cache_hits: 0, cache_misses: 0, snapshots: Vec::new(), priorities: Vec::new() } }

    pub fn set_mtime(&mut self, file: &str, mtime: u64) {
        if let Some(entry) = self.file_mtimes.iter_mut().find(|(f, _)| f == file) { entry.1 = mtime; }
        else { self.file_mtimes.push((file.to_string(), mtime)); }
    }

    pub fn add_dependency(&mut self, file: &str, dep: &str) {
        if let Some(entry) = self.dep_graph.iter_mut().find(|(f, _)| f == file) { entry.1.push(dep.to_string()); }
        else { self.dep_graph.push((file.to_string(), vec![dep.to_string()])); }
    }

    pub fn invalidate(&mut self, file: &str) {
        if !self.dirty.contains(&file.to_string()) { self.dirty.push(file.to_string()); }
        // Also invalidate dependents
        let dependents: Vec<String> = self.dep_graph.iter()
            .filter(|(_, deps)| deps.contains(&file.to_string()))
            .map(|(f, _)| f.clone()).collect();
        for dep in dependents { if !self.dirty.contains(&dep) { self.dirty.push(dep); } }
    }

    pub fn is_dirty(&self, file: &str) -> bool { self.dirty.contains(&file.to_string()) }
    pub fn dirty_count(&self) -> usize { self.dirty.len() }
    pub fn cache_hit(&mut self) { self.cache_hits += 1; }
    pub fn cache_miss(&mut self) { self.cache_misses += 1; }
    pub fn hit_rate(&self) -> f64 { let total = self.cache_hits + self.cache_misses; if total == 0 { 0.0 } else { self.cache_hits as f64 / total as f64 } }

    pub fn snapshot(&mut self) { self.snapshots.push(self.dirty.clone()); }
    pub fn restore(&mut self) -> bool { if let Some(snap) = self.snapshots.pop() { self.dirty = snap; true } else { false } }

    pub fn set_priority(&mut self, file: &str, priority: u32) { self.priorities.push((file.to_string(), priority)); }
    pub fn gc(&mut self) { self.dirty.clear(); }
    pub fn file_count(&self) -> usize { self.file_mtimes.len() }
}

impl Default for IncrementalExt { fn default() -> Self { Self::new() } }

// ─── v0.8: Advanced Incremental ───

pub struct IncrementalV2 {
    hashes: Vec<(String, u64)>,
    module_graph: Vec<(String, Vec<String>)>,
    compile_queue: Vec<String>,
    error_cache: Vec<(String, Vec<String>)>,
    metrics: Vec<(String, f64)>,
    cancelled: bool,
}

impl IncrementalV2 {
    pub fn new() -> Self { IncrementalV2 { hashes: Vec::new(), module_graph: Vec::new(), compile_queue: Vec::new(), error_cache: Vec::new(), metrics: Vec::new(), cancelled: false } }
    pub fn set_hash(&mut self, file: &str, hash: u64) {
        if let Some(e) = self.hashes.iter_mut().find(|(f, _)| f == file) { e.1 = hash; }
        else { self.hashes.push((file.to_string(), hash)); }
    }
    pub fn has_changed(&self, file: &str, new_hash: u64) -> bool { self.hashes.iter().find(|(f, _)| f == file).map(|(_, h)| *h != new_hash).unwrap_or(true) }
    pub fn add_module(&mut self, name: &str, deps: Vec<&str>) { self.module_graph.push((name.to_string(), deps.into_iter().map(str::to_string).collect())); }
    pub fn enqueue(&mut self, file: &str) { if !self.compile_queue.contains(&file.to_string()) { self.compile_queue.push(file.to_string()); } }
    pub fn dequeue(&mut self) -> Option<String> { if self.compile_queue.is_empty() { None } else { Some(self.compile_queue.remove(0)) } }
    pub fn queue_len(&self) -> usize { self.compile_queue.len() }
    pub fn cache_errors(&mut self, file: &str, errors: Vec<&str>) { self.error_cache.push((file.to_string(), errors.into_iter().map(str::to_string).collect())); }
    pub fn get_errors(&self, file: &str) -> Vec<String> { self.error_cache.iter().find(|(f, _)| f == file).map(|(_, e)| e.clone()).unwrap_or_default() }
    pub fn record_metric(&mut self, file: &str, ms: f64) { self.metrics.push((file.to_string(), ms)); }
    pub fn avg_compile_time(&self) -> f64 { if self.metrics.is_empty() { 0.0 } else { self.metrics.iter().map(|(_, t)| t).sum::<f64>() / self.metrics.len() as f64 } }
    pub fn cancel(&mut self) { self.cancelled = true; }
    pub fn is_cancelled(&self) -> bool { self.cancelled }
    pub fn module_count(&self) -> usize { self.module_graph.len() }
}

impl Default for IncrementalV2 { fn default() -> Self { Self::new() } }

// ─── v0.9: Production Build ───

#[derive(Debug, Clone, PartialEq)]
pub enum BuildProfile { Debug, Release, Test }

#[derive(Debug, Clone, PartialEq)]
pub enum RebuildStrategy { Full, Incremental, Clean }

pub struct BuildPipeline {
    profile: BuildProfile,
    strategy: RebuildStrategy,
    artifacts: Vec<(String, Vec<String>)>, // (input, outputs)
    source_maps: Vec<(String, String)>, // (file, map_data)
    workers: u32,
    progress: Vec<(String, f64)>, // (stage, percent)
    dep_versions: Vec<(String, String)>,
    fingerprint: Option<u64>,
}

impl BuildPipeline {
    pub fn new(profile: BuildProfile) -> Self { BuildPipeline { profile, strategy: RebuildStrategy::Incremental, artifacts: Vec::new(), source_maps: Vec::new(), workers: 1, progress: Vec::new(), dep_versions: Vec::new(), fingerprint: None } }
    pub fn set_strategy(&mut self, s: RebuildStrategy) { self.strategy = s; }
    pub fn set_workers(&mut self, n: u32) { self.workers = n.max(1); }
    pub fn add_artifact(&mut self, input: &str, outputs: Vec<&str>) { self.artifacts.push((input.to_string(), outputs.into_iter().map(str::to_string).collect())); }
    pub fn add_source_map(&mut self, file: &str, data: &str) { self.source_maps.push((file.to_string(), data.to_string())); }
    pub fn report_progress(&mut self, stage: &str, pct: f64) { self.progress.push((stage.to_string(), pct.clamp(0.0, 100.0))); }
    pub fn add_dep_version(&mut self, dep: &str, ver: &str) { self.dep_versions.push((dep.to_string(), ver.to_string())); }
    pub fn set_fingerprint(&mut self, fp: u64) { self.fingerprint = Some(fp); }
    pub fn get_artifacts(&self, input: &str) -> Vec<String> { self.artifacts.iter().find(|(i, _)| i == input).map(|(_, o)| o.clone()).unwrap_or_default() }
    pub fn get_source_map(&self, file: &str) -> Option<String> { self.source_maps.iter().find(|(f, _)| f == file).map(|(_, d)| d.clone()) }
    pub fn is_release(&self) -> bool { matches!(self.profile, BuildProfile::Release) }
    pub fn worker_count(&self) -> u32 { self.workers }
    pub fn latest_progress(&self) -> Option<f64> { self.progress.last().map(|(_, p)| *p) }
}

// ─── v1.0: Full Build System ───

pub struct BuildSystem {
    cache_entries: Vec<(String, Vec<u8>)>,
    cache_limit: usize,
    build_graph: Vec<(String, Vec<String>)>,
    telemetry: Vec<(String, String)>,
    plugins: Vec<String>,
    hooks: Vec<(String, String)>, // (phase, hook_name)
    locked_version: Option<String>,
    hermetic: bool,
    healthy: bool,
}

impl BuildSystem {
    pub fn new() -> Self { BuildSystem { cache_entries: Vec::new(), cache_limit: 100, build_graph: Vec::new(), telemetry: Vec::new(), plugins: Vec::new(), hooks: Vec::new(), locked_version: None, hermetic: false, healthy: true } }
    pub fn cache_put(&mut self, key: &str, data: Vec<u8>) { if self.cache_entries.len() >= self.cache_limit { self.cache_entries.remove(0); } self.cache_entries.push((key.to_string(), data)); }
    pub fn cache_get(&self, key: &str) -> Option<&[u8]> { self.cache_entries.iter().find(|(k, _)| k == key).map(|(_, d)| d.as_slice()) }
    pub fn cache_size(&self) -> usize { self.cache_entries.iter().map(|(_, d)| d.len()).sum() }
    pub fn set_cache_limit(&mut self, limit: usize) { self.cache_limit = limit; }
    pub fn evict_lru(&mut self) { if !self.cache_entries.is_empty() { self.cache_entries.remove(0); } }
    pub fn add_dep(&mut self, from: &str, to: Vec<&str>) { self.build_graph.push((from.to_string(), to.into_iter().map(str::to_string).collect())); }
    pub fn critical_path(&self) -> Vec<String> { self.build_graph.iter().max_by_key(|(_, deps)| deps.len()).map(|(n, deps)| { let mut p = vec![n.clone()]; p.extend(deps.iter().cloned()); p }).unwrap_or_default() }
    pub fn log_event(&mut self, event: &str, data: &str) { self.telemetry.push((event.to_string(), data.to_string())); }
    pub fn register_plugin(&mut self, name: &str) { self.plugins.push(name.to_string()); }
    pub fn add_hook(&mut self, phase: &str, hook: &str) { self.hooks.push((phase.to_string(), hook.to_string())); }
    pub fn lock_version(&mut self, ver: &str) { self.locked_version = Some(ver.to_string()); }
    pub fn check_version(&self, ver: &str) -> bool { self.locked_version.as_ref().map(|v| v == ver).unwrap_or(true) }
    pub fn set_hermetic(&mut self, v: bool) { self.hermetic = v; }
    pub fn is_hermetic(&self) -> bool { self.hermetic }
    pub fn plugin_count(&self) -> usize { self.plugins.len() }
    pub fn sign_artifact(content: &[u8]) -> u64 { content.iter().fold(0u64, |acc, b| acc.wrapping_mul(31).wrapping_add(*b as u64)) }
    pub fn health_check(&self) -> bool { self.healthy }
    pub fn report(&self) -> String { format!("cache:{} graph:{} plugins:{} hermetic:{}", self.cache_entries.len(), self.build_graph.len(), self.plugins.len(), self.hermetic) }
}

impl Default for BuildSystem { fn default() -> Self { Self::new() } }

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_first_compile_is_full() {
        let mut compiler = IncrementalCompiler::new();
        let result = compiler.compile("let x = 42\nview main = column [\n  text x\n]");
        assert!(matches!(result, IncrementalResult::Full(_)));
    }

    #[test]
    fn test_same_source_is_empty_patch() {
        let mut compiler = IncrementalCompiler::new();
        let src = "let x = 42\nview main = column [\n  text x\n]";
        compiler.compile(src);
        let result = compiler.compile(src);
        match result {
            IncrementalResult::Patch(js) => assert!(js.is_empty()),
            _ => panic!("expected empty patch for identical source"),
        }
    }

    #[test]
    fn test_signal_value_change_is_patch() {
        let mut compiler = IncrementalCompiler::new();
        let src1 = "let x = 42\nview main = column [\n  text x\n]";
        let src2 = "let x = 99\nview main = column [\n  text x\n]";
        compiler.compile(src1);
        let result = compiler.compile(src2);
        match result {
            IncrementalResult::Patch(js) => {
                assert!(js.contains("DS.signals.x"));
                assert!(js.contains("99"));
            }
            _ => panic!("expected patch for signal value change"),
        }
    }

    #[test]
    fn test_structural_change_is_full() {
        let mut compiler = IncrementalCompiler::new();
        let src1 = "let x = 42\nview main = column [\n  text x\n]";
        let src2 = "let x = 42\nlet y = 10\nview main = column [\n  text x\n]";
        compiler.compile(src1);
        let result = compiler.compile(src2);
        assert!(matches!(result, IncrementalResult::Full(_)));
    }

    #[test]
    fn test_view_change_triggers_full() {
        let mut compiler = IncrementalCompiler::new();
        let src1 = "let x = 42\nview main = column [\n  text x\n]";
        let src2 = "let x = 42\nview main = row [\n  text x\n]";
        compiler.compile(src1);
        let result = compiler.compile(src2);
        assert!(matches!(result, IncrementalResult::Full(_)));
    }

    // ── v0.7 Incremental Compiler Tests ─────────────────────

    #[test]
    fn test_error_then_fix_is_full() {
        let mut compiler = IncrementalCompiler::new();
        let src1 = "let x = 42\nview main = column [\n  text x\n]";
        compiler.compile(src1);
        // Introduce a syntax error
        let bad = "let x = !@#\nview main = column [\n  text x\n]";
        let err_result = compiler.compile(bad);
        assert!(matches!(err_result, IncrementalResult::Error(_)));
        // Fix the error with a DIFFERENT valid source — should trigger full
        // (prev_program is from src1, but the new source has structural changes)
        let src3 = "let x = 42\nlet y = 10\nview main = column [\n  text x\n  text y\n]";
        let result = compiler.compile(src3);
        assert!(matches!(result, IncrementalResult::Full(_)),
            "fixing a syntax error with new structure should trigger full recompile");
    }

    #[test]
    fn test_whitespace_only_change() {
        let mut compiler = IncrementalCompiler::new();
        let src1 = "let x = 42\nview main = column [\n  text x\n]";
        let src2 = "let x = 42\n\nview main = column [\n  text x\n]";
        compiler.compile(src1);
        let result = compiler.compile(src2);
        // Whitespace change in between declarations — source changes but AST stays same
        match result {
            IncrementalResult::Patch(js) => assert!(js.is_empty(), "whitespace-only should be no-op patch"),
            IncrementalResult::Full(_) => {} // also acceptable — parser may differ on spans
            other => panic!("unexpected: {:?}", std::mem::discriminant(&other)),
        }
    }

    #[test]
    fn test_comment_only_change() {
        let mut compiler = IncrementalCompiler::new();
        let src1 = "let x = 42\nview main = column [\n  text x\n]";
        let src2 = "-- a comment\nlet x = 42\nview main = column [\n  text x\n]";
        compiler.compile(src1);
        let result = compiler.compile(src2);
        // Adding a comment shouldn't change signals or views
        match result {
            IncrementalResult::Patch(js) => assert!(js.is_empty(), "comment-only should be no-op"),
            IncrementalResult::Full(_) => {} // also acceptable
            other => panic!("unexpected: {:?}", std::mem::discriminant(&other)),
        }
    }

    #[test]
    fn test_add_signal_is_full() {
        let mut compiler = IncrementalCompiler::new();
        let src1 = "let x = 1\nview main = text x";
        let src2 = "let x = 1\nlet y = 2\nview main = text x";
        compiler.compile(src1);
        let result = compiler.compile(src2);
        assert!(matches!(result, IncrementalResult::Full(_)),
            "adding a signal should trigger full recompile");
    }

    #[test]
    fn test_remove_signal_is_full() {
        let mut compiler = IncrementalCompiler::new();
        let src1 = "let x = 1\nlet y = 2\nview main = text x";
        let src2 = "let x = 1\nview main = text x";
        compiler.compile(src1);
        let result = compiler.compile(src2);
        assert!(matches!(result, IncrementalResult::Full(_)),
            "removing a signal should trigger full recompile");
    }

    #[test]
    fn test_multi_signal_patch() {
        let mut compiler = IncrementalCompiler::new();
        let src1 = "let a = 1\nlet b = 2\nlet c = 3\nview main = column [\n  text a\n  text b\n  text c\n]";
        let src2 = "let a = 10\nlet b = 20\nlet c = 3\nview main = column [\n  text a\n  text b\n  text c\n]";
        compiler.compile(src1);
        let result = compiler.compile(src2);
        match result {
            IncrementalResult::Patch(js) => {
                assert!(js.contains("DS.signals.a"), "should patch signal a");
                assert!(js.contains("DS.signals.b"), "should patch signal b");
                assert!(!js.contains("DS.signals.c"), "should NOT patch unchanged signal c");
            }
            other => panic!("expected patch for multi-signal change, got {:?}", std::mem::discriminant(&other)),
        }
    }

    #[test]
    fn test_large_program_incremental() {
        let mut compiler = IncrementalCompiler::new();
        let base = "let s0 = 0\nlet s1 = 1\nlet s2 = 2\nlet s3 = 3\nlet s4 = 4\n\
                    let s5 = 5\nlet s6 = 6\nlet s7 = 7\nlet s8 = 8\nlet s9 = 9\n\
                    view main = column [\n  text s0\n  text s1\n  text s9\n]";
        compiler.compile(base);
        // Change only one signal in a 10-signal program
        let modified = base.replace("let s5 = 5", "let s5 = 55");
        let result = compiler.compile(&modified);
        match result {
            IncrementalResult::Patch(js) => {
                assert!(js.contains("DS.signals.s5"), "should patch only s5");
                assert!(js.contains("55"), "should contain new value");
            }
            other => panic!("expected patch, got {:?}", std::mem::discriminant(&other)),
        }
    }

    // ─── v0.7 Tests ───

    #[test]
    fn test_file_mtime() { let mut ic = IncrementalExt::new(); ic.set_mtime("a.ds", 100); assert_eq!(ic.file_count(), 1); }

    #[test]
    fn test_dependency() { let mut ic = IncrementalExt::new(); ic.add_dependency("a.ds", "b.ds"); ic.invalidate("b.ds"); assert!(ic.is_dirty("a.ds")); }

    #[test]
    fn test_cache_stats() { let mut ic = IncrementalExt::new(); ic.cache_hit(); ic.cache_hit(); ic.cache_miss(); assert!((ic.hit_rate() - 0.666).abs() < 0.01); }

    #[test]
    fn test_snapshot() { let mut ic = IncrementalExt::new(); ic.invalidate("a.ds"); ic.snapshot(); ic.gc(); assert_eq!(ic.dirty_count(), 0); assert!(ic.restore()); assert_eq!(ic.dirty_count(), 1); }

    #[test]
    fn test_gc() { let mut ic = IncrementalExt::new(); ic.invalidate("a.ds"); ic.invalidate("b.ds"); ic.gc(); assert_eq!(ic.dirty_count(), 0); }

    #[test]
    fn test_dirty_count() { let mut ic = IncrementalExt::new(); ic.invalidate("x.ds"); assert_eq!(ic.dirty_count(), 1); }

    #[test]
    fn test_cascade_invalidate() { let mut ic = IncrementalExt::new(); ic.add_dependency("main.ds", "util.ds"); ic.invalidate("util.ds"); assert!(ic.is_dirty("util.ds")); assert!(ic.is_dirty("main.ds")); }

    #[test]
    fn test_priority() { let mut ic = IncrementalExt::new(); ic.set_priority("hot.ds", 10); }

    #[test]
    fn test_no_restore() { let mut ic = IncrementalExt::new(); assert!(!ic.restore()); }

    // ─── v0.8 Tests ───

    #[test]
    fn test_hash_change() { let mut ic = IncrementalV2::new(); ic.set_hash("a.ds", 100); assert!(!ic.has_changed("a.ds", 100)); assert!(ic.has_changed("a.ds", 200)); }

    #[test]
    fn test_hash_new_file() { let ic = IncrementalV2::new(); assert!(ic.has_changed("new.ds", 100)); }

    #[test]
    fn test_compile_queue() { let mut ic = IncrementalV2::new(); ic.enqueue("a.ds"); ic.enqueue("b.ds"); assert_eq!(ic.queue_len(), 2); assert_eq!(ic.dequeue(), Some("a.ds".into())); assert_eq!(ic.queue_len(), 1); }

    #[test]
    fn test_error_cache() { let mut ic = IncrementalV2::new(); ic.cache_errors("a.ds", vec!["bad syntax"]); assert_eq!(ic.get_errors("a.ds").len(), 1); assert!(ic.get_errors("b.ds").is_empty()); }

    #[test]
    fn test_metrics() { let mut ic = IncrementalV2::new(); ic.record_metric("a.ds", 10.0); ic.record_metric("b.ds", 20.0); assert!((ic.avg_compile_time() - 15.0).abs() < 0.01); }

    #[test]
    fn test_cancel() { let mut ic = IncrementalV2::new(); assert!(!ic.is_cancelled()); ic.cancel(); assert!(ic.is_cancelled()); }

    #[test]
    fn test_module_graph() { let mut ic = IncrementalV2::new(); ic.add_module("app", vec!["utils", "ui"]); assert_eq!(ic.module_count(), 1); }

    #[test]
    fn test_dequeue_empty() { let mut ic = IncrementalV2::new(); assert_eq!(ic.dequeue(), None); }

    #[test]
    fn test_no_dup_enqueue() { let mut ic = IncrementalV2::new(); ic.enqueue("a.ds"); ic.enqueue("a.ds"); assert_eq!(ic.queue_len(), 1); }

    // ─── v0.9 Tests ───

    #[test]
    fn test_build_profile() { let bp = BuildPipeline::new(BuildProfile::Release); assert!(bp.is_release()); }

    #[test]
    fn test_build_debug() { let bp = BuildPipeline::new(BuildProfile::Debug); assert!(!bp.is_release()); }

    #[test]
    fn test_workers() { let mut bp = BuildPipeline::new(BuildProfile::Debug); bp.set_workers(4); assert_eq!(bp.worker_count(), 4); }

    #[test]
    fn test_workers_min() { let mut bp = BuildPipeline::new(BuildProfile::Debug); bp.set_workers(0); assert_eq!(bp.worker_count(), 1); }

    #[test]
    fn test_artifacts() { let mut bp = BuildPipeline::new(BuildProfile::Debug); bp.add_artifact("app.ds", vec!["app.js", "app.css"]); assert_eq!(bp.get_artifacts("app.ds").len(), 2); assert!(bp.get_artifacts("none.ds").is_empty()); }

    #[test]
    fn test_source_maps() { let mut bp = BuildPipeline::new(BuildProfile::Debug); bp.add_source_map("app.js", "map_data"); assert_eq!(bp.get_source_map("app.js"), Some("map_data".into())); }

    #[test]
    fn test_progress() { let mut bp = BuildPipeline::new(BuildProfile::Debug); bp.report_progress("parse", 50.0); assert_eq!(bp.latest_progress(), Some(50.0)); }

    #[test]
    fn test_strategy() { let mut bp = BuildPipeline::new(BuildProfile::Debug); bp.set_strategy(RebuildStrategy::Clean); }

    #[test]
    fn test_fingerprint() { let mut bp = BuildPipeline::new(BuildProfile::Debug); bp.set_fingerprint(12345); }

    // ─── v1.0 Tests ───

    #[test]
    fn test_cache_put_get() { let mut bs = BuildSystem::new(); bs.cache_put("a.ds", vec![1,2,3]); assert_eq!(bs.cache_get("a.ds"), Some([1u8,2,3].as_slice())); }
    #[test]
    fn test_cache_miss() { let bs = BuildSystem::new(); assert_eq!(bs.cache_get("nothing"), None); }
    #[test]
    fn test_cache_eviction() { let mut bs = BuildSystem::new(); bs.set_cache_limit(2); bs.cache_put("a", vec![1]); bs.cache_put("b", vec![2]); bs.cache_put("c", vec![3]); assert_eq!(bs.cache_get("a"), None); assert!(bs.cache_get("c").is_some()); }
    #[test]
    fn test_cache_size() { let mut bs = BuildSystem::new(); bs.cache_put("a", vec![1,2]); bs.cache_put("b", vec![3]); assert_eq!(bs.cache_size(), 3); }
    #[test]
    fn test_build_graph() { let mut bs = BuildSystem::new(); bs.add_dep("app", vec!["utils", "ui"]); assert_eq!(bs.critical_path().len(), 3); }
    #[test]
    fn test_plugins() { let mut bs = BuildSystem::new(); bs.register_plugin("minify"); bs.register_plugin("compress"); assert_eq!(bs.plugin_count(), 2); }
    #[test]
    fn test_hooks() { let mut bs = BuildSystem::new(); bs.add_hook("pre-build", "lint"); }
    #[test]
    fn test_version_lock() { let mut bs = BuildSystem::new(); bs.lock_version("1.0.0"); assert!(bs.check_version("1.0.0")); assert!(!bs.check_version("0.9.0")); }
    #[test]
    fn test_no_version_lock() { let bs = BuildSystem::new(); assert!(bs.check_version("anything")); }
    #[test]
    fn test_hermetic() { let mut bs = BuildSystem::new(); bs.set_hermetic(true); assert!(bs.is_hermetic()); }
    #[test]
    fn test_not_hermetic() { let bs = BuildSystem::new(); assert!(!bs.is_hermetic()); }
    #[test]
    fn test_sign_artifact() { let sig = BuildSystem::sign_artifact(&[1,2,3]); assert_ne!(sig, 0); }
    #[test]
    fn test_health() { let bs = BuildSystem::new(); assert!(bs.health_check()); }
    #[test]
    fn test_telemetry() { let mut bs = BuildSystem::new(); bs.log_event("build_start", "t=0"); }
    #[test]
    fn test_evict_lru() { let mut bs = BuildSystem::new(); bs.cache_put("a", vec![1]); bs.evict_lru(); assert_eq!(bs.cache_get("a"), None); }
    #[test]
    fn test_report_v1() { let bs = BuildSystem::new(); assert!(bs.report().contains("cache:0")); }
    #[test]
    fn test_empty_critical_path() { let bs = BuildSystem::new(); assert!(bs.critical_path().is_empty()); }
    #[test]
    fn test_sign_empty() { assert_eq!(BuildSystem::sign_artifact(&[]), 0); }
}

