#![allow(clippy::useless_conversion)]

use crate::context::Context;
use crate::emitters::jsonl_emitter::{JsonlBlock, JsonlElement, JsonlQuotedString};
use crate::emitters::wasot_file_emitter::WasotFileEmitter;
use crate::events::{Emitter, Event};
use crate::parsers::selectors::{
    parser::parse_selector as rust_parse_selector,
    specificity::{calculate_simple_selector_group as rust_calculate_specificity, Specificity},
};
use crate::parsers::wasot::parser::{ExternalIdReference, Link, TargetType};
use crate::parsers::wasot::{Footnote, Id, MetaLine, ParsedArguments, QuotedString};
use crate::tools::build_all_stdlib_tools;
use crate::transformers::{transformer_pipeline, SelectorTransformer};
use pyo3::prelude::*;
use serde_json::json;
use std::collections::HashMap;
use std::sync::{Arc, Mutex};

/// Type of link target in Wasot.
///
/// Variants:
///     URI: External URL (https://, mailto:, etc.)
///     ID: Local ID reference within the document
///     FOOTNOTE: Footnote reference (*1, *2, etc.)
///     ID_IN_DOCUMENT: Cross-document reference
#[pyclass(name = "TargetType", eq, eq_int)]
#[derive(Clone, PartialEq, Debug)]
pub enum PyTargetType {
    #[pyo3(name = "URI")]
    Uri = 0,
    #[pyo3(name = "FOOTNOTE")]
    FootNote = 1,
    #[pyo3(name = "ID")]
    Id = 2,
    #[pyo3(name = "ID_IN_DOCUMENT")]
    IdInDocument = 3,
}

#[pymethods]
impl PyTargetType {
    fn __repr__(&self) -> String {
        match self {
            Self::Uri => "TargetType.URI".to_string(),
            Self::FootNote => "TargetType.FOOTNOTE".to_string(),
            Self::Id => "TargetType.ID".to_string(),
            Self::IdInDocument => "TargetType.ID_IN_DOCUMENT".to_string(),
        }
    }
}

impl From<&TargetType> for PyTargetType {
    fn from(tt: &TargetType) -> Self {
        match tt {
            TargetType::Uri => Self::Uri,
            TargetType::FootNote => Self::FootNote,
            TargetType::Id => Self::Id,
            TargetType::IdInDocument(_) => Self::IdInDocument,
        }
    }
}

/// External ID reference for cross-document links.
///
/// Attributes:
///     document_id (str): The target document's ID.
///     local_id (str): The ID within that document.
///     document_path (str | None): Resolved file path if known.
#[pyclass(name = "ExternalIdReference")]
#[derive(Clone)]
pub struct PyExternalIdReference {
    #[pyo3(get)]
    document_id: String,
    #[pyo3(get)]
    local_id: String,
    #[pyo3(get)]
    document_path: Option<String>,
}

#[pymethods]
impl PyExternalIdReference {
    #[new]
    #[pyo3(signature = (document_id, local_id, document_path=None))]
    fn new(document_id: String, local_id: String, document_path: Option<String>) -> Self {
        Self {
            document_id,
            local_id,
            document_path,
        }
    }

    fn __repr__(&self) -> String {
        format!(
            "ExternalIdReference(document_id={:?}, local_id={:?}, document_path={:?})",
            self.document_id, self.local_id, self.document_path
        )
    }

    fn __str__(&self) -> String {
        self.__repr__()
    }
}

impl From<&ExternalIdReference> for PyExternalIdReference {
    fn from(eir: &ExternalIdReference) -> Self {
        Self {
            document_id: eir.document_id.value.clone(),
            local_id: eir.local_id.value.clone(),
            document_path: eir.document_path.clone(),
        }
    }
}

/// A footnote definition.
///
/// Attributes:
///     marker (int): The footnote number (1, 2, 3, etc.)
///     link (str): The footnote content/URL.
#[pyclass(name = "Footnote")]
#[derive(Clone)]
pub struct PyFootnote {
    #[pyo3(get)]
    marker: u32,
    #[pyo3(get)]
    link: String,
}

#[pymethods]
impl PyFootnote {
    #[new]
    fn new(marker: u32, link: String) -> Self {
        Self { marker, link }
    }

    fn __repr__(&self) -> String {
        format!("Footnote(marker={}, link={:?})", self.marker, self.link)
    }

    fn __str__(&self) -> String {
        self.__repr__()
    }
}

impl From<&Footnote> for PyFootnote {
    fn from(f: &Footnote) -> Self {
        Self {
            marker: f.marker,
            link: f.link.clone(),
        }
    }
}

/// Parsed arguments from a block or link.
///
/// Attributes:
///     args (list[QuotedString]): Positional arguments.
///     kwargs (list[tuple[QuotedString, QuotedString]]): Keyword arguments as (key, value) tuples.
#[pyclass(name = "Arguments")]
#[derive(Clone)]
pub struct PyArguments {
    #[pyo3(get)]
    args: Vec<PyQuotedString>,
    #[pyo3(get)]
    kwargs: Vec<(PyQuotedString, PyQuotedString)>,
}

#[pymethods]
impl PyArguments {
    #[new]
    #[pyo3(signature = (args=None, kwargs=None))]
    fn new(
        args: Option<Vec<PyQuotedString>>,
        kwargs: Option<Vec<(PyQuotedString, PyQuotedString)>>,
    ) -> Self {
        Self {
            args: args.unwrap_or_default(),
            kwargs: kwargs.unwrap_or_default(),
        }
    }

    fn __repr__(&self) -> String {
        let args_repr: Vec<String> = self.args.iter().map(|a| a.__repr__()).collect();
        let kwargs_repr: Vec<String> = self
            .kwargs
            .iter()
            .map(|(k, v)| format!("({}, {})", k.__repr__(), v.__repr__()))
            .collect();
        format!(
            "Arguments(args=[{}], kwargs=[{}])",
            args_repr.join(", "),
            kwargs_repr.join(", ")
        )
    }

    fn __str__(&self) -> String {
        self.__repr__()
    }
}

/// A link within text content.
///
/// Attributes:
///     name (QuotedString | None): Display name of the link.
///     target (str | None): Target reference.
///     target_type (TargetType): Type of target (URI, ID, FOOTNOTE, ID_IN_DOCUMENT).
///     arguments (Arguments): Link arguments.
///     start_index (int): Position in source text.
///     length (int): Length in source text.
///     document_ref (ExternalIdReference | None): Cross-document reference details.
#[pyclass(name = "Link")]
#[derive(Clone)]
pub struct PyLink {
    #[pyo3(get)]
    name: Option<PyQuotedString>,
    #[pyo3(get)]
    target: Option<String>,
    #[pyo3(get)]
    target_type: PyTargetType,
    #[pyo3(get)]
    arguments: PyArguments,
    #[pyo3(get)]
    start_index: usize,
    #[pyo3(get)]
    length: usize,
    #[pyo3(get)]
    document_ref: Option<PyExternalIdReference>,
}

#[pymethods]
impl PyLink {
    #[new]
    #[pyo3(signature = (target_type, start_index, length, name=None, target=None, arguments=None, document_ref=None))]
    fn new(
        target_type: PyTargetType,
        start_index: usize,
        length: usize,
        name: Option<PyQuotedString>,
        target: Option<String>,
        arguments: Option<PyArguments>,
        document_ref: Option<PyExternalIdReference>,
    ) -> Self {
        Self {
            name,
            target,
            target_type,
            arguments: arguments.unwrap_or_else(|| PyArguments::new(None, None)),
            start_index,
            length,
            document_ref,
        }
    }

