#![allow(clippy::unwrap_used, clippy::expect_used, clippy::panic)]
use std::sync::{
    Arc, Mutex,
    atomic::{AtomicBool, AtomicUsize, Ordering},
};

use async_trait::async_trait;
use futures::stream;
use meerkat::{
    AgentBuildConfig, AgentBuilder, AgentFactory, AgentLlmClient, AgentToolDispatcher,
    BuildAgentError, Config, LlmDoneOutcome, LlmEvent, LlmRequest, ToolDef, ToolError, ToolResult,
};
use meerkat_client::LlmClient;
use meerkat_core::ToolDispatchOutcome;
use meerkat_core::{
    AssistantBlock, BlobId, BlobRef, LlmStreamResult, Message, Provider, ProviderImageMetadata,
    RevisedPromptDisposition, StopReason, ToolCallView, Usage,
};
use meerkat_core::{HookEngine, HookEngineError, HookExecutionReport, HookId, HookInvocation};
use meerkat_tools::schema_for;
use schemars::JsonSchema;
use serde::Deserialize;
use serde_json::json;
#[path = "support/test_session_store.rs"]
mod test_session_store;
use test_session_store::TestSessionStore;

#[allow(dead_code)]
#[derive(Debug, JsonSchema, Deserialize)]
struct EchoInput {
    message: String,
}

struct MockLlmClient {
    calls: Arc<AtomicUsize>,
}

#[async_trait]
impl LlmClient for MockLlmClient {
    fn project_replay_messages(
        &self,
        messages: &[meerkat_core::Message],
    ) -> Result<Vec<meerkat_core::Message>, meerkat_client::LlmError> {
        Ok(messages.to_vec())
    }

    fn stream<'a>(
        &'a self,
        _request: &'a LlmRequest,
    ) -> std::pin::Pin<
        Box<dyn futures::Stream<Item = Result<LlmEvent, meerkat_client::LlmError>> + Send + 'a>,
    > {
        let call_index = self.calls.fetch_add(1, Ordering::SeqCst);
        if call_index == 0 {
            Box::pin(stream::iter(vec![
                Ok(LlmEvent::ToolCallComplete {
                    id: "tc-1".into(),
                    name: "echo".into(),
                    args: json!({"message": "hello"}),
                    meta: None,
                }),
                Ok(LlmEvent::Done {
                    outcome: LlmDoneOutcome::Success {
                        stop_reason: meerkat::StopReason::ToolUse,
                    },
                }),
            ]))
        } else {
            Box::pin(stream::iter(vec![
                Ok(LlmEvent::TextDelta {
                    delta: "done".to_string(),
                    meta: None,
                }),
                Ok(LlmEvent::Done {
                    outcome: LlmDoneOutcome::Success {
                        stop_reason: meerkat::StopReason::EndTurn,
                    },
                }),
            ]))
        }
    }

    fn provider(&self) -> &'static str {
        "mock"
    }

    async fn health_check(&self) -> Result<(), meerkat_client::LlmError> {
        Ok(())
    }
}

struct RecordingDispatcher {
    called: Arc<AtomicBool>,
}

#[async_trait]
impl AgentToolDispatcher for RecordingDispatcher {
    fn tools(&self) -> Arc<[Arc<ToolDef>]> {
        vec![Arc::new(ToolDef {
            name: "echo".into(),
            description: "Echo tool".into(),
            input_schema: schema_for::<EchoInput>(),
            provenance: None,
        })]
        .into()
    }

    async fn dispatch(&self, call: ToolCallView<'_>) -> Result<ToolDispatchOutcome, ToolError> {
        self.called.store(true, Ordering::SeqCst);
        let args: EchoInput = call
            .parse_args()
            .map_err(|e| ToolError::invalid_arguments(call.name, e.to_string()))?;
        let payload = json!({ "message": args.message, "tool": call.name });
        Ok(ToolResult::new(call.id.to_string(), payload.to_string(), false).into())
    }
}

struct ImageAgentLlmClient;

