use itertools::Itertools;
use serde::{Deserialize, Serialize};
use std::str::FromStr;
use wasm_bindgen::prelude::*;

#[cfg(feature = "opfs")]
use futures::StreamExt;
#[cfg(feature = "opfs")]
use opfs::{DirectoryHandle, FileHandle};
#[cfg(feature = "opfs")]
use std::{cell::RefCell, collections::HashMap, rc::Rc};

#[wasm_bindgen(typescript_custom_section)]
const TS_CUSTOM_SECTION: &'static str = r#"
export type DefinedValueType = 'Function' | 'Variable';

export interface DefinedValue {
  name: string;
  args?: string[];
  doc: string;
  valueType: DefinedValueType;
}

export interface Diagnostic {
  startLine: number,
  startColumn: number,
  endLine: number,
  endColumn: number,
  message: string,
}

export interface InlayHint {
  line: number,
  column: number,
  label: string,
}

export interface HoverResult {
  content: string;
}

export interface Options {
    isUpdate: boolean,
    inputFormat: 'markdown' | 'text' | 'mdx' | 'html' | 'null' | 'raw' | null,
    listStyle: 'dash' | 'plus' | 'star' | null,
    linkTitleStyle: 'double' | 'single' | 'paren' | null,
    linkUrlStyle: 'angle' | 'none' | null,
}

export function definedValues(code: string, module?: string): Promise<ReadonlyArray<DefinedValue>>;
export function diagnostics(code: string, enableTypeCheck?: boolean): Promise<ReadonlyArray<Diagnostic>>;
export function hover(code: string, line: number, column: number): Promise<HoverResult | null>;
export function inlayHints(code: string): Promise<ReadonlyArray<InlayHint>>;
export function run(code: string, content: string, options: Options): Promise<string>;
"#;

#[derive(Serialize, Deserialize)]
pub enum DefinedValueType {
    Function,
    Selector,
    Variable,
    Module,
}

#[derive(Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct DefinedValue {
    name: String,
    args: Option<Vec<String>>,
    doc: String,
    value_type: DefinedValueType,
}

#[derive(Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct Diagnostic {
    start_line: u32,
    start_column: u32,
    end_line: u32,
    end_column: u32,
    message: String,
}

#[derive(Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct InlayHint {
    line: u32,
    column: u32,
    label: String,
}

#[derive(Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct HoverResult {
    content: String,
}

#[derive(Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
#[wasm_bindgen(js_name=RunOptions, skip_typescript)]
struct Options {
    is_update: bool,
    input_format: Option<InputFormat>,
    list_style: Option<ListStyle>,
    link_title_style: Option<TitleSurroundStyle>,
    link_url_style: Option<UrlSurroundStyle>,
}

#[derive(Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub enum InputFormat {
    #[serde(rename = "markdown")]
    Markdown,
    #[serde(rename = "mdx")]
    Mdx,
    #[serde(rename = "text")]
    Text,
    #[serde(rename = "html")]
    Html,
    #[serde(rename = "null")]
    Null,
    #[serde(rename = "raw")]
    Raw,
}

impl FromStr for InputFormat {
    type Err = String;

    fn from_str(s: &str) -> Result<Self, Self::Err> {
        match s {
            "markdown" => Ok(Self::Markdown),
            "mdx" => Ok(Self::Mdx),
            "text" => Ok(Self::Text),
            _ => Err(format!("Unknown input format: {}", s)),
        }
    }
}

#[derive(Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub enum ListStyle {
    #[serde(rename = "dash")]
    Dash,
    #[serde(rename = "plus")]
    Plus,
    #[serde(rename = "star")]
    Star,
}

impl FromStr for ListStyle {
    type Err = String;

    fn from_str(s: &str) -> Result<Self, Self::Err> {
        match s {
            "dash" => Ok(Self::Dash),
            "plus" => Ok(Self::Plus),
            "star" => Ok(Self::Plus),
            _ => Err(format!("Unknown list style: {}", s)),
        }
    }
}

#[derive(Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub enum TitleSurroundStyle {
    #[serde(rename = "double")]
    Double,
    #[serde(rename = "single")]
    Single,
    #[serde(rename = "paren")]
    Paren,
}

impl FromStr for TitleSurroundStyle {
    type Err = String;

