mod common;

use std::fs;
use std::net::TcpListener;
use std::path::PathBuf;
use std::sync::{Arc, Mutex, OnceLock};
use std::thread;
use std::time::Duration;

use serde_json::{json, Value};
use uuid::Uuid;

use common::MockTransport;
use sgclaw::agent::{
    handle_browser_message, handle_browser_message_with_context, AgentRuntimeContext,
};
use sgclaw::compat::runtime::CompatTaskContext;
use sgclaw::config::SgClawSettings;
use sgclaw::pipe::{Action, AgentMessage, BrowserMessage, BrowserPipeTool, Timing};
use sgclaw::security::MacPolicy;
use tungstenite::{accept, error::ProtocolError, Message};

fn env_lock() -> &'static Mutex<()> {
    static LOCK: OnceLock<Mutex<()>> = OnceLock::new();
    LOCK.get_or_init(|| Mutex::new(()))
}

fn temp_workspace_root() -> PathBuf {
    let root = std::env::temp_dir().join(format!("sgclaw-agent-runtime-{}", Uuid::new_v4()));
    fs::create_dir_all(&root).unwrap();
    root
}

fn write_config(
    root: &PathBuf,
    api_key: &str,
    base_url: &str,
    model: &str,
    skills_dir: Option<&str>,
    browser_ws_url: Option<&str>,
    direct_submit_skill: Option<&str>,
) -> PathBuf {
    let config_path = root.join("sgclaw_config.json");
    let mut payload = json!({
        "apiKey": api_key,
        "baseUrl": base_url,
        "model": model,
        "runtimeProfile": "BrowserAttached"
    });
    if let Some(skills_dir) = skills_dir {
        payload["skillsDir"] = json!(skills_dir);
    }
    if let Some(browser_ws_url) = browser_ws_url {
        payload["browserWsUrl"] = json!(browser_ws_url);
    }
    if let Some(direct_submit_skill) = direct_submit_skill {
        payload["directSubmitSkill"] = json!(direct_submit_skill);
    }
    fs::write(&config_path, serde_json::to_string_pretty(&payload).unwrap()).unwrap();
    config_path
}

fn real_skill_lib_root() -> PathBuf {
    PathBuf::from(env!("CARGO_MANIFEST_DIR"))
        .ancestors()
        .find_map(|ancestor| {
            let candidate = ancestor.join("skill_lib");
            candidate.is_dir().then_some(candidate)
        })
        .expect("workspace should have sgClaw skill_lib ancestor")
}

fn start_browser_ws_server() -> (String, Arc<Mutex<Vec<String>>>, thread::JoinHandle<()>) {
    let listener = TcpListener::bind("127.0.0.1:0").unwrap();
    let address = listener.local_addr().unwrap();
    let frames = Arc::new(Mutex::new(Vec::new()));
    let frames_for_thread = Arc::clone(&frames);

    let handle = thread::spawn(move || {
        let (stream, _) = listener.accept().unwrap();
        stream
            .set_read_timeout(Some(Duration::from_secs(1)))
            .unwrap();
        stream
            .set_write_timeout(Some(Duration::from_secs(1)))
            .unwrap();
        let mut socket = accept(stream).unwrap();
        let mut action_count = 0_u64;

        loop {
            let message = match socket.read() {
                Ok(message) => message,
                Err(tungstenite::Error::ConnectionClosed)
                | Err(tungstenite::Error::AlreadyClosed)
                | Err(tungstenite::Error::Protocol(
                    ProtocolError::ResetWithoutClosingHandshake,
                )) => break,
                Err(err) => panic!("browser ws test server read failed: {err}"),
            };
            let payload = match message {
                Message::Text(text) => text.to_string(),
                Message::Ping(payload) => {
                    socket.send(Message::Pong(payload)).unwrap();
                    continue;
                }
                Message::Close(_) => break,
                other => panic!("expected text frame, got {other:?}"),
            };
            frames_for_thread.lock().unwrap().push(payload.clone());

            let parsed: Value = serde_json::from_str(&payload).unwrap();
            if parsed.get("type").and_then(Value::as_str) == Some("register") {
                continue;
            }

            let values = parsed.as_array().expect("browser action frame should be an array");
            let request_url = values[0].as_str().expect("request_url should be a string");
            let action = values[1].as_str().expect("action should be a string");
            action_count += 1;

            socket
                .send(Message::Text(
                    r#"{"type":"welcome","client_id":1,"server_time":"2026-04-04T00:00:00"}"#
                        .to_string()
                        .into(),
                ))
                .unwrap();
            socket.send(Message::Text("0".into())).unwrap();

            let callback_frame = match action {
                "sgHideBrowserCallAfterLoaded" => {
                    let target_url = values[2].as_str().expect("navigate target_url should be a string");
                    json!([
                        request_url,
                        "callBackJsToCpp",
                        format!(
                            "{request_url}@_@{target_url}@_@sgclaw_cb_{action_count}@_@sgHideBrowserCallAfterLoaded@_@"
                        )
                    ])
                }
                "sgBrowserExcuteJsCodeByArea" => {
                    let target_url = values[2].as_str().expect("script target_url should be a string");
                    let response_text = if action_count == 2 {
                        "知乎热榜\n1 问题一 344万热度\n2 问题二 266万热度".to_string()
                    } else {
                        r#"{"source":"https://www.zhihu.com/hot","sheet_name":"知乎热榜","columns":["rank","title","heat"],"rows":[[1,"问题一","344万"],[2,"问题二","266万"]]}"#.to_string()
                    };
                    json!([
                        request_url,
                        "callBackJsToCpp",
                        format!(
                            "{request_url}@_@{target_url}@_@sgclaw_cb_{action_count}@_@sgBrowserExcuteJsCodeByArea@_@{response_text}"
                        )
                    ])
                }
                other => panic!("unexpected browser action {other}"),
            };

            socket
                .send(Message::Text(callback_frame.to_string().into()))
                .unwrap();

            if action_count >= 3 {
                break;
            }
        }
    });

    (format!("ws://{address}"), frames, handle)
}

