mod regex;

use crate::ALL_BOOKS;
use crate::Versification;
use crate::book::{Book, get_book};
use crate::reference::Reference;
use crate::reference_parser::regex::TRIGGER_MAP;
use std::collections::HashSet;

/// Options for configuring Bible reference parsing behavior
#[derive(Debug, Clone, Default)]
pub struct ParseOptions {
    /// Enable fuzzy matching for book names (typo tolerance)
    pub fuzzy_matching: bool,
    /// Allow bare chapter:verse patterns without book names
    pub bare_references: bool,
    /// Relax word boundary requirements
    pub relaxed_boundaries: bool,
    /// Enable contextual resolution for "verse X" and "chapter X" patterns
    pub contextual_resolution: bool,
    /// Versification system to use for verse count validation and chapter expansion
    pub versification: Versification,
}

impl ParseOptions {
    /// Create new ParseOptions with all options disabled (strict parsing)
    pub fn new() -> Self {
        Self::default()
    }

    /// Preset for search bar / optimistic parsing with all options enabled
    pub fn optimistic() -> Self {
        Self {
            fuzzy_matching: true,
            bare_references: true,
            relaxed_boundaries: true,
            contextual_resolution: true,
            versification: Versification::default(),
        }
    }

    /// Enable or disable fuzzy matching for book names
    pub fn fuzzy_matching(mut self, enabled: bool) -> Self {
        self.fuzzy_matching = enabled;
        self
    }

    /// Enable or disable bare chapter:verse patterns without book names
    pub fn bare_references(mut self, enabled: bool) -> Self {
        self.bare_references = enabled;
        self
    }

    /// Enable or disable relaxed word boundary requirements
    pub fn relaxed_boundaries(mut self, enabled: bool) -> Self {
        self.relaxed_boundaries = enabled;
        self
    }

    /// Enable or disable contextual resolution for patterns like "verse 12" or "chapter 3"
    pub fn contextual_resolution(mut self, enabled: bool) -> Self {
        self.contextual_resolution = enabled;
        self
    }

    /// Set the versification system to use for verse count validation and chapter expansion
    pub fn with_versification(mut self, versification: Versification) -> Self {
        self.versification = versification;
        self
    }
}

/// Byte offsets into the original parsed source.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct Span {
    /// Start byte offset, inclusive.
    pub start: usize,
    /// End byte offset, exclusive.
    pub end: usize,
}

/// Non-fatal parser event emitted while scanning text for references.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ParseDiagnostic {
    /// Source span associated with the diagnostic.
    pub span: Span,
    /// Severity of the diagnostic.
    pub severity: DiagnosticSeverity,
    /// Diagnostic-specific payload.
    pub kind: ParseDiagnosticKind,
}

/// Severity for parser diagnostics.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum DiagnosticSeverity {
    /// Informational event for an accepted reference, such as a fuzzy match.
    Info,
    /// Suspicious input that did not prevent parsing.
    Warning,
    /// A candidate reference failed to parse.
    Error,
}

/// Specific parser diagnostic event.
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum ParseDiagnosticKind {
    /// A contextual phrase such as `verse 12` was resolved from an earlier reference.
    ContextResolved {
        /// Human-readable context used to resolve the reference.
        resolved_from: String,
    },
    /// A contextual phrase could not be resolved to a concrete reference.
    ContextResolutionFailed,
    /// A fuzzy book-name match was accepted.
    FuzzyBookMatch {
        /// Original matched text from the source.
        matched: String,
        /// Canonical book title selected by the parser.
        resolved_to: &'static str,
        /// Levenshtein distance between the matched text and canonical title.
        distance: usize,
    },
    /// A candidate referenced a chapter that does not exist in the configured versification.
    InvalidChapter {
        /// Canonical book title for the rejected candidate.
        book: &'static str,
        /// Chapter number from the rejected candidate.
        chapter: usize,
    },
    /// A candidate referenced a verse that does not exist in the configured versification.
    InvalidVerse {
        /// Canonical book title for the rejected candidate.
        book: &'static str,
        /// Chapter number from the rejected candidate.
        chapter: usize,
        /// Verse number from the rejected candidate.
        verse: usize,
    },
    /// A candidate range was malformed or unsupported.
    InvalidRange {
        /// Human-readable reason the range was rejected.
        reason: String,
    },
    /// A bare chapter:verse pattern was found without enough context to resolve a book.
    AmbiguousBareReference {
        /// Chapter number from the bare reference.
        chapter: usize,
        /// Verse number from the bare reference.
        verse: usize,
    },
}

/// Context information for references resolved from contextual patterns
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ContextualResolution {
    /// The original text that was matched (e.g., "verse 12")
    pub original_text: String,
    /// The reference this was resolved from (e.g., "Ephesians 2")
    pub resolved_from: String,
}

/// Deprecated alias for [`ContextualResolution`].
#[deprecated(note = "Use ContextualResolution instead")]
pub type ContextualContext = ContextualResolution;

/// A located reference within text, containing the reference and its byte offsets
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct LocatedReference {
    /// The parsed Bible reference
    pub reference: Reference,
    /// Start byte offset in the original text (inclusive)
    pub start: usize,
    /// End byte offset in the original text (exclusive)
    pub end: usize,
    /// Optional contextual resolution information
    pub context: Option<ContextualResolution>,
}

/// Parsed text containing all discovered Bible references with their locations
#[derive(Debug, Clone)]
pub struct Text {
    /// The original input text
    pub source: String,
    /// All references found in the text with their offsets
    pub references: Vec<LocatedReference>,
    /// Non-fatal parser diagnostics emitted while scanning the text
    pub diagnostics: Vec<ParseDiagnostic>,
}

/// Reusable Bible reference parser configured with [`ParseOptions`].
#[derive(Debug, Clone, Default)]
pub struct Parser {
    options: ParseOptions,
}

impl Parser {
    /// Create a parser with default parsing options.
    pub fn new() -> Self {
        Self::default()
    }

    /// Replace this parser's parsing options.
    pub fn with_options(mut self, options: ParseOptions) -> Self {
        self.options = options;
        self
    }

    /// Return the parser's current options.
    pub fn options(&self) -> &ParseOptions {
        &self.options
    }

    /// Parse source text into located references and diagnostics.
    pub fn parse(&self, input: &str) -> Text {
        Text::parse_with_options(input, &self.options)
    }
}

impl Text {
    /// Construct parsed text from already-located references.
    pub fn new(source: String, references: Vec<LocatedReference>) -> Self {
        let diagnostics = diagnostics_for_references(&references);
        Self {
            source,
            references,
            diagnostics,
        }
    }

    /// Parse a string for Bible references, returning a Text struct with all references and their offsets
    pub fn parse(input: &str) -> Self {
        Self::parse_with_options(input, &ParseOptions::default())
    }

    /// Parse a string for Bible references with custom options
    pub fn parse_with_options(input: &str, options: &ParseOptions) -> Self {
        let (references, diagnostics) = parse_reference_internal(input, options, true);
        Text {
            source: input.to_string(),
            references,
            diagnostics,
        }
    }

    /// Get the substring of the original text for a located reference
    pub fn get_text(&self, located: &LocatedReference) -> &str {
        &self.source[located.start..located.end]
    }

    /// Iterate over parsed references without location metadata.
    pub fn references(&self) -> impl Iterator<Item = &Reference> {
        self.references.iter().map(|located| &located.reference)
    }

    /// Returns the original text with all Bible references removed.
    /// Useful for parsing search queries like "the good shepherd John" to "the good shepherd".
    /// Consecutive whitespace is collapsed and the result is trimmed.
    pub fn text_without_references(&self) -> String {
        if self.references.is_empty() {
            return self.source.clone();
        }

        let mut result = String::with_capacity(self.source.len());
        let mut last_end = 0;

        for located in &self.references {
            // Add text before this reference
            if located.start > last_end {
                result.push_str(&self.source[last_end..located.start]);
            }
            last_end = located.end;
        }

        // Add any remaining text after the last reference
        if last_end < self.source.len() {
            result.push_str(&self.source[last_end..]);
        }

        // Collapse consecutive whitespace and trim
        result.split_whitespace().collect::<Vec<_>>().join(" ")
    }
}

fn diagnostics_for_references(references: &[LocatedReference]) -> Vec<ParseDiagnostic> {
    references
        .iter()
        .filter_map(|located| {
            located.context.as_ref().map(|context| ParseDiagnostic {
                span: Span {
                    start: located.start,
                    end: located.end,
                },
                severity: DiagnosticSeverity::Info,
                kind: ParseDiagnosticKind::ContextResolved {
                    resolved_from: context.resolved_from.clone(),
                },
            })
        })
        .collect()
}

/// Parse references from text and return them with their byte offsets
pub fn parse_reference_with_offsets(input: &str) -> Vec<LocatedReference> {
    parse_reference_with_offsets_and_options(input, &ParseOptions::default())
}

/// Parse references from text with custom options, returning them with their byte offsets
pub fn parse_reference_with_offsets_and_options(
    input: &str,
    options: &ParseOptions,
) -> Vec<LocatedReference> {
    parse_reference_internal(input, options, false).0
}

