//! Integration tests for the bearing indexing public API.
//!
//! These tests exercise the public API as an external consumer would,
//! verifying that IndexWriter, Document, Field types, and Directory
//! implementations work correctly together.

#[macro_use]
extern crate assertables;

use std::io;
use std::thread;

use bearing::document::{
    Document, FieldValue, binary_doc_values_field, double_field, double_range_field, feature_field,
    float_field, float_range_field, int_field, int_range_field, keyword_field, lat_lon_point,
    long_field, long_range_field, numeric_doc_values_field, sorted_doc_values_field,
    sorted_numeric_doc_values_field, sorted_set_doc_values_field, stored_bytes_field,
    stored_double_field, stored_float_field, stored_int_field, stored_long_field,
    stored_string_field, string_field, text_field,
};
use bearing::index::{IndexWriter, IndexWriterConfig};
use bearing::store::{Directory, FSDirectory, MemoryDirectory};

/// Helper: creates a document with path, modified, and contents fields.
fn make_simple_doc(path: &str, modified: i64, contents: &str) -> Document {
    let mut doc = Document::new();
    doc.add(text_field("body", contents));
    doc.add(keyword_field("id", path));
    doc.add(long_field("ts", modified));
    doc
}

/// Helper: creates a document with all 8 field types.
fn make_all_fields_doc() -> Document {
    let mut doc = Document::new();
    doc.add(text_field("body", "the quick brown fox"));
    doc.add(keyword_field("category", "animals"));
    doc.add(string_field("id", "doc-1", true));
    doc.add(long_field("timestamp", 1_700_000_000));
    doc.add(int_field("count", 42, true));
    doc.add(float_field("score", 3.125, true));
    doc.add(double_field("price", 99.99, true));
    doc.add(stored_string_field("author", "Todd"));
    doc
}

// ---------------------------------------------------------------------------
// Single-threaded indexing
// ---------------------------------------------------------------------------

#[test]
fn single_threaded_index_and_commit() -> io::Result<()> {
    let writer = IndexWriter::new();

    for i in 0..10 {
        let mut doc = Document::new();
        doc.add(text_field("body", &format!("document number {i}")));
        doc.add(keyword_field("id", &format!("doc-{i}")));
        writer.add_document(doc)?;
    }

    let result = writer.commit()?;
    assert_eq!(writer.num_docs(), 10);

    let files = result.into_segment_files()?;
    assert_not_empty!(files, "commit should produce segment files");

    // Should have a segments_N file
    assert_any!(files.iter(), |f: &bearing::store::SegmentFile| f
        .name
        .starts_with("segments_"));
    // Should have compound file entries (.cfs, .cfe)
    assert_any!(files.iter(), |f: &bearing::store::SegmentFile| f
        .name
        .ends_with(".cfs"));
    assert_any!(files.iter(), |f: &bearing::store::SegmentFile| f
        .name
        .ends_with(".cfe"));
    // Should have segment info (.si)
    assert_any!(files.iter(), |f: &bearing::store::SegmentFile| f
        .name
        .ends_with(".si"));

    Ok(())
}

// ---------------------------------------------------------------------------
// Multi-threaded indexing
// ---------------------------------------------------------------------------

#[test]
fn multi_threaded_indexing() -> io::Result<()> {
    let writer = IndexWriter::new();
    let num_threads = 4;
    let docs_per_thread = 25;

    let handles: Vec<_> = (0..num_threads)
        .map(|t| {
            let w = writer.clone();
            thread::spawn(move || {
                for i in 0..docs_per_thread {
                    let mut doc = Document::new();
                    doc.add(text_field("body", &format!("thread {t} doc {i}")));
                    doc.add(keyword_field("id", &format!("t{t}-d{i}")));
                    w.add_document(doc).expect("add_document failed");
                }
            })
        })
        .collect();

    for h in handles {
        h.join().expect("thread panicked");
    }

    let result = writer.commit()?;
    assert_eq!(writer.num_docs(), num_threads * docs_per_thread);

    let files = result.into_segment_files()?;
    assert_any!(files.iter(), |f: &bearing::store::SegmentFile| f
        .name
        .starts_with("segments_"));

    Ok(())
}

