//! Builder for `AgentConfig`.
//!
//! `AgentConfigBuilder` is the direct replacement for the `QueryMTAgent` +
//! `AgentBuilderExt` pattern. It constructs an `AgentConfig` without going
//! through the old fat-struct intermediate.

use crate::agent::agent_config::AgentConfig;
use crate::agent::core::{
    AgentMode, ClientState, DelegationContextConfig, DelegationContextTiming, SnapshotPolicy,
    ToolConfig, ToolPolicy,
};
use crate::agent::session_mcp::{NoopSessionMcpAttachmentSource, SessionMcpAttachmentSource};
use crate::config::{
    CompactionConfig, DelegationWaitPolicy, McpServerConfig, PruningConfig, RateLimitConfig,
    RuntimeExecutionPolicy, ToolOutputConfig,
};
use crate::delegation::{AgentRegistry, DefaultAgentRegistry};
use crate::event_fanout::EventFanout;
use crate::event_sink::EventSink;
use crate::hooks::Hooks;
use crate::index::{WorkspaceIndexManagerActor, WorkspaceIndexManagerConfig};
use crate::middleware::{
    AgentModeMiddleware, ContextConfig, ContextMiddleware, DelegationConfig, DelegationMiddleware,
    LimitsConfig, LimitsMiddleware, MiddlewareDriver,
};
use crate::session::compaction::SessionCompaction;
use crate::session::projection::EventJournal;
use crate::session::provider::SessionProvider;
use crate::session::store::SessionStore;
use crate::tools::ToolRegistry;
use agent_client_protocol::schema::AuthMethod;
use arc_swap::ArcSwap;
use querymt::LLMParams;
use querymt::plugin::host::PluginRegistry;
use std::collections::HashSet;
use std::sync::{Arc, Mutex as StdMutex};
use tokio::sync::Mutex;

/// Builder that constructs an [`AgentConfig`] directly.
///
/// Replaces the old `QueryMTAgent` + `AgentBuilderExt` pattern: instead of
/// building a fat intermediate struct that then copies ~25 fields into
/// `AgentConfig`, callers use this builder and call `build()` to get the
/// config directly.
pub struct AgentConfigBuilder {
    provider: Arc<SessionProvider>,
    event_journal: Arc<dyn EventJournal>,
    event_fanout: Arc<EventFanout>,
    default_mode: Arc<StdMutex<AgentMode>>,
    max_steps: Option<usize>,
    snapshot_policy: SnapshotPolicy,
    assume_mutating: bool,
    mutating_tools: HashSet<String>,
    max_prompt_bytes: Option<usize>,
    tool_config: ToolConfig,
    tool_registry: ToolRegistry,
    middleware_drivers: Vec<Arc<dyn MiddlewareDriver>>,
    auth_methods: Vec<AuthMethod>,
    agent_registry: Arc<dyn AgentRegistry + Send + Sync>,
    delegation_context_config: DelegationContextConfig,
    workspace_manager_actor: kameo::actor::ActorRef<WorkspaceIndexManagerActor>,
    execution_timeout_secs: u64,
    delegation_wait_policy: DelegationWaitPolicy,
    delegation_wait_timeout_secs: u64,
    delegation_cancel_grace_secs: u64,
    execution_policy: RuntimeExecutionPolicy,
    compaction: SessionCompaction,
    snapshot_backend: Option<Arc<dyn crate::snapshot::SnapshotBackend>>,
    snapshot_gc_config: crate::snapshot::GcConfig,
    pending_elicitations: crate::elicitation::PendingElicitationMap,
    mcp_servers: Vec<McpServerConfig>,
    session_mcp_attachment_source: Arc<dyn SessionMcpAttachmentSource>,
    schedule_repository: Option<Arc<dyn crate::session::repo_schedule::ScheduleRepository>>,
    knowledge_store: Option<Arc<dyn crate::knowledge::KnowledgeStore>>,
    slash_command_registry: crate::slash_commands::SlashCommandRegistry,
    hooks: Hooks,
}

