//! Chaos engineering middleware for HTTP

use crate::{
    config::{ConnectionErrorKind, CorruptionType},
    fault::FaultInjector,
    latency::LatencyInjector,
    latency_metrics::LatencyMetricsTracker,
    rate_limit::RateLimiter,
    resilience::{Bulkhead, CircuitBreaker},
    traffic_shaping::TrafficShaper,
    ChaosConfig,
};
use axum::{
    body::Body,
    extract::{Request, State},
    http::StatusCode,
    middleware::Next,
    response::{IntoResponse, Response},
};
use http_body_util::BodyExt;
use rand::Rng;
use std::{net::SocketAddr, sync::Arc};
use tokio::sync::RwLock;
use tracing::{debug, warn};

/// Chaos middleware state
///
/// This middleware reads configuration from a shared `Arc<RwLock<ChaosConfig>>`
/// to support hot-reload of chaos settings at runtime.
#[derive(Clone)]
pub struct ChaosMiddleware {
    /// Shared chaos configuration (read on each request for hot-reload support)
    config: Arc<RwLock<ChaosConfig>>,
    /// Latency metrics tracker for recording injected latencies
    latency_tracker: Arc<LatencyMetricsTracker>,
    /// Cached injectors (recreated when config changes)
    /// These are cached for performance but can be updated via update_from_config()
    latency_injector: Arc<RwLock<LatencyInjector>>,
    fault_injector: Arc<RwLock<FaultInjector>>,
    rate_limiter: Arc<RwLock<RateLimiter>>,
    traffic_shaper: Arc<RwLock<TrafficShaper>>,
    circuit_breaker: Arc<RwLock<CircuitBreaker>>,
    bulkhead: Arc<RwLock<Bulkhead>>,
}

impl ChaosMiddleware {
    /// Create new chaos middleware from shared config
    ///
    /// # Arguments
    /// * `config` - Shared chaos configuration (Arc<RwLock<ChaosConfig>>)
    /// * `latency_tracker` - Latency metrics tracker for recording injected latencies
    ///
    /// The middleware will read from the shared config on each request,
    /// allowing hot-reload of chaos settings without restarting the server.
    pub fn new(
        config: Arc<RwLock<ChaosConfig>>,
        latency_tracker: Arc<LatencyMetricsTracker>,
    ) -> Self {
        // Initialize injectors with defaults (will be updated via init_from_config)
        let latency_injector = Arc::new(RwLock::new(LatencyInjector::new(Default::default())));

        // FaultInjector doesn't support hot-reload, but we'll read from config directly
        // Keep a reference for compatibility but won't use it for fault injection
        // Note: We wrap it in RwLock for consistency, even though we read from config directly
        let fault_injector = Arc::new(RwLock::new(FaultInjector::new(Default::default())));

        let rate_limiter = Arc::new(RwLock::new(RateLimiter::new(Default::default())));

        let traffic_shaper = Arc::new(RwLock::new(TrafficShaper::new(Default::default())));

        let circuit_breaker = Arc::new(RwLock::new(CircuitBreaker::new(Default::default())));

        let bulkhead = Arc::new(RwLock::new(Bulkhead::new(Default::default())));

        Self {
            config,
            latency_tracker,
            latency_injector,
            fault_injector,
            rate_limiter,
            traffic_shaper,
            circuit_breaker,
            bulkhead,
        }
    }

    /// Initialize middleware from config (async version)
    ///
    /// This should be called after creation to sync injectors with the actual config.
    /// This is a convenience method that calls `update_from_config()`.
    pub async fn init_from_config(&self) {
        self.update_from_config().await;
    }