#[test]
fn browser_ws_server_treats_reset_without_closing_handshake_as_disconnect() {
    let err = tungstenite::Error::Protocol(ProtocolError::ResetWithoutClosingHandshake);
    assert!(matches!(
        err,
        tungstenite::Error::Protocol(ProtocolError::ResetWithoutClosingHandshake)
    ));
}

fn provider_path_test_policy() -> MacPolicy {
    policy_for_domains(&["www.baidu.com"])
}

fn direct_runtime_test_policy() -> MacPolicy {
    policy_for_domains(&["95598.sgcc.com.cn"])
}

fn test_policy() -> MacPolicy {
    policy_for_domains(&["www.zhihu.com"])
}

fn policy_for_domains(domains: &[&str]) -> MacPolicy {
    MacPolicy::from_json_str(
        &serde_json::json!({
            "version": "1.0",
            "domains": { "allowed": domains },
            "pipe_actions": {
                "allowed": ["click", "type", "navigate", "getText", "eval"],
                "blocked": []
            }
        })
        .to_string(),
    )
    .unwrap()
}

fn build_direct_runtime_skill_root() -> PathBuf {
    let root = std::env::temp_dir().join(format!(
        "sgclaw-agent-runtime-skill-root-{}",
        Uuid::new_v4()
    ));
    let skill_dir = root.join("fault-details-report");
    let script_dir = skill_dir.join("scripts");

    fs::create_dir_all(&script_dir).unwrap();
    fs::write(
        skill_dir.join("SKILL.toml"),
        r#"
[skill]
name = "fault-details-report"
description = "Collect 95598 fault detail data via browser eval."
version = "0.1.0"

[[tools]]
name = "collect_fault_details"
description = "Collect structured fault detail rows for a specific period."
kind = "browser_script"
command = "scripts/collect_fault_details.js"

[tools.args]
period = "YYYY-MM period to collect."
"#,
    )
    .unwrap();
    fs::write(
        script_dir.join("collect_fault_details.js"),
        r#"
return {
  fault_type: "outage",
  observed_at: `${args.period}-15 09:00`,
  affected_scope: "line-7",
  expected_domain: args.expected_domain,
  artifact_payload: "report artifact payload"
};
"#,
    )
    .unwrap();

    root
}

fn write_direct_submit_config(workspace_root: &std::path::Path, skill_root: &std::path::Path) -> PathBuf {
    let config_path = workspace_root.join("sgclaw_config.json");
    fs::write(
        &config_path,
        serde_json::json!({
            "providers": [],
            "skillsDir": skill_root,
            "directSubmitSkill": "fault-details-report.collect_fault_details"
        })
        .to_string(),
    )
    .unwrap();
    config_path
}

