use std::net::TcpStream;
use std::sync::{Arc, Mutex};
use std::time::Duration;

#[cfg(test)]
use reqwest::blocking::Client;
#[cfg(test)]
use serde_json::{json, Map};
#[cfg(test)]
use serde_json::Value;
use tungstenite::stream::MaybeTlsStream;
use tungstenite::{connect, Message, WebSocket};

use crate::agent::{
    run_submit_task_with_browser_backend, AgentEventSink, AgentRuntimeContext,
};
use crate::browser::callback_host::LiveBrowserCallbackHost;
use crate::browser::ws_backend::WsClient;
#[cfg(test)]
use crate::browser::bridge_contract::{
    BridgeBrowserActionError, BridgeBrowserActionReply, BridgeBrowserActionRequest,
    BridgeBrowserActionSuccess,
};
#[cfg(test)]
use crate::browser::bridge_transport::BridgeActionTransport;
use crate::browser::{BrowserBackend, BrowserCallbackBackend};
#[cfg(test)]
use crate::browser::BridgeBrowserBackend;
use crate::pipe::{AgentMessage, BrowserMessage, PipeError, Transport};
#[cfg(test)]
use crate::pipe::Timing;
use crate::security::MacPolicy;

use super::{ClientMessage, ServiceMessage};

const BROWSER_RESPONSE_TIMEOUT: Duration = Duration::from_secs(30);
#[cfg(test)]
const DEFAULT_BRIDGE_BASE_URL: &str = "http://localhost:23323";

#[derive(Debug, Default)]
pub struct ServiceSession {
    attached: Mutex<bool>,
    task_running: Mutex<bool>,
}

impl ServiceSession {
    pub fn new() -> Self {
        Self {
            attached: Mutex::new(false),
            task_running: Mutex::new(false),
        }
    }

    pub fn try_attach_client(&self) -> Result<(), ServiceMessage> {
        let mut attached = self.attached.lock().unwrap();
        if *attached {
            return Err(ServiceMessage::Busy {
                message: "service already has an attached client".to_string(),
            });
        }
        *attached = true;
        Ok(())
    }

    pub fn detach_client(&self) {
        let mut attached = self.attached.lock().unwrap();
        *attached = false;

        let mut task_running = self.task_running.lock().unwrap();
        *task_running = false;
    }

    pub fn try_start_task(&self) -> Result<(), ServiceMessage> {
        let attached = self.attached.lock().unwrap();
        if !*attached {
            return Err(ServiceMessage::Busy {
                message: "service has no attached client".to_string(),
            });
        }
        drop(attached);

        let mut task_running = self.task_running.lock().unwrap();
        if *task_running {
            return Err(ServiceMessage::Busy {
                message: "service already has a running task".to_string(),
            });
        }
        *task_running = true;
        Ok(())
    }

    pub fn finish_task(&self) {
        let mut task_running = self.task_running.lock().unwrap();
        *task_running = false;
    }
}

#[derive(Debug)]
pub struct ServiceEventSink {
    sent: Mutex<Vec<ServiceMessage>>,
    writer: Option<Mutex<WebSocket<TcpStream>>>,
}

impl Default for ServiceEventSink {
    fn default() -> Self {
        Self {
            sent: Mutex::new(Vec::new()),
            writer: None,
        }
    }
}

impl ServiceEventSink {
    pub fn from_websocket(websocket: WebSocket<TcpStream>) -> Self {
        Self {
            sent: Mutex::new(Vec::new()),
            writer: Some(Mutex::new(websocket)),
        }
    }

    pub fn send_service_message(&self, message: ServiceMessage) -> Result<(), PipeError> {
        self.sent.lock().unwrap().push(message.clone());
        if let Some(writer) = &self.writer {
            let payload = serde_json::to_string(&message)?;
            writer
                .lock()
                .map_err(|_| PipeError::Protocol("service websocket writer lock poisoned".to_string()))?
                .send(Message::Text(payload.into()))
                .map_err(|err| map_service_websocket_error(err, "send"))?;
        }
        Ok(())
    }