    /// Update injectors from current config
    ///
    /// This method should be called when the config is updated to refresh
    /// the cached injectors. For hot-reload support, this is called automatically
    /// when processing requests if the config has changed.
    pub async fn update_from_config(&self) {
        let config = self.config.read().await;

        // Update latency injector
        {
            let mut injector = self.latency_injector.write().await;
            *injector = LatencyInjector::new(config.latency.clone().unwrap_or_default());
        }

        // Note: FaultInjector doesn't have an update method, so we'd need to recreate it
        // For now, we'll read from config directly in the middleware

        // Update rate limiter
        {
            let mut limiter = self.rate_limiter.write().await;
            *limiter = RateLimiter::new(config.rate_limit.clone().unwrap_or_default());
        }

        // Update traffic shaper
        {
            let mut shaper = self.traffic_shaper.write().await;
            *shaper = TrafficShaper::new(config.traffic_shaping.clone().unwrap_or_default());
        }

        // Update circuit breaker
        {
            let mut breaker = self.circuit_breaker.write().await;
            *breaker = CircuitBreaker::new(config.circuit_breaker.clone().unwrap_or_default());
        }

        // Update bulkhead
        {
            let mut bh = self.bulkhead.write().await;
            *bh = Bulkhead::new(config.bulkhead.clone().unwrap_or_default());
        }
    }

    /// Get latency injector (read-only access)
    pub fn latency_injector(&self) -> Arc<RwLock<LatencyInjector>> {
        self.latency_injector.clone()
    }

    /// Get fault injector (read-only access)
    /// Note: FaultInjector doesn't support hot-reload, so we read from config directly
    pub fn fault_injector(&self) -> Arc<RwLock<FaultInjector>> {
        self.fault_injector.clone()
    }

    /// Get rate limiter (read-only access)
    pub fn rate_limiter(&self) -> Arc<RwLock<RateLimiter>> {
        self.rate_limiter.clone()
    }

    /// Get traffic shaper (read-only access)
    pub fn traffic_shaper(&self) -> Arc<RwLock<TrafficShaper>> {
        self.traffic_shaper.clone()
    }

    /// Get circuit breaker (read-only access)
    pub fn circuit_breaker(&self) -> Arc<RwLock<CircuitBreaker>> {
        self.circuit_breaker.clone()
    }

    /// Get bulkhead (read-only access)
    pub fn bulkhead(&self) -> Arc<RwLock<Bulkhead>> {
        self.bulkhead.clone()
    }

    /// Get shared config (for direct access if needed)
    pub fn config(&self) -> Arc<RwLock<ChaosConfig>> {
        self.config.clone()
    }

    /// Get latency tracker
    pub fn latency_tracker(&self) -> &Arc<LatencyMetricsTracker> {
        &self.latency_tracker
    }
}

/// Chaos middleware handler (takes state directly, for use with from_fn)
pub async fn chaos_middleware_with_state(
    chaos: Arc<ChaosMiddleware>,
    req: Request<Body>,
    next: Next,
) -> Response {
    // Call the main handler by creating a temporary State extractor
    // We do this by putting the state in request extensions temporarily
    let (mut parts, body) = req.into_parts();
    parts.extensions.insert(chaos.clone());
    let req = Request::from_parts(parts, body);

    // Now we can use the State extractor pattern
    // But actually, let's just call the core logic directly
    chaos_middleware_core(chaos, req, next).await
}

/// Chaos middleware handler (uses State extractor, for use with from_fn_with_state)
pub async fn chaos_middleware(
    State(chaos): State<Arc<ChaosMiddleware>>,
    req: Request<Body>,
    next: Next,
) -> Response {
    chaos_middleware_core(chaos, req, next).await
}

