//! Script-kompilator for orkestreringer.
//!
//! Parser menneskelig scriptspråk og kompilerer til tekniske CLI-kall.
//! Brukerens "transkriber lydfilen (stor modell)" blir til
//! "synops-transcribe --cas-hash {event.cas_hash} --model large".
//!
//! Kompilatoren er deterministisk — ingen AI, ingen nettverkskall.
//! Matchingen bruker `cli_tool`-noders `aliases` og `args_hints`.
//!
//! Ref: docs/concepts/orkestrering.md § 4 "To lag"

use serde::{Deserialize, Serialize};
use std::collections::HashMap;

// =============================================================================
// Verktøyregister — representerer cli_tool-noder fra PG
// =============================================================================

/// Et CLI-verktøy hentet fra en `cli_tool`-node i grafen.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ToolDef {
    /// Binærnavn (f.eks. "synops-transcribe")
    pub binary: String,
    /// Norske alias-verb som brukeren kan skrive (f.eks. ["transkriber", "transkribering"])
    pub aliases: Vec<String>,
    /// Beskrivelse for feilmeldinger
    pub description: String,
    /// Mapping fra menneskelige argumenter til CLI-flagg/variabler
    /// F.eks. "stor modell" → "--model large", "lydfilen" → "{event.cas_hash}"
    pub args_hints: HashMap<String, String>,
}

/// Samling av tilgjengelige verktøy (bygget fra cli_tool-noder i PG).
#[derive(Debug, Clone, Default)]
pub struct ToolRegistry {
    pub tools: Vec<ToolDef>,
}

impl ToolRegistry {
    /// Bygg et register fra cli_tool-noders metadata (JSONB).
    pub fn from_metadata(rows: &[(serde_json::Value,)]) -> Self {
        let tools = rows
            .iter()
            .filter_map(|(meta,)| {
                let binary = meta.get("binary")?.as_str()?.to_string();
                let aliases: Vec<String> = meta
                    .get("aliases")?
                    .as_array()?
                    .iter()
                    .filter_map(|v| v.as_str().map(|s| s.to_string()))
                    .collect();
                let description = meta
                    .get("description")
                    .and_then(|v| v.as_str())
                    .unwrap_or("")
                    .to_string();
                let args_hints: HashMap<String, String> = meta
                    .get("args_hints")
                    .and_then(|v| v.as_object())
                    .map(|obj| {
                        obj.iter()
                            .filter_map(|(k, v)| v.as_str().map(|s| (k.clone(), s.to_string())))
                            .collect()
                    })
                    .unwrap_or_default();
                Some(ToolDef {
                    binary,
                    aliases,
                    description,
                    args_hints,
                })
            })
            .collect();
        ToolRegistry { tools }
    }

    /// Finn verktøy basert på et verb (case-insensitive).
    fn find_by_verb(&self, verb: &str) -> Option<&ToolDef> {
        let lower = verb.to_lowercase();
        self.tools.iter().find(|t| {
            t.aliases.iter().any(|a| a.to_lowercase() == lower)
                || t.binary.to_lowercase() == lower
        })
    }

    /// Alle tilgjengelige verb (for feilmeldinger).
    fn all_aliases(&self) -> Vec<&str> {
        self.tools
            .iter()
            .flat_map(|t| t.aliases.iter().map(|a| a.as_str()))
            .collect()
    }

    /// Fuzzy-match: finn nærmeste alias til et ukjent verb.
    fn suggest(&self, verb: &str) -> Option<String> {
        let lower = verb.to_lowercase();
        let mut best: Option<(&str, usize)> = None;
        for alias in self.all_aliases() {
            let dist = levenshtein(&lower, &alias.to_lowercase());
            if dist <= 3 {
                if best.is_none() || dist < best.unwrap().1 {
                    best = Some((alias, dist));
                }
            }
        }
        best.map(|(a, _)| a.to_string())
    }
}

// =============================================================================
// AST — parsert representasjon av menneskelig script
// =============================================================================