fn parse_reference_internal(
    input: &str,
    options: &ParseOptions,
    emit_diagnostics: bool,
) -> (Vec<LocatedReference>, Vec<ParseDiagnostic>) {
    let mut located_refs = Vec::new();
    let mut diagnostics = Vec::new();
    let versification = options.versification;

    // Try standard book-based parsing
    let poss_refs = contains_possible_references_with_options(input, options);

    // Deduplicate: only run each book's regex once
    let mut seen_books: HashSet<&str> = HashSet::new();

    for (possible, book, is_fuzzy) in poss_refs {
        if seen_books.contains(book.title) {
            continue;
        }
        seen_books.insert(book.title);

        if is_fuzzy {
            // For fuzzy matches, we need to parse the chapter:verse after the fuzzy-matched word
            if let Some(located) = parse_fuzzy_match(
                input,
                possible,
                book,
                options.relaxed_boundaries,
                versification,
            ) {
                if emit_diagnostics {
                    diagnostics.push(ParseDiagnostic {
                        span: Span {
                            start: located.start,
                            end: located.end,
                        },
                        severity: DiagnosticSeverity::Info,
                        kind: ParseDiagnosticKind::FuzzyBookMatch {
                            matched: possible.to_string(),
                            resolved_to: book.title,
                            distance: strsim::levenshtein(
                                &possible.to_lowercase(),
                                &book.title.to_lowercase(),
                            ),
                        },
                    });
                }
                located_refs.push(located);
            }
        } else {
            // For exact matches, use standard or relaxed regex
            let regex = if options.relaxed_boundaries {
                regex::get_relaxed_regex(book)
            } else {
                TRIGGER_MAP.get(possible).copied()
            };

            if let Some(re) = regex {
                for cap in re.captures_iter(input) {
                    if let Some(reference) =
                        extract_reference_from_capture(book, &cap, versification)
                    {
                        if let Some(mat) = cap.get(0) {
                            located_refs.push(LocatedReference {
                                reference,
                                start: mat.start(),
                                end: mat.end(),
                                context: None,
                            });
                        }
                    }
                }
            }
        }
    }

    // Try bare reference parsing if enabled
    if options.bare_references {
        let bare_refs = parse_bare_references(input);
        located_refs.extend(bare_refs);
    }

    // Try contextual resolution if enabled
    if options.contextual_resolution {
        let contextual_refs = parse_contextual_references(input, &located_refs, versification);
        located_refs.extend(contextual_refs);
    }

    if emit_diagnostics {
        diagnostics.extend(diagnostics_for_references(&located_refs));
    }

    // Sort by start offset to maintain text order
    located_refs.sort_by_key(|r| r.start);

    // Remove duplicates (same start position)
    located_refs.dedup_by_key(|r| r.start);

    (located_refs, diagnostics)
}

/// Parse a fuzzy-matched book name and extract chapter:verse that follows it
fn parse_fuzzy_match(
    input: &str,
    matched_word: &str,
    book: &'static Book,
    relaxed_boundaries: bool,
    versification: Versification,
) -> Option<LocatedReference> {
    // Find where the matched word is in the input
    let word_lower = matched_word.to_lowercase();
    let input_lower = input.to_lowercase();
    let word_start = input_lower.find(&word_lower)?;
    let word_end = word_start + matched_word.len();

    // Look for chapter:verse pattern after the word
    let after_word = &input[word_end..];

    // Match optional space followed by chapter(:verse(-end_verse)?)?
    // The \s* allows for no space (relaxed) or space (strict) between book and chapter
    let chap_verse_regex = ::regex::Regex::new(
        r"^\s*(?P<chapter>\d{1,3})(?:[:.\s](?P<verse>\d{1,3})(?:\s*(?:--|[-–—])\s*(?P<end_verse>\d{1,3}))?)?"
    ).ok()?;

    if let Some(cap) = chap_verse_regex.captures(after_word) {
        let chapter: Option<usize> = cap.name("chapter").and_then(|m| m.as_str().parse().ok());
        let verse: Option<usize> = cap.name("verse").and_then(|m| m.as_str().parse().ok());
        let end_verse: Option<usize> = cap.name("end_verse").and_then(|m| m.as_str().parse().ok());

        let match_end = word_end + cap.get(0).map(|m| m.end()).unwrap_or(0);

        // Check end boundary
        let has_end_boundary = match_end == input.len() || {
            let next_char = input[match_end..].chars().next().unwrap();
            if relaxed_boundaries {
                !next_char.is_alphabetic()
            } else {
                !next_char.is_alphanumeric()
            }
        };

        if !has_end_boundary {
            return None;
        }

        let reference = match (chapter, verse, end_verse) {
            (Some(ch), Some(start_v), Some(end_v)) => Reference::Range {
                book: book.title,
                start_chapter: ch,
                start_verse: start_v,
                end_book: book.title,
                end_chapter: ch,
                end_verse: end_v,
            },
            (Some(ch), Some(v), None) => Reference::Single {
                book: book.title,
                chapter: ch,
                verse: v,
            },
            (Some(ch), None, _) => {
                let end_verse = book.verses_in_chapter_versified(ch as u8, versification)? as usize;
                Reference::Range {
                    book: book.title,
                    start_chapter: ch,
                    start_verse: 1,
                    end_book: book.title,
                    end_chapter: ch,
                    end_verse,
                }
            }
            (None, None, _) => {
                // Just the book name
                let end_chapter = book.chapters_versified(versification) as usize;
                let end_verse =
                    book.verses_in_chapter_versified(end_chapter as u8, versification)? as usize;
                Reference::Range {
                    book: book.title,
                    start_chapter: 1,
                    start_verse: 1,
                    end_book: book.title,
                    end_chapter,
                    end_verse,
                }
            }
            (None, Some(_), _) => return None,
        };

        return Some(LocatedReference {
            reference,
            start: word_start,
            end: match_end,
            context: None,
        });
    }

    // No chapter:verse found, just return the book reference
    let end_chapter = book.chapters_versified(versification) as usize;
    let end_verse = book.verses_in_chapter_versified(end_chapter as u8, versification)? as usize;
    Some(LocatedReference {
        reference: Reference::Range {
            book: book.title,
            start_chapter: 1,
            start_verse: 1,
            end_book: book.title,
            end_chapter,
            end_verse,
        },
        start: word_start,
        end: word_end,
        context: None,
    })
}

pub fn parse_reference(input: &str) -> Vec<Reference> {
    parse_reference_with_options(input, &ParseOptions::default())
}

/// Parse references from text with custom options
pub fn parse_reference_with_options(input: &str, options: &ParseOptions) -> Vec<Reference> {
    parse_reference_with_offsets_and_options(input, options)
        .into_iter()
        .map(|lr| lr.reference)
        .collect()
}

fn extract_reference_from_capture(
    book: &'static Book,
    cap: &::regex::Captures,
    versification: Versification,
) -> Option<Reference> {
    let chapter: Option<usize> = cap.name("chapter").and_then(|m| m.as_str().parse().ok());

    let verse: Option<usize> = cap.name("verse").and_then(|m| m.as_str().parse().ok());

    let end_verse: Option<usize> = cap.name("end_verse").and_then(|m| m.as_str().parse().ok());

    match (chapter, verse, end_verse) {
        // Full reference with verse range: book + chapter + verse + end_verse -> Range
        (Some(ch), Some(start_v), Some(end_v)) => Some(Reference::Range {
            book: book.title,
            start_chapter: ch,
            start_verse: start_v,
            end_book: book.title,
            end_chapter: ch,
            end_verse: end_v,
        }),
        // Full reference: book + chapter + verse -> Single
        (Some(ch), Some(v), None) => Some(Reference::Single {
            book: book.title,
            chapter: ch,
            verse: v,
        }),
        // Chapter only: book + chapter -> Range for entire chapter
        (Some(ch), None, _) => {
            let end_verse = book.verses_in_chapter_versified(ch as u8, versification)? as usize;
            Some(Reference::Range {
                book: book.title,
                start_chapter: ch,
                start_verse: 1,
                end_book: book.title,
                end_chapter: ch,
                end_verse,
            })
        }
        // Book only: -> Range for entire book
        (None, None, _) => {
            let end_chapter = book.chapters_versified(versification) as usize;
            let end_verse =
                book.verses_in_chapter_versified(end_chapter as u8, versification)? as usize;
            Some(Reference::Range {
                book: book.title,
                start_chapter: 1,
                start_verse: 1,
                end_book: book.title,
                end_chapter,
                end_verse,
            })
        }
        // Verse without chapter doesn't make sense
        (None, Some(_), _) => None,
    }
}

pub fn contains_possible_references(input: &str) -> Vec<&str> {
    contains_possible_references_with_options(input, &ParseOptions::default())
        .into_iter()
        .map(|(s, _, _)| s)
        .collect()
}

