//! Sessions API for `OpenCode`.
//!
//! This module provides methods for session endpoints (18 total).

use crate::error::Result;
use crate::http::HttpClient;
use crate::http::encode_path_segment;
use crate::types::session::CreateSessionRequest;
use crate::types::session::RevertRequest;
use crate::types::session::Session;
use crate::types::session::SessionDiff;
use crate::types::session::SessionInitRequest;
use crate::types::session::SessionListParams;
use crate::types::session::SessionStatus;
use crate::types::session::SessionStatusInfo;
use crate::types::session::ShareInfo;
use crate::types::session::SummarizeRequest;
use crate::types::session::TodoItem;
use crate::types::session::UpdateSessionRequest;
use reqwest::Method;
use std::collections::HashMap;

/// Sessions API client.
#[derive(Clone)]
pub struct SessionsApi {
    http: HttpClient,
}

impl SessionsApi {
    /// Create a new Sessions API client.
    pub fn new(http: HttpClient) -> Self {
        Self { http }
    }

    /// Create a new session.
    ///
    /// # Errors
    ///
    /// Returns an error if the request fails.
    pub async fn create(&self, req: &CreateSessionRequest) -> Result<Session> {
        let body = serde_json::to_value(req)?;
        self.http
            .request_json(Method::POST, "/session", Some(body))
            .await
    }

    /// Get session by ID.
    ///
    /// # Errors
    ///
    /// Returns an error if the request fails or session not found.
    pub async fn get(&self, id: &str) -> Result<Session> {
        let sid = encode_path_segment(id);
        self.http
            .request_json(Method::GET, &format!("/session/{sid}"), None)
            .await
    }

    /// List all sessions.
    ///
    /// # Errors
    ///
    /// Returns an error if the request fails.
    pub async fn list(&self) -> Result<Vec<Session>> {
        self.list_filtered(&SessionListParams::default()).await
    }

    /// List sessions with typed query parameters.
    ///
    /// # Errors
    ///
    /// Returns an error if the request fails.
    pub async fn list_filtered(&self, params: &SessionListParams) -> Result<Vec<Session>> {
        let query = params.to_query_pairs();
        self.http
            .request_json_with_query(Method::GET, "/session", &query, None)
            .await
    }

    /// Delete session by ID.
    ///
    /// # Errors
    ///
    /// Returns an error if the request fails.
    pub async fn delete(&self, id: &str) -> Result<bool> {
        let sid = encode_path_segment(id);
        self.http
            .request_json::<bool>(Method::DELETE, &format!("/session/{sid}"), None)
            .await
    }

    /// Fork a session from a specific point.
    ///
    /// # Errors
    ///
    /// Returns an error if the request fails.
    pub async fn fork(&self, id: &str) -> Result<Session> {
        let sid = encode_path_segment(id);
        self.http
            .request_json(
                Method::POST,
                &format!("/session/{sid}/fork"),
                Some(serde_json::json!({})),
            )
            .await
    }

    /// Abort an active session.
    ///
    /// # Errors
    ///
    /// Returns an error if the request fails.
    pub async fn abort(&self, id: &str) -> Result<bool> {
        let sid = encode_path_segment(id);
        self.http
            .request_json::<bool>(
                Method::POST,
                &format!("/session/{sid}/abort"),
                Some(serde_json::json!({})),
            )
            .await
    }

    /// Get session status.
    ///
    /// # Errors
    ///
    /// Returns an error if the request fails.
    pub async fn status(&self) -> Result<SessionStatus> {
        let map: HashMap<String, SessionStatusInfo> = self
            .http
            .request_json(Method::GET, "/session/status", None)
            .await?;

        let active: Vec<String> = map
            .into_iter()
            .filter_map(|(sid, status)| status.is_busy_like().then_some(sid))
            .collect();

        let busy = !active.is_empty();
        let active_session_id = if active.len() == 1 {
            active.into_iter().next()
        } else {
            None
        };

        Ok(SessionStatus {
            active_session_id,
            busy,
        })
    }

