use reqwest::Client;
use serde_json::Value;

/// Sanitize a value used as a URL path segment.
/// Strips characters that could cause path traversal or query injection.
fn sanitize_path_segment(s: &str) -> String {
    s.chars()
        .filter(|c| c.is_alphanumeric() || *c == '-' || *c == '_')
        .collect()
}

/// REST API client for the Roboticus server.
pub struct ApiClient {
    base_url: String,
    api_key: Option<String>,
    client: Client,
}

#[allow(dead_code)]
impl ApiClient {
    pub fn new(base_url: &str, api_key: Option<String>) -> Self {
        if let Some(ref key) = api_key {
            if key.contains('\r') || key.contains('\n') {
                panic!("API key must not contain newline (\\r or \\n) characters");
            }
        }
        Self {
            base_url: base_url.trim_end_matches('/').to_string(),
            api_key,
            client: Client::new(),
        }
    }

    fn request(&self, method: reqwest::Method, path: &str) -> reqwest::RequestBuilder {
        let mut req = self
            .client
            .request(method, format!("{}{path}", self.base_url));
        if let Some(ref key) = self.api_key {
            req = req.header("x-api-key", key);
        }
        req
    }

    pub async fn health(&self) -> Result<Value, reqwest::Error> {
        self.request(reqwest::Method::GET, "/api/health")
            .send()
            .await?
            .json()
            .await
    }

    pub async fn create_session(&self) -> Result<Value, reqwest::Error> {
        self.request(reqwest::Method::POST, "/api/sessions")
            .json(&serde_json::json!({}))
            .send()
            .await?
            .error_for_status()?
            .json()
            .await
    }

    pub async fn get_session_messages(&self, session_id: &str) -> Result<Value, reqwest::Error> {
        let safe_id = sanitize_path_segment(session_id);
        self.request(
            reqwest::Method::GET,
            &format!("/api/sessions/{safe_id}/messages"),
        )
        .send()
        .await?
        .json()
        .await
    }

    pub async fn send_message(
        &self,
        session_id: &str,
        content: &str,
    ) -> Result<Value, reqwest::Error> {
        self.request(reqwest::Method::POST, "/api/agent/message")
            .json(&serde_json::json!({
                "content": content,
                "session_id": session_id,
                "channel": "tui",
            }))
            .send()
            .await?
            .error_for_status()?
            .json()
            .await
    }

    pub async fn get_ws_ticket(&self) -> Result<Option<String>, reqwest::Error> {
        if self.api_key.is_none() {
            return Ok(None);
        }
        let resp: Value = self
            .request(reqwest::Method::POST, "/api/ws-ticket")
            .send()
            .await?
            .json()
            .await?;
        Ok(resp
            .get("ticket")
            .and_then(|v| v.as_str())
            .map(String::from))
    }

    pub async fn agent_status(&self) -> Result<Value, reqwest::Error> {
        self.request(reqwest::Method::GET, "/api/agent/status")
            .send()
            .await?
            .json()
            .await
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use wiremock::matchers::{body_json, header, method, path};
    use wiremock::{Mock, MockServer, ResponseTemplate};

    #[tokio::test]
    async fn get_ws_ticket_returns_none_without_api_key() {
        let client = ApiClient::new("http://localhost:3000", None);
        assert_eq!(client.get_ws_ticket().await.unwrap(), None);
    }

    #[test]
    fn new_with_no_api_key() {
        let client = ApiClient::new("http://localhost:3000", None);
        assert_eq!(client.base_url, "http://localhost:3000");
        assert!(client.api_key.is_none());
    }

    #[test]
    fn new_with_valid_api_key() {
        let client = ApiClient::new("http://localhost:3000/", Some("my-key".into()));
        assert_eq!(client.base_url, "http://localhost:3000");
        assert_eq!(client.api_key.as_deref(), Some("my-key"));
    }

    #[test]
    fn new_strips_trailing_slash() {
        let client = ApiClient::new("http://example.com///", None);
        assert_eq!(client.base_url, "http://example.com");
    }

    #[test]
    #[should_panic(expected = "API key must not contain newline")]
    fn new_panics_on_newline_in_key() {
        ApiClient::new("http://localhost", Some("bad\nkey".into()));
    }

    #[test]
    #[should_panic(expected = "API key must not contain newline")]
    fn new_panics_on_carriage_return_in_key() {
        ApiClient::new("http://localhost", Some("bad\rkey".into()));
    }

    #[test]
    fn new_allows_key_with_special_chars_no_newlines() {
        // Should not panic
        let client = ApiClient::new(
            "http://localhost",
            Some("key-with-$pecial_chars!@#%^&*()".into()),
        );
        assert!(client.api_key.is_some());
    }

    #[test]
    fn sanitize_path_segment_passes_alphanumeric() {
        assert_eq!(sanitize_path_segment("abc-123_XYZ"), "abc-123_XYZ");
    }

    #[test]
    fn sanitize_path_segment_strips_traversal() {
        assert_eq!(sanitize_path_segment("../../etc/passwd"), "etcpasswd");
    }

    #[test]
    fn sanitize_path_segment_strips_query_chars() {
        assert_eq!(sanitize_path_segment("id?foo=bar&x=1"), "idfoobarx1");
    }

    #[test]
    fn sanitize_path_segment_strips_slashes() {
        assert_eq!(sanitize_path_segment("a/b\\c"), "abc");
    }

    #[test]
    fn request_builds_full_url_without_api_key_header() {
        let client = ApiClient::new("http://localhost:3000/", None);
        let request = client
            .request(reqwest::Method::GET, "/api/health")
            .build()
            .unwrap();
        assert_eq!(request.url().as_str(), "http://localhost:3000/api/health");
        assert!(request.headers().get("x-api-key").is_none());
    }

    #[test]
    fn request_adds_api_key_header_when_present() {
        let client = ApiClient::new("http://localhost:3000", Some("secret-key".into()));
        let request = client
            .request(reqwest::Method::POST, "/api/ws-ticket")
            .build()
            .unwrap();
        assert_eq!(
            request.url().as_str(),
            "http://localhost:3000/api/ws-ticket"
        );
        assert_eq!(request.headers().get("x-api-key").unwrap(), "secret-key");
    }

    #[tokio::test]
    async fn health_uses_expected_route() {
        let server = MockServer::start().await;
        Mock::given(method("GET"))
            .and(path("/api/health"))
            .respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!({
                "ok": true,
                "agent": "Roboticus"
            })))
            .mount(&server)
            .await;

