//! Integration test for parsing biblical references from documents with YAML frontmatter

use biblicist::{ParseOptions, Reference, parse_reference_with_options};
use serde::Deserialize;
use std::collections::HashSet;
use std::fs;
use std::path::Path;

#[derive(Debug, Deserialize)]
struct Frontmatter {
    references: Vec<String>,
}

#[derive(Debug)]
struct TestDocument {
    name: String,
    expected_references: Vec<String>,
    content: String,
}

impl TestDocument {
    fn from_file(path: &Path) -> Result<Self, Box<dyn std::error::Error>> {
        let content = fs::read_to_string(path)?;
        let name = path
            .file_name()
            .and_then(|n| n.to_str())
            .unwrap_or("unknown")
            .to_string();

        // Split frontmatter from content
        let parts: Vec<&str> = content.splitn(3, "---").collect();
        if parts.len() < 3 {
            return Err(format!("No YAML frontmatter found in {}", name).into());
        }

        let frontmatter_str = parts[1];
        let body = parts[2];

        let frontmatter: Frontmatter = serde_yaml::from_str(frontmatter_str)?;

        Ok(TestDocument {
            name,
            expected_references: frontmatter.references,
            content: body.to_string(),
        })
    }
}

#[derive(Debug, PartialEq)]
enum ComparisonResult {
    Match,
    Miss,  // Expected but not found
    Extra, // Found but not expected
}

#[derive(Debug)]
struct TestResult {
    reference: String,
    result: ComparisonResult,
}

fn normalize_reference(reference: &str) -> String {
    // Normalize whitespace, capitalization, and dash types for comparison
    reference
        .trim()
        .replace('–', "-") // en-dash to hyphen
        .replace('—', "-") // em-dash to hyphen
        .chars()
        .map(|c| c.to_lowercase().to_string())
        .collect::<String>()
        .split_whitespace()
        .collect::<Vec<_>>()
        .join(" ")
}

/// Check if a reference is a book-only reference (soft reference)
fn is_book_only_reference(reference: &Reference) -> bool {
    match reference {
        Reference::Range {
            start_chapter,
            start_verse,
            end_chapter,
            end_verse,
            ..
        } => {
            // It's a book-only reference if it spans from chapter 1, verse 1 to the last chapter and verse
            *start_chapter == 1 && *start_verse == 1 && {
                // Check if it's the full book by comparing with the book's actual extent
                use biblicist::get_book;
                if let Reference::Range { book, .. } = reference {
                    if let Some(book_obj) = get_book(book) {
                        let max_ch = book_obj.chapters() as usize;
                        if *end_chapter == max_ch {
                            if let Some(max_v) = book_obj.verses_in_chapter(max_ch as u8) {
                                return *end_verse == max_v as usize;
                            }
                        }
                    }
                }
                false
            }
        }
        _ => false,
    }
}

fn compare_references(expected: &[String], actual: &[Reference]) -> Vec<TestResult> {
    let mut results = Vec::new();

    // Separate soft references (book-only) from specific references
    let (soft_refs, specific_refs): (Vec<_>, Vec<_>) =
        actual.iter().partition(|r| is_book_only_reference(r));

    // Convert expected references to a set for easier comparison
    let expected_set: HashSet<String> = expected.iter().map(|r| normalize_reference(r)).collect();

    // Convert actual specific references to normalized strings
    let specific_normalized: Vec<String> = specific_refs
        .iter()
        .map(|r| normalize_reference(&r.to_string()))
        .collect();

    let specific_set: HashSet<String> = specific_normalized.iter().cloned().collect();

    // Check for matches and misses
    for exp_ref in expected {
        let normalized = normalize_reference(exp_ref);
        if specific_set.contains(&normalized) {
            results.push(TestResult {
                reference: exp_ref.clone(),
                result: ComparisonResult::Match,
            });
        } else {
            results.push(TestResult {
                reference: exp_ref.clone(),
                result: ComparisonResult::Miss,
            });
        }
    }

    // Check for extras (found but not expected)
    // Only include specific references, not soft references
    for act_ref in specific_refs {
        let normalized = normalize_reference(&act_ref.to_string());
        if !expected_set.contains(&normalized) {
            results.push(TestResult {
                reference: act_ref.to_string(),
                result: ComparisonResult::Extra,
            });
        }
    }

    results
}