fn direct_submit_runtime_context(skill_root: &std::path::Path) -> AgentRuntimeContext {
    let workspace_root = std::env::temp_dir().join(format!(
        "sgclaw-agent-runtime-workspace-{}",
        Uuid::new_v4()
    ));
    fs::create_dir_all(&workspace_root).unwrap();
    let config_path = write_direct_submit_config(&workspace_root, skill_root);
    AgentRuntimeContext::new(Some(config_path), workspace_root)
}

fn submit_fault_details_message() -> BrowserMessage {
    BrowserMessage::SubmitTask {
        instruction: "请采集 2026-03 的故障明细并返回结果".to_string(),
        conversation_id: String::new(),
        messages: vec![],
        page_url: "https://95598.sgcc.com.cn/".to_string(),
        page_title: "网上国网".to_string(),
    }
}

fn submit_zhihu_hotlist_export_message() -> BrowserMessage {
    BrowserMessage::SubmitTask {
        instruction: "打开知乎热榜，获取前10条数据，并导出 Excel".to_string(),
        conversation_id: String::new(),
        messages: vec![],
        page_url: String::new(),
        page_title: String::new(),
    }
}

fn direct_submit_mode_logs(sent: &[AgentMessage]) -> Vec<String> {
    sent.iter()
        .filter_map(|message| match message {
            AgentMessage::LogEntry { level, message } if level == "mode" => Some(message.clone()),
            _ => None,
        })
        .collect()
}

fn direct_submit_completion(sent: &[AgentMessage]) -> Option<(bool, String)> {
    sent.iter().find_map(|message| match message {
        AgentMessage::TaskComplete { success, summary } => Some((*success, summary.clone())),
        _ => None,
    })
}

fn success_browser_response(seq: u64, data: serde_json::Value) -> BrowserMessage {
    BrowserMessage::Response {
        seq,
        success: true,
        data,
        aom_snapshot: vec![],
        timing: Timing {
            queue_ms: 1,
            exec_ms: 10,
        },
    }
}

fn report_artifact_browser_response(
    seq: u64,
    status: &str,
    partial_reasons: &[&str],
    detail_rows: Vec<serde_json::Value>,
    summary_rows: Vec<serde_json::Value>,
) -> BrowserMessage {
    success_browser_response(
        seq,
        serde_json::json!({
            "text": {
                "type": "report-artifact",
                "report_name": "fault-details-report",
                "period": "2026-03",
                "selected_range": {
                    "start": "2026-03-08 16:00:00",
                    "end": "2026-03-09 16:00:00"
                },
                "columns": ["qxdbh"],
                "rows": detail_rows,
                "sections": [{
                    "name": "summary-sheet",
                    "columns": ["index"],
                    "rows": summary_rows
                }],
                "counts": {
                    "detail_rows": detail_rows.len(),
                    "summary_rows": summary_rows.len()
                },
                "status": status,
                "partial_reasons": partial_reasons,
                "downstream": {
                    "export": {
                        "attempted": true,
                        "success": status != "blocked" && status != "error",
                        "path": "http://localhost/export.xlsx"
                    },
                    "report_log": {
                        "attempted": true,
                        "success": partial_reasons.is_empty(),
                        "error": partial_reasons
                            .first()
                            .copied()
                            .unwrap_or("")
                    }
                }
            }
        }),
    )
}

#[test]
fn direct_submit_runtime_executes_fault_details_skill_without_provider_path() {
    let skill_root = build_direct_runtime_skill_root();
    let transport = Arc::new(MockTransport::new(vec![success_browser_response(
        1,
        serde_json::json!({
            "text": {
                "fault_type": "outage",
                "observed_at": "2026-03-15 09:00",
                "affected_scope": "line-7"
            }
        }),
    )]));
    let browser_tool = BrowserPipeTool::new(
        transport.clone(),
        direct_runtime_test_policy(),
        vec![1, 2, 3, 4, 5, 6, 7, 8],
    )
    .with_response_timeout(Duration::from_secs(1));
    let mut settings = SgClawSettings::from_legacy_deepseek_fields(
        "unused-key".to_string(),
        "http://127.0.0.1:9".to_string(),
        "unused-model".to_string(),
        Some(skill_root.clone()),
    )
    .unwrap();
    settings.direct_submit_skill = Some("fault-details-report.collect_fault_details".to_string());

    let summary = sgclaw::compat::direct_skill_runtime::execute_direct_submit_skill(
        browser_tool,
        "请采集 2026-03 的故障明细并返回结果",
        &CompatTaskContext {
            page_url: Some("https://95598.sgcc.com.cn/".to_string()),
            ..CompatTaskContext::default()
        },
        PathBuf::from(env!("CARGO_MANIFEST_DIR")).as_path(),
        &settings,
    )
    .unwrap();

    assert!(summary.success);
    assert!(summary.summary.contains("fault_type"));
    let sent = transport.sent_messages();
    assert!(sent.iter().all(|message| !matches!(message, AgentMessage::LogEntry { level, message } if level == "info" && message.contains("DeepSeek config loaded"))));
    assert!(matches!(
        &sent[0],
        AgentMessage::Command {
            seq,
            action,
            params,
            security,
        } if *seq == 1
            && action == &Action::Eval
            && security.expected_domain == "95598.sgcc.com.cn"
            && params["script"].as_str().is_some_and(|script| script.contains("2026-03"))
    ));
}

