use crate::product::agent::config::set_project_trust_level;
use crate::product::agent::config_loader::SYSTEM_CONFIG_TOML_FILE_UNIX;
use crate::product::app_server_protocol::AskForApproval;
use crate::product::app_server_protocol::ConfigBatchWriteParams;
use crate::product::app_server_protocol::ConfigEdit;
use crate::product::app_server_protocol::ConfigLayerSource;
use crate::product::app_server_protocol::ConfigReadParams;
use crate::product::app_server_protocol::ConfigReadResponse;
use crate::product::app_server_protocol::ConfigValueWriteParams;
use crate::product::app_server_protocol::ConfigWriteResponse;
use crate::product::app_server_protocol::JSONRPCError;
use crate::product::app_server_protocol::JSONRPCResponse;
use crate::product::app_server_protocol::MergeStrategy;
use crate::product::app_server_protocol::RequestId;
use crate::product::app_server_protocol::SandboxMode;
use crate::product::app_server_protocol::ToolsV2;
use crate::product::app_server_protocol::WriteStatus;
use crate::product::protocol::config_types::TrustLevel;
use crate::product::utils_absolute_path::AbsolutePathBuf;
use crate::test_support::app_server::McpProcess;
use crate::test_support::app_server::test_path_buf_with_windows;
use crate::test_support::app_server::test_tmp_path_buf;
use crate::test_support::app_server::to_response;
use anyhow::Result;
use pretty_assertions::assert_eq;
use serde_json::json;
use tempfile::TempDir;
use tokio::time::timeout;

const DEFAULT_READ_TIMEOUT: std::time::Duration = std::time::Duration::from_secs(10);

fn write_config(lha_home: &TempDir, contents: &str) -> Result<()> {
    Ok(std::fs::write(
        lha_home.path().join("config.toml"),
        contents,
    )?)
}

#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn config_read_returns_effective_and_layers() -> Result<()> {
    let lha_home = TempDir::new()?;
    write_config(
        &lha_home,
        r#"
sandbox_mode = "workspace-write"
"#,
    )?;
    let lha_home_path = lha_home.path().canonicalize()?;
    let user_file = AbsolutePathBuf::try_from(lha_home_path.join("config.toml"))?;

    let mut mcp = McpProcess::new(lha_home.path()).await?;
    timeout(DEFAULT_READ_TIMEOUT, mcp.initialize()).await??;

    let request_id = mcp
        .send_config_read_request(ConfigReadParams {
            include_layers: true,
            cwd: None,
        })
        .await?;
    let resp: JSONRPCResponse = timeout(
        DEFAULT_READ_TIMEOUT,
        mcp.read_stream_until_response_message(RequestId::Integer(request_id)),
    )
    .await??;
    let ConfigReadResponse {
        config,
        origins,
        layers,
    } = to_response(resp)?;

    assert_eq!(config.sandbox_mode, Some(SandboxMode::WorkspaceWrite));
    assert_eq!(
        origins.get("sandbox_mode").expect("origin").name,
        ConfigLayerSource::User {
            file: user_file.clone(),
        }
    );
    let layers = layers.expect("layers present");
    assert_layers_user_then_optional_system(&layers, user_file)?;

    Ok(())
}

#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn config_read_includes_tools() -> Result<()> {
    let lha_home = TempDir::new()?;
    write_config(
        &lha_home,
        r#"
[tools]
web_search = true
view_image = false
"#,
    )?;
    let lha_home_path = lha_home.path().canonicalize()?;
    let user_file = AbsolutePathBuf::try_from(lha_home_path.join("config.toml"))?;

    let mut mcp = McpProcess::new(lha_home.path()).await?;
    timeout(DEFAULT_READ_TIMEOUT, mcp.initialize()).await??;

    let request_id = mcp
        .send_config_read_request(ConfigReadParams {
            include_layers: true,
            cwd: None,
        })
        .await?;
    let resp: JSONRPCResponse = timeout(
        DEFAULT_READ_TIMEOUT,
        mcp.read_stream_until_response_message(RequestId::Integer(request_id)),
    )
    .await??;
    let ConfigReadResponse {
        config,
        origins,
        layers,
    } = to_response(resp)?;

    let tools = config.tools.expect("tools present");
    assert_eq!(
        tools,
        ToolsV2 {
            web_search: Some(true),
            view_image: Some(false),
        }
    );
    assert_eq!(
        origins.get("tools.web_search").expect("origin").name,
        ConfigLayerSource::User {
            file: user_file.clone(),
        }
    );
    assert_eq!(
        origins.get("tools.view_image").expect("origin").name,
        ConfigLayerSource::User {
            file: user_file.clone(),
        }
    );

    let layers = layers.expect("layers present");
    assert_layers_user_then_optional_system(&layers, user_file)?;

    Ok(())
}