/// Find possible book references in input, returning (matched text, book, is_fuzzy_match)
fn contains_possible_references_with_options<'a>(
    input: &'a str,
    options: &ParseOptions,
) -> Vec<(&'a str, &'static Book, bool)> {
    use aho_corasick::{AhoCorasick, MatchKind};
    use once_cell::sync::Lazy;

    static AC: Lazy<AhoCorasick> = Lazy::new(|| {
        let patterns: Vec<&str> = ALL_BOOKS
            .iter()
            .flat_map(|b| {
                let mut book_patterns = vec![b.title];
                book_patterns.extend(b.abbreviations);
                book_patterns.extend(b.aliases);
                book_patterns
            })
            .collect();
        aho_corasick::AhoCorasickBuilder::new()
            .ascii_case_insensitive(true)
            .match_kind(MatchKind::LeftmostLongest)
            .build(patterns)
            .unwrap()
    });

    let mut results: Vec<(&'a str, &'static Book, bool)> = AC
        .find_iter(input)
        .filter(|m| {
            if options.relaxed_boundaries {
                // With relaxed boundaries, we still require that we're not in the middle of a word
                // but we allow digits to be adjacent (e.g., "john3:16")
                let start = m.start();
                let end = m.end();

                let has_start_boundary = start == 0 || {
                    let prev_char = input[..start].chars().last().unwrap();
                    !prev_char.is_alphabetic()
                };

                let has_end_boundary = end == input.len() || {
                    let next_char = input[end..].chars().next().unwrap();
                    // Allow digits after book name for relaxed mode
                    !next_char.is_alphabetic()
                };

                has_start_boundary && has_end_boundary
            } else {
                // Strict word boundaries
                let start = m.start();
                let end = m.end();

                let has_start_boundary = start == 0 || {
                    let prev_char = input[..start].chars().last().unwrap();
                    !prev_char.is_alphanumeric()
                };

                let has_end_boundary = end == input.len() || {
                    let next_char = input[end..].chars().next().unwrap();
                    !next_char.is_alphanumeric()
                };

                has_start_boundary && has_end_boundary
            }
        })
        .filter_map(|m| {
            let matched = &input[m.start()..m.end()];
            get_book(matched).map(|book| (matched, book, false)) // false = not a fuzzy match
        })
        .collect();

    // If fuzzy matching is enabled, also try to find fuzzy matches
    if options.fuzzy_matching {
        let fuzzy_matches = find_fuzzy_book_matches(input, options.relaxed_boundaries);
        // Mark fuzzy matches with true
        results.extend(fuzzy_matches.into_iter().map(|(s, b)| (s, b, true)));
    }

    results
}

