use chrono::{TimeZone, Utc};
use futures_util::{SinkExt, StreamExt};
use serde_json::json;
use std::collections::HashMap;
use std::sync::Mutex;
use tokio::sync::broadcast;
use tokio_tungstenite::{connect_async, tungstenite::Message};
use tracing::{debug, error, info, warn};
use uforia_ms_types::{Asset, CmError, IvEvent, IvQuote};

use crate::reconnect::ReconnectPolicy;

/// Number of strikes to select on each side of ATM.
const STRIKES_PER_SIDE: usize = 3;

/// Interval between option subscription refreshes (seconds).
const REFRESH_INTERVAL_SECS: u64 = 15 * 60;

/// Cached IV data for a single option instrument.
#[derive(Debug, Clone)]
pub struct OptionTickerState {
    pub instrument: String,
    pub asset: Asset,
    pub expiry: String,
    pub strike: f64,
    pub kind: OptionKind,
    pub mark_iv: f64,
    pub delta: f64,
    pub underlying_price: f64,
    pub timestamp_ms: i64,
}

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum OptionKind {
    Call,
    Put,
}

/// Deribit WebSocket feed for options implied volatility.
///
/// Subscribes to `ticker.{instrument}` channels for ATM options to derive
/// implied volatility. Computes ATM IV from the `mark_iv` field in Deribit
/// ticker updates. Tracks a surface of near-ATM options across the nearest
/// expiries and computes skew_25d and term_slope.
pub struct DeribitFeed {
    ws_url: String,
    rest_url: String,
    assets: Vec<Asset>,
    iv_tx: broadcast::Sender<IvEvent>,
    reconnect_base_ms: u64,
    reconnect_max_ms: u64,
    /// Per-instrument IV state: keyed by (Asset, instrument_name).
    iv_state: Mutex<HashMap<(Asset, String), OptionTickerState>>,
    /// Currently subscribed option channels (so we can diff on refresh).
    option_channels: Mutex<Vec<String>>,
}

impl DeribitFeed {
    pub fn new(
        ws_url: String,
        assets: Vec<Asset>,
        iv_tx: broadcast::Sender<IvEvent>,
        reconnect_base_ms: u64,
        reconnect_max_ms: u64,
    ) -> Self {
        Self {
            rest_url: ws_url
                .replace("wss://", "https://")
                .replace("ws://", "http://")
                .replace("/ws/api/v2", "/api/v2"),
            ws_url,
            assets,
            iv_tx,
            reconnect_base_ms,
            reconnect_max_ms,
            iv_state: Mutex::new(HashMap::new()),
            option_channels: Mutex::new(Vec::new()),
        }
    }

    fn resolve_asset(instrument: &str) -> Option<Asset> {
        if instrument.starts_with("BTC") {
            Some(Asset::Btc)
        } else if instrument.starts_with("ETH") {
            Some(Asset::Eth)
        } else {
            None
        }
    }

    /// Parse a Deribit option instrument name like "BTC-28JUN24-50000-C".
    /// Returns (expiry, strike, kind) or None if not an option instrument.
    fn parse_option_instrument(instrument: &str) -> Option<(String, f64, OptionKind)> {
        let parts: Vec<&str> = instrument.split('-').collect();
        if parts.len() != 4 {
            return None;
        }
        let expiry = parts[1].to_string();
        let strike: f64 = parts[2].parse().ok()?;
        let kind = match parts[3] {
            "C" => OptionKind::Call,
            "P" => OptionKind::Put,
            _ => return None,
        };
        Some((expiry, strike, kind))
    }

    /// Build the Deribit subscribe message using JSON-RPC.
    ///
    /// Subscribes to `ticker.{ASSET}-PERPETUAL.raw` plus any option channels
    /// that have been discovered.
    fn subscribe_message(&self) -> String {
        let mut channels: Vec<String> = self
            .assets
            .iter()
            .map(|asset| {
                let base = asset.symbol(uforia_ms_types::Venue::Deribit);
                format!("ticker.{}-PERPETUAL.raw", base)
            })
            .collect();

        if let Ok(opt_channels) = self.option_channels.lock() {
            channels.extend(opt_channels.iter().cloned());
        }

        json!({
            "jsonrpc": "2.0",
            "id": 1,
            "method": "public/subscribe",
            "params": {
                "channels": channels
            }
        })
        .to_string()
    }

    /// Build option ticker channels for a given asset and spot price.
    ///
    /// Selects the nearest expiry and picks `STRIKES_PER_SIDE` strikes on each
    /// side of ATM, for both calls and puts. Returns channel names like
    /// `ticker.BTC-28JUN24-50000-C.raw`.
    pub fn build_option_channels(instruments: &[InstrumentInfo], spot_price: f64) -> Vec<String> {
        if instruments.is_empty() || spot_price <= 0.0 {
            return Vec::new();
        }

        // Group by expiry, pick the two nearest
        let mut expiries: Vec<String> = instruments
            .iter()
            .map(|i| i.expiry.clone())
            .collect::<std::collections::HashSet<_>>()
            .into_iter()
            .collect();
        expiries.sort();

        let selected_expiries: Vec<&str> = expiries.iter().take(2).map(|s| s.as_str()).collect();

        let mut channels = Vec::new();

        for expiry in &selected_expiries {
            // Get all strikes for this expiry (deduped)
            let mut strikes: Vec<f64> = instruments
                .iter()
                .filter(|i| i.expiry == *expiry)
                .map(|i| ordered_float::OrderedFloat(i.strike))
                .collect::<std::collections::HashSet<_>>()
                .into_iter()
                .map(|s| s.into_inner())
                .collect();
            strikes.sort_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal));

