//! Namespaced RPC methods (§4.3).

use serde::{Deserialize, Serialize};

/// Observable browser-side effect class for a protocol method.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum MethodEffect {
    /// Reads browser/session state without dispatching page input.
    PassiveRead,
    /// Dispatches temporary input such as hover probes. It must not submit,
    /// navigate, or persist page state, but it can trigger page event handlers.
    TransientInput,
    /// Drives browser/page state such as clicking, filling, navigation, tabs,
    /// or arbitrary page script.
    BrowserMutation,
    /// Control-plane/session lifecycle operation. These are deliberately not
    /// gated by the pending browser-action interrupt path.
    ControlPlane,
}

/// Namespaced method string (`system.handshake`, `tool.tab_list`, …).
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub enum Method {
    #[serde(rename = "system.handshake")]
    SystemHandshake,
    #[serde(rename = "system.ping")]
    SystemPing,
    #[serde(rename = "system.status")]
    SystemStatus,

    #[serde(rename = "session.start")]
    SessionStart,
    #[serde(rename = "session.stop")]
    SessionStop,
    #[serde(rename = "session.stop_all")]
    SessionStopAll,
    #[serde(rename = "session.list")]
    SessionList,

    #[serde(rename = "browser.list")]
    BrowserList,

    #[serde(rename = "tool.session_start")]
    ToolSessionStart,
    #[serde(rename = "tool.session_stop")]
    ToolSessionStop,
    #[serde(rename = "tool.window_resize")]
    ToolWindowResize,
    #[serde(rename = "tool.emulate")]
    ToolEmulate,
    #[serde(rename = "tool.tab_list")]
    ToolTabList,
    #[serde(rename = "tool.tab_create")]
    ToolTabCreate,
    #[serde(rename = "tool.tab_close")]
    ToolTabClose,
    #[serde(rename = "tool.tab_borrow")]
    ToolTabBorrow,
    #[serde(rename = "tool.tab_return")]
    ToolTabReturn,
    #[serde(rename = "tool.tab_select")]
    ToolTabSelect,
    #[serde(rename = "tool.navigate")]
    ToolNavigate,
    #[serde(rename = "tool.navigate_back")]
    ToolNavigateBack,
    #[serde(rename = "tool.navigate_forward")]
    ToolNavigateForward,
    #[serde(rename = "tool.reload")]
    ToolReload,
    #[serde(rename = "tool.click")]
    ToolClick,
    #[serde(rename = "tool.fill")]
    ToolFill,
    #[serde(rename = "tool.press")]
    ToolPress,
    #[serde(rename = "tool.select")]
    ToolSelect,
    #[serde(rename = "tool.snapshot")]
    ToolSnapshot,
    #[serde(rename = "tool.observe")]
    ToolObserve,
    #[serde(rename = "tool.get_html")]
    ToolGetHtml,
    #[serde(rename = "tool.screenshot")]
    ToolScreenshot,
    #[serde(rename = "tool.console")]
    ToolConsole,
    #[serde(rename = "tool.network")]
    ToolNetwork,
    #[serde(rename = "tool.evaluate")]
    ToolEvaluate,
    #[serde(rename = "tool.wait_for_navigation")]
    ToolWaitForNavigation,
    #[serde(rename = "tool.wait_ms")]
    ToolWaitMs,
    #[serde(rename = "tool.request_help")]
    ToolRequestHelp,
    #[serde(rename = "tool.record_start")]
    ToolRecordStart,
    #[serde(rename = "tool.record_stop")]
    ToolRecordStop,
    #[serde(rename = "tool.record_await")]
    ToolRecordAwait,

    #[serde(rename = "cancel")]
    Cancel,
}