#[test]
fn submit_task_uses_direct_skill_mode_without_llm_configuration() {
    std::env::remove_var("DEEPSEEK_API_KEY");
    std::env::remove_var("DEEPSEEK_BASE_URL");
    std::env::remove_var("DEEPSEEK_MODEL");

    let skill_root = build_direct_runtime_skill_root();
    let runtime_context = direct_submit_runtime_context(&skill_root);
    let transport = Arc::new(MockTransport::new(vec![success_browser_response(
        1,
        serde_json::json!({
            "text": {
                "fault_type": "outage",
                "observed_at": "2026-03-15 09:00",
                "affected_scope": "line-7",
                "artifact_payload": "report artifact payload"
            }
        }),
    )]));
    let browser_tool = BrowserPipeTool::new(
        transport.clone(),
        direct_runtime_test_policy(),
        vec![1, 2, 3, 4, 5, 6, 7, 8],
    )
    .with_response_timeout(Duration::from_secs(1));

    handle_browser_message_with_context(
        transport.as_ref(),
        &browser_tool,
        &runtime_context,
        submit_fault_details_message(),
    )
    .unwrap();

    let sent = transport.sent_messages();
    let completion = direct_submit_completion(&sent).expect("task completion");

    assert!(completion.0, "expected direct submit task to succeed: {sent:?}");
    assert!(
        completion.1.contains("report artifact payload"),
        "expected report artifact payload in summary: {}",
        completion.1
    );
    assert!(
        !completion.1.contains("未配置大语言模型"),
        "did not expect missing-llm summary: {}",
        completion.1
    );
}

#[test]
fn submit_task_rejects_invalid_direct_submit_skill_config_before_routing() {
    std::env::remove_var("DEEPSEEK_API_KEY");
    std::env::remove_var("DEEPSEEK_BASE_URL");
    std::env::remove_var("DEEPSEEK_MODEL");

    let skill_root = build_direct_runtime_skill_root();
    let workspace_root = std::env::temp_dir().join(format!(
        "sgclaw-invalid-direct-submit-workspace-{}",
        Uuid::new_v4()
    ));
    fs::create_dir_all(&workspace_root).unwrap();
    let config_path = workspace_root.join("sgclaw_config.json");
    fs::write(
        &config_path,
        serde_json::json!({
            "providers": [],
            "skillsDir": skill_root,
            "directSubmitSkill": "fault-details-report"
        })
        .to_string(),
    )
    .unwrap();

    let runtime_context = AgentRuntimeContext::new(Some(config_path), workspace_root);
    let transport = Arc::new(MockTransport::new(vec![]));
    let browser_tool = BrowserPipeTool::new(
        transport.clone(),
        direct_runtime_test_policy(),
        vec![1, 2, 3, 4, 5, 6, 7, 8],
    )
    .with_response_timeout(Duration::from_secs(1));

    handle_browser_message_with_context(
        transport.as_ref(),
        &browser_tool,
        &runtime_context,
        submit_fault_details_message(),
    )
    .unwrap();

    let sent = transport.sent_messages();
    assert!(matches!(
        sent.last(),
        Some(AgentMessage::TaskComplete { success, summary })
            if !success && summary.contains("skill.tool")
    ));
    assert!(direct_submit_mode_logs(&sent).is_empty());
    assert!(!sent.iter().any(|message| matches!(message, AgentMessage::Command { .. })));
}

