//! Shared test helpers for integration tests.

use std::path::{Path, PathBuf};
use std::sync::Arc;

use furnace_mcp::db::kuzu::KuzuGraph;
use furnace_mcp::db::lancedb::LanceDb;
use furnace_mcp::db::sqlite::SqliteDb;
use furnace_mcp::scaling::embedding::{EmbeddingModel, HashEmbedder, EMBEDDING_DIM};
use furnace_mcp::types::LlmProvider;

// ---------------------------------------------------------------------------
// MockLlm
// ---------------------------------------------------------------------------

pub struct MockLlm {
    pub response: String,
}

impl MockLlm {
    pub fn new(response: &str) -> Self {
        Self {
            response: response.to_string(),
        }
    }
}

#[async_trait::async_trait]
impl LlmProvider for MockLlm {
    fn name(&self) -> &str {
        "mock"
    }

    fn is_available(&self) -> bool {
        true
    }

    async fn complete(&self, _prompt: &str) -> anyhow::Result<String> {
        Ok(self.response.clone())
    }

    async fn complete_with_system(&self, _system: &str, _prompt: &str) -> anyhow::Result<String> {
        Ok(self.response.clone())
    }
}

// ---------------------------------------------------------------------------
// TestEmbedder
// ---------------------------------------------------------------------------

/// An embedder that delegates to `HashEmbedder` but overwrites `v[0]` with a
/// configurable marker value.  This lets tests detect *which* embedder
/// produced a given vector by checking `v[0]`.
pub struct TestEmbedder {
    pub name: String,
    pub marker: f32,
    inner: HashEmbedder,
}

impl TestEmbedder {
    pub fn new(name: &str, marker: f32) -> Self {
        Self {
            name: name.to_string(),
            marker,
            inner: HashEmbedder,
        }
    }
}

impl EmbeddingModel for TestEmbedder {
    fn name(&self) -> &str {
        &self.name
    }

    fn embed(&self, text: &str) -> Vec<f32> {
        let mut v = self.inner.embed(text);
        if !v.is_empty() {
            v[0] = self.marker;
        }
        v
    }

    fn dimension(&self) -> usize {
        EMBEDDING_DIM
    }
}

// ---------------------------------------------------------------------------
// TestFixture
// ---------------------------------------------------------------------------

pub struct TestFixture {
    pub sqlite: Arc<SqliteDb>,
    pub kuzu: Arc<KuzuGraph>,
    pub lancedb: Arc<LanceDb>,
    pub llm: Arc<dyn LlmProvider>,
    pub embedder: Arc<dyn EmbeddingModel>,
    pub workspace_root: PathBuf,
    // Prevent cleanup until fixture is dropped.
    _sqlite_dir: tempfile::TempDir,
    _kuzu_dir: tempfile::TempDir,
    _lance_dir: tempfile::TempDir,
    _workspace_dir: tempfile::TempDir,
}

impl TestFixture {
    /// Create a fixture with the default `HashEmbedder`.
    pub async fn new() -> Self {
        Self::with_embedder(Arc::new(HashEmbedder)).await
    }

    /// Create a fixture with a custom embedder.
    pub async fn with_embedder(embedder: Arc<dyn EmbeddingModel>) -> Self {
        let sqlite_dir = tempfile::tempdir().expect("sqlite tempdir");
        let kuzu_dir = tempfile::tempdir().expect("kuzu tempdir");
        let lance_dir = tempfile::tempdir().expect("lance tempdir");
        let workspace_dir = tempfile::tempdir().expect("workspace tempdir");

        let sqlite = Arc::new(
            SqliteDb::new(&sqlite_dir.path().join("test.db")).expect("SqliteDb::new"),
        );
        let kuzu = Arc::new(
            KuzuGraph::new(&kuzu_dir.path().join("test_kuzu")).expect("KuzuGraph::new"),
        );
        let lancedb = Arc::new(
            LanceDb::new(&lance_dir.path().join("test_lance"))
                .await
                .expect("LanceDb::new"),
        );

        let llm: Arc<dyn LlmProvider> = Arc::new(MockLlm::new("mock response"));

        Self {
            sqlite,
            kuzu,
            lancedb,
            llm,
            embedder,
            workspace_root: workspace_dir.path().to_path_buf(),
            _sqlite_dir: sqlite_dir,
            _kuzu_dir: kuzu_dir,
            _lance_dir: lance_dir,
            _workspace_dir: workspace_dir,
        }
    }

    /// Write a minimal set of source files into the workspace tempdir so that
    /// Elrond's `scan_workspace` / `collect_source_file_strings` find them.
    pub fn create_source_files(&self) {
        let src = self.workspace_root.join("src");
        let agents = src.join("agents");
        let tests = self.workspace_root.join("tests");

        std::fs::create_dir_all(&agents).expect("create src/agents");
        std::fs::create_dir_all(&tests).expect("create tests/");

        write_file(&src, "main.rs", "pub fn main() { println!(\"hello\"); }");
        write_file(&src, "lib.rs", "pub fn helper() -> String { String::new() }");
        write_file(
            &agents,
            "mod.rs",
            "pub mod gandalf;\npub fn agent_entry() {}",
        );
        write_file(
            &tests,
            "test.rs",
            "#[test]\nfn it_works() { assert!(true); }",
        );
    }
}

fn write_file(dir: &Path, name: &str, content: &str) {
    std::fs::write(dir.join(name), content).expect("write_file");
}