/// Et parsert steg i scriptet.
#[derive(Debug, Clone)]
pub struct ParsedStep {
    /// Linjenummer i kildekoden (1-indeksert)
    pub line_number: usize,
    /// Stegnummer (1, 2, 3 ...)
    pub step_number: usize,
    /// Verbet brukeren skrev (f.eks. "transkriber")
    pub verb: String,
    /// Objektet etter verbet (f.eks. "lydfilen")
    pub object: String,
    /// Argumenter i parentes (f.eks. ["stor modell"])
    pub args: Vec<String>,
    /// Fallback ved feil (valgfri)
    pub fallback: Option<Box<ParsedStep>>,
    /// Råteksten slik brukeren skrev den
    pub raw: String,
}

/// Parsert script med alle steg.
#[derive(Debug, Clone)]
pub struct ParsedScript {
    pub steps: Vec<ParsedStep>,
    /// Global feilhåndtering (siste "ved feil:"-linje)
    pub global_fallback: Option<ParsedStep>,
}

// =============================================================================
// Kompileringsresultat
// =============================================================================

/// Et kompilert steg klart for utførelse.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CompiledStep {
    pub step_number: usize,
    pub binary: String,
    pub args: Vec<String>,
    /// Fallback-kommando ved feil
    pub fallback: Option<Box<CompiledStep>>,
}

/// Kompilert script — klart for vaktmesteren.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CompiledScript {
    pub steps: Vec<CompiledStep>,
    pub global_fallback: Option<CompiledStep>,
    /// Teknisk lag — lesbar representasjon av kompilert script
    pub technical: String,
}

/// En diagnostisk melding fra kompilatoren.
#[derive(Debug, Clone, Serialize)]
pub struct Diagnostic {
    pub line: usize,
    pub severity: Severity,
    pub message: String,
    pub suggestion: Option<String>,
    pub raw_input: String,
    pub compiled_output: Option<String>,
}

#[derive(Debug, Clone, Serialize, PartialEq)]
pub enum Severity {
    Ok,
    Error,
}

/// Resultat fra kompileringen.
#[derive(Debug, Clone, Serialize)]
pub struct CompileResult {
    pub diagnostics: Vec<Diagnostic>,
    pub compiled: Option<CompiledScript>,
}

impl CompileResult {
    pub fn has_errors(&self) -> bool {
        self.diagnostics.iter().any(|d| d.severity == Severity::Error)
    }

    /// Formater diagnostikk som Rust-stil rapport.
    pub fn format_report(&self) -> String {
        let mut out = String::new();
        out.push_str("┌─ Kompilering ─────────────────────────────────────┐\n");
        out.push_str("│                                                   │\n");

        for d in &self.diagnostics {
            match d.severity {
                Severity::Ok => {
                    out.push_str(&format!(
                        "│  ✓ Linje {}: {}\n",
                        d.line, d.raw_input
                    ));
                    if let Some(ref compiled) = d.compiled_output {
                        out.push_str(&format!("│    → {compiled}\n"));
                    }
                }
                Severity::Error => {
                    out.push_str(&format!(
                        "│  ✗ Linje {}: {}\n",
                        d.line, d.raw_input
                    ));
                    out.push_str(&format!("│    Feil: {}\n", d.message));
                    if let Some(ref sug) = d.suggestion {
                        out.push_str(&format!("│    Mente du: \"{sug}\"?\n"));
                    }
                }
            }
            out.push_str("│                                                   │\n");
        }

        let errors = self.diagnostics.iter().filter(|d| d.severity == Severity::Error).count();
        if errors == 0 {
            out.push_str("│  Kompilering OK.\n");
        } else {
            out.push_str(&format!(
                "│  {} feil. Rett opp og prøv igjen.\n",
                errors
            ));
        }
        out.push_str("└───────────────────────────────────────────────────┘");
        out
    }
}

// =============================================================================
// Gyldige trigger-kontekstvariabler
// =============================================================================

/// Gyldige variabelprefiks og felter fra TriggerContext.
const VALID_EVENT_VARS: &[&str] = &[
    "event.node_id",
    "event.node_kind",
    "event.edge_type",
    "event.source_id",
    "event.target_id",
    "event.op",
    "event.cas_hash",
    "event.communication_id",
    "event.collection_id",
    "event.url",
    "event.created_by",
];

/// Sjekk om en variabelreferanse er gyldig.
fn is_valid_variable(var: &str) -> bool {
    // Stripp { og }
    let inner = var.trim_matches(|c| c == '{' || c == '}');
    VALID_EVENT_VARS.contains(&inner)
}