impl AgentConfigBuilder {
    /// Create a new builder with the required infrastructure.
    ///
    /// Registers all built-in tools in the default tool registry.
    pub fn new(
        plugin_registry: Arc<PluginRegistry>,
        store: Arc<dyn SessionStore>,
        event_journal: Arc<dyn EventJournal>,
        initial_config: LLMParams,
    ) -> Self {
        let provider = Arc::new(SessionProvider::new(
            Arc::clone(&plugin_registry),
            store,
            initial_config,
        ));
        let mut tool_registry = ToolRegistry::new();
        tool_registry.extend(crate::tools::builtins::all_builtin_tools());

        Self {
            provider,
            event_journal,
            event_fanout: Arc::new(EventFanout::new()),
            default_mode: Arc::new(StdMutex::new(AgentMode::Build)),
            max_steps: None,
            snapshot_policy: SnapshotPolicy::None,
            assume_mutating: true,
            mutating_tools: HashSet::new(),
            max_prompt_bytes: None,
            tool_config: ToolConfig::default(),
            tool_registry,
            middleware_drivers: Vec::new(),
            auth_methods: Vec::new(),
            agent_registry: Arc::new(DefaultAgentRegistry::new()),
            delegation_context_config: DelegationContextConfig {
                timing: DelegationContextTiming::FirstTurnOnly,
                auto_inject: true,
            },
            workspace_manager_actor: WorkspaceIndexManagerActor::new(
                WorkspaceIndexManagerConfig::default(),
            ),
            execution_timeout_secs: 300,
            delegation_wait_policy: DelegationWaitPolicy::default(),
            delegation_wait_timeout_secs: 120,
            delegation_cancel_grace_secs: 5,
            execution_policy: RuntimeExecutionPolicy::default(),
            compaction: SessionCompaction::new(),
            snapshot_backend: None,
            snapshot_gc_config: crate::snapshot::GcConfig::default(),
            pending_elicitations: Arc::new(Mutex::new(std::collections::HashMap::new())),
            mcp_servers: Vec::new(),
            session_mcp_attachment_source: Arc::new(NoopSessionMcpAttachmentSource),
            schedule_repository: None,
            knowledge_store: None,
            slash_command_registry: crate::slash_commands::SlashCommandRegistry::empty(),
            hooks: Hooks::disabled(),
        }
    }

    /// Create a builder from a pre-built [`SessionProvider`] and event journal.
    ///
    /// Use this when you already have a `SessionProvider` (e.g. with a mock
    /// store or a custom LLM provider) and want builder defaults for
    /// everything else.  Does **not** register built-in tools — call
    /// [`with_tool_registry`] if you need them.
    pub fn from_provider(
        provider: Arc<SessionProvider>,
        event_journal: Arc<dyn EventJournal>,
    ) -> Self {
        Self {
            provider,
            event_journal,
            event_fanout: Arc::new(EventFanout::new()),
            default_mode: Arc::new(StdMutex::new(AgentMode::Build)),
            max_steps: None,
            snapshot_policy: SnapshotPolicy::None,
            assume_mutating: true,
            mutating_tools: HashSet::new(),
            max_prompt_bytes: None,
            tool_config: ToolConfig::default(),
            tool_registry: ToolRegistry::new(),
            middleware_drivers: Vec::new(),
            auth_methods: Vec::new(),
            agent_registry: Arc::new(DefaultAgentRegistry::new()),
            delegation_context_config: DelegationContextConfig {
                timing: DelegationContextTiming::FirstTurnOnly,
                auto_inject: true,
            },
            workspace_manager_actor: WorkspaceIndexManagerActor::new(
                WorkspaceIndexManagerConfig::default(),
            ),
            execution_timeout_secs: 300,
            delegation_wait_policy: DelegationWaitPolicy::default(),
            delegation_wait_timeout_secs: 120,
            delegation_cancel_grace_secs: 5,
            execution_policy: RuntimeExecutionPolicy::default(),
            compaction: SessionCompaction::new(),
            snapshot_backend: None,
            snapshot_gc_config: crate::snapshot::GcConfig::default(),
            pending_elicitations: Arc::new(Mutex::new(std::collections::HashMap::new())),
            mcp_servers: Vec::new(),
            session_mcp_attachment_source: Arc::new(NoopSessionMcpAttachmentSource),
            schedule_repository: None,
            knowledge_store: None,
            slash_command_registry: crate::slash_commands::SlashCommandRegistry::empty(),
            hooks: Hooks::disabled(),
        }
    }