#[async_trait]
impl AgentLlmClient for ImageAgentLlmClient {
    async fn stream_response(
        &self,
        _messages: &[Message],
        _tools: &[Arc<ToolDef>],
        _max_tokens: u32,
        _temperature: Option<f32>,
        _provider_params: Option<&meerkat_core::lifecycle::run_primitive::ProviderParamsOverride>,
    ) -> Result<LlmStreamResult, meerkat::AgentError> {
        Ok(LlmStreamResult::new(
            vec![AssistantBlock::Image {
                image_id: meerkat_core::AssistantImageId::new(
                    "00000000-0000-4000-8000-000000000124"
                        .parse()
                        .expect("valid uuid"),
                ),
                blob_ref: BlobRef {
                    blob_id: BlobId::new("image-blob-124"),
                    media_type: "image/png".to_string(),
                },
                media_type: meerkat_core::MediaType("image/png".to_string()),
                width: 64,
                height: 32,
                revised_prompt: RevisedPromptDisposition::NotRequested,
                meta: ProviderImageMetadata::NotEmitted,
            }],
            StopReason::EndTurn,
            Usage::default(),
        ))
    }

    fn provider(&self) -> &'static str {
        "anthropic"
    }

    fn model(&self) -> &'static str {
        "claude-sonnet-4-6"
    }
}

struct CustomAgentLlmClient;

#[async_trait]
impl AgentLlmClient for CustomAgentLlmClient {
    async fn stream_response(
        &self,
        _messages: &[Message],
        _tools: &[Arc<ToolDef>],
        _max_tokens: u32,
        _temperature: Option<f32>,
        _provider_params: Option<&meerkat_core::lifecycle::run_primitive::ProviderParamsOverride>,
    ) -> Result<LlmStreamResult, meerkat::AgentError> {
        Ok(LlmStreamResult::new(
            vec![AssistantBlock::Text {
                text: "custom ok".to_string(),
                meta: None,
            }],
            StopReason::EndTurn,
            Usage::default(),
        ))
    }

    fn provider(&self) -> &'static str {
        "custom-provider"
    }

    fn model(&self) -> &'static str {
        "custom-agent-model"
    }
}

struct NoopCompactor;

impl meerkat_core::compact::Compactor for NoopCompactor {
    fn should_compact(&self, _ctx: &meerkat_core::compact::CompactionContext) -> bool {
        false
    }

    fn compaction_prompt(&self) -> &'static str {
        ""
    }

    fn max_summary_tokens(&self) -> u32 {
        1
    }

    fn rebuild_history(
        &self,
        messages: &[Message],
        _summary: &str,
    ) -> meerkat_core::compact::CompactionResult {
        meerkat_core::compact::CompactionResult {
            messages: messages.to_vec(),
            discarded: Vec::new(),
        }
    }
}

struct NoopMemoryStore;

#[async_trait]
impl meerkat_core::memory::MemoryStore for NoopMemoryStore {
    async fn index_scoped_batch(
        &self,
        batch: meerkat_core::memory::MemoryIndexBatch,
    ) -> Result<meerkat_core::memory::MemoryIndexReceipt, meerkat_core::memory::MemoryStoreError>
    {
        Ok(meerkat_core::memory::MemoryIndexReceipt {
            scope: batch.scope().clone(),
            indexed_entries: batch.len(),
        })
    }

    async fn search(
        &self,
        _scope: &meerkat_core::memory::MemorySearchScope,
        _query: &str,
        _limit: usize,
    ) -> Result<Vec<meerkat_core::memory::MemoryResult>, meerkat_core::memory::MemoryStoreError>
    {
        Ok(Vec::new())
    }
}

struct ParamsCaptureClient {
    captured: Mutex<serde_json::Value>,
}

impl ParamsCaptureClient {
    fn new() -> Self {
        Self {
            captured: Mutex::new(serde_json::Value::Null),
        }
    }

    fn captured_params(&self) -> Option<serde_json::Value> {
        let value = self.captured.lock().expect("capture mutex").clone();
        if value.is_null() { None } else { Some(value) }
    }
}

#[async_trait]
impl LlmClient for ParamsCaptureClient {
    fn project_replay_messages(
        &self,
        messages: &[meerkat_core::Message],
    ) -> Result<Vec<meerkat_core::Message>, meerkat_client::LlmError> {
        Ok(messages.to_vec())
    }