// =============================================================================
// Parser — menneskelig script → AST
// =============================================================================

/// Parser det menneskelige scriptlaget til en AST.
///
/// Forventet format:
/// ```text
/// 1. transkriber lydfilen (stor modell)
///    ved feil: transkriber lydfilen (medium modell)
/// 2. oppsummer samtalen
///
/// ved feil: opprett oppgave "Pipeline feilet" (bug)
/// ```
///
/// NÅR/HVIS-linjer ignoreres (de er allerede i metadata.trigger).
pub fn parse(script: &str) -> Result<ParsedScript, String> {
    let mut steps = Vec::new();
    let mut global_fallback: Option<ParsedStep> = None;
    let lines: Vec<&str> = script.lines().collect();
    let mut i = 0;

    while i < lines.len() {
        let line = lines[i].trim();

        // Skip tomme linjer og NÅR/HVIS (allerede håndtert av trigger-metadata)
        if line.is_empty()
            || line.starts_with("NÅR ")
            || line.starts_with("HVIS ")
        {
            i += 1;
            continue;
        }

        // Nummerert steg: "1. verb objekt (args)"
        if let Some(step) = try_parse_numbered_step(line, i + 1) {
            let mut step = step;

            // Sjekk neste linje for innrykket "ved feil:" (steg-fallback).
            // Kun innrykket fallback (starter med whitespace) er steg-fallback.
            // Uinnrykket "ved feil:" er global fallback.
            if i + 1 < lines.len() {
                let next_raw = lines[i + 1];
                let is_indented = next_raw.starts_with(' ') || next_raw.starts_with('\t');
                if is_indented {
                    let next = next_raw.trim();
                    if let Some(fallback) = try_parse_fallback(next, i + 2) {
                        step.fallback = Some(Box::new(fallback));
                        i += 1; // Hopp over fallback-linjen
                    }
                }
            }

            steps.push(step);
            i += 1;
            continue;
        }

        // Global "ved feil:" (ikke innrykket under et steg)
        if let Some(fb) = try_parse_fallback(line, i + 1) {
            global_fallback = Some(fb);
            i += 1;
            continue;
        }

        // Ukjent linje — hopp over (kommentarer, blanke, etc.)
        i += 1;
    }

    if steps.is_empty() {
        return Err("Scriptet inneholder ingen steg. Forventet format: \"1. verb objekt (args)\"".into());
    }

    Ok(ParsedScript {
        steps,
        global_fallback,
    })
}

/// Forsøk å parse en nummerert steg-linje.
/// Format: "<N>. <verb> <objekt> [(<arg1>, <arg2>)]"
fn try_parse_numbered_step(line: &str, line_number: usize) -> Option<ParsedStep> {
    // Match "N. rest"
    let dot_pos = line.find('.')?;
    let num_str = &line[..dot_pos];
    let step_number: usize = num_str.trim().parse().ok()?;
    let rest = line[dot_pos + 1..].trim();

    if rest.is_empty() {
        return None;
    }

    let (verb, object, args) = parse_step_content(rest);

    Some(ParsedStep {
        line_number,
        step_number,
        verb,
        object,
        args,
        fallback: None,
        raw: line.to_string(),
    })
}

/// Parse en "ved feil:"-linje.
fn try_parse_fallback(line: &str, line_number: usize) -> Option<ParsedStep> {
    let lower = line.to_lowercase();
    let rest = if lower.starts_with("ved feil:") {
        line["ved feil:".len()..].trim()
    } else if lower.starts_with("ved_feil:") {
        line["ved_feil:".len()..].trim()
    } else {
        return None;
    };

    if rest.is_empty() {
        return None;
    }

    let (verb, object, args) = parse_step_content(rest);

    Some(ParsedStep {
        line_number,
        step_number: 0,
        verb,
        object,
        args,
        fallback: None,
        raw: line.to_string(),
    })
}