    fn __repr__(&self) -> String {
        let name_repr = self
            .name
            .as_ref()
            .map_or("None".to_string(), |n| n.__repr__());
        let doc_ref_repr = self
            .document_ref
            .as_ref()
            .map_or("None".to_string(), |d| d.__repr__());
        format!(
            "Link(name={}, target={:?}, target_type={:?}, arguments={}, start_index={}, length={}, document_ref={})",
            name_repr,
            self.target,
            self.target_type,
            self.arguments.__repr__(),
            self.start_index,
            self.length,
            doc_ref_repr
        )
    }

    fn __str__(&self) -> String {
        self.__repr__()
    }
}

/// A string value that may or may not have been quoted in the source.
///
/// Attributes:
///     content (str): The actual string content.
///     quoted (bool): True if the string was quoted in the source.
///     links (list[Link]): Links detected within the content.
#[pyclass(name = "QuotedString")]
#[derive(Clone)]
pub struct PyQuotedString {
    #[pyo3(get)]
    content: String,
    #[pyo3(get)]
    quoted: bool,
    #[pyo3(get)]
    links: Vec<PyLink>,
}

#[pymethods]
impl PyQuotedString {
    #[new]
    #[pyo3(signature = (content, quoted=None, links=None))]
    fn new(content: String, quoted: Option<bool>, links: Option<Vec<PyLink>>) -> Self {
        Self {
            content,
            quoted: quoted.unwrap_or(false),
            links: links.unwrap_or_default(),
        }
    }

    fn __str__(&self) -> String {
        self.content.clone()
    }

    fn __repr__(&self) -> String {
        let links_repr: Vec<String> = self.links.iter().map(|l| l.__repr__()).collect();
        format!(
            "QuotedString(content={:?}, quoted={}, links=[{}])",
            self.content,
            self.quoted,
            links_repr.join(", ")
        )
    }
}

/// Convert a Rust Link to PyLink (needs to be a function due to circular dependency)
fn convert_link(link: &Link) -> PyLink {
    // Convert name - handle circular reference by not including links in name's QuotedString
    let name = link.name.as_ref().map(|qs| PyQuotedString {
        content: qs.content.clone(),
        quoted: qs.quoted,
        links: vec![], // Don't recurse into links within link names
    });

    // Convert arguments
    let arguments = PyArguments {
        args: link
            .arguments
            .args
            .iter()
            .map(|qs| PyQuotedString {
                content: qs.content.clone(),
                quoted: qs.quoted,
                links: vec![], // Don't recurse
            })
            .collect(),
        kwargs: link
            .arguments
            .kwargs
            .iter()
            .map(|(k, v)| {
                (
                    PyQuotedString {
                        content: k.content.clone(),
                        quoted: k.quoted,
                        links: vec![],
                    },
                    PyQuotedString {
                        content: v.content.clone(),
                        quoted: v.quoted,
                        links: vec![],
                    },
                )
            })
            .collect(),
    };

    // Extract document_ref if target_type is IdInDocument
    let document_ref = match &link.target_type {
        TargetType::IdInDocument(eir) => Some(PyExternalIdReference::from(eir)),
        _ => None,
    };

    PyLink {
        name,
        target: link.target.clone(),
        target_type: PyTargetType::from(&link.target_type),
        arguments,
        start_index: link.start_index,
        length: link.length,
        document_ref,
    }
}

/// Convert a Rust QuotedString to PyQuotedString
fn convert_quoted_string(qs: &QuotedString) -> PyQuotedString {
    PyQuotedString {
        content: qs.content.clone(),
        quoted: qs.quoted,
        links: qs.links.iter().map(convert_link).collect(),
    }
}

impl From<&QuotedString> for PyQuotedString {
    fn from(qs: &QuotedString) -> Self {
        convert_quoted_string(qs)
    }
}

impl From<QuotedString> for PyQuotedString {
    fn from(qs: QuotedString) -> Self {
        convert_quoted_string(&qs)
    }
}

impl From<&ParsedArguments> for PyArguments {
    fn from(args: &ParsedArguments) -> Self {
        Self {
            args: args.args.iter().map(convert_quoted_string).collect(),
            kwargs: args
                .kwargs
                .iter()
                .map(|(k, v)| (convert_quoted_string(k), convert_quoted_string(v)))
                .collect(),
        }
    }
}

// ============================================================================
// JSON Serialization/Deserialization Helpers
// ============================================================================

/// Convert a PyQuotedString to JsonlQuotedString
fn py_quoted_string_to_jsonl(qs: &PyQuotedString) -> JsonlQuotedString {
    JsonlQuotedString {
        content: qs.content.clone(),
        quoted: qs.quoted,
    }
}

/// Convert a JsonlQuotedString to PyQuotedString
fn jsonl_to_py_quoted_string(jqs: &JsonlQuotedString) -> PyQuotedString {
    PyQuotedString {
        content: jqs.content.clone(),
        quoted: jqs.quoted,
        links: vec![], // JSONL format doesn't preserve links inside quoted strings
    }
}

/// Convert PyMetaline to JsonlBlock format (for serialization)
fn py_metaline_to_jsonl_block(meta: &PyMetaline, is_context_line: bool) -> JsonlBlock {
    let args: Vec<JsonlQuotedString> = meta
        .arguments
        .args
        .iter()
        .map(py_quoted_string_to_jsonl)
        .collect();

    let kwargs: Vec<(JsonlQuotedString, JsonlQuotedString)> = meta
        .arguments
        .kwargs
        .iter()
        .map(|(k, v)| (py_quoted_string_to_jsonl(k), py_quoted_string_to_jsonl(v)))
        .collect();

    let inline_text = if meta.inline_text.content.is_empty() {
        None
    } else {
        Some(py_quoted_string_to_jsonl(&meta.inline_text))
    };

    JsonlBlock {
        block_type: meta.type_name.clone(),
        id: meta.block_id.clone(),
        level: meta.level,
        is_void: meta.is_void,
        is_context_line,
        args,
        kwargs,
        inline_text,
        content: vec![],
    }
}

/// Convert JsonlBlock to PyMetaline
fn jsonl_block_to_py_metaline(block: &JsonlBlock) -> PyMetaline {
    let args: Vec<PyQuotedString> = block.args.iter().map(jsonl_to_py_quoted_string).collect();

    let kwargs: Vec<(PyQuotedString, PyQuotedString)> = block
        .kwargs
        .iter()
        .map(|(k, v)| (jsonl_to_py_quoted_string(k), jsonl_to_py_quoted_string(v)))
        .collect();

    let inline_text = block
        .inline_text
        .as_ref()
        .map(jsonl_to_py_quoted_string)
        .unwrap_or_else(|| PyQuotedString::new(String::new(), None, None));

    PyMetaline {
        type_name: block.block_type.clone(),
        inline_text,
        level: block.level,
        line_index: None, // JSONL format doesn't preserve line_index
        arguments: PyArguments { args, kwargs },
        block_id: block.id.clone(),
        is_void: block.is_void,
    }
}

/// A keyword argument from a metaline (e.g., `key=value` or `key="quoted value"`).
///
/// Attributes:
///     key (QuotedString): The argument key.
///     value (QuotedString): The argument value.
#[pyclass(name = "KeywordArgument")]
#[derive(Clone)]
pub struct PyKeywordArgument {
    #[pyo3(get)]
    key: PyQuotedString,
    #[pyo3(get)]
    value: PyQuotedString,
}

#[pymethods]
impl PyKeywordArgument {
    #[new]
    fn new(key: PyQuotedString, value: PyQuotedString) -> Self {
        Self { key, value }
    }
}

/// A positional argument from a metaline (e.g., `arg1` or `"quoted arg"`).
///
/// Attributes:
///     value (QuotedString): The argument value.
#[pyclass(name = "PositionalArgument")]
#[derive(Clone)]
pub struct PyPositionalArgument {
    #[pyo3(get)]
    value: PyQuotedString,
}

#[pymethods]
impl PyPositionalArgument {
    #[new]
    fn new(value: PyQuotedString) -> Self {
        Self { value }
    }
}