/// Core chaos middleware logic
async fn chaos_middleware_core(
    chaos: Arc<ChaosMiddleware>,
    req: Request<Body>,
    next: Next,
) -> Response {
    // Read config at start of request (supports hot-reload)
    let config = chaos.config.read().await;

    // Early return if chaos is disabled
    if !config.enabled {
        drop(config);
        return next.run(req).await;
    }

    let path = req.uri().path().to_string();

    // Extract client IP from request extensions (set by ConnectInfo if available) or headers
    let ip = req
        .extensions()
        .get::<SocketAddr>()
        .map(|addr| addr.ip().to_string())
        .or_else(|| {
            req.headers()
                .get("x-forwarded-for")
                .or_else(|| req.headers().get("x-real-ip"))
                .and_then(|h| h.to_str().ok())
                .map(|s| s.split(',').next().unwrap_or(s).trim().to_string())
        })
        .unwrap_or_else(|| "127.0.0.1".to_string());

    debug!("Chaos middleware processing: {} {}", req.method(), path);

    // Release config lock early (we'll read specific configs as needed)
    drop(config);

    // Check circuit breaker
    {
        let circuit_breaker = chaos.circuit_breaker.read().await;
        if !circuit_breaker.allow_request().await {
            warn!("Circuit breaker open, rejecting request: {}", path);
            return (
                StatusCode::SERVICE_UNAVAILABLE,
                "Service temporarily unavailable (circuit breaker open)",
            )
                .into_response();
        }
    }

    // Try to acquire bulkhead slot
    let _bulkhead_guard = {
        let bulkhead = chaos.bulkhead.read().await;
        match bulkhead.try_acquire().await {
            Ok(guard) => guard,
            Err(e) => {
                warn!("Bulkhead rejected request: {} - {:?}", path, e);
                return (StatusCode::SERVICE_UNAVAILABLE, format!("Service overloaded: {}", e))
                    .into_response();
            }
        }
    };

    // Check rate limits
    let rate_limiter = chaos.rate_limiter.read().await;
    if let Err(_e) = rate_limiter.check(Some(&ip), Some(&path)) {
        drop(rate_limiter);
        warn!("Rate limit exceeded: {} - {}", ip, path);
        crate::metrics::CHAOS_METRICS.record_rate_limit_violation(&path);
        crate::metrics::CHAOS_METRICS.record_fault("rate_limit", &path);
        return (StatusCode::TOO_MANY_REQUESTS, "Rate limit exceeded").into_response();
    }
    drop(rate_limiter);

    // Check connection limits
    let traffic_shaper = chaos.traffic_shaper.read().await;
    if !traffic_shaper.check_connection_limit() {
        drop(traffic_shaper);
        warn!("Connection limit exceeded");
        crate::metrics::CHAOS_METRICS.record_fault("connection_limit", &path);
        return (StatusCode::SERVICE_UNAVAILABLE, "Connection limit exceeded").into_response();
    }

    // Always release connection on scope exit
    let _connection_guard = crate::traffic_shaping::ConnectionGuard::new(traffic_shaper.clone());

    // Check for packet loss (simulate dropped connection)
    if traffic_shaper.should_drop_packet() {
        drop(traffic_shaper);
        warn!("Simulating packet loss for: {}", path);
        crate::metrics::CHAOS_METRICS.record_fault("packet_loss", &path);
        return (StatusCode::REQUEST_TIMEOUT, "Connection dropped").into_response();
    }
    drop(traffic_shaper);

    // Inject latency and record it for metrics. `inject_with_breakdown` reports
    // the base delay and the jitter delta separately so we can count jitter
    // applications as its own fault_type (Issue #79 — user could not see
    // jitter details in the TUI Chaos screen even though latency was firing).
    let (delay_ms, _base_ms, jitter_abs) = {
        let latency_injector = chaos.latency_injector.read().await;
        latency_injector.inject_with_breakdown().await
    };
    if delay_ms > 0 {
        chaos.latency_tracker.record_latency(delay_ms);
        crate::metrics::CHAOS_METRICS.record_latency(&path, delay_ms as f64);
        if jitter_abs > 0 {
            crate::metrics::CHAOS_METRICS.record_fault("jitter", &path);
            crate::metrics::CHAOS_METRICS.record_jitter(&path, jitter_abs as f64);
        }
    }

    // Detect chunked transfer encoding *before* we collect the body, since
    // body collection consumes the stream.
    let is_chunked =
        req.headers().get_all(http::header::TRANSFER_ENCODING).iter().any(|v| {
            v.to_str().map(|s| s.to_ascii_lowercase().contains("chunked")).unwrap_or(false)
        });

    // Extract body size for bandwidth throttling and request-matcher gating.
    let (parts, body) = req.into_parts();
    let body_bytes = match body.collect().await {
        Ok(collected) => collected.to_bytes(),
        Err(e) => {
            warn!("Failed to read request body: {}", e);
            return (StatusCode::BAD_REQUEST, "Failed to read request body").into_response();
        }
    };

    let request_size = body_bytes.len();

    // Check for fault injection (read from config for hot-reload)
    let config = chaos.config.read().await;
    let fault_config = config.fault_injection.as_ref();
    let should_inject_fault = fault_config.map(|f| f.enabled).unwrap_or(false);
    let matcher_passed = fault_config
        .and_then(|f| f.request_matcher.as_ref())
        .map(|matcher| {
            let header_pairs: Vec<(String, String)> = parts
                .headers
                .iter()
                .filter_map(|(k, v)| {
                    v.to_str().ok().map(|s| (k.as_str().to_string(), s.to_string()))
                })
                .collect();
            let header_iter =
                header_pairs.iter().map(|(k, v)| (k.as_str(), v.as_str())).collect::<Vec<_>>();
            matcher.matches(Some(ip.as_str()), header_iter, Some(request_size), is_chunked)
        })
        .unwrap_or(true);

    let http_error_status = if should_inject_fault && matcher_passed {
        // Check probability and get error status
        fault_config.and_then(|f| {
            let mut rng = rand::rng();
            if rng.random::<f64>() <= f.http_error_probability && !f.http_errors.is_empty() {
                Some(f.http_errors[rng.random_range(0..f.http_errors.len())])
            } else {
                None
            }
        })
    } else {
        None
    };

    // Connection-error fault at the HTTP layer. `TcpReset` / `TcpClose` kinds
    // are handled by `ChaosTcpListener` at accept time; `Http503` (the default)
    // surfaces here as an application-level 503 so the `connection_errors`
    // knob also produces a recordable fault counter when the listener wrapper
    // isn't installed. Issue #79 — Srikanth's round-3 reply.
    let connection_error_kind = if should_inject_fault && matcher_passed {
        fault_config.and_then(|f| {
            if !f.connection_errors {
                return None;
            }
            let mut rng = rand::rng();
            if rng.random::<f64>() <= f.connection_error_probability {
                Some(f.connection_error_kind)
            } else {
                None
            }
        })
    } else {
        None
    };

    // Decide whether to inject a true timeout: sleep for timeout_ms then 504.
    // Decoupled from partial_responses, which has its own probability further down.
    let timeout_fault = if should_inject_fault && matcher_passed {
        fault_config.and_then(|f| {
            if !f.timeout_errors {
                return None;
            }
            let mut rng = rand::rng();
            if rng.random::<f64>() <= f.timeout_probability {
                Some(f.timeout_ms)
            } else {
                None
            }
        })
    } else {
        None
    };
    drop(config);

    if let Some(status_code) = http_error_status {
        warn!("Injecting HTTP error: {}", status_code);
        crate::metrics::CHAOS_METRICS.record_fault("http_error", &path);
        return (
            StatusCode::from_u16(status_code).unwrap_or(StatusCode::INTERNAL_SERVER_ERROR),
            format!("Injected error: {}", status_code),
        )
            .into_response();
    }

    // HTTP-layer connection-error injection (when `connection_error_kind` is
    // `Http503`). TCP-level kinds are short-circuited by the listener wrapper
    // before the request reaches us, so we'd never see them here.
    if let Some(ConnectionErrorKind::Http503) = connection_error_kind {
        warn!("Injecting connection_error (Http503) for: {}", path);
        crate::metrics::CHAOS_METRICS.record_fault("connection_error", &path);
        return (
            StatusCode::SERVICE_UNAVAILABLE,
            "Injected connection error (Service Unavailable)",
        )
            .into_response();
    }

    if let Some(timeout_ms) = timeout_fault {
        warn!("Injecting timeout: sleeping {}ms then returning 504", timeout_ms);
        crate::metrics::CHAOS_METRICS.record_fault("timeout", &path);
        tokio::time::sleep(std::time::Duration::from_millis(timeout_ms)).await;
        return (StatusCode::GATEWAY_TIMEOUT, "Injected timeout (Gateway Timeout)").into_response();
    }

    // Throttle request bandwidth and record any actual delay applied.
    {
        let traffic_shaper = chaos.traffic_shaper.read().await;
        let delay_ms = traffic_shaper.throttle_bandwidth(request_size).await;
        if delay_ms > 0 {
            crate::metrics::CHAOS_METRICS.record_fault("bandwidth_throttle", &path);
            crate::metrics::CHAOS_METRICS.record_bandwidth_throttle(
                &path,
                "request",
                delay_ms as f64,
            );
        }
    }

    // Reconstruct request
    let req = Request::from_parts(parts, Body::from(body_bytes));

    // Pass to next handler
    let response = next.run(req).await;

    // Record circuit breaker result based on response status
    let status = response.status();
    {
        let circuit_breaker = chaos.circuit_breaker.read().await;
        if status.is_server_error() || status == StatusCode::SERVICE_UNAVAILABLE {
            circuit_breaker.record_failure().await;
        } else if status.is_success() {
            circuit_breaker.record_success().await;
        }
    }

    // Extract response body size for bandwidth throttling
    let (parts, body) = response.into_parts();
    let response_body_bytes = match body.collect().await {
        Ok(collected) => collected.to_bytes(),
        Err(e) => {
            warn!("Failed to read response body: {}", e);
            return (StatusCode::INTERNAL_SERVER_ERROR, "Failed to read response body")
                .into_response();
        }
    };

    let response_size = response_body_bytes.len();

    // Check if should truncate response (partial response simulation).
    // Gated on the `partial_responses` flag (the dedicated field) and its own
    // probability. Previously this path was incorrectly tied to `timeout_errors`.
    let config = chaos.config.read().await;
    let should_truncate = matcher_passed
        && config
            .fault_injection
            .as_ref()
            .map(|f| {
                if !f.enabled || !f.partial_responses {
                    return false;
                }
                let mut rng = rand::rng();
                rng.random::<f64>() <= f.partial_response_probability
            })
            .unwrap_or(false);
    let should_corrupt =
        matcher_passed && config.fault_injection.as_ref().map(|f| f.enabled).unwrap_or(false);
    let corruption_type = config
        .fault_injection
        .as_ref()
        .map(|f| f.corruption_type)
        .unwrap_or(CorruptionType::None);
    drop(config);

    // Did the upstream response declare chunked encoding? If so we drop the
    // chunked terminator by keeping Transfer-Encoding intact but writing fewer
    // bytes (axum will re-encode without the trailing `0\r\n\r\n`-equivalent
    // terminator since the body simply ends short). Otherwise we keep the
    // original Content-Length header so the client perceives a real truncation.
    let response_was_chunked =
        parts.headers.get_all(http::header::TRANSFER_ENCODING).iter().any(|v| {
            v.to_str().map(|s| s.to_ascii_lowercase().contains("chunked")).unwrap_or(false)
        });

    let mut parts = parts;
    let mut final_body_bytes = if should_truncate {
        warn!(
            "Injecting partial response (chunked={}, original_size={})",
            response_was_chunked, response_size
        );
        crate::metrics::CHAOS_METRICS.record_fault("partial_response", &path);
        let mut rng = rand::rng();
        let truncate_at = if response_size > 0 {
            rng.random_range(0..response_size)
        } else {
            0
        };
        if !response_was_chunked {
            // Preserve the original Content-Length so the client reads fewer
            // bytes than promised and sees an unexpected EOF.
            if let Ok(orig_len) = http::HeaderValue::from_str(&response_size.to_string()) {
                parts.headers.insert(http::header::CONTENT_LENGTH, orig_len);
            }
        }
        response_body_bytes.slice(0..truncate_at).to_vec()
    } else {
        response_body_bytes.to_vec()
    };

    // Apply payload corruption if enabled
    if should_corrupt && corruption_type != CorruptionType::None {
        warn!("Injecting payload corruption: {:?}", corruption_type);
        crate::metrics::CHAOS_METRICS.record_fault("payload_corruption", &path);
        final_body_bytes = corrupt_payload(&final_body_bytes, corruption_type);
    }

    let final_body = Body::from(final_body_bytes);

    // Throttle response bandwidth and record the delay.
    {
        let traffic_shaper = chaos.traffic_shaper.read().await;
        let delay_ms = traffic_shaper.throttle_bandwidth(response_size).await;
        if delay_ms > 0 {
            crate::metrics::CHAOS_METRICS.record_fault("bandwidth_throttle", &path);
            crate::metrics::CHAOS_METRICS.record_bandwidth_throttle(
                &path,
                "response",
                delay_ms as f64,
            );
        }
    }

    // Surface what the server injected back to the client. Bench clients and
    // browser devtools can read these headers without scraping `/metrics`.
    // Issue #79 — Srikanth's round-3 reply asked to see injected latency on
    // the client side. `parts` is already `mut` from the partial-response
    // truncation block above.
    if delay_ms > 0 {
        if let Ok(v) = http::HeaderValue::from_str(&delay_ms.to_string()) {
            parts.headers.insert("x-mockforge-injected-latency-ms", v);
        }
    }
    if jitter_abs > 0 {
        if let Ok(v) = http::HeaderValue::from_str(&jitter_abs.to_string()) {
            parts.headers.insert("x-mockforge-injected-jitter-ms", v);
        }
    }
    if should_truncate {
        parts
            .headers
            .insert("x-mockforge-fault", http::HeaderValue::from_static("partial_response"));
    }

    Response::from_parts(parts, final_body)
}