/// Parse innholdet av et steg: "verb objekt (arg1, arg2)"
/// Returnerer (verb, objekt, [args]).
fn parse_step_content(content: &str) -> (String, String, Vec<String>) {
    // Ekstraher argumenter i parentes
    let (main, args) = if let Some(paren_start) = content.find('(') {
        let paren_end = content.rfind(')').unwrap_or(content.len());
        let args_str = &content[paren_start + 1..paren_end];
        let args: Vec<String> = args_str
            .split(',')
            .map(|a| a.trim().to_string())
            .filter(|a| !a.is_empty())
            .collect();
        let main = content[..paren_start].trim();
        (main, args)
    } else {
        (content.trim(), Vec::new())
    };

    // Splitt verb og objekt
    let parts: Vec<&str> = main.splitn(2, ' ').collect();
    let verb = parts[0].to_string();
    let object = if parts.len() > 1 {
        parts[1].trim().to_string()
    } else {
        String::new()
    };

    (verb, object, args)
}

// =============================================================================
// Kompilator — AST + ToolRegistry → CompiledScript
// =============================================================================

/// Kompilerer et parsert script til tekniske CLI-kall.
///
/// Ser opp verktøy i registeret, mapper argumenter via `args_hints`,
/// og validerer variabelreferanser.
pub fn compile(parsed: &ParsedScript, registry: &ToolRegistry) -> CompileResult {
    let mut diagnostics = Vec::new();
    let mut compiled_steps = Vec::new();

    for step in &parsed.steps {
        match compile_step(step, registry) {
            Ok((compiled, diag)) => {
                diagnostics.push(diag);
                compiled_steps.push(compiled);
            }
            Err(diag) => {
                diagnostics.push(diag);
            }
        }
    }

    // Kompiler global fallback
    let global_fallback = parsed.global_fallback.as_ref().and_then(|fb| {
        match compile_step(fb, registry) {
            Ok((compiled, diag)) => {
                diagnostics.push(diag);
                Some(compiled)
            }
            Err(diag) => {
                diagnostics.push(diag);
                None
            }
        }
    });

    let has_errors = diagnostics.iter().any(|d| d.severity == Severity::Error);

    let compiled = if has_errors {
        None
    } else {
        let technical = format_technical(&compiled_steps, &global_fallback);
        Some(CompiledScript {
            steps: compiled_steps,
            global_fallback,
            technical,
        })
    };

    CompileResult {
        diagnostics,
        compiled,
    }
}

/// Prøv å finne verktøy med flerords-verb.
///
/// Parseren splitter "generer feed for samlingen" til verb="generer", object="feed for samlingen".
/// Hvis "generer" ikke matcher noe alias, prøv "generer feed" (verb + første ord av object),
/// og returner resten av object som effektivt objekt.
fn find_tool_with_multiword_verb<'a>(
    step: &ParsedStep,
    registry: &'a ToolRegistry,
) -> Result<(&'a ToolDef, String), Diagnostic> {
    // 1. Prøv enkeltord-verb
    if let Some(tool) = registry.find_by_verb(&step.verb) {
        return Ok((tool, step.object.clone()));
    }

    // 2. Prøv flerords-verb: "verb + N første ord av object"
    if !step.object.is_empty() {
        let object_words: Vec<&str> = step.object.split_whitespace().collect();
        for take in 1..=object_words.len().min(3) {
            let candidate = format!("{} {}", step.verb, object_words[..take].join(" "));
            if let Some(tool) = registry.find_by_verb(&candidate) {
                let remaining = object_words[take..].join(" ");
                return Ok((tool, remaining));
            }
        }
    }

    // 3. Ingen match — gi feilmelding
    Err(Diagnostic {
        line: step.line_number,
        severity: Severity::Error,
        message: format!("\"{}\" matcher ingen verktøy", step.verb),
        suggestion: registry.suggest(&step.verb),
        raw_input: step.raw.clone(),
        compiled_output: None,
    })
}