/// A parsed metaline representing a Wasot block definition.
///
/// A metaline is the opening line of a block, e.g., `.paragraph key=value .. inline text`
///
/// Attributes:
///     type_name (str): The block type name (e.g., "paragraph", "section").
///     inline_text (QuotedString): Text after `..` on the same line, with any embedded links.
///     level (int): Nesting level (number of dots).
///     line_index (int | None): Line number in the source file.
///     arguments (Arguments): Positional and keyword arguments.
///     block_id (str | None): Optional block ID (e.g., from `:my-id`).
///     is_void (bool): True if this is a void element (trailing colon syntax).
///
/// Legacy attributes (for backwards compatibility):
///     positional_args (list[PositionalArgument]): Positional arguments.
///     keyword_args (list[KeywordArgument]): Keyword arguments.
#[pyclass(name = "Metaline")]
#[derive(Clone)]
pub struct PyMetaline {
    #[pyo3(get)]
    type_name: String,
    #[pyo3(get)]
    inline_text: PyQuotedString,
    #[pyo3(get)]
    level: usize,
    #[pyo3(get)]
    line_index: Option<usize>,
    #[pyo3(get)]
    arguments: PyArguments,
    #[pyo3(get)]
    block_id: Option<String>,
    #[pyo3(get)]
    is_void: bool,
}

#[pymethods]
impl PyMetaline {
    #[new]
    #[pyo3(signature = (type_name, level, inline_text=None, line_index=None, arguments=None, block_id=None, is_void=None))]
    fn new(
        type_name: String,
        level: usize,
        inline_text: Option<PyQuotedString>,
        line_index: Option<usize>,
        arguments: Option<PyArguments>,
        block_id: Option<String>,
        is_void: Option<bool>,
    ) -> Self {
        Self {
            type_name,
            inline_text: inline_text
                .unwrap_or_else(|| PyQuotedString::new(String::new(), None, None)),
            level,
            line_index,
            arguments: arguments.unwrap_or_else(|| PyArguments::new(None, None)),
            block_id,
            is_void: is_void.unwrap_or(false),
        }
    }

    /// Legacy accessor: get inline_text as string (for backwards compatibility)
    #[getter]
    fn inline_text_str(&self) -> String {
        self.inline_text.content.clone()
    }

    /// Legacy accessor: positional arguments
    #[getter]
    fn positional_args(&self) -> Vec<PyPositionalArgument> {
        self.arguments
            .args
            .iter()
            .map(|qs| PyPositionalArgument { value: qs.clone() })
            .collect()
    }

    /// Legacy accessor: keyword arguments
    #[getter]
    fn keyword_args(&self) -> Vec<PyKeywordArgument> {
        self.arguments
            .kwargs
            .iter()
            .map(|(k, v)| PyKeywordArgument {
                key: k.clone(),
                value: v.clone(),
            })
            .collect()
    }

    fn __repr__(&self) -> String {
        format!(
            "Metaline(type_name={:?}, level={}, inline_text={}, line_index={:?}, arguments={}, block_id={:?}, is_void={})",
            self.type_name,
            self.level,
            self.inline_text.__repr__(),
            self.line_index,
            self.arguments.__repr__(),
            self.block_id,
            self.is_void
        )
    }

    fn __str__(&self) -> String {
        self.__repr__()
    }
}

impl PyMetaline {
    fn from_meta_line(meta_line: &MetaLine) -> Self {
        Self {
            type_name: meta_line.type_name.clone(),
            inline_text: convert_quoted_string(&meta_line.inline_text),
            level: meta_line.level,
            line_index: meta_line.line_index,
            arguments: PyArguments::from(&meta_line.arguments),
            block_id: if meta_line.id.value.is_empty() {
                None
            } else {
                Some(meta_line.id.value.clone())
            },
            is_void: meta_line.is_void,
        }
    }
}

/// A context object for storing and retrieving values during parsing.
///
/// Context provides both global and scoped storage. Scoped variables are
/// automatically managed based on block nesting level.
///
/// Methods:
///     put(key, value): Store a global value.
///     get(key) -> value: Retrieve a value (checks scoped first, then global).
///     put_scoped(key, value): Store a scoped value at current nesting level.
///     get_scoped(key) -> value: Retrieve a scoped value.
///
/// Attributes:
///     items (dict): All global items.
///     scoped_items (dict): All visible scoped items at current level.
#[pyclass(name = "Context")]
#[derive(Clone)]
pub struct PyContext {
    // Use the general-purpose Context internally for logic
    context: Context,
    // Keep Python objects separately for proper Python interop
    py_items: Arc<Mutex<HashMap<String, PyObject>>>,
    py_scoped_items: Arc<Mutex<Vec<HashMap<String, PyObject>>>>,
}

#[pymethods]
impl PyContext {
    #[new]
    fn new() -> Self {
        // Create context with stdlib tools included
        let stdlib_tools = build_all_stdlib_tools().unwrap_or_else(|e| {
            eprintln!("Warning: Failed to load stdlib tools: {e}");
            Vec::new()
        });

        Self {
            context: Context::with_tools(stdlib_tools),
            py_items: Arc::new(Mutex::new(HashMap::new())),
            py_scoped_items: Arc::new(Mutex::new(vec![HashMap::new()])),
        }
    }

    fn put(&self, key: String, value: PyObject) {
        // Store in Python storage
        Python::with_gil(|py| {
            let mut items = self.py_items.lock().unwrap();
            items.insert(key.clone(), value.clone_ref(py));

            // Also store a JSON representation in the underlying context
            if let Ok(s) = value.extract::<String>(py) {
                self.context.put(key, json!(s));
            } else if let Ok(i) = value.extract::<i64>(py) {
                self.context.put(key, json!(i));
            } else if let Ok(f) = value.extract::<f64>(py) {
                self.context.put(key, json!(f));
            } else if let Ok(b) = value.extract::<bool>(py) {
                self.context.put(key, json!(b));
            } else if value.is_none(py) {
                self.context.put(key, json!(null));
            } else {
                // For complex types, store string representation
                self.context.put(key, json!(value.to_string()));
            }
        });
    }

    fn get(&self, key: String, py: Python) -> PyObject {
        // First check scoped Python items
        let scoped = self.py_scoped_items.lock().unwrap();
        let depth = self.context.current_depth();

        // Search from current depth up to root (0)
        for level in (0..=depth.min(scoped.len().saturating_sub(1))).rev() {
            if let Some(scope) = scoped.get(level) {
                if let Some(value) = scope.get(&key) {
                    return value.clone_ref(py);
                }
            }
        }

        // Fall back to global Python items
        let items = self.py_items.lock().unwrap();
        items
            .get(&key)
            .map(|v| v.clone_ref(py))
            .unwrap_or_else(|| py.None())
    }

    /// Put a scoped variable at the current stack level
    fn put_scoped(&self, key: String, value: PyObject) {
        Python::with_gil(|py| {
            let mut scoped = self.py_scoped_items.lock().unwrap();
            let depth = self.context.current_depth();

            // Ensure we have enough levels
            while scoped.len() <= depth {
                scoped.push(HashMap::new());
            }

            if let Some(scope) = scoped.get_mut(depth) {
                scope.insert(key.clone(), value.clone_ref(py));
            }

            // Also store in underlying context
            if let Ok(s) = value.extract::<String>(py) {
                self.context.put_scoped(key, json!(s));
            } else if let Ok(i) = value.extract::<i64>(py) {
                self.context.put_scoped(key, json!(i));
            } else if let Ok(f) = value.extract::<f64>(py) {
                self.context.put_scoped(key, json!(f));
            } else if let Ok(b) = value.extract::<bool>(py) {
                self.context.put_scoped(key, json!(b));
            } else if value.is_none(py) {
                self.context.put_scoped(key, json!(null));
            } else {
                self.context.put_scoped(key, json!(value.to_string()));
            }
        });
    }