    pub fn recv_client_message(&self) -> Result<Option<ClientMessage>, PipeError> {
        let Some(writer) = &self.writer else {
            return Err(PipeError::Protocol(
                "service sink has no websocket reader".to_string(),
            ));
        };

        loop {
            let mut websocket = writer
                .lock()
                .map_err(|_| PipeError::Protocol("service websocket writer lock poisoned".to_string()))?;
            match websocket.read() {
                Ok(Message::Text(text)) => return Ok(Some(serde_json::from_str(&text)?)),
                Ok(Message::Close(_)) => return Ok(None),
                Ok(Message::Ping(payload)) => {
                    websocket
                        .send(Message::Pong(payload))
                        .map_err(|err| PipeError::Protocol(format!("service websocket pong failed: {err}")))?;
                }
                Ok(_) => {}
                Err(tungstenite::Error::ConnectionClosed) | Err(tungstenite::Error::AlreadyClosed) => {
                    return Ok(None)
                }
                Err(err) => return Err(map_service_websocket_error(err, "read")),
            }
        }
    }

    pub fn sent_messages(&self) -> Vec<ServiceMessage> {
        self.sent.lock().unwrap().clone()
    }
}

impl AgentEventSink for ServiceEventSink {
    fn send(&self, message: &AgentMessage) -> Result<(), PipeError> {
        let mapped = match message {
            AgentMessage::StatusChanged { state } => ServiceMessage::StatusChanged {
                state: state.clone(),
            },
            AgentMessage::LogEntry { level, message } => ServiceMessage::LogEntry {
                level: level.clone(),
                message: message.clone(),
            },
            AgentMessage::TaskComplete { success, summary } => ServiceMessage::TaskComplete {
                success: *success,
                summary: summary.clone(),
            },
            _ => {
                return Err(PipeError::Protocol(
                    "unsupported agent message for service sink".to_string(),
                ))
            }
        };

        self.send_service_message(mapped)
    }
}

fn map_service_websocket_error(err: tungstenite::Error, operation: &str) -> PipeError {
    match err {
        tungstenite::Error::ConnectionClosed
        | tungstenite::Error::AlreadyClosed
        | tungstenite::Error::Protocol(tungstenite::error::ProtocolError::ResetWithoutClosingHandshake)
        | tungstenite::Error::Protocol(tungstenite::error::ProtocolError::SendAfterClosing) => {
            PipeError::PipeClosed
        }
        tungstenite::Error::Io(io_err)
            if matches!(
                io_err.kind(),
                std::io::ErrorKind::TimedOut | std::io::ErrorKind::WouldBlock
            ) =>
        {
            PipeError::Timeout
        }
        tungstenite::Error::Io(io_err)
            if matches!(
                io_err.kind(),
                std::io::ErrorKind::ConnectionAborted
                    | std::io::ErrorKind::ConnectionReset
                    | std::io::ErrorKind::BrokenPipe
                    | std::io::ErrorKind::UnexpectedEof
            ) =>
        {
            PipeError::PipeClosed
        }
        tungstenite::Error::Io(io_err) => {
            PipeError::Protocol(format!("service websocket {operation} failed: {io_err}"))
        }
        other => PipeError::Protocol(format!("service websocket {operation} failed: {other}")),
    }
}

fn send_status_changed(sink: &ServiceEventSink, state: &str) -> Result<(), PipeError> {
    sink.send(&AgentMessage::StatusChanged {
        state: state.to_string(),
    })
}