/// Kompiler ett steg.
fn compile_step(
    step: &ParsedStep,
    registry: &ToolRegistry,
) -> Result<(CompiledStep, Diagnostic), Diagnostic> {
    // Spesialtilfelle: "opprett oppgave" → work_item
    if step.verb.to_lowercase() == "opprett" && step.object.to_lowercase().starts_with("oppgave") {
        return compile_work_item(step);
    }

    // Finn verktøy via verb — prøv først enkeltord, deretter flerords-verb
    // (f.eks. "generer feed" der verb="generer" og object="feed for samlingen")
    let (tool, effective_object) = find_tool_with_multiword_verb(step, registry)?;

    // Bygg argumentliste
    let mut cli_args = Vec::new();

    // Map objektet via args_hints (f.eks. "lydfilen" → "--cas-hash {event.cas_hash}")
    if !effective_object.is_empty() {
        if let Some(hint) = tool.args_hints.get(&effective_object.to_lowercase()) {
            // Hint kan inneholde variabel ({event.xxx}) eller flagg (--flag value)
            expand_hint(hint, &mut cli_args);
        }
        // Objektet som ikke matcher args_hints ignoreres (det er kontekst for brukeren)
    }

    // Map argumenter i parentes
    for arg in &step.args {
        let lower = arg.to_lowercase();
        if let Some(hint) = tool.args_hints.get(&lower) {
            expand_hint(hint, &mut cli_args);
        } else {
            // Prøv å matche mot args_hints med fuzzy match
            let suggestion = tool
                .args_hints
                .keys()
                .find(|k| levenshtein(&lower, &k.to_lowercase()) <= 2)
                .cloned();
            return Err(Diagnostic {
                line: step.line_number,
                severity: Severity::Error,
                message: format!(
                    "Ukjent argument \"{}\" for {}",
                    arg, tool.binary
                ),
                suggestion,
                raw_input: step.raw.clone(),
                compiled_output: None,
            });
        }
    }

    // Valider at alle variabelreferanser i args er gyldige
    for a in &cli_args {
        if a.starts_with('{') && a.ends_with('}') && !is_valid_variable(a) {
            return Err(Diagnostic {
                line: step.line_number,
                severity: Severity::Error,
                message: format!("Ukjent variabel: {a}"),
                suggestion: Some(format!(
                    "Gyldige variabler: {}",
                    VALID_EVENT_VARS.join(", ")
                )),
                raw_input: step.raw.clone(),
                compiled_output: None,
            });
        }
    }

    // Kompiler fallback rekursivt
    let fallback = step
        .fallback
        .as_ref()
        .and_then(|fb| match compile_step(fb, registry) {
            Ok((compiled, _)) => Some(Box::new(compiled)),
            Err(_) => None, // Fallback-feil er allerede rapportert
        });

    let compiled_output = format_step_cli(&tool.binary, &cli_args);

    Ok((
        CompiledStep {
            step_number: step.step_number,
            binary: tool.binary.clone(),
            args: cli_args,
            fallback,
        },
        Diagnostic {
            line: step.line_number,
            severity: Severity::Ok,
            message: String::new(),
            suggestion: None,
            raw_input: step.raw.clone(),
            compiled_output: Some(compiled_output),
        },
    ))
}

/// Kompiler "opprett oppgave" → work_item.
fn compile_work_item(step: &ParsedStep) -> Result<(CompiledStep, Diagnostic), Diagnostic> {
    // Ekstraher tittel: alt etter "oppgave" i objektet, eller i anførselstegn
    let obj = &step.object;
    let title = if let Some(start) = obj.find('"') {
        let end = obj[start + 1..].find('"').map(|i| i + start + 1).unwrap_or(obj.len());
        &obj[start + 1..end]
    } else {
        // Alt etter "oppgave "
        obj.strip_prefix("oppgave").map(|s| s.trim()).unwrap_or(obj)
    };

    let mut args = vec![format!("\"{}\"", title)];

    // Tags fra parentes-argumenter
    for arg in &step.args {
        args.push("--tag".to_string());
        args.push(arg.clone());
    }

    let compiled_output = format!("work_item {}", args.join(" "));

    Ok((
        CompiledStep {
            step_number: step.step_number,
            binary: "work_item".to_string(),
            args,
            fallback: None,
        },
        Diagnostic {
            line: step.line_number,
            severity: Severity::Ok,
            message: String::new(),
            suggestion: None,
            raw_input: step.raw.clone(),
            compiled_output: Some(compiled_output),
        },
    ))
}

/// Ekspander en args_hint-verdi til CLI-argumenter.
/// F.eks. "--model large" → ["--model", "large"]
/// F.eks. "{event.cas_hash}" → ["{event.cas_hash}"]
/// F.eks. "--cas-hash {event.cas_hash}" → ["--cas-hash", "{event.cas_hash}"]
fn expand_hint(hint: &str, args: &mut Vec<String>) {
    // Splitt på whitespace, men bevar {variabler} og "strenger" intakt
    for token in hint.split_whitespace() {
        args.push(token.to_string());
    }
}