// ---------------------------------------------------------------------------
// IndexWriterConfig
// ---------------------------------------------------------------------------

#[test]
fn config_max_buffered_docs() -> io::Result<()> {
    let config = IndexWriterConfig::new().set_max_buffered_docs(5);
    assert_eq!(config.max_buffered_docs(), 5);

    let writer = IndexWriter::with_config(config);

    // Add more than max_buffered_docs to trigger at least one flush
    for i in 0..20 {
        let mut doc = Document::new();
        doc.add(text_field("body", &format!("document {i}")));
        writer.add_document(doc)?;
    }

    let result = writer.commit()?;
    assert_eq!(writer.num_docs(), 20);

    let files = result.into_segment_files()?;
    assert_not_empty!(files);

    Ok(())
}

#[test]
fn config_ram_buffer_size() {
    let config = IndexWriterConfig::new().set_ram_buffer_size_mb(32.0);
    assert_in_delta!(config.ram_buffer_size_mb(), 32.0, f64::EPSILON);
}

#[test]
fn config_defaults() {
    let config = IndexWriterConfig::new();
    assert_in_delta!(config.ram_buffer_size_mb(), 16.0, f64::EPSILON);
    assert_eq!(config.max_buffered_docs(), -1);
}

// ---------------------------------------------------------------------------
// Empty commit
// ---------------------------------------------------------------------------

#[test]
fn empty_commit() -> io::Result<()> {
    let writer = IndexWriter::new();
    let result = writer.commit()?;
    assert_eq!(writer.num_docs(), 0);

    let files = result.into_segment_files()?;
    // Even an empty commit should produce a segments file
    assert_any!(files.iter(), |f: &bearing::store::SegmentFile| f
        .name
        .starts_with("segments_"));

    Ok(())
}

// ---------------------------------------------------------------------------
// All field factory functions
// ---------------------------------------------------------------------------

#[test]
fn all_field_types_commit_successfully() -> io::Result<()> {
    let writer = IndexWriter::new();
    let doc = make_all_fields_doc();
    writer.add_document(doc)?;

    let result = writer.commit()?;
    assert_eq!(writer.num_docs(), 1);

    let files = result.into_segment_files()?;
    assert_not_empty!(files);

    Ok(())
}

#[test]
fn stored_only_fields_commit_successfully() -> io::Result<()> {
    let writer = IndexWriter::new();

    let mut doc = Document::new();
    doc.add(stored_string_field("s", "hello"));
    doc.add(stored_int_field("i", 42));
    doc.add(stored_long_field("l", 123_456_789));
    doc.add(stored_float_field("f", 2.75));
    doc.add(stored_double_field("d", 3.125));
    doc.add(stored_bytes_field("b", vec![0xDE, 0xAD, 0xBE, 0xEF]));
    // Need at least one indexed field for the document to be valid
    doc.add(keyword_field("id", "stored-only"));
    writer.add_document(doc)?;

    let result = writer.commit()?;
    assert_eq!(writer.num_docs(), 1);

    let files = result.into_segment_files()?;
    assert_not_empty!(files);

    Ok(())
}

// ---------------------------------------------------------------------------
// Document & Field accessors
// ---------------------------------------------------------------------------

