#[allow(dead_code)]
#[path = "acp_common_tests/mod.rs"]
mod common_tests;

use common_tests::fixtures::server::AcpServerConnection;
use common_tests::fixtures::{
    run_test, send_custom, Connection, PermissionDecision, Session, SessionData,
    TestConnectionConfig,
};
use goose::acp::server::AcpProviderFactory;
use goose::model::ModelConfig;
use goose::providers::base::{MessageStream, Provider};
use goose::providers::errors::ProviderError;
use goose_test_support::{EnforceSessionId, IgnoreSessionId};
use std::path::PathBuf;
use std::sync::{Arc, Mutex};

use common_tests::fixtures::OpenAiFixture;

struct MockProvider {
    name: String,
    model_config: ModelConfig,
    recommended_models: Vec<String>,
    supported_models: Vec<String>,
}

#[async_trait::async_trait]
impl Provider for MockProvider {
    fn get_name(&self) -> &str {
        &self.name
    }

    async fn stream(
        &self,
        _model_config: &ModelConfig,
        _session_id: &str,
        _system: &str,
        _messages: &[goose::conversation::message::Message],
        _tools: &[rmcp::model::Tool],
    ) -> Result<MessageStream, ProviderError> {
        unimplemented!()
    }

    fn get_model_config(&self) -> ModelConfig {
        self.model_config.clone()
    }

    async fn fetch_recommended_models(&self) -> Result<Vec<String>, ProviderError> {
        Ok(self.recommended_models.clone())
    }

    async fn fetch_supported_models(&self) -> Result<Vec<String>, ProviderError> {
        Ok(self.supported_models.clone())
    }
}

fn mock_provider_factory() -> AcpProviderFactory {
    Arc::new(|provider_name, model_config, _extensions, _working_dir| {
        Box::pin(async move {
            let recommended_models = match provider_name.as_str() {
                "anthropic" => vec![
                    "claude-3-7-sonnet-latest".to_string(),
                    "claude-3-5-haiku-latest".to_string(),
                ],
                _ => vec!["gpt-4o".to_string(), "o4-mini".to_string()],
            };
            Ok(Arc::new(MockProvider {
                name: provider_name,
                model_config,
                supported_models: recommended_models.clone(),
                recommended_models,
            }) as Arc<dyn Provider>)
        })
    })
}

#[test]
fn test_custom_get_tools() {
    run_test(async move {
        let openai = OpenAiFixture::new(vec![], Arc::new(EnforceSessionId::default())).await;
        let mut conn = AcpServerConnection::new(TestConnectionConfig::default(), openai).await;

        let SessionData { session, .. } = conn.new_session().await.unwrap();
        let session_id = session.session_id().0.clone();

        let result = send_custom(
            conn.cx(),
            "_goose/unstable/tools/list",
            serde_json::json!({ "sessionId": session_id }),
        )
        .await;
        assert!(result.is_ok(), "expected ok, got: {:?}", result);

        let response = result.unwrap();
        let tools = response.get("tools").expect("missing 'tools' field");
        assert!(tools.is_array(), "tools should be array");
    });
}

#[test]
fn test_custom_get_extensions() {
    run_test(async move {
        let openai = OpenAiFixture::new(vec![], Arc::new(EnforceSessionId::default())).await;
        let conn = AcpServerConnection::new(TestConnectionConfig::default(), openai).await;

        let result = send_custom(
            conn.cx(),
            "_goose/unstable/config/extensions/list",
            serde_json::json!({}),
        )
        .await;
        assert!(result.is_ok(), "expected ok, got: {:?}", result);

        let response = result.unwrap();
        assert!(
            response.get("extensions").is_some(),
            "missing 'extensions' field"
        );
        assert!(
            response.get("warnings").is_some(),
            "missing 'warnings' field"
        );
    });
}

#[test]
fn test_new_session_passes_cwd_to_provider_factory() {
    run_test(async move {
        let openai = OpenAiFixture::new(vec![], Arc::new(EnforceSessionId::default())).await;
        let cwd = tempfile::tempdir().unwrap();
        let expected_cwd = cwd.path().to_path_buf();
        let captured_cwds = Arc::new(Mutex::new(Vec::<Option<PathBuf>>::new()));
        let factory_cwds = Arc::clone(&captured_cwds);
        let provider_factory: AcpProviderFactory = Arc::new(
            move |provider_name, model_config, _extensions, working_dir| {
                factory_cwds.lock().unwrap().push(working_dir);
                Box::pin(async move {
                    Ok(Arc::new(MockProvider {
                        name: provider_name,
                        model_config,
                        recommended_models: Vec::new(),
                        supported_models: Vec::new(),
                    }) as Arc<dyn Provider>)
                })
            },
        );

        let mut conn = AcpServerConnection::new(
            TestConnectionConfig {
                cwd: Some(cwd),
                provider_factory: Some(provider_factory),
                ..Default::default()
            },
            openai,
        )
        .await;

        conn.new_session().await.unwrap();

        let captured_cwd = tokio::time::timeout(std::time::Duration::from_secs(1), async {
            loop {
                if let Some(cwd) = captured_cwds.lock().unwrap().first().cloned() {
                    break cwd;
                }
                tokio::time::sleep(std::time::Duration::from_millis(10)).await;
            }
        })
        .await
        .expect("provider factory was not called");

        assert_eq!(captured_cwd, Some(expected_cwd));
    });
}

