//! Integration tests for the scaling infrastructure wiring.
//!
//! Validates that EmbeddingModel, PartitionRouter, and all 9 agents
//! compose correctly through the builder API.

mod common;

use std::sync::Arc;

use furnace_mcp::agents::aragorn::Aragorn;
use furnace_mcp::agents::elrond::Elrond;
use furnace_mcp::agents::gandalf::Gandalf;
use furnace_mcp::agents::gimli::Gimli;
use furnace_mcp::agents::legolas::Legolas;
use furnace_mcp::agents::morpheus::Morpheus;
use furnace_mcp::agents::trinity::Trinity;
use furnace_mcp::autoforge::workflow::AutoForge;
use furnace_mcp::config::{AgentConfig, SearchConfig};
use furnace_mcp::recipes::manager::RecipeManager;
use furnace_mcp::scaling::auto_optimizer::AutoOptimizer;
use furnace_mcp::scaling::bloom::BloomFilterIndex;
use furnace_mcp::scaling::embedding::{create_embedder, EmbeddingModel, HashEmbedder, EMBEDDING_DIM};
use furnace_mcp::scaling::hot_cache::CachedEntity;
use furnace_mcp::scaling::lazy_loader::LazyIndexer;
use furnace_mcp::scaling::partition::PartitionRouter;
use furnace_mcp::search::freshness::FreshnessManager;
use furnace_mcp::search::orchestrator::SearchOrchestrator;
use furnace_mcp::search::tier2_hybrid::Tier2Search;
use furnace_mcp::search::tier3_agentic::Tier3Search;
use furnace_mcp::types::SearchRequest;

use common::{TestEmbedder, TestFixture};

// =========================================================================
// Group A — EmbeddingModel wiring
// =========================================================================

#[tokio::test]
async fn test_elrond_uses_injected_embedder() {
    let te = Arc::new(TestEmbedder::new("test-marker", 42.0));
    let f = TestFixture::with_embedder(te.clone()).await;
    f.create_source_files();

    let elrond = Elrond::new(
        Arc::clone(&f.kuzu),
        Arc::clone(&f.lancedb),
        f.workspace_root.clone(),
    )
    .with_embedder(te.clone() as Arc<dyn EmbeddingModel>);

    // Index a single file through the injected embedder.
    elrond.index_file("src/main.rs").await.expect("index_file");

    // Verify that the vector stored in LanceDb was produced by our
    // TestEmbedder — its v[0] should be the marker value 42.0.
    let results = f
        .lancedb
        .search(&te.embed("anything"), 1)
        .await
        .expect("lancedb search");

    // We expect at least one result (the file we just indexed).
    assert!(!results.is_empty(), "Expected at least one search result");
}

#[tokio::test]
async fn test_elrond_fallback_without_embedder() {
    let f = TestFixture::new().await;
    f.create_source_files();

    let elrond = Elrond::new(
        Arc::clone(&f.kuzu),
        Arc::clone(&f.lancedb),
        f.workspace_root.clone(),
    );
    // No `.with_embedder()` — should use the fallback `embed_text()`.

    elrond.index_file("src/main.rs").await.expect("index_file fallback");
}

#[tokio::test]
async fn test_orchestrator_embedder_accessor() {
    let te: Arc<dyn EmbeddingModel> = Arc::new(TestEmbedder::new("test-e", 99.0));
    let f = TestFixture::with_embedder(te.clone()).await;

    let orchestrator = SearchOrchestrator::new(
        Some(Arc::clone(&f.lancedb)),
        Some(Arc::clone(&f.kuzu)),
        Arc::clone(&f.sqlite),
        Arc::clone(&f.llm),
        SearchConfig::default(),
        f.workspace_root.clone(),
        te.clone(),
    );

    assert_eq!(orchestrator.embedder().name(), "test-e");
    assert_eq!(orchestrator.embedder().dimension(), EMBEDDING_DIM);
}

#[tokio::test]
async fn test_freshness_manager_uses_embedder() {
    let te: Arc<dyn EmbeddingModel> = Arc::new(TestEmbedder::new("fresh-e", 7.0));
    let f = TestFixture::with_embedder(te.clone()).await;

    let _fm = FreshnessManager::new(Arc::clone(&f.lancedb), te);
    // Construction succeeds — no panic.
}

#[tokio::test]
async fn test_gimli_with_embedder() {
    let te: Arc<dyn EmbeddingModel> = Arc::new(TestEmbedder::new("gimli-e", 1.0));
    let f = TestFixture::new().await;

    let _gimli = Gimli::new(Arc::clone(&f.llm), Arc::clone(&f.sqlite))
        .with_embedder(te);
}