    fn stream<'a>(
        &'a self,
        request: &'a LlmRequest,
    ) -> std::pin::Pin<
        Box<dyn futures::Stream<Item = Result<LlmEvent, meerkat_client::LlmError>> + Send + 'a>,
    > {
        *self.captured.lock().expect("capture mutex") = request
            .provider_params
            .as_ref()
            .map(|tag| serde_json::to_value(tag).unwrap_or(serde_json::json!({})))
            .unwrap_or(serde_json::json!({}));
        Box::pin(stream::iter(vec![
            Ok(LlmEvent::TextDelta {
                delta: "ok".to_string(),
                meta: None,
            }),
            Ok(LlmEvent::Done {
                outcome: LlmDoneOutcome::Success {
                    stop_reason: meerkat::StopReason::EndTurn,
                },
            }),
        ]))
    }

    fn provider(&self) -> &'static str {
        "anthropic"
    }

    async fn health_check(&self) -> Result<(), meerkat_client::LlmError> {
        Ok(())
    }
}

struct CountingHookEngine {
    executions: Arc<AtomicUsize>,
}

#[async_trait]
impl HookEngine for CountingHookEngine {
    fn matching_hooks(
        &self,
        _invocation: &HookInvocation,
        _overrides: Option<&meerkat::HookRunOverrides>,
    ) -> Result<Vec<HookId>, HookEngineError> {
        Ok(vec![HookId("counting-hook".to_string())])
    }

    async fn execute(
        &self,
        _invocation: HookInvocation,
        _overrides: Option<&meerkat::HookRunOverrides>,
    ) -> Result<HookExecutionReport, HookEngineError> {
        self.executions.fetch_add(1, Ordering::SeqCst);
        Ok(HookExecutionReport::empty())
    }
}

#[tokio::test]
async fn test_sdk_agentfactory_tool_dispatch() {
    let calls = Arc::new(AtomicUsize::new(0));
    let dispatcher_called = Arc::new(AtomicBool::new(false));

    let factory = AgentFactory::new(".rkat/sessions");
    let llm_client = Arc::new(MockLlmClient { calls });
    let llm_adapter = Arc::new(
        factory
            .build_llm_adapter(llm_client, "claude-sonnet-4-6")
            .await,
    );

    let store = Arc::new(TestSessionStore::new());
    let store_adapter = Arc::new(factory.build_store_adapter(store).await);

    let tools: Arc<dyn AgentToolDispatcher> = Arc::new(RecordingDispatcher {
        called: dispatcher_called.clone(),
    });

    let mut agent = AgentBuilder::new()
        .model("claude-sonnet-4-6")
        .max_tokens_per_turn(64)
        .build(llm_adapter, tools, store_adapter)
        .await
        .expect("public builder build");

    let result = agent
        .run("Use the echo tool.".to_string().into())
        .await
        .expect("agent run");

    assert!(
        dispatcher_called.load(Ordering::SeqCst),
        "tool should be dispatched"
    );
    assert!(
        result.text.contains("done"),
        "agent should complete after tool call"
    );
}