/// Find fuzzy matches for book names using Levenshtein distance
fn find_fuzzy_book_matches(input: &str, relaxed_boundaries: bool) -> Vec<(&str, &'static Book)> {
    use strsim::levenshtein;

    let mut results = Vec::new();

    // Build list of all book patterns with their books
    let all_patterns: Vec<(&str, &'static Book)> = ALL_BOOKS
        .iter()
        .flat_map(|b| {
            let mut patterns: Vec<(&str, &'static Book)> = vec![(b.title, *b)];
            patterns.extend(b.abbreviations.iter().map(|&abbr| (abbr, *b)));
            patterns.extend(b.aliases.iter().map(|&alias| (alias, *b)));
            patterns
        })
        .collect();

    // Find word boundaries and extract words with their positions
    // For relaxed boundaries, we split on alphabetic runs only (digits don't count as part of words)
    // For strict boundaries, we split on alphanumeric runs
    let mut words: Vec<(usize, usize)> = Vec::new(); // (start, end) byte positions
    let mut in_word = false;
    let mut word_start = 0;

    for (i, c) in input.char_indices() {
        let is_word_char = if relaxed_boundaries {
            c.is_alphabetic()
        } else {
            c.is_alphanumeric()
        };

        if is_word_char {
            if !in_word {
                word_start = i;
                in_word = true;
            }
        } else if in_word {
            words.push((word_start, i));
            in_word = false;
        }
    }
    if in_word {
        words.push((word_start, input.len()));
    }

    for (word_start, word_end) in words {
        let word = &input[word_start..word_end];
        let word_lower = word.to_lowercase();

        // Skip very short words (likely not book names)
        if word_lower.len() < 3 {
            continue;
        }

        for (pattern, book) in &all_patterns {
            let pattern_lower = pattern.to_lowercase();

            // Calculate max allowed edits based on pattern length
            let max_edits = if pattern_lower.len() <= 4 {
                1
            } else if pattern_lower.len() <= 8 {
                2
            } else {
                3
            };

            let distance = levenshtein(&word_lower, &pattern_lower);

            if distance > 0 && distance <= max_edits {
                // Check boundaries in original input
                let has_start_boundary = word_start == 0 || {
                    let prev_char = input[..word_start].chars().last().unwrap();
                    if relaxed_boundaries {
                        !prev_char.is_alphabetic()
                    } else {
                        !prev_char.is_alphanumeric()
                    }
                };

                let has_end_boundary = word_end == input.len() || {
                    let next_char = input[word_end..].chars().next().unwrap();
                    if relaxed_boundaries {
                        !next_char.is_alphabetic()
                    } else {
                        !next_char.is_alphanumeric()
                    }
                };

                if has_start_boundary && has_end_boundary {
                    results.push((word, *book));
                    break; // Found a match for this word
                }
            }
        }
    }

    results
}

/// Parse contextual references like "verse 12" or "chapter 3" by looking back at previously parsed references
fn parse_contextual_references(
    input: &str,
    existing_refs: &[LocatedReference],
    versification: Versification,
) -> Vec<LocatedReference> {
    use once_cell::sync::Lazy;

    // Regex to match contextual patterns:
    // - "verse 12" / "verses 12" / "verses 12-15"
    // - "chapter 3" / "chapters 3" / "chapters 3-5"
    // - "v. 12" / "vv. 12-15"
    // - "ch. 3" / "chs. 3-5"
    static CONTEXTUAL_REGEX: Lazy<::regex::Regex> = Lazy::new(|| {
        ::regex::Regex::new(
            r"(?xi)
            \b(?:verse|verses|v|vv?\.)\s+(?P<verse>\d{1,3})(?:\s*(?:--|[-–—])\s*(?P<end_verse>\d{1,3}))?
            |
            \b(?:chapter|chapters|ch|chs?\.)\s+(?P<chapter>\d{1,3})(?:\s*(?:--|[-–—])\s*(?P<end_chapter>\d{1,3}))?
            "
        )
        .unwrap()
    });

    let mut results = Vec::new();

    for cap in CONTEXTUAL_REGEX.captures_iter(input) {
        let mat = cap.get(0).unwrap();
        let start = mat.start();
        let end = mat.end();
        let original_text = mat.as_str().to_string();

        // Check if this is a verse or chapter pattern
        let is_verse_pattern = cap.name("verse").is_some();

        if is_verse_pattern {
            // Parse verse number(s)
            let verse: usize = cap
                .name("verse")
                .and_then(|m| m.as_str().parse().ok())
                .unwrap_or(0);
            let end_verse: Option<usize> =
                cap.name("end_verse").and_then(|m| m.as_str().parse().ok());

            // Look backwards through existing references to find a context
            if let Some(context_ref) =
                find_context_for_verse(existing_refs, start, verse, end_verse, versification)
            {
                results.push(LocatedReference {
                    reference: context_ref.reference.clone(),
                    start,
                    end,
                    context: Some(ContextualResolution {
                        original_text,
                        resolved_from: context_ref.resolved_from,
                    }),
                });
            }
        } else {
            // Parse chapter number(s)
            let chapter: usize = cap
                .name("chapter")
                .and_then(|m| m.as_str().parse().ok())
                .unwrap_or(0);
            let end_chapter: Option<usize> = cap
                .name("end_chapter")
                .and_then(|m| m.as_str().parse().ok());

            // Look backwards through existing references to find a context
            if let Some(context_ref) =
                find_context_for_chapter(existing_refs, start, chapter, end_chapter, versification)
            {
                results.push(LocatedReference {
                    reference: context_ref.reference.clone(),
                    start,
                    end,
                    context: Some(ContextualResolution {
                        original_text,
                        resolved_from: context_ref.resolved_from,
                    }),
                });
            }
        }
    }

    results
}

struct ContextResult {
    reference: Reference,
    resolved_from: String,
}

/// Find context for a verse reference by walking backwards through existing references
/// Prefers Range references over Single references as they typically indicate the main passage
fn find_context_for_verse(
    existing_refs: &[LocatedReference],
    position: usize,
    verse: usize,
    end_verse: Option<usize>,
    versification: Versification,
) -> Option<ContextResult> {
    // First pass: Look for verse-specific Range references (most specific)
    for loc_ref in existing_refs.iter().rev() {
        if loc_ref.start >= position {
            continue; // Skip references after our position
        }

        // Only consider verse-specific Range references in first pass
        if let Reference::Range {
            book,
            start_chapter,
            start_verse: start_v,
            end_chapter,
            end_verse: end_v,
            ..
        } = &loc_ref.reference
        {
            // Skip book-only references (full book spans)
            if *start_chapter == 1 && *start_v == 1 {
                if let Some(book_obj) = get_book(book) {
                    let max_ch = book_obj.chapters_versified(versification) as usize;
                    if *end_chapter == max_ch {
                        if let Some(max_v) =
                            book_obj.verses_in_chapter_versified(max_ch as u8, versification)
                        {
                            if *end_v == max_v as usize {
                                continue; // Skip this book-only reference
                            }
                        }
                    }
                }
            }

            // Skip full-chapter ranges (e.g., "Galatians 2" which becomes 2:1-21)
            // These are less specific than verse-specific ranges like "Ephesians 2:11-22"
            if *start_chapter == *end_chapter && *start_v == 1 {
                if let Some(book_obj) = get_book(book) {
                    if let Some(max_v) =
                        book_obj.verses_in_chapter_versified(*start_chapter as u8, versification)
                    {
                        if *end_v == max_v as usize {
                            continue; // Skip this full-chapter range
                        }
                    }
                }
            }

            // Skip if this is an empty book name
            if book.is_empty() {
                continue;
            }

            // Use the end chapter as the context (most recent chapter mentioned in the range)
            let chapter = *end_chapter;

            // Check if this book/chapter can contain the verse
            if let Some(book_obj) = get_book(book) {
                if let Some(max_verses) =
                    book_obj.verses_in_chapter_versified(chapter as u8, versification)
                {
                    if verse <= max_verses as usize {
                        // Found a valid Range context!
                        let reference = if let Some(end_v) = end_verse {
                            // Verify end_verse is also valid
                            if end_v <= max_verses as usize {
                                Reference::Range {
                                    book,
                                    start_chapter: chapter,
                                    start_verse: verse,
                                    end_book: book,
                                    end_chapter: chapter,
                                    end_verse: end_v,
                                }
                            } else {
                                continue; // end_verse is out of range
                            }
                        } else {
                            Reference::Single {
                                book,
                                chapter,
                                verse,
                            }
                        };

                        return Some(ContextResult {
                            reference,
                            resolved_from: format!("{} {}", book, chapter),
                        });
                    }
                }
            }
        }
    }

    // Second pass: Fall back to Single references if no Range found
    for loc_ref in existing_refs.iter().rev() {
        if loc_ref.start >= position {
            continue; // Skip references after our position
        }

        // Try to extract book and chapter from the reference
        let (book, chapter) = match &loc_ref.reference {
            Reference::Single { book, chapter, .. } => (book, *chapter),
            Reference::Range {
                book,
                start_chapter,
                start_verse,
                end_chapter,
                end_verse,
                ..
            } => {
                // Skip book-only references (full book spans)
                // These start at chapter 1, verse 1 and go to the last chapter/verse of the book
                if *start_chapter == 1 && *start_verse == 1 {
                    if let Some(book_obj) = get_book(book) {
                        let max_ch = book_obj.chapters_versified(versification) as usize;
                        if *end_chapter == max_ch {
                            if let Some(max_v) =
                                book_obj.verses_in_chapter_versified(max_ch as u8, versification)
                            {
                                if *end_verse == max_v as usize {
                                    continue; // Skip this book-only reference
                                }
                            }
                        }
                    }
                }
                // For ranges, use the end chapter as the context
                // (most recent chapter mentioned)
                (book, *end_chapter)
            }
        };

        // Skip if this is a book-only reference (empty book name)
        if book.is_empty() {
            continue;
        }

        // Check if this book/chapter can contain the verse
        if let Some(book_obj) = get_book(book) {
            if let Some(max_verses) =
                book_obj.verses_in_chapter_versified(chapter as u8, versification)
            {
                if verse <= max_verses as usize {
                    // Found a valid context!
                    let reference = if let Some(end_v) = end_verse {
                        // Verify end_verse is also valid
                        if end_v <= max_verses as usize {
                            Reference::Range {
                                book,
                                start_chapter: chapter,
                                start_verse: verse,
                                end_book: book,
                                end_chapter: chapter,
                                end_verse: end_v,
                            }
                        } else {
                            continue; // end_verse is out of range
                        }
                    } else {
                        Reference::Single {
                            book,
                            chapter,
                            verse,
                        }
                    };

                    return Some(ContextResult {
                        reference,
                        resolved_from: format!("{} {}", book, chapter),
                    });
                }
            }
        }
    }

    None
}

/// Find context for a chapter reference by walking backwards through existing references
/// Prefers Range references over Single references as they typically indicate the main passage
fn find_context_for_chapter(
    existing_refs: &[LocatedReference],
    position: usize,
    chapter: usize,
    end_chapter: Option<usize>,
    versification: Versification,
) -> Option<ContextResult> {
    // First pass: Look for Range references (preferred as they indicate main passages)
    for loc_ref in existing_refs.iter().rev() {
        if loc_ref.start >= position {
            continue; // Skip references after our position
        }

        // Only consider Range references in first pass
        if let Reference::Range {
            book,
            start_chapter,
            start_verse,
            end_chapter: ref_end_chapter,
            end_verse,
            ..
        } = &loc_ref.reference
        {
            // Skip book-only references
            if *start_chapter == 1 && *start_verse == 1 {
                if let Some(book_obj) = get_book(book) {
                    let max_ch = book_obj.chapters_versified(versification) as usize;
                    if *ref_end_chapter == max_ch {
                        if let Some(max_v) =
                            book_obj.verses_in_chapter_versified(max_ch as u8, versification)
                        {
                            if *end_verse == max_v as usize {
                                continue; // Skip this book-only reference
                            }
                        }
                    }
                }
            }

            // Skip if empty book name
            if book.is_empty() {
                continue;
            }

            // Check if this book contains the chapter
            if let Some(book_obj) = get_book(book) {
                let max_chapters = book_obj.chapters_versified(versification) as usize;
                if chapter <= max_chapters {
                    // Found a valid Range context!
                    let reference = if let Some(end_ch) = end_chapter {
                        // Verify end_chapter is also valid
                        if end_ch <= max_chapters {
                            // Get the last verse of the end chapter
                            let end_verse = book_obj
                                .verses_in_chapter_versified(end_ch as u8, versification)
                                .unwrap_or(1) as usize;

                            Reference::Range {
                                book,
                                start_chapter: chapter,
                                start_verse: 1,
                                end_book: book,
                                end_chapter: end_ch,
                                end_verse,
                            }
                        } else {
                            continue; // end_chapter is out of range
                        }
                    } else {
                        // Single chapter - create a range for the entire chapter
                        let end_verse = book_obj
                            .verses_in_chapter_versified(chapter as u8, versification)
                            .unwrap_or(1) as usize;

                        Reference::Range {
                            book,
                            start_chapter: chapter,
                            start_verse: 1,
                            end_book: book,
                            end_chapter: chapter,
                            end_verse,
                        }
                    };

                    return Some(ContextResult {
                        reference,
                        resolved_from: book.to_string(),
                    });
                }
            }
        }
    }

    // Second pass: Fall back to Single references if no Range found
    for loc_ref in existing_refs.iter().rev() {
        if loc_ref.start >= position {
            continue; // Skip references after our position
        }

        // Try to extract book from the reference
        // Also skip book-only references (full book spans)
        let book = match &loc_ref.reference {
            Reference::Single { book, .. } => book,
            Reference::Range {
                book,
                start_chapter,
                start_verse,
                end_chapter,
                end_verse,
                ..
            } => {
                // Skip book-only references
                if *start_chapter == 1 && *start_verse == 1 {
                    if let Some(book_obj) = get_book(book) {
                        let max_ch = book_obj.chapters_versified(versification) as usize;
                        if *end_chapter == max_ch {
                            if let Some(max_v) =
                                book_obj.verses_in_chapter_versified(max_ch as u8, versification)
                            {
                                if *end_verse == max_v as usize {
                                    continue; // Skip this book-only reference
                                }
                            }
                        }
                    }
                }
                book
            }
        };

        // Skip if this is a book-only reference (empty book name)
        if book.is_empty() {
            continue;
        }

        // Check if this book contains the chapter
        if let Some(book_obj) = get_book(book) {
            let max_chapters = book_obj.chapters_versified(versification) as usize;
            if chapter <= max_chapters {
                // Found a valid context!
                let reference = if let Some(end_ch) = end_chapter {
                    // Verify end_chapter is also valid
                    if end_ch <= max_chapters {
                        // Get the last verse of the end chapter
                        let end_verse = book_obj
                            .verses_in_chapter_versified(end_ch as u8, versification)
                            .unwrap_or(1) as usize;

                        Reference::Range {
                            book,
                            start_chapter: chapter,
                            start_verse: 1,
                            end_book: book,
                            end_chapter: end_ch,
                            end_verse,
                        }
                    } else {
                        continue; // end_chapter is out of range
                    }
                } else {
                    // Single chapter - create a range for the entire chapter
                    let end_verse = book_obj
                        .verses_in_chapter_versified(chapter as u8, versification)
                        .unwrap_or(1) as usize;

                    Reference::Range {
                        book,
                        start_chapter: chapter,
                        start_verse: 1,
                        end_book: book,
                        end_chapter: chapter,
                        end_verse,
                    }
                };

                return Some(ContextResult {
                    reference,
                    resolved_from: book.to_string(),
                });
            }
        }
    }

    None
}

/// Parse bare chapter:verse references (without book names)
fn parse_bare_references(input: &str) -> Vec<LocatedReference> {
    use once_cell::sync::Lazy;

    // Match patterns like "3:16", "1:1-5", "119:105"
    // Note: We can't use look-around in Rust regex, so we match broadly and check boundaries in code
    static BARE_REF_REGEX: Lazy<::regex::Regex> = Lazy::new(|| {
        ::regex::Regex::new(r"(?P<chapter>\d{1,3})[:.](?P<verse>\d{1,3})(?:\s*(?:--|[-–—])\s*(?P<end_verse>\d{1,3}))?").unwrap()
    });

    let mut results = Vec::new();

    for cap in BARE_REF_REGEX.captures_iter(input) {
        if let Some(mat) = cap.get(0) {
            let start = mat.start();
            let end = mat.end();

            // Check word boundaries manually
            let has_start_boundary = start == 0 || {
                let prev_char = input[..start].chars().last().unwrap();
                !prev_char.is_alphanumeric()
            };

            let has_end_boundary = end == input.len() || {
                let next_char = input[end..].chars().next().unwrap();
                !next_char.is_alphanumeric()
            };

            if !has_start_boundary || !has_end_boundary {
                continue;
            }

            let chapter: usize = cap
                .name("chapter")
                .and_then(|m: ::regex::Match| m.as_str().parse().ok())
                .unwrap_or(0);
            let verse: usize = cap
                .name("verse")
                .and_then(|m: ::regex::Match| m.as_str().parse().ok())
                .unwrap_or(0);
            let end_verse: Option<usize> = cap
                .name("end_verse")
                .and_then(|m: ::regex::Match| m.as_str().parse().ok());

            // Create a partial reference (we'll use a placeholder book)
            // For now, return as a Range without a specific book context
            // The caller can interpret this as needing context
            let reference = if let Some(end_v) = end_verse {
                Reference::Range {
                    book: "",
                    start_chapter: chapter,
                    start_verse: verse,
                    end_book: "",
                    end_chapter: chapter,
                    end_verse: end_v,
                }
            } else {
                Reference::Single {
                    book: "",
                    chapter,
                    verse,
                }
            };

            results.push(LocatedReference {
                reference,
                start: mat.start(),
                end: mat.end(),
                context: None,
            });
        }
    }

    results
}

#[cfg(test)]
mod tests {
    use crate::reference_parser::{
        DiagnosticSeverity, ParseDiagnosticKind, Parser, Text, parse_reference,
        parse_reference_with_offsets,
    };

    #[test]
    fn detect_possible_references() {
        let input =
            "In the beginning (Gen 1:1), the Word existed (Jhn 1 1). Guard this truth (2 Tim).";
        let possibles = super::contains_possible_references(input);
        println!("{:?}", possibles);
        assert_eq!(possibles.len(), 3);
        assert_eq!(possibles[0], "Gen");
        assert_eq!(possibles[1], "Jhn");
        assert_eq!(possibles[2], "2 Tim");
    }

    #[test]
    fn parse_basic_reference() {
        assert_eq!("John 3:16", parse_reference("John 3:16")[0].to_string());
        assert_eq!(
            "2 Timothy 2:15",
            parse_reference("2 Timothy 2:15")[0].to_string()
        );
    }

    #[test]
    fn parse_reference_without_verse() {
        assert_eq!("Genesis 1", parse_reference("Genesis 1")[0].to_string());
    }

    #[test]
    fn parse_reference_with_abbreviation() {
        assert_eq!("Genesis 1:1", parse_reference("Gen 1:1")[0].to_string());
        assert_eq!("John 1:1", parse_reference("Jhn 1:1")[0].to_string());
        assert_eq!(
            "2 Timothy 3:16",
            parse_reference("2 Tim 3:16")[0].to_string()
        );
    }

    #[test]
    fn parse_reference_with_alias() {
        assert_eq!(
            "John 1:1",
            parse_reference("The Gospel of John 1:1")[0].to_string()
        );
    }

    #[test]
    fn parse_reference_within_text() {
        let input =
            "In the beginning (Gen 1:1), the Word existed (Jhn 1 1). Guard this truth (2 Tim).";
        let references = super::parse_reference(input);
        assert_eq!(references.len(), 3);
        assert_eq!(references[0].to_string(), "Genesis 1:1");
        assert_eq!(references[1].to_string(), "John 1:1");
        assert_eq!(references[2].to_string(), "2 Timothy");
    }

    #[test]
    fn parse_reference_with_offsets_basic() {
        let input = "Read John 3:16 today";
        let located = parse_reference_with_offsets(input);

        assert_eq!(located.len(), 1);
        assert_eq!(located[0].reference.to_string(), "John 3:16");
        assert_eq!(located[0].start, 5);
        assert_eq!(located[0].end, 14);
        assert_eq!(&input[located[0].start..located[0].end], "John 3:16");
    }

    #[test]
    fn text_struct_parse() {
        let input =
            "In the beginning (Gen 1:1), the Word existed (Jhn 1 1). Guard this truth (2 Tim).";
        let text = Text::parse(input);

        assert_eq!(text.source, input);
        assert_eq!(text.references.len(), 3);

        // Check that offsets are sorted by position
        assert!(text.references[0].start < text.references[1].start);
        assert!(text.references[1].start < text.references[2].start);

        // Check we can extract the original text for each reference
        assert_eq!(text.get_text(&text.references[0]), "Gen 1:1");
        assert_eq!(text.get_text(&text.references[1]), "Jhn 1 1");
        assert_eq!(text.get_text(&text.references[2]), "2 Tim");
    }

    #[test]
    fn parser_parse_returns_text_with_diagnostics() {
        let parser = Parser::default();
        let text = parser.parse("Read John 3:16 today");

        assert_eq!(text.references().count(), 1);
        assert!(text.diagnostics.is_empty());
    }

    #[test]
    fn parser_emits_fuzzy_match_diagnostic() {
        let parser = Parser::new().with_options(ParseOptions::new().fuzzy_matching(true));
        let text = parser.parse("Read Genisis 1:1 today");

        assert_eq!(text.references().count(), 1);
        assert_eq!(text.diagnostics.len(), 1);
        assert_eq!(text.diagnostics[0].severity, DiagnosticSeverity::Info);
        assert!(matches!(
            text.diagnostics[0].kind,
            ParseDiagnosticKind::FuzzyBookMatch {
                ref matched,
                resolved_to: "Genesis",
                ..
            } if matched == "Genisis"
        ));
    }

    #[test]
    fn text_struct_multiple_references() {
        let input = "Compare Genesis 1:1 with John 1:1 and Revelation 22:21";
        let text = Text::parse(input);

        assert_eq!(text.references.len(), 3);
        assert_eq!(text.references[0].reference.to_string(), "Genesis 1:1");
        assert_eq!(text.references[1].reference.to_string(), "John 1:1");
        assert_eq!(text.references[2].reference.to_string(), "Revelation 22:21");

        // Verify offsets are correct
        assert_eq!(text.get_text(&text.references[0]), "Genesis 1:1");
        assert_eq!(text.get_text(&text.references[1]), "John 1:1");
        assert_eq!(text.get_text(&text.references[2]), "Revelation 22:21");
    }

    #[test]
    fn text_struct_empty_input() {
        let text = Text::parse("");
        assert_eq!(text.references.len(), 0);

        let text = Text::parse("No references here.");
        assert_eq!(text.references.len(), 0);
    }

    #[test]
    fn text_without_references_basic() {
        let text = Text::parse("the good shepherd John");
        assert_eq!(text.text_without_references(), "the good shepherd");
    }

    #[test]
    fn text_without_references_with_verse() {
        let text = Text::parse("the good shepherd John 10:11");
        assert_eq!(text.text_without_references(), "the good shepherd");
    }

    #[test]
    fn text_without_references_multiple() {
        let text = Text::parse("Compare Genesis 1:1 with John 1:1");
        assert_eq!(text.text_without_references(), "Compare with");
    }

    #[test]
    fn text_without_references_at_start() {
        let text = Text::parse("John 3:16 is famous");
        assert_eq!(text.text_without_references(), "is famous");
    }

    #[test]
    fn text_without_references_no_refs() {
        let text = Text::parse("just some regular text");
        assert_eq!(text.text_without_references(), "just some regular text");
    }

    #[test]
    fn text_without_references_only_refs() {
        let text = Text::parse("Genesis 1:1");
        assert_eq!(text.text_without_references(), "");
    }

    #[test]
    fn recognize_all_66_books() {
        // Test all 66 books of the Bible with various forms (full name, abbreviations)
        let test_cases = [
            // Old Testament
            ("Genesis 1:1", "Genesis 1:1"),
            ("Gen 1:1", "Genesis 1:1"),
            ("Exodus 20:1", "Exodus 20:1"),
            ("Ex 20:1", "Exodus 20:1"),
            ("Leviticus 19:18", "Leviticus 19:18"),
            ("Lev 19:18", "Leviticus 19:18"),
            ("Numbers 6:24", "Numbers 6:24"),
            ("Num 6:24", "Numbers 6:24"),
            ("Deuteronomy 6:4", "Deuteronomy 6:4"),
            ("Deut 6:4", "Deuteronomy 6:4"),
            ("Joshua 1:9", "Joshua 1:9"),
            ("Josh 1:9", "Joshua 1:9"),
            ("Judges 6:12", "Judges 6:12"),
            ("Judg 6:12", "Judges 6:12"),
            ("Ruth 1:16", "Ruth 1:16"),
            ("1 Samuel 3:10", "1 Samuel 3:10"),
            ("1 Sam 3:10", "1 Samuel 3:10"),
            ("2 Samuel 7:12", "2 Samuel 7:12"),
            ("2 Sam 7:12", "2 Samuel 7:12"),
            ("1 Kings 3:9", "1 Kings 3:9"),
            ("1 Kgs 3:9", "1 Kings 3:9"),
            ("2 Kings 2:11", "2 Kings 2:11"),
            ("2 Kgs 2:11", "2 Kings 2:11"),
            ("1 Chronicles 16:34", "1 Chronicles 16:34"),
            ("1 Chr 16:34", "1 Chronicles 16:34"),
            ("2 Chronicles 7:14", "2 Chronicles 7:14"),
            ("2 Chr 7:14", "2 Chronicles 7:14"),
            ("Ezra 7:10", "Ezra 7:10"),
            ("Nehemiah 8:10", "Nehemiah 8:10"),
            ("Neh 8:10", "Nehemiah 8:10"),
            ("Esther 4:14", "Esther 4:14"),
            ("Esth 4:14", "Esther 4:14"),
            ("Job 1:21", "Job 1:21"),
            ("Psalms 23:1", "Psalms 23:1"),
            ("Ps 23:1", "Psalms 23:1"),
            ("Psalm 23:1", "Psalms 23:1"),
            ("Proverbs 3:5", "Proverbs 3:5"),
            ("Prov 3:5", "Proverbs 3:5"),
            ("Ecclesiastes 3:1", "Ecclesiastes 3:1"),
            ("Eccl 3:1", "Ecclesiastes 3:1"),
            ("Song of Songs 2:4", "Song of Songs 2:4"),
            ("Song of Solomon 2:4", "Song of Songs 2:4"),
            ("Isaiah 53:5", "Isaiah 53:5"),
            ("Isa 53:5", "Isaiah 53:5"),
            ("Jeremiah 29:11", "Jeremiah 29:11"),
            ("Jer 29:11", "Jeremiah 29:11"),
            ("Lamentations 3:22", "Lamentations 3:22"),
            ("Lam 3:22", "Lamentations 3:22"),
            ("Ezekiel 37:1", "Ezekiel 37:1"),
            ("Ezek 37:1", "Ezekiel 37:1"),
            ("Daniel 3:17", "Daniel 3:17"),
            ("Dan 3:17", "Daniel 3:17"),
            ("Hosea 6:6", "Hosea 6:6"),
            ("Hos 6:6", "Hosea 6:6"),
            ("Joel 2:28", "Joel 2:28"),
            ("Amos 5:24", "Amos 5:24"),
            ("Obadiah 1:4", "Obadiah 1:4"),
            ("Obad 1:4", "Obadiah 1:4"),
            ("Jonah 2:9", "Jonah 2:9"),
            ("Micah 6:8", "Micah 6:8"),
            ("Mic 6:8", "Micah 6:8"),
            ("Nahum 1:7", "Nahum 1:7"),
            ("Nah 1:7", "Nahum 1:7"),
            ("Habakkuk 2:4", "Habakkuk 2:4"),
            ("Hab 2:4", "Habakkuk 2:4"),
            ("Zephaniah 3:17", "Zephaniah 3:17"),
            ("Zeph 3:17", "Zephaniah 3:17"),
            ("Haggai 2:9", "Haggai 2:9"),
            ("Hag 2:9", "Haggai 2:9"),
            ("Zechariah 9:9", "Zechariah 9:9"),
            ("Zech 9:9", "Zechariah 9:9"),
            ("Malachi 3:10", "Malachi 3:10"),
            ("Mal 3:10", "Malachi 3:10"),
            // New Testament
            ("Matthew 5:3", "Matthew 5:3"),
            ("Matt 5:3", "Matthew 5:3"),
            ("Mt 5:3", "Matthew 5:3"),
            ("Mark 1:1", "Mark 1:1"),
            ("Mk 1:1", "Mark 1:1"),
            ("Luke 2:10", "Luke 2:10"),
            ("Lk 2:10", "Luke 2:10"),
            ("John 3:16", "John 3:16"),
            ("Jn 3:16", "John 3:16"),
            ("Acts 2:38", "Acts 2:38"),
            ("Romans 8:28", "Romans 8:28"),
            ("Rom 8:28", "Romans 8:28"),
            ("1 Corinthians 13:4", "1 Corinthians 13:4"),
            ("1 Cor 13:4", "1 Corinthians 13:4"),
            ("2 Corinthians 5:17", "2 Corinthians 5:17"),
            ("2 Cor 5:17", "2 Corinthians 5:17"),
            ("Galatians 5:22", "Galatians 5:22"),
            ("Gal 5:22", "Galatians 5:22"),
            ("Ephesians 2:8", "Ephesians 2:8"),
            ("Eph 2:8", "Ephesians 2:8"),
            ("Philippians 4:13", "Philippians 4:13"),
            ("Phil 4:13", "Philippians 4:13"),
            ("Colossians 3:23", "Colossians 3:23"),
            ("Col 3:23", "Colossians 3:23"),
            ("1 Thessalonians 5:16", "1 Thessalonians 5:16"),
            ("1 Thess 5:16", "1 Thessalonians 5:16"),
            ("2 Thessalonians 3:3", "2 Thessalonians 3:3"),
            ("2 Thess 3:3", "2 Thessalonians 3:3"),
            ("1 Timothy 6:12", "1 Timothy 6:12"),
            ("1 Tim 6:12", "1 Timothy 6:12"),
            ("2 Timothy 2:15", "2 Timothy 2:15"),
            ("2 Tim 2:15", "2 Timothy 2:15"),
            ("Titus 3:5", "Titus 3:5"),
            ("Tit 3:5", "Titus 3:5"),
            ("Philemon 1:6", "Philemon 1:6"),
            ("Phlm 1:6", "Philemon 1:6"),
            ("Hebrews 11:1", "Hebrews 11:1"),
            ("Heb 11:1", "Hebrews 11:1"),
            ("James 1:2", "James 1:2"),
            ("Jas 1:2", "James 1:2"),
            ("1 Peter 5:7", "1 Peter 5:7"),
            ("1 Pet 5:7", "1 Peter 5:7"),
            ("2 Peter 1:3", "2 Peter 1:3"),
            ("2 Pet 1:3", "2 Peter 1:3"),
            ("1 John 4:8", "1 John 4:8"),
            ("1 Jn 4:8", "1 John 4:8"),
            ("2 John 1:6", "2 John 1:6"),
            ("2 Jn 1:6", "2 John 1:6"),
            ("3 John 1:4", "3 John 1:4"),
            ("3 Jn 1:4", "3 John 1:4"),
            ("Jude 1:3", "Jude 1:3"),
            ("Revelation 21:4", "Revelation 21:4"),
            ("Rev 21:4", "Revelation 21:4"),
        ];

        for (input, expected) in test_cases {
            let result = parse_reference(input);
            assert!(
                !result.is_empty(),
                "Failed to parse: '{}' - no references found",
                input
            );
            assert_eq!(
                result[0].to_string(),
                expected,
                "Failed for input: '{}'",
                input
            );
        }
    }

    #[test]
    fn stress_test_large_text_parsing() {
        use std::time::Instant;

        // Generate a large text with many Bible references interspersed
        let sample_text = r#"
            The Bible begins with Genesis 1:1 saying "In the beginning God created the heavens and the earth."
            Exodus 20:3 gives us the first commandment. Leviticus 19:18 teaches us to love our neighbor.
            Numbers 6:24 contains the priestly blessing. Deuteronomy 6:4 is the Shema.
            Joshua 1:9 encourages us to be strong and courageous. Judges 6:12 calls Gideon a mighty warrior.
            Ruth 1:16 shows loyalty. 1 Samuel 3:10 records Samuel's response to God.
            2 Samuel 7:12 contains the Davidic covenant. 1 Kings 3:9 shows Solomon's wisdom.
            2 Kings 2:11 describes Elijah's ascension. 1 Chronicles 16:34 praises God's goodness.
            2 Chronicles 7:14 calls for repentance. Ezra 7:10 describes Ezra's devotion.
            Nehemiah 8:10 reminds us that joy is our strength. Esther 4:14 speaks of purpose.
            Job 1:21 shows acceptance of God's will. Psalms 23:1 declares the Lord as shepherd.
            Proverbs 3:5 calls us to trust in the Lord. Ecclesiastes 3:1 speaks of seasons.
            Song of Songs 2:4 describes God's love. Isaiah 53:5 prophesies the Messiah's suffering.
            Jeremiah 29:11 gives hope for the future. Lamentations 3:22 speaks of God's mercies.
            Ezekiel 37:1 describes the valley of dry bones. Daniel 3:17 shows faith in trial.
            Hosea 6:6 teaches about mercy over sacrifice. Joel 2:28 prophesies the Spirit's outpouring.
            Amos 5:24 calls for justice. Obadiah 1:4 warns the proud. Jonah 2:9 declares salvation.
            Micah 6:8 defines what God requires. Nahum 1:7 declares God's goodness.
            Habakkuk 2:4 says the righteous live by faith. Zephaniah 3:17 describes God's joy.
            Haggai 2:9 promises glory. Zechariah 9:9 prophesies the coming king.
            Malachi 3:10 challenges us to tithe. Matthew 5:3 begins the Beatitudes.
            Mark 1:1 introduces the Gospel. Luke 2:10 announces good news of great joy.
            John 3:16 is the most famous verse. Acts 2:38 calls for repentance and baptism.
            Romans 8:28 assures us all things work for good. 1 Corinthians 13:4 defines love.
            2 Corinthians 5:17 speaks of new creation. Galatians 5:22 lists the fruit of the Spirit.
            Ephesians 2:8 explains salvation by grace. Philippians 4:13 declares strength in Christ.
            Colossians 3:23 teaches us to work heartily. 1 Thessalonians 5:16 calls us to rejoice.
            2 Thessalonians 3:3 assures God's faithfulness. 1 Timothy 6:12 urges fighting the good fight.
            2 Timothy 2:15 calls for rightly handling the word. Titus 3:5 speaks of salvation.
            Philemon 1:6 speaks of sharing faith. Hebrews 11:1 defines faith.
            James 1:2 teaches about trials. 1 Peter 5:7 tells us to cast our cares.
            2 Peter 1:3 speaks of divine power. 1 John 4:8 declares God is love.
            2 John 1:6 defines walking in love. 3 John 1:4 expresses joy in truth.
            Jude 1:3 urges contending for the faith. Revelation 21:4 promises no more tears.
        "#;

        // Repeat the text to make it larger
        let large_text = sample_text.repeat(100);
        let text_size = large_text.len();

        let start = Instant::now();
        let references = parse_reference(&large_text);
        let duration = start.elapsed();

        // Verify we found the expected number of references
        // Each iteration should find ~66 references (one per book mentioned)
        assert!(
            references.len() >= 66 * 100,
            "Expected at least {} references, found {}",
            66 * 100,
            references.len()
        );

        // Performance assertion: should complete in under 1 second for ~350KB of text
        assert!(
            duration.as_millis() < 1000,
            "Parsing took too long: {:?} for {} bytes ({} references)",
            duration,
            text_size,
            references.len()
        );

        println!(
            "Stress test: Parsed {} bytes in {:?}, found {} references ({:.2} refs/ms; {:.2} words/ms)",
            text_size,
            duration,
            references.len(),
            references.len() as f64 / duration.as_millis() as f64,
            436f64 * 100f64 / duration.as_millis() as f64
        );
    }

    // ==================== VERSE RANGE TESTS ====================

    #[test]
    fn parse_verse_range_with_hyphen() {
        let refs = parse_reference("John 3:16-18");
        assert_eq!(refs.len(), 1);
        assert_eq!(refs[0].to_string(), "John 3:16–18");

        // Verify it's a Range variant
        match &refs[0] {
            crate::reference::Reference::Range {
                book,
                start_chapter,
                start_verse,
                end_chapter,
                end_verse,
                ..
            } => {
                assert_eq!(*book, "John");
                assert_eq!(*start_chapter, 3);
                assert_eq!(*start_verse, 16);
                assert_eq!(*end_chapter, 3);
                assert_eq!(*end_verse, 18);
            }
            _ => panic!("Expected Range reference"),
        }
    }

    #[test]
    fn parse_verse_range_with_en_dash() {
        let refs = parse_reference("Genesis 1:1–5");
        assert_eq!(refs.len(), 1);
        assert_eq!(refs[0].to_string(), "Genesis 1:1–5");

        match &refs[0] {
            crate::reference::Reference::Range {
                start_verse,
                end_verse,
                ..
            } => {
                assert_eq!(*start_verse, 1);
                assert_eq!(*end_verse, 5);
            }
            _ => panic!("Expected Range reference"),
        }
    }

    #[test]
    fn parse_verse_range_with_em_dash() {
        let refs = parse_reference("Romans 8:28—39");
        assert_eq!(refs.len(), 1);
        assert_eq!(refs[0].to_string(), "Romans 8:28–39");

        match &refs[0] {
            crate::reference::Reference::Range {
                start_verse,
                end_verse,
                ..
            } => {
                assert_eq!(*start_verse, 28);
                assert_eq!(*end_verse, 39);
            }
            _ => panic!("Expected Range reference"),
        }
    }

    #[test]
    fn parse_verse_range_with_spaces() {
        // Test with spaces around the dash
        let refs = parse_reference("Matthew 5:1 - 12");
        assert_eq!(refs.len(), 1);
        assert_eq!(refs[0].to_string(), "Matthew 5:1–12");
    }

    #[test]
    fn parse_verse_range_small_range() {
        let refs = parse_reference("Psalm 23:1-2");
        assert_eq!(refs.len(), 1);

        match &refs[0] {
            crate::reference::Reference::Range {
                start_verse,
                end_verse,
                ..
            } => {
                assert_eq!(*start_verse, 1);
                assert_eq!(*end_verse, 2);
            }
            _ => panic!("Expected Range reference"),
        }
    }

    #[test]
    fn parse_verse_range_large_range() {
        let refs = parse_reference("John 1:1-51");
        assert_eq!(refs.len(), 1);

        match &refs[0] {
            crate::reference::Reference::Range {
                start_verse,
                end_verse,
                ..
            } => {
                assert_eq!(*start_verse, 1);
                assert_eq!(*end_verse, 51);
            }
            _ => panic!("Expected Range reference"),
        }
    }

    #[test]
    fn parse_verse_range_with_abbreviation() {
        let refs = parse_reference("Gen 2:4-25");
        assert_eq!(refs.len(), 1);
        assert_eq!(refs[0].to_string(), "Genesis 2:4–25");
    }

    #[test]
    fn parse_verse_range_numbered_book() {
        let refs = parse_reference("1 Corinthians 13:1-13");
        assert_eq!(refs.len(), 1);
        // 1 Corinthians 13 has exactly 13 verses, so "1 Corinthians 13:1-13" is the full chapter
        // and should be displayed as "1 Corinthians 13"
        assert_eq!(refs[0].to_string(), "1 Corinthians 13");

        match &refs[0] {
            crate::reference::Reference::Range {
                book,
                start_chapter,
                start_verse,
                end_verse,
                ..
            } => {
                assert_eq!(*book, "1 Corinthians");
                assert_eq!(*start_chapter, 13);
                assert_eq!(*start_verse, 1);
                assert_eq!(*end_verse, 13);
            }
            _ => panic!("Expected Range reference"),
        }
    }

    #[test]
    fn parse_verse_range_in_context() {
        let text = "Read John 3:16-20 for context";
        let refs = parse_reference(text);
        assert_eq!(refs.len(), 1);
        assert_eq!(refs[0].to_string(), "John 3:16–20");
    }

    #[test]
    fn parse_verse_range_with_offsets() {
        let input = "Check out John 3:16-18 today";
        let located = parse_reference_with_offsets(input);

        assert_eq!(located.len(), 1);
        assert_eq!(located[0].reference.to_string(), "John 3:16–18");
        assert_eq!(located[0].start, 10);
        assert_eq!(located[0].end, 22);
        assert_eq!(&input[located[0].start..located[0].end], "John 3:16-18");
    }

    #[test]
    fn parse_multiple_ranges() {
        let text = "Read Genesis 1:1-5 and John 3:16-20";
        let refs = parse_reference(text);
        assert_eq!(refs.len(), 2);
        assert_eq!(refs[0].to_string(), "Genesis 1:1–5");
        assert_eq!(refs[1].to_string(), "John 3:16–20");
    }

    #[test]
    fn parse_mixed_single_and_range() {
        let text = "John 3:16 and John 3:17-20";
        let refs = parse_reference(text);
        assert_eq!(refs.len(), 2);

        // First should be Single
        match &refs[0] {
            crate::reference::Reference::Single { verse, .. } => {
                assert_eq!(*verse, 16);
            }
            _ => panic!("Expected Single reference"),
        }

        // Second should be Range
        match &refs[1] {
            crate::reference::Reference::Range {
                start_verse,
                end_verse,
                ..
            } => {
                assert_eq!(*start_verse, 17);
                assert_eq!(*end_verse, 20);
            }
            _ => panic!("Expected Range reference"),
        }
    }

    #[test]
    fn text_without_references_includes_ranges() {
        let text = Text::parse("the good shepherd John 10:11-16");
        assert_eq!(text.text_without_references(), "the good shepherd");
    }

    #[test]
    fn parse_verse_range_famous_passages() {
        // Test some well-known passage ranges
        let test_cases = [
            ("Romans 8:28-39", "Romans 8:28–39"),
            ("Ephesians 6:10-18", "Ephesians 6:10–18"),
            ("Psalm 119:1-8", "Psalms 119:1–8"), // "Psalm" maps to book "Psalms"
            ("Matthew 5:3-12", "Matthew 5:3–12"), // The Beatitudes
            ("1 Corinthians 13:4-8", "1 Corinthians 13:4–8"), // Love chapter
        ];

        for (input, expected) in test_cases {
            let refs = parse_reference(input);
            assert_eq!(refs.len(), 1, "Failed for input: {}", input);
            assert_eq!(refs[0].to_string(), expected, "Failed for input: {}", input);
        }
    }

    #[test]
    fn parse_verse_range_colon_separator() {
        // Test with colon separator between chapter and verse
        let refs = parse_reference("John 3:16-20");
        assert_eq!(refs.len(), 1);
        assert_eq!(refs[0].to_string(), "John 3:16–20");
    }

    #[test]
    fn parse_verse_range_space_separator() {
        // Test with space separator between chapter and verse (common in some locales)
        let refs = parse_reference("John 3 16-20");
        assert_eq!(refs.len(), 1);
        assert_eq!(refs[0].to_string(), "John 3:16–20");
    }

    #[test]
    fn parse_verse_range_edge_case_single_verse() {
        // A range with the same start and end verse should still be a Range
        let refs = parse_reference("John 3:16-16");
        assert_eq!(refs.len(), 1);

        match &refs[0] {
            crate::reference::Reference::Range {
                start_verse,
                end_verse,
                ..
            } => {
                assert_eq!(*start_verse, 16);
                assert_eq!(*end_verse, 16);
            }
            _ => panic!("Expected Range reference"),
        }
    }

    #[test]
    fn parse_verse_range_not_confused_with_negative() {
        // Make sure "-16" isn't parsed as part of a range when there's no verse before it
        let refs = parse_reference("John 3");
        assert_eq!(refs.len(), 1);
        // Should be parsed as full chapter, not involving any negative numbers
        match &refs[0] {
            crate::reference::Reference::Range {
                start_verse,
                end_verse,
                ..
            } => {
                assert_eq!(*start_verse, 1);
                assert!(*end_verse > 1); // Full chapter
            }
            _ => panic!("Expected Range reference for full chapter"),
        }
    }

    #[test]
    fn parse_verse_range_does_not_cross_chapters() {
        // User input like "John 3:16-4:2" is not currently supported
        // This test documents current behavior - it should parse "John 3:16" only
        let refs = parse_reference("John 3:16-4:2");
        // The regex won't match the cross-chapter part, so we get "John 3:16-4" as a range
        // within chapter 3 (though verse 4 doesn't make sense after 16)
        assert!(refs.len() >= 1);
    }

    #[test]
    fn parse_verse_range_realistic_search_query() {
        // Test a realistic search scenario with a range
        let text = "the bread of life discourse John 6:22-59";
        let parsed = Text::parse(text);

        assert_eq!(parsed.references.len(), 1);
        assert_eq!(parsed.references[0].reference.to_string(), "John 6:22–59");
        assert_eq!(
            parsed.text_without_references(),
            "the bread of life discourse"
        );
    }

    // ==================== OPTIMISTIC PARSING TESTS ====================

    use super::{ParseOptions, parse_reference_with_options};

    #[test]
    fn parse_options_default() {
        let opts = ParseOptions::new();
        assert!(!opts.fuzzy_matching);
        assert!(!opts.bare_references);
        assert!(!opts.relaxed_boundaries);
    }

    #[test]
    fn parse_options_optimistic() {
        let opts = ParseOptions::optimistic();
        assert!(opts.fuzzy_matching);
        assert!(opts.bare_references);
        assert!(opts.relaxed_boundaries);
    }

    #[test]
    fn parse_options_builder() {
        let opts = ParseOptions::new()
            .fuzzy_matching(true)
            .bare_references(false)
            .relaxed_boundaries(true);
        assert!(opts.fuzzy_matching);
        assert!(!opts.bare_references);
        assert!(opts.relaxed_boundaries);
    }

    #[test]
    fn optimistic_relaxed_boundaries_no_space() {
        // Test parsing without space between book and chapter
        let opts = ParseOptions::new().relaxed_boundaries(true);
        let refs = parse_reference_with_options("john3:16", &opts);
        assert_eq!(refs.len(), 1);
        assert_eq!(refs[0].to_string(), "John 3:16");
    }

    #[test]
    fn optimistic_relaxed_boundaries_gen1() {
        let opts = ParseOptions::new().relaxed_boundaries(true);
        let refs = parse_reference_with_options("gen1:1", &opts);
        assert_eq!(refs.len(), 1);
        assert_eq!(refs[0].to_string(), "Genesis 1:1");
    }

    #[test]
    fn optimistic_relaxed_boundaries_numbered_book() {
        let opts = ParseOptions::new().relaxed_boundaries(true);
        let refs = parse_reference_with_options("1john1:1", &opts);
        assert_eq!(refs.len(), 1);
        assert_eq!(refs[0].to_string(), "1 John 1:1");
    }

    #[test]
    fn optimistic_fuzzy_matching_genesis_typo() {
        let opts = ParseOptions::new().fuzzy_matching(true);
        let refs = parse_reference_with_options("Genisis 1:1", &opts);
        assert_eq!(refs.len(), 1);
        assert_eq!(refs[0].to_string(), "Genesis 1:1");
    }

    #[test]
    fn optimistic_fuzzy_matching_matthew_typo() {
        let opts = ParseOptions::new().fuzzy_matching(true);
        let refs = parse_reference_with_options("Mathew 5:3", &opts);
        assert_eq!(refs.len(), 1);
        assert_eq!(refs[0].to_string(), "Matthew 5:3");
    }

    #[test]
    fn optimistic_fuzzy_matching_revelations() {
        // Common mistake: "Revelations" instead of "Revelation"
        let opts = ParseOptions::new().fuzzy_matching(true);
        let refs = parse_reference_with_options("Revelations 21:4", &opts);
        assert_eq!(refs.len(), 1);
        assert_eq!(refs[0].to_string(), "Revelation 21:4");
    }

    #[test]
    fn optimistic_bare_references_chapter_verse() {
        let opts = ParseOptions::new().bare_references(true);
        let refs = parse_reference_with_options("3:16", &opts);
        assert_eq!(refs.len(), 1);
        // Bare references have empty book
        match &refs[0] {
            crate::reference::Reference::Single {
                book,
                chapter,
                verse,
            } => {
                assert_eq!(*book, "");
                assert_eq!(*chapter, 3);
                assert_eq!(*verse, 16);
            }
            _ => panic!("Expected Single reference"),
        }
    }

    #[test]
    fn optimistic_bare_references_with_range() {
        let opts = ParseOptions::new().bare_references(true);
        let refs = parse_reference_with_options("1:1-5", &opts);
        assert_eq!(refs.len(), 1);
        match &refs[0] {
            crate::reference::Reference::Range {
                book,
                start_chapter,
                start_verse,
                end_verse,
                ..
            } => {
                assert_eq!(*book, "");
                assert_eq!(*start_chapter, 1);
                assert_eq!(*start_verse, 1);
                assert_eq!(*end_verse, 5);
            }
            _ => panic!("Expected Range reference"),
        }
    }

    #[test]
    fn optimistic_bare_references_psalm_119() {
        let opts = ParseOptions::new().bare_references(true);
        let refs = parse_reference_with_options("119:105", &opts);
        assert_eq!(refs.len(), 1);
        match &refs[0] {
            crate::reference::Reference::Single { chapter, verse, .. } => {
                assert_eq!(*chapter, 119);
                assert_eq!(*verse, 105);
            }
            _ => panic!("Expected Single reference"),
        }
    }

    #[test]
    fn optimistic_full_preset_search_query() {
        // Test a realistic search bar scenario with optimistic preset
        let opts = ParseOptions::optimistic();
        let refs = parse_reference_with_options("good shepherd jn10:11", &opts);
        assert_eq!(refs.len(), 1);
        assert_eq!(refs[0].to_string(), "John 10:11");
    }

    #[test]
    fn optimistic_text_parse_with_options() {
        let opts = ParseOptions::optimistic();
        let text = Text::parse_with_options("love chapter 1cor13", &opts);
        assert_eq!(text.references.len(), 1);
        assert_eq!(text.references[0].reference.to_string(), "1 Corinthians 13");
    }

    #[test]
    fn optimistic_does_not_affect_strict_parsing() {
        // Ensure default/strict parsing still works correctly
        let refs = parse_reference("John 3:16");
        assert_eq!(refs.len(), 1);
        assert_eq!(refs[0].to_string(), "John 3:16");

        // Strict mode should not find "john3:16" without space
        let refs = parse_reference("john3:16");
        assert_eq!(refs.len(), 0);
    }

    #[test]
    fn optimistic_combined_fuzzy_and_relaxed() {
        let opts = ParseOptions::new()
            .fuzzy_matching(true)
            .relaxed_boundaries(true);
        // Typo + no space
        let refs = parse_reference_with_options("mathew5:3", &opts);
        assert_eq!(refs.len(), 1);
        assert_eq!(refs[0].to_string(), "Matthew 5:3");
    }

    #[test]
    fn optimistic_mixed_input_search_query() {
        // Test realistic search: keywords + compact reference
        let opts = ParseOptions::optimistic();
        let text = Text::parse_with_options("bread of life jn6:35", &opts);

        assert_eq!(text.references.len(), 1);
        assert_eq!(text.references[0].reference.to_string(), "John 6:35");
        assert_eq!(text.text_without_references(), "bread of life");
    }

    // ── Versification tests ─────────────────────────────────────────────────

    #[test]
    fn versification_default_is_english() {
        use crate::Versification;
        let opts = ParseOptions::new();
        assert_eq!(opts.versification, Versification::English);
    }

    #[test]
    fn versification_builder_method() {
        use crate::Versification;
        let opts = ParseOptions::new().with_versification(Versification::Masoretic);
        assert_eq!(opts.versification, Versification::Masoretic);
    }

    #[test]
    fn versification_english_joel_chapter_only_expands_correctly() {
        use crate::Versification;
        // English Joel has 3 chapters; chapter 3 has 21 verses
        let opts = ParseOptions::new().with_versification(Versification::English);
        let refs = parse_reference_with_options("Joel 3", &opts);
        assert_eq!(refs.len(), 1);
        assert_eq!(refs[0].to_string(), "Joel 3");
    }

    #[test]
    fn versification_masoretic_joel_chapter_three_has_five_verses() {
        use crate::Versification;
        // Masoretic Joel ch3 = 5 verses (English 2:28-32)
        let opts = ParseOptions::new().with_versification(Versification::Masoretic);
        let refs = parse_reference_with_options("Joel 3", &opts);
        assert_eq!(refs.len(), 1);
        // When expanded, Joel 3 in MT = 5 verses
        if let crate::Reference::Range { end_verse, .. } = &refs[0] {
            assert_eq!(*end_verse, 5);
        } else {
            panic!("Expected a Range reference");
        }
    }

    #[test]
    fn versification_masoretic_joel_has_four_chapters() {
        use crate::Versification;
        // Masoretic Joel has 4 chapters; chapter 4 exists
        let opts = ParseOptions::new().with_versification(Versification::Masoretic);
        let refs = parse_reference_with_options("Joel 4", &opts);
        assert_eq!(refs.len(), 1);
        if let crate::Reference::Range { end_verse, .. } = &refs[0] {
            assert_eq!(*end_verse, 21);
        } else {
            panic!("Expected a Range reference");
        }
    }

    #[test]
    fn versification_english_joel_chapter_four_does_not_exist() {
        use crate::Versification;
        // English Joel has only 3 chapters; "Joel 4" should not be recognized
        let opts = ParseOptions::new().with_versification(Versification::English);
        let refs = parse_reference_with_options("Joel 4", &opts);
        assert_eq!(refs.len(), 0);
    }

    #[test]
    fn versification_masoretic_malachi_has_three_chapters() {
        use crate::Versification;
        // Masoretic Malachi has 3 chapters; chapter 3 has 24 verses
        let opts = ParseOptions::new().with_versification(Versification::Masoretic);
        let refs = parse_reference_with_options("Malachi 3", &opts);
        assert_eq!(refs.len(), 1);
        if let crate::Reference::Range { end_verse, .. } = &refs[0] {
            assert_eq!(*end_verse, 24);
        } else {
            panic!("Expected a Range reference");
        }
    }

    #[test]
    fn versification_english_malachi_chapter_four_expands_correctly() {
        use crate::Versification;
        // English Malachi ch4 = 6 verses
        let opts = ParseOptions::new().with_versification(Versification::English);
        let refs = parse_reference_with_options("Malachi 4", &opts);
        assert_eq!(refs.len(), 1);
        if let crate::Reference::Range { end_verse, .. } = &refs[0] {
            assert_eq!(*end_verse, 6);
        } else {
            panic!("Expected a Range reference");
        }
    }
}