    /// Get a scoped variable (searches from current level up to root)
    fn get_scoped(&self, key: String, py: Python) -> PyObject {
        let scoped = self.py_scoped_items.lock().unwrap();
        let depth = self.context.current_depth();

        // Search from current depth up to root (0)
        for level in (0..=depth.min(scoped.len().saturating_sub(1))).rev() {
            if let Some(scope) = scoped.get(level) {
                if let Some(value) = scope.get(&key) {
                    return value.clone_ref(py);
                }
            }
        }

        py.None()
    }

    #[getter]
    fn items(&self, py: Python) -> PyResult<PyObject> {
        let items = self.py_items.lock().unwrap();
        let dict = pyo3::types::PyDict::new_bound(py);
        for (k, v) in items.iter() {
            dict.set_item(k, v)?;
        }
        Ok(dict.into())
    }

    #[getter]
    fn scoped_items(&self, py: Python) -> PyResult<PyObject> {
        let scoped = self.py_scoped_items.lock().unwrap();
        let depth = self.context.current_depth();

        // Create a dict with all visible scoped variables
        let dict = pyo3::types::PyDict::new_bound(py);

        // Add variables from all levels up to current depth (root to current)
        for level in 0..=depth.min(scoped.len().saturating_sub(1)) {
            if let Some(scope) = scoped.get(level) {
                for (k, v) in scope.iter() {
                    dict.set_item(k, v)?; // Later levels override earlier ones
                }
            }
        }

        Ok(dict.into())
    }
}

impl PyContext {
    /// Internal method to handle events for automatic scope management
    /// This feeds events to the underlying Context which implements Consumer
    fn handle_event(&self, event: &Event) {
        // Let the Context handle its own scope management
        self.context.handle_event(event);

        // Sync Python scoped storage with Context's scope depth
        let depth = self.context.current_depth();
        let mut scoped = self.py_scoped_items.lock().unwrap();

        // Ensure we have enough levels in Python storage
        while scoped.len() <= depth {
            scoped.push(HashMap::new());
        }

        // Clear Python scoped variables to match what Context did
        // When Context clears a scope (e.g., for siblings), we need to clear Python storage too

        // First, clear any Python scoped variables beyond current depth
        for level in (depth + 1)..scoped.len() {
            if let Some(scope) = scoped.get_mut(level) {
                scope.clear();
            }
        }

        // Also check if the current level was cleared by Context (for sibling blocks)
        // We can detect this by checking if the Context's scoped items at this level are empty
        let context_scoped = self.context.visible_scoped_items();
        if context_scoped.is_empty() && depth > 0 {
            // Context cleared the current scope, so clear Python's too
            if let Some(scope) = scoped.get_mut(depth) {
                scope.clear();
            }
        }
    }
}

/// Event emitted when a new block starts.
///
/// This event is fired when the parser encounters a metaline like `.paragraph`.
///
/// Attributes:
///     meta_line (Metaline): The parsed metaline for this block.
///     context (Context): The current parsing context.
///     block_type (str): Shortcut for meta_line.type_name.
///     level (int): Shortcut for meta_line.level.
///     inline_text (str): Shortcut for meta_line.inline_text.
///     line_index (int | None): Shortcut for meta_line.line_index.
#[pyclass(name = "BlockStartedEvent")]
#[derive(Clone)]
pub struct PyBlockStartedEvent {
    #[pyo3(get)]
    meta_line: PyMetaline,
    #[pyo3(get)]
    context: PyContext,
}

#[pymethods]
impl PyBlockStartedEvent {
    #[new]
    fn new(meta_line: PyMetaline, context: PyContext) -> Self {
        Self { meta_line, context }
    }

    #[getter]
    fn line_index(&self) -> Option<usize> {
        self.meta_line.line_index
    }

    #[getter]
    fn block_type(&self) -> String {
        self.meta_line.type_name.clone()
    }

    fn __repr__(&self) -> String {
        format!(
            "BlockStartedEvent(meta_line={}, context=Context(...))",
            self.meta_line.__repr__()
        )
    }

    fn __str__(&self) -> String {
        self.__repr__()
    }

    /// Serialize the event to JSON string (JSONL format compatible)
    fn to_json(&self) -> PyResult<String> {
        let jsonl_block = py_metaline_to_jsonl_block(&self.meta_line, false);
        let element = JsonlElement::Block(jsonl_block);
        serde_json::to_string(&element).map_err(|e| {
            PyErr::new::<pyo3::exceptions::PyValueError, _>(format!(
                "Failed to serialize to JSON: {e}"
            ))
        })
    }

    /// Deserialize a BlockStartedEvent from JSON string
    #[staticmethod]
    fn from_json(json_str: &str) -> PyResult<Self> {
        let element: JsonlElement = serde_json::from_str(json_str).map_err(|e| {
            PyErr::new::<pyo3::exceptions::PyValueError, _>(format!("Failed to parse JSON: {e}"))
        })?;

        match element {
            JsonlElement::Block(block) => {
                if block.is_context_line || block.is_void {
                    return Err(PyErr::new::<pyo3::exceptions::PyValueError, _>(
                        "JSON represents a context line or void element, not a block started event",
                    ));
                }
                let meta_line = jsonl_block_to_py_metaline(&block);
                let context = PyContext::new();
                Ok(Self { meta_line, context })
            }
            JsonlElement::Text { .. } => Err(PyErr::new::<pyo3::exceptions::PyValueError, _>(
                "JSON represents a text event, not a block started event",
            )),
        }
    }
}

/// Event emitted when a block ends.
///
/// This event is fired when the parser finishes processing a block,
/// either by encountering a sibling/parent block or an explicit close (`./`).
#[pyclass(name = "BlockFinishedEvent")]
#[derive(Clone)]
pub struct PyBlockFinishedEvent {}

#[pymethods]
impl PyBlockFinishedEvent {
    #[new]
    fn new() -> Self {
        Self {}
    }

    fn __repr__(&self) -> String {
        "BlockFinishedEvent()".to_string()
    }

    fn __str__(&self) -> String {
        self.__repr__()
    }

    /// Serialize the event to JSON string
    /// Note: BlockFinishedEvent has no payload, so this returns a simple marker object
    fn to_json(&self) -> PyResult<String> {
        Ok(r#"{"type":"block_finished"}"#.to_string())
    }

    /// Deserialize a BlockFinishedEvent from JSON string
    #[staticmethod]
    fn from_json(json_str: &str) -> PyResult<Self> {
        // Parse JSON to verify it's valid
        let value: serde_json::Value = serde_json::from_str(json_str).map_err(|e| {
            PyErr::new::<pyo3::exceptions::PyValueError, _>(format!("Failed to parse JSON: {e}"))
        })?;

        // Check if it's a block_finished marker
        if let Some(type_field) = value.get("type") {
            if type_field == "block_finished" {
                return Ok(Self {});
            }
        }

        Err(PyErr::new::<pyo3::exceptions::PyValueError, _>(
            "JSON does not represent a BlockFinishedEvent",
        ))
    }
}

/// Event emitted when text content is encountered.
///
/// This event contains the text lines within a block, along with any links and footnotes.
///
/// Attributes:
///     lines (list[str]): The text content as a list of lines.
///     links (list[list[Link]]): Links found in each line (one list per line).
///     footnotes (list[Footnote]): Footnotes found in the text.
#[pyclass(name = "TextEvent")]
#[derive(Clone)]
pub struct PyTextEvent {
    #[pyo3(get)]
    lines: Vec<String>,
    #[pyo3(get)]
    links: Vec<Vec<PyLink>>,
    #[pyo3(get)]
    footnotes: Vec<PyFootnote>,
}

#[pymethods]
impl PyTextEvent {
    #[new]
    #[pyo3(signature = (lines, links=None, footnotes=None))]
    fn new(
        lines: Vec<String>,
        links: Option<Vec<Vec<PyLink>>>,
        footnotes: Option<Vec<PyFootnote>>,
    ) -> Self {
        Self {
            lines,
            links: links.unwrap_or_default(),
            footnotes: footnotes.unwrap_or_default(),
        }
    }