pub fn serve_client(
    context: &AgentRuntimeContext,
    session: &ServiceSession,
    sink: Arc<ServiceEventSink>,
    browser_ws_url: &str,
    mac_policy: &MacPolicy,
) -> Result<(), PipeError> {
    // Cache the browser callback host across tasks so the helper page tab is
    // opened only once per client session instead of once per task.
    let mut cached_host: Option<Arc<LiveBrowserCallbackHost>> = None;

    loop {
        let Some(message) = sink.recv_client_message()? else {
            return Ok(());
        };

        match message {
            ClientMessage::Connect => send_status_changed(sink.as_ref(), "connected")?,
            ClientMessage::Start => send_status_changed(sink.as_ref(), "started")?,
            ClientMessage::Stop => send_status_changed(sink.as_ref(), "stopped")?,
            ClientMessage::Ping => sink.send_service_message(ServiceMessage::Pong)?,
            ClientMessage::SubmitTask {
                instruction,
                conversation_id,
                messages,
                page_url,
                page_title,
            } => {
                let request = ClientMessage::SubmitTask {
                    instruction,
                    conversation_id,
                    messages,
                    page_url: page_url.clone(),
                    page_title,
                }
                .into_submit_task_request()
                .expect("submit task request");
                if let Err(message) = session.try_start_task() {
                    sink.send_service_message(message)?;
                    continue;
                }

                if request.instruction.trim().is_empty() {
                    let result = sink.send(&AgentMessage::TaskComplete {
                        success: false,
                        summary: "请输入任务内容。".to_string(),
                    });
                    session.finish_task();
                    result?;
                    continue;
                }

                // Lazily create and cache the browser callback host. On first
                // task it opens the helper page; subsequent tasks reuse it.
                if cached_host.is_none() {
                    let bootstrap_url = initial_request_url_for_submit_task(&request);
                    match LiveBrowserCallbackHost::start_with_browser_ws_url(
                        browser_ws_url,
                        &bootstrap_url,
                        Duration::from_secs(15),
                        BROWSER_RESPONSE_TIMEOUT,
                        false, // use_hidden_domain: visible tab for now
                    ) {
                        Ok(host) => {
                            cached_host = Some(Arc::new(host));
                        }
                        Err(err) => {
                            session.finish_task();
                            eprintln!("task execution failed: {err}");
                            sink.send(&AgentMessage::TaskComplete {
                                success: false,
                                summary: format!("任务执行失败: {err}"),
                            })?;
                            continue;
                        }
                    }
                } else {
                    cached_host.as_ref().unwrap().reset_pending_state();
                }

                let host = cached_host.as_ref().unwrap();
                let browser_backend: Arc<dyn BrowserBackend> =
                    Arc::new(BrowserCallbackBackend::new(
                        host.clone(),
                        mac_policy.clone(),
                        host.helper_url().to_string(),
                    ));

                let result = run_submit_task_with_browser_backend(
                    &NoopTransport,
                    sink.as_ref(),
                    browser_backend,
                    context,
                    request,
                );
                session.finish_task();
                match result {
                    Ok(()) => {}
                    Err(PipeError::PipeClosed) => return Err(PipeError::PipeClosed),
                    Err(err) => {
                        eprintln!("task execution failed: {err}");
                        sink.send(&AgentMessage::TaskComplete {
                            success: false,
                            summary: format!("任务执行失败: {err}"),
                        })?;
                    }
                }
            }
        }
    }
}

pub(crate) fn initial_request_url_for_submit_task(request: &crate::agent::SubmitTaskRequest) -> String {
    request
        .page_url
        .as_deref()
        .map(str::trim)
        .filter(|value| !value.is_empty())
        .map(ToString::to_string)
        .or_else(|| derive_request_url_from_instruction(&request.instruction))
        .unwrap_or_else(|| "about:blank".to_string())
}