#[tokio::test]
async fn public_agentbuilder_matches_factory_session_runtime_invariants() {
    let temp = tempfile::tempdir().expect("tempdir");
    let factory = AgentFactory::new(temp.path().join("sessions"));
    let config = Config::default();
    let model = "claude-sonnet-4-6";
    let prompt = "builder parity prompt";

    let factory_client = Arc::new(MockLlmClient {
        calls: Arc::new(AtomicUsize::new(0)),
    });
    let factory_tools: Arc<dyn AgentToolDispatcher> = Arc::new(RecordingDispatcher {
        called: Arc::new(AtomicBool::new(false)),
    });
    let factory_store = Arc::new(TestSessionStore::new());
    let factory_store = Arc::new(factory.build_store_adapter(factory_store).await);
    let factory_agent = factory
        .build_agent(
            AgentBuildConfig {
                max_tokens: Some(64),
                system_prompt: Some(prompt.to_string()),
                llm_client_override: Some(factory_client),
                tool_dispatcher_override: Some(factory_tools),
                session_store_override: Some(factory_store),
                ..AgentBuildConfig::new(model)
            },
            &config,
        )
        .await
        .expect("factory build");

    let public_client = Arc::new(MockLlmClient {
        calls: Arc::new(AtomicUsize::new(0)),
    });
    let public_llm = Arc::new(factory.build_llm_adapter(public_client, model).await);
    let public_tools: Arc<dyn AgentToolDispatcher> = Arc::new(RecordingDispatcher {
        called: Arc::new(AtomicBool::new(false)),
    });
    let public_store = Arc::new(TestSessionStore::new());
    let public_store = Arc::new(factory.build_store_adapter(public_store).await);

    let public_agent = AgentBuilder::new()
        .model(model)
        .max_tokens_per_turn(64)
        .system_prompt(prompt)
        .build(public_llm, public_tools, public_store)
        .await
        .expect("public builder build");

    let factory_metadata = factory_agent
        .session()
        .session_metadata()
        .expect("factory session metadata");
    let public_metadata = public_agent
        .session()
        .session_metadata()
        .expect("public builder should persist factory session metadata");
    assert_eq!(public_metadata.model, factory_metadata.model);
    assert_eq!(public_metadata.provider, factory_metadata.provider);
    assert_eq!(public_metadata.max_tokens, factory_metadata.max_tokens);
    assert_eq!(
        public_metadata.structured_output_retries,
        factory_metadata.structured_output_retries
    );

    let factory_build_state = factory_agent
        .session()
        .build_state()
        .expect("factory build state");
    let public_build_state = public_agent
        .session()
        .build_state()
        .expect("public builder should persist factory build state");
    assert_eq!(
        public_build_state.system_prompt,
        factory_build_state.system_prompt
    );
    assert_eq!(
        public_build_state.call_timeout_override,
        factory_build_state.call_timeout_override
    );

    assert!(
        factory_agent.execution_snapshot().is_some(),
        "factory path wires runtime turn-state"
    );
    assert!(
        public_agent.execution_snapshot().is_some(),
        "public AgentBuilder must wire the same runtime turn-state default"
    );
}

#[tokio::test]
async fn public_agentbuilder_uses_factory_provider_defaults() {
    let temp = tempfile::tempdir().expect("tempdir");
    let client = Arc::new(ParamsCaptureClient::new());
    let override_client: Arc<dyn LlmClient> = client.clone();
    let mut agent = AgentBuilder::new()
        .with_factory(AgentFactory::new(temp.path().join("sessions")))
        .with_config(Config::default())
        .model("claude-sonnet-4-6")
        .llm_client(override_client)
        .try_build()
        .await
        .expect("public builder build");

    agent.run("test".to_string().into()).await.expect("run");

    let params = client
        .captured_params()
        .expect("factory provider defaults should produce provider params");
    assert!(
        params.get("web_search").is_some(),
        "public AgentBuilder should inherit factory provider defaults: {params}"
    );
}

#[tokio::test]
async fn public_agentbuilder_routes_hook_engine_through_factory() {
    let temp = tempfile::tempdir().expect("tempdir");
    let executions = Arc::new(AtomicUsize::new(0));
    let hook_engine = Arc::new(CountingHookEngine {
        executions: Arc::clone(&executions),
    });
    let client: Arc<dyn LlmClient> = Arc::new(MockLlmClient {
        calls: Arc::new(AtomicUsize::new(1)),
    });

    let mut agent = AgentBuilder::new()
        .with_factory(AgentFactory::new(temp.path().join("sessions")))
        .with_config(Config::default())
        .model("claude-sonnet-4-6")
        .llm_client(client)
        .with_hook_engine(hook_engine)
        .try_build()
        .await
        .expect("public builder build");

    agent.run("test".to_string().into()).await.expect("run");
    assert!(
        executions.load(Ordering::SeqCst) > 0,
        "public AgentBuilder should route hook engine through factory build"
    );
}