#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn config_read_includes_project_layers_for_cwd() -> Result<()> {
    let lha_home = TempDir::new()?;
    write_config(&lha_home, r#"approval_policy = "on-request""#)?;

    let workspace = TempDir::new()?;
    let project_config_dir = workspace.path().join(".lha");
    std::fs::create_dir_all(&project_config_dir)?;
    std::fs::write(
        project_config_dir.join("config.toml"),
        r#"
sandbox_mode = "read-only"
"#,
    )?;
    set_project_trust_level(lha_home.path(), workspace.path(), TrustLevel::Trusted)?;
    let project_config = AbsolutePathBuf::try_from(project_config_dir)?;

    let mut mcp = McpProcess::new(lha_home.path()).await?;
    timeout(DEFAULT_READ_TIMEOUT, mcp.initialize()).await??;

    let request_id = mcp
        .send_config_read_request(ConfigReadParams {
            include_layers: true,
            cwd: Some(workspace.path().to_string_lossy().into_owned()),
        })
        .await?;
    let resp: JSONRPCResponse = timeout(
        DEFAULT_READ_TIMEOUT,
        mcp.read_stream_until_response_message(RequestId::Integer(request_id)),
    )
    .await??;
    let ConfigReadResponse {
        config, origins, ..
    } = to_response(resp)?;

    assert_eq!(config.sandbox_mode, Some(SandboxMode::ReadOnly));
    assert_eq!(
        origins.get("sandbox_mode").expect("origin").name,
        ConfigLayerSource::Project {
            dot_lha_folder: project_config
        }
    );

    Ok(())
}

#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn config_read_includes_system_layer_and_overrides() -> Result<()> {
    let lha_home = TempDir::new()?;
    let user_dir = test_path_buf_with_windows("/user", Some(r"C:\Users\user"));
    let system_dir = test_path_buf_with_windows("/system", Some(r"C:\System"));
    write_config(
        &lha_home,
        &format!(
            r#"
approval_policy = "on-request"
sandbox_mode = "workspace-write"

[sandbox_workspace_write]
writable_roots = [{}]
network_access = true
"#,
            serde_json::json!(user_dir)
        ),
    )?;
    let lha_home_path = lha_home.path().canonicalize()?;
    let user_file = AbsolutePathBuf::try_from(lha_home_path.join("config.toml"))?;

    let managed_path = lha_home.path().join("managed_config.toml");
    let managed_file = AbsolutePathBuf::try_from(managed_path.clone())?;
    std::fs::write(
        &managed_path,
        format!(
            r#"
approval_policy = "never"

[sandbox_workspace_write]
writable_roots = [{}]
"#,
            serde_json::json!(system_dir.clone())
        ),
    )?;

    let managed_path_str = managed_path.display().to_string();

    let mut mcp = McpProcess::new_with_env(
        lha_home.path(),
        &[(
            "CODEX_APP_SERVER_MANAGED_CONFIG_PATH",
            Some(&managed_path_str),
        )],
    )
    .await?;
    timeout(DEFAULT_READ_TIMEOUT, mcp.initialize()).await??;

    let request_id = mcp
        .send_config_read_request(ConfigReadParams {
            include_layers: true,
            cwd: None,
        })
        .await?;
    let resp: JSONRPCResponse = timeout(
        DEFAULT_READ_TIMEOUT,
        mcp.read_stream_until_response_message(RequestId::Integer(request_id)),
    )
    .await??;
    let ConfigReadResponse {
        config,
        origins,
        layers,
    } = to_response(resp)?;

    assert_eq!(config.approval_policy, Some(AskForApproval::Never));
    assert_eq!(
        origins.get("approval_policy").expect("origin").name,
        ConfigLayerSource::LegacyManagedConfigTomlFromFile {
            file: managed_file.clone(),
        }
    );

    assert_eq!(config.sandbox_mode, Some(SandboxMode::WorkspaceWrite));
    assert_eq!(
        origins.get("sandbox_mode").expect("origin").name,
        ConfigLayerSource::User {
            file: user_file.clone(),
        }
    );

    let sandbox = config
        .sandbox_workspace_write
        .as_ref()
        .expect("sandbox workspace write");
    assert_eq!(sandbox.writable_roots, vec![system_dir]);
    assert_eq!(
        origins
            .get("sandbox_workspace_write.writable_roots.0")
            .expect("origin")
            .name,
        ConfigLayerSource::LegacyManagedConfigTomlFromFile {
            file: managed_file.clone(),
        }
    );

    assert!(sandbox.network_access);
    assert_eq!(
        origins
            .get("sandbox_workspace_write.network_access")
            .expect("origin")
            .name,
        ConfigLayerSource::User {
            file: user_file.clone(),
        }
    );

    let layers = layers.expect("layers present");
    assert_layers_managed_user_then_optional_system(&layers, managed_file, user_file)?;

    Ok(())
}