#[test]
fn test_load_session_passes_load_cwd_to_provider_factory() {
    run_test(async move {
        let openai = OpenAiFixture::new(vec![], Arc::new(EnforceSessionId::default())).await;
        let initial_cwd = tempfile::tempdir().unwrap();
        let captured_cwds = Arc::new(Mutex::new(Vec::<Option<PathBuf>>::new()));
        let factory_cwds = Arc::clone(&captured_cwds);
        let provider_factory: AcpProviderFactory = Arc::new(
            move |provider_name, model_config, _extensions, working_dir| {
                factory_cwds.lock().unwrap().push(working_dir);
                Box::pin(async move {
                    Ok(Arc::new(MockProvider {
                        name: provider_name,
                        model_config,
                        recommended_models: Vec::new(),
                        supported_models: Vec::new(),
                    }) as Arc<dyn Provider>)
                })
            },
        );

        let mut conn = AcpServerConnection::new(
            TestConnectionConfig {
                cwd: Some(initial_cwd),
                provider_factory: Some(provider_factory),
                ..Default::default()
            },
            openai,
        )
        .await;

        let SessionData { session, .. } = conn.new_session().await.unwrap();
        let session_id = session.session_id().0.to_string();
        let SessionData {
            session: loaded, ..
        } = conn.load_session(&session_id, vec![]).await.unwrap();
        let expected_cwd = loaded.work_dir();

        let captured_cwd = tokio::time::timeout(std::time::Duration::from_secs(1), async {
            loop {
                if let Some(cwd) = captured_cwds.lock().unwrap().get(1).cloned() {
                    break cwd;
                }
                tokio::time::sleep(std::time::Duration::from_millis(10)).await;
            }
        })
        .await
        .expect("provider factory was not called for load session");

        assert_eq!(captured_cwd, Some(expected_cwd));
    });
}

#[test]
fn test_custom_list_builtin_skill_sources() {
    run_test(async move {
        let openai = OpenAiFixture::new(vec![], Arc::new(EnforceSessionId::default())).await;
        let conn = AcpServerConnection::new(TestConnectionConfig::default(), openai).await;

        let response = send_custom(
            conn.cx(),
            "_goose/unstable/sources/list",
            serde_json::json!({ "type": "builtinSkill" }),
        )
        .await
        .expect("builtin skill sources list should succeed");
        let sources = response
            .get("sources")
            .and_then(|value| value.as_array())
            .expect("missing sources array");
        let builtin = sources
            .iter()
            .find(|source| source.get("name") == Some(&serde_json::json!("goose-doc-guide")))
            .expect("expected goose-doc-guide builtin skill");

        assert_eq!(
            builtin.get("type"),
            Some(&serde_json::json!("builtinSkill"))
        );
        assert_eq!(builtin.get("global"), Some(&serde_json::json!(true)));
        assert_eq!(
            builtin.get("path"),
            Some(&serde_json::json!("builtin://skills/goose-doc-guide"))
        );
    });
}

#[test]
fn test_custom_provider_inventory_includes_metadata() {
    run_test(async {
        let openai = OpenAiFixture::new(vec![], Arc::new(EnforceSessionId::default())).await;
        let conn = AcpServerConnection::new(TestConnectionConfig::default(), openai).await;

        let response = send_custom(
            conn.cx(),
            "_goose/unstable/providers/list",
            serde_json::json!({}),
        )
        .await
        .expect("provider inventory should succeed");
        let providers = response
            .get("entries")
            .and_then(|value| value.as_array())
            .expect("missing entries array");
        let openai = providers
            .iter()
            .find(|provider| provider.get("providerId") == Some(&serde_json::json!("openai")))
            .expect("expected openai inventory entry");

        assert!(openai.get("providerName").is_some(), "missing providerName");
        assert!(openai.get("description").is_some(), "missing description");
        assert!(openai.get("defaultModel").is_some(), "missing defaultModel");
        assert!(openai.get("providerType").is_some(), "missing providerType");
        assert!(openai.get("configKeys").is_some(), "missing configKeys");
        assert!(openai.get("setupSteps").is_some(), "missing setupSteps");
    });
}