impl Method {
    /// Browser-side effect classification for this method.
    ///
    /// Used by the daemon's pending-interrupt machinery: when the
    /// user has clicked the agent-window mask's stop button, the
    /// next tool call that dispatches browser/page input for that
    /// session is rejected with `ErrorCode::UserAborted`. Passive reads
    /// and control-plane RPCs pass through transparently.
    ///
    /// **Compile-time enforcement.** The match below is exhaustive
    /// (no `_ =>` fallthrough). Adding a new `Method` variant is a
    /// compile error here, so classification cannot silently be
    /// skipped — the author has to make a deliberate choice.
    ///
    /// **Judgment calls** (read these before adding new variants):
    ///
    /// * `tool.evaluate` is classified as browser-mutating because the
    ///   daemon cannot statically distinguish a `document.title`
    ///   read from a `form.submit()` write.
    /// * `tool.observe` is classified as transient input: its bounded
    ///   hover probes do not commit browser state, but they dispatch real
    ///   page input events and therefore must be gated like automation.
    /// * `tool.wait_*` are classified as read-only: they do not
    ///   initiate any browser action; they observe state only.
    /// * `session.*` and `tool.session_*` are NOT gated. Blocking
    ///   `session.stop` would prevent the agent from gracefully
    ///   tearing down after observing the user's interrupt.
    /// * `cancel` is NOT gated. It's a control-plane operation
    ///   (stops another in-flight RPC), not a browser action.
    pub fn effect(&self) -> MethodEffect {
        match self {
            // Browser/page mutations — gated by pending-interrupt.
            Method::ToolTabCreate
            | Method::ToolTabClose
            | Method::ToolTabBorrow
            | Method::ToolTabReturn
            | Method::ToolTabSelect
            | Method::ToolWindowResize
            | Method::ToolEmulate
            | Method::ToolNavigate
            | Method::ToolNavigateBack
            | Method::ToolNavigateForward
            | Method::ToolReload
            | Method::ToolClick
            | Method::ToolFill
            | Method::ToolPress
            | Method::ToolSelect
            | Method::ToolEvaluate
            // May navigate via optional `url` and changes Agent Window
            // chrome; gate behind pending-interrupt like other writes.
            | Method::ToolRecordStart => MethodEffect::BrowserMutation,

            // Transient input — no committed browser action, but still page
            // input. It must be stopped by pending user interrupts.
            Method::ToolObserve => MethodEffect::TransientInput,

            // Passive reads — transparent.
            // `record_stop` / `record_await` observe / finish a recording
            // without driving new automation gestures, so they stay
            // ungated (teardown after interrupt must still work).
            Method::ToolTabList
            | Method::ToolSnapshot
            | Method::ToolGetHtml
            | Method::ToolScreenshot
            | Method::ToolConsole
            | Method::ToolNetwork
            | Method::ToolWaitForNavigation
            | Method::ToolWaitMs
            | Method::ToolRequestHelp
            | Method::ToolRecordStop
            | Method::ToolRecordAwait => MethodEffect::PassiveRead,

            // Session lifecycle — not gated.
            Method::SessionStart
            | Method::SessionStop
            | Method::SessionStopAll
            | Method::SessionList
            | Method::ToolSessionStart
            | Method::ToolSessionStop => MethodEffect::ControlPlane,

            // System / control — not gated.
            Method::SystemHandshake
            | Method::SystemPing
            | Method::SystemStatus
            | Method::BrowserList
            | Method::Cancel => MethodEffect::ControlPlane,
        }
    }

    /// Whether this RPC drives browser/page state in the traditional sense.
    pub fn is_mutating(&self) -> bool {
        self.effect() == MethodEffect::BrowserMutation
    }