#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn config_value_write_replaces_value() -> Result<()> {
    let temp_dir = TempDir::new()?;
    let lha_home = temp_dir.path().canonicalize()?;
    write_config(
        &temp_dir,
        r#"
approval_policy = "on-request"
"#,
    )?;

    let mut mcp = McpProcess::new(&lha_home).await?;
    timeout(DEFAULT_READ_TIMEOUT, mcp.initialize()).await??;

    let read_id = mcp
        .send_config_read_request(ConfigReadParams {
            include_layers: false,
            cwd: None,
        })
        .await?;
    let read_resp: JSONRPCResponse = timeout(
        DEFAULT_READ_TIMEOUT,
        mcp.read_stream_until_response_message(RequestId::Integer(read_id)),
    )
    .await??;
    let read: ConfigReadResponse = to_response(read_resp)?;
    let expected_version = read
        .origins
        .get("approval_policy")
        .map(|m| m.version.clone());

    let write_id = mcp
        .send_config_value_write_request(ConfigValueWriteParams {
            file_path: None,
            key_path: "approval_policy".to_string(),
            value: json!("never"),
            merge_strategy: MergeStrategy::Replace,
            expected_version,
        })
        .await?;
    let write_resp: JSONRPCResponse = timeout(
        DEFAULT_READ_TIMEOUT,
        mcp.read_stream_until_response_message(RequestId::Integer(write_id)),
    )
    .await??;
    let write: ConfigWriteResponse = to_response(write_resp)?;
    let expected_file_path = AbsolutePathBuf::resolve_path_against_base("config.toml", lha_home)?;

    assert_eq!(write.status, WriteStatus::Ok);
    assert_eq!(write.file_path, expected_file_path);
    assert!(write.overridden_metadata.is_none());

    let verify_id = mcp
        .send_config_read_request(ConfigReadParams {
            include_layers: false,
            cwd: None,
        })
        .await?;
    let verify_resp: JSONRPCResponse = timeout(
        DEFAULT_READ_TIMEOUT,
        mcp.read_stream_until_response_message(RequestId::Integer(verify_id)),
    )
    .await??;
    let verify: ConfigReadResponse = to_response(verify_resp)?;
    assert_eq!(verify.config.approval_policy, Some(AskForApproval::Never));

    Ok(())
}

#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn config_value_write_rejects_version_conflict() -> Result<()> {
    let lha_home = TempDir::new()?;
    write_config(
        &lha_home,
        r#"
approval_policy = "on-request"
"#,
    )?;

    let mut mcp = McpProcess::new(lha_home.path()).await?;
    timeout(DEFAULT_READ_TIMEOUT, mcp.initialize()).await??;

    let write_id = mcp
        .send_config_value_write_request(ConfigValueWriteParams {
            file_path: Some(lha_home.path().join("config.toml").display().to_string()),
            key_path: "approval_policy".to_string(),
            value: json!("never"),
            merge_strategy: MergeStrategy::Replace,
            expected_version: Some("sha256:stale".to_string()),
        })
        .await?;

    let err: JSONRPCError = timeout(
        DEFAULT_READ_TIMEOUT,
        mcp.read_stream_until_error_message(RequestId::Integer(write_id)),
    )
    .await??;
    let code = err
        .error
        .data
        .as_ref()
        .and_then(|d| d.get("config_write_error_code"))
        .and_then(|v| v.as_str());
    assert_eq!(code, Some("configVersionConflict"));

    Ok(())
}