    fn from_str(s: &str) -> Result<Self, Self::Err> {
        match s {
            "double" => Ok(Self::Double),
            "single" => Ok(Self::Single),
            "paren" => Ok(Self::Paren),
            _ => Err(format!("Unknown title surround style: {}", s)),
        }
    }
}

#[derive(Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub enum UrlSurroundStyle {
    #[serde(rename = "angle")]
    Angle,
    #[serde(rename = "none")]
    None,
}

impl FromStr for UrlSurroundStyle {
    type Err = String;

    fn from_str(s: &str) -> Result<Self, Self::Err> {
        match s {
            "angle" => Ok(Self::Angle),
            "none" => Ok(Self::None),
            _ => Err(format!("Unknown URL surround style: {}", s)),
        }
    }
}

#[derive(Serialize, Deserialize, Debug, Clone, Default)]
#[wasm_bindgen]
#[serde(rename_all = "camelCase")]
pub struct ConversionOptions {
    pub extract_scripts_as_code_blocks: bool,
    pub generate_front_matter: bool,
    pub use_title_as_h1: bool,
}

impl From<ConversionOptions> for mq_markdown::ConversionOptions {
    fn from(options: ConversionOptions) -> Self {
        Self {
            extract_scripts_as_code_blocks: options.extract_scripts_as_code_blocks,
            generate_front_matter: options.generate_front_matter,
            use_title_as_h1: options.use_title_as_h1,
        }
    }
}

#[derive(Debug, Clone, Default)]
pub struct WasmModuleResolver {
    #[cfg(feature = "opfs")]
    /// Cache of preloaded module contents, keyed by module name
    cache: Rc<RefCell<HashMap<String, String>>>,
    #[cfg(feature = "opfs")]
    /// Root directory handle for OPFS access
    root_dir: Rc<RefCell<Option<opfs::persistent::DirectoryHandle>>>,
    #[cfg(feature = "opfs")]
    /// Flag indicating whether OPFS is available
    is_available: Rc<RefCell<bool>>,
}

impl WasmModuleResolver {
    pub fn new() -> Self {
        Self::default()
    }

    /// Initializes the OPFS root directory handle
    ///
    /// If OPFS is not available, this method will silently fail and the resolver
    /// will operate as a NoOp resolver (only using manually added modules via `add_module`).
    pub async fn initialize(&self) {
        #[cfg(feature = "opfs")]
        match opfs::persistent::app_specific_dir().await {
            Ok(root) => {
                *self.root_dir.borrow_mut() = Some(root);
                *self.is_available.borrow_mut() = true;
            }
            Err(_) => {
                // OPFS is not available, resolver will work as NoOp
                *self.is_available.borrow_mut() = false;
            }
        }
    }

    /// Preloads all `.mq` modules from OPFS into the cache
    ///
    /// This method scans the OPFS root directory for all `.mq` files and loads them into cache.
    /// Module names are stored without the `.mq` extension (e.g., `csv.mq` becomes `csv`).
    ///
    /// If OPFS is not available, this method returns immediately without error.
    pub async fn preload_modules(&self) {
        #[cfg(feature = "opfs")]
        {
            // Skip if OPFS is not available
            if !*self.is_available.borrow() {
                return;
            }

            let root = match self.root_dir.borrow().as_ref() {
                Some(r) => r.clone(),
                None => return, // Should not happen if is_available is true, but be defensive
            };

            let mut entries = match root.entries().await {
                Ok(e) => e,
                Err(_) => return, // Failed to get directory entries
            };

            while let Some(result) = entries.next().await {
                let (name, entry) = match result {
                    Ok(e) => e,
                    Err(_) => continue, // Skip entries that fail to read
                };

                match entry {
                    opfs::DirectoryEntry::File(file_handle) => {
                        // Only process .mq files
                        if !name.ends_with(".mq") {
                            continue;
                        }

                        // Read file contents
                        let data = match file_handle.read().await {
                            Ok(d) => d,
                            Err(_) => continue, // Skip files that fail to read
                        };

                        let contents = match String::from_utf8(data) {
                            Ok(c) => c,
                            Err(_) => continue, // Skip files that are not valid UTF-8
                        };

                        // Store with module name (without .mq extension)
                        let module_name = name.strip_suffix(".mq").unwrap_or(&name);
                        self.cache.borrow_mut().insert(module_name.to_string(), contents);
                    }
                    opfs::DirectoryEntry::Directory(_) => {
                        // Skip directories for now
                        continue;
                    }
                }
            }
        }
    }