fn derive_request_url_from_instruction(instruction: &str) -> Option<String> {
    if crate::compat::workflow_executor::detect_route(instruction, None, None)
        .is_some_and(|route| {
            matches!(
                route,
                crate::compat::workflow_executor::WorkflowRoute::ZhihuHotlistExportXlsx
                    | crate::compat::workflow_executor::WorkflowRoute::ZhihuHotlistScreen
                    | crate::compat::workflow_executor::WorkflowRoute::ZhihuArticleEntry
                    | crate::compat::workflow_executor::WorkflowRoute::ZhihuArticleAutoPublishGenerated
            )
        })
    {
        return Some("https://www.zhihu.com".to_string());
    }

    if crate::compat::workflow_executor::detect_route(instruction, None, None)
        .is_some_and(|route| {
            matches!(
                route,
                crate::compat::workflow_executor::WorkflowRoute::ZhihuArticleDraft
                    | crate::compat::workflow_executor::WorkflowRoute::ZhihuArticlePublish
            )
        })
    {
        return Some("https://zhuanlan.zhihu.com".to_string());
    }

    // 台区线损相关
    // TODO: 临时方案，后续应从 skill 配置或 deterministic_submit 解析结果中获取
    if instruction.contains("线损") || instruction.contains("lineloss") {
        return Some("http://20.76.57.61:18080".to_string());
    }

    None
}

pub(crate) struct ServiceWsClient {
    websocket: Mutex<WebSocket<MaybeTlsStream<TcpStream>>>,
}

impl ServiceWsClient {
    pub(crate) fn connect(browser_ws_url: &str) -> Result<Self, PipeError> {
        let (mut websocket, _) = connect(browser_ws_url)
            .map_err(|err| PipeError::Protocol(format!("browser websocket connect failed: {err}")))?;
        configure_browser_ws_timeouts(&mut websocket, BROWSER_RESPONSE_TIMEOUT)?;
        websocket
            .send(Message::Text(
                r#"{"type":"register","role":"web"}"#.to_string().into(),
            ))
            .map_err(|err| map_service_websocket_error(err, "register"))?;
        Ok(Self {
            websocket: Mutex::new(websocket),
        })
    }
}

impl WsClient for ServiceWsClient {
    fn send_text(&self, payload: &str) -> Result<(), PipeError> {
        self.websocket
            .lock()
            .map_err(|_| PipeError::Protocol("browser websocket lock poisoned".to_string()))?
            .send(Message::Text(payload.to_string().into()))
            .map_err(|err| map_service_websocket_error(err, "send"))
    }

    fn recv_text_timeout(&self, timeout: Duration) -> Result<String, PipeError> {
        let mut websocket = self
            .websocket
            .lock()
            .map_err(|_| PipeError::Protocol("browser websocket lock poisoned".to_string()))?;
        set_plain_browser_ws_read_timeout(&mut websocket, Some(timeout))?;
        loop {
            match websocket.read() {
                Ok(Message::Text(text)) => return Ok(text.to_string()),
                Ok(Message::Close(_)) => return Err(PipeError::PipeClosed),
                Ok(Message::Ping(payload)) => {
                    websocket
                        .send(Message::Pong(payload))
                        .map_err(|err| map_service_websocket_error(err, "pong"))?;
                }
                Ok(_) => {}
                Err(err) => return Err(map_service_websocket_error(err, "read")),
            }
        }
    }
}

fn configure_browser_ws_timeouts(
    websocket: &mut WebSocket<MaybeTlsStream<TcpStream>>,
    timeout: Duration,
) -> Result<(), PipeError> {
    match websocket.get_mut() {
        MaybeTlsStream::Plain(stream) => {
            stream.set_read_timeout(Some(timeout))?;
            stream.set_write_timeout(Some(timeout))?;
            Ok(())
        }
        _ => Ok(()),
    }
}

fn set_plain_browser_ws_read_timeout(
    websocket: &mut WebSocket<MaybeTlsStream<TcpStream>>,
    timeout: Option<Duration>,
) -> Result<(), PipeError> {
    match websocket.get_mut() {
        MaybeTlsStream::Plain(stream) => {
            stream.set_read_timeout(timeout)?;
            Ok(())
        }
        _ => Ok(()),
    }
}

struct NoopTransport;

impl Transport for NoopTransport {
    fn send(&self, _message: &AgentMessage) -> Result<(), PipeError> {
        Ok(())
    }