#[tokio::test]
async fn public_agentbuilder_with_config_supplies_default_model() {
    let temp = tempfile::tempdir().expect("tempdir");
    let mut config = Config::default();
    config.agent.model = "claude-sonnet-4-6".to_string();
    let client: Arc<dyn LlmClient> = Arc::new(MockLlmClient {
        calls: Arc::new(AtomicUsize::new(1)),
    });

    let agent = AgentBuilder::new()
        .with_factory(AgentFactory::new(temp.path().join("sessions")))
        .with_config(config)
        .llm_client(client)
        .try_build()
        .await
        .expect("public builder build");

    let metadata = agent
        .session()
        .session_metadata()
        .expect("factory session metadata");
    assert_eq!(
        metadata.model, "claude-sonnet-4-6",
        "with_config should update the default build model unless .model() was explicit"
    );
}

#[tokio::test]
async fn public_agentbuilder_preserves_agent_llm_image_blocks() {
    let temp = tempfile::tempdir().expect("tempdir");
    let factory = AgentFactory::new(temp.path().join("sessions"));
    let image_client: Arc<dyn AgentLlmClient> = Arc::new(ImageAgentLlmClient);
    let tools: Arc<dyn AgentToolDispatcher> = Arc::new(RecordingDispatcher {
        called: Arc::new(AtomicBool::new(false)),
    });
    let store = Arc::new(TestSessionStore::new());
    let store = Arc::new(factory.build_store_adapter(store).await);

    let mut agent = AgentBuilder::new()
        .with_factory(factory)
        .model("claude-sonnet-4-6")
        .build(image_client, tools, store)
        .await
        .expect("public builder build");

    agent.run("return an image".into()).await.expect("run");

    let image_block = agent.session().messages().iter().find_map(|message| {
        if let Message::BlockAssistant(assistant) = message {
            assistant.blocks.iter().find_map(|block| {
                if let AssistantBlock::Image { blob_ref, .. } = block {
                    Some(blob_ref.clone())
                } else {
                    None
                }
            })
        } else {
            None
        }
    });

    assert_eq!(
        image_block
            .expect("image block should be persisted")
            .blob_id,
        BlobId::new("image-blob-124"),
        "public builder compatibility path must preserve AgentLlmClient image blocks"
    );
}

#[tokio::test]
async fn public_agentbuilder_build_uses_agent_llm_provider_for_uncatalogued_model() {
    let temp = tempfile::tempdir().expect("tempdir");
    let factory = AgentFactory::new(temp.path().join("sessions"));
    let custom_client: Arc<dyn AgentLlmClient> = Arc::new(CustomAgentLlmClient);
    let tools: Arc<dyn AgentToolDispatcher> = Arc::new(RecordingDispatcher {
        called: Arc::new(AtomicBool::new(false)),
    });
    let store = Arc::new(TestSessionStore::new());
    let store = Arc::new(factory.build_store_adapter(store).await);

    let agent = AgentBuilder::new()
        .with_factory(factory)
        .model("custom-agent-model")
        .build(custom_client, tools, store)
        .await
        .expect("public builder should accept AgentLlmClient provider identity");

    let metadata = agent
        .session()
        .session_metadata()
        .expect("factory metadata");
    assert_eq!(metadata.provider, Provider::Other);
    assert_eq!(metadata.model, "custom-agent-model");
}

#[tokio::test]
async fn public_agentbuilder_build_defaults_to_agent_llm_identity_when_model_is_not_explicit() {
    let temp = tempfile::tempdir().expect("tempdir");
    let factory = AgentFactory::new(temp.path().join("sessions"));
    let custom_client: Arc<dyn AgentLlmClient> = Arc::new(CustomAgentLlmClient);
    let tools: Arc<dyn AgentToolDispatcher> = Arc::new(RecordingDispatcher {
        called: Arc::new(AtomicBool::new(false)),
    });
    let store = Arc::new(TestSessionStore::new());
    let store = Arc::new(factory.build_store_adapter(store).await);

    let agent = AgentBuilder::new()
        .with_factory(factory)
        .build(custom_client, tools, store)
        .await
        .expect("public builder should derive default identity from AgentLlmClient");

    let metadata = agent
        .session()
        .session_metadata()
        .expect("factory metadata");
    assert_eq!(metadata.provider, Provider::Other);
    assert_eq!(
        metadata.model, "custom-agent-model",
        "implicit public builder model should not fall back to Config::default() when an AgentLlmClient override supplies identity"
    );
}