            // Find nearest ATM index
            let atm_idx = strikes
                .iter()
                .enumerate()
                .min_by(|(_, a), (_, b)| {
                    let da = (*a - spot_price).abs();
                    let db = (*b - spot_price).abs();
                    da.partial_cmp(&db).unwrap_or(std::cmp::Ordering::Equal)
                })
                .map(|(i, _)| i)
                .unwrap_or(0);

            let lo = atm_idx.saturating_sub(STRIKES_PER_SIDE);
            let hi = (atm_idx + STRIKES_PER_SIDE + 1).min(strikes.len());
            let selected = &strikes[lo..hi];

            let asset_prefix = instruments
                .iter()
                .find(|i| i.expiry == *expiry)
                .map(|i| i.base_currency.as_str())
                .unwrap_or("BTC");

            for &strike in selected {
                let strike_str = format_strike(strike);
                for kind in &["C", "P"] {
                    channels.push(format!(
                        "ticker.{}-{}-{}-{}.raw",
                        asset_prefix, expiry, strike_str, kind
                    ));
                }
            }
        }

        channels
    }

    /// Fetch active option instruments from the Deribit REST API for a given
    /// currency, and select near-ATM channels based on `spot_price`.
    pub async fn fetch_option_instruments(
        rest_url: &str,
        currency: &str,
    ) -> Result<Vec<InstrumentInfo>, CmError> {
        let url = format!(
            "{}/public/get_instruments?currency={}&kind=option&expired=false",
            rest_url, currency
        );

        let resp = reqwest::get(&url).await.map_err(|e| CmError::Feed {
            venue: "deribit".into(),
            msg: format!("REST get_instruments failed: {}", e),
        })?;

        let body: serde_json::Value = resp.json().await.map_err(|e| CmError::Parse {
            context: "deribit_instruments".into(),
            msg: format!("JSON parse error: {}", e),
        })?;

        let result = body
            .get("result")
            .and_then(|v| v.as_array())
            .ok_or_else(|| CmError::Parse {
                context: "deribit_instruments".into(),
                msg: "missing 'result' array".into(),
            })?;

        let mut instruments = Vec::new();
        for item in result {
            let name = item
                .get("instrument_name")
                .and_then(|v| v.as_str())
                .unwrap_or("");
            let strike = item.get("strike").and_then(|v| v.as_f64()).unwrap_or(0.0);
            let expiry_ts = item
                .get("expiration_timestamp")
                .and_then(|v| v.as_i64())
                .unwrap_or(0);
            let base = item
                .get("base_currency")
                .and_then(|v| v.as_str())
                .unwrap_or(currency);

            // Extract expiry string from instrument name (e.g., "28JUN24" from "BTC-28JUN24-50000-C")
            let parts: Vec<&str> = name.split('-').collect();
            if parts.len() != 4 {
                continue;
            }

            instruments.push(InstrumentInfo {
                instrument_name: name.to_string(),
                base_currency: base.to_string(),
                expiry: parts[1].to_string(),
                expiry_timestamp: expiry_ts,
                strike,
            });
        }

        Ok(instruments)
    }

    /// Refresh option subscriptions based on current spot prices.
    /// Returns new subscribe/unsubscribe messages if channels changed.
    pub async fn refresh_subscriptions(&self) -> Result<Option<(String, Option<String>)>, CmError> {
        let mut new_channels = Vec::new();

        for asset in &self.assets {
            let currency = asset.symbol(uforia_ms_types::Venue::Deribit);

            // Get current spot price from IV state (from perpetual ticker)
            let spot = self.get_spot_price(*asset);
            if spot <= 0.0 {
                debug!(asset = %asset, "no spot price yet, skipping option refresh");
                continue;
            }

            let instruments = Self::fetch_option_instruments(&self.rest_url, currency).await?;

            let channels = Self::build_option_channels(&instruments, spot);
            new_channels.extend(channels);
        }

        let old_channels = {
            let guard = self.option_channels.lock().unwrap();
            guard.clone()
        };

        if new_channels == old_channels {
            return Ok(None);
        }

        // Compute channels to unsubscribe (in old but not in new)
        let unsub: Vec<String> = old_channels
            .iter()
            .filter(|c| !new_channels.contains(c))
            .cloned()
            .collect();

        // Store the new channels
        {
            let mut guard = self.option_channels.lock().unwrap();
            *guard = new_channels.clone();
        }

        // Clear stale IV state for instruments we're unsubscribing from
        if !unsub.is_empty() {
            let mut state = self.iv_state.lock().unwrap();
            state.retain(|(_asset, inst), _| {
                let channel = format!("ticker.{}.raw", inst);
                !unsub.contains(&channel)
            });
        }

        let sub_msg = json!({
            "jsonrpc": "2.0",
            "id": 2,
            "method": "public/subscribe",
            "params": {
                "channels": new_channels
            }
        })
        .to_string();

        let unsub_msg = if unsub.is_empty() {
            None
        } else {
            Some(
                json!({
                    "jsonrpc": "2.0",
                    "id": 3,
                    "method": "public/unsubscribe",
                    "params": {
                        "channels": unsub
                    }
                })
                .to_string(),
            )
        };

        info!(
            new_count = new_channels.len(),
            unsub_count = unsub.len(),
            "refreshed option subscriptions"
        );

        Ok(Some((sub_msg, unsub_msg)))
    }

    /// Get current spot price for an asset from perpetual mark_price stored in
    /// IV state, or from any option's underlying_price.
    fn get_spot_price(&self, asset: Asset) -> f64 {
        let state = self.iv_state.lock().unwrap();
        // Check for underlying_price from any option ticker for this asset
        for ((a, _), ticker) in state.iter() {
            if *a == asset && ticker.underlying_price > 0.0 {
                return ticker.underlying_price;
            }
        }
        0.0
    }

    pub async fn run(
        &self,
        mut shutdown: tokio::sync::watch::Receiver<bool>,
    ) -> Result<(), CmError> {
        let mut policy = ReconnectPolicy::new(self.reconnect_base_ms, self.reconnect_max_ms);
        info!(url = %self.ws_url, "deribit feed starting");

        loop {
            match connect_async(&self.ws_url).await {
                Ok((ws_stream, _)) => {
                    policy.reset();
                    info!("deribit connected");

                    let (mut write, mut read) = ws_stream.split();

                    // Subscribe to perpetual tickers + any known option channels
                    let sub_msg = self.subscribe_message();
                    if let Err(e) = write.send(Message::Text(sub_msg.into())).await {
                        error!(error = %e, "deribit failed to subscribe");
                        continue;
                    }
                    info!("deribit subscribed");

                    // Deribit sends heartbeat requests that need responses
                    // Also set heartbeat
                    let heartbeat_msg = json!({
                        "jsonrpc": "2.0",
                        "id": 9999,
                        "method": "public/set_heartbeat",
                        "params": {
                            "interval": 30
                        }
                    })
                    .to_string();
                    let _ = write.send(Message::Text(heartbeat_msg.into())).await;

                    // Timer for refreshing option subscriptions
                    let refresh_interval = tokio::time::Duration::from_secs(REFRESH_INTERVAL_SECS);
                    let mut refresh_timer = tokio::time::interval(refresh_interval);
                    // Skip the first immediate tick
                    refresh_timer.tick().await;

                    loop {
                        tokio::select! {
                            msg = read.next() => {
                                match msg {
                                    Some(Ok(Message::Text(text))) => {
                                        match self.handle_message(&text) {
                                            Ok(Some(response)) => {
                                                let _ = write.send(Message::Text(response.into())).await;
                                            }
                                            Ok(None) => {}
                                            Err(e) => {
                                                debug!(error = %e, "deribit parse error");
                                            }
                                        }
                                    }
                                    Some(Ok(Message::Ping(data))) => {
                                        let _ = write.send(Message::Pong(data)).await;
                                    }
                                    Some(Ok(Message::Close(_))) => {
                                        warn!("deribit received close frame");
                                        break;
                                    }
                                    Some(Err(e)) => {
                                        error!(error = %e, "deribit ws error");
                                        break;
                                    }
                                    None => {
                                        warn!("deribit stream ended");
                                        break;
                                    }
                                    _ => {}
                                }
                            }
                            _ = refresh_timer.tick() => {
                                match self.refresh_subscriptions().await {
                                    Ok(Some((sub, unsub))) => {
                                        if let Some(unsub_msg) = unsub {
                                            let _ = write.send(Message::Text(unsub_msg.into())).await;
                                        }
                                        let _ = write.send(Message::Text(sub.into())).await;
                                    }
                                    Ok(None) => {
                                        debug!("option subscriptions unchanged");
                                    }
                                    Err(e) => {
                                        warn!(error = %e, "failed to refresh option subscriptions");
                                    }
                                }
                            }
                            _ = shutdown.changed() => {
                                if *shutdown.borrow() {
                                    info!("deribit shutting down");
                                    return Ok(());
                                }
                            }
                        }
                    }
                }
                Err(e) => {
                    error!(error = %e, "deribit connection failed");
                }
            }

            if *shutdown.borrow() {
                info!("deribit shutting down (before reconnect)");
                return Ok(());
            }

            policy.wait("deribit").await;
        }
    }

    /// Handle a Deribit JSON-RPC message. Returns an optional response string
    /// (for heartbeat test_request responses).
    fn handle_message(&self, text: &str) -> Result<Option<String>, CmError> {
        let msg: serde_json::Value = serde_json::from_str(text).map_err(|e| CmError::Parse {
            context: "deribit".into(),
            msg: format!("JSON parse error: {}", e),
        })?;

        // Handle heartbeat test_request
        if let Some(method) = msg.get("method").and_then(|v| v.as_str()) {
            if method == "heartbeat" {
                let hb_type = msg
                    .get("params")
                    .and_then(|p| p.get("type"))
                    .and_then(|v| v.as_str())
                    .unwrap_or("");

                if hb_type == "test_request" {
                    let response = json!({
                        "jsonrpc": "2.0",
                        "id": 9998,
                        "method": "public/test"
                    })
                    .to_string();
                    return Ok(Some(response));
                }
                return Ok(None);
            }

            // Handle subscription notifications
            if method == "subscription" {
                if let Some(params) = msg.get("params") {
                    self.handle_subscription(params)?;
                }
                return Ok(None);
            }
        }

        // Handle RPC responses (subscribe confirmations, etc.)
        if msg.get("id").is_some() {
            debug!("deribit RPC response");
        }

        Ok(None)
    }

    fn handle_subscription(&self, params: &serde_json::Value) -> Result<(), CmError> {
        let channel = params.get("channel").and_then(|v| v.as_str()).unwrap_or("");

        let data = params.get("data").ok_or_else(|| CmError::Parse {
            context: "deribit_subscription".into(),
            msg: "missing 'data'".into(),
        })?;

        if channel.starts_with("ticker.") {
            self.handle_ticker(channel, data)?;
        }

        Ok(())
    }

    /// Handle a Deribit ticker update.
    ///
    /// For option instruments, stores IV data in `iv_state` and computes
    /// aggregate IV metrics (ATM IV, skew_25d, term_slope).
    ///
    /// For perpetual tickers, the mark_price is used as a spot price proxy
    /// but no IV event is emitted (no mark_iv field).
    fn handle_ticker(&self, _channel: &str, data: &serde_json::Value) -> Result<(), CmError> {
        let instrument = data
            .get("instrument_name")
            .and_then(|v| v.as_str())
            .unwrap_or("");

        let asset = match Self::resolve_asset(instrument) {
            Some(a) => a,
            None => return Ok(()),
        };

        let ts_ms = data
            .get("timestamp")
            .and_then(|v| v.as_i64())
            .unwrap_or_else(|| Utc::now().timestamp_millis());

        let timestamp = Utc
            .timestamp_millis_opt(ts_ms)
            .single()
            .unwrap_or_else(Utc::now);

        // Check if this is an option instrument
        if let Some((expiry, strike, kind)) = Self::parse_option_instrument(instrument) {
            if let Some(mark_iv) = data.get("mark_iv").and_then(|v| v.as_f64()) {
                let delta = data
                    .get("greeks")
                    .and_then(|g| g.get("delta"))
                    .and_then(|v| v.as_f64())
                    .unwrap_or(0.0);

                let underlying_price = data
                    .get("underlying_price")
                    .and_then(|v| v.as_f64())
                    .unwrap_or(0.0);

                let ticker_state = OptionTickerState {
                    instrument: instrument.to_string(),
                    asset,
                    expiry,
                    strike,
                    kind,
                    mark_iv: mark_iv / 100.0,
                    delta,
                    underlying_price,
                    timestamp_ms: ts_ms,
                };

                // Store in IV state
                {
                    let mut state = self.iv_state.lock().unwrap();
                    state.insert((asset, instrument.to_string()), ticker_state);
                }

                // Compute and publish aggregated IV metrics
                let iv_quote = self.compute_iv_surface(asset, timestamp);
                let _ = self.iv_tx.send(IvEvent::IvUpdate(iv_quote));
            }
        } else if instrument.contains("PERPETUAL") {
            // Store underlying price from perpetual mark_price so
            // refresh_subscriptions can use it for ATM strike selection.
            // We do NOT emit an IvEvent for perpetuals (no mark_iv).
            let mark_price = data
                .get("mark_price")
                .and_then(|v| v.as_f64())
                .unwrap_or(0.0);

            if mark_price > 0.0 {
                let mut state = self.iv_state.lock().unwrap();
                // Store as a synthetic entry so get_spot_price finds it
                state.insert(
                    (asset, instrument.to_string()),
                    OptionTickerState {
                        instrument: instrument.to_string(),
                        asset,
                        expiry: String::new(),
                        strike: 0.0,
                        kind: OptionKind::Call,
                        mark_iv: 0.0,
                        delta: 0.0,
                        underlying_price: mark_price,
                        timestamp_ms: ts_ms,
                    },
                );
            }
        }

        Ok(())
    }

    /// Compute IV surface metrics from stored option ticker data for a given asset.
    ///
    /// - `atm_iv`: IV of the option closest to delta=0.5 (ATM) in the nearest expiry.
    /// - `skew_25d`: IV(25d put) - IV(25d call). Approximated by finding the puts
    ///   with delta nearest -0.25 and calls with delta nearest 0.25.
    /// - `term_slope`: IV(ATM second expiry) - IV(ATM first expiry).
    fn compute_iv_surface(&self, asset: Asset, timestamp: chrono::DateTime<Utc>) -> IvQuote {
        let state = self.iv_state.lock().unwrap();

        // Collect all option entries (excluding perpetual synthetic entries)
        let options: Vec<&OptionTickerState> = state
            .values()
            .filter(|s| s.asset == asset && !s.expiry.is_empty() && s.mark_iv > 0.0)
            .collect();

        if options.is_empty() {
            return IvQuote {
                timestamp,
                asset,
                atm_iv: 0.0,
                skew_25d: 0.0,
                term_slope: 0.0,
            };
        }

        // Group by expiry
        let mut by_expiry: HashMap<&str, Vec<&OptionTickerState>> = HashMap::new();
        for opt in &options {
            by_expiry.entry(opt.expiry.as_str()).or_default().push(opt);
        }

        let mut expiries: Vec<&str> = by_expiry.keys().copied().collect();
        expiries.sort();

        // ATM IV: closest to |delta| = 0.5 in nearest expiry
        let nearest_expiry = expiries[0];
        let nearest_opts = &by_expiry[nearest_expiry];

        let atm_iv = nearest_opts
            .iter()
            .min_by(|a, b| {
                let da = (a.delta.abs() - 0.5).abs();
                let db = (b.delta.abs() - 0.5).abs();
                da.partial_cmp(&db).unwrap_or(std::cmp::Ordering::Equal)
            })
            .map(|o| o.mark_iv)
            .unwrap_or(0.0);

        // Skew 25d: find put with delta nearest -0.25 and call with delta nearest +0.25
        let put_25d_iv = nearest_opts
            .iter()
            .filter(|o| o.kind == OptionKind::Put)
            .min_by(|a, b| {
                let da = (a.delta - (-0.25)).abs();
                let db = (b.delta - (-0.25)).abs();
                da.partial_cmp(&db).unwrap_or(std::cmp::Ordering::Equal)
            })
            .map(|o| o.mark_iv);

        let call_25d_iv = nearest_opts
            .iter()
            .filter(|o| o.kind == OptionKind::Call)
            .min_by(|a, b| {
                let da = (a.delta - 0.25).abs();
                let db = (b.delta - 0.25).abs();
                da.partial_cmp(&db).unwrap_or(std::cmp::Ordering::Equal)
            })
            .map(|o| o.mark_iv);

        let skew_25d = match (put_25d_iv, call_25d_iv) {
            (Some(p), Some(c)) => p - c,
            _ => 0.0,
        };

        // Term slope: ATM IV of second expiry - ATM IV of first expiry
        let term_slope = if expiries.len() >= 2 {
            let second_expiry = expiries[1];
            let second_opts = &by_expiry[second_expiry];
            let second_atm = second_opts
                .iter()
                .min_by(|a, b| {
                    let da = (a.delta.abs() - 0.5).abs();
                    let db = (b.delta.abs() - 0.5).abs();
                    da.partial_cmp(&db).unwrap_or(std::cmp::Ordering::Equal)
                })
                .map(|o| o.mark_iv)
                .unwrap_or(0.0);
            second_atm - atm_iv
        } else {
            0.0
        };

        IvQuote {
            timestamp,
            asset,
            atm_iv,
            skew_25d,
            term_slope,
        }
    }
}

