//! Shared test fixtures for remote feature tests.
//!
//! All fixtures follow the project convention:
//!
//! ```rust,ignore
//! let f = SomeFixture::new().await;
//! // use f.field, f.method(), ...
//! ```
//!
//! Mesh-dependent fixtures share a single `OnceCell<MeshHandle>` — kameo
//! allows only one global `ActorSwarm` per process, but one mesh is sufficient
//! because actors registered under distinct DHT names simulate separate nodes.

#[cfg(all(test, feature = "remote"))]
pub(crate) mod fixtures {
    use crate::agent::agent_config::AgentConfig;
    use crate::agent::agent_config_builder::AgentConfigBuilder;
    use crate::agent::remote::NodeId;
    use crate::agent::remote::mesh::MeshHandle;
    use crate::agent::remote::node_manager::RemoteNodeManager;
    use crate::agent::session_registry::SessionRegistry;
    use crate::session::backend::StorageBackend as _;
    use crate::session::sqlite_storage::SqliteStorage;
    use kameo::actor::{ActorRef, Spawn};
    use querymt::LLMParams;
    use querymt::LLMProvider;
    use querymt::chat::{ChatMessage, ChatResponse, FinishReason, StreamChunk, Tool};
    use querymt::completion::{CompletionProvider, CompletionRequest, CompletionResponse};
    use querymt::embedding::EmbeddingProvider;
    use querymt::error::LLMError;
    use querymt::plugin::host::PluginRegistry;
    use querymt_remote::ProviderHostActor;
    use std::path::PathBuf;
    use std::pin::Pin;
    use std::sync::{Arc, OnceLock};
    use tempfile::TempDir;
    use tokio::sync::{Mutex, OnceCell};

    // ── Persistent runtime that owns the swarm event loop ────────────────────
    //
    // `#[tokio::test]` creates a *per-test* runtime.  `bootstrap_mesh` spawns
    // the libp2p swarm event-loop task with `tokio::spawn`, binding it to
    // whichever runtime calls it first.  When that test finishes its runtime
    // is shut down, killing the swarm task.  Every subsequent test that touches
    // `ActorSwarm::get()` then finds a dead `mpsc::Sender` and panics:
    //
    //   "the swarm should never stop running: SendError { .. }"
    //
    // Fix: bootstrap the mesh on a dedicated `tokio::Runtime` that lives for
    // the entire process lifetime.  All swarm I/O (the event-loop task and the
    // libp2p TCP listener) runs on this runtime; the `MeshHandle` returned is
    // just a cheaply-cloneable capability object whose channel sender is
    // `Send + 'static` and works from any runtime context.

    static MESH_RUNTIME: OnceLock<tokio::runtime::Runtime> = OnceLock::new();