    /// Manually adds a module to the cache
    ///
    /// This is useful for injecting module contents without using OPFS
    pub fn add_module(&self, _module_name: &str, _content: String) {
        #[cfg(feature = "opfs")]
        self.cache.borrow_mut().insert(_module_name.to_string(), _content);
    }

    /// Clears the module cache
    pub fn clear_cache(&self) {
        #[cfg(feature = "opfs")]
        self.cache.borrow_mut().clear();
    }
}

impl mq_lang::ModuleResolver for WasmModuleResolver {
    fn resolve(&self, module_name: &str) -> Result<String, mq_lang::ModuleError> {
        #[cfg(feature = "opfs")]
        return self.cache.borrow().get(module_name).cloned().ok_or_else(|| {
            mq_lang::ModuleError::NotFound(std::borrow::Cow::Owned(format!(
                "Module '{}' not found in cache. Use preload_modules() to load it first.",
                module_name
            )))
        });
        #[cfg(not(feature = "opfs"))]
        return Err(mq_lang::ModuleError::NotFound(std::borrow::Cow::Owned(format!(
            "Module '{}' not found. Module resolution is not supported in this environment.",
            module_name
        ))));
    }

    fn get_path(&self, module_name: &str) -> Result<String, mq_lang::ModuleError> {
        Ok(module_name.to_string())
    }

    fn search_paths(&self) -> Vec<std::path::PathBuf> {
        vec![]
    }

    fn set_search_paths(&mut self, _: Vec<std::path::PathBuf>) {
        // OPFS doesn't use search paths
    }
}

#[wasm_bindgen(js_name=run, skip_typescript)]
pub async fn run(code: &str, content: &str, options: JsValue) -> Result<String, JsValue> {
    let resolver = WasmModuleResolver::new();
    resolver.initialize().await;
    resolver.preload_modules().await;

    let options: Options = serde_wasm_bindgen::from_value(options)
        .map_err(|e| JsValue::from_str(&format!("Failed to parse options: {}", e)))?;

    let is_update = options.is_update;
    let mut engine = mq_lang::Engine::new(resolver);

    engine.load_builtin_module();

    let input = match options.input_format.as_ref().unwrap_or(&InputFormat::Markdown) {
        InputFormat::Text => mq_lang::parse_text_input(content),
        InputFormat::Html => mq_lang::parse_html_input(content),
        InputFormat::Mdx => mq_lang::parse_mdx_input(content),
        InputFormat::Markdown => mq_lang::parse_markdown_input(content),
        InputFormat::Raw => Ok(mq_lang::raw_input(content)),
        InputFormat::Null => Ok(mq_lang::null_input()),
    }
    .map_err(|e| JsValue::from_str(&format!("Failed to parse input content: {}", e)))?;

    engine
        .eval(code, input.clone().into_iter())
        .map_err(|e| JsValue::from_str(&format!("{}", &e)))
        .map(|result_values| {
            let values = if matches!(options.input_format, Some(InputFormat::Markdown)) && is_update {
                let values: mq_lang::RuntimeValues = input.into();
                values.update_with(result_values)
            } else {
                result_values
            };

            let mut markdown = mq_markdown::Markdown::new(
                values
                    .into_iter()
                    .map(|runtime_value| match runtime_value {
                        mq_lang::RuntimeValue::Markdown(node, _) => *node,
                        _ => runtime_value.to_string().into(),
                    })
                    .collect(),
            );
            markdown.set_options(mq_markdown::RenderOptions {
                list_style: options
                    .list_style
                    .map(|style| match style {
                        ListStyle::Dash => mq_markdown::ListStyle::Dash,
                        ListStyle::Plus => mq_markdown::ListStyle::Plus,
                        ListStyle::Star => mq_markdown::ListStyle::Star,
                    })
                    .unwrap_or_default(),
                link_title_style: options
                    .link_title_style
                    .map(|style| match style {
                        TitleSurroundStyle::Double => mq_markdown::TitleSurroundStyle::Double,
                        TitleSurroundStyle::Single => mq_markdown::TitleSurroundStyle::Single,
                        TitleSurroundStyle::Paren => mq_markdown::TitleSurroundStyle::Paren,
                    })
                    .unwrap_or_default(),
                link_url_style: options
                    .link_url_style
                    .map(|style| match style {
                        UrlSurroundStyle::Angle => mq_markdown::UrlSurroundStyle::Angle,
                        UrlSurroundStyle::None => mq_markdown::UrlSurroundStyle::None,
                    })
                    .unwrap_or_default(),
            });
            markdown.to_string()
        })
}