/// Format a strike price for Deribit instrument names.
/// Deribit uses integer strikes (e.g., 50000, not 50000.0).
fn format_strike(strike: f64) -> String {
    if strike == strike.floor() {
        format!("{}", strike as i64)
    } else {
        format!("{}", strike)
    }
}

/// Metadata for a Deribit option instrument from the REST API.
#[derive(Debug, Clone)]
pub struct InstrumentInfo {
    pub instrument_name: String,
    pub base_currency: String,
    pub expiry: String,
    pub expiry_timestamp: i64,
    pub strike: f64,
}

/// Parse a Deribit ticker for IV extraction (standalone helper for testing).
pub fn parse_deribit_ticker(data: &serde_json::Value) -> Result<Option<(Asset, f64)>, CmError> {
    let instrument = data
        .get("instrument_name")
        .and_then(|v| v.as_str())
        .ok_or_else(|| CmError::Parse {
            context: "deribit_ticker".into(),
            msg: "missing instrument_name".into(),
        })?;

    let asset = match DeribitFeed::resolve_asset(instrument) {
        Some(a) => a,
        None => return Ok(None),
    };

    let mark_iv = match data.get("mark_iv").and_then(|v| v.as_f64()) {
        Some(iv) => iv / 100.0,
        None => return Ok(None),
    };

    Ok(Some((asset, mark_iv)))
}

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

    fn make_feed() -> (DeribitFeed, broadcast::Receiver<IvEvent>) {
        let (tx, rx) = broadcast::channel(1024);
        let feed = DeribitFeed::new(
            "wss://www.deribit.com/ws/api/v2".into(),
            vec![Asset::Btc, Asset::Eth],
            tx,
            1000,
            60000,
        );
        (feed, rx)
    }

    #[test]
    fn test_subscribe_message() {
        let (feed, _) = make_feed();
        let sub: serde_json::Value = serde_json::from_str(&feed.subscribe_message()).unwrap();
        assert_eq!(sub["method"], "public/subscribe");
        let channels = sub["params"]["channels"].as_array().unwrap();
        assert!(channels.contains(&json!("ticker.BTC-PERPETUAL.raw")));
        assert!(channels.contains(&json!("ticker.ETH-PERPETUAL.raw")));
    }

    #[test]
    fn test_subscribe_message_includes_option_channels() {
        let (feed, _) = make_feed();

        // Simulate option channels having been discovered
        {
            let mut guard = feed.option_channels.lock().unwrap();
            *guard = vec![
                "ticker.BTC-28JUN24-50000-C.raw".to_string(),
                "ticker.BTC-28JUN24-50000-P.raw".to_string(),
            ];
        }

        let sub: serde_json::Value = serde_json::from_str(&feed.subscribe_message()).unwrap();
        let channels = sub["params"]["channels"].as_array().unwrap();
        // Perpetuals
        assert!(channels.contains(&json!("ticker.BTC-PERPETUAL.raw")));
        assert!(channels.contains(&json!("ticker.ETH-PERPETUAL.raw")));
        // Options
        assert!(channels.contains(&json!("ticker.BTC-28JUN24-50000-C.raw")));
        assert!(channels.contains(&json!("ticker.BTC-28JUN24-50000-P.raw")));
    }

    #[test]
    fn test_handle_option_ticker() {
        let (feed, mut rx) = make_feed();

        let msg = json!({
            "jsonrpc": "2.0",
            "method": "subscription",
            "params": {
                "channel": "ticker.BTC-28JUN24-50000-C.raw",
                "data": {
                    "instrument_name": "BTC-28JUN24-50000-C",
                    "timestamp": 1672515782136i64,
                    "mark_iv": 55.0,
                    "mark_price": 0.05,
                    "underlying_price": 50000.0,
                    "greeks": {
                        "delta": 0.5,
                        "gamma": 0.001,
                        "vega": 50.0,
                        "theta": -10.0
                    },
                    "interest_rate": 0.0,
                    "best_bid_price": 0.049,
                    "best_ask_price": 0.051,
                    "best_bid_amount": 10.0,
                    "best_ask_amount": 5.0
                }
            }
        });

        feed.handle_message(&msg.to_string()).unwrap();
        let event = rx.try_recv().unwrap();
        match event {
            IvEvent::IvUpdate(iv) => {
                assert_eq!(iv.asset, Asset::Btc);
                // 55.0% -> 0.55
                assert!((iv.atm_iv - 0.55).abs() < 1e-10);
            }
        }
    }

    #[test]
    fn test_handle_eth_option_ticker() {
        let (feed, mut rx) = make_feed();

        let msg = json!({
            "jsonrpc": "2.0",
            "method": "subscription",
            "params": {
                "channel": "ticker.ETH-28JUN24-3000-C.raw",
                "data": {
                    "instrument_name": "ETH-28JUN24-3000-C",
                    "timestamp": 1672515782136i64,
                    "mark_iv": 72.5,
                    "mark_price": 0.08,
                    "underlying_price": 3000.0,
                    "greeks": {
                        "delta": 0.48,
                        "gamma": 0.002,
                        "vega": 30.0,
                        "theta": -5.0
                    }
                }
            }
        });

        feed.handle_message(&msg.to_string()).unwrap();
        let event = rx.try_recv().unwrap();
        match event {
            IvEvent::IvUpdate(iv) => {
                assert_eq!(iv.asset, Asset::Eth);
                assert!((iv.atm_iv - 0.725).abs() < 1e-10);
            }
        }
    }

    #[test]
    fn test_handle_heartbeat_test_request() {
        let (feed, _) = make_feed();

        let msg = json!({
            "jsonrpc": "2.0",
            "method": "heartbeat",
            "params": {
                "type": "test_request"
            }
        });

        let result = feed.handle_message(&msg.to_string()).unwrap();
        // Should return a response to send back
        assert!(result.is_some());
        let response: serde_json::Value = serde_json::from_str(&result.unwrap()).unwrap();
        assert_eq!(response["method"], "public/test");
    }

    #[test]
    fn test_handle_heartbeat_non_test() {
        let (feed, _) = make_feed();

        let msg = json!({
            "jsonrpc": "2.0",
            "method": "heartbeat",
            "params": {
                "type": "heartbeat"
            }
        });

        let result = feed.handle_message(&msg.to_string()).unwrap();
        assert!(result.is_none());
    }

    #[test]
    fn test_handle_rpc_response() {
        let (feed, mut rx) = make_feed();

        let msg = json!({
            "jsonrpc": "2.0",
            "id": 1,
            "result": ["ticker.BTC-PERPETUAL.raw"]
        });

        feed.handle_message(&msg.to_string()).unwrap();
        assert!(rx.try_recv().is_err());
    }

    #[test]
    fn test_perpetual_ticker_no_iv() {
        let (feed, mut rx) = make_feed();

        // Perpetual tickers don't have mark_iv, so no IvEvent should be emitted
        let msg = json!({
            "jsonrpc": "2.0",
            "method": "subscription",
            "params": {
                "channel": "ticker.BTC-PERPETUAL.raw",
                "data": {
                    "instrument_name": "BTC-PERPETUAL",
                    "timestamp": 1672515782136i64,
                    "mark_price": 50000.0,
                    "index_price": 49999.5,
                    "funding_8h": 0.0001,
                    "current_funding": 0.00005,
                    "best_bid_price": 49999.0,
                    "best_ask_price": 50001.0
                }
            }
        });

        feed.handle_message(&msg.to_string()).unwrap();
        // No mark_iv -> no IvEvent
        assert!(rx.try_recv().is_err());

        // But spot price should be stored
        let spot = feed.get_spot_price(Asset::Btc);
        assert!((spot - 50000.0).abs() < 1e-10);
    }

    #[test]
    fn test_parse_deribit_ticker_helper() {
        let data = json!({
            "instrument_name": "BTC-28JUN24-50000-C",
            "timestamp": 1672515782136i64,
            "mark_iv": 60.0,
            "mark_price": 0.05
        });

        let result = parse_deribit_ticker(&data).unwrap();
        assert!(result.is_some());
        let (asset, iv) = result.unwrap();
        assert_eq!(asset, Asset::Btc);
        assert!((iv - 0.60).abs() < 1e-10);
    }

    #[test]
    fn test_parse_deribit_ticker_no_iv() {
        let data = json!({
            "instrument_name": "BTC-PERPETUAL",
            "timestamp": 1672515782136i64,
            "mark_price": 50000.0
        });

        let result = parse_deribit_ticker(&data).unwrap();
        assert!(result.is_none());
    }

    #[test]
    fn test_resolve_asset() {
        assert_eq!(
            DeribitFeed::resolve_asset("BTC-PERPETUAL"),
            Some(Asset::Btc)
        );
        assert_eq!(
            DeribitFeed::resolve_asset("BTC-28JUN24-50000-C"),
            Some(Asset::Btc)
        );
        assert_eq!(
            DeribitFeed::resolve_asset("ETH-PERPETUAL"),
            Some(Asset::Eth)
        );
        assert_eq!(DeribitFeed::resolve_asset("SOL-PERPETUAL"), None);
    }

    #[test]
    fn test_parse_option_instrument() {
        let result = DeribitFeed::parse_option_instrument("BTC-28JUN24-50000-C");
        assert!(result.is_some());
        let (expiry, strike, kind) = result.unwrap();
        assert_eq!(expiry, "28JUN24");
        assert!((strike - 50000.0).abs() < 1e-10);
        assert_eq!(kind, OptionKind::Call);

        let result = DeribitFeed::parse_option_instrument("ETH-28JUN24-3000-P");
        let (_, strike, kind) = result.unwrap();
        assert!((strike - 3000.0).abs() < 1e-10);
        assert_eq!(kind, OptionKind::Put);

        // Perpetual is not an option
        assert!(DeribitFeed::parse_option_instrument("BTC-PERPETUAL").is_none());
    }

    #[test]
    fn test_build_option_channels() {
        let instruments = vec![
            InstrumentInfo {
                instrument_name: "BTC-28JUN24-48000-C".into(),
                base_currency: "BTC".into(),
                expiry: "28JUN24".into(),
                expiry_timestamp: 1719561600000,
                strike: 48000.0,
            },
            InstrumentInfo {
                instrument_name: "BTC-28JUN24-49000-C".into(),
                base_currency: "BTC".into(),
                expiry: "28JUN24".into(),
                expiry_timestamp: 1719561600000,
                strike: 49000.0,
            },
            InstrumentInfo {
                instrument_name: "BTC-28JUN24-50000-C".into(),
                base_currency: "BTC".into(),
                expiry: "28JUN24".into(),
                expiry_timestamp: 1719561600000,
                strike: 50000.0,
            },
            InstrumentInfo {
                instrument_name: "BTC-28JUN24-51000-C".into(),
                base_currency: "BTC".into(),
                expiry: "28JUN24".into(),
                expiry_timestamp: 1719561600000,
                strike: 51000.0,
            },
            InstrumentInfo {
                instrument_name: "BTC-28JUN24-52000-C".into(),
                base_currency: "BTC".into(),
                expiry: "28JUN24".into(),
                expiry_timestamp: 1719561600000,
                strike: 52000.0,
            },
        ];

        let channels = DeribitFeed::build_option_channels(&instruments, 50000.0);

        // Should include strikes around 50000: at least 49000, 50000, 51000
        // with both C and P for each
        assert!(channels.contains(&"ticker.BTC-28JUN24-50000-C.raw".to_string()));
        assert!(channels.contains(&"ticker.BTC-28JUN24-50000-P.raw".to_string()));
        assert!(channels.contains(&"ticker.BTC-28JUN24-49000-C.raw".to_string()));
        assert!(channels.contains(&"ticker.BTC-28JUN24-51000-C.raw".to_string()));

        // Empty instruments
        let empty = DeribitFeed::build_option_channels(&[], 50000.0);
        assert!(empty.is_empty());

        // Zero spot price
        let zero_spot = DeribitFeed::build_option_channels(&instruments, 0.0);
        assert!(zero_spot.is_empty());
    }

    #[test]
    fn test_iv_surface_computation() {
        let (feed, mut rx) = make_feed();

        // Feed multiple option tickers for BTC nearest expiry
        let tickers = vec![
            // ATM call (delta ~ 0.5)
            json!({
                "jsonrpc": "2.0",
                "method": "subscription",
                "params": {
                    "channel": "ticker.BTC-28JUN24-50000-C.raw",
                    "data": {
                        "instrument_name": "BTC-28JUN24-50000-C",
                        "timestamp": 1672515782136i64,
                        "mark_iv": 55.0,
                        "underlying_price": 50000.0,
                        "greeks": { "delta": 0.50 }
                    }
                }
            }),
            // 25d call (delta ~ 0.25)
            json!({
                "jsonrpc": "2.0",
                "method": "subscription",
                "params": {
                    "channel": "ticker.BTC-28JUN24-55000-C.raw",
                    "data": {
                        "instrument_name": "BTC-28JUN24-55000-C",
                        "timestamp": 1672515782136i64,
                        "mark_iv": 52.0,
                        "underlying_price": 50000.0,
                        "greeks": { "delta": 0.25 }
                    }
                }
            }),
            // 25d put (delta ~ -0.25)
            json!({
                "jsonrpc": "2.0",
                "method": "subscription",
                "params": {
                    "channel": "ticker.BTC-28JUN24-45000-P.raw",
                    "data": {
                        "instrument_name": "BTC-28JUN24-45000-P",
                        "timestamp": 1672515782136i64,
                        "mark_iv": 60.0,
                        "underlying_price": 50000.0,
                        "greeks": { "delta": -0.25 }
                    }
                }
            }),
        ];

        // Process all tickers
        for ticker in &tickers {
            feed.handle_message(&ticker.to_string()).unwrap();
        }

        // Drain to get the last event (which has full surface)
        let mut last_event = None;
        while let Ok(event) = rx.try_recv() {
            last_event = Some(event);
        }

        let event = last_event.expect("should have received an IvEvent");
        match event {
            IvEvent::IvUpdate(iv) => {
                assert_eq!(iv.asset, Asset::Btc);
                // ATM IV: the option with delta closest to 0.5 is the 50000-C with IV=0.55
                assert!((iv.atm_iv - 0.55).abs() < 1e-10);
                // Skew 25d: put_25d_iv(0.60) - call_25d_iv(0.52) = 0.08
                assert!((iv.skew_25d - 0.08).abs() < 1e-10);
                // No second expiry, so term_slope should be 0
                assert!((iv.term_slope).abs() < 1e-10);
            }
        }
    }

    #[test]
    fn test_iv_surface_with_term_slope() {
        let (feed, mut rx) = make_feed();

        // Two different expiries
        let tickers = vec![
            // Nearest expiry ATM
            json!({
                "jsonrpc": "2.0",
                "method": "subscription",
                "params": {
                    "channel": "ticker.BTC-28JUN24-50000-C.raw",
                    "data": {
                        "instrument_name": "BTC-28JUN24-50000-C",
                        "timestamp": 1672515782136i64,
                        "mark_iv": 55.0,
                        "underlying_price": 50000.0,
                        "greeks": { "delta": 0.50 }
                    }
                }
            }),
            // Second expiry ATM (higher IV = contango)
            json!({
                "jsonrpc": "2.0",
                "method": "subscription",
                "params": {
                    "channel": "ticker.BTC-28SEP24-50000-C.raw",
                    "data": {
                        "instrument_name": "BTC-28SEP24-50000-C",
                        "timestamp": 1672515782136i64,
                        "mark_iv": 58.0,
                        "underlying_price": 50000.0,
                        "greeks": { "delta": 0.49 }
                    }
                }
            }),
        ];

        for ticker in &tickers {
            feed.handle_message(&ticker.to_string()).unwrap();
        }

        let mut last_event = None;
        while let Ok(event) = rx.try_recv() {
            last_event = Some(event);
        }

        let event = last_event.expect("should have received an IvEvent");
        match event {
            IvEvent::IvUpdate(iv) => {
                assert_eq!(iv.asset, Asset::Btc);
                // ATM IV from nearest expiry (28JUN24): 0.55
                assert!((iv.atm_iv - 0.55).abs() < 1e-10);
                // Term slope: 0.58 - 0.55 = 0.03
                assert!((iv.term_slope - 0.03).abs() < 1e-10);
            }
        }
    }

    #[test]
    fn test_skew_from_mock_option_tickers() {
        let (feed, mut rx) = make_feed();

        // Typical skew scenario: OTM puts are more expensive (higher IV) than OTM calls
        let tickers = vec![
            // ATM call
            json!({
                "jsonrpc": "2.0",
                "method": "subscription",
                "params": {
                    "channel": "ticker.ETH-28JUN24-3000-C.raw",
                    "data": {
                        "instrument_name": "ETH-28JUN24-3000-C",
                        "timestamp": 1672515782136i64,
                        "mark_iv": 70.0,
                        "underlying_price": 3000.0,
                        "greeks": { "delta": 0.50 }
                    }
                }
            }),
            // OTM call (25d)
            json!({
                "jsonrpc": "2.0",
                "method": "subscription",
                "params": {
                    "channel": "ticker.ETH-28JUN24-3300-C.raw",
                    "data": {
                        "instrument_name": "ETH-28JUN24-3300-C",
                        "timestamp": 1672515782136i64,
                        "mark_iv": 65.0,
                        "underlying_price": 3000.0,
                        "greeks": { "delta": 0.25 }
                    }
                }
            }),
            // OTM put (25d) - typically higher IV due to skew
            json!({
                "jsonrpc": "2.0",
                "method": "subscription",
                "params": {
                    "channel": "ticker.ETH-28JUN24-2700-P.raw",
                    "data": {
                        "instrument_name": "ETH-28JUN24-2700-P",
                        "timestamp": 1672515782136i64,
                        "mark_iv": 78.0,
                        "underlying_price": 3000.0,
                        "greeks": { "delta": -0.25 }
                    }
                }
            }),
        ];

        for ticker in &tickers {
            feed.handle_message(&ticker.to_string()).unwrap();
        }

        let mut last_event = None;
        while let Ok(event) = rx.try_recv() {
            last_event = Some(event);
        }

        let event = last_event.expect("should have received an IvEvent");
        match event {
            IvEvent::IvUpdate(iv) => {
                assert_eq!(iv.asset, Asset::Eth);
                // ATM IV from 3000-C: 0.70
                assert!((iv.atm_iv - 0.70).abs() < 1e-10);
                // Skew: put_25d(0.78) - call_25d(0.65) = 0.13 (positive = downside skew)
                assert!((iv.skew_25d - 0.13).abs() < 1e-10);
            }
        }
    }

    #[test]
    fn test_iv_state_stored_on_option_ticker() {
        let (feed, _rx) = make_feed();

        let msg = json!({
            "jsonrpc": "2.0",
            "method": "subscription",
            "params": {
                "channel": "ticker.BTC-28JUN24-50000-C.raw",
                "data": {
                    "instrument_name": "BTC-28JUN24-50000-C",
                    "timestamp": 1672515782136i64,
                    "mark_iv": 55.0,
                    "underlying_price": 50000.0,
                    "greeks": { "delta": 0.5 }
                }
            }
        });

        feed.handle_message(&msg.to_string()).unwrap();

        let state = feed.iv_state.lock().unwrap();
        let key = (Asset::Btc, "BTC-28JUN24-50000-C".to_string());
        assert!(state.contains_key(&key));
        let entry = &state[&key];
        assert!((entry.mark_iv - 0.55).abs() < 1e-10);
        assert!((entry.delta - 0.5).abs() < 1e-10);
        assert_eq!(entry.expiry, "28JUN24");
        assert!((entry.strike - 50000.0).abs() < 1e-10);
        assert_eq!(entry.kind, OptionKind::Call);
    }

    #[test]
    fn test_perpetual_stores_spot_price() {
        let (feed, _rx) = make_feed();

        let msg = json!({
            "jsonrpc": "2.0",
            "method": "subscription",
            "params": {
                "channel": "ticker.BTC-PERPETUAL.raw",
                "data": {
                    "instrument_name": "BTC-PERPETUAL",
                    "timestamp": 1672515782136i64,
                    "mark_price": 50123.45,
                    "index_price": 50100.0
                }
            }
        });

        feed.handle_message(&msg.to_string()).unwrap();

        let spot = feed.get_spot_price(Asset::Btc);
        assert!((spot - 50123.45).abs() < 1e-10);
    }

    #[test]
    fn test_format_strike() {
        assert_eq!(format_strike(50000.0), "50000");
        assert_eq!(format_strike(3000.0), "3000");
        assert_eq!(format_strike(1234.5), "1234.5");
    }
}