#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn config_batch_write_applies_multiple_edits() -> Result<()> {
    let tmp_dir = TempDir::new()?;
    let lha_home = tmp_dir.path().canonicalize()?;
    write_config(&tmp_dir, "")?;

    let mut mcp = McpProcess::new(&lha_home).await?;
    timeout(DEFAULT_READ_TIMEOUT, mcp.initialize()).await??;

    let writable_root = test_tmp_path_buf();
    let batch_id = mcp
        .send_config_batch_write_request(ConfigBatchWriteParams {
            file_path: Some(lha_home.join("config.toml").display().to_string()),
            edits: vec![
                ConfigEdit {
                    key_path: "sandbox_mode".to_string(),
                    value: json!("workspace-write"),
                    merge_strategy: MergeStrategy::Replace,
                },
                ConfigEdit {
                    key_path: "sandbox_workspace_write".to_string(),
                    value: json!({
                        "writable_roots": [writable_root.clone()],
                        "network_access": false
                    }),
                    merge_strategy: MergeStrategy::Replace,
                },
            ],
            expected_version: None,
        })
        .await?;
    let batch_resp: JSONRPCResponse = timeout(
        DEFAULT_READ_TIMEOUT,
        mcp.read_stream_until_response_message(RequestId::Integer(batch_id)),
    )
    .await??;
    let batch_write: ConfigWriteResponse = to_response(batch_resp)?;
    assert_eq!(batch_write.status, WriteStatus::Ok);
    let expected_file_path = AbsolutePathBuf::resolve_path_against_base("config.toml", lha_home)?;
    assert_eq!(batch_write.file_path, expected_file_path);

    let read_id = mcp
        .send_config_read_request(ConfigReadParams {
            include_layers: false,
            cwd: None,
        })
        .await?;
    let read_resp: JSONRPCResponse = timeout(
        DEFAULT_READ_TIMEOUT,
        mcp.read_stream_until_response_message(RequestId::Integer(read_id)),
    )
    .await??;
    let read: ConfigReadResponse = to_response(read_resp)?;
    assert_eq!(read.config.sandbox_mode, Some(SandboxMode::WorkspaceWrite));
    let sandbox = read
        .config
        .sandbox_workspace_write
        .as_ref()
        .expect("sandbox workspace write");
    assert_eq!(sandbox.writable_roots, vec![writable_root]);
    assert!(!sandbox.network_access);

    Ok(())
}