#[test]
fn submit_task_treats_partial_report_artifact_as_success_with_warning_summary() {
    std::env::remove_var("DEEPSEEK_API_KEY");
    std::env::remove_var("DEEPSEEK_BASE_URL");
    std::env::remove_var("DEEPSEEK_MODEL");

    let skill_root = build_direct_runtime_skill_root();
    let runtime_context = direct_submit_runtime_context(&skill_root);
    let transport = Arc::new(MockTransport::new(vec![report_artifact_browser_response(
        1,
        "partial",
        &["report_log_failed"],
        vec![serde_json::json!({ "qxdbh": "QX-1" })],
        vec![serde_json::json!({ "index": 1 })],
    )]));
    let browser_tool = BrowserPipeTool::new(
        transport.clone(),
        direct_runtime_test_policy(),
        vec![1, 2, 3, 4, 5, 6, 7, 8],
    )
    .with_response_timeout(Duration::from_secs(1));

    handle_browser_message_with_context(
        transport.as_ref(),
        &browser_tool,
        &runtime_context,
        submit_fault_details_message(),
    )
    .unwrap();

    let sent = transport.sent_messages();
    let completion = direct_submit_completion(&sent).expect("task completion");

    assert!(completion.0, "expected partial artifact to succeed: {sent:?}");
    assert!(completion.1.contains("fault-details-report"));
    assert!(completion.1.contains("2026-03"));
    assert!(completion.1.contains("status=partial"));
    assert!(completion.1.contains("detail_rows=1"));
    assert!(completion.1.contains("summary_rows=1"));
    assert!(completion.1.contains("report_log_failed"));
}

#[test]
fn submit_task_treats_empty_report_artifact_as_success() {
    std::env::remove_var("DEEPSEEK_API_KEY");
    std::env::remove_var("DEEPSEEK_BASE_URL");
    std::env::remove_var("DEEPSEEK_MODEL");

    let skill_root = build_direct_runtime_skill_root();
    let runtime_context = direct_submit_runtime_context(&skill_root);
    let transport = Arc::new(MockTransport::new(vec![report_artifact_browser_response(
        1,
        "empty",
        &[],
        vec![],
        vec![],
    )]));
    let browser_tool = BrowserPipeTool::new(
        transport.clone(),
        direct_runtime_test_policy(),
        vec![1, 2, 3, 4, 5, 6, 7, 8],
    )
    .with_response_timeout(Duration::from_secs(1));

    handle_browser_message_with_context(
        transport.as_ref(),
        &browser_tool,
        &runtime_context,
        submit_fault_details_message(),
    )
    .unwrap();

    let sent = transport.sent_messages();
    let completion = direct_submit_completion(&sent).expect("task completion");

    assert!(completion.0, "expected empty artifact to succeed: {sent:?}");
    assert!(completion.1.contains("status=empty"));
    assert!(completion.1.contains("detail_rows=0"));
}

#[test]
fn submit_task_treats_blocked_report_artifact_as_failure() {
    std::env::remove_var("DEEPSEEK_API_KEY");
    std::env::remove_var("DEEPSEEK_BASE_URL");
    std::env::remove_var("DEEPSEEK_MODEL");

    let skill_root = build_direct_runtime_skill_root();
    let runtime_context = direct_submit_runtime_context(&skill_root);
    let transport = Arc::new(MockTransport::new(vec![report_artifact_browser_response(
        1,
        "blocked",
        &["selected_range_unavailable"],
        vec![],
        vec![],
    )]));
    let browser_tool = BrowserPipeTool::new(
        transport.clone(),
        direct_runtime_test_policy(),
        vec![1, 2, 3, 4, 5, 6, 7, 8],
    )
    .with_response_timeout(Duration::from_secs(1));

    handle_browser_message_with_context(
        transport.as_ref(),
        &browser_tool,
        &runtime_context,
        submit_fault_details_message(),
    )
    .unwrap();

    let sent = transport.sent_messages();
    let completion = direct_submit_completion(&sent).expect("task completion");

    assert!(!completion.0, "expected blocked artifact to fail: {sent:?}");
    assert!(completion.1.contains("status=blocked"));
    assert!(completion.1.contains("selected_range_unavailable"));
}