    /// Build the final [`AgentConfig`].
    pub fn build(self) -> AgentConfig {
        let event_sink = Arc::new(EventSink::new(
            self.event_journal.clone(),
            self.event_fanout.clone(),
        ));
        AgentConfig {
            provider: self.provider,
            event_sink,
            agent_registry: self.agent_registry,
            workspace_manager_actor: self.workspace_manager_actor,
            default_mode: self.default_mode,
            default_reasoning_effort: Arc::new(ArcSwap::from_pointee(None)),
            tool_config: self.tool_config,
            tool_registry: self.tool_registry,
            middleware_drivers: self.middleware_drivers,
            auth_methods: self.auth_methods,
            max_steps: self.max_steps,
            snapshot_policy: self.snapshot_policy,
            assume_mutating: self.assume_mutating,
            mutating_tools: self.mutating_tools,
            max_prompt_bytes: self.max_prompt_bytes,
            execution_timeout_secs: self.execution_timeout_secs,
            delegation_wait_policy: self.delegation_wait_policy,
            delegation_wait_timeout_secs: self.delegation_wait_timeout_secs,
            delegation_cancel_grace_secs: self.delegation_cancel_grace_secs,
            execution_policy: self.execution_policy,
            compaction: self.compaction,
            snapshot_backend: self.snapshot_backend,
            snapshot_gc_config: self.snapshot_gc_config,
            delegation_context_config: self.delegation_context_config,
            pending_elicitations: self.pending_elicitations,
            mcp_servers: self.mcp_servers,
            session_mcp_attachment_source: self.session_mcp_attachment_source,
            schedule_repository: self.schedule_repository,
            knowledge_store: self.knowledge_store,
            slash_command_registry: self.slash_command_registry,
            hooks: self.hooks,
        }
    }

    // ── Template context ──────────────────────────────────────────────────

    /// Set the agent ID used for `{{ agent_id }}` template resolution in
    /// system prompts.  Must be called before `build()` and before the
    /// `Arc<SessionProvider>` is shared (i.e. while refcount is 1).
    pub fn with_agent_id(mut self, id: impl Into<String>) -> Self {
        if let Some(provider) = Arc::get_mut(&mut self.provider) {
            provider.agent_id = Some(id.into());
        }
        self
    }

    // ── Delegation ────────────────────────────────────────────────────────

    /// Sets the agent registry for delegation functionality.
    ///
    /// Also auto-registers `DelegationMiddleware` when the registry is non-empty
    /// and `auto_inject` is enabled.
    pub fn with_agent_registry(mut self, registry: Arc<dyn AgentRegistry + Send + Sync>) -> Self {
        self.agent_registry = registry.clone();

        // Remove old DelegateTool and add new one with registry
        self.tool_registry.remove("delegate");
        self.tool_registry
            .add(Arc::new(crate::tools::builtins::DelegateTool::new()));

        if self.delegation_context_config.auto_inject && !registry.list_agents().is_empty() {
            let middleware = DelegationMiddleware::new(
                self.provider.history_store(),
                registry,
                DelegationConfig {
                    context_timing: self.delegation_context_config.timing,
                    prevent_duplicates: false,
                    auto_inject: true,
                },
            );
            self.middleware_drivers.push(Arc::new(middleware));
        }

        self
    }

    /// Sets the agent registry without auto-registering `DelegateTool` or
    /// `DelegationMiddleware`. Use this when you wire delegation externally
    /// (e.g. via a `DelegationOrchestrator` added as an observer).
    pub fn with_agent_registry_only(
        mut self,
        registry: Arc<dyn AgentRegistry + Send + Sync>,
    ) -> Self {
        self.agent_registry = registry;
        self
    }

    /// Sets the delegation context timing.
    pub fn with_delegation_context_timing(mut self, timing: DelegationContextTiming) -> Self {
        self.delegation_context_config.timing = timing;
        self
    }

    /// Enables or disables auto delegation context injection.
    pub fn with_auto_delegation_context(mut self, enabled: bool) -> Self {
        self.delegation_context_config.auto_inject = enabled;
        self
    }