    fn recv_timeout(&self, _timeout: Duration) -> Result<BrowserMessage, PipeError> {
        Err(PipeError::Timeout)
    }
}

#[cfg(test)]
mod pipe_closed_mapping_tests {
    use super::*;

    #[test]
    fn map_service_websocket_error_treats_connection_aborted_send_as_pipe_closed() {
        let err = tungstenite::Error::Io(std::io::Error::from(std::io::ErrorKind::ConnectionAborted));
        assert!(matches!(map_service_websocket_error(err, "send"), PipeError::PipeClosed));
    }

    #[test]
    fn map_service_websocket_error_treats_send_after_closing_as_pipe_closed() {
        let err = tungstenite::Error::Protocol(tungstenite::error::ProtocolError::SendAfterClosing);
        assert!(matches!(map_service_websocket_error(err, "send"), PipeError::PipeClosed));
    }
}

#[cfg(test)]
struct ServiceBridgeTransport {
    bridge_base_url: String,
    response_timeout: Duration,
}

#[cfg(test)]
impl ServiceBridgeTransport {
    fn with_timeout(bridge_base_url: String, response_timeout: Duration) -> Self {
        Self {
            bridge_base_url,
            response_timeout,
        }
    }

    fn endpoint_url(&self, action: &str) -> String {
        format!("{}/{}", self.bridge_base_url.trim_end_matches('/'), action)
    }
}

#[cfg(test)]
impl BridgeActionTransport for ServiceBridgeTransport {
    fn execute(
        &self,
        request: BridgeBrowserActionRequest,
    ) -> Result<BridgeBrowserActionReply, PipeError> {
        let url = self.endpoint_url(&request.action);
        let response_timeout = self.response_timeout;
        std::thread::spawn(move || execute_bridge_http_request(url, response_timeout, request))
            .join()
            .map_err(|_| PipeError::Protocol("browser bridge worker thread panicked".to_string()))?
    }
}

#[cfg(test)]
fn execute_bridge_http_request(
    url: String,
    response_timeout: Duration,
    request: BridgeBrowserActionRequest,
) -> Result<BridgeBrowserActionReply, PipeError> {
    let BridgeBrowserActionRequest {
        action,
        params,
        expected_domain,
    } = request;
    let payload = json!([action, params, expected_domain]);
    let client = Client::builder()
        .timeout(response_timeout)
        .build()
        .unwrap_or_else(|_| Client::new());
    let response = client
        .post(url)
        .json(&payload)
        .send()
        .map_err(map_bridge_http_error)?;
    let status = response.status();
    if !status.is_success() {
        return Err(PipeError::Protocol(format!(
            "browser bridge request failed: HTTP {status}"
        )));
    }
    let value: Value = response.json().map_err(|err| {
        PipeError::Protocol(format!("browser bridge response decode failed: {err}"))
    })?;
    normalize_bridge_action_reply(value)
}

#[cfg(test)]
fn bridge_base_url_from_browser_ws_url(browser_ws_url: &str) -> String {
    let trimmed = browser_ws_url.trim();
    if trimmed.is_empty() {
        return DEFAULT_BRIDGE_BASE_URL.to_string();
    }

    if trimmed.starts_with("http://") || trimmed.starts_with("https://") {
        return trimmed.trim_end_matches('/').to_string();
    }

    let normalized = trimmed
        .strip_prefix("ws://")
        .map(|rest| format!("http://{rest}"))
        .or_else(|| trimmed.strip_prefix("wss://").map(|rest| format!("https://{rest}")))
        .unwrap_or_else(|| trimmed.to_string());

    let Ok(parsed) = reqwest::Url::parse(&normalized) else {
        return DEFAULT_BRIDGE_BASE_URL.to_string();
    };

    let host = parsed.host_str().unwrap_or("localhost");
    let is_default_browser_ws = parsed.scheme() == "http"
        && parsed.port_or_known_default() == Some(12345)
        && matches!(host, "127.0.0.1" | "localhost");
    if is_default_browser_ws {
        return DEFAULT_BRIDGE_BASE_URL.to_string();
    }

    let mut base = format!("{}://{}", parsed.scheme(), host);
    if let Some(port) = parsed.port() {
        base.push(':');
        base.push_str(&port.to_string());
    }
    base
}