#[test]
fn field_accessors() {
    let f = text_field("body", "hello world");
    assert_eq!(f.name(), "body");
    assert_eq!(f.string_value(), Some("hello world"));
    assert_none!(f.numeric_value());

    let f = long_field("ts", 12345);
    assert_eq!(f.name(), "ts");
    assert_eq!(f.numeric_value(), Some(12345));

    let f = int_field("count", 7, false);
    assert_eq!(f.name(), "count");
    assert!(matches!(f.value(), FieldValue::Int(7)));

    let f = float_field("score", 1.5, false);
    assert_eq!(f.name(), "score");
    assert!(matches!(f.value(), FieldValue::Float(v) if (*v - 1.5).abs() < f32::EPSILON));

    let f = double_field("price", 99.99, false);
    assert_eq!(f.name(), "price");
    assert!(matches!(f.value(), FieldValue::Double(v) if (*v - 99.99).abs() < f64::EPSILON));

    let f = keyword_field("tag", "rust");
    assert_eq!(f.name(), "tag");
    assert_eq!(f.string_value(), Some("rust"));

    let f = string_field("id", "abc", true);
    assert_eq!(f.name(), "id");
    assert_eq!(f.string_value(), Some("abc"));
}

#[test]
fn document_construction() {
    let mut doc = Document::new();
    assert_is_empty!(doc.fields);

    doc.add(text_field("a", "hello"));
    doc.add(keyword_field("b", "world"));
    assert_len_eq_x!(&doc.fields, 2);
    assert_eq!(doc.fields[0].name(), "a");
    assert_eq!(doc.fields[1].name(), "b");
}

// ---------------------------------------------------------------------------
// In-memory round-trip via MemoryDirectory
// ---------------------------------------------------------------------------

#[test]
fn memory_directory_round_trip() -> io::Result<()> {
    let writer = IndexWriter::new();

    for i in 0..5 {
        let mut doc = Document::new();
        doc.add(text_field("body", &format!("memory test {i}")));
        doc.add(keyword_field("id", &format!("mem-{i}")));
        writer.add_document(doc)?;
    }

    let result = writer.commit()?;
    let mut dir = MemoryDirectory::new();
    let file_names = result.write_to_directory(&mut dir)?;

    assert_not_empty!(file_names);
    assert_any!(file_names.iter(), |n: &String| n.starts_with("segments_"));

    // Verify all files are accessible through the directory
    let listed = dir.list_all()?;
    for name in &file_names {
        assert_contains!(listed, name);
        let len = dir.file_length(name)?;
        assert_gt!(len, 0, "file '{name}' should have non-zero length");
    }

    Ok(())
}

// ---------------------------------------------------------------------------
// Segment file naming conventions
// ---------------------------------------------------------------------------

#[test]
fn segment_file_names_follow_lucene_conventions() -> io::Result<()> {
    let writer = IndexWriter::new();

    let mut doc = Document::new();
    doc.add(text_field("body", "naming test"));
    writer.add_document(doc)?;

    let result = writer.commit()?;
    let files = result.into_segment_files()?;

    for file in &files {
        let name = &file.name;
        // segments_N or _N.ext or _N_xxx.ext
        assert!(
            name.starts_with("segments_") || name.starts_with('_'),
            "unexpected file name pattern: {name}"
        );
    }

    Ok(())
}

// ---------------------------------------------------------------------------
// Filesystem round-trip via FSDirectory
// ---------------------------------------------------------------------------

#[test]
fn fs_directory_round_trip() -> io::Result<()> {
    let tmp_dir = std::env::temp_dir().join("bearing_integration_test_fs");
    // Clean up from any previous run
    if tmp_dir.exists() {
        std::fs::remove_dir_all(&tmp_dir)?;
    }

    let fs_dir = FSDirectory::open(&tmp_dir)?;
    let writer = IndexWriter::with_directory(Box::new(fs_dir));
    writer.add_document(make_simple_doc(
        "fs-1",
        1_000_000,
        "filesystem test document",
    ))?;
    let result = writer.commit()?;

    // Files are already on disk — verify via file_names()
    let file_names = result.file_names();
    assert_not_empty!(file_names);
    assert_any!(file_names.iter(), |n: &String| n.starts_with("segments_"));

    for name in file_names {
        let path = tmp_dir.join(name);
        assert!(
            path.exists(),
            "file should exist on disk: {}",
            path.display()
        );
        let meta = std::fs::metadata(&path)?;
        assert_gt!(meta.len(), 0, "file should be non-empty: {name}");
    }

    // Clean up
    std::fs::remove_dir_all(&tmp_dir)?;

    Ok(())
}