/// Formater et steg som CLI-kommando.
fn format_step_cli(binary: &str, args: &[String]) -> String {
    if args.is_empty() {
        binary.to_string()
    } else {
        format!("{} {}", binary, args.join(" "))
    }
}

/// Formater kompilert script som teknisk lag-tekst.
fn format_technical(steps: &[CompiledStep], global_fallback: &Option<CompiledStep>) -> String {
    let mut out = String::new();
    for step in steps {
        out.push_str(&format!(
            "{}. {}\n",
            step.step_number,
            format_step_cli(&step.binary, &step.args)
        ));
        if let Some(ref fb) = step.fallback {
            out.push_str(&format!(
                "   VED_FEIL: {}\n",
                format_step_cli(&fb.binary, &fb.args)
            ));
        }
    }
    if let Some(fb) = global_fallback {
        out.push_str(&format!(
            "\nVED_FEIL: {}\n",
            format_step_cli(&fb.binary, &fb.args)
        ));
    }
    out
}

// =============================================================================
// Hent verktøyregister fra PG
// =============================================================================

/// Hent alle cli_tool-noder fra PG og bygg et ToolRegistry.
pub async fn load_tool_registry(db: &sqlx::PgPool) -> Result<ToolRegistry, String> {
    let rows = sqlx::query_as::<_, (serde_json::Value,)>(
        "SELECT metadata FROM nodes WHERE node_kind = 'cli_tool'",
    )
    .fetch_all(db)
    .await
    .map_err(|e| format!("Feil ved henting av cli_tool-noder: {e}"))?;

    Ok(ToolRegistry::from_metadata(&rows))
}

/// Full kompileringspipeline: parse script → hent verktøy → kompiler.
///
/// Returnerer kompileringsresultat med diagnostikk og (ved suksess) kompilert script.
pub async fn compile_script(
    db: &sqlx::PgPool,
    script: &str,
) -> Result<CompileResult, String> {
    let parsed = parse(script)?;
    let registry = load_tool_registry(db).await?;
    Ok(compile(&parsed, &registry))
}

// =============================================================================
// Levenshtein-avstand (enkel implementasjon for fuzzy-matching)
// =============================================================================

fn levenshtein(a: &str, b: &str) -> usize {
    let a_chars: Vec<char> = a.chars().collect();
    let b_chars: Vec<char> = b.chars().collect();
    let m = a_chars.len();
    let n = b_chars.len();

    if m == 0 {
        return n;
    }
    if n == 0 {
        return m;
    }

    let mut prev: Vec<usize> = (0..=n).collect();
    let mut curr = vec![0; n + 1];

    for i in 1..=m {
        curr[0] = i;
        for j in 1..=n {
            let cost = if a_chars[i - 1] == b_chars[j - 1] { 0 } else { 1 };
            curr[j] = (prev[j] + 1)
                .min(curr[j - 1] + 1)
                .min(prev[j - 1] + cost);
        }
        std::mem::swap(&mut prev, &mut curr);
    }

    prev[n]
}