#[test]
fn test_custom_preferences_read_save_remove() {
    run_test(async {
        let data_root = tempfile::tempdir().unwrap();
        std::fs::write(
            data_root
                .path()
                .join(goose::config::base::CONFIG_YAML_NAME),
            "GOOSE_MODEL: gpt-4o\nGOOSE_PROVIDER: openai\nGOOSE_AUTO_COMPACT_THRESHOLD: 0.7\nVOICE_AUTO_SUBMIT_PHRASES: send it\n",
        )
        .unwrap();
        let openai = OpenAiFixture::new(vec![], Arc::new(EnforceSessionId::default())).await;
        let config = TestConnectionConfig {
            data_root: data_root.path().to_path_buf(),
            ..Default::default()
        };
        let conn = AcpServerConnection::new(config, openai).await;

        let response = send_custom(
            conn.cx(),
            "_goose/unstable/preferences/read",
            serde_json::json!({
                "keys": [
                    "autoCompactThreshold",
                    "voiceAutoSubmitPhrases",
                    "voiceDictationPreferredMic"
                ],
            }),
        )
        .await
        .expect("preferences read should succeed");
        assert_eq!(
            response.get("values"),
            Some(&serde_json::json!([
                { "key": "autoCompactThreshold", "value": 0.7 },
                { "key": "voiceAutoSubmitPhrases", "value": "send it" },
                { "key": "voiceDictationPreferredMic", "value": null },
            ]))
        );

        send_custom(
            conn.cx(),
            "_goose/unstable/preferences/save",
            serde_json::json!({
                "values": [
                    { "key": "voiceDictationProvider", "value": "__disabled__" },
                    { "key": "voiceDictationPreferredMic", "value": "mic-1" }
                ],
            }),
        )
        .await
        .expect("preferences save should succeed");

        send_custom(
            conn.cx(),
            "_goose/unstable/preferences/remove",
            serde_json::json!({
                "keys": ["voiceDictationProvider"],
            }),
        )
        .await
        .expect("preferences remove should succeed");

        let response = send_custom(
            conn.cx(),
            "_goose/unstable/preferences/read",
            serde_json::json!({
                "keys": ["voiceDictationProvider", "voiceDictationPreferredMic"],
            }),
        )
        .await
        .expect("preferences read after remove should succeed");
        assert_eq!(
            response.get("values"),
            Some(&serde_json::json!([
                { "key": "voiceDictationProvider", "value": null },
                { "key": "voiceDictationPreferredMic", "value": "mic-1" },
            ]))
        );
    });
}

#[test]
fn test_custom_preferences_save_rejects_invalid_values() {
    run_test(async {
        let openai = OpenAiFixture::new(vec![], Arc::new(EnforceSessionId::default())).await;
        let conn = AcpServerConnection::new(TestConnectionConfig::default(), openai).await;

        let invalid_payloads = [
            serde_json::json!({
                "values": [{ "key": "autoCompactThreshold", "value": 0 }],
            }),
            serde_json::json!({
                "values": [{ "key": "autoCompactThreshold", "value": 1.1 }],
            }),
            serde_json::json!({
                "values": [{ "key": "voiceAutoSubmitPhrases", "value": ["send"] }],
            }),
            serde_json::json!({
                "values": [{ "key": "voiceDictationProvider", "value": "bogus" }],
            }),
            serde_json::json!({
                "values": [{ "key": "voiceDictationPreferredMic", "value": "" }],
            }),
        ];

        for payload in invalid_payloads {
            let result = send_custom(conn.cx(), "_goose/unstable/preferences/save", payload).await;
            assert!(result.is_err(), "expected invalid params error");
        }

        let result = send_custom(
            conn.cx(),
            "_goose/unstable/preferences/save",
            serde_json::json!({
                "values": [
                    { "key": "voiceDictationPreferredMic", "value": "mic-1" },
                    { "key": "voiceDictationProvider", "value": "bogus" }
                ],
            }),
        )
        .await;
        assert!(result.is_err(), "expected invalid params error");

        let response = send_custom(
            conn.cx(),
            "_goose/unstable/preferences/read",
            serde_json::json!({
                "keys": ["voiceDictationPreferredMic"],
            }),
        )
        .await
        .expect("preferences read should succeed");
        assert_eq!(
            response.get("values"),
            Some(&serde_json::json!([
                { "key": "voiceDictationPreferredMic", "value": null },
            ]))
        );
    });
}