// ---------------------------------------------------------------------------
// Multiple segments via max_buffered_docs
// ---------------------------------------------------------------------------

#[test]
fn multiple_segments_via_flush() -> io::Result<()> {
    let config = IndexWriterConfig::new().set_max_buffered_docs(3);
    let writer = IndexWriter::with_config(config);

    // Add 10 docs with max_buffered_docs=3 -> should produce multiple segments
    for i in 0..10 {
        let mut doc = Document::new();
        doc.add(text_field("body", &format!("segment test doc {i}")));
        doc.add(keyword_field("id", &format!("seg-{i}")));
        writer.add_document(doc)?;
    }

    let result = writer.commit()?;
    assert_eq!(writer.num_docs(), 10);

    let files = result.into_segment_files()?;

    // With max_buffered_docs=3 and 10 docs, we should have multiple .si files
    let si_count = files.iter().filter(|f| f.name.ends_with(".si")).count();
    assert_gt!(
        si_count,
        1,
        "expected multiple segments, got {si_count} .si files"
    );

    Ok(())
}

// ---------------------------------------------------------------------------
// Mixed field types in single document
// ---------------------------------------------------------------------------

#[test]
fn all_field_types_in_single_document() -> io::Result<()> {
    let writer = IndexWriter::new();
    let doc = make_all_fields_doc();

    // Verify the document has the expected number of fields
    assert_len_eq_x!(&doc.fields, 8);

    writer.add_document(doc)?;
    let result = writer.commit()?;

    let mut dir = MemoryDirectory::new();
    let file_names = result.write_to_directory(&mut dir)?;

    // Should produce valid index files
    assert_any!(file_names.iter(), |n: &String| n.starts_with("segments_"));
    assert_any!(file_names.iter(), |n: &String| n.ends_with(".si"));
    assert_any!(file_names.iter(), |n: &String| n.ends_with(".cfs"));

    Ok(())
}

// ---------------------------------------------------------------------------
// Large batch indexing
// ---------------------------------------------------------------------------

#[test]
fn large_batch_indexing() -> io::Result<()> {
    let writer = IndexWriter::new();

    for i in 0..1000 {
        let mut doc = Document::new();
        doc.add(text_field(
            "body",
            &format!("bulk document number {i} with some extra text for variety"),
        ));
        doc.add(keyword_field(
            "category",
            if i % 2 == 0 { "even" } else { "odd" },
        ));
        doc.add(long_field("id", i));
        writer.add_document(doc)?;
    }

    let result = writer.commit()?;
    assert_eq!(writer.num_docs(), 1000);

    let files = result.into_segment_files()?;
    assert_not_empty!(files);

    Ok(())
}

// ---------------------------------------------------------------------------
// Non-compound file mode
// ---------------------------------------------------------------------------

#[test]
fn non_compound_mode() -> io::Result<()> {
    let config = IndexWriterConfig::new().set_use_compound_file(false);
    let writer = IndexWriter::with_config(config);

    for i in 0..5 {
        writer.add_document(make_simple_doc(
            &format!("nc-{i}"),
            i as i64,
            &format!("non-compound test {i}"),
        ))?;
    }

    let result = writer.commit()?;
    assert_eq!(writer.num_docs(), 5);

    let file_names = result.file_names().to_vec();

    // No compound files
    assert!(!file_names.iter().any(|n| n.ends_with(".cfs")));
    assert!(!file_names.iter().any(|n| n.ends_with(".cfe")));

    // Individual sub-files present
    assert_any!(file_names.iter(), |n: &String| n.ends_with(".fnm"));
    assert_any!(file_names.iter(), |n: &String| n.ends_with(".si"));

    // Can still read files via into_segment_files
    let files = result.into_segment_files()?;
    assert_not_empty!(files);
    assert_gt!(files.len(), 4);

    Ok(())
}

// ---------------------------------------------------------------------------
// FSDirectory with non-compound mode
// ---------------------------------------------------------------------------