    /// Get status for a specific session.
    ///
    /// # Errors
    ///
    /// Returns an error if the request fails.
    pub async fn status_for(&self, session_id: &str) -> Result<SessionStatusInfo> {
        let map: HashMap<String, SessionStatusInfo> = self
            .http
            .request_json(Method::GET, "/session/status", None)
            .await?;
        Ok(map
            .get(session_id)
            .cloned()
            .unwrap_or(SessionStatusInfo::Idle))
    }

    /// Get the full per-session status map.
    ///
    /// # Errors
    ///
    /// Returns an error if the request fails.
    pub async fn status_map(&self) -> Result<HashMap<String, SessionStatusInfo>> {
        self.http
            .request_json(Method::GET, "/session/status", None)
            .await
    }

    /// Get children of a session (forked sessions).
    ///
    /// # Errors
    ///
    /// Returns an error if the request fails.
    pub async fn children(&self, id: &str) -> Result<Vec<Session>> {
        let sid = encode_path_segment(id);
        self.http
            .request_json(Method::GET, &format!("/session/{sid}/children"), None)
            .await
    }

    /// Get todos for a session.
    ///
    /// # Errors
    ///
    /// Returns an error if the request fails.
    pub async fn todo(&self, id: &str) -> Result<Vec<TodoItem>> {
        let sid = encode_path_segment(id);
        self.http
            .request_json(Method::GET, &format!("/session/{sid}/todo"), None)
            .await
    }

    /// Update a session.
    ///
    /// # Errors
    ///
    /// Returns an error if the request fails.
    pub async fn update(&self, id: &str, req: &UpdateSessionRequest) -> Result<Session> {
        let sid = encode_path_segment(id);
        let body = serde_json::to_value(req)?;
        self.http
            .request_json(Method::PATCH, &format!("/session/{sid}"), Some(body))
            .await
    }

    /// Initialize a session.
    ///
    /// # Errors
    ///
    /// Returns an error if the request fails.
    pub async fn init(&self, id: &str, req: &SessionInitRequest) -> Result<bool> {
        let sid = encode_path_segment(id);
        let body = serde_json::to_value(req)?;
        self.http
            .request_json(Method::POST, &format!("/session/{sid}/init"), Some(body))
            .await
    }

    /// Share a session.
    ///
    /// # Errors
    ///
    /// Returns an error if the request fails.
    pub async fn share(&self, id: &str) -> Result<ShareInfo> {
        let sid = encode_path_segment(id);
        self.http
            .request_json(
                Method::POST,
                &format!("/session/{sid}/share"),
                Some(serde_json::json!({})),
            )
            .await
    }

    /// Unshare a session.
    ///
    /// # Errors
    ///
    /// Returns an error if the request fails.
    pub async fn unshare(&self, id: &str) -> Result<()> {
        let sid = encode_path_segment(id);
        self.http
            .request_empty(Method::DELETE, &format!("/session/{sid}/share"), None)
            .await
    }

    /// Get session diff.
    ///
    /// # Errors
    ///
    /// Returns an error if the request fails.
    pub async fn diff(&self, id: &str) -> Result<SessionDiff> {
        let sid = encode_path_segment(id);
        self.http
            .request_json(Method::GET, &format!("/session/{sid}/diff"), None)
            .await
    }

    /// Get session diff since a specific message.
    ///
    /// # Errors
    ///
    /// Returns an error if the request fails.
    pub async fn diff_since_message(&self, id: &str, message_id: &str) -> Result<SessionDiff> {
        let sid = encode_path_segment(id);
        self.http
            .request_json_with_query(
                Method::GET,
                &format!("/session/{sid}/diff"),
                &[("messageID", message_id.to_string())],
                None,
            )
            .await
    }

    /// Summarize a session.
    ///
    /// # Errors
    ///
    /// Returns an error if the request fails.
    pub async fn summarize(&self, id: &str, req: &SummarizeRequest) -> Result<bool> {
        let sid = encode_path_segment(id);
        let body = serde_json::to_value(req)?;
        self.http
            .request_json::<bool>(
                Method::POST,
                &format!("/session/{sid}/summarize"),
                Some(body),
            )
            .await
    }