#[test]
fn submit_task_treats_error_report_artifact_as_failure() {
    std::env::remove_var("DEEPSEEK_API_KEY");
    std::env::remove_var("DEEPSEEK_BASE_URL");
    std::env::remove_var("DEEPSEEK_MODEL");

    let skill_root = build_direct_runtime_skill_root();
    let runtime_context = direct_submit_runtime_context(&skill_root);
    let transport = Arc::new(MockTransport::new(vec![report_artifact_browser_response(
        1,
        "error",
        &["detail_normalization_failed"],
        vec![],
        vec![],
    )]));
    let browser_tool = BrowserPipeTool::new(
        transport.clone(),
        direct_runtime_test_policy(),
        vec![1, 2, 3, 4, 5, 6, 7, 8],
    )
    .with_response_timeout(Duration::from_secs(1));

    handle_browser_message_with_context(
        transport.as_ref(),
        &browser_tool,
        &runtime_context,
        submit_fault_details_message(),
    )
    .unwrap();

    let sent = transport.sent_messages();
    let completion = direct_submit_completion(&sent).expect("task completion");

    assert!(!completion.0, "expected error artifact to fail: {sent:?}");
    assert!(completion.1.contains("status=error"));
    assert!(completion.1.contains("detail_normalization_failed"));
}

#[test]
fn submit_task_routes_zhihu_hotlist_export_before_direct_submit() {
    std::env::remove_var("DEEPSEEK_API_KEY");
    std::env::remove_var("DEEPSEEK_BASE_URL");
    std::env::remove_var("DEEPSEEK_MODEL");

    let skill_root = build_direct_runtime_skill_root();
    let runtime_context = direct_submit_runtime_context(&skill_root);
    let transport = Arc::new(MockTransport::new(vec![]));
    let browser_tool = BrowserPipeTool::new(
        transport.clone(),
        policy_for_domains(&["www.zhihu.com"]),
        vec![1, 2, 3, 4, 5, 6, 7, 8],
    )
    .with_response_timeout(Duration::from_secs(1));

    handle_browser_message_with_context(
        transport.as_ref(),
        &browser_tool,
        &runtime_context,
        submit_zhihu_hotlist_export_message(),
    )
    .unwrap();

    let sent = transport.sent_messages();
    let mode_logs = direct_submit_mode_logs(&sent);
    let completion = direct_submit_completion(&sent).expect("task completion");

    assert_eq!(mode_logs, vec!["zeroclaw_process_message_primary".to_string()]);
    assert!(
        !completion.0,
        "expected zhihu export without page context to fail before browser actions: {sent:?}"
    );
    assert!(
        !completion
            .1
            .contains("direct submit skill requires page_url so expected_domain can be derived"),
        "unexpected direct submit fallback: {sent:?}"
    );
}

#[test]
fn direct_skill_mode_logs_direct_skill_primary() {
    std::env::remove_var("DEEPSEEK_API_KEY");
    std::env::remove_var("DEEPSEEK_BASE_URL");
    std::env::remove_var("DEEPSEEK_MODEL");

    let skill_root = build_direct_runtime_skill_root();
    let runtime_context = direct_submit_runtime_context(&skill_root);
    let transport = Arc::new(MockTransport::new(vec![success_browser_response(
        1,
        serde_json::json!({
            "text": {
                "fault_type": "outage",
                "observed_at": "2026-03-15 09:00",
                "affected_scope": "line-7",
                "artifact_payload": "report artifact payload"
            }
        }),
    )]));
    let browser_tool = BrowserPipeTool::new(
        transport.clone(),
        direct_runtime_test_policy(),
        vec![1, 2, 3, 4, 5, 6, 7, 8],
    )
    .with_response_timeout(Duration::from_secs(1));

    handle_browser_message_with_context(
        transport.as_ref(),
        &browser_tool,
        &runtime_context,
        submit_fault_details_message(),
    )
    .unwrap();

    let sent = transport.sent_messages();
    let mode_logs = direct_submit_mode_logs(&sent);

    assert_eq!(mode_logs, vec!["direct_skill_primary".to_string()]);
    assert!(
        !mode_logs.iter().any(|mode| mode == "compat_llm_primary"),
        "unexpected compat mode logs: {mode_logs:?}"
    );
    assert!(
        !mode_logs
            .iter()
            .any(|mode| mode == "zeroclaw_process_message_primary"),
        "unexpected zeroclaw mode logs: {mode_logs:?}"
    );
}