#[test]
fn fs_directory_non_compound() -> io::Result<()> {
    let tmp_dir = std::env::temp_dir().join("bearing_integration_test_fs_nc");
    if tmp_dir.exists() {
        std::fs::remove_dir_all(&tmp_dir)?;
    }

    let config = IndexWriterConfig::new().set_use_compound_file(false);
    let fs_dir = FSDirectory::open(&tmp_dir)?;
    let writer = IndexWriter::with_config_and_directory(config, Box::new(fs_dir));

    writer.add_document(make_simple_doc("fs-nc-1", 100, "non-compound fs test"))?;
    let result = writer.commit()?;

    let file_names = result.file_names();
    assert!(!file_names.iter().any(|n| n.ends_with(".cfs")));
    assert_any!(file_names.iter(), |n: &String| n.ends_with(".fnm"));

    // Verify files on disk
    for name in file_names {
        let path = tmp_dir.join(name);
        assert!(path.exists(), "file should exist: {}", path.display());
    }

    std::fs::remove_dir_all(&tmp_dir)?;
    Ok(())
}

// ---------------------------------------------------------------------------
// Doc-values-only field types
// ---------------------------------------------------------------------------

#[test]
fn doc_values_only_fields() -> io::Result<()> {
    let config = IndexWriterConfig::new().set_use_compound_file(false);
    let writer = IndexWriter::with_config(config);

    for i in 0..10 {
        let mut doc = Document::new();
        doc.add(text_field("body", &format!("doc values test {i}")));
        doc.add(numeric_doc_values_field("count", i * 10));
        doc.add(binary_doc_values_field(
            "hash",
            vec![(i as u8) * 11, (i as u8) * 22],
        ));
        doc.add(sorted_doc_values_field(
            "category",
            format!("cat-{}", i % 3).as_bytes(),
        ));
        doc.add(sorted_set_doc_values_field(
            "tag",
            &format!("tag-{}", i % 5),
        ));
        doc.add(sorted_numeric_doc_values_field("priority", i % 4));
        writer.add_document(doc)?;
    }

    let result = writer.commit()?;
    assert_eq!(writer.num_docs(), 10);

    let mut dir = MemoryDirectory::new();
    let file_names = result.write_to_directory(&mut dir)?;

    // Doc values files should exist
    assert_any!(file_names.iter(), |n: &String| n.ends_with(".dvm"));
    assert_any!(file_names.iter(), |n: &String| n.ends_with(".dvd"));

    // Verify files have content
    for name in &file_names {
        if name.ends_with(".dvm") || name.ends_with(".dvd") {
            let len = dir.file_length(name)?;
            assert_gt!(len, 0, "{name} should have non-zero length");
        }
    }

    Ok(())
}

#[test]
fn doc_values_only_fields_non_compound() -> io::Result<()> {
    let config = IndexWriterConfig::new().set_use_compound_file(false);
    let writer = IndexWriter::with_config(config);

    for i in 0..5 {
        let mut doc = Document::new();
        doc.add(text_field("body", &format!("non-compound dv test {i}")));
        doc.add(numeric_doc_values_field("count", i * 100));
        doc.add(binary_doc_values_field("payload", vec![i as u8; 4]));
        doc.add(sorted_doc_values_field("status", b"active"));
        doc.add(sorted_set_doc_values_field("region", "us-east"));
        doc.add(sorted_numeric_doc_values_field("score", i * 10));
        writer.add_document(doc)?;
    }

    let result = writer.commit()?;
    let file_names = result.file_names().to_vec();

    // Non-compound: .dvm and .dvd should be individual files
    assert_any!(file_names.iter(), |n: &String| n.ends_with(".dvm"));
    assert_any!(file_names.iter(), |n: &String| n.ends_with(".dvd"));

    Ok(())
}