#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn config_rpc_reads_and_writes_memory_settings() -> Result<()> {
    let tmp_dir = TempDir::new()?;
    let lha_home = tmp_dir.path().canonicalize()?;
    write_config(
        &tmp_dir,
        r#"
[features]
memories = true

[memories]
use_memories = false
generate_memories = true
dedicated_tools = false
"#,
    )?;

    let mut mcp = McpProcess::new(&lha_home).await?;
    timeout(DEFAULT_READ_TIMEOUT, mcp.initialize()).await??;

    let read_id = mcp
        .send_config_read_request(ConfigReadParams {
            include_layers: false,
            cwd: None,
        })
        .await?;
    let read_resp: JSONRPCResponse = timeout(
        DEFAULT_READ_TIMEOUT,
        mcp.read_stream_until_response_message(RequestId::Integer(read_id)),
    )
    .await??;
    let read: ConfigReadResponse = to_response(read_resp)?;
    assert_eq!(config_bool(&read, &["features", "memories"]), Some(true));
    assert_eq!(
        config_bool(&read, &["memories", "use_memories"]),
        Some(false)
    );
    assert_eq!(
        config_bool(&read, &["memories", "generate_memories"]),
        Some(true)
    );
    assert_eq!(
        config_bool(&read, &["memories", "dedicated_tools"]),
        Some(false)
    );

    let feature_write_id = mcp
        .send_config_value_write_request(ConfigValueWriteParams {
            file_path: None,
            key_path: "features.memories".to_string(),
            value: json!(false),
            merge_strategy: MergeStrategy::Replace,
            expected_version: None,
        })
        .await?;
    let feature_write_resp: JSONRPCResponse = timeout(
        DEFAULT_READ_TIMEOUT,
        mcp.read_stream_until_response_message(RequestId::Integer(feature_write_id)),
    )
    .await??;
    let feature_write: ConfigWriteResponse = to_response(feature_write_resp)?;
    assert_eq!(feature_write.status, WriteStatus::Ok);

    let batch_id = mcp
        .send_config_batch_write_request(ConfigBatchWriteParams {
            file_path: None,
            edits: vec![
                ConfigEdit {
                    key_path: "memories.use_memories".to_string(),
                    value: json!(true),
                    merge_strategy: MergeStrategy::Replace,
                },
                ConfigEdit {
                    key_path: "memories.generate_memories".to_string(),
                    value: json!(false),
                    merge_strategy: MergeStrategy::Replace,
                },
                ConfigEdit {
                    key_path: "memories.dedicated_tools".to_string(),
                    value: json!(true),
                    merge_strategy: MergeStrategy::Replace,
                },
            ],
            expected_version: None,
        })
        .await?;
    let batch_resp: JSONRPCResponse = timeout(
        DEFAULT_READ_TIMEOUT,
        mcp.read_stream_until_response_message(RequestId::Integer(batch_id)),
    )
    .await??;
    let batch_write: ConfigWriteResponse = to_response(batch_resp)?;
    assert_eq!(batch_write.status, WriteStatus::Ok);

    let verify_id = mcp
        .send_config_read_request(ConfigReadParams {
            include_layers: false,
            cwd: None,
        })
        .await?;
    let verify_resp: JSONRPCResponse = timeout(
        DEFAULT_READ_TIMEOUT,
        mcp.read_stream_until_response_message(RequestId::Integer(verify_id)),
    )
    .await??;
    let verify: ConfigReadResponse = to_response(verify_resp)?;
    assert_eq!(config_bool(&verify, &["features", "memories"]), Some(false));
    assert_eq!(
        config_bool(&verify, &["memories", "use_memories"]),
        Some(true)
    );
    assert_eq!(
        config_bool(&verify, &["memories", "generate_memories"]),
        Some(false)
    );
    assert_eq!(
        config_bool(&verify, &["memories", "dedicated_tools"]),
        Some(true)
    );

    Ok(())
}

fn config_bool(read: &ConfigReadResponse, path: &[&str]) -> Option<bool> {
    let mut value = read.config.additional.get(*path.first()?)?;
    for segment in &path[1..] {
        value = value.get(*segment)?;
    }
    value.as_bool()
}

fn assert_layers_user_then_optional_system(
    layers: &[crate::product::app_server_protocol::ConfigLayer],
    user_file: AbsolutePathBuf,
) -> Result<()> {
    if cfg!(unix) {
        let system_file = AbsolutePathBuf::from_absolute_path(SYSTEM_CONFIG_TOML_FILE_UNIX)?;
        assert_eq!(layers.len(), 2);
        assert_eq!(layers[0].name, ConfigLayerSource::User { file: user_file });
        assert_eq!(
            layers[1].name,
            ConfigLayerSource::System { file: system_file }
        );
    } else {
        assert_eq!(layers.len(), 1);
        assert_eq!(layers[0].name, ConfigLayerSource::User { file: user_file });
    }
    Ok(())
}

fn assert_layers_managed_user_then_optional_system(
    layers: &[crate::product::app_server_protocol::ConfigLayer],
    managed_file: AbsolutePathBuf,
    user_file: AbsolutePathBuf,
) -> Result<()> {
    if cfg!(unix) {
        let system_file = AbsolutePathBuf::from_absolute_path(SYSTEM_CONFIG_TOML_FILE_UNIX)?;
        assert_eq!(layers.len(), 3);
        assert_eq!(
            layers[0].name,
            ConfigLayerSource::LegacyManagedConfigTomlFromFile { file: managed_file }
        );
        assert_eq!(layers[1].name, ConfigLayerSource::User { file: user_file });
        assert_eq!(
            layers[2].name,
            ConfigLayerSource::System { file: system_file }
        );
    } else {
        assert_eq!(layers.len(), 2);
        assert_eq!(
            layers[0].name,
            ConfigLayerSource::LegacyManagedConfigTomlFromFile { file: managed_file }
        );
        assert_eq!(layers[1].name, ConfigLayerSource::User { file: user_file });
    }
    Ok(())
}