    /// Get all links flattened into a single list (convenience method)
    fn all_links(&self) -> Vec<PyLink> {
        self.links.iter().flatten().cloned().collect()
    }

    /// Get links for a specific line (0-indexed)
    fn links_for_line(&self, line_index: usize) -> Vec<PyLink> {
        self.links.get(line_index).cloned().unwrap_or_default()
    }

    fn __repr__(&self) -> String {
        let links_repr: Vec<String> = self
            .links
            .iter()
            .map(|line_links| {
                let inner: Vec<String> = line_links.iter().map(|l| l.__repr__()).collect();
                format!("[{}]", inner.join(", "))
            })
            .collect();
        let footnotes_repr: Vec<String> = self.footnotes.iter().map(|f| f.__repr__()).collect();
        format!(
            "TextEvent(lines={:?}, links=[{}], footnotes=[{}])",
            self.lines,
            links_repr.join(", "),
            footnotes_repr.join(", ")
        )
    }

    fn __str__(&self) -> String {
        self.__repr__()
    }

    /// Serialize the event to JSON string (JSONL format compatible)
    fn to_json(&self) -> PyResult<String> {
        let element = JsonlElement::Text {
            lines: self.lines.clone(),
        };
        serde_json::to_string(&element).map_err(|e| {
            PyErr::new::<pyo3::exceptions::PyValueError, _>(format!(
                "Failed to serialize to JSON: {e}"
            ))
        })
    }

    /// Deserialize a TextEvent from JSON string
    #[staticmethod]
    fn from_json(json_str: &str) -> PyResult<Self> {
        let element: JsonlElement = serde_json::from_str(json_str).map_err(|e| {
            PyErr::new::<pyo3::exceptions::PyValueError, _>(format!("Failed to parse JSON: {e}"))
        })?;

        match element {
            JsonlElement::Text { lines } => Ok(Self {
                lines,
                links: vec![],     // JSONL format doesn't preserve links
                footnotes: vec![], // JSONL format doesn't preserve footnotes
            }),
            JsonlElement::Block(_) => Err(PyErr::new::<pyo3::exceptions::PyValueError, _>(
                "JSON represents a block, not a text event",
            )),
        }
    }
}

/// Event emitted for context lines (e.g., `./set foo=bar`).
///
/// Context lines provide metadata without being part of the content hierarchy.
/// They start with `./` followed by a command name.
///
/// Attributes:
///     meta_line (Metaline): The parsed metaline for this context line.
///     block_type (str): The command name (e.g., "set").
///     inline_text (str): The inline content after the command.
///     level (int): The nesting level.
///     line_index (int | None): Line number in the source.
#[pyclass(name = "ContextLineEvent")]
#[derive(Clone)]
pub struct PyContextLineEvent {
    #[pyo3(get)]
    meta_line: PyMetaline,
}

#[pymethods]
impl PyContextLineEvent {
    #[new]
    fn new(meta_line: PyMetaline) -> Self {
        Self { meta_line }
    }

    fn __repr__(&self) -> String {
        format!("ContextLineEvent(meta_line={})", self.meta_line.__repr__())
    }

    fn __str__(&self) -> String {
        self.__repr__()
    }

    /// Serialize the event to JSON string (JSONL format compatible)
    fn to_json(&self) -> PyResult<String> {
        let jsonl_block = py_metaline_to_jsonl_block(&self.meta_line, true);
        let element = JsonlElement::Block(jsonl_block);
        serde_json::to_string(&element).map_err(|e| {
            PyErr::new::<pyo3::exceptions::PyValueError, _>(format!(
                "Failed to serialize to JSON: {e}"
            ))
        })
    }

    /// Deserialize a ContextLineEvent from JSON string
    #[staticmethod]
    fn from_json(json_str: &str) -> PyResult<Self> {
        let element: JsonlElement = serde_json::from_str(json_str).map_err(|e| {
            PyErr::new::<pyo3::exceptions::PyValueError, _>(format!("Failed to parse JSON: {e}"))
        })?;

        match element {
            JsonlElement::Block(block) => {
                if !block.is_context_line {
                    return Err(PyErr::new::<pyo3::exceptions::PyValueError, _>(
                        "JSON does not represent a context line event",
                    ));
                }
                let meta_line = jsonl_block_to_py_metaline(&block);
                Ok(Self { meta_line })
            }
            JsonlElement::Text { .. } => Err(PyErr::new::<pyo3::exceptions::PyValueError, _>(
                "JSON represents a text event, not a context line event",
            )),
        }
    }
}

/// Event emitted for void elements (e.g., `.hr:`).
///
/// Void elements are self-closing and cannot have children, like HTML `<br>` or `<hr>`.
/// They are indicated by a trailing colon in Wasot syntax.
///
/// Attributes:
///     meta_line (Metaline): The parsed metaline for this void element.
///     block_type (str): The element type (e.g., "hr").
///     level (int): The nesting level.
///     inline_text (str): Any inline text after the element name.
///     line_index (int | None): Line number in the source.
#[pyclass(name = "VoidElementEvent")]
#[derive(Clone)]
pub struct PyVoidElementEvent {
    #[pyo3(get)]
    meta_line: PyMetaline,
}

#[pymethods]
impl PyVoidElementEvent {
    #[new]
    fn new(meta_line: PyMetaline) -> Self {
        Self { meta_line }
    }

    fn __repr__(&self) -> String {
        format!("VoidElementEvent(meta_line={})", self.meta_line.__repr__())
    }

    fn __str__(&self) -> String {
        self.__repr__()
    }

    /// Serialize the event to JSON string (JSONL format compatible)
    fn to_json(&self) -> PyResult<String> {
        // Void elements are serialized with is_void=true
        let mut jsonl_block = py_metaline_to_jsonl_block(&self.meta_line, false);
        jsonl_block.is_void = true;
        let element = JsonlElement::Block(jsonl_block);
        serde_json::to_string(&element).map_err(|e| {
            PyErr::new::<pyo3::exceptions::PyValueError, _>(format!(
                "Failed to serialize to JSON: {e}"
            ))
        })
    }