#[cfg(test)]
fn map_bridge_http_error(err: reqwest::Error) -> PipeError {
    if err.is_timeout() {
        PipeError::Timeout
    } else {
        PipeError::Protocol(format!("browser bridge request failed: {err}"))
    }
}

#[cfg(test)]
fn normalize_bridge_action_reply(value: Value) -> Result<BridgeBrowserActionReply, PipeError> {
    if let Ok(reply) = serde_json::from_value::<BridgeBrowserActionReply>(value.clone()) {
        return Ok(reply);
    }

    match value {
        Value::Number(number) if number.as_i64() == Some(0) => Ok(bridge_success_reply(serde_json::json!({}))),
        Value::String(text) if text.trim() == "0" => Ok(bridge_success_reply(serde_json::json!({}))),
        Value::Object(object) => normalize_bridge_action_reply_object(object),
        other => Err(PipeError::Protocol(format!(
            "invalid browser bridge reply: {other}"
        ))),
    }
}

#[cfg(test)]
fn normalize_bridge_action_reply_object(
    object: Map<String, Value>,
) -> Result<BridgeBrowserActionReply, PipeError> {
    if let Some(success) = object.get("success").and_then(Value::as_bool) {
        return Ok(if success {
            bridge_success_reply(success_data_from_object(&object))
        } else {
            bridge_error_reply(error_message_from_object(&object), error_details_from_object(&object))
        });
    }

    if let Some(code) = object.get("code").and_then(Value::as_i64) {
        return Ok(if code == 0 {
            bridge_success_reply(success_data_from_object(&object))
        } else {
            bridge_error_reply(
                error_message_from_object(&object),
                object_to_value(object.clone()),
            )
        });
    }

    if object.contains_key("data") || object.contains_key("result") || object.contains_key("text") {
        return Ok(bridge_success_reply(success_data_from_object(&object)));
    }

    Err(PipeError::Protocol(format!(
        "invalid browser bridge reply: {}",
        object_to_value(object)
    )))
}

#[cfg(test)]
fn bridge_success_reply(data: Value) -> BridgeBrowserActionReply {
    BridgeBrowserActionReply::Success(BridgeBrowserActionSuccess {
        data,
        aom_snapshot: vec![],
        timing: Timing {
            queue_ms: 0,
            exec_ms: 0,
        },
    })
}

#[cfg(test)]
fn bridge_error_reply(message: String, details: Value) -> BridgeBrowserActionReply {
    BridgeBrowserActionReply::Error(BridgeBrowserActionError { message, details })
}

#[cfg(test)]
fn success_data_from_object(object: &Map<String, Value>) -> Value {
    object
        .get("data")
        .cloned()
        .or_else(|| object.get("result").cloned())
        .or_else(|| object.get("text").cloned().map(|text| json!({ "text": text })))
        .unwrap_or_else(|| json!({}))
}

#[cfg(test)]
fn error_message_from_object(object: &Map<String, Value>) -> String {
    object
        .get("message")
        .and_then(Value::as_str)
        .or_else(|| object.get("error").and_then(Value::as_str))
        .map(ToString::to_string)
        .unwrap_or_else(|| "browser bridge action failed".to_string())
}

#[cfg(test)]
fn error_details_from_object(object: &Map<String, Value>) -> Value {
    object
        .get("details")
        .cloned()
        .unwrap_or_else(|| object_to_value(object.clone()))
}

#[cfg(test)]
fn object_to_value(object: Map<String, Value>) -> Value {
    Value::Object(object)
}