    fn mesh_runtime() -> &'static tokio::runtime::Runtime {
        MESH_RUNTIME.get_or_init(|| {
            tokio::runtime::Builder::new_multi_thread()
                .enable_all()
                .thread_name("test-mesh-rt")
                .build()
                .expect("create persistent mesh runtime")
        })
    }

    // ── Single shared mesh ────────────────────────────────────────────────────

    static TEST_MESH: OnceCell<MeshHandle> = OnceCell::const_new();
    static TEST_CONFIG_DIR: OnceLock<PathBuf> = OnceLock::new();

    fn ensure_remote_test_config_dir() -> &'static PathBuf {
        TEST_CONFIG_DIR.get_or_init(|| {
            let config_dir = tempfile::tempdir()
                .expect("failed to create temp dir for remote test config")
                .keep();
            // Route remote test state away from ~/.qmt.
            unsafe {
                std::env::set_var("QMT_CONFIG_DIR", &config_dir);
            }
            config_dir
        })
    }

    /// Return the process-wide test mesh, bootstrapping it on first call.
    ///
    /// All tests share the same `MeshHandle`. Use unique DHT names per test
    /// (e.g. via `uuid::Uuid::now_v7()`) to prevent cross-test interference.
    ///
    /// The mesh swarm event loop is pinned to a dedicated long-lived runtime
    /// (`MESH_RUNTIME`) so it survives the teardown of individual per-test
    /// runtimes created by `#[tokio::test]`.
    pub fn random_node_id() -> String {
        NodeId::from_peer_id(
            libp2p::identity::Keypair::generate_ed25519()
                .public()
                .to_peer_id(),
        )
        .to_string()
    }

    pub async fn get_test_mesh() -> &'static MeshHandle {
        TEST_MESH
            .get_or_init(|| async {
                // Run bootstrap_mesh on the persistent runtime so the swarm
                // event-loop task is owned by a runtime that never shuts down.
                mesh_runtime()
                    .spawn(async {
                        // Ensure remote tests write config-backed state to an
                        // isolated location instead of ~/.qmt.
                        let _ = ensure_remote_test_config_dir();

                        // Use a temp directory for the identity file so tests
                        // don't pollute ~/.qmt/mesh_identity.key and each test
                        // suite run gets a fresh PeerId (no stale Kademlia state).
                        let identity_dir = tempfile::tempdir()
                            .expect("failed to create temp dir for test identity");
                        let identity_path = identity_dir.path().join("test_identity.key");

                        let cfg = querymt_remote::MeshRuntimeConfig {
                            enabled: true,
                            lan: Some(querymt_remote::LanMeshConfig {
                                listen: Some("/ip4/127.0.0.1/tcp/0".to_string()),
                                discovery: querymt_remote::LanDiscovery::None,
                                directory:
                                    querymt_remote::mesh_runtime_config::DirectoryMode::default(),
                            }),
                            iroh_enabled: false,
                            iroh_scopes: Vec::new(),
                            identity_file: Some(identity_path),
                            request_timeout: std::time::Duration::from_secs(300),
                            stream_reconnect_grace: std::time::Duration::from_secs(120),
                            node_name: None,
                            peers: vec![],
                            auto_fallback: false,
                        };
                        // Leak the tempdir so it lives for the entire process
                        // (the test mesh is a process-global singleton).
                        std::mem::forget(identity_dir);
                        querymt_remote::bootstrap_mesh_handle(&cfg)
                            .await
                            .expect("test mesh bootstrap failed")
                    })
                    .await
                    .expect("mesh bootstrap task panicked")
            })
            .await
    }

    // ── Minimal AgentConfig ───────────────────────────────────────────────────

    /// Fixture for a minimal `AgentConfig` backed by an in-memory SQLite store.
    ///
    /// Suitable for any test that does not need real LLM calls.
    ///
    /// ```rust,ignore
    /// let f = AgentConfigFixture::new().await;
    /// // f.config   — Arc<AgentConfig>
    /// // f._tempdir — kept alive for the duration of the test
    /// ```
    pub struct AgentConfigFixture {
        pub config: Arc<AgentConfig>,
        /// Keep the `TempDir` alive for the duration of the test.
        pub _tempdir: TempDir,
    }

    impl AgentConfigFixture {
        pub async fn new() -> Self {
            let temp_dir = TempDir::new().expect("create temp dir");
            let config_path = temp_dir.path().join("providers.toml");
            std::fs::write(&config_path, "providers = []\n").expect("write providers.toml");
            let registry = PluginRegistry::from_path(&config_path).expect("create plugin registry");
            let plugin_registry = Arc::new(registry);

            let storage = Arc::new(
                SqliteStorage::connect(":memory:".into())
                    .await
                    .expect("create sqlite storage"),
            );
            let llm = LLMParams::new().provider("mock").model("mock");

            let builder = AgentConfigBuilder::new(
                plugin_registry,
                storage.session_store(),
                storage.event_journal(),
                llm,
            );
            let config = Arc::new(builder.build());

            Self {
                config,
                _tempdir: temp_dir,
            }
        }
    }

    // ── MockLLMProvider ───────────────────────────────────────────────────────

    /// A minimal `LLMProvider` that returns a fixed text response.
    ///
    /// Use `MockLLMProvider::new("hello")` to get a provider that always
    /// responds with `"hello"`. Optionally track call counts via the shared
    /// `call_count` mutex.
    pub struct MockLLMProvider {
        pub response_text: String,
        /// Incremented on each `chat_with_tools` call.
        pub call_count: Arc<Mutex<usize>>,
    }

    impl MockLLMProvider {
        pub fn new(response: impl Into<String>) -> Self {
            Self {
                response_text: response.into(),
                call_count: Arc::new(Mutex::new(0)),
            }
        }

        pub fn call_counter(&self) -> Arc<Mutex<usize>> {
            Arc::clone(&self.call_count)
        }
    }

    /// Concrete `ChatResponse` returned by `MockLLMProvider`.
    #[derive(Debug)]
    pub struct MockChatResp {
        pub text: String,
        pub finish: FinishReason,
    }

    impl std::fmt::Display for MockChatResp {
        fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
            write!(f, "{}", self.text)
        }
    }

    impl ChatResponse for MockChatResp {
        fn text(&self) -> Option<String> {
            Some(self.text.clone())
        }
        fn thinking(&self) -> Option<String> {
            None
        }
        fn tool_calls(&self) -> Option<Vec<querymt::ToolCall>> {
            None
        }
        fn finish_reason(&self) -> Option<FinishReason> {
            Some(self.finish)
        }
        fn usage(&self) -> Option<querymt::Usage> {
            None
        }
    }

    #[async_trait::async_trait]
    impl querymt::chat::ChatProvider for MockLLMProvider {
        async fn chat_with_tools(
            &self,
            _messages: &[ChatMessage],
            _tools: Option<&[Tool]>,
        ) -> Result<Box<dyn ChatResponse>, LLMError> {
            let mut count = self.call_count.lock().await;
            *count += 1;
            Ok(Box::new(MockChatResp {
                text: self.response_text.clone(),
                finish: FinishReason::Stop,
            }))
        }

        async fn chat_stream_with_tools(
            &self,
            _messages: &[ChatMessage],
            _tools: Option<&[Tool]>,
        ) -> Result<
            Pin<Box<dyn futures_util::Stream<Item = Result<StreamChunk, LLMError>> + Send>>,
            LLMError,
        > {
            use futures_util::stream;
            let text = self.response_text.clone();
            let chunks = vec![
                Ok(StreamChunk::Text(text)),
                Ok(StreamChunk::Done {
                    finish_reason: querymt::chat::FinishReason::Stop,
                }),
            ];
            Ok(Box::pin(stream::iter(chunks)))
        }
    }

    #[async_trait::async_trait]
    impl CompletionProvider for MockLLMProvider {
        async fn complete(&self, _req: &CompletionRequest) -> Result<CompletionResponse, LLMError> {
            Err(LLMError::NotImplemented("mock".into()))
        }
    }

    #[async_trait::async_trait]
    impl EmbeddingProvider for MockLLMProvider {
        async fn embed(&self, _input: Vec<String>) -> Result<Vec<Vec<f32>>, LLMError> {
            Err(LLMError::NotImplemented("mock".into()))
        }
    }

    impl LLMProvider for MockLLMProvider {}

    // ── ProviderHostFixture ───────────────────────────────────────────────────

    /// Fixture for `ProviderHostActor` tests that do **not** require a mesh.
    ///
    /// ```rust,ignore
    /// let f = ProviderHostFixture::new().await;
    /// // f.actor_ref  — ActorRef<ProviderHostActor>
    /// // f.config     — Arc<AgentConfig>
    /// ```
    pub struct ProviderHostFixture {
        pub actor_ref: ActorRef<ProviderHostActor>,
        pub _config: Arc<AgentConfig>,
        pub _tempdir: TempDir,
    }

    impl ProviderHostFixture {
        pub async fn new() -> Self {
            let f = AgentConfigFixture::new().await;
            let actor = crate::agent::remote::provider_host_backend::provider_host_from_config(
                f.config.clone(),
            );
            let actor_ref = ProviderHostActor::spawn(actor);
            Self {
                actor_ref,
                _config: f.config,
                _tempdir: f._tempdir,
            }
        }
    }

    // ── NodeManagerFixture ────────────────────────────────────────────────────

    /// Fixture for `RemoteNodeManager` tests that do not require mesh registration.
    ///
    /// ```rust,ignore
    /// let f = NodeManagerFixture::new().await;
    /// // f.actor_ref  — ActorRef<RemoteNodeManager>
    /// // f.config     — Arc<AgentConfig>
    /// ```
    pub struct NodeManagerFixture {
        pub actor_ref: ActorRef<RemoteNodeManager>,
        pub config: Arc<AgentConfig>,
        pub _tempdir: TempDir,
    }

    impl NodeManagerFixture {
        /// Create a node manager fixture without mesh bootstrap.
        pub async fn new() -> Self {
            let f = AgentConfigFixture::new().await;
            let registry = Arc::new(Mutex::new(SessionRegistry::new(f.config.clone())));
            let nm = RemoteNodeManager::new(
                f.config.clone(),
                registry,
                None,
                Arc::new(parking_lot::Mutex::new(None)),
            );
            let actor_ref = RemoteNodeManager::spawn(nm);
            Self {
                actor_ref,
                config: f.config,
                _tempdir: f._tempdir,
            }
        }

        /// Create a node manager fixture with mesh enabled.
        pub async fn new_with_mesh() -> Self {
            let mesh = get_test_mesh().await;
            let f = AgentConfigFixture::new().await;
            let registry = Arc::new(Mutex::new(SessionRegistry::new(f.config.clone())));
            registry.lock().await.set_mesh(Some(mesh.clone()));
            let nm = RemoteNodeManager::new(
                f.config.clone(),
                registry,
                Some(mesh.clone()),
                Arc::new(parking_lot::Mutex::new(None)),
            );
            let actor_ref = RemoteNodeManager::spawn(nm);
            Self {
                actor_ref,
                config: f.config,
                _tempdir: f._tempdir,
            }
        }
    }

    // ── MeshNodeManagerFixture ────────────────────────────────────────────────

    /// Fixture for a `RemoteNodeManager` registered in the shared test mesh.
    ///
    /// Use `test_id` (e.g. `uuid::Uuid::now_v7().to_string()`) to produce a
    /// unique DHT name and prevent cross-test interference.
    ///
    /// ```rust,ignore
    /// let f = MeshNodeManagerFixture::new("alpha", &test_id).await;
    /// // f.actor_ref  — ActorRef<RemoteNodeManager>
    /// // f.dht_name   — "scope::lan::default::node_manager::alpha-{test_id}"
    /// // f.mesh       — &'static MeshHandle
    /// ```
    pub struct MeshNodeManagerFixture {
        pub actor_ref: ActorRef<RemoteNodeManager>,
        pub config: Arc<AgentConfig>,
        pub dht_name: String,
        pub _mesh: &'static MeshHandle,
        pub _tempdir: TempDir,
    }

    impl MeshNodeManagerFixture {
        pub async fn new(label: &str, test_id: &str) -> Self {
            let mesh = get_test_mesh().await;
            let f = AgentConfigFixture::new().await;
            let registry = Arc::new(Mutex::new(SessionRegistry::new(f.config.clone())));
            registry.lock().await.set_mesh(Some(mesh.clone()));
            let nm = RemoteNodeManager::new(
                f.config.clone(),
                registry,
                Some(mesh.clone()),
                Arc::new(parking_lot::Mutex::new(None)),
            );
            let actor_ref = RemoteNodeManager::spawn(nm);

            let dht_name = format!("scope::lan::default::node_manager::{}-{}", label, test_id);
            let registration_name = dht_name.clone();
            let mesh_for_registration = mesh.clone();
            let actor_ref_for_registration = actor_ref.clone();
            mesh_runtime()
                .spawn(async move {
                    mesh_for_registration
                        .register_actor(
                            actor_ref_for_registration.clone(),
                            registration_name.clone(),
                        )
                        .await;

                    let looked_up = mesh_for_registration
                        .lookup_actor::<RemoteNodeManager>(registration_name.clone())
                        .await
                        .expect("node manager self-lookup failed after registration")
                        .expect("node manager self-lookup returned none after registration");
                    assert_eq!(
                        looked_up.id(),
                        actor_ref_for_registration.id(),
                        "registered node manager lookup should resolve to the same actor id"
                    );
                })
                .await
                .expect("node manager registration task panicked");

            Self {
                actor_ref,
                config: f.config,
                dht_name,
                _mesh: mesh,
                _tempdir: f._tempdir,
            }
        }

        /// Best-effort teardown for tests sharing the process-wide mesh.
        ///
        /// Only deregisters the DHT name — does NOT stop the actor, because
        /// `stop_gracefully()` poisons the process-global `ActorSwarm` and
        /// causes all subsequent `register_actor` calls to time out.
        pub async fn cleanup(&self) {
            self._mesh.deregister_actor(&self.dht_name);
        }
    }

    // ── TwoNodeFixture ────────────────────────────────────────────────────────

    /// Fixture that simulates two logical nodes ("alpha" and "beta") sharing
    /// the same in-process mesh.
    ///
    /// Alpha acts as the caller, beta as the session host.
    ///
    /// ```rust,ignore
    /// let f = TwoNodeFixture::new(&test_id).await;
    /// // f.alpha / f.beta  — MeshNodeManagerFixture
    /// // f.mesh            — &'static MeshHandle
    /// ```
    pub struct TwoNodeFixture {
        pub alpha: MeshNodeManagerFixture,
        pub beta: MeshNodeManagerFixture,
        pub mesh: &'static MeshHandle,
    }

    impl TwoNodeFixture {
        pub async fn new(test_id: &str) -> Self {
            let mesh = get_test_mesh().await;
            let alpha = MeshNodeManagerFixture::new("alpha", test_id).await;
            let beta = MeshNodeManagerFixture::new("beta", test_id).await;
            Self { alpha, beta, mesh }
        }

        /// Best-effort teardown for both logical nodes.
        pub async fn cleanup(&self) {
            self.beta.cleanup().await;
            self.alpha.cleanup().await;
        }
    }

    // ── ProviderRoutingFixture ────────────────────────────────────────────────

    /// Fixture for provider-routing integration tests.
    ///
    /// Sets up:
    ///  - Alpha's `ProviderHostActor` (with a `MockLLMProvider`) registered in DHT.
    ///  - Beta's `RemoteNodeManager` registered in DHT.
    ///  - The shared mesh handle.
    ///
    /// ```rust,ignore
    /// let f = ProviderRoutingFixture::new(&test_id).await;
    /// // f.provider_host_ref  — ActorRef<ProviderHostActor> for alpha
    /// // f.provider_host_dht  — "provider_host::peer::alpha-{test_id}"
    /// // f.beta               — MeshNodeManagerFixture
    /// // f.call_count         — Arc<Mutex<usize>> to verify mock was invoked
    /// ```
    pub struct ProviderRoutingFixture {
        pub _provider_host_ref: ActorRef<ProviderHostActor>,
        pub _provider_host_dht: String,
        pub beta: MeshNodeManagerFixture,
        pub _alpha_config: Arc<AgentConfig>,
        pub _call_count: Arc<Mutex<usize>>,
        pub mesh: &'static MeshHandle,
        pub _alpha_tempdir: TempDir,
    }

    impl ProviderRoutingFixture {
        pub async fn new(test_id: &str) -> Self {
            let mesh = get_test_mesh().await;

            // Alpha: AgentConfig + ProviderHostActor
            let alpha_f = AgentConfigFixture::new().await;
            let mock = MockLLMProvider::new("provider routing response");
            let call_count = mock.call_counter();
            let _mock_arc: Arc<dyn LLMProvider> = Arc::new(mock);

            // We use the real ProviderHostActor with the alpha AgentConfig.
            // The mock provider won't be invoked via the normal plugin path;
            // provider routing integration tests that need the mock to fire
            // should use direct `ask(ProviderChatRequest)` with a provider
            // name resolvable from the config.
            let actor = crate::agent::remote::provider_host_backend::provider_host_from_config(
                alpha_f.config.clone(),
            );
            let actor_ref = ProviderHostActor::spawn(actor);

            let provider_host_dht = crate::agent::remote::scope::scoped_provider_host(
                &crate::agent::remote::scope::MeshScopeId::lan_default(),
                &format!("alpha-{}", test_id),
            );
            let mesh_for_registration = mesh.clone();
            let actor_ref_for_registration = actor_ref.clone();
            let provider_host_registration = provider_host_dht.clone();
            mesh_runtime()
                .spawn(async move {
                    mesh_for_registration
                        .register_actor(actor_ref_for_registration, provider_host_registration)
                        .await;
                })
                .await
                .expect("provider host registration task panicked");

            let beta = MeshNodeManagerFixture::new("beta", test_id).await;

            Self {
                _provider_host_ref: actor_ref,
                _provider_host_dht: provider_host_dht,
                beta,
                _alpha_config: alpha_f.config,
                _call_count: call_count,
                mesh,
                _alpha_tempdir: alpha_f._tempdir,
            }
        }
    }

    // ── RemoteAgentHandleFixture ─────────────────────────────────────────────

    /// Fixture for `RemoteAgentHandle` / `AgentHandle` tests.
    ///
    /// Creates a `RemoteNodeManager` and wraps it in a
    /// `RemoteAgentHandle` via the same construction path as
    /// `setup_mesh_from_config`.  Tests only require local actor refs —
    /// no DHT lookup is exercised.
    #[allow(dead_code)]
    pub struct RemoteAgentHandleFixture {
        pub handle: Arc<crate::agent::remote::remote_handle::RemoteAgentHandle>,
        pub node_manager: ActorRef<RemoteNodeManager>,
        pub mesh: &'static MeshHandle,
        pub _tempdir: TempDir,
    }

    #[allow(dead_code)]
    impl RemoteAgentHandleFixture {
        pub async fn new(test_id: &str) -> Self {
            let mesh = get_test_mesh().await;
            let nm_fixture = MeshNodeManagerFixture::new("stub", test_id).await;

            let handle = Arc::new(crate::agent::remote::remote_handle::RemoteAgentHandle::new(
                format!("stub-{}", test_id),
                None,
                mesh.clone(),
            ));

            Self {
                handle,
                node_manager: nm_fixture.actor_ref,
                mesh,
                _tempdir: nm_fixture._tempdir,
            }
        }
    }

    #[test]
    fn remote_test_config_dir_overrides_mesh_state_path() {
        let config_dir = ensure_remote_test_config_dir();
        let mesh_state_path = crate::agent::remote::mesh_state::default_mesh_state_path()
            .expect("mesh state path should resolve");
        assert_eq!(mesh_state_path, config_dir.join("mesh_state.json"));
    }
}