    /// Deserialize a VoidElementEvent from JSON string
    #[staticmethod]
    fn from_json(json_str: &str) -> PyResult<Self> {
        let element: JsonlElement = serde_json::from_str(json_str).map_err(|e| {
            PyErr::new::<pyo3::exceptions::PyValueError, _>(format!("Failed to parse JSON: {e}"))
        })?;

        match element {
            JsonlElement::Block(block) => {
                if !block.is_void {
                    return Err(PyErr::new::<pyo3::exceptions::PyValueError, _>(
                        "JSON does not represent a void element event",
                    ));
                }
                let meta_line = jsonl_block_to_py_metaline(&block);
                Ok(Self { meta_line })
            }
            JsonlElement::Text { .. } => Err(PyErr::new::<pyo3::exceptions::PyValueError, _>(
                "JSON represents a text event, not a void element event",
            )),
        }
    }
}

/// Helper function to convert a Rust Event to the appropriate Python event object
fn convert_event_to_py(event: Event, context: &PyContext, py: Python) -> Option<PyObject> {
    match event {
        Event::BlockStarted { meta_line } => {
            let py_meta_line = PyMetaline::from_meta_line(&meta_line);
            let py_event = PyBlockStartedEvent::new(py_meta_line, context.clone());
            Some(py_event.into_py(py))
        }
        Event::TextFinished {
            lines,
            links,
            footnotes,
        } => {
            // Convert links: Vec<Vec<Link>> -> Vec<Vec<PyLink>>
            let py_links: Vec<Vec<PyLink>> = links
                .iter()
                .map(|line_links| line_links.iter().map(convert_link).collect())
                .collect();

            // Convert footnotes: Vec<Footnote> -> Vec<PyFootnote>
            let py_footnotes: Vec<PyFootnote> = footnotes.iter().map(PyFootnote::from).collect();

            let py_event = PyTextEvent {
                lines,
                links: py_links,
                footnotes: py_footnotes,
            };
            Some(py_event.into_py(py))
        }
        Event::BlockFinished => {
            let py_event = PyBlockFinishedEvent::new();
            Some(py_event.into_py(py))
        }
        Event::ContextLine { meta_line } => {
            let py_meta_line = PyMetaline::from_meta_line(&meta_line);
            let py_event = PyContextLineEvent::new(py_meta_line);
            Some(py_event.into_py(py))
        }
        Event::VoidElement { meta_line } => {
            let py_meta_line = PyMetaline::from_meta_line(&meta_line);
            let py_event = PyVoidElementEvent::new(py_meta_line);
            Some(py_event.into_py(py))
        }
    }
}

/// Parse Wasot content and return a list of events
#[pyfunction]
#[pyo3(signature = (content, selectors=None))]
#[allow(clippy::useless_conversion)]
fn parse_wasot_string_to_events(
    content: String,
    selectors: Option<Vec<String>>,
) -> PyResult<Vec<PyObject>> {
    Python::with_gil(|py| {
        let mut events = Vec::new();
        let context = PyContext::new();

        // If selectors are provided, use the transformer
        if let Some(selector_strings) = selectors {
            let emitter = WasotFileEmitter::from_string(content);
            let selector_transformer = SelectorTransformer::new(selector_strings);

            // Create a pipeline with the selector transformer
            let composed = transformer_pipeline(emitter, vec![selector_transformer]);

            // Run and collect filtered events
            let reader = composed.subscribe();
            composed.run();
            for event in reader {
                if let Some(py_event) = convert_event_to_py(event, &context, py) {
                    events.push(py_event);
                }
            }
        } else {
            // Use the facade API to parse the content without filtering
            for event_result in crate::facade::parse_wasot_string(content) {
                if let Ok(event) = event_result {
                    if let Some(py_event) = convert_event_to_py(event, &context, py) {
                        events.push(py_event);
                    }
                }
            }
        }

        Ok(events)
    })
}

/// Parse Wasot file and return a list of events
#[pyfunction]
#[pyo3(signature = (file_path, selectors=None))]
#[allow(clippy::useless_conversion)]
fn parse_wasot_file_to_events(
    file_path: String,
    selectors: Option<Vec<String>>,
) -> PyResult<Vec<PyObject>> {
    Python::with_gil(|py| {
        let mut events = Vec::new();
        let context = PyContext::new();

        // If selectors are provided, use the transformer
        if let Some(selector_strings) = selectors {
            let emitter = WasotFileEmitter::from_file(&file_path);
            let selector_transformer = SelectorTransformer::new(selector_strings);

            // Create a pipeline with the selector transformer
            let composed = transformer_pipeline(emitter, vec![selector_transformer]);

            // Run and collect filtered events
            let reader = composed.subscribe();
            composed.run();
            for event in reader {
                if let Some(py_event) = convert_event_to_py(event, &context, py) {
                    events.push(py_event);
                }
            }
        } else {
            // Use the facade API to parse the file without filtering
            match crate::facade::parse_wasot_file(&file_path) {
                Ok(event_iter) => {
                    for event_result in event_iter {
                        if let Ok(event) = event_result {
                            if let Some(py_event) = convert_event_to_py(event, &context, py) {
                                events.push(py_event);
                            }
                        }
                    }
                }
                Err(e) => {
                    return Err(PyErr::new::<pyo3::exceptions::PyIOError, _>(format!(
                        "Failed to read file: {e}"
                    )));
                }
            }
        }

        Ok(events)
    })
}

// Selector-related Python classes

/// CSS-like selector specificity tuple (ids, classes, types).
///
/// Higher specificity takes precedence when multiple selectors match.
///
/// Attributes:
///     ids (int): Count of ID selectors (e.g., `#intro`).
///     classes_attributes_pseudos (int): Count of class, attribute, and pseudo selectors.
///     types (int): Count of type selectors (e.g., `paragraph`).
#[pyclass(name = "Specificity")]
#[derive(Clone)]
pub struct PySpecificity {
    #[pyo3(get)]
    ids: u32,
    #[pyo3(get)]
    classes_attributes_pseudos: u32,
    #[pyo3(get)]
    types: u32,
}

#[pymethods]
impl PySpecificity {
    #[new]
    fn new(ids: u32, classes_attributes_pseudos: u32, types: u32) -> Self {
        Self {
            ids,
            classes_attributes_pseudos,
            types,
        }
    }

    fn __str__(&self) -> String {
        format!(
            "({},{},{})",
            self.ids, self.classes_attributes_pseudos, self.types
        )
    }