#[test]
fn production_submit_task_with_ws_and_direct_submit_config_routes_zhihu_before_direct_submit() {
    let _guard = env_lock().lock().unwrap_or_else(|err| err.into_inner());
    std::env::set_var("SGCLAW_DISABLE_POST_EXPORT_OPEN", "1");
    std::env::remove_var("DEEPSEEK_API_KEY");
    std::env::remove_var("DEEPSEEK_BASE_URL");
    std::env::remove_var("DEEPSEEK_MODEL");

    let workspace_root = temp_workspace_root();
    let (ws_url, _frames, ws_handle) = start_browser_ws_server();
    let config_path = write_config(
        &workspace_root,
        "deepseek-test-key",
        "http://127.0.0.1:9",
        "deepseek-chat",
        Some(real_skill_lib_root().to_str().unwrap()),
        Some(&ws_url),
        Some("fault-details-report.collect_fault_details"),
    );

    let transport = Arc::new(MockTransport::new(vec![]));
    let browser_tool = BrowserPipeTool::new(
        transport.clone(),
        test_policy(),
        vec![1, 2, 3, 4, 5, 6, 7, 8],
    )
    .with_response_timeout(Duration::from_secs(1));
    let runtime_context = AgentRuntimeContext::new(Some(config_path), workspace_root);

    handle_browser_message_with_context(
        transport.as_ref(),
        &browser_tool,
        &runtime_context,
        BrowserMessage::SubmitTask {
            instruction: "打开知乎热榜，获取前10条数据，并导出 Excel".to_string(),
            conversation_id: String::new(),
            messages: vec![],
            page_url: String::new(),
            page_title: String::new(),
        },
    )
    .unwrap();

    ws_handle.join().unwrap();

    let sent = transport.sent_messages();
    let mode_logs = direct_submit_mode_logs(&sent);
    let completion = direct_submit_completion(&sent).expect("task completion");

    assert!(
        mode_logs
            .iter()
            .any(|mode| mode == "zeroclaw_process_message_primary"),
        "expected orchestration mode log before direct submit: {sent:?}"
    );
    assert!(
        !mode_logs.iter().any(|mode| mode == "direct_skill_primary"),
        "unexpected direct submit mode log for zhihu ws submit: {sent:?}"
    );
    assert!(completion.0, "expected zhihu ws submit to succeed: {sent:?}");
    assert!(
        !completion
            .1
            .contains("direct submit skill requires page_url so expected_domain can be derived"),
        "unexpected direct-submit page_url failure: {sent:?}"
    );

    std::env::remove_var("SGCLAW_DISABLE_POST_EXPORT_OPEN");
}