    /// Revert a session to a previous state.
    ///
    /// # Errors
    ///
    /// Returns an error if the request fails.
    pub async fn revert(&self, id: &str, req: &RevertRequest) -> Result<Session> {
        let sid = encode_path_segment(id);
        let body = serde_json::to_value(req)?;
        self.http
            .request_json(Method::POST, &format!("/session/{sid}/revert"), Some(body))
            .await
    }

    /// Unrevert a session (undo a revert).
    ///
    /// # Errors
    ///
    /// Returns an error if the request fails.
    pub async fn unrevert(&self, id: &str) -> Result<Session> {
        let sid = encode_path_segment(id);
        self.http
            .request_json(
                Method::POST,
                &format!("/session/{sid}/unrevert"),
                Some(serde_json::json!({})),
            )
            .await
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::http::HttpConfig;
    use std::time::Duration;
    use wiremock::Mock;
    use wiremock::MockServer;
    use wiremock::ResponseTemplate;
    use wiremock::matchers::body_json;
    use wiremock::matchers::method;
    use wiremock::matchers::path;
    use wiremock::matchers::query_param;

    #[tokio::test]
    async fn test_create_session() {
        let mock_server = MockServer::start().await;

        Mock::given(method("POST"))
            .and(path("/session"))
            .and(body_json(serde_json::json!({})))
            .respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!({
                "id": "session123",
                "slug": "session123",
                "projectId": "proj1",
                "directory": "/path",
                "title": "New Session",
                "version": "1.0",
                "time": {"created": 1_234_567_890, "updated": 1_234_567_890}
            })))
            .mount(&mock_server)
            .await;

        let http = HttpClient::new(HttpConfig {
            base_url: mock_server.uri(),
            directory: None,
            workspace: None,
            timeout: Duration::from_secs(30),
        })
        .unwrap();

