/// Network simulation — fetch() and XMLHttpRequest.
///
/// NO real network connections. Every request is logged as an observation
/// and returns a programmable fake response. The researcher can configure
/// responses per URL pattern.
///
use std::collections::HashMap;

use jsdet_core::observation::Value;

/// A fake HTTP response.
#[derive(Debug, Clone)]
pub struct FakeResponse {
    pub status: u16,
    pub status_text: String,
    pub headers: HashMap<String, String>,
    pub body: String,
    pub content_type: String,
}

impl Default for FakeResponse {
    fn default() -> Self {
        Self {
            status: 200,
            status_text: "OK".into(),
            headers: HashMap::new(),
            body: String::new(),
            content_type: "text/html".into(),
        }
    }
}

impl FakeResponse {
    pub fn ok(body: &str) -> Self {
        Self {
            body: body.into(),
            ..Self::default()
        }
    }

    pub fn json(body: &str) -> Self {
        Self {
            body: body.into(),
            content_type: "application/json".into(),
            ..Self::default()
        }
    }

    pub fn not_found() -> Self {
        Self {
            status: 404,
            status_text: "Not Found".into(),
            body: "Not Found".into(),
            ..Self::default()
        }
    }

    pub fn error() -> Self {
        Self {
            status: 500,
            status_text: "Internal Server Error".into(),
            body: "Internal Server Error".into(),
            ..Self::default()
        }
    }

    /// Serialize as a JSON object matching the Fetch API Response shape.
    pub fn to_fetch_response_json(&self) -> String {
        let headers_json = serde_json::to_string(&self.headers).unwrap_or_default();
        format!(
            r#"{{"ok":{},"status":{},"statusText":"{}","headers":{},"body":"{}","type":"basic","url":"","redirected":false}}"#,
            self.status >= 200 && self.status < 300,
            self.status,
            self.status_text,
            headers_json,
            self.body.replace('"', "\\\""),
        )
    }
}

/// Registry of URL patterns → fake responses.
///
/// Researchers configure this to simulate specific server behaviors:
/// - C2 communication patterns
/// - Login page responses
/// - Redirect chains
#[derive(Debug, Default)]
pub struct ResponseRegistry {
    /// Exact URL → response.
    exact: HashMap<String, FakeResponse>,
    /// URL prefix → response.
    prefix: Vec<(String, FakeResponse)>,
    /// Default response when no pattern matches.
    fallback: Option<FakeResponse>,
}

impl ResponseRegistry {
    pub fn new() -> Self {
        Self::default()
    }

    /// Register a response for an exact URL.
    pub fn add_exact(&mut self, url: impl Into<String>, response: FakeResponse) {
        self.exact.insert(url.into(), response);
    }

    /// Register a response for all URLs starting with prefix.
    pub fn add_prefix(&mut self, prefix: impl Into<String>, response: FakeResponse) {
        self.prefix.push((prefix.into(), response));
    }

    /// Set the fallback response for unmatched URLs.
    pub fn set_fallback(&mut self, response: FakeResponse) {
        self.fallback = Some(response);
    }

    /// Look up the response for a URL.
    pub fn get(&self, url: &str) -> FakeResponse {
        if let Some(r) = self.exact.get(url) {
            return r.clone();
        }
        for (prefix, response) in &self.prefix {
            if url.starts_with(prefix.as_str()) {
                return response.clone();
            }
        }
        self.fallback
            .clone()
            .unwrap_or_else(FakeResponse::not_found)
    }
}

/// Handle a fetch() call.
pub fn handle_fetch(url: &str, _method: &str, registry: &ResponseRegistry) -> Value {
    let response = registry.get(url);
    Value::json(response.to_fetch_response_json())
}

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

    #[test]
    fn exact_url_match() {
        let mut reg = ResponseRegistry::new();
        reg.add_exact(
            "https://api.example.com/data",
            FakeResponse::json(r#"{"key":"val"}"#),
        );
        let resp = reg.get("https://api.example.com/data");
        assert_eq!(resp.status, 200);
        assert!(resp.body.contains("key"));
    }

    #[test]
    fn prefix_match() {
        let mut reg = ResponseRegistry::new();
        reg.add_prefix("https://cdn.", FakeResponse::ok("cdn content"));
        let resp = reg.get("https://cdn.example.com/lib.js");
        assert_eq!(resp.body, "cdn content");
    }

    #[test]
    fn fallback_when_no_match() {
        let reg = ResponseRegistry::new();
        let resp = reg.get("https://unknown.com");
        assert_eq!(resp.status, 404);
    }
}