#[test]
fn production_submit_task_routes_zhihu_through_ws_backend_without_helper_bootstrap() {
    let _guard = env_lock().lock().unwrap_or_else(|err| err.into_inner());
    std::env::set_var("SGCLAW_DISABLE_POST_EXPORT_OPEN", "1");

    std::env::remove_var("DEEPSEEK_API_KEY");
    std::env::remove_var("DEEPSEEK_BASE_URL");
    std::env::remove_var("DEEPSEEK_MODEL");

    let workspace_root = temp_workspace_root();
    let (ws_url, frames, ws_handle) = start_browser_ws_server();
    let config_path = write_config(
        &workspace_root,
        "deepseek-test-key",
        "http://127.0.0.1:9",
        "deepseek-chat",
        Some(real_skill_lib_root().to_str().unwrap()),
        Some(&ws_url),
        None,
    );

    let transport = Arc::new(MockTransport::new(vec![]));
    let browser_tool = BrowserPipeTool::new(
        transport.clone(),
        test_policy(),
        vec![1, 2, 3, 4, 5, 6, 7, 8],
    )
    .with_response_timeout(Duration::from_secs(1));
    let runtime_context = AgentRuntimeContext::new(Some(config_path), workspace_root.clone());

    handle_browser_message_with_context(
        transport.as_ref(),
        &browser_tool,
        &runtime_context,
        BrowserMessage::SubmitTask {
            instruction: "打开知乎热榜，获取前10条数据，并导出 Excel".to_string(),
            conversation_id: String::new(),
            messages: vec![],
            page_url: String::new(),
            page_title: String::new(),
        },
    )
    .unwrap();

    ws_handle.join().unwrap();

    let sent = transport.sent_messages();
    let websocket_frames = frames.lock().unwrap().clone();

    assert_eq!(websocket_frames.len(), 4, "{websocket_frames:?}");
    assert_eq!(websocket_frames[0], r#"{"type":"register","role":"web"}"#);
    assert!(!websocket_frames
        .iter()
        .any(|frame| frame.contains("/sgclaw/browser-helper.html")));
    assert!(!websocket_frames
        .iter()
        .any(|frame| frame.contains("\"sgBrowerserOpenPage\"")));

    let navigate: Value = serde_json::from_str(&websocket_frames[1]).unwrap();
    assert_eq!(navigate[0], json!("https://www.zhihu.com"));
    assert_eq!(navigate[1], json!("sgHideBrowserCallAfterLoaded"));
    assert_eq!(navigate[2], json!("https://www.zhihu.com/hot"));

    let get_text: Value = serde_json::from_str(&websocket_frames[2]).unwrap();
    assert_eq!(get_text[0], json!("https://www.zhihu.com/hot"));
    assert_eq!(get_text[1], json!("sgBrowserExcuteJsCodeByArea"));
    assert_eq!(get_text[2], json!("https://www.zhihu.com/hot"));

    let eval: Value = serde_json::from_str(&websocket_frames[3]).unwrap();
    assert_eq!(eval[0], json!("https://www.zhihu.com/hot"));
    assert_eq!(eval[1], json!("sgBrowserExcuteJsCodeByArea"));
    assert_eq!(eval[2], json!("https://www.zhihu.com/hot"));

    assert!(sent.iter().any(|message| {
        matches!(
            message,
            AgentMessage::LogEntry { level, message }
                if level == "mode" && message == "zeroclaw_process_message_primary"
        )
    }));
    assert!(sent.iter().any(|message| {
        matches!(
            message,
            AgentMessage::TaskComplete { success, summary }
                if *success && summary.contains("已导出并打开知乎热榜 Excel") && summary.contains(".xlsx")
        )
    }));
    std::env::remove_var("SGCLAW_DISABLE_POST_EXPORT_OPEN");
    assert!(!sent.iter().any(|message| matches!(message, AgentMessage::Command { .. })));
}

#[test]
fn lifecycle_messages_emit_status_events_without_browser_commands() {
    let transport = Arc::new(MockTransport::new(vec![]));
    let browser_tool = BrowserPipeTool::new(
        transport.clone(),
        test_policy(),
        vec![1, 2, 3, 4, 5, 6, 7, 8],
    )
    .with_response_timeout(Duration::from_secs(1));

    sgclaw::agent::handle_browser_message(transport.as_ref(), &browser_tool, BrowserMessage::Connect)
        .unwrap();
    sgclaw::agent::handle_browser_message(transport.as_ref(), &browser_tool, BrowserMessage::Start)
        .unwrap();
    sgclaw::agent::handle_browser_message(transport.as_ref(), &browser_tool, BrowserMessage::Stop)
        .unwrap();

    let sent = transport.sent_messages();
    assert_eq!(
        sent,
        vec![
            AgentMessage::StatusChanged {
                state: "connected".to_string(),
            },
            AgentMessage::StatusChanged {
                state: "started".to_string(),
            },
            AgentMessage::StatusChanged {
                state: "stopped".to_string(),
            },
        ]
    );
    assert!(!sent
        .iter()
        .any(|message| { matches!(message, AgentMessage::Command { .. }) }));
}

#[test]
fn production_submit_task_does_not_route_into_legacy_runtime_without_llm_config() {
    std::env::remove_var("DEEPSEEK_API_KEY");
    std::env::remove_var("DEEPSEEK_BASE_URL");
    std::env::remove_var("DEEPSEEK_MODEL");

    let transport = Arc::new(MockTransport::new(vec![]));
    let browser_tool = BrowserPipeTool::new(
        transport.clone(),
        provider_path_test_policy(),
        vec![1, 2, 3, 4, 5, 6, 7, 8],
    )
    .with_response_timeout(Duration::from_secs(1));

    handle_browser_message(
        transport.as_ref(),
        &browser_tool,
        BrowserMessage::SubmitTask {
            instruction: "打开百度".to_string(),
            conversation_id: String::new(),
            messages: vec![],
            page_url: String::new(),
            page_title: String::new(),
        },
    )
    .unwrap();

    let sent = transport.sent_messages();
    assert!(matches!(
        sent.last(),
        Some(AgentMessage::TaskComplete { success, summary })
            if !success && summary.contains("未配置大语言模型")
    ));
    assert!(!sent
        .iter()
        .any(|message| { matches!(message, AgentMessage::Command { .. }) }));
}