#[test]
fn test_custom_defaults_read() {
    run_test(async {
        let data_root = tempfile::tempdir().unwrap();
        std::fs::write(
            data_root.path().join(goose::config::base::CONFIG_YAML_NAME),
            "GOOSE_MODEL: claude-3-5-haiku-latest\nGOOSE_PROVIDER: anthropic\n",
        )
        .unwrap();
        let openai = OpenAiFixture::new(vec![], Arc::new(EnforceSessionId::default())).await;
        let config = TestConnectionConfig {
            data_root: data_root.path().to_path_buf(),
            ..Default::default()
        };
        let conn = AcpServerConnection::new(config, openai).await;

        let response = send_custom(
            conn.cx(),
            "_goose/unstable/defaults/read",
            serde_json::json!({}),
        )
        .await
        .expect("defaults read should succeed");
        assert_eq!(
            response,
            serde_json::json!({
                "providerId": "anthropic",
                "modelId": "claude-3-5-haiku-latest",
            })
        );
    });
}

#[test]
fn test_custom_dictation_secret_save_delete() {
    let root = tempfile::tempdir().unwrap();
    let root_path = root.path().to_string_lossy().to_string();
    let _env = env_lock::lock_env([
        ("GOOSE_PATH_ROOT", Some(root_path.as_str())),
        ("GOOSE_DISABLE_KEYRING", Some("1")),
        ("GROQ_API_KEY", None::<&str>),
    ]);
    let config_dir = goose::config::paths::Paths::config_dir();
    std::fs::create_dir_all(&config_dir).unwrap();
    std::fs::write(
        config_dir.join(goose::config::base::CONFIG_YAML_NAME),
        "GOOSE_MODEL: gpt-4o\nGOOSE_PROVIDER: openai\nGOOSE_DISABLE_KEYRING: true\n",
    )
    .unwrap();

    run_test(async move {
        let openai = OpenAiFixture::new(vec![], Arc::new(EnforceSessionId::default())).await;
        let config = TestConnectionConfig {
            data_root: config_dir.clone(),
            ..Default::default()
        };
        let conn = AcpServerConnection::new(config, openai).await;

        send_custom(
            conn.cx(),
            "_goose/unstable/dictation/secret/save",
            serde_json::json!({
                "provider": "groq",
                "value": "groq-key",
            }),
        )
        .await
        .expect("dictation secret save should succeed");

        let config = send_custom(
            conn.cx(),
            "_goose/unstable/dictation/config",
            serde_json::json!({}),
        )
        .await
        .expect("dictation config should succeed");
        assert_eq!(
            config
                .pointer("/providers/groq/configured")
                .and_then(|value| value.as_bool()),
            Some(true)
        );

        let provider_config_result = send_custom(
            conn.cx(),
            "_goose/unstable/dictation/secret/save",
            serde_json::json!({
                "provider": "openai",
                "value": "openai-key",
            }),
        )
        .await;
        assert!(
            provider_config_result.is_err(),
            "provider-config dictation providers should be rejected"
        );

        let unknown_result = send_custom(
            conn.cx(),
            "_goose/unstable/dictation/secret/save",
            serde_json::json!({
                "provider": "unknown",
                "value": "key",
            }),
        )
        .await;
        assert!(
            unknown_result.is_err(),
            "unknown provider should be rejected"
        );

        send_custom(
            conn.cx(),
            "_goose/unstable/dictation/secret/delete",
            serde_json::json!({
                "provider": "groq",
            }),
        )
        .await
        .expect("dictation secret delete should succeed");

        let config = send_custom(
            conn.cx(),
            "_goose/unstable/dictation/config",
            serde_json::json!({}),
        )
        .await
        .expect("dictation config should succeed");
        assert_eq!(
            config
                .pointer("/providers/groq/configured")
                .and_then(|value| value.as_bool()),
            Some(false)
        );
    });
}

#[test]
fn test_raw_config_and_secret_methods_are_removed() {
    run_test(async {
        let openai = OpenAiFixture::new(vec![], Arc::new(EnforceSessionId::default())).await;
        let conn = AcpServerConnection::new(TestConnectionConfig::default(), openai).await;

        for method in [
            "_goose/config/read",
            "_goose/config/upsert",
            "_goose/config/remove",
            "_goose/secret/check",
            "_goose/secret/upsert",
            "_goose/secret/remove",
        ] {
            let result = send_custom(conn.cx(), method, serde_json::json!({})).await;
            assert!(result.is_err(), "{method} should be removed");
        }
    });
}