/// Corrupt a payload based on the corruption type
fn corrupt_payload(data: &[u8], corruption_type: CorruptionType) -> Vec<u8> {
    if data.is_empty() {
        return data.to_vec();
    }

    let mut rng = rand::rng();
    let mut corrupted = data.to_vec();

    match corruption_type {
        CorruptionType::None => corrupted,
        CorruptionType::RandomBytes => {
            // Replace 10% of bytes with random values
            let num_bytes_to_corrupt = (data.len() as f64 * 0.1).max(1.0) as usize;
            for _ in 0..num_bytes_to_corrupt {
                let index = rng.random_range(0..data.len());
                corrupted[index] = rng.random::<u8>();
            }
            corrupted
        }
        CorruptionType::Truncate => {
            // Truncate at random position (between 50% and 90% of original length)
            let min_truncate = data.len() / 2;
            let max_truncate = (data.len() as f64 * 0.9) as usize;
            let truncate_at = if max_truncate > min_truncate {
                rng.random_range(min_truncate..=max_truncate)
            } else {
                min_truncate
            };
            corrupted.truncate(truncate_at);
            corrupted
        }
        CorruptionType::BitFlip => {
            // Flip random bits in 10% of bytes
            let num_bytes_to_flip = (data.len() as f64 * 0.1).max(1.0) as usize;
            for _ in 0..num_bytes_to_flip {
                let index = rng.random_range(0..data.len());
                let bit_to_flip = rng.random_range(0..8);
                corrupted[index] ^= 1 << bit_to_flip;
            }
            corrupted
        }
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::config::{LatencyConfig, RateLimitConfig};
    use crate::latency_metrics::LatencyMetricsTracker;

    #[tokio::test]
    async fn test_middleware_creation() {
        let config = ChaosConfig {
            enabled: true,
            latency: Some(LatencyConfig {
                enabled: true,
                fixed_delay_ms: Some(10),
                ..Default::default()
            }),
            ..Default::default()
        };

        let latency_tracker = Arc::new(LatencyMetricsTracker::new());
        let config_arc = Arc::new(RwLock::new(config));
        let middleware = ChaosMiddleware::new(config_arc, latency_tracker);
        // Initialize middleware from config to sync injectors with actual config
        middleware.init_from_config().await;
        assert!(middleware.latency_injector.read().await.is_enabled());
    }

    #[tokio::test]
    async fn test_rate_limiting() {
        let config = Arc::new(RwLock::new(ChaosConfig {
            enabled: true,
            rate_limit: Some(RateLimitConfig {
                enabled: true,
                requests_per_second: 1,
                burst_size: 2, // burst_size is the total capacity, not additional requests
                ..Default::default()
            }),
            ..Default::default()
        }));

        let latency_tracker = Arc::new(LatencyMetricsTracker::new());
        let middleware = Arc::new(ChaosMiddleware::new(config.clone(), latency_tracker));
        middleware.init_from_config().await;

        // First two requests should succeed (rate + burst)
        {
            let rate_limiter = middleware.rate_limiter.read().await;
            assert!(rate_limiter.check(Some("127.0.0.1"), Some("/test")).is_ok());
            assert!(rate_limiter.check(Some("127.0.0.1"), Some("/test")).is_ok());
        }

        // Third should fail
        {
            let rate_limiter = middleware.rate_limiter.read().await;
            assert!(rate_limiter.check(Some("127.0.0.1"), Some("/test")).is_err());
        }
    }

    #[tokio::test]
    async fn test_latency_recording() {
        let config = Arc::new(RwLock::new(ChaosConfig {
            enabled: true,
            latency: Some(LatencyConfig {
                enabled: true,
                fixed_delay_ms: Some(50),
                probability: 1.0,
                ..Default::default()
            }),
            ..Default::default()
        }));

        let latency_tracker = Arc::new(LatencyMetricsTracker::new());
        let middleware = Arc::new(ChaosMiddleware::new(config.clone(), latency_tracker.clone()));
        middleware.init_from_config().await;

        // Verify tracker is accessible via getter
        let tracker_from_middleware = middleware.latency_tracker();
        assert_eq!(Arc::as_ptr(tracker_from_middleware), Arc::as_ptr(&latency_tracker));

        // Manually inject latency and record it (simulating what middleware does)
        let delay_ms = {
            let injector = middleware.latency_injector.read().await;
            injector.inject().await
        };
        if delay_ms > 0 {
            latency_tracker.record_latency(delay_ms);
        }

        // Verify latency was recorded
        let samples = latency_tracker.get_samples();
        assert!(!samples.is_empty(), "Should have recorded at least one latency sample");
        assert_eq!(samples[0].latency_ms, 50, "Recorded latency should match injected delay");
    }

    /// Issue #79 item 6: pre-fix the prometheus counter sat at zero forever
    /// because nothing called `record_fault`. This is a coarse smoke check
    /// that the counter mechanism itself works — the actual call sites are
    /// covered by the wiring in `chaos_middleware_core` and `ChaosTcpListener`.
    #[test]
    fn fault_counter_bumps_when_record_fault_is_called() {
        use crate::metrics::CHAOS_METRICS;
        let endpoint = "/test_chaos_fault_wiring_smoke";
        let before = CHAOS_METRICS
            .faults_injected_total
            .with_label_values(&["http_error", endpoint])
            .get();
        CHAOS_METRICS.record_fault("http_error", endpoint);
        let after = CHAOS_METRICS
            .faults_injected_total
            .with_label_values(&["http_error", endpoint])
            .get();
        assert_eq!((after - before) as u64, 1);
    }
}