    /// Whether a pending user interrupt should reject this method.
    pub fn requires_interrupt_gate(&self) -> bool {
        matches!(
            self.effect(),
            MethodEffect::TransientInput | MethodEffect::BrowserMutation
        )
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::{CancelParams, CancelResult};
    use serde_json::json;

    #[test]
    fn cancel_method_round_trips() {
        let method: Method = serde_json::from_value(json!("cancel")).unwrap();
        assert_eq!(method, Method::Cancel);
        assert_eq!(serde_json::to_value(method).unwrap(), json!("cancel"));
    }

    #[test]
    fn console_method_round_trips() {
        let method: Method = serde_json::from_value(json!("tool.console")).unwrap();
        assert_eq!(method, Method::ToolConsole);
        assert_eq!(serde_json::to_value(method).unwrap(), json!("tool.console"));
    }

    #[test]
    fn network_method_round_trips() {
        let method: Method = serde_json::from_value(json!("tool.network")).unwrap();
        assert_eq!(method, Method::ToolNetwork);
        assert_eq!(serde_json::to_value(method).unwrap(), json!("tool.network"));
    }

    #[test]
    fn emulate_method_round_trips() {
        let method: Method = serde_json::from_value(json!("tool.emulate")).unwrap();
        assert_eq!(method, Method::ToolEmulate);
        assert_eq!(serde_json::to_value(method).unwrap(), json!("tool.emulate"));
    }

    #[test]
    fn cancel_params_and_result_round_trip() {
        let params: CancelParams = serde_json::from_value(json!({ "rpc_id": "wait-1" })).unwrap();
        assert_eq!(params.rpc_id, "wait-1");
        let result = CancelResult { cancelled: true };
        assert_eq!(
            serde_json::to_value(result).unwrap(),
            json!({ "cancelled": true })
        );
    }

    #[test]
    fn is_mutating_classifies_read_only_tools_as_non_mutating() {
        assert!(!Method::ToolTabList.is_mutating());
        assert!(!Method::ToolSnapshot.is_mutating());
        assert!(!Method::ToolObserve.is_mutating());
        assert!(!Method::ToolGetHtml.is_mutating());
        assert!(!Method::ToolScreenshot.is_mutating());
        assert!(!Method::ToolConsole.is_mutating());
        assert!(!Method::ToolNetwork.is_mutating());
        assert!(!Method::ToolWaitForNavigation.is_mutating());
        assert!(!Method::ToolWaitMs.is_mutating());
    }

    #[test]
    fn is_mutating_classifies_mutating_tools_as_mutating() {
        assert!(Method::ToolTabCreate.is_mutating());
        assert!(Method::ToolTabClose.is_mutating());
        assert!(Method::ToolTabBorrow.is_mutating());
        assert!(Method::ToolTabReturn.is_mutating());
        assert!(Method::ToolTabSelect.is_mutating());
        assert!(Method::ToolNavigate.is_mutating());
        assert!(Method::ToolNavigateBack.is_mutating());
        assert!(Method::ToolNavigateForward.is_mutating());
        assert!(Method::ToolReload.is_mutating());
        assert!(Method::ToolClick.is_mutating());
        assert!(Method::ToolFill.is_mutating());
        assert!(Method::ToolPress.is_mutating());
        assert!(Method::ToolSelect.is_mutating());
        assert!(Method::ToolEvaluate.is_mutating());
        assert!(Method::ToolRecordStart.is_mutating());
        assert!(Method::ToolWindowResize.is_mutating());
        assert!(Method::ToolEmulate.is_mutating());
    }

    #[test]
    fn is_mutating_classifies_record_stop_await_as_non_mutating() {
        assert!(!Method::ToolRecordStop.is_mutating());
        assert!(!Method::ToolRecordAwait.is_mutating());
    }

    #[test]
    fn is_mutating_classifies_session_lifecycle_as_non_mutating() {
        // Session lifecycle RPCs are not "mutating" for the purposes
        // of pending-interrupt gating — gating them would prevent the
        // agent from gracefully tearing down after observing the
        // user's interrupt.
        assert!(!Method::SessionStart.is_mutating());
        assert!(!Method::SessionStop.is_mutating());
        assert!(!Method::SessionStopAll.is_mutating());
        assert!(!Method::SessionList.is_mutating());
        assert!(!Method::ToolSessionStart.is_mutating());
        assert!(!Method::ToolSessionStop.is_mutating());
    }

    #[test]
    fn is_mutating_classifies_system_methods_as_non_mutating() {
        assert!(!Method::SystemHandshake.is_mutating());
        assert!(!Method::SystemPing.is_mutating());
        assert!(!Method::SystemStatus.is_mutating());
        assert!(!Method::BrowserList.is_mutating());
        assert!(!Method::Cancel.is_mutating());
    }

    #[test]
    fn effect_classifies_observe_as_transient_input() {
        assert_eq!(Method::ToolSnapshot.effect(), MethodEffect::PassiveRead);
        assert_eq!(Method::ToolObserve.effect(), MethodEffect::TransientInput);
        assert_eq!(Method::ToolClick.effect(), MethodEffect::BrowserMutation);
        assert_eq!(Method::Cancel.effect(), MethodEffect::ControlPlane);
    }

    #[test]
    fn interrupt_gate_includes_transient_input() {
        assert!(!Method::ToolSnapshot.requires_interrupt_gate());
        assert!(Method::ToolObserve.requires_interrupt_gate());
        assert!(Method::ToolClick.requires_interrupt_gate());
        assert!(!Method::Cancel.requires_interrupt_gate());
    }
}