#[tokio::test]
async fn test_gimli_without_embedder() {
    let f = TestFixture::new().await;
    let _gimli = Gimli::new(Arc::clone(&f.llm), Arc::clone(&f.sqlite));
}

#[tokio::test]
async fn test_recipe_manager_with_embedder() {
    let te: Arc<dyn EmbeddingModel> = Arc::new(TestEmbedder::new("recipe-e", 3.0));
    let dir1 = tempfile::tempdir().unwrap();
    let dir2 = tempfile::tempdir().unwrap();

    let rm = RecipeManager::new(dir1.path().to_path_buf(), dir2.path().to_path_buf())
        .await
        .expect("RecipeManager::new");

    let rm = rm.with_embedder(te);
    rm.ensure_recipes_indexed().await;
}

#[tokio::test]
async fn test_all_consumers_share_same_arc() {
    let te: Arc<dyn EmbeddingModel> = Arc::new(TestEmbedder::new("shared", 5.0));
    let f = TestFixture::with_embedder(te.clone()).await;

    let _elrond = Elrond::new(
        Arc::clone(&f.kuzu),
        Arc::clone(&f.lancedb),
        f.workspace_root.clone(),
    )
    .with_embedder(te.clone());

    let _gimli = Gimli::new(Arc::clone(&f.llm), Arc::clone(&f.sqlite))
        .with_embedder(te.clone());

    let orchestrator = SearchOrchestrator::new(
        Some(Arc::clone(&f.lancedb)),
        Some(Arc::clone(&f.kuzu)),
        Arc::clone(&f.sqlite),
        Arc::clone(&f.llm),
        SearchConfig::default(),
        f.workspace_root.clone(),
        te.clone(),
    );

    // Verify the orchestrator holds the same Arc we passed in.
    assert!(Arc::ptr_eq(&te, orchestrator.embedder()));
}

#[tokio::test]
async fn test_create_embedder_default_is_hash() {
    let embedder = create_embedder(None);
    assert_eq!(embedder.name(), "hash-768d");
    assert_eq!(embedder.dimension(), 768);
    assert!(!embedder.requires_external_runtime());
}

// =========================================================================
// Group B — PartitionRouter wiring
// =========================================================================

#[tokio::test]
async fn test_orchestrator_creates_default_router() {
    let f = TestFixture::new().await;

    let orchestrator = SearchOrchestrator::new(
        Some(Arc::clone(&f.lancedb)),
        Some(Arc::clone(&f.kuzu)),
        Arc::clone(&f.sqlite),
        Arc::clone(&f.llm),
        SearchConfig::default(),
        f.workspace_root.clone(),
        Arc::clone(&f.embedder),
    );

    assert_eq!(orchestrator.partition_router().partition_count(), 0);
}

#[tokio::test]
async fn test_orchestrator_router_population() {
    let f = TestFixture::new().await;
    f.create_source_files();

    let orchestrator = SearchOrchestrator::new(
        Some(Arc::clone(&f.lancedb)),
        Some(Arc::clone(&f.kuzu)),
        Arc::clone(&f.sqlite),
        Arc::clone(&f.llm),
        SearchConfig::default(),
        f.workspace_root.clone(),
        Arc::clone(&f.embedder),
    );

    // Populate the router with our test workspace files.
    let elrond = Elrond::new(
        Arc::clone(&f.kuzu),
        Arc::clone(&f.lancedb),
        f.workspace_root.clone(),
    );

    let files = elrond.collect_source_file_strings().expect("collect files");
    assert!(!files.is_empty(), "Expected some source files");

    orchestrator.partition_router().build_from_files(&files);
    assert!(
        orchestrator.partition_router().partition_count() > 0,
        "Expected partitions after build"
    );

    // A query hint for "agents" should route to a partition containing agents files.
    let routed = orchestrator
        .partition_router()
        .route_query(&["agents".to_string()]);
    let has_agents = routed.iter().any(|p| p.contains("agents"));
    assert!(
        has_agents,
        "Expected 'agents' partition in routed results: {:?}",
        routed
    );
}

#[tokio::test]
async fn test_elrond_collect_source_file_strings() {
    let f = TestFixture::new().await;
    f.create_source_files();

    let elrond = Elrond::new(
        Arc::clone(&f.kuzu),
        Arc::clone(&f.lancedb),
        f.workspace_root.clone(),
    );

    let files = elrond.collect_source_file_strings().expect("collect");
    assert!(!files.is_empty());

    // Files should be relative (not start with '/').
    for file in &files {
        assert!(
            !file.starts_with('/'),
            "Expected relative path, got: {}",
            file
        );
    }

    // Verify they can be fed into PartitionRouter.
    let router = PartitionRouter::new(2);
    router.build_from_files(&files);
    assert!(router.partition_count() > 0);
}