#[test]
fn multi_valued_sorted_numeric_and_sorted_set() -> io::Result<()> {
    let writer = IndexWriter::new();

    for i in 0..10 {
        let mut doc = Document::new();
        doc.add(text_field("body", &format!("multi-valued doc {i}")));

        // Multi-valued SORTED_NUMERIC: each doc gets 1-3 values
        doc.add(sorted_numeric_doc_values_field("priorities", i * 10));
        if i % 2 == 0 {
            doc.add(sorted_numeric_doc_values_field("priorities", i * 10 + 1));
        }
        if i % 3 == 0 {
            doc.add(sorted_numeric_doc_values_field("priorities", i * 10 + 2));
        }

        // Multi-valued SORTED_SET: each doc gets 1-3 tags
        doc.add(sorted_set_doc_values_field(
            "tags",
            &format!("tag-{}", i % 5),
        ));
        if i % 2 == 0 {
            doc.add(sorted_set_doc_values_field(
                "tags",
                &format!("tag-{}", (i + 1) % 5),
            ));
        }
        if i % 3 == 0 {
            doc.add(sorted_set_doc_values_field(
                "tags",
                &format!("tag-{}", (i + 2) % 5),
            ));
        }

        writer.add_document(doc)?;
    }

    let result = writer.commit()?;
    assert_eq!(writer.num_docs(), 10);

    let files = result.into_segment_files()?;
    assert_not_empty!(files);
    assert_any!(files.iter(), |f: &bearing::store::SegmentFile| f
        .name
        .ends_with(".cfs"));

    Ok(())
}

#[test]
fn lat_lon_point_fields() -> io::Result<()> {
    let writer = IndexWriter::new();

    for i in 0..5 {
        let mut doc = Document::new();
        doc.add(text_field("body", &format!("geo doc {i}")));
        doc.add(lat_lon_point(
            "location",
            40.7128 + i as f64 * 0.01,
            -74.006 + i as f64 * 0.01,
        ));
        writer.add_document(doc)?;
    }

    let result = writer.commit()?;
    assert_eq!(writer.num_docs(), 5);

    let files = result.into_segment_files()?;
    assert_not_empty!(files);

    Ok(())
}

#[test]
fn range_fields() -> io::Result<()> {
    let writer = IndexWriter::new();

    for i in 0..5 {
        let mut doc = Document::new();
        doc.add(text_field("body", &format!("range doc {i}")));
        doc.add(int_range_field("int_range", &[i * 10], &[i * 10 + 5]));
        doc.add(long_range_field(
            "long_range",
            &[i as i64 * 100],
            &[i as i64 * 100 + 50],
        ));
        doc.add(float_range_field(
            "float_range",
            &[i as f32 * 1.0],
            &[i as f32 * 1.0 + 0.5],
        ));
        doc.add(double_range_field(
            "double_range",
            &[i as f64 * 10.0],
            &[i as f64 * 10.0 + 5.0],
        ));
        // Multi-dimensional range
        doc.add(int_range_field(
            "multi_range",
            &[i * 10, i * 20],
            &[i * 10 + 5, i * 20 + 10],
        ));
        writer.add_document(doc)?;
    }

    let result = writer.commit()?;
    assert_eq!(writer.num_docs(), 5);

    let files = result.into_segment_files()?;
    assert_not_empty!(files);

    Ok(())
}

#[test]
fn feature_fields() -> io::Result<()> {
    let writer = IndexWriter::new();

    for i in 0..5 {
        let mut doc = Document::new();
        doc.add(text_field("body", &format!("feature doc {i}")));
        doc.add(feature_field("features", "pagerank", (i + 1) as f32 * 0.5));
        doc.add(feature_field("features", "freshness", (i + 1) as f32 * 2.0));
        writer.add_document(doc)?;
    }

    let result = writer.commit()?;
    assert_eq!(writer.num_docs(), 5);

    let files = result.into_segment_files()?;
    assert_not_empty!(files);

    Ok(())
}