        let sessions = SessionsApi::new(http);
        let session = sessions
            .create(&CreateSessionRequest::default())
            .await
            .unwrap();
        assert_eq!(session.id, "session123");
    }

    #[tokio::test]
    async fn test_get_session() {
        let mock_server = MockServer::start().await;

        Mock::given(method("GET"))
            .and(path("/session/abc123"))
            .respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!({
                "id": "abc123",
                "slug": "abc123",
                "projectId": "p1",
                "directory": "/path",
                "title": "Test Session",
                "version": "1.0",
                "time": {"created": 1_234_567_890, "updated": 1_234_567_890}
            })))
            .mount(&mock_server)
            .await;

        let http = HttpClient::new(HttpConfig {
            base_url: mock_server.uri(),
            directory: None,
            workspace: None,
            timeout: Duration::from_secs(30),
        })
        .unwrap();

        let sessions = SessionsApi::new(http);
        let session = sessions.get("abc123").await.unwrap();
        assert_eq!(session.id, "abc123");
        assert_eq!(session.title, "Test Session");
    }

    #[tokio::test]
    async fn test_list_sessions() {
        let mock_server = MockServer::start().await;

        Mock::given(method("GET"))
            .and(path("/session"))
            .respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!([
                {"id": "s1", "slug": "s1", "projectId": "p1", "directory": "/path", "title": "S1", "version": "1.0", "time": {"created": 1_234_567_890, "updated": 1_234_567_890}},
                {"id": "s2", "slug": "s2", "projectId": "p1", "directory": "/path", "title": "S2", "version": "1.0", "time": {"created": 1_234_567_890, "updated": 1_234_567_890}}
            ])))
            .mount(&mock_server)
            .await;

        let http = HttpClient::new(HttpConfig {
            base_url: mock_server.uri(),
            directory: None,
            workspace: None,
            timeout: Duration::from_secs(30),
        })
        .unwrap();

        let sessions = SessionsApi::new(http);
        let list = sessions.list().await.unwrap();
        assert_eq!(list.len(), 2);
    }

    #[tokio::test]
    async fn test_list_filtered_sessions_encodes_query() {
        let mock_server = MockServer::start().await;

        Mock::given(method("GET"))
            .and(path("/session"))
            .and(query_param("scope", "project"))
            .and(query_param("path", "src/lib.rs"))
            .and(query_param("roots", "true"))
            .and(query_param("start", "1234567890"))
            .and(query_param("search", "hello"))
            .and(query_param("limit", "25"))
            .respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!([])))
            .mount(&mock_server)
            .await;

        let http = HttpClient::new(HttpConfig {
            base_url: mock_server.uri(),
            directory: None,
            workspace: None,
            timeout: Duration::from_secs(30),
        })
        .unwrap();

        let sessions = SessionsApi::new(http);
        let list = sessions
            .list_filtered(&SessionListParams {
                scope: Some(crate::types::session::SessionListScope::Project),
                path: Some("src/lib.rs".to_string()),
                roots: Some(true),
                start: Some(1_234_567_890),
                search: Some("hello".to_string()),
                limit: Some(25),
            })
            .await
            .unwrap();

        assert!(list.is_empty());
    }

    #[tokio::test]
    async fn test_status_map_from_modern_response() {
        let mock_server = MockServer::start().await;

        Mock::given(method("GET"))
            .and(path("/session/status"))
            .respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!({
                "s1": {"type": "busy"},
                "s2": {"type": "retry", "attempt": 2, "message": "rate limited", "next": 12345}
            })))
            .mount(&mock_server)
            .await;

        let http = HttpClient::new(HttpConfig {
            base_url: mock_server.uri(),
            directory: None,
            workspace: None,
            timeout: Duration::from_secs(30),
        })
        .unwrap();

        let sessions = SessionsApi::new(http);
        let map = sessions.status_map().await.unwrap();

        assert!(matches!(map.get("s1"), Some(SessionStatusInfo::Busy)));
        assert!(matches!(
            map.get("s2"),
            Some(SessionStatusInfo::Retry { attempt: 2, .. })
        ));
    }

    #[tokio::test]
    async fn test_status_from_modern_response() {
        let mock_server = MockServer::start().await;

        Mock::given(method("GET"))
            .and(path("/session/status"))
            .respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!({
                "s1": {"type": "busy"},
                "s2": {"type": "retry", "attempt": 2, "message": "rate limited", "next": 12345}
            })))
            .mount(&mock_server)
            .await;

        let http = HttpClient::new(HttpConfig {
            base_url: mock_server.uri(),
            directory: None,
            workspace: None,
            timeout: Duration::from_secs(30),
        })
        .unwrap();

        let sessions = SessionsApi::new(http);
        let status = sessions.status().await.unwrap();

        assert!(status.busy);
        assert!(status.active_session_id.is_none());
    }

    #[tokio::test]
    async fn test_status_treats_unknown_as_busy() {
        let mock_server = MockServer::start().await;

        Mock::given(method("GET"))
            .and(path("/session/status"))
            .respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!({
                "s1": {"type": "paused"}
            })))
            .mount(&mock_server)
            .await;

        let http = HttpClient::new(HttpConfig {
            base_url: mock_server.uri(),
            directory: None,
            workspace: None,
            timeout: Duration::from_secs(30),
        })
        .unwrap();

        let sessions = SessionsApi::new(http);
        let status = sessions.status().await.unwrap();

        assert!(status.busy);
        assert_eq!(status.active_session_id.as_deref(), Some("s1"));
    }

    #[tokio::test]
    async fn test_delete_session() {
        let mock_server = MockServer::start().await;

        Mock::given(method("DELETE"))
            .and(path("/session/del123"))
            .respond_with(ResponseTemplate::new(200).set_body_json(true))
            .mount(&mock_server)
            .await;

        let http = HttpClient::new(HttpConfig {
            base_url: mock_server.uri(),
            directory: None,
            workspace: None,
            timeout: Duration::from_secs(30),
        })
        .unwrap();

        let sessions = SessionsApi::new(http);
        let ok = sessions.delete("del123").await.unwrap();
        assert!(ok);
    }

    #[tokio::test]
    async fn test_children() {
        let mock_server = MockServer::start().await;

        Mock::given(method("GET"))
            .and(path("/session/parent123/children"))
            .respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!([
                {"id": "child1", "slug": "child1", "projectId": "p1", "directory": "/path", "title": "Child 1", "version": "1.0", "time": {"created": 1_234_567_890, "updated": 1_234_567_890}}
            ])))
            .mount(&mock_server)
            .await;

        let http = HttpClient::new(HttpConfig {
            base_url: mock_server.uri(),
            directory: None,
            workspace: None,
            timeout: Duration::from_secs(30),
        })
        .unwrap();

        let sessions = SessionsApi::new(http);
        let children = sessions.children("parent123").await.unwrap();
        assert_eq!(children.len(), 1);
        assert_eq!(children[0].id, "child1");
    }

    #[tokio::test]
    async fn test_todo() {
        let mock_server = MockServer::start().await;

        Mock::given(method("GET"))
            .and(path("/session/s1/todo"))
            .respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!([
                {"id": "t1", "content": "Task 1", "completed": false},
                {"id": "t2", "content": "Task 2", "completed": true}
            ])))
            .mount(&mock_server)
            .await;

        let http = HttpClient::new(HttpConfig {
            base_url: mock_server.uri(),
            directory: None,
            workspace: None,
            timeout: Duration::from_secs(30),
        })
        .unwrap();

        let sessions = SessionsApi::new(http);
        let todos = sessions.todo("s1").await.unwrap();
        assert_eq!(todos.len(), 2);
        assert!(!todos[0].completed);
        assert!(todos[1].completed);
    }

    #[tokio::test]
    async fn test_update_session() {
        let mock_server = MockServer::start().await;

        Mock::given(method("PATCH"))
            .and(path("/session/s1"))
            .respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!({
                "id": "s1",
                "slug": "s1",
                "projectId": "p1",
                "directory": "/path",
                "title": "Updated Title",
                "version": "1.0",
                "time": {"created": 1_234_567_890, "updated": 1_234_567_891}
            })))
            .mount(&mock_server)
            .await;

        let http = HttpClient::new(HttpConfig {
            base_url: mock_server.uri(),
            directory: None,
            workspace: None,
            timeout: Duration::from_secs(30),
        })
        .unwrap();

        let sessions = SessionsApi::new(http);
        let session = sessions
            .update(
                "s1",
                &UpdateSessionRequest {
                    title: Some("Updated Title".into()),
                },
            )
            .await
            .unwrap();
        assert_eq!(session.title, "Updated Title");
    }

    #[tokio::test]
    async fn test_share() {
        let mock_server = MockServer::start().await;

        Mock::given(method("POST"))
            .and(path("/session/s1/share"))
            .respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!({
                "url": "https://share.example.com/s1"
            })))
            .mount(&mock_server)
            .await;

        let http = HttpClient::new(HttpConfig {
            base_url: mock_server.uri(),
            directory: None,
            workspace: None,
            timeout: Duration::from_secs(30),
        })
        .unwrap();

        let sessions = SessionsApi::new(http);
        let share = sessions.share("s1").await.unwrap();
        assert_eq!(share.url, "https://share.example.com/s1");
    }

    #[tokio::test]
    async fn test_unshare() {
        let mock_server = MockServer::start().await;

        Mock::given(method("DELETE"))
            .and(path("/session/s1/share"))
            .respond_with(ResponseTemplate::new(204))
            .mount(&mock_server)
            .await;

        let http = HttpClient::new(HttpConfig {
            base_url: mock_server.uri(),
            directory: None,
            workspace: None,
            timeout: Duration::from_secs(30),
        })
        .unwrap();

        let sessions = SessionsApi::new(http);
        sessions.unshare("s1").await.unwrap();
    }

    #[tokio::test]
    async fn test_diff() {
        let mock_server = MockServer::start().await;

        // Server returns an array of file diff objects
        Mock::given(method("GET"))
            .and(path("/session/s1/diff"))
            .respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!([
                {
                    "file": "file.rs",
                    "patch": "@@ -1 +1 @@\n-old content\n+new content",
                    "additions": 1,
                    "deletions": 1,
                    "status": "modified"
                }
            ])))
            .mount(&mock_server)
            .await;

        let http = HttpClient::new(HttpConfig {
            base_url: mock_server.uri(),
            directory: None,
            workspace: None,
            timeout: Duration::from_secs(30),
        })
        .unwrap();

        let sessions = SessionsApi::new(http);
        let diff = sessions.diff("s1").await.unwrap();
        assert_eq!(diff.len(), 1);
        assert_eq!(diff[0].file, "file.rs");
        assert!(diff[0].patch.contains("new content"));
        assert_eq!(diff[0].additions, 1);
        assert_eq!(diff[0].deletions, 1);
    }

    #[tokio::test]
    async fn test_diff_since_message_uses_message_id_query() {
        let mock_server = MockServer::start().await;

        Mock::given(method("GET"))
            .and(path("/session/s1/diff"))
            .and(query_param("messageID", "m1"))
            .respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!([])))
            .mount(&mock_server)
            .await;

        let http = HttpClient::new(HttpConfig {
            base_url: mock_server.uri(),
            directory: None,
            workspace: None,
            timeout: Duration::from_secs(30),
        })
        .unwrap();

        let sessions = SessionsApi::new(http);
        let diff = sessions.diff_since_message("s1", "m1").await.unwrap();
        assert!(diff.is_empty());
    }

    #[tokio::test]
    async fn test_init_sends_required_body() {
        let mock_server = MockServer::start().await;

        Mock::given(method("POST"))
            .and(path("/session/s1/init"))
            .and(body_json(serde_json::json!({
                "modelID": "claude-sonnet-4",
                "providerID": "anthropic",
                "messageID": "msg-123"
            })))
            .respond_with(ResponseTemplate::new(200).set_body_json(true))
            .mount(&mock_server)
            .await;

        let http = HttpClient::new(HttpConfig {
            base_url: mock_server.uri(),
            directory: None,
            workspace: None,
            timeout: Duration::from_secs(30),
        })
        .unwrap();

        let sessions = SessionsApi::new(http);
        let ok = sessions
            .init(
                "s1",
                &SessionInitRequest {
                    model_id: "claude-sonnet-4".to_string(),
                    provider_id: "anthropic".to_string(),
                    message_id: "msg-123".to_string(),
                },
            )
            .await
            .unwrap();
        assert!(ok);
    }

    #[tokio::test]
    async fn test_summarize() {
        let mock_server = MockServer::start().await;

        Mock::given(method("POST"))
            .and(path("/session/s1/summarize"))
            .respond_with(ResponseTemplate::new(200).set_body_json(true))
            .mount(&mock_server)
            .await;

        let http = HttpClient::new(HttpConfig {
            base_url: mock_server.uri(),
            directory: None,
            workspace: None,
            timeout: Duration::from_secs(30),
        })
        .unwrap();

        let sessions = SessionsApi::new(http);
        let ok = sessions
            .summarize(
                "s1",
                &SummarizeRequest {
                    provider_id: "anthropic".into(),
                    model_id: "claude-3-5-sonnet".into(),
                    auto: None,
                },
            )
            .await
            .unwrap();
        assert!(ok);
    }

    #[tokio::test]
    async fn test_revert() {
        let mock_server = MockServer::start().await;

        Mock::given(method("POST"))
            .and(path("/session/s1/revert"))
            .respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!({
                "id": "s1",
                "slug": "s1",
                "projectId": "p1",
                "directory": "/path",
                "title": "Reverted Session",
                "version": "1.0",
                "time": {"created": 1_234_567_890, "updated": 1_234_567_891}
            })))
            .mount(&mock_server)
            .await;

        let http = HttpClient::new(HttpConfig {
            base_url: mock_server.uri(),
            directory: None,
            workspace: None,
            timeout: Duration::from_secs(30),
        })
        .unwrap();

        let sessions = SessionsApi::new(http);
        let session = sessions
            .revert(
                "s1",
                &crate::types::session::RevertRequest {
                    message_id: "m5".into(),
                    part_id: None,
                },
            )
            .await
            .unwrap();
        assert_eq!(session.id, "s1");
    }

    #[tokio::test]
    async fn test_unrevert() {
        let mock_server = MockServer::start().await;

        Mock::given(method("POST"))
            .and(path("/session/s1/unrevert"))
            .respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!({
                "id": "s1",
                "slug": "s1",
                "projectId": "p1",
                "directory": "/path",
                "title": "Unreverted Session",
                "version": "1.0",
                "time": {"created": 1_234_567_890, "updated": 1_234_567_891}
            })))
            .mount(&mock_server)
            .await;

        let http = HttpClient::new(HttpConfig {
            base_url: mock_server.uri(),
            directory: None,
            workspace: None,
            timeout: Duration::from_secs(30),
        })
        .unwrap();

        let sessions = SessionsApi::new(http);
        let session = sessions.unrevert("s1").await.unwrap();
        assert_eq!(session.id, "s1");
    }

    // ==================== Error Case Tests ====================

    #[tokio::test]
    async fn test_get_session_not_found() {
        let mock_server = MockServer::start().await;

        Mock::given(method("GET"))
            .and(path("/session/nonexistent"))
            .respond_with(ResponseTemplate::new(404).set_body_json(serde_json::json!({
                "name": "NotFound",
                "message": "Session not found",
                "data": {"id": "nonexistent"}
            })))
            .mount(&mock_server)
            .await;

        let http = HttpClient::new(HttpConfig {
            base_url: mock_server.uri(),
            directory: None,
            workspace: None,
            timeout: Duration::from_secs(30),
        })
        .unwrap();

        let sessions = SessionsApi::new(http);
        let result = sessions.get("nonexistent").await;
        assert!(result.is_err());
        let err = result.unwrap_err();
        assert!(err.is_not_found());
        assert_eq!(err.error_name(), Some("NotFound"));
    }

    #[tokio::test]
    async fn test_create_session_validation_error() {
        let mock_server = MockServer::start().await;

        Mock::given(method("POST"))
            .and(path("/session"))
            .respond_with(ResponseTemplate::new(400).set_body_json(serde_json::json!({
                "name": "ValidationError",
                "message": "Invalid session configuration"
            })))
            .mount(&mock_server)
            .await;

        let http = HttpClient::new(HttpConfig {
            base_url: mock_server.uri(),
            directory: None,
            workspace: None,
            timeout: Duration::from_secs(30),
        })
        .unwrap();

        let sessions = SessionsApi::new(http);
        let result = sessions.create(&CreateSessionRequest::default()).await;
        assert!(result.is_err());
        let err = result.unwrap_err();
        assert!(err.is_validation_error());
    }

    #[tokio::test]
    async fn test_children_not_found() {
        let mock_server = MockServer::start().await;

        Mock::given(method("GET"))
            .and(path("/session/missing/children"))
            .respond_with(ResponseTemplate::new(404).set_body_json(serde_json::json!({
                "name": "NotFound",
                "message": "Session not found"
            })))
            .mount(&mock_server)
            .await;

        let http = HttpClient::new(HttpConfig {
            base_url: mock_server.uri(),
            directory: None,
            workspace: None,
            timeout: Duration::from_secs(30),
        })
        .unwrap();

        let sessions = SessionsApi::new(http);
        let result = sessions.children("missing").await;
        assert!(result.is_err());
        assert!(result.unwrap_err().is_not_found());
    }

    #[tokio::test]
    async fn test_update_session_not_found() {
        let mock_server = MockServer::start().await;

        Mock::given(method("PATCH"))
            .and(path("/session/missing"))
            .respond_with(ResponseTemplate::new(404).set_body_json(serde_json::json!({
                "name": "NotFound",
                "message": "Session not found"
            })))
            .mount(&mock_server)
            .await;

        let http = HttpClient::new(HttpConfig {
            base_url: mock_server.uri(),
            directory: None,
            workspace: None,
            timeout: Duration::from_secs(30),
        })
        .unwrap();

        let sessions = SessionsApi::new(http);
        let result = sessions
            .update("missing", &UpdateSessionRequest { title: None })
            .await;
        assert!(result.is_err());
        assert!(result.unwrap_err().is_not_found());
    }

    #[tokio::test]
    async fn test_share_server_error() {
        let mock_server = MockServer::start().await;

        Mock::given(method("POST"))
            .and(path("/session/s1/share"))
            .respond_with(ResponseTemplate::new(500).set_body_json(serde_json::json!({
                "name": "InternalError",
                "message": "Failed to generate share link"
            })))
            .mount(&mock_server)
            .await;

        let http = HttpClient::new(HttpConfig {
            base_url: mock_server.uri(),
            directory: None,
            workspace: None,
            timeout: Duration::from_secs(30),
        })
        .unwrap();

        let sessions = SessionsApi::new(http);
        let result = sessions.share("s1").await;
        assert!(result.is_err());
        assert!(result.unwrap_err().is_server_error());
    }

    #[tokio::test]
    async fn test_diff_not_found() {
        let mock_server = MockServer::start().await;

        Mock::given(method("GET"))
            .and(path("/session/missing/diff"))
            .respond_with(ResponseTemplate::new(404).set_body_json(serde_json::json!({
                "name": "NotFound",
                "message": "Session not found"
            })))
            .mount(&mock_server)
            .await;

        let http = HttpClient::new(HttpConfig {
            base_url: mock_server.uri(),
            directory: None,
            workspace: None,
            timeout: Duration::from_secs(30),
        })
        .unwrap();

        let sessions = SessionsApi::new(http);
        let result = sessions.diff("missing").await;
        assert!(result.is_err());
        assert!(result.unwrap_err().is_not_found());
    }

    #[tokio::test]
    async fn test_summarize_validation_error() {
        let mock_server = MockServer::start().await;

        Mock::given(method("POST"))
            .and(path("/session/s1/summarize"))
            .respond_with(ResponseTemplate::new(400).set_body_json(serde_json::json!({
                "name": "ValidationError",
                "message": "Invalid provider or model"
            })))
            .mount(&mock_server)
            .await;

        let http = HttpClient::new(HttpConfig {
            base_url: mock_server.uri(),
            directory: None,
            workspace: None,
            timeout: Duration::from_secs(30),
        })
        .unwrap();

        let sessions = SessionsApi::new(http);
        let result = sessions
            .summarize(
                "s1",
                &SummarizeRequest {
                    provider_id: "invalid".into(),
                    model_id: "invalid".into(),
                    auto: None,
                },
            )
            .await;
        assert!(result.is_err());
        assert!(result.unwrap_err().is_validation_error());
    }

    #[tokio::test]
    async fn test_revert_not_found() {
        let mock_server = MockServer::start().await;

        Mock::given(method("POST"))
            .and(path("/session/missing/revert"))
            .respond_with(ResponseTemplate::new(404).set_body_json(serde_json::json!({
                "name": "NotFound",
                "message": "Session not found"
            })))
            .mount(&mock_server)
            .await;

        let http = HttpClient::new(HttpConfig {
            base_url: mock_server.uri(),
            directory: None,
            workspace: None,
            timeout: Duration::from_secs(30),
        })
        .unwrap();

        let sessions = SessionsApi::new(http);
        let result = sessions
            .revert(
                "missing",
                &crate::types::session::RevertRequest {
                    message_id: "m1".into(),
                    part_id: None,
                },
            )
            .await;
        assert!(result.is_err());
        assert!(result.unwrap_err().is_not_found());
    }

    #[tokio::test]
    async fn test_abort_server_error() {
        let mock_server = MockServer::start().await;

        Mock::given(method("POST"))
            .and(path("/session/s1/abort"))
            .respond_with(ResponseTemplate::new(500).set_body_json(serde_json::json!({
                "name": "InternalError",
                "message": "Failed to abort session"
            })))
            .mount(&mock_server)
            .await;

        let http = HttpClient::new(HttpConfig {
            base_url: mock_server.uri(),
            directory: None,
            workspace: None,
            timeout: Duration::from_secs(30),
        })
        .unwrap();

        let sessions = SessionsApi::new(http);
        let result = sessions.abort("s1").await;
        assert!(result.is_err());
        assert!(result.unwrap_err().is_server_error());
    }

    #[tokio::test]
    async fn test_todo_not_found() {
        let mock_server = MockServer::start().await;

        Mock::given(method("GET"))
            .and(path("/session/missing/todo"))
            .respond_with(ResponseTemplate::new(404).set_body_json(serde_json::json!({
                "name": "NotFound",
                "message": "Session not found"
            })))
            .mount(&mock_server)
            .await;

        let http = HttpClient::new(HttpConfig {
            base_url: mock_server.uri(),
            directory: None,
            workspace: None,
            timeout: Duration::from_secs(30),
        })
        .unwrap();

        let sessions = SessionsApi::new(http);
        let result = sessions.todo("missing").await;
        assert!(result.is_err());
        assert!(result.unwrap_err().is_not_found());
    }
}