#[tokio::test]
async fn test_tier2_with_partition_router() {
    let f = TestFixture::new().await;

    let bloom = Arc::new(BloomFilterIndex::new());
    let router = Arc::new(PartitionRouter::new(2));

    let _tier2 = Tier2Search::new(
        Arc::clone(&f.lancedb),
        f.workspace_root.clone(),
        bloom,
        Arc::clone(&f.embedder),
    )
    .with_partition_router(router);
}

#[tokio::test]
async fn test_tier3_with_partition_router() {
    let f = TestFixture::new().await;

    let router = Arc::new(PartitionRouter::new(2));

    let _tier3 = Tier3Search::new(
        Arc::clone(&f.llm),
        Some(Arc::clone(&f.lancedb)),
        f.workspace_root.clone(),
        Arc::clone(&f.embedder),
    )
    .with_partition_router(router);
}

#[tokio::test]
async fn test_empty_router_graceful() {
    let router = PartitionRouter::new(2);

    // All operations on empty router should return empty, not panic.
    let routed = router.route_query(&["anything".to_string()]);
    assert!(routed.is_empty());

    let files = router.files_in_partition("nonexistent");
    assert!(files.is_empty());

    assert_eq!(router.partition_count(), 0);
}

// =========================================================================
// Group C — End-to-end assembly
// =========================================================================

#[tokio::test]
async fn test_full_assembly_mirrors_lazy_init() {
    let f = TestFixture::new().await;
    f.create_source_files();

    let embedder: Arc<dyn EmbeddingModel> = Arc::new(HashEmbedder);

    // Elrond
    let elrond = Arc::new(
        Elrond::new(
            Arc::clone(&f.kuzu),
            Arc::clone(&f.lancedb),
            f.workspace_root.clone(),
        )
        .with_embedder(embedder.clone()),
    );

    // LazyIndexer
    let _lazy = Arc::new(LazyIndexer::new(
        Arc::clone(&elrond),
        Arc::clone(&f.sqlite),
        f.workspace_root.clone(),
    ));

    // SearchOrchestrator
    let orchestrator = Arc::new(SearchOrchestrator::new(
        Some(Arc::clone(&f.lancedb)),
        Some(Arc::clone(&f.kuzu)),
        Arc::clone(&f.sqlite),
        Arc::clone(&f.llm),
        SearchConfig::default(),
        f.workspace_root.clone(),
        embedder.clone(),
    ));

    // RecipeManager
    let recipe_dir = tempfile::tempdir().unwrap();
    let user_recipe_dir = tempfile::tempdir().unwrap();
    let _rm = Arc::new(
        RecipeManager::new(
            recipe_dir.path().to_path_buf(),
            user_recipe_dir.path().to_path_buf(),
        )
        .await
        .expect("RecipeManager")
        .with_embedder(embedder.clone()),
    );

    // Gandalf
    let gandalf = Arc::new(
        Gandalf::new(Arc::clone(&f.kuzu), Arc::clone(&f.sqlite))
            .with_search(Arc::clone(&orchestrator)),
    );

    // Gimli
    let gimli = Arc::new(
        Gimli::new(Arc::clone(&f.llm), Arc::clone(&f.sqlite))
            .with_embedder(embedder.clone()),
    );

    // Morpheus
    let morpheus = Arc::new(Morpheus::new(Arc::clone(&f.sqlite), Arc::clone(&f.kuzu)));

    // Trinity
    let trinity = Arc::new(Trinity::new(Arc::clone(&f.llm), Arc::clone(&f.sqlite)));

    // Legolas
    let legolas = Arc::new(Legolas::new(Arc::clone(&f.kuzu), Arc::clone(&f.sqlite)));

    // Aragorn
    let config = AgentConfig::default();
    let _aragorn = Aragorn::new(Arc::clone(&f.sqlite), &config, f.workspace_root.clone())
        .with_agents(Arc::clone(&gandalf), Arc::clone(&gimli))
        .with_elrond(Arc::clone(&elrond));

    // AutoForge
    let _autoforge = AutoForge::new(Arc::clone(&f.sqlite), f.workspace_root.clone())
        .with_trinity(trinity)
        .with_legolas(legolas)
        .with_morpheus(morpheus)
        .with_llm(Arc::clone(&f.llm));

    // If we got here, all 9 agents + orchestrator constructed without error.
}