#[cfg(test)]
fn read_http_json_body(stream: &mut impl std::io::Read) -> Value {
    let mut buffer = Vec::new();
    let mut headers_end = None;

    while headers_end.is_none() {
        let mut chunk = [0_u8; 1024];
        let bytes = std::io::Read::read(stream, &mut chunk).unwrap();
        assert!(bytes > 0, "unexpected EOF while reading headers");
        buffer.extend_from_slice(&chunk[..bytes]);
        headers_end = buffer.windows(4).position(|window| window == b"\r\n\r\n");
    }

    let headers_end = headers_end.unwrap() + 4;
    let headers = String::from_utf8(buffer[..headers_end].to_vec()).unwrap();
    let content_length = headers
        .lines()
        .find_map(|line| {
            let (name, value) = line.split_once(':')?;
            name.eq_ignore_ascii_case("content-length")
                .then(|| value.trim().parse::<usize>().unwrap())
        })
        .unwrap_or(0);

    while buffer.len() < headers_end + content_length {
        let mut chunk = vec![0_u8; content_length];
        let bytes = std::io::Read::read(stream, &mut chunk).unwrap();
        assert!(bytes > 0, "unexpected EOF while reading body");
        buffer.extend_from_slice(&chunk[..bytes]);
    }

    serde_json::from_slice(&buffer[headers_end..headers_end + content_length]).unwrap()
}

#[cfg(test)]
fn write_http_json_response(stream: &mut impl std::io::Write, status: &str, body: &Value) {
    let payload = body.to_string();
    let response = format!(
        "HTTP/1.1 {status}\r\nContent-Type: application/json\r\nContent-Length: {}\r\nConnection: close\r\n\r\n{}",
        payload.len(),
        payload
    );
    std::io::Write::write_all(stream, response.as_bytes()).unwrap();
    std::io::Write::flush(stream).unwrap();
}

#[cfg(test)]
mod tests {
    use super::*;
    use std::net::TcpListener;
    use std::sync::{mpsc, Arc};
    use std::thread;

    use serde_json::json;

    use crate::agent::SubmitTaskRequest;
    use crate::browser::BrowserBackend;
    use crate::pipe::Action;