    /// Sets the delegation context config directly.
    pub fn with_delegation_context_config(mut self, config: DelegationContextConfig) -> Self {
        self.delegation_context_config = config;
        self
    }

    // ── Execution limits ──────────────────────────────────────────────────

    /// Sets the maximum number of execution steps.
    pub fn with_max_steps(mut self, max_steps: usize) -> Self {
        self.max_steps = Some(max_steps);
        self
    }

    /// Sets the maximum prompt size in bytes.
    pub fn with_max_prompt_bytes(mut self, bytes: usize) -> Self {
        self.max_prompt_bytes = Some(bytes);
        self
    }

    /// Sets the execution timeout in seconds.
    pub fn with_execution_timeout_secs(mut self, secs: u64) -> Self {
        self.execution_timeout_secs = secs;
        self
    }

    /// Configure delegation wait behavior for this agent.
    pub fn with_delegation_wait_policy(mut self, policy: DelegationWaitPolicy) -> Self {
        self.delegation_wait_policy = policy;
        self
    }

    /// Configure timeout used when waiting for delegated work.
    pub fn with_delegation_wait_timeout_secs(mut self, timeout_secs: u64) -> Self {
        self.delegation_wait_timeout_secs = timeout_secs;
        self
    }

    /// Configure cancellation grace period for timed-out delegation cleanup.
    pub fn with_delegation_cancel_grace_secs(mut self, grace_secs: u64) -> Self {
        self.delegation_cancel_grace_secs = grace_secs;
        self
    }

    // ── Middleware ─────────────────────────────────────────────────────────

    /// Adds limits middleware.
    pub fn with_limits(mut self, config: LimitsConfig) -> Self {
        self.middleware_drivers
            .push(Arc::new(LimitsMiddleware::new(config)));
        self
    }

    /// Adds context management middleware.
    pub fn with_context_management(mut self, config: ContextConfig) -> Self {
        self.middleware_drivers
            .push(Arc::new(ContextMiddleware::new(config)));
        self
    }

    /// Adds delegation middleware.
    pub fn with_delegation(mut self, config: DelegationConfig) -> Self {
        let middleware = DelegationMiddleware::new(
            self.provider.history_store(),
            self.agent_registry.clone(),
            config,
        );
        self.middleware_drivers.push(Arc::new(middleware));
        self
    }