#[tokio::test]
async fn test_scan_populates_bloom_and_partitions() {
    let f = TestFixture::new().await;
    f.create_source_files();

    let elrond = Elrond::new(
        Arc::clone(&f.kuzu),
        Arc::clone(&f.lancedb),
        f.workspace_root.clone(),
    )
    .with_embedder(Arc::clone(&f.embedder));

    let stats = elrond.scan_workspace().await.expect("scan_workspace");
    assert!(stats.files_indexed > 0, "Expected indexed files, got {:?}", stats);

    // Populate bloom from kuzu tokens.
    let bloom = BloomFilterIndex::new();
    let files = elrond.collect_source_file_strings().expect("collect files");
    for file in &files {
        let tokens = BloomFilterIndex::tokenize_query(file);
        bloom.index_file(file, &tokens);
    }
    assert!(bloom.file_count() > 0, "Expected bloom entries");

    // Populate partition router.
    let router = PartitionRouter::new(2);
    router.build_from_files(&files);
    assert!(router.partition_count() > 0, "Expected partitions");
}

#[tokio::test]
async fn test_lazy_indexer_wiring() {
    let f = TestFixture::new().await;

    let elrond = Arc::new(Elrond::new(
        Arc::clone(&f.kuzu),
        Arc::clone(&f.lancedb),
        f.workspace_root.clone(),
    ));

    let lazy = LazyIndexer::new(
        elrond,
        Arc::clone(&f.sqlite),
        f.workspace_root.clone(),
    );

    // File not tracked → queue_if_needed should return false.
    let queued = lazy.queue_if_needed("x.rs").await;
    assert!(!queued, "Untracked file should not be queued");

    // Empty queue → process_batch returns 0.
    let processed = lazy.process_batch(10).await;
    assert_eq!(processed, 0, "Empty batch should process 0");
}

#[tokio::test]
async fn test_auto_optimizer_wiring() {
    let f = TestFixture::new().await;

    let optimizer = AutoOptimizer::new(Arc::clone(&f.sqlite));

    let strategy = optimizer.strategy_for_file("src/main.rs").await;
    // Should return a valid strategy (even for unknown file).
    assert_eq!(strategy.file_path, "src/main.rs");

    let optimized = optimizer.re_optimize_all().await;
    // No index metadata in a fresh DB → empty.
    assert!(optimized.is_empty(), "Expected empty re_optimize on fresh DB");
}

#[tokio::test]
async fn test_hot_cache_wiring() {
    let f = TestFixture::new().await;

    let orchestrator = SearchOrchestrator::new(
        Some(Arc::clone(&f.lancedb)),
        Some(Arc::clone(&f.kuzu)),
        Arc::clone(&f.sqlite),
        Arc::clone(&f.llm),
        SearchConfig::default(),
        f.workspace_root.clone(),
        Arc::clone(&f.embedder),
    );

    let cache = orchestrator.hot_cache();

    // Put a CachedEntity.
    cache.put(
        "test-key",
        CachedEntity::Raw("hello".to_string()),
    );

    // Get it back.
    let got = cache.get("test-key");
    assert!(got.is_some(), "Expected cache hit");

    // Stats should show a hit.
    let stats = cache.stats();
    assert!(stats.hits >= 1, "Expected hits >= 1, got {}", stats.hits);
}

// =========================================================================
// Group D — Cross-agent scaling
// =========================================================================

#[tokio::test]
async fn test_elrond_scan_feeds_search() {
    let f = TestFixture::new().await;
    f.create_source_files();

    let elrond = Elrond::new(
        Arc::clone(&f.kuzu),
        Arc::clone(&f.lancedb),
        f.workspace_root.clone(),
    )
    .with_embedder(Arc::clone(&f.embedder));

    elrond.scan_workspace().await.expect("scan");

    // Populate bloom + partitions.
    let files = elrond.collect_source_file_strings().expect("collect");
    let orchestrator = SearchOrchestrator::new(
        Some(Arc::clone(&f.lancedb)),
        Some(Arc::clone(&f.kuzu)),
        Arc::clone(&f.sqlite),
        Arc::clone(&f.llm),
        SearchConfig::default(),
        f.workspace_root.clone(),
        Arc::clone(&f.embedder),
    );
    orchestrator.bloom_index().index_file("src/main.rs", &BloomFilterIndex::tokenize_query("main"));
    orchestrator.partition_router().build_from_files(&files);

    // Search should succeed (may return empty because MockLlm).
    let request = SearchRequest {
        query: "main function".to_string(),
        ..Default::default()
    };
    let response = orchestrator.search(&request).await;
    assert!(response.is_ok(), "Search should succeed: {:?}", response.err());
}