fn assert_unsupported_builder_injection(error: BuildAgentError, method: &str) {
    let BuildAgentError::Config(message) = error else {
        panic!("expected Config error for {method}, got {error:?}");
    };
    assert!(
        message.contains(method),
        "error should name unsupported method {method}: {message}"
    );
    assert!(
        message.contains("AgentFactory/AgentBuildConfig"),
        "error should point callers to factory-owned settings: {message}"
    );
}

fn unwrap_build_error(
    result: Result<meerkat::DynAgent, BuildAgentError>,
    context: &str,
) -> BuildAgentError {
    match result {
        Ok(_) => panic!("{context}"),
        Err(error) => error,
    }
}

#[tokio::test]
async fn public_agentbuilder_rejects_standalone_core_injections_loudly() {
    let temp = tempfile::tempdir().expect("tempdir");
    let factory = AgentFactory::new(temp.path().join("sessions"));
    let client: Arc<dyn LlmClient> = Arc::new(MockLlmClient {
        calls: Arc::new(AtomicUsize::new(1)),
    });

    let provider_defaults_error = AgentBuilder::new()
        .with_factory(factory.clone())
        .model("claude-sonnet-4-6")
        .llm_client(Arc::clone(&client))
        .provider_tool_defaults(json!({"web_search": {"type": "custom"}}))
        .try_build()
        .await;
    let provider_defaults_error = unwrap_build_error(
        provider_defaults_error,
        "provider_tool_defaults should not be silently ignored",
    );
    assert_unsupported_builder_injection(provider_defaults_error, "provider_tool_defaults");

    let compactor_error = AgentBuilder::new()
        .with_factory(factory.clone())
        .model("claude-sonnet-4-6")
        .llm_client(Arc::clone(&client))
        .compactor(Arc::new(NoopCompactor))
        .try_build()
        .await;
    let compactor_error =
        unwrap_build_error(compactor_error, "compactor should not be silently ignored");
    assert_unsupported_builder_injection(compactor_error, "compactor");

    let memory_error = AgentBuilder::new()
        .with_factory(factory.clone())
        .model("claude-sonnet-4-6")
        .llm_client(Arc::clone(&client))
        .memory_store(Arc::new(NoopMemoryStore))
        .try_build()
        .await;
    let memory_error =
        unwrap_build_error(memory_error, "memory_store should not be silently ignored");
    assert_unsupported_builder_injection(memory_error, "memory_store");

    let turn_state_error = AgentBuilder::new()
        .with_factory(factory)
        .model("claude-sonnet-4-6")
        .llm_client(client)
        .with_turn_state_handle(Arc::new(
            meerkat_runtime::RuntimeTurnStateHandle::ephemeral(),
        ))
        .try_build()
        .await;
    let turn_state_error = unwrap_build_error(
        turn_state_error,
        "with_turn_state_handle should not be silently ignored",
    );
    assert_unsupported_builder_injection(turn_state_error, "with_turn_state_handle");
}

#[test]
fn public_agentfactory_value_is_not_core_policy_authority() {
    let mut builder = std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR"));
    builder.pop();
    builder.push("meerkat-core/src/agent/builder.rs");
    let builder = std::fs::read_to_string(&builder).expect("read core AgentBuilder implementation");

    assert!(
        builder.contains("validate_factory_policy()?")
            && !builder.contains("std::panic::Location::caller")
            && !builder.contains("validate_factory_policy_caller")
            && !builder.contains("canonical_factory_source_path")
            && !builder.contains("pub struct AgentFactoryPolicyAuthority")
            && !builder.contains("pub fn from_registered_source<A: 'static>")
            && !builder.contains("pub fn agent_factory_policy_authority"),
        "core factory-policy build must validate factory-composed policy state \
         without trusting runtime source paths or exposing a public core \
         authority type or minting helper"
    );
}