#[test]
fn test_reference_parsing_from_documents() {
    let fixtures_dir = concat!(env!("CARGO_MANIFEST_DIR"), "/tests/fixtures");
    let fixtures_path = Path::new(fixtures_dir);

    if !fixtures_path.exists() {
        panic!("Fixtures directory not found: {}", fixtures_dir);
    }

    let entries = fs::read_dir(fixtures_path).expect("Failed to read fixtures directory");

    let mut all_passed = true;
    let mut total_tests = 0;

    for entry in entries {
        let entry = entry.expect("Failed to read directory entry");
        let path = entry.path();

        // Only process .md files, but exclude README.md
        if !path.extension().map(|e| e == "md").unwrap_or(false) {
            continue;
        }

        if path.file_name().and_then(|n| n.to_str()) == Some("README.md") {
            continue;
        }

        total_tests += 1;

        println!("\n{}", "=".repeat(60));
        println!("Testing: {}", path.display());
        println!("{}", "=".repeat(60));

        let doc = match TestDocument::from_file(&path) {
            Ok(d) => d,
            Err(e) => {
                eprintln!("Failed to parse document: {}", e);
                all_passed = false;
                continue;
            }
        };

        println!("\nExpected {} references:", doc.expected_references.len());
        for (i, ref_str) in doc.expected_references.iter().enumerate() {
            println!("  {}. {}", i + 1, ref_str);
        }

        // Parse references from the content with contextual resolution enabled
        let options = ParseOptions::new().contextual_resolution(true);
        let parsed_refs = parse_reference_with_options(&doc.content, &options);

        // Separate soft references (book-only mentions)
        let (soft_refs, specific_refs): (Vec<_>, Vec<_>) =
            parsed_refs.iter().partition(|r| is_book_only_reference(r));

        println!("\nFound {} specific references:", specific_refs.len());
        for (i, ref_obj) in specific_refs.iter().enumerate() {
            println!("  {}. {}", i + 1, ref_obj);
        }

        if !soft_refs.is_empty() {
            println!("\nSoft references (book mentions): {}", soft_refs.len());
            for (i, ref_obj) in soft_refs.iter().enumerate() {
                println!("  {}. {}", i + 1, ref_obj);
            }
        }

        // Compare expected vs actual
        let comparison = compare_references(&doc.expected_references, &parsed_refs);

        let matches: Vec<_> = comparison
            .iter()
            .filter(|r| r.result == ComparisonResult::Match)
            .collect();
        let misses: Vec<_> = comparison
            .iter()
            .filter(|r| r.result == ComparisonResult::Miss)
            .collect();
        let extras: Vec<_> = comparison
            .iter()
            .filter(|r| r.result == ComparisonResult::Extra)
            .collect();

        println!("\n--- Results ---");
        println!(
            "Matches: {}/{}",
            matches.len(),
            doc.expected_references.len()
        );

        if !misses.is_empty() {
            println!("\nMissed references (expected but not found):");
            for miss in &misses {
                println!("  - {}", miss.reference);
            }
        }

        if !extras.is_empty() {
            println!("\nExtra references (found but not expected):");
            for extra in &extras {
                println!("  + {}", extra.reference);
            }
        }

        // Calculate accuracy
        let total_expected = doc.expected_references.len();
        let accuracy = if total_expected > 0 {
            (matches.len() as f64 / total_expected as f64) * 100.0
        } else {
            0.0
        };

        println!("\nAccuracy: {:.1}%", accuracy);

        // Mark test as failed if not 100% accuracy (for now, to see what we're dealing with)
        if accuracy < 100.0 {
            all_passed = false;
        }
    }

    if total_tests == 0 {
        panic!("No test documents found in fixtures directory");
    }

    println!("\n{}", "=".repeat(60));
    println!("Summary: Tested {} document(s)", total_tests);
    println!("{}", "=".repeat(60));

    if !all_passed {
        println!("\nNote: This is a baseline test. Some references may be tricky to parse.");
        println!("Review the results above to understand current parsing capabilities.");
        // Don't panic for now - let's see the baseline first
        // panic!("Some tests did not achieve 100% accuracy");
    }
}
