use anyhow::Result;
use async_trait::async_trait;
use vtcode_core::core::interfaces::acp::{AcpClientAdapter, AcpLaunchParams};

mod agent;
pub(crate) mod constants;
mod helpers;
mod session;
mod types;

pub(crate) use agent::ZedAgent;
use session::run_acp_agent;

#[derive(Debug, Default, Clone, Copy)]
pub struct ZedAcpAdapter;

#[async_trait(?Send)]
impl AcpClientAdapter for ZedAcpAdapter {
    async fn serve(&self, params: AcpLaunchParams<'_>) -> Result<()> {
        run_acp_agent(
            params.agent_config,
            params.runtime_config,
            Some("Zed".to_string()),
        )
        .await
    }
}

#[derive(Debug, Default, Clone, Copy)]
pub struct StandardAcpAdapter;

#[async_trait(?Send)]
impl AcpClientAdapter for StandardAcpAdapter {
    async fn serve(&self, params: AcpLaunchParams<'_>) -> Result<()> {
        run_acp_agent(params.agent_config, params.runtime_config, None).await
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::tooling::{
        TOOL_LIST_FILES_ITEMS_KEY, TOOL_LIST_FILES_RESULT_KEY, TOOL_LIST_FILES_URI_ARG,
    };
    use crate::zed::helpers::{
        SESSION_CONFIG_MODE_ID, SESSION_CONFIG_MODEL_ID, SESSION_CONFIG_PROVIDER_ID,
        SESSION_CONFIG_THOUGHT_LEVEL_ID,
    };
    use crate::zed::types::NotificationEnvelope;
    use agent_client_protocol::{
        Agent, LoadSessionRequest, NewSessionRequest, SessionConfigKind, SessionModeId,
        SetSessionConfigOptionRequest, ToolCallStatus,
    };
    use assert_fs::TempDir;
    use serde_json::{Value, json};
    use std::collections::BTreeMap;
    use std::path::Path;
    use tokio::fs;
    use tokio::sync::mpsc;
    use vtcode_core::config::core::PromptCachingConfig;
    use vtcode_core::config::models::{ModelId, Provider};
    use vtcode_core::config::types::{
        AgentConfig as CoreAgentConfig, ModelSelectionSource, ReasoningEffortLevel,
        UiSurfacePreference,
    };
    use vtcode_core::config::{AgentClientProtocolZedConfig, CommandsConfig, ToolsConfig};
    use vtcode_core::core::agent::snapshots::{
        DEFAULT_CHECKPOINTS_ENABLED, DEFAULT_MAX_AGE_DAYS, DEFAULT_MAX_SNAPSHOTS,
    };
    use vtcode_core::core::interfaces::SessionMode;

    async fn build_agent(workspace: &Path) -> ZedAgent {
        let core_config = CoreAgentConfig {
            model: "gpt-5.4".to_string(),
            api_key: String::new(),
            provider: "openai".to_string(),
            api_key_env: "TEST_API_KEY".to_string(),
            workspace: workspace.to_path_buf(),
            verbose: false,
            quiet: false,
            theme: "test".to_string(),
            reasoning_effort: ReasoningEffortLevel::Low,
            ui_surface: UiSurfacePreference::default(),
            prompt_cache: PromptCachingConfig::default(),
            model_source: ModelSelectionSource::WorkspaceConfig,
            custom_api_keys: BTreeMap::new(),
            checkpointing_enabled: DEFAULT_CHECKPOINTS_ENABLED,
            checkpointing_storage_dir: None,
            checkpointing_max_snapshots: DEFAULT_MAX_SNAPSHOTS,
            checkpointing_max_age_days: Some(DEFAULT_MAX_AGE_DAYS),
            max_conversation_turns: 1000,
            model_behavior: None,
            openai_chatgpt_auth: None,
        };

        let mut zed_config = AgentClientProtocolZedConfig::default();
        zed_config.tools.list_files = true;
        zed_config.tools.read_file = false;

        let tools_config = ToolsConfig::default();
        let (tx, mut rx) = mpsc::unbounded_channel::<NotificationEnvelope>();
        // Spawn a task to handle session updates so send_update doesn't fail
        tokio::spawn(async move {
            while let Some(envelope) = rx.recv().await {
                let _ = envelope.completion.send(());
            }
        });

        ZedAgent::new(
            core_config,
            zed_config,
            tools_config,
            CommandsConfig::default(),
            String::new(),
            tx,
            Some("Zed".to_string()),
        )
        .await
    }

    fn list_items_from_payload(payload: &Value) -> Vec<Value> {
        payload
            .get(TOOL_LIST_FILES_RESULT_KEY)
            .and_then(Value::as_object)
            .and_then(|result| result.get(TOOL_LIST_FILES_ITEMS_KEY))
            .and_then(Value::as_array)
            .cloned()
            .unwrap_or_default()
    }

    #[tokio::test]
    async fn run_list_files_defaults_to_workspace_root() {
        let temp = TempDir::new().unwrap();
        let subdir = temp.path().join("src");
        fs::create_dir(&subdir).await.unwrap();
        let file_path = subdir.join("example.txt");
        fs::write(&file_path, "hello").await.unwrap();

        let agent = build_agent(temp.path()).await;
        let report = agent.run_list_files(&json!({"path": "src"})).await.unwrap();

        assert!(matches!(report.status, ToolCallStatus::Completed));
        let payload = report.raw_output.unwrap();
        let items = list_items_from_payload(&payload);
        assert!(items.iter().any(|item| {
            item.get("name")
                .and_then(Value::as_str)
                .map(|name| name == "example.txt")
                .unwrap_or(false)
        }));
    }

    #[tokio::test]
    async fn run_list_files_accepts_uri_argument() {
        let temp = TempDir::new().unwrap();
        let nested = temp.path().join("nested");
        fs::create_dir_all(&nested).await.unwrap();
        let inner = nested.join("inner.txt");
        fs::write(&inner, "data").await.unwrap();

        let agent = build_agent(temp.path()).await;
        let uri = format!("file://{}", nested.to_string_lossy());
        let report = agent
            .run_list_files(&json!({ TOOL_LIST_FILES_URI_ARG: uri }))
            .await
            .unwrap();

        assert!(matches!(report.status, ToolCallStatus::Completed));
        let payload = report.raw_output.unwrap();
        let items = list_items_from_payload(&payload);
        assert!(items.iter().any(|item| {
            item.get("path")
                .and_then(Value::as_str)
                .map(|path| path.contains("inner.txt"))
                .unwrap_or(false)
        }));
    }

    #[tokio::test]
    async fn load_session_returns_existing_session_state() {
        let temp = TempDir::new().unwrap();
        let agent = build_agent(temp.path()).await;
        let session_id = agent.register_session();

        // Change the mode of the registered session
        {
            let session = agent.session_handle(&session_id).unwrap();
            let mut data = session.data.borrow_mut();
            data.current_mode = SessionMode::Architect;
            data.reasoning_effort = ReasoningEffortLevel::High;
        }

        let args = LoadSessionRequest::new(session_id, temp.path());
        let response = agent.load_session(args).await.unwrap();

        assert_eq!(
            response.modes.unwrap().current_mode_id,
            SessionModeId::new("architect")
        );
        let config_options = response.config_options.unwrap();
        assert_eq!(config_options.len(), 4);
        assert!(config_options.iter().any(|option| {
            option.id == crate::acp::SessionConfigId::new(SESSION_CONFIG_MODE_ID)
                && matches!(
                    &option.kind,
                    SessionConfigKind::Select(select)
                        if select.current_value
                            == crate::acp::SessionConfigValueId::new("architect")
                )
        }));
        assert!(config_options.iter().any(|option| {
            option.id == crate::acp::SessionConfigId::new(SESSION_CONFIG_THOUGHT_LEVEL_ID)
                && matches!(
                    &option.kind,
                    SessionConfigKind::Select(select)
                        if select.current_value
                            == crate::acp::SessionConfigValueId::new("high")
                )
        }));
        assert!(config_options.iter().any(|option| {
            option.id == crate::acp::SessionConfigId::new(SESSION_CONFIG_PROVIDER_ID)
                && matches!(
                    &option.kind,
                    SessionConfigKind::Select(select)
                        if select.current_value
                            == crate::acp::SessionConfigValueId::new("openai")
                )
        }));
        assert!(config_options.iter().any(|option| {
            option.id == crate::acp::SessionConfigId::new(SESSION_CONFIG_MODEL_ID)
                && matches!(
                    &option.kind,
                    SessionConfigKind::Select(select)
                        if select.current_value
                            == crate::acp::SessionConfigValueId::new("gpt-5.4")
                )
        }));
    }

    #[tokio::test]
    async fn new_session_returns_config_options() {
        let temp = TempDir::new().unwrap();
        let agent = build_agent(temp.path()).await;

        let response = agent
            .new_session(NewSessionRequest::new(temp.path()))
            .await
            .unwrap();

        assert_eq!(
            response.modes.unwrap().current_mode_id,
            SessionModeId::new("code")
        );
        let config_options = response.config_options.unwrap();
        assert_eq!(config_options.len(), 4);
        assert!(config_options.iter().any(|option| {
            option.id == crate::acp::SessionConfigId::new(SESSION_CONFIG_MODE_ID)
                && matches!(
                    &option.kind,
                    SessionConfigKind::Select(select)
                        if select.current_value
                            == crate::acp::SessionConfigValueId::new("code")
                )
        }));
        assert!(config_options.iter().any(|option| {
            option.id == crate::acp::SessionConfigId::new(SESSION_CONFIG_THOUGHT_LEVEL_ID)
                && matches!(
                    &option.kind,
                    SessionConfigKind::Select(select)
                        if select.current_value
                            == crate::acp::SessionConfigValueId::new("low")
                )
        }));
        assert!(config_options.iter().any(|option| {
            option.id == crate::acp::SessionConfigId::new(SESSION_CONFIG_PROVIDER_ID)
                && matches!(
                    &option.kind,
                    SessionConfigKind::Select(select)
                        if select.current_value
                            == crate::acp::SessionConfigValueId::new("openai")
                )
        }));
        assert!(config_options.iter().any(|option| {
            option.id == crate::acp::SessionConfigId::new(SESSION_CONFIG_MODEL_ID)
                && matches!(
                    &option.kind,
                    SessionConfigKind::Select(select)
                        if select.current_value
                            == crate::acp::SessionConfigValueId::new("gpt-5.4")
                )
        }));
    }

    #[tokio::test]
    async fn set_session_config_option_updates_mode() {
        let temp = TempDir::new().unwrap();
        let agent = build_agent(temp.path()).await;
        let session_id = agent.register_session();

        let response = agent
            .set_session_config_option(SetSessionConfigOptionRequest::new(
                session_id.clone(),
                SESSION_CONFIG_MODE_ID,
                "architect",
            ))
            .await
            .unwrap();

        let session = agent.session_handle(&session_id).unwrap();
        assert_eq!(session.data.borrow().current_mode, SessionMode::Architect);
        assert!(response.config_options.iter().any(|option| {
            option.id == crate::acp::SessionConfigId::new(SESSION_CONFIG_MODE_ID)
                && matches!(
                    &option.kind,
                    SessionConfigKind::Select(select)
                        if select.current_value
                            == crate::acp::SessionConfigValueId::new("architect")
                )
        }));
    }

    #[tokio::test]
    async fn set_session_config_option_updates_reasoning_effort() {
        let temp = TempDir::new().unwrap();
        let agent = build_agent(temp.path()).await;
        let session_id = agent.register_session();

        let response = agent
            .set_session_config_option(SetSessionConfigOptionRequest::new(
                session_id.clone(),
                SESSION_CONFIG_THOUGHT_LEVEL_ID,
                "xhigh",
            ))
            .await
            .unwrap();

        let session = agent.session_handle(&session_id).unwrap();
        assert_eq!(
            session.data.borrow().reasoning_effort,
            ReasoningEffortLevel::XHigh
        );
        assert!(response.config_options.iter().any(|option| {
            option.id == crate::acp::SessionConfigId::new(SESSION_CONFIG_THOUGHT_LEVEL_ID)
                && matches!(
                    &option.kind,
                    SessionConfigKind::Select(select)
                        if select.current_value
                            == crate::acp::SessionConfigValueId::new("xhigh")
                )
        }));
    }

    #[tokio::test]
    async fn set_session_config_option_updates_provider_and_auto_switches_model() {
        let temp = TempDir::new().unwrap();
        let agent = build_agent(temp.path()).await;
        let session_id = agent.register_session();
        let anthropic_default = ModelId::default_single_for_provider(Provider::Anthropic).as_str();

        let response = agent
            .set_session_config_option(SetSessionConfigOptionRequest::new(
                session_id.clone(),
                SESSION_CONFIG_PROVIDER_ID,
                "anthropic",
            ))
            .await
            .unwrap();

        let session = agent.session_handle(&session_id).unwrap();
        assert_eq!(session.data.borrow().provider, "anthropic");
        assert_eq!(session.data.borrow().model, anthropic_default);
        assert!(response.config_options.iter().any(|option| {
            option.id == crate::acp::SessionConfigId::new(SESSION_CONFIG_PROVIDER_ID)
                && matches!(
                    &option.kind,
                    SessionConfigKind::Select(select)
                        if select.current_value
                            == crate::acp::SessionConfigValueId::new("anthropic")
                )
        }));
        assert!(response.config_options.iter().any(|option| {
            option.id == crate::acp::SessionConfigId::new(SESSION_CONFIG_MODEL_ID)
                && matches!(
                    &option.kind,
                    SessionConfigKind::Select(select)
                        if select.current_value
                            == crate::acp::SessionConfigValueId::new(anthropic_default)
                )
        }));
    }

    #[tokio::test]
    async fn set_session_config_option_updates_model_for_provider() {
        let temp = TempDir::new().unwrap();
        let agent = build_agent(temp.path()).await;
        let session_id = agent.register_session();

        let response = agent
            .set_session_config_option(SetSessionConfigOptionRequest::new(
                session_id.clone(),
                SESSION_CONFIG_MODEL_ID,
                "gpt-5.4-mini",
            ))
            .await
            .unwrap();

        let session = agent.session_handle(&session_id).unwrap();
        assert_eq!(session.data.borrow().provider, "openai");
        assert_eq!(session.data.borrow().model, "gpt-5.4-mini");
        assert!(response.config_options.iter().any(|option| {
            option.id == crate::acp::SessionConfigId::new(SESSION_CONFIG_MODEL_ID)
                && matches!(
                    &option.kind,
                    SessionConfigKind::Select(select)
                        if select.current_value
                            == crate::acp::SessionConfigValueId::new("gpt-5.4-mini")
                )
        }));
    }

    #[tokio::test]
    async fn run_switch_mode_updates_session_mode() {
        let temp = TempDir::new().unwrap();
        let agent = build_agent(temp.path()).await;
        let session_id = agent.register_session();

        // Verify initial mode is "code" (default in register_session)
        {
            let session = agent.session_handle(&session_id).unwrap();
            assert_eq!(session.data.borrow().current_mode, SessionMode::Code);
        }

        // Switch to "architect"
        let args = json!({ "mode_id": "architect" });
        let report = agent.run_switch_mode(&session_id, &args).await.unwrap();

        assert!(matches!(report.status, ToolCallStatus::Completed));

        // Verify session mode was updated
        {
            let session = agent.session_handle(&session_id).unwrap();
            assert_eq!(session.data.borrow().current_mode, SessionMode::Architect);
        }
    }
}