#[wasm_bindgen(js_name=toAst)]
pub async fn to_ast(code: &str) -> Result<String, JsValue> {
    let token_arena = mq_lang::Shared::new(mq_lang::SharedCell::new(mq_lang::Arena::new(1024)));
    mq_lang::parse(code, token_arena)
        .map_err(|e| JsValue::from_str(&format!("Failed to parse code: {}", e)))
        .and_then(|json| {
            serde_json::to_string(&json).map_err(|e| JsValue::from_str(&format!("Failed to serialize AST: {}", e)))
        })
}

#[wasm_bindgen(js_name=format)]
pub async fn format(code: &str) -> Result<String, JsValue> {
    mq_formatter::Formatter::default()
        .format(code)
        .map_err(|e| JsValue::from_str(&format!("{:?}", &e)))
}

#[wasm_bindgen(js_name=htmlToMarkdown)]
pub async fn html_to_markdown(html_input: &str, options: Option<ConversionOptions>) -> Result<String, JsValue> {
    mq_markdown::convert_html_to_markdown(html_input, options.map(Into::into).unwrap_or_default())
        .map_err(|e| JsValue::from_str(&format!("Failed to convert HTML to Markdown: {}", e)))
}

#[wasm_bindgen(js_name=toHtml)]
pub async fn to_html(markdown_input: &str) -> String {
    mq_markdown::to_html(markdown_input)
}

#[wasm_bindgen(js_name=diagnostics, skip_typescript)]
pub async fn diagnostics(code: &str, enable_type_check: Option<bool>) -> JsValue {
    let (_, errors) = mq_lang::parse_recovery(code);
    let mut errors = errors
        .error_ranges(code)
        .iter()
        .map(|(message, range)| Diagnostic {
            start_line: range.start.line,
            start_column: range.start.column as u32,
            end_line: range.end.line,
            end_column: range.end.column as u32,
            message: message.to_owned(),
        })
        .collect::<Vec<_>>();

    if enable_type_check.unwrap_or(false) && errors.is_empty() {
        let mut hir = mq_hir::Hir::default();
        hir.add_code(None, code);

        let mut checker = mq_check::TypeChecker::default();
        let type_errors = checker.check(&hir);

        for error in type_errors {
            if let Some(range) = error.location() {
                errors.push(Diagnostic {
                    start_line: range.start.line,
                    start_column: range.start.column as u32,
                    end_line: range.end.line,
                    end_column: range.end.column as u32,
                    message: error.to_string(),
                });
            }
        }
    }

    serde_wasm_bindgen::to_value(&errors).unwrap()
}

#[wasm_bindgen(js_name=inlayHints, skip_typescript)]
pub async fn inlay_hints(code: &str) -> JsValue {
    let mut hir = mq_hir::Hir::default();
    hir.add_code(None, code);

    let mut checker = mq_check::TypeChecker::default();
    let _ = checker.check(&hir);

    let symbol_types = checker.symbol_types();

    let hints: Vec<InlayHint> = hir
        .symbols()
        .filter_map(|(symbol_id, symbol)| {
            if hir.is_builtin_symbol(symbol) {
                return None;
            }

            let show = matches!(
                symbol.kind,
                mq_hir::SymbolKind::Variable
                    | mq_hir::SymbolKind::Function(_)
                    | mq_hir::SymbolKind::DestructuringBinding
                    | mq_hir::SymbolKind::PatternVariable { .. }
            );
            if !show {
                return None;
            }

            let range = symbol.source.text_range.as_ref()?;
            let type_scheme = symbol_types.get(&symbol_id)?;

            if !type_scheme.ty.is_concrete() {
                return None;
            }

            Some(InlayHint {
                line: range.end.line,
                column: range.end.column as u32,
                label: format!(": {}", type_scheme),
            })
        })
        .collect();

    serde_wasm_bindgen::to_value(&hints).unwrap_or_else(|_| JsValue::from(js_sys::Array::new()))
}