#[test]
fn test_provider_switching_updates_session_state() {
    run_test(async {
        let openai = OpenAiFixture::new(vec![], Arc::new(EnforceSessionId::default())).await;
        let config = TestConnectionConfig {
            provider_factory: Some(mock_provider_factory()),
            current_model: "gpt-4o".to_string(),
            ..Default::default()
        };
        let mut conn = AcpServerConnection::new(config, openai).await;

        let SessionData { session, .. } = conn.new_session().await.unwrap();
        let session_id = session.session_id().0.clone();

        conn.set_config_option(&session_id, "provider", "anthropic")
            .await
            .expect("provider switch to anthropic should succeed");

        conn.set_config_option(&session_id, "provider", "openai")
            .await
            .expect("provider switch to openai should succeed");

        conn.set_config_option(&session_id, "provider", "goose")
            .await
            .expect("provider reset to goose should succeed");
    });
}

#[test]
fn test_custom_unknown_method() {
    run_test(async {
        let openai = OpenAiFixture::new(vec![], Arc::new(EnforceSessionId::default())).await;
        let conn = AcpServerConnection::new(TestConnectionConfig::default(), openai).await;

        let result = send_custom(conn.cx(), "_unknown/method", serde_json::json!({})).await;
        assert!(result.is_err(), "expected method_not_found error");
    });
}

#[test]
fn test_developer_fs_requests_use_acp_session_id() {
    run_test(async {
        let seen_session_id = Arc::new(Mutex::new(None::<String>));
        let seen_session_id_clone = Arc::clone(&seen_session_id);
        let openai = OpenAiFixture::new(
            vec![
                (
                    "Use the read tool to read /tmp/test_acp_read.txt and output only its contents."
                        .to_string(),
                    include_str!("acp_test_data/openai_fs_read_tool_call.txt"),
                ),
                (
                    r#""content":"test-read-content-12345""#.into(),
                    include_str!("acp_test_data/openai_fs_read_tool_result.txt"),
                ),
            ],
            Arc::new(IgnoreSessionId),
        )
        .await;
        let config = TestConnectionConfig {
            // gpt-5-nano routes to the Responses API; use a Chat Completions
            // model so the canned SSE fixtures are parsed correctly.
            current_model: "gpt-4.1".to_string(),
            read_text_file: Some(Arc::new(move |req| {
                *seen_session_id_clone.lock().unwrap() = Some(req.session_id.0.to_string());
                Ok(agent_client_protocol::schema::ReadTextFileResponse::new(
                    "test-read-content-12345",
                ))
            })),
            ..Default::default()
        };
        let mut conn = AcpServerConnection::new(config, openai).await;

        let SessionData { mut session, .. } = conn.new_session().await.unwrap();
        let acp_session_id = session.session_id().0.to_string();

        let output = session
            .prompt(
                "Use the read tool to read /tmp/test_acp_read.txt and output only its contents.",
                PermissionDecision::Cancel,
            )
            .await
            .expect("prompt should succeed");

        assert_eq!(output.text, "test-read-content-12345");
        assert_eq!(
            seen_session_id.lock().unwrap().as_deref(),
            Some(acp_session_id.as_str()),
            "ACP read request should use the ACP session/thread ID",
        );
    });
}

#[test]
fn test_custom_provider_supported_models_lists_raw_provider_models() {
    run_test(async move {
        let openai = OpenAiFixture::new(vec![], Arc::new(EnforceSessionId::default())).await;
        let provider_factory: AcpProviderFactory =
            Arc::new(|provider_name, model_config, _extensions, _working_dir| {
                Box::pin(async move {
                    Ok(Arc::new(MockProvider {
                        name: provider_name,
                        model_config,
                        recommended_models: vec!["canonical-filtered-model".to_string()],
                        supported_models: vec![
                            "goose-claude-opus-4-8".to_string(),
                            "raw-databricks-endpoint".to_string(),
                        ],
                    }) as Arc<dyn Provider>)
                })
            });
        let conn = AcpServerConnection::new(
            TestConnectionConfig {
                provider_factory: Some(provider_factory),
                ..Default::default()
            },
            openai,
        )
        .await;

        let response = send_custom(
            conn.cx(),
            "_goose/unstable/providers/supported-models/list",
            serde_json::json!({ "providerId": "openai" }),
        )
        .await
        .expect("provider supported models list should succeed");

        assert_eq!(
            response.get("providerId"),
            Some(&serde_json::json!("openai"))
        );
        assert_eq!(
            response.get("models"),
            Some(&serde_json::json!([
                "goose-claude-opus-4-8",
                "raw-databricks-endpoint"
            ]))
        );
    });
}