// =============================================================================
// Tester
// =============================================================================

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

    fn test_registry() -> ToolRegistry {
        ToolRegistry {
            tools: vec![
                ToolDef {
                    binary: "synops-transcribe".into(),
                    aliases: vec!["transkriber".into(), "transkribering".into()],
                    description: "Whisper-transkribering".into(),
                    args_hints: HashMap::from([
                        ("lydfilen".into(), "--cas-hash {event.cas_hash}".into()),
                        ("stor modell".into(), "--model large".into()),
                        ("medium modell".into(), "--model medium".into()),
                    ]),
                },
                ToolDef {
                    binary: "synops-summarize".into(),
                    aliases: vec!["oppsummer".into()],
                    description: "AI-oppsummering".into(),
                    args_hints: HashMap::from([(
                        "samtalen".into(),
                        "--communication-id {event.communication_id}".into(),
                    )]),
                },
                ToolDef {
                    binary: "synops-rss".into(),
                    aliases: vec!["oppdater rss-feed".into(), "oppdater".into()],
                    description: "RSS-generering".into(),
                    args_hints: HashMap::from([(
                        "rss-feed".into(),
                        "--collection-id {event.collection_id}".into(),
                    )]),
                },
            ],
        }
    }

    #[test]
    fn test_parse_basic_script() {
        let script = r#"
NÅR innspilling avsluttet
HVIS samling har podcast

1. transkriber lydfilen (stor modell)
   ved feil: transkriber lydfilen (medium modell)
2. oppsummer samtalen
3. oppdater rss-feed

ved feil: opprett oppgave "Pipeline feilet" (bug)
"#;
        let parsed = parse(script).unwrap();
        assert_eq!(parsed.steps.len(), 3);
        assert_eq!(parsed.steps[0].verb, "transkriber");
        assert_eq!(parsed.steps[0].object, "lydfilen");
        assert_eq!(parsed.steps[0].args, vec!["stor modell"]);
        assert!(parsed.steps[0].fallback.is_some());
        assert_eq!(parsed.steps[1].verb, "oppsummer");
        assert_eq!(parsed.steps[2].verb, "oppdater");
        assert!(parsed.global_fallback.is_some());
    }

    #[test]
    fn test_compile_full_script() {
        let script = r#"
1. transkriber lydfilen (stor modell)
   ved feil: transkriber lydfilen (medium modell)
2. oppsummer samtalen
"#;
        let parsed = parse(script).unwrap();
        let registry = test_registry();
        let result = compile(&parsed, &registry);

        assert!(!result.has_errors());
        let compiled = result.compiled.unwrap();
        assert_eq!(compiled.steps.len(), 2);
        assert_eq!(compiled.steps[0].binary, "synops-transcribe");
        assert_eq!(
            compiled.steps[0].args,
            vec!["--cas-hash", "{event.cas_hash}", "--model", "large"]
        );
        assert!(compiled.steps[0].fallback.is_some());
        let fb = compiled.steps[0].fallback.as_ref().unwrap();
        assert_eq!(fb.args, vec!["--cas-hash", "{event.cas_hash}", "--model", "medium"]);
    }

    #[test]
    fn test_compile_unknown_verb() {
        let script = "1. send epost til deltakerne\n";
        let parsed = parse(script).unwrap();
        let registry = test_registry();
        let result = compile(&parsed, &registry);

        assert!(result.has_errors());
        assert_eq!(result.diagnostics[0].severity, Severity::Error);
        assert!(result.diagnostics[0].message.contains("matcher ingen verktøy"));
    }

    #[test]
    fn test_compile_unknown_arg() {
        let script = "1. transkriber lydfilen (ekstra stor modell)\n";
        let parsed = parse(script).unwrap();
        let registry = test_registry();
        let result = compile(&parsed, &registry);

        assert!(result.has_errors());
        assert!(result.diagnostics[0].message.contains("Ukjent argument"));
    }

    #[test]
    fn test_compile_work_item() {
        let script = "1. opprett oppgave \"Pipeline feilet\" (bug)\n";
        let parsed = parse(script).unwrap();
        let registry = test_registry();
        let result = compile(&parsed, &registry);

        assert!(!result.has_errors());
        let compiled = result.compiled.unwrap();
        assert_eq!(compiled.steps[0].binary, "work_item");
        assert_eq!(compiled.steps[0].args, vec!["\"Pipeline feilet\"", "--tag", "bug"]);
    }

    #[test]
    fn test_compile_report_format() {
        let script = "1. transkriber lydfilen (stor modell)\n2. send epost\n";
        let parsed = parse(script).unwrap();
        let registry = test_registry();
        let result = compile(&parsed, &registry);

        let report = result.format_report();
        assert!(report.contains("✓ Linje"));
        assert!(report.contains("✗ Linje"));
        assert!(report.contains("1 feil"));
    }

    #[test]
    fn test_parse_empty_script() {
        assert!(parse("").is_err());
        assert!(parse("NÅR noe skjer\nHVIS noe\n").is_err());
    }

    #[test]
    fn test_levenshtein_basic() {
        assert_eq!(levenshtein("transkriber", "transkriber"), 0);
        assert_eq!(levenshtein("transkiber", "transkriber"), 1);
        assert_eq!(levenshtein("", "abc"), 3);
        assert_eq!(levenshtein("abc", ""), 3);
    }

    #[test]
    fn test_fuzzy_suggestion() {
        let registry = test_registry();
        // "transkiber" er nær "transkriber"
        let suggestion = registry.suggest("transkiber");
        assert_eq!(suggestion, Some("transkriber".into()));
    }

    #[test]
    fn test_from_metadata() {
        let rows = vec![(json!({
            "binary": "synops-transcribe",
            "aliases": ["transkriber"],
            "description": "Test",
            "args_hints": {"lydfilen": "--cas-hash {event.cas_hash}"}
        }),)];
        let registry = ToolRegistry::from_metadata(&rows);
        assert_eq!(registry.tools.len(), 1);
        assert_eq!(registry.tools[0].binary, "synops-transcribe");
    }

    #[test]
    fn test_valid_variables() {
        assert!(is_valid_variable("{event.node_id}"));
        assert!(is_valid_variable("{event.cas_hash}"));
        assert!(!is_valid_variable("{event.nonexistent}"));
        assert!(!is_valid_variable("{foo.bar}"));
    }

    #[test]
    fn test_global_fallback_as_ved_feil() {
        let script = "1. oppsummer samtalen\nved feil: opprett oppgave \"Feil\" (bug)\n";
        let parsed = parse(script).unwrap();
        assert!(parsed.global_fallback.is_some());
        assert_eq!(parsed.global_fallback.unwrap().verb, "opprett");
    }

    #[test]
    fn test_multiword_verb() {
        // "oppdater rss-feed" er et flerords-alias. Parseren splitter til
        // verb="oppdater", object="rss-feed". compile_step skal finne alias
        // "oppdater rss-feed" via flerords-matching.
        let registry = test_registry();
        let script = "1. oppdater rss-feed\n";
        let parsed = parse(script).unwrap();
        let result = compile(&parsed, &registry);
        // Bør kompilere OK — "oppdater" matcher direkte som alias
        assert!(!result.has_errors());

        // Test med alias som KUN finnes som flerord
        let mut multi_registry = ToolRegistry {
            tools: vec![ToolDef {
                binary: "synops-rss".into(),
                aliases: vec!["generer feed".into()],
                description: "RSS".into(),
                args_hints: HashMap::from([(
                    "for samlingen".into(),
                    "--collection-id {event.collection_id}".into(),
                )]),
            }],
        };
        let script2 = "1. generer feed for samlingen\n";
        let parsed2 = parse(script2).unwrap();
        let result2 = compile(&parsed2, &multi_registry);
        assert!(
            !result2.has_errors(),
            "Flerords-verb 'generer feed' bør matche: {:?}",
            result2.diagnostics
        );
        let compiled = result2.compiled.unwrap();
        assert_eq!(compiled.steps[0].binary, "synops-rss");
        assert_eq!(
            compiled.steps[0].args,
            vec!["--collection-id", "{event.collection_id}"]
        );
    }

    #[test]
    fn test_compile_clip_url() {
        let registry = ToolRegistry {
            tools: vec![ToolDef {
                binary: "synops-clip".into(),
                aliases: vec!["clip".into(), "klipp".into(), "hent artikkel".into(), "clip url".into()],
                description: "Hent og parse webartikler".into(),
                args_hints: HashMap::from([
                    ("url".into(), "--url {arg}".into()),
                    ("fra event".into(), "--url {event.url}".into()),
                    ("lagre node".into(), "--write".into()),
                    ("bruker".into(), "--created-by {event.created_by}".into()),
                    ("med timeout".into(), "--timeout {arg}".into()),
                    ("force playwright".into(), "--playwright".into()),
                ]),
            }],
        };

        // Test: "clip fra event (lagre node, bruker)"
        let script = "1. clip fra event (lagre node, bruker)\n";
        let parsed = parse(script).unwrap();
        let result = compile(&parsed, &registry);
        assert!(
            !result.has_errors(),
            "clip fra event bør kompilere: {:?}",
            result.diagnostics
        );
        let compiled = result.compiled.unwrap();
        assert_eq!(compiled.steps[0].binary, "synops-clip");
        assert_eq!(
            compiled.steps[0].args,
            vec!["--url", "{event.url}", "--write", "--created-by", "{event.created_by}"]
        );
    }
}