#[wasm_bindgen(js_name=definedValues, skip_typescript)]
pub async fn defined_values(code: &str, module: Option<String>) -> Result<JsValue, JsValue> {
    let mut hir = mq_hir::Hir::default();
    hir.add_code(None, code);

    // If module is specified, find the module symbol
    let module_id = if let Some(ref module_name) = module {
        hir.symbols().find_map(|(id, symbol)| {
            if symbol.is_module() && symbol.value.as_ref().map(|v| v.as_str()) == Some(module_name.as_str()) {
                Some(id)
            } else {
                None
            }
        })
    } else {
        None
    };

    let symbols = hir
        .symbols()
        .filter_map(|(_symbol_id, symbol)| {
            // Filter by module if specified
            if module_id.is_some() && symbol.parent != module_id {
                return None;
            }

            match symbol {
                mq_hir::Symbol {
                    kind: mq_hir::SymbolKind::Function(params),
                    value: Some(value),
                    doc,
                    ..
                } => Some(DefinedValue {
                    name: value.to_string(),
                    args: Some(params.iter().map(|param| param.to_string()).collect()),
                    doc: doc.iter().map(|(_, doc)| doc.to_string()).join("\n"),
                    value_type: DefinedValueType::Function,
                }),
                mq_hir::Symbol {
                    kind: mq_hir::SymbolKind::Selector(_),
                    value: Some(value),
                    doc,
                    ..
                } => Some(DefinedValue {
                    name: value.to_string(),
                    args: None,
                    doc: doc.iter().map(|(_, doc)| doc.to_string()).join("\n"),
                    value_type: DefinedValueType::Selector,
                }),
                mq_hir::Symbol {
                    kind: mq_hir::SymbolKind::Variable,
                    value: Some(value),
                    doc,
                    ..
                } => Some(DefinedValue {
                    name: value.to_string(),
                    args: None,
                    doc: doc.iter().map(|(_, doc)| doc.to_string()).join("\n"),
                    value_type: DefinedValueType::Variable,
                }),
                _ => None,
            }
        })
        .collect::<Vec<_>>();

    Ok(serde_wasm_bindgen::to_value(&symbols).unwrap())
}

/// Returns `true` if a doc line is a deprecation marker.
fn is_deprecated_marker(text: &str) -> bool {
    let lower = text.trim().to_lowercase();
    lower == "deprecated" || lower.starts_with("deprecated:") || lower.starts_with("deprecated ")
}

/// Extracts the human-readable message from a deprecation marker line, if any.
fn extract_deprecated_message(text: &str) -> Option<String> {
    let after_colon = text.trim().split_once(':')?.1.trim();
    if after_colon.is_empty() {
        None
    } else {
        Some(after_colon.to_string())
    }
}

/// Builds a Markdown hover string from a pre-formatted signature, doc comments, and
/// deprecation status.
fn format_hover_content(signature: &str, docs: &[mq_hir::Doc], deprecated: bool) -> String {
    let mut sections: Vec<String> = Vec::new();

    sections.push(format!("```mq\n{}\n```", signature));

    if deprecated {
        let dep_msg = docs
            .iter()
            .find(|(_, text)| is_deprecated_marker(text))
            .and_then(|(_, text)| extract_deprecated_message(text));

        match dep_msg {
            Some(msg) => sections.push(format!("> ⚠️ **Deprecated**: {}", msg)),
            None => sections.push("> ⚠️ **Deprecated**".to_string()),
        }
    }

    let doc_text = docs
        .iter()
        .filter(|(_, text)| !is_deprecated_marker(text))
        .map(|(_, text)| text.as_str())
        .collect::<Vec<_>>()
        .join("\n");

    if !doc_text.trim().is_empty() {
        sections.push("---".to_string());
        sections.push(doc_text);
    }

    sections.join("\n\n")
}