    /// Adds a single middleware.
    pub fn with_middleware<M: MiddlewareDriver + 'static>(mut self, middleware: M) -> Self {
        self.middleware_drivers.push(Arc::new(middleware));
        self
    }

    /// Adds multiple middlewares.
    pub fn with_middlewares(mut self, middlewares: Vec<Arc<dyn MiddlewareDriver>>) -> Self {
        self.middleware_drivers.extend(middlewares);
        self
    }

    /// Adds agent mode middleware with a custom plan-mode reminder.
    pub fn with_agent_mode_middleware<T: Into<String>>(mut self, reminder: T) -> Self {
        self.middleware_drivers
            .push(Arc::new(AgentModeMiddleware::new(reminder.into())));
        self
    }

    // ── Auth ──────────────────────────────────────────────────────────────

    /// Sets authentication methods.
    pub fn with_auth_methods<I>(mut self, methods: I) -> Self
    where
        I: IntoIterator<Item = AuthMethod>,
    {
        self.auth_methods = methods.into_iter().collect();
        self
    }

    /// Sets the client state.
    pub fn with_client_state(self, _state: ClientState) -> Self {
        // ClientState lives on AgentHandle (connection level), not AgentConfig.
        // Accept the call for API compatibility but do nothing here.
        self
    }

    // ── Snapshot ──────────────────────────────────────────────────────────

    /// Sets the snapshot policy.
    pub fn with_snapshot_policy(mut self, policy: SnapshotPolicy) -> Self {
        self.snapshot_policy = policy;
        self
    }

    /// Sets the snapshot backend for undo/redo support.
    pub fn with_snapshot_backend(
        mut self,
        backend: Arc<dyn crate::snapshot::SnapshotBackend>,
    ) -> Self {
        self.snapshot_backend = Some(backend);
        self
    }

    /// Sets the snapshot GC configuration.
    pub fn with_snapshot_gc_config(mut self, config: crate::snapshot::GcConfig) -> Self {
        self.snapshot_gc_config = config;
        self
    }

    // ── MCP servers ───────────────────────────────────────────────────────

    /// Sets the MCP servers to attach to every new session (from TOML `[[mcp]]` config).
    pub fn with_mcp_servers(mut self, servers: Vec<McpServerConfig>) -> Self {
        self.mcp_servers = servers;
        self
    }

    // ── Session MCP attachment source ──────────────────────────────────────

    /// Sets the source of runtime MCP attachments (e.g., mobile in-process MCP
    /// peers) that should be added to every session materialized on this node.
    ///
    /// Defaults to [`NoopSessionMcpAttachmentSource`] if not called.
    pub fn with_session_mcp_attachment_source(
        mut self,
        source: Arc<dyn SessionMcpAttachmentSource>,
    ) -> Self {
        self.session_mcp_attachment_source = source;
        self
    }

    // ── Scheduling & Knowledge ────────────────────────────────────────────

    /// Set the schedule repository (from storage backend).
    pub fn with_schedule_repository(
        mut self,
        repo: Arc<dyn crate::session::repo_schedule::ScheduleRepository>,
    ) -> Self {
        self.schedule_repository = Some(repo);
        self
    }

    /// Set the knowledge store (from storage backend).
    pub fn with_knowledge_store(
        mut self,
        store: Arc<dyn crate::knowledge::KnowledgeStore>,
    ) -> Self {
        self.knowledge_store = Some(store);
        self
    }

    /// Set the slash command registry.
    pub fn with_slash_command_registry(
        mut self,
        registry: crate::slash_commands::SlashCommandRegistry,
    ) -> Self {
        self.slash_command_registry = registry;
        self
    }

    /// Set the hooks runtime.
    pub fn with_hooks(mut self, hooks: Hooks) -> Self {
        self.hooks = hooks;
        self
    }

    // ── Tools ─────────────────────────────────────────────────────────────

    /// Sets the tool policy.
    pub fn with_tool_policy(mut self, policy: ToolPolicy) -> Self {
        self.tool_config.policy = policy;
        self
    }

    /// Sets the tool configuration.
    pub fn with_tool_config(mut self, config: ToolConfig) -> Self {
        self.tool_config = config;
        self
    }

    /// Sets the tool registry (replaces the default one).
    pub fn with_tool_registry(mut self, registry: ToolRegistry) -> Self {
        self.tool_registry = registry;
        self
    }

    /// Sets the allowed tools allowlist.
    pub fn with_allowed_tools<I, S>(mut self, tool_names: I) -> Self
    where
        I: IntoIterator<Item = S>,
        S: Into<String>,
    {
        self.tool_config.allowlist = Some(
            tool_names
                .into_iter()
                .map(Into::into)
                .collect::<HashSet<_>>(),
        );
        self
    }

    /// Sets the denied tools denylist.
    pub fn with_denied_tools<I, S>(mut self, tool_names: I) -> Self
    where
        I: IntoIterator<Item = S>,
        S: Into<String>,
    {
        self.tool_config.denylist = tool_names.into_iter().map(Into::into).collect();
        self
    }

    /// Sets specific tools to be considered mutating.
    pub fn with_mutating_tools<I>(mut self, tools: I) -> Self
    where
        I: IntoIterator<Item = String>,
    {
        self.mutating_tools = tools.into_iter().collect();
        self
    }

    /// Sets whether to assume all tools are mutating by default.
    pub fn with_assume_mutating(mut self, assume: bool) -> Self {
        self.assume_mutating = assume;
        self
    }

    // ── Compaction ────────────────────────────────────────────────────────

    /// Sets the full execution policy at once (tool output, pruning, compaction, rate limit).
    pub fn with_execution_policy(mut self, policy: RuntimeExecutionPolicy) -> Self {
        self.execution_policy = policy;
        self
    }

    /// Sets the tool output truncation configuration (Layer 1).
    pub fn with_tool_output_config(mut self, config: ToolOutputConfig) -> Self {
        self.execution_policy.tool_output = config;
        self
    }

    /// Sets the pruning configuration (Layer 2).
    pub fn with_pruning_config(mut self, config: PruningConfig) -> Self {
        self.execution_policy.pruning = config;
        self
    }

    /// Sets the AI compaction configuration (Layer 3).
    pub fn with_compaction_config(mut self, config: CompactionConfig) -> Self {
        self.execution_policy.compaction = config;
        self
    }

    /// Enables full compaction with default settings.
    pub fn with_compaction_enabled(self) -> Self {
        self.with_pruning_config(PruningConfig::default())
            .with_compaction_config(CompactionConfig::default())
    }

    // ── Rate limiting ─────────────────────────────────────────────────────

    /// Sets the rate limit retry configuration.
    pub fn with_rate_limit_config(mut self, config: RateLimitConfig) -> Self {
        self.execution_policy.rate_limit = config;
        self
    }

    // ── Mode ──────────────────────────────────────────────────────────────

    /// Sets the initial agent mode.
    pub fn with_agent_mode(self, mode: AgentMode) -> Self {
        if let Ok(mut default_mode) = self.default_mode.lock() {
            *default_mode = mode;
        }
        self
    }

    // ── Remote mesh routing ───────────────────────────────────────────────────

    // ── Internal accessors (used by simple::agent and quorum) ─────────────

    /// Read the current `event_journal`.
    pub fn event_journal(&self) -> Arc<dyn EventJournal> {
        self.event_journal.clone()
    }

    /// Read the current `compaction_config` (for auto-ContextMiddleware check).
    pub fn compaction_config(&self) -> &CompactionConfig {
        &self.execution_policy.compaction
    }

    /// Read the current `default_mode` (for AgentHandle construction).
    pub fn default_mode_value(&self) -> AgentMode {
        self.default_mode
            .lock()
            .map(|m| *m)
            .unwrap_or(AgentMode::Build)
    }

    /// Extend the tool registry with additional tools.
    pub fn extend_tool_registry(
        &mut self,
        tools: impl IntoIterator<Item = Arc<dyn crate::tools::Tool>>,
    ) {
        for tool in tools {
            self.tool_registry.add(tool);
        }
    }

    /// Mutably access the tool registry (for adding tools during build).
    pub fn tool_registry_mut(&mut self) -> &mut ToolRegistry {
        &mut self.tool_registry
    }

    /// Mutably push a middleware driver (for quorum helper function).
    pub fn push_middleware(&mut self, driver: Arc<dyn MiddlewareDriver>) {
        self.middleware_drivers.push(driver);
    }

    /// Borrow the `SessionProvider` (used by `DelegationMiddleware` constructor in quorum).
    pub fn provider(&self) -> &Arc<SessionProvider> {
        &self.provider
    }

    /// Borrow the `agent_registry` (used by `DelegationMiddleware` constructor in quorum).
    pub fn agent_registry(&self) -> Arc<dyn AgentRegistry + Send + Sync> {
        self.agent_registry.clone()
    }

    /// Borrow the `delegation_context_config` (used when building DelegationMiddleware).
    pub fn delegation_context_config(&self) -> &DelegationContextConfig {
        &self.delegation_context_config
    }

    /// Apply execution policy and snapshot backend from an `ExecutionPolicy`.
    ///
    /// Sets the runtime execution policy and, if the snapshot backend is "git",
    /// wires up a `GitSnapshotBackend`. This replaces 4+ copy-pasted match blocks.
    pub fn with_snapshot_from_execution(mut self, exec: &crate::config::ExecutionPolicy) -> Self {
        self = self.with_execution_policy(RuntimeExecutionPolicy::from(exec));
        match exec.snapshot.backend.as_str() {
            "git" => {
                use crate::snapshot::git::GitSnapshotBackend;
                self.with_snapshot_backend(Arc::new(GitSnapshotBackend::new()))
            }
            "none" | "" => self,
            other => {
                log::warn!("Unknown snapshot backend '{}', ignoring", other);
                self
            }
        }
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::agent::core::{AgentMode, SnapshotPolicy, ToolPolicy};
    use crate::config::PruningConfig;
    use crate::session::backend::StorageBackend;
    use crate::session::sqlite_storage::SqliteStorage;
    use crate::test_utils::helpers::empty_plugin_registry;
    use querymt::LLMParams;
    use std::sync::Arc;

    async fn make_builder() -> (AgentConfigBuilder, tempfile::TempDir) {
        let (registry, temp_dir) = empty_plugin_registry().unwrap();
        let storage = Arc::new(SqliteStorage::connect(":memory:".into()).await.unwrap());
        let llm = LLMParams::new().provider("mock").model("mock-model");
        let builder = AgentConfigBuilder::new(
            Arc::new(registry),
            storage.session_store(),
            storage.event_journal(),
            llm,
        );
        (builder, temp_dir)
    }

    // ── Defaults ────────────────────────────────────────────────────────────

    #[tokio::test]
    async fn default_mode_is_build() {
        let (builder, _td) = make_builder().await;
        assert_eq!(builder.default_mode_value(), AgentMode::Build);
    }

    #[tokio::test]
    async fn default_tool_registry_has_builtin_tools() {
        let (builder, _td) = make_builder().await;
        let config = builder.build();
        // Built-in tools should include at minimum "shell", "read_tool", etc.
        let names: Vec<_> = config
            .tool_registry
            .definitions()
            .iter()
            .map(|t| t.function.name.clone())
            .collect();
        assert!(!names.is_empty(), "default tool registry should have tools");
    }

    // ── Builder setters ─────────────────────────────────────────────────────

    #[tokio::test]
    async fn with_agent_mode_overrides_default() {
        let (builder, _td) = make_builder().await;
        let builder = builder.with_agent_mode(AgentMode::Plan);
        assert_eq!(builder.default_mode_value(), AgentMode::Plan);
    }

    #[tokio::test]
    async fn with_max_steps_sets_value() {
        let (builder, _td) = make_builder().await;
        let builder = builder.with_max_steps(42);
        let config = builder.build();
        assert_eq!(config.max_steps, Some(42));
    }

    #[tokio::test]
    async fn with_max_prompt_bytes_sets_value() {
        let (builder, _td) = make_builder().await;
        let builder = builder.with_max_prompt_bytes(8192);
        let config = builder.build();
        assert_eq!(config.max_prompt_bytes, Some(8192));
    }

    #[tokio::test]
    async fn with_snapshot_policy_sets_value() {
        let (builder, _td) = make_builder().await;
        let builder = builder.with_snapshot_policy(SnapshotPolicy::Diff);
        let config = builder.build();
        assert_eq!(config.snapshot_policy, SnapshotPolicy::Diff);
    }

    #[tokio::test]
    async fn with_assume_mutating_false() {
        let (builder, _td) = make_builder().await;
        let builder = builder.with_assume_mutating(false);
        let config = builder.build();
        assert!(!config.assume_mutating);
    }

    #[tokio::test]
    async fn with_tool_policy_changes_policy() {
        let (builder, _td) = make_builder().await;
        let builder = builder.with_tool_policy(ToolPolicy::BuiltInOnly);
        let config = builder.build();
        assert_eq!(config.tool_config.policy, ToolPolicy::BuiltInOnly);
    }

    #[tokio::test]
    async fn with_allowed_tools_sets_allowlist() {
        let (builder, _td) = make_builder().await;
        let builder = builder.with_allowed_tools(["shell", "read_tool"]);
        let config = builder.build();
        let allowlist = config.tool_config.allowlist.unwrap();
        assert!(allowlist.contains("shell"));
        assert!(allowlist.contains("read_tool"));
        assert!(!allowlist.contains("delete_file"));
    }

    #[tokio::test]
    async fn with_denied_tools_sets_denylist() {
        let (builder, _td) = make_builder().await;
        let builder = builder.with_denied_tools(["shell".to_string(), "delete_file".to_string()]);
        let config = builder.build();
        assert!(config.tool_config.denylist.contains("shell"));
        assert!(config.tool_config.denylist.contains("delete_file"));
    }

    #[tokio::test]
    async fn with_compaction_enabled_sets_defaults() {
        let (builder, _td) = make_builder().await;
        let builder = builder.with_compaction_enabled();
        let config = builder.build();
        // Should have a non-default compaction config
        let _ = config.execution_policy.compaction;
        let _ = config.execution_policy.pruning;
        // Just confirm build succeeds
    }

    #[tokio::test]
    async fn with_pruning_config_sets_value() {
        let (builder, _td) = make_builder().await;
        let pruning = PruningConfig::default();
        let builder = builder.with_pruning_config(pruning);
        let config = builder.build();
        let _ = config.execution_policy.pruning;
    }

    #[tokio::test]
    async fn with_mutating_tools_sets_set() {
        let (builder, _td) = make_builder().await;
        let builder = builder.with_mutating_tools(["my_tool".to_string()]);
        let config = builder.build();
        assert!(config.mutating_tools.contains("my_tool"));
    }

    // ── build() produces valid AgentConfig ──────────────────────────────────

    #[tokio::test]
    async fn build_produces_valid_config_with_defaults() {
        let (builder, _td) = make_builder().await;
        let config = builder.build();
        // Default execution timeout
        assert_eq!(config.execution_timeout_secs, 300);
        // Default: no max steps
        assert!(config.max_steps.is_none());
        // Default: no max prompt bytes
        assert!(config.max_prompt_bytes.is_none());
        // Default: snapshot disabled
        assert_eq!(config.snapshot_policy, SnapshotPolicy::None);
    }

    #[tokio::test]
    async fn event_journal_accessor_works() {
        let (builder, _td) = make_builder().await;
        let journal = builder.event_journal();
        // Just verify we get an Arc back
        let _ = Arc::strong_count(&journal);
    }

    #[tokio::test]
    async fn provider_accessor_works() {
        let (builder, _td) = make_builder().await;
        let _provider = builder.provider();
    }

    // ── Middleware integration ───────────────────────────────────────────────

    #[tokio::test]
    async fn with_middleware_adds_to_chain() {
        use crate::middleware::{LimitsConfig, LimitsMiddleware};
        let (builder, _td) = make_builder().await;
        let builder = builder.with_middleware(LimitsMiddleware::new(LimitsConfig::default()));
        let config = builder.build();
        assert_eq!(config.middleware_drivers.len(), 1);
    }

    #[tokio::test]
    async fn with_middlewares_adds_multiple() {
        use crate::middleware::{LimitsConfig, LimitsMiddleware};
        let (builder, _td) = make_builder().await;
        let drivers: Vec<Arc<dyn MiddlewareDriver>> = vec![
            Arc::new(LimitsMiddleware::new(LimitsConfig::default())),
            Arc::new(LimitsMiddleware::new(LimitsConfig::default())),
        ];
        let builder = builder.with_middlewares(drivers);
        let config = builder.build();
        assert_eq!(config.middleware_drivers.len(), 2);
    }

    #[tokio::test]
    async fn create_driver_from_config_with_max_steps() {
        let (builder, _td) = make_builder().await;
        let builder = builder.with_max_steps(50);
        let config = builder.build();
        let _driver = config.create_driver();
    }

    // ── MCP servers ─────────────────────────────────────────────────────────

    #[tokio::test]
    async fn with_mcp_servers_stores_and_builds() {
        let (builder, _td) = make_builder().await;
        let servers = vec![
            McpServerConfig::Http {
                name: "test-server".to_string(),
                url: "https://mcp.example.com/mcp".to_string(),
                headers: std::collections::HashMap::from([(
                    "Authorization".to_string(),
                    "Bearer token".to_string(),
                )]),
            },
            McpServerConfig::Stdio {
                name: "local-server".to_string(),
                command: "npx".to_string(),
                args: vec!["-y".to_string(), "@example/mcp-server".to_string()],
                env: std::collections::HashMap::new(),
            },
        ];
        let builder = builder.with_mcp_servers(servers);
        let config = builder.build();
        assert_eq!(config.mcp_servers.len(), 2);
        assert!(
            matches!(&config.mcp_servers[0], McpServerConfig::Http { name, .. } if name == "test-server")
        );
        assert!(
            matches!(&config.mcp_servers[1], McpServerConfig::Stdio { name, .. } if name == "local-server")
        );
    }

    #[tokio::test]
    async fn mcp_servers_empty_by_default() {
        let (builder, _td) = make_builder().await;
        let config = builder.build();
        assert!(config.mcp_servers.is_empty());
    }
}