#[test]
fn mixed_doc_values_and_regular_fields() -> io::Result<()> {
    let writer = IndexWriter::new();

    for i in 0..5 {
        let mut doc = Document::new();
        // Regular fields
        doc.add(keyword_field("id", &format!("doc-{i}")));
        doc.add(long_field("timestamp", 1_700_000_000 + i));
        doc.add(text_field("body", &format!("mixed field test {i}")));
        // Doc-values-only fields
        doc.add(numeric_doc_values_field("weight", i * 100));
        doc.add(sorted_doc_values_field("tier", b"gold"));
        doc.add(binary_doc_values_field("blob", vec![0xFF; 8]));
        writer.add_document(doc)?;
    }

    let result = writer.commit()?;
    assert_eq!(writer.num_docs(), 5);

    let files = result.into_segment_files()?;
    assert_not_empty!(files);

    Ok(())
}

// ---------------------------------------------------------------------------
// Sparse fields (field present in some but not all documents)
// ---------------------------------------------------------------------------

#[test]
fn sparse_numeric_doc_values() -> io::Result<()> {
    let config = IndexWriterConfig::new().set_use_compound_file(false);
    let writer = IndexWriter::with_config(config);

    for i in 0..10 {
        let mut doc = Document::new();
        doc.add(keyword_field("id", &format!("doc-{i}")));
        // Only even-numbered docs have the numeric doc values field
        if i % 2 == 0 {
            doc.add(numeric_doc_values_field("score", i * 100));
        }
        writer.add_document(doc)?;
    }

    let result = writer.commit()?;
    assert_eq!(writer.num_docs(), 10);

    let mut dir = MemoryDirectory::new();
    let file_names = result.write_to_directory(&mut dir)?;

    assert_any!(file_names.iter(), |n: &String| n.ends_with(".dvm"));
    assert_any!(file_names.iter(), |n: &String| n.ends_with(".dvd"));

    for name in &file_names {
        if name.ends_with(".dvm") || name.ends_with(".dvd") {
            let len = dir.file_length(name)?;
            assert_gt!(len, 0, "{name} should have non-zero length");
        }
    }

    Ok(())
}

#[test]
fn sparse_norms_text_field() -> io::Result<()> {
    let config = IndexWriterConfig::new().set_use_compound_file(false);
    let writer = IndexWriter::with_config(config);

    for i in 0..10 {
        let mut doc = Document::new();
        doc.add(keyword_field("id", &format!("doc-{i}")));
        // Only some docs have the text field (which generates norms)
        if i % 3 == 0 {
            doc.add(text_field("body", &format!("sparse norms test {i}")));
        }
        writer.add_document(doc)?;
    }

    let result = writer.commit()?;
    assert_eq!(writer.num_docs(), 10);

    let mut dir = MemoryDirectory::new();
    let file_names = result.write_to_directory(&mut dir)?;

    assert_any!(file_names.iter(), |n: &String| n.ends_with(".nvm"));
    assert_any!(file_names.iter(), |n: &String| n.ends_with(".nvd"));

    for name in &file_names {
        if name.ends_with(".nvm") || name.ends_with(".nvd") {
            let len = dir.file_length(name)?;
            assert_gt!(len, 0, "{name} should have non-zero length");
        }
    }

    Ok(())
}

#[test]
fn sparse_mixed_doc_values_types() -> io::Result<()> {
    let config = IndexWriterConfig::new().set_use_compound_file(false);
    let writer = IndexWriter::with_config(config);

    for i in 0..10 {
        let mut doc = Document::new();
        doc.add(keyword_field("id", &format!("doc-{i}")));

        // Each doc values type is sparse (present in different subsets)
        if i < 5 {
            doc.add(numeric_doc_values_field("num", i as i64));
        }
        if i % 2 == 0 {
            doc.add(binary_doc_values_field("bin", vec![i as u8; 4]));
        }
        if i % 3 != 0 {
            doc.add(sorted_doc_values_field(
                "sorted",
                format!("val-{i}").as_bytes(),
            ));
        }
        if i >= 3 {
            doc.add(sorted_numeric_doc_values_field("sn", (i * 10) as i64));
        }

        writer.add_document(doc)?;
    }

    let result = writer.commit()?;
    assert_eq!(writer.num_docs(), 10);

    let mut dir = MemoryDirectory::new();
    let file_names = result.write_to_directory(&mut dir)?;
    assert_not_empty!(file_names);

    // Verify doc values files exist and have content
    for name in &file_names {
        if name.ends_with(".dvm") || name.ends_with(".dvd") {
            let len = dir.file_length(name)?;
            assert_gt!(len, 0, "{name} should have non-zero length");
        }
    }

    Ok(())
}