/// Returns hover information (Markdown content) for the symbol at `line`/`column` (1-based).
///
/// Returns `null` if no symbol is found at the given position.
#[wasm_bindgen(js_name=hover, skip_typescript)]
pub async fn hover(code: &str, line: u32, column: u32) -> JsValue {
    let mut hir = mq_hir::Hir::default();
    let (source_id, _) = hir.add_code(None, code);

    let mut checker = mq_check::TypeChecker::default();
    let _ = checker.check(&hir);
    let type_env = checker.symbol_types();

    let pos = mq_lang::Position::new(line, column as usize);
    let Some((symbol_id, symbol)) = hir.find_symbol_in_position(source_id, pos) else {
        return JsValue::NULL;
    };

    let deprecated = symbol.is_deprecated();
    let type_scheme = type_env.get(&symbol_id);

    let signature = match &symbol.kind {
        mq_hir::SymbolKind::Function(args) | mq_hir::SymbolKind::Macro(args) => {
            let type_annotation = type_scheme.map(|s| format!(": {}", s.ty)).unwrap_or_default();
            format!(
                "{}({}){}",
                symbol.value.as_deref().unwrap_or_default(),
                args.iter().map(|p| p.to_string()).join(", "),
                type_annotation
            )
        }
        mq_hir::SymbolKind::Variable
        | mq_hir::SymbolKind::DestructuringBinding
        | mq_hir::SymbolKind::PatternVariable { .. } => {
            let type_annotation = type_scheme.map(|s| format!(": {}", s.ty)).unwrap_or_default();
            format!("{}{}", symbol.value.as_deref().unwrap_or_default(), type_annotation)
        }
        _ => return JsValue::NULL,
    };

    let result = HoverResult {
        content: format_hover_content(&signature, &symbol.doc, deprecated),
    };
    serde_wasm_bindgen::to_value(&result).unwrap_or(JsValue::NULL)
}

#[cfg(test)]
mod tests {
    use super::*;
    use wasm_bindgen_test::*;
    wasm_bindgen_test_configure!(run_in_browser);

    #[allow(unused)]
    #[wasm_bindgen_test]
    async fn test_script_run_simple() {
        let result = run(
            "downcase() | ltrimstr(\"hello\") | upcase() | trim()",
            "Hello world",
            serde_wasm_bindgen::to_value(&Options {
                is_update: true,
                input_format: None,
                list_style: None,
                link_title_style: None,
                link_url_style: None,
            })
            .unwrap(),
        );
        assert_eq!(result.await.unwrap(), "WORLD\n");
    }

    #[allow(unused)]
    #[wasm_bindgen_test]
    async fn test_script_run_list() {
        let result = run(
            ".[]",
            "- test",
            serde_wasm_bindgen::to_value(&Options {
                is_update: true,
                input_format: None,
                list_style: Some(ListStyle::Star),
                link_title_style: None,
                link_url_style: None,
            })
            .unwrap(),
        );
        assert_eq!(result.await.unwrap(), "* test\n");
    }

    #[allow(unused)]
    #[wasm_bindgen_test]
    async fn test_script_run_link() {
        let result = run(
            ".link",
            "[test](https://example.com)",
            serde_wasm_bindgen::to_value(&Options {
                is_update: true,
                input_format: None,
                list_style: None,
                link_title_style: None,
                link_url_style: Some(UrlSurroundStyle::Angle),
            })
            .unwrap(),
        );
        assert_eq!(result.await.unwrap(), "[test](<https://example.com>)\n");
    }

    #[allow(unused)]
    #[wasm_bindgen_test]
    async fn test_script_run_invalid_syntax() {
        assert!(
            run(
                "invalid syntax",
                "test",
                serde_wasm_bindgen::to_value(&Options {
                    is_update: true,
                    input_format: None,
                    list_style: None,
                    link_title_style: None,
                    link_url_style: None,
                })
                .unwrap()
            )
            .await
            .is_err()
        );
    }