#[tokio::test]
async fn test_gandalf_with_orchestrator_sharing_embedder() {
    let te: Arc<dyn EmbeddingModel> = Arc::new(TestEmbedder::new("shared-gand", 11.0));
    let f = TestFixture::with_embedder(te.clone()).await;

    let orchestrator = Arc::new(SearchOrchestrator::new(
        Some(Arc::clone(&f.lancedb)),
        Some(Arc::clone(&f.kuzu)),
        Arc::clone(&f.sqlite),
        Arc::clone(&f.llm),
        SearchConfig::default(),
        f.workspace_root.clone(),
        te,
    ));

    let _gandalf = Gandalf::new(Arc::clone(&f.kuzu), Arc::clone(&f.sqlite))
        .with_search(orchestrator);
}

#[tokio::test]
async fn test_aragorn_with_scaled_agents() {
    let te: Arc<dyn EmbeddingModel> = Arc::new(TestEmbedder::new("shared-aragorn", 22.0));
    let f = TestFixture::with_embedder(te.clone()).await;

    let elrond = Arc::new(
        Elrond::new(
            Arc::clone(&f.kuzu),
            Arc::clone(&f.lancedb),
            f.workspace_root.clone(),
        )
        .with_embedder(te.clone()),
    );

    let gandalf = Arc::new(Gandalf::new(Arc::clone(&f.kuzu), Arc::clone(&f.sqlite)));

    let gimli = Arc::new(
        Gimli::new(Arc::clone(&f.llm), Arc::clone(&f.sqlite))
            .with_embedder(te),
    );

    let config = AgentConfig::default();
    let _aragorn = Aragorn::new(Arc::clone(&f.sqlite), &config, f.workspace_root.clone())
        .with_agents(gandalf, gimli)
        .with_elrond(elrond);
}

#[tokio::test]
async fn test_autoforge_with_agents() {
    let f = TestFixture::new().await;

    let trinity = Arc::new(Trinity::new(Arc::clone(&f.llm), Arc::clone(&f.sqlite)));
    let legolas = Arc::new(Legolas::new(Arc::clone(&f.kuzu), Arc::clone(&f.sqlite)));
    let morpheus = Arc::new(Morpheus::new(Arc::clone(&f.sqlite), Arc::clone(&f.kuzu)));

    let _autoforge = AutoForge::new(Arc::clone(&f.sqlite), f.workspace_root.clone())
        .with_trinity(trinity)
        .with_legolas(legolas)
        .with_morpheus(morpheus)
        .with_llm(Arc::clone(&f.llm));
}

// =========================================================================
// Group E — Fallback & degradation
// =========================================================================

#[tokio::test]
async fn test_orchestrator_without_vector_db() {
    let f = TestFixture::new().await;

    let orchestrator = SearchOrchestrator::new(
        None, // no vector DB
        Some(Arc::clone(&f.kuzu)),
        Arc::clone(&f.sqlite),
        Arc::clone(&f.llm),
        SearchConfig::default(),
        f.workspace_root.clone(),
        Arc::clone(&f.embedder),
    );

    assert!(
        orchestrator.freshness_manager().is_none(),
        "Freshness manager should be None without vector DB"
    );
}

#[tokio::test]
async fn test_orchestrator_without_kuzu() {
    let f = TestFixture::new().await;

    let _orchestrator = SearchOrchestrator::new(
        Some(Arc::clone(&f.lancedb)),
        None, // no kuzu
        Arc::clone(&f.sqlite),
        Arc::clone(&f.llm),
        SearchConfig::default(),
        f.workspace_root.clone(),
        Arc::clone(&f.embedder),
    );
}

#[tokio::test]
async fn test_create_embedder_with_nonexistent_path() {
    let embedder = create_embedder(Some("/nonexistent/model/path"));

    // Without onnx feature: returns hash embedder.
    // With onnx feature: OnnxEmbedder that falls back to hash.
    // Either way, name should contain "hash" and produce valid vectors.
    assert!(
        embedder.name().contains("hash"),
        "Expected hash-based name, got: {}",
        embedder.name()
    );

    let vec = embedder.embed("test");
    assert_eq!(vec.len(), EMBEDDING_DIM);

    // Verify it's a valid embedding (non-zero).
    let sum: f32 = vec.iter().map(|x| x.abs()).sum();
    assert!(sum > 0.0, "Embedding should be non-zero");
}