#[test]
fn sparse_fields_with_fs_directory() -> io::Result<()> {
    let tmp_dir = std::env::temp_dir().join("bearing_integration_test_sparse_fs");
    if tmp_dir.exists() {
        std::fs::remove_dir_all(&tmp_dir)?;
    }

    let config = IndexWriterConfig::new().set_use_compound_file(false);
    let fs_dir = FSDirectory::open(&tmp_dir)?;
    let writer = IndexWriter::with_config_and_directory(config, Box::new(fs_dir));

    for i in 0..10 {
        let mut doc = Document::new();
        doc.add(keyword_field("id", &format!("doc-{i}")));
        // Sparse text field (norms) + sparse numeric doc values
        if i % 2 == 0 {
            doc.add(text_field("body", &format!("fs sparse test {i}")));
        }
        if i % 3 == 0 {
            doc.add(numeric_doc_values_field("count", i as i64));
        }
        writer.add_document(doc)?;
    }

    let result = writer.commit()?;
    assert_eq!(writer.num_docs(), 10);

    let file_names = result.file_names();
    assert_not_empty!(file_names);

    // Verify files exist on disk
    for name in file_names {
        let path = tmp_dir.join(name);
        assert!(path.exists(), "expected file {} on disk", path.display());
    }

    std::fs::remove_dir_all(&tmp_dir)?;
    Ok(())
}

// ---------------------------------------------------------------------------
// Multi-threaded indexing with point fields (regression for BKD empty segment)
// ---------------------------------------------------------------------------

#[test]
fn multi_threaded_few_docs_with_points() -> io::Result<()> {
    // 4 docs with point fields across 4 threads — some threads get empty segments.
    // Regression test: empty/small segments with multi-dim points (LatLonPoint, ranges)
    // previously panicked in the BKD writer.
    let config = IndexWriterConfig::new().set_use_compound_file(false);
    let writer = IndexWriter::with_config(config);

    let handles: Vec<_> = (0..4)
        .map(|i| {
            let w = writer.clone();
            thread::spawn(move || {
                let mut doc = Document::new();
                doc.add(keyword_field("id", &format!("doc-{i}")));
                doc.add(long_field("ts", 1_700_000_000 + i));
                doc.add(int_field("count", i as i32 * 10, true));
                doc.add(float_field("score", i as f32 * 1.5, true));
                doc.add(double_field("price", i as f64 * 99.99, true));
                doc.add(lat_lon_point(
                    "location",
                    40.7128 + i as f64 * 0.01,
                    -74.006 + i as f64 * 0.01,
                ));
                doc.add(int_range_field("int_range", &[i as i32], &[i as i32 + 100]));
                doc.add(long_range_field("long_range", &[i], &[i + 1000]));
                doc.add(float_range_field(
                    "float_range",
                    &[i as f32 / 10.0],
                    &[i as f32 / 10.0 + 1.0],
                ));
                doc.add(double_range_field(
                    "double_range",
                    &[i as f64 * 0.1],
                    &[i as f64 * 0.1 + 1.0],
                ));
                w.add_document(doc).expect("add_document failed");
            })
        })
        .collect();

    for h in handles {
        h.join().expect("thread panicked");
    }

    let result = writer.commit()?;
    assert_eq!(writer.num_docs(), 4);

    let mut dir = MemoryDirectory::new();
    let file_names = result.write_to_directory(&mut dir)?;
    assert_not_empty!(file_names);

    Ok(())
}