    #[allow(unused)]
    #[wasm_bindgen_test]
    async fn test_script_format() {
        let result = format(r#"downcase()|ltrimstr("hello")|upcase()|trim()"#).await.unwrap();
        assert_eq!(result, r#"downcase() | ltrimstr("hello") | upcase() | trim()"#);
    }

    #[allow(unused)]
    #[wasm_bindgen_test]
    async fn test_script_format_invalid() {
        assert!(format("x=>").await.is_err());
    }

    #[allow(unused)]
    #[wasm_bindgen_test]
    async fn test_defined_values_without_module() {
        let code = r#"
            def foo(x): x | upcase();
            def bar(y): y | downcase();
            | let $var = 42;
        "#;

        let result = defined_values(code, None).await.unwrap();
        let values: Vec<DefinedValue> = serde_wasm_bindgen::from_value(result).unwrap();

        // Should return all defined values
        assert!(values.iter().any(|v| v.name == "foo"));
        assert!(values.iter().any(|v| v.name == "bar"));
        assert!(values.iter().any(|v| v.name == "$var"));
    }

    #[allow(unused)]
    #[wasm_bindgen_test]
    async fn test_defined_values_with_module() {
        let code = r#"
            module mymodule:
                def module_func(x): x | upcase();
                def another_func(y): y | downcase();
                | let $module_var = 100;
            end

            def top_level_func(z): z;
            | let $top_level_var = 42;
        "#;

        let result = defined_values(code, Some("mymodule".to_string())).await.unwrap();
        let values: Vec<DefinedValue> = serde_wasm_bindgen::from_value(result).unwrap();

        // Should return only values from mymodule
        assert!(values.iter().any(|v| v.name == "module_func"));
        assert!(values.iter().any(|v| v.name == "another_func"));
        assert!(values.iter().any(|v| v.name == "$module_var"));

        // Should NOT return top-level values
        assert!(!values.iter().any(|v| v.name == "top_level_func"));
        assert!(!values.iter().any(|v| v.name == "$top_level_var"));
    }

    #[allow(unused)]
    #[wasm_bindgen_test]
    async fn test_defined_values_with_nonexistent_module() {
        let code = r#"
            def foo(x): x | upcase();
        "#;

        let result = defined_values(code, Some("nonexistent".to_string())).await.unwrap();
        let values: Vec<DefinedValue> = serde_wasm_bindgen::from_value(result).unwrap();

        // Should return empty array since module doesn't exist
        assert!(!values.is_empty());
    }

    #[allow(unused)]
    #[wasm_bindgen_test]
    async fn test_wasm_module_resolver_add_module() {
        let resolver = WasmModuleResolver::new();

        // Manually add a module to cache
        resolver.add_module("test", "def foo(x): x | upcase();".to_string());

        // Should be able to resolve it
        let result = mq_lang::ModuleResolver::resolve(&resolver, "test");
        #[cfg(feature = "opfs")]
        {
            assert!(result.is_ok());
            assert_eq!(result.unwrap(), "def foo(x): x | upcase();");
        }
        #[cfg(not(feature = "opfs"))]
        assert!(result.is_err());
    }

    #[allow(unused)]
    #[wasm_bindgen_test]
    async fn test_wasm_module_resolver_not_found() {
        let resolver = WasmModuleResolver::new();

        // Should fail when module is not in cache
        let result = mq_lang::ModuleResolver::resolve(&resolver, "nonexistent");
        assert!(result.is_err());
    }

    #[allow(unused)]
    #[wasm_bindgen_test]
    async fn test_wasm_module_resolver_clear_cache() {
        let resolver = WasmModuleResolver::new();

        // Add a module
        resolver.add_module("test", "content".to_string());
        #[cfg(feature = "opfs")]
        assert!(mq_lang::ModuleResolver::resolve(&resolver, "test").is_ok());

        // Clear cache
        resolver.clear_cache();

        // Should no longer be resolvable
        assert!(mq_lang::ModuleResolver::resolve(&resolver, "test").is_err());
    }

    #[cfg(feature = "opfs")]
    #[allow(unused)]
    #[wasm_bindgen_test]
    async fn test_opfs_create_and_import_module() {
        use opfs::{FileHandle as _, WritableFileStream as _};

        // Initialize OPFS
        let resolver = WasmModuleResolver::new();
        resolver.initialize().await;

        // Skip test if OPFS is not available
        if !*resolver.is_available.borrow() {
            return;
        }

        // Get root directory handle
        let root = opfs::persistent::app_specific_dir()
            .await
            .expect("Failed to get OPFS root directory");

        // Create a test module file in OPFS
        let module_content = r#"def upcase_exclaim(x): x | upcase() | s"${self}!";"#;
        let file_name = "test_module.mq";

        // Write the module file to OPFS
        {
            use opfs::DirectoryHandle as _;

            let mut file_handle = root
                .get_file_handle_with_options(file_name, &opfs::GetFileHandleOptions { create: true })
                .await
                .expect("Failed to get file handle");

            let mut writer = file_handle
                .create_writable_with_options(&opfs::CreateWritableOptions {
                    keep_existing_data: false,
                })
                .await
                .expect("Failed to create writable");

            writer
                .write_at_cursor_pos(module_content.as_bytes())
                .await
                .expect("Failed to write to file");

            writer.close().await.expect("Failed to close writer");
        }

        // Preload modules from OPFS
        resolver.preload_modules().await;

        // Verify the module was loaded into cache
        let resolved_content =
            mq_lang::ModuleResolver::resolve(&resolver, "test_module").expect("Module should be found in cache");
        assert_eq!(resolved_content, module_content);

        // Test using the imported module in code execution
        let code = r#"
            let tm = import "test_module"
            | tm::upcase_exclaim()
        "#;

        let mut engine = mq_lang::Engine::new(resolver.clone());
        engine.load_builtin_module();

        let input = mq_lang::parse_text_input("hello world").expect("Failed to parse input");

        let result = engine
            .eval(code, input.into_iter())
            .expect("Failed to evaluate code with imported module");

        // Convert result to string and verify
        let output: Vec<String> = result.into_iter().map(|v| v.to_string()).collect();

        assert_eq!(output.join(""), "HELLO WORLD!");
    }

    #[cfg(feature = "opfs")]
    #[allow(unused)]
    #[wasm_bindgen_test]
    async fn test_opfs_multiple_modules() {
        use opfs::{DirectoryHandle as _, FileHandle as _, WritableFileStream as _};

        // Initialize OPFS
        let resolver = WasmModuleResolver::new();
        resolver.initialize().await;

        // Skip test if OPFS is not available
        if !*resolver.is_available.borrow() {
            return;
        }

        let root = opfs::persistent::app_specific_dir()
            .await
            .expect("Failed to get OPFS root directory");

        // Create multiple test module files
        let modules = vec![
            ("math.mq", r#"def double(x): x * 2;"#),
            ("string.mq", r#"def greet(name): s"Hello, ${name}!";"#),
        ];

        for (file_name, content) in &modules {
            let mut file_handle = root
                .get_file_handle_with_options(file_name, &opfs::GetFileHandleOptions { create: true })
                .await
                .unwrap_or_else(|_| panic!("Failed to get file handle for {}", file_name));

            let mut writer = file_handle
                .create_writable_with_options(&opfs::CreateWritableOptions {
                    keep_existing_data: false,
                })
                .await
                .unwrap_or_else(|_| panic!("Failed to create writable for {}", file_name));

            writer
                .write_at_cursor_pos(content.as_bytes())
                .await
                .unwrap_or_else(|_| panic!("Failed to write to {}", file_name));

            writer
                .close()
                .await
                .unwrap_or_else(|_| panic!("Failed to close writer for {}", file_name));
        }

        // Preload all modules
        resolver.preload_modules().await;

        // Verify all modules are loaded
        assert!(mq_lang::ModuleResolver::resolve(&resolver, "math").is_ok());
        assert!(mq_lang::ModuleResolver::resolve(&resolver, "string").is_ok());

        // Test using multiple imported modules
        let code = r#"
            import "string"
            | string::greet("World")
        "#;

        let mut engine = mq_lang::Engine::new(resolver.clone());
        engine.load_builtin_module();

        let input = mq_lang::null_input();
        let result = engine.eval(code, input.into_iter()).expect("Failed to evaluate code");

        let output: Vec<String> = result.into_iter().map(|v| v.to_string()).collect();

        assert_eq!(output.join(""), "Hello, World!");

        // Note: File cleanup is skipped as OPFS persistent storage is isolated per origin
    }

    #[allow(unused)]
    #[wasm_bindgen_test]
    async fn test_html_to_markdown() {
        let html = r#"<h1>Hello World</h1><p>This is a <strong>test</strong>.</p>"#;
        let options = ConversionOptions::default();

        let result = html_to_markdown(html, Some(options)).await;
        assert!(result.is_ok());
        let markdown = result.unwrap();
        assert!(markdown.contains("# Hello World"));
        assert!(markdown.contains("**test**"));
    }

    #[allow(unused)]
    #[wasm_bindgen_test]
    async fn test_to_html() {
        let markdown = "# Hello World\n\nThis is a **test**.";
        let result = to_html(markdown).await;

        assert!(result.contains("<h1>Hello World</h1>"));
        assert!(result.contains("<strong>test</strong>"));
    }
}