    fn __repr__(&self) -> String {
        format!(
            "Specificity(ids={}, classes_attributes_pseudos={}, types={})",
            self.ids, self.classes_attributes_pseudos, self.types
        )
    }
}

impl From<Specificity> for PySpecificity {
    fn from(spec: Specificity) -> Self {
        Self {
            ids: spec.0,
            classes_attributes_pseudos: spec.1,
            types: spec.2,
        }
    }
}

/// Calculate the specificity of a parsed selector
#[pyfunction]
fn calculate_specificity(selector: &str) -> PyResult<PySpecificity> {
    let selector_group = rust_parse_selector(selector).map_err(|e| {
        PyErr::new::<pyo3::exceptions::PyValueError, _>(format!("Failed to parse selector: {e}"))
    })?;
    let specificity = rust_calculate_specificity(&selector_group);
    Ok(PySpecificity::from(specificity))
}

/// Parse a metaline string and create a PyMetaline
#[pyfunction]
fn parse_metaline(line: &str, line_index: usize) -> PyResult<PyMetaline> {
    match MetaLine::parse(line, line_index) {
        Some(meta_line) => Ok(PyMetaline::from_meta_line(&meta_line)),
        None => Err(PyErr::new::<pyo3::exceptions::PyValueError, _>(format!(
            "Failed to parse metaline: '{line}'"
        ))),
    }
}

/// Iterator that yields Wasot parsing events.
///
/// Returned by `parse_wasot_string()` and `parse_wasot_file()`.
/// Yields events in document order: BlockStartedEvent, TextEvent,
/// ContextLineEvent, VoidElementEvent, and BlockFinishedEvent.
#[pyclass(name = "WasotIterator")]
pub struct PyWasotIterator {
    events: Vec<PyObject>,
    index: usize,
    context: PyContext,
}

#[pymethods]
impl PyWasotIterator {
    fn __iter__(slf: PyRef<'_, Self>) -> PyRef<'_, Self> {
        slf
    }

    fn __next__(mut slf: PyRefMut<'_, Self>, py: Python) -> Option<PyObject> {
        if slf.index >= slf.events.len() {
            return None;
        }

        let event = slf.events[slf.index].clone_ref(py);
        slf.index += 1;

        // Update context based on the event type
        if let Ok(block_event) = event.extract::<PyBlockStartedEvent>(py) {
            // Create a Rust Event from the PyBlockStartedEvent for context management
            let rust_event = Event::BlockStarted {
                meta_line: MetaLine {
                    level: block_event.meta_line.level,
                    type_name: block_event.meta_line.type_name.clone(),
                    id: Id::new(block_event.meta_line.block_id.clone().unwrap_or_default()),
                    arguments: ParsedArguments {
                        args: vec![],
                        kwargs: vec![],
                    },
                    inline_text: QuotedString::new(
                        block_event.meta_line.inline_text.content.clone(),
                        block_event.meta_line.inline_text.quoted,
                    ),
                    line_index: block_event.meta_line.line_index,
                    is_void: block_event.meta_line.is_void,
                },
            };

            // Let the context handle its own scope management
            slf.context.handle_event(&rust_event);

            // Return the event with context attached
            let event_with_context =
                PyBlockStartedEvent::new(block_event.meta_line, slf.context.clone());
            return Some(event_with_context.into_py(py));
        } else if event.extract::<PyBlockFinishedEvent>(py).is_ok() {
            // Let the context handle its own scope management
            let finished_event = Event::BlockFinished;
            slf.context.handle_event(&finished_event);
        }

        Some(event)
    }
}

/// Parse Wasot content from a string and return an iterator
#[pyfunction]
#[pyo3(signature = (content, _tools=None))]
fn parse_wasot_string(
    py: Python,
    content: String,
    _tools: Option<Vec<PyObject>>,
) -> PyResult<PyWasotIterator> {
    let mut events = Vec::new();
    let context = PyContext::new();

    // Parse events
    for event_result in crate::facade::parse_wasot_string(content) {
        if let Ok(event) = event_result {
            if let Some(py_event) = convert_event_to_py(event, &context, py) {
                events.push(py_event);
            }
        }
    }

    Ok(PyWasotIterator {
        events,
        index: 0,
        context,
    })
}

/// Parse Wasot file and return an iterator
#[pyfunction]
#[pyo3(signature = (file_path, _tools=None))]
fn parse_wasot_file(
    py: Python,
    file_path: String,
    _tools: Option<Vec<PyObject>>,
) -> PyResult<PyWasotIterator> {
    let mut events = Vec::new();
    let context = PyContext::new();

    // Parse events
    match crate::facade::parse_wasot_file(&file_path) {
        Ok(event_iter) => {
            for event_result in event_iter {
                if let Ok(event) = event_result {
                    if let Some(py_event) = convert_event_to_py(event, &context, py) {
                        events.push(py_event);
                    }
                }
            }
        }
        Err(e) => {
            return Err(PyErr::new::<pyo3::exceptions::PyIOError, _>(format!(
                "Failed to read file: {e}"
            )));
        }
    }

    Ok(PyWasotIterator {
        events,
        index: 0,
        context,
    })
}

pub fn pywasot(m: &Bound<'_, PyModule>) -> PyResult<()> {
    // Core data types
    m.add_class::<PyTargetType>()?;
    m.add_class::<PyExternalIdReference>()?;
    m.add_class::<PyFootnote>()?;
    m.add_class::<PyArguments>()?;
    m.add_class::<PyLink>()?;
    m.add_class::<PyQuotedString>()?;
    m.add_class::<PyKeywordArgument>()?;
    m.add_class::<PyPositionalArgument>()?;
    m.add_class::<PyMetaline>()?;

    // Context
    m.add_class::<PyContext>()?;

    // Events
    m.add_class::<PyBlockStartedEvent>()?;
    m.add_class::<PyBlockFinishedEvent>()?;
    m.add_class::<PyTextEvent>()?;
    m.add_class::<PyContextLineEvent>()?;
    m.add_class::<PyVoidElementEvent>()?;

    // Iterators and utilities
    m.add_class::<PyWasotIterator>()?;
    m.add_class::<PySpecificity>()?;

    // Functions
    m.add_function(wrap_pyfunction!(parse_wasot_string_to_events, m)?)?;
    m.add_function(wrap_pyfunction!(parse_wasot_file_to_events, m)?)?;
    m.add_function(wrap_pyfunction!(parse_wasot_string, m)?)?;
    m.add_function(wrap_pyfunction!(parse_wasot_file, m)?)?;
    m.add_function(wrap_pyfunction!(calculate_specificity, m)?)?;
    m.add_function(wrap_pyfunction!(parse_metaline, m)?)?;

    Ok(())
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::parsers::wasot::{MetaLine, ParsedArguments, QuotedString};
    use pyo3::types::PyDict;

    #[test]
    fn test_py_quoted_string_creation() {
        pyo3::prepare_freethreaded_python();
        Python::with_gil(|_py| {
            let qs = PyQuotedString::new("test".to_string(), Some(true));
            assert_eq!(qs.content, "test");
            assert!(qs.quoted);
            assert_eq!(qs.__str__(), "test");
        });
    }

    #[test]
    fn test_py_quoted_string_from_rust() {
        pyo3::prepare_freethreaded_python();
        Python::with_gil(|_py| {
            let rust_qs = QuotedString::new("hello world".to_string(), false);
            let py_qs = PyQuotedString::from(rust_qs);
            assert_eq!(py_qs.content, "hello world");
            assert!(!py_qs.quoted);
        });
    }

    #[test]
    fn test_py_keyword_argument() {
        pyo3::prepare_freethreaded_python();
        Python::with_gil(|_py| {
            let key = PyQuotedString::new("key".to_string(), None);
            let value = PyQuotedString::new("value".to_string(), Some(true));
            let kwarg = PyKeywordArgument::new(key.clone(), value.clone());
            assert_eq!(kwarg.key.content, "key");
            assert_eq!(kwarg.value.content, "value");
            assert!(kwarg.value.quoted);
        });
    }

    #[test]
    fn test_py_positional_argument() {
        pyo3::prepare_freethreaded_python();
        Python::with_gil(|_py| {
            let value = PyQuotedString::new("arg1".to_string(), Some(false));
            let pos_arg = PyPositionalArgument::new(value.clone());
            assert_eq!(pos_arg.value.content, "arg1");
            assert!(!pos_arg.value.quoted);
        });
    }

    #[test]
    fn test_py_metaline_creation() {
        pyo3::prepare_freethreaded_python();
        Python::with_gil(|_py| {
            let args = vec![];
            let metaline = PyMetaline::new(
                "paragraph".to_string(),
                "Some inline text".to_string(),
                1,
                Some(42),
                Some(args),
            );
            assert_eq!(metaline.type_name, "paragraph");
            assert_eq!(metaline.inline_text, "Some inline text");
            assert_eq!(metaline.level, 1);
            assert_eq!(metaline.line_index, Some(42));
            assert!(metaline.arguments.is_empty());
        });
    }

    #[test]
    fn test_py_metaline_from_rust() {
        pyo3::prepare_freethreaded_python();
        Python::with_gil(|py| {
            let rust_metaline = MetaLine {
                level: 2,
                type_name: "section".to_string(),
                id: crate::parsers::wasot::Id::new("test-id".to_string()),
                arguments: ParsedArguments {
                    args: vec![QuotedString::new("arg1".to_string(), false)],
                    kwargs: vec![(
                        QuotedString::new("key".to_string(), false),
                        QuotedString::new("value".to_string(), true),
                    )],
                },
                inline_text: QuotedString::new("Inline content".to_string(), false),
                line_index: Some(10),
                is_void: false,
            };

            let py_metaline = PyMetaline::from_meta_line(&rust_metaline, py).unwrap();
            assert_eq!(py_metaline.type_name, "section");
            assert_eq!(py_metaline.inline_text, "Inline content");
            assert_eq!(py_metaline.level, 2);
            assert_eq!(py_metaline.line_index, Some(10));
            assert_eq!(py_metaline.arguments.len(), 2); // 1 positional + 1 keyword
        });
    }

    #[test]
    fn test_py_context() {
        pyo3::prepare_freethreaded_python();
        Python::with_gil(|py| {
            let context = PyContext::new();

            // Test putting and getting values
            let test_value: PyObject = "test_value".into_py(py);
            context.put("key1".to_string(), test_value.clone_ref(py));

            let retrieved = context.get("key1".to_string(), py);
            // Check if they're the same object
            assert!(retrieved.is(&test_value));

            // Test getting non-existent key returns None
            let none_value = context.get("nonexistent".to_string(), py);
            assert!(none_value.is_none(py));

            // Test items getter
            let items = context.items(py).unwrap();
            let dict = items.downcast_bound::<PyDict>(py).unwrap();
            assert_eq!(dict.len(), 1);
            assert!(dict.contains("key1").unwrap());
        });
    }

    #[test]
    fn test_py_context_scoped_variables() {
        pyo3::prepare_freethreaded_python();
        Python::with_gil(|py| {
            let context = PyContext::new();

            // Test scoped variables
            let scoped_val: PyObject = "scoped_value".into_py(py);
            context.put_scoped("scoped_key".to_string(), scoped_val.clone_ref(py));

            // Should be retrievable via get_scoped
            let retrieved = context.get_scoped("scoped_key".to_string(), py);
            assert!(retrieved.is(&scoped_val));

            // Should also be retrievable via regular get (which checks scoped first)
            let retrieved_via_get = context.get("scoped_key".to_string(), py);
            assert!(retrieved_via_get.is(&scoped_val));

            // Simulate pushing a new scope via BlockStarted event
            let event = Event::BlockStarted {
                meta_line: MetaLine {
                    level: 2, // Going deeper
                    type_name: "test".to_string(),
                    id: crate::parsers::wasot::Id::new("".to_string()),
                    arguments: crate::parsers::wasot::ParsedArguments {
                        args: vec![],
                        kwargs: vec![],
                    },
                    inline_text: QuotedString::new("".to_string(), false),
                    line_index: Some(0),
                    is_void: false,
                },
            };
            context.handle_event(&event);

            // Variable from parent scope should still be accessible
            let parent_val = context.get_scoped("scoped_key".to_string(), py);
            assert!(parent_val.is(&scoped_val));

            // Add a variable in the new scope
            let child_val: PyObject = "child_value".into_py(py);
            context.put_scoped("child_key".to_string(), child_val.clone_ref(py));

            // Simulate popping the scope via BlockStarted event at lower level
            let event = Event::BlockStarted {
                meta_line: MetaLine {
                    level: 1, // Going back up
                    type_name: "test2".to_string(),
                    id: crate::parsers::wasot::Id::new("".to_string()),
                    arguments: crate::parsers::wasot::ParsedArguments {
                        args: vec![],
                        kwargs: vec![],
                    },
                    inline_text: QuotedString::new("".to_string(), false),
                    line_index: Some(1),
                    is_void: false,
                },
            };
            context.handle_event(&event);

            // Child variable should no longer be accessible
            let gone = context.get_scoped("child_key".to_string(), py);
            assert!(gone.is_none(py));

            // Parent variable should still be accessible
            let still_there = context.get_scoped("scoped_key".to_string(), py);
            assert!(still_there.is(&scoped_val));
        });
    }

    #[test]
    fn test_py_block_started_event() {
        pyo3::prepare_freethreaded_python();
        Python::with_gil(|_py| {
            let metaline = PyMetaline::new(
                "div".to_string(),
                "Content here".to_string(),
                3,
                Some(100),
                None,
            );
            let context = PyContext::new();
            let event = PyBlockStartedEvent::new(metaline.clone(), context.clone());

            assert_eq!(event.line_index(), Some(100));
            assert_eq!(event.block_type(), "div");
            assert_eq!(event.inline_text(), "Content here");
            assert_eq!(event.level(), 3);
        });
    }

    #[test]
    fn test_py_text_event() {
        pyo3::prepare_freethreaded_python();
        Python::with_gil(|_py| {
            let lines = vec!["Line 1".to_string(), "Line 2".to_string()];
            let event = PyTextEvent::new(lines.clone());
            assert_eq!(event.lines, lines);
        });
    }

    #[test]
    fn test_py_specificity() {
        pyo3::prepare_freethreaded_python();
        Python::with_gil(|_py| {
            let spec = PySpecificity::new(1, 2, 3);
            assert_eq!(spec.ids, 1);
            assert_eq!(spec.classes_attributes_pseudos, 2);
            assert_eq!(spec.types, 3);
            assert_eq!(spec.__str__(), "(1,2,3)");
            assert_eq!(
                spec.__repr__(),
                "Specificity(ids=1, classes_attributes_pseudos=2, types=3)"
            );
        });
    }

    #[test]
    fn test_py_specificity_from_rust() {
        pyo3::prepare_freethreaded_python();
        Python::with_gil(|_py| {
            let rust_spec = Specificity(2, 4, 6);
            let py_spec = PySpecificity::from(rust_spec);
            assert_eq!(py_spec.ids, 2);
            assert_eq!(py_spec.classes_attributes_pseudos, 4);
            assert_eq!(py_spec.types, 6);
        });
    }

    #[test]
    fn test_calculate_specificity_function() {
        pyo3::prepare_freethreaded_python();
        Python::with_gil(|_py| {
            // Test simple type selector
            let spec = calculate_specificity("div").unwrap();
            assert_eq!(spec.ids, 0);
            assert_eq!(spec.classes_attributes_pseudos, 0);
            assert_eq!(spec.types, 1);

            // Test ID selector
            let spec = calculate_specificity("#myid").unwrap();
            assert_eq!(spec.ids, 1);
            assert_eq!(spec.classes_attributes_pseudos, 0);
            assert_eq!(spec.types, 0);

            // Test invalid selector
            let result = calculate_specificity("@invalid");
            assert!(result.is_err());
        });
    }

    #[test]
    fn test_parse_metaline_function() {
        pyo3::prepare_freethreaded_python();
        Python::with_gil(|_py| {
            // Test valid metaline
            let result = parse_metaline(".paragraph .. Some text", 10).unwrap();
            assert_eq!(result.type_name, "paragraph");
            assert_eq!(result.inline_text, "Some text");
            assert_eq!(result.level, 1);
            assert_eq!(result.line_index, Some(10));

            // Test invalid metaline
            let result = parse_metaline("not a metaline", 0);
            assert!(result.is_err());
        });
    }

    #[test]
    fn test_parse_wasot_string_to_events() {
        pyo3::prepare_freethreaded_python();
        Python::with_gil(|py| {
            let content = ".paragraph\nSome text\n.section\nMore text";
            let events = parse_wasot_string_to_events(content.to_string(), None).unwrap();

            // Should have BlockStarted, TextFinished, BlockFinished for each block
            assert!(events.len() >= 6); // At least 2 blocks × 3 events

            // Check first event is BlockStartedEvent
            let first_event = &events[0];
            assert!(first_event.extract::<PyBlockStartedEvent>(py).is_ok());
        });
    }

    #[test]
    fn test_parse_wasot_string_with_selectors() {
        pyo3::prepare_freethreaded_python();
        Python::with_gil(|_py| {
            let content = ".document\n.paragraph\nSome text\n.section\nMore text";
            let selectors = vec!["paragraph".to_string()];
            let events =
                parse_wasot_string_to_events(content.to_string(), Some(selectors)).unwrap();

            // Should only have events for matching blocks
            // The exact count depends on the selector implementation
            assert!(!events.is_empty());
        });
    }

    #[test]
    fn test_py_wasot_iterator() {
        pyo3::prepare_freethreaded_python();
        Python::with_gil(|py| {
            let content = ".paragraph\nText content";
            let iterator = parse_wasot_string(py, content.to_string(), None).unwrap();

            // Check that the iterator was created with events
            assert!(!iterator.events.is_empty());
            assert_eq!(iterator.index, 0);

            // Note: We can't easily test __iter__ and __next__ directly in Rust tests
            // Those are meant to be called from Python
        });
    }

    #[test]
    fn test_context_sharing() {
        pyo3::prepare_freethreaded_python();
        Python::with_gil(|py| {
            let context1 = PyContext::new();
            let context2 = context1.clone();

            // Put value in context1
            context1.put("shared".to_string(), "value".into_py(py));

            // Should be accessible from context2
            let value = context2.get("shared".to_string(), py);
            assert!(!value.is_none(py));
        });
    }
}