    fn service_test_policy() -> MacPolicy {
        MacPolicy::from_json_str(
            r#"{
                "version": "1.0",
                "domains": { "allowed": ["www.baidu.com", "www.zhihu.com"] },
                "pipe_actions": {
                    "allowed": ["navigate", "getText", "eval"],
                    "blocked": []
                }
            }"#,
        )
        .unwrap()
    }

    #[test]
    fn initial_request_url_prefers_submit_task_page_url() {
        let request = SubmitTaskRequest {
            instruction: "打开知乎热榜".to_string(),
            page_url: Some(" https://www.zhihu.com/ ".to_string()),
            ..SubmitTaskRequest::default()
        };

        assert_eq!(
            initial_request_url_for_submit_task(&request),
            "https://www.zhihu.com/"
        );
    }

    #[test]
    fn initial_request_url_falls_back_to_zhihu_origin_for_hotlist_routes() {
        let request = SubmitTaskRequest {
            instruction: "打开知乎热榜，获取前10条数据，并导出 Excel".to_string(),
            ..SubmitTaskRequest::default()
        };

        assert_eq!(
            initial_request_url_for_submit_task(&request),
            "https://www.zhihu.com"
        );
    }

    #[test]
    fn initial_request_url_falls_back_to_zhihu_origin_for_generated_article_publish_routes() {
        let request = SubmitTaskRequest {
            instruction: "在知乎自动发表一篇名称为人工智能技能大全".to_string(),
            ..SubmitTaskRequest::default()
        };

        assert_eq!(
            initial_request_url_for_submit_task(&request),
            "https://www.zhihu.com"
        );
    }

    #[test]
    fn initial_request_url_falls_back_to_lineloss_origin_for_lineloss_instructions() {
        let request = SubmitTaskRequest {
            instruction: "兰州公司 台区线损大数据 月累计线损率统计分析。。。".to_string(),
            ..SubmitTaskRequest::default()
        };

        assert_eq!(
            initial_request_url_for_submit_task(&request),
            "http://20.76.57.61:18080"
        );
    }

    #[test]
    fn bridge_base_url_defaults_local_browser_ws_endpoint_to_http_bridge() {
        assert_eq!(
            bridge_base_url_from_browser_ws_url("ws://127.0.0.1:12345"),
            "http://localhost:23323"
        );
        assert_eq!(
            bridge_base_url_from_browser_ws_url("ws://localhost:12345"),
            "http://localhost:23323"
        );
    }

    #[test]
    fn bridge_base_url_maps_non_default_ws_endpoint_to_http_origin() {
        assert_eq!(
            bridge_base_url_from_browser_ws_url("ws://127.0.0.1:40123"),
            "http://127.0.0.1:40123"
        );
    }

    #[test]
    fn service_bridge_transport_posts_semantic_request_and_maps_success_reply() {
        let listener = TcpListener::bind("127.0.0.1:0").unwrap();
        let address = listener.local_addr().unwrap();
        let base_url = format!("http://{address}");
        let (request_tx, request_rx) = mpsc::channel();

        let server = thread::spawn(move || {
            let (mut stream, _) = listener.accept().unwrap();
            let request = read_http_json_body(&mut stream);
            request_tx.send(request).unwrap();
            write_http_json_response(
                &mut stream,
                "200 OK",
                &json!({
                    "success": true,
                    "data": { "text": "天气" }
                }),
            );
        });

        let backend = BridgeBrowserBackend::new(
            Arc::new(ServiceBridgeTransport::with_timeout(
                base_url,
                Duration::from_secs(1),
            )),
            service_test_policy(),
        );

        let output = backend
            .invoke(Action::GetText, json!({ "selector": "body" }), "www.zhihu.com")
            .expect("bridge transport should normalize success reply");

        let request = request_rx.recv_timeout(Duration::from_secs(1)).unwrap();
        server.join().unwrap();

        assert_eq!(
            request,
            json!([
                "getText",
                { "selector": "body" },
                "www.zhihu.com"
            ])
        );
        assert!(output.success);
        assert_eq!(output.data, json!({ "text": "天气" }));
    }

    #[test]
    fn service_bridge_transport_maps_bridge_error_reply_to_pipe_error() {
        let listener = TcpListener::bind("127.0.0.1:0").unwrap();
        let address = listener.local_addr().unwrap();
        let base_url = format!("http://{address}");

        let server = thread::spawn(move || {
            let (mut stream, _) = listener.accept().unwrap();
            let _request = read_http_json_body(&mut stream);
            write_http_json_response(
                &mut stream,
                "200 OK",
                &json!({
                    "success": false,
                    "message": "selector not found",
                    "details": { "selector": "#missing" }
                }),
            );
        });

        let backend = BridgeBrowserBackend::new(
            Arc::new(ServiceBridgeTransport::with_timeout(
                base_url,
                Duration::from_secs(1),
            )),
            service_test_policy(),
        );

        let error = backend
            .invoke(Action::GetText, json!({ "selector": "#missing" }), "www.zhihu.com")
            .expect_err("bridge transport should surface semantic bridge failures");

        server.join().unwrap();
        assert!(matches!(
            error,
            PipeError::Protocol(message) if message == "bridge action failed: selector not found"
        ));
    }

    #[test]
    fn service_bridge_transport_maps_http_timeout_to_timeout() {
        let transport = ServiceBridgeTransport::with_timeout(
            "http://127.0.0.1:1".to_string(),
            Duration::from_millis(20),
        );

        let error = transport
            .execute(BridgeBrowserActionRequest::new(
                "navigate",
                json!({ "url": "https://www.zhihu.com/hot" }),
                "www.zhihu.com",
            ))
            .expect_err("unreachable bridge should surface a transport error");

        assert!(matches!(error, PipeError::Protocol(_) | PipeError::Timeout));
    }
}