        let client = ApiClient::new(&server.uri(), None);
        let response = client.health().await.unwrap();
        assert_eq!(response["agent"], "Roboticus");
    }

    #[tokio::test]
    async fn create_session_posts_empty_json_body() {
        let server = MockServer::start().await;
        Mock::given(method("POST"))
            .and(path("/api/sessions"))
            .and(body_json(serde_json::json!({})))
            .respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!({
                "session_id": "sess-123"
            })))
            .mount(&server)
            .await;

        let client = ApiClient::new(&server.uri(), None);
        let response = client.create_session().await.unwrap();
        assert_eq!(response["session_id"], "sess-123");
    }

    #[tokio::test]
    async fn get_session_messages_sanitizes_path_segment() {
        let server = MockServer::start().await;
        Mock::given(method("GET"))
            .and(path("/api/sessions/etcpasswd/messages"))
            .respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!({
                "messages": []
            })))
            .mount(&server)
            .await;

        let client = ApiClient::new(&server.uri(), None);
        let response = client
            .get_session_messages("../../etc/passwd")
            .await
            .unwrap();
        assert_eq!(response["messages"].as_array().unwrap().len(), 0);
    }

    #[tokio::test]
    async fn send_message_posts_expected_payload_and_auth_header() {
        let server = MockServer::start().await;
        Mock::given(method("POST"))
            .and(path("/api/agent/message"))
            .and(header("x-api-key", "secret-key"))
            .and(body_json(serde_json::json!({
                "content": "hello",
                "session_id": "sess-123",
                "channel": "tui"
            })))
            .respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!({
                "content": "hi back"
            })))
            .mount(&server)
            .await;

        let client = ApiClient::new(&server.uri(), Some("secret-key".into()));
        let response = client.send_message("sess-123", "hello").await.unwrap();
        assert_eq!(response["content"], "hi back");
    }

    #[tokio::test]
    async fn get_ws_ticket_reads_ticket_when_api_key_present() {
        let server = MockServer::start().await;
        Mock::given(method("POST"))
            .and(path("/api/ws-ticket"))
            .and(header("x-api-key", "secret-key"))
            .respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!({
                "ticket": "ticket-123"
            })))
            .mount(&server)
            .await;

        let client = ApiClient::new(&server.uri(), Some("secret-key".into()));
        let response = client.get_ws_ticket().await.unwrap();
        assert_eq!(response.as_deref(), Some("ticket-123"));
    }

    #[tokio::test]
    async fn agent_status_uses_expected_route() {
        let server = MockServer::start().await;
        Mock::given(method("GET"))
            .and(path("/api/agent/status"))
            .respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!({
                "status": "running"
            })))
            .mount(&server)
            .await;

        let client = ApiClient::new(&server.uri(), None);
        let response = client.agent_status().await.unwrap();
        assert_eq!(response["status"], "running");
    }
}
