use super::traits::{
    budget_state_label, cost_override_scope_label, redact_optional_observer_payload,
    usage_period_label, BudgetOverrideEvent, BudgetThresholdEvent, Observer, ObserverEvent,
    ObserverMetric,
};
use opentelemetry::metrics::{Counter, Gauge, Histogram};
use opentelemetry::trace::{Span, SpanKind, Status, Tracer};
use opentelemetry::{global, KeyValue};
use opentelemetry_otlp::WithExportConfig;
use opentelemetry_sdk::metrics::SdkMeterProvider;
use opentelemetry_sdk::trace::SdkTracerProvider;
use std::any::Any;
use std::time::SystemTime;

/// OpenTelemetry-backed observer — exports traces and metrics via OTLP.
pub struct OtelObserver {
    tracer_provider: SdkTracerProvider,
    meter_provider: SdkMeterProvider,

    // Metrics instruments
    agent_starts: Counter<u64>,
    agent_duration: Histogram<f64>,
    llm_calls: Counter<u64>,
    llm_duration: Histogram<f64>,
    tool_calls: Counter<u64>,
    tool_duration: Histogram<f64>,
    channel_messages: Counter<u64>,
    heartbeat_ticks: Counter<u64>,
    errors: Counter<u64>,
    budget_warnings: Counter<u64>,
    budget_exceeded: Counter<u64>,
    budget_overrides: Counter<u64>,
    request_latency: Histogram<f64>,
    tokens_used: Counter<u64>,
    cost_usd_last: Gauge<f64>,
    active_sessions: Gauge<u64>,
    queue_depth: Gauge<u64>,
    image_ingress: Counter<u64>,
    audio_ingress: Counter<u64>,
}

fn budget_threshold_trace_fields(event: &BudgetThresholdEvent) -> Vec<(String, String)> {
    let mut fields = vec![
        (
            "budget_state".to_string(),
            budget_state_label(event.budget_state).to_string(),
        ),
        (
            "period".to_string(),
            usage_period_label(event.period).to_string(),
        ),
        (
            "current_usd".to_string(),
            format!("{:.6}", event.current_usd),
        ),
        (
            "projected_usd".to_string(),
            format!("{:.6}", event.projected_usd),
        ),
        ("limit_usd".to_string(), format!("{:.6}", event.limit_usd)),
        (
            "percent_used".to_string(),
            format!("{:.2}", event.percent_used),
        ),
        ("session_id".to_string(), event.session_id.clone()),
    ];
    if let Some(surface) = event.surface.as_ref() {
        fields.push(("surface".to_string(), surface.clone()));
    }
    fields
}

fn budget_override_trace_fields(event: &BudgetOverrideEvent) -> Vec<(String, String)> {
    let mut fields = vec![
        ("action".to_string(), event.action.as_str().to_string()),
        (
            "scope".to_string(),
            cost_override_scope_label(event.scope).to_string(),
        ),
        ("actor".to_string(), event.redacted_actor()),
        (
            "previous_state".to_string(),
            budget_state_label(event.previous_state).to_string(),
        ),
    ];

    if let Some(reason) = event.redacted_reason() {
        fields.push(("reason".to_string(), reason));
    }
    if let Some(period) = event.period {
        fields.push(("period".to_string(), usage_period_label(period).to_string()));
    }
    if let Some(session_id) = event.session_id.as_ref() {
        fields.push(("session_id".to_string(), session_id.clone()));
    }
    if let Some(override_id) = event.override_id.as_ref() {
        fields.push(("override_id".to_string(), override_id.clone()));
    }
    if let Some(surface) = redact_optional_observer_payload(event.surface.as_deref()) {
        fields.push(("surface".to_string(), surface));
    }

    fields
}

fn key_values_from_fields(fields: Vec<(String, String)>) -> Vec<KeyValue> {
    fields
        .into_iter()
        .map(|(key, value)| KeyValue::new(key, value))
        .collect()
}

impl OtelObserver {
    /// Create a new OTel observer exporting to the given OTLP endpoint.
    ///
    /// Uses HTTP/protobuf transport (port 4318 by default).
    /// Falls back to `http://localhost:4318` if no endpoint is provided.
    pub fn new(endpoint: Option<&str>, service_name: Option<&str>) -> Result<Self, String> {
        let endpoint = endpoint.unwrap_or("http://localhost:4318");
        let service_name = service_name.unwrap_or("corvus");

        // ── Trace exporter ──────────────────────────────────────
        let span_exporter = opentelemetry_otlp::SpanExporter::builder()
            .with_http()
            .with_endpoint(endpoint)
            .build()
            .map_err(|e| format!("Failed to create OTLP span exporter: {e}"))?;

        let tracer_provider = SdkTracerProvider::builder()
            .with_batch_exporter(span_exporter)
            .with_resource(
                opentelemetry_sdk::Resource::builder()
                    .with_service_name(service_name.to_string())
                    .build(),
            )
            .build();

        global::set_tracer_provider(tracer_provider.clone());

        // ── Metric exporter ─────────────────────────────────────
        let metric_exporter = opentelemetry_otlp::MetricExporter::builder()
            .with_http()
            .with_endpoint(endpoint)
            .build()
            .map_err(|e| format!("Failed to create OTLP metric exporter: {e}"))?;

        let metric_reader =
            opentelemetry_sdk::metrics::PeriodicReader::builder(metric_exporter).build();

        let meter_provider = opentelemetry_sdk::metrics::SdkMeterProvider::builder()
            .with_reader(metric_reader)
            .with_resource(
                opentelemetry_sdk::Resource::builder()
                    .with_service_name(service_name.to_string())
                    .build(),
            )
            .build();

        let meter_provider_clone = meter_provider.clone();
        global::set_meter_provider(meter_provider);

        // ── Create metric instruments ────────────────────────────
        let meter = global::meter("corvus");

        let agent_starts = meter
            .u64_counter("corvus.agent.starts")
            .with_description("Total agent invocations")
            .build();

        let agent_duration = meter
            .f64_histogram("corvus.agent.duration")
            .with_description("Agent invocation duration in seconds")
            .with_unit("s")
            .build();

        let llm_calls = meter
            .u64_counter("corvus.llm.calls")
            .with_description("Total LLM provider calls")
            .build();

        let llm_duration = meter
            .f64_histogram("corvus.llm.duration")
            .with_description("LLM provider call duration in seconds")
            .with_unit("s")
            .build();

        let tool_calls = meter
            .u64_counter("corvus.tool.calls")
            .with_description("Total tool calls")
            .build();

        let tool_duration = meter
            .f64_histogram("corvus.tool.duration")
            .with_description("Tool execution duration in seconds")
            .with_unit("s")
            .build();

        let channel_messages = meter
            .u64_counter("corvus.channel.messages")
            .with_description("Total channel messages")
            .build();

        let heartbeat_ticks = meter
            .u64_counter("corvus.heartbeat.ticks")
            .with_description("Total heartbeat ticks")
            .build();

        let errors = meter
            .u64_counter("corvus.errors")
            .with_description("Total errors by component")
            .build();

        let request_latency = meter
            .f64_histogram("corvus.request.latency")
            .with_description("Request latency in seconds")
            .with_unit("s")
            .build();

        let budget_warnings = meter
            .u64_counter("corvus.budget.warnings")
            .with_description("Budget warning lifecycle events")
            .build();

        let budget_exceeded = meter
            .u64_counter("corvus.budget.exceeded")
            .with_description("Budget hard block lifecycle events")
            .build();

        let budget_overrides = meter
            .u64_counter("corvus.budget.overrides")
            .with_description("Budget override lifecycle events")
            .build();

        let tokens_used = meter
            .u64_counter("corvus.tokens.used")
            .with_description("Total tokens consumed (monotonic)")
            .build();

        let cost_usd_last = meter
            .f64_gauge("corvus.cost.usd")
            .with_description("Latest observed request cost in USD")
            .with_unit("usd")
            .build();

        let active_sessions = meter
            .u64_gauge("corvus.sessions.active")
            .with_description("Current number of active sessions")
            .build();

        let queue_depth = meter
            .u64_gauge("corvus.queue.depth")
            .with_description("Current message queue depth")
            .build();

        let image_ingress = meter
            .u64_counter("corvus.image.ingress")
            .with_description("Image ingress lifecycle events")
            .build();

        let audio_ingress = meter
            .u64_counter("corvus.audio.ingress")
            .with_description("Audio ingress lifecycle events")
            .build();

        Ok(Self {
            tracer_provider,
            meter_provider: meter_provider_clone,
            agent_starts,
            agent_duration,
            llm_calls,
            llm_duration,
            tool_calls,
            tool_duration,
            channel_messages,
            heartbeat_ticks,
            errors,
            budget_warnings,
            budget_exceeded,
            budget_overrides,
            request_latency,
            tokens_used,
            cost_usd_last,
            active_sessions,
            queue_depth,
            image_ingress,
            audio_ingress,
        })
    }
}

impl Observer for OtelObserver {
    fn record_event(&self, event: &ObserverEvent) {
        let tracer = global::tracer("corvus");

        match event {
            ObserverEvent::AgentStart { provider, model } => {
                self.agent_starts.add(
                    1,
                    &[
                        KeyValue::new("provider", provider.clone()),
                        KeyValue::new("model", model.clone()),
                    ],
                );
            }
            ObserverEvent::LlmRequest { .. }
            | ObserverEvent::ToolCallStart { .. }
            | ObserverEvent::TurnComplete
            | ObserverEvent::MissionStarted { .. }
            | ObserverEvent::MissionCheckpointProgress { .. }
            | ObserverEvent::MissionGuardrailViolation { .. }
            | ObserverEvent::MissionCompleted { .. }
            | ObserverEvent::MissionTerminated { .. } => {}
            ObserverEvent::BudgetWarning(event) => {
                let attrs = [
                    KeyValue::new("period", usage_period_label(event.period)),
                    KeyValue::new(
                        "surface",
                        event
                            .surface
                            .clone()
                            .unwrap_or_else(|| "unknown".to_string()),
                    ),
                ];
                self.budget_warnings.add(1, &attrs);

                let mut span = tracer.build(
                    opentelemetry::trace::SpanBuilder::from_name("budget.warning")
                        .with_kind(SpanKind::Internal)
                        .with_attributes(key_values_from_fields(budget_threshold_trace_fields(
                            event,
                        ))),
                );
                span.set_status(Status::Ok);
                span.end();
            }
            ObserverEvent::BudgetExceeded(event) => {
                let attrs = [
                    KeyValue::new("period", usage_period_label(event.period)),
                    KeyValue::new(
                        "surface",
                        event
                            .surface
                            .clone()
                            .unwrap_or_else(|| "unknown".to_string()),
                    ),
                ];
                self.budget_exceeded.add(1, &attrs);

                let mut span = tracer.build(
                    opentelemetry::trace::SpanBuilder::from_name("budget.exceeded")
                        .with_kind(SpanKind::Internal)
                        .with_attributes(key_values_from_fields(budget_threshold_trace_fields(
                            event,
                        ))),
                );
                span.set_status(Status::error("budget exceeded"));
                span.end();
            }
            ObserverEvent::BudgetOverride(event) => {
                let attrs = [
                    KeyValue::new("action", event.action.as_str()),
                    KeyValue::new("scope", cost_override_scope_label(event.scope)),
                    KeyValue::new(
                        "surface",
                        event
                            .surface
                            .clone()
                            .unwrap_or_else(|| "unknown".to_string()),
                    ),
                ];
                self.budget_overrides.add(1, &attrs);

                let mut span = tracer.build(
                    opentelemetry::trace::SpanBuilder::from_name("budget.override")
                        .with_kind(SpanKind::Internal)
                        .with_attributes(key_values_from_fields(budget_override_trace_fields(
                            event,
                        ))),
                );
                span.set_status(Status::Ok);
                span.end();
            }
            ObserverEvent::AudioIngress(evt) => {
                let reason_str = evt
                    .reason
                    .as_ref()
                    .map(|r| r.to_string())
                    .unwrap_or_default();
                self.audio_ingress.add(
                    1,
                    &[
                        KeyValue::new("channel", evt.channel.clone()),
                        KeyValue::new("audio.outcome", format!("{:?}", evt.outcome)),
                        KeyValue::new("audio.reason", reason_str),
                    ],
                );
            }
            ObserverEvent::ImageIngress(evt) => {
                let reason_str = evt
                    .reason
                    .as_ref()
                    .map(|r| r.to_string())
                    .unwrap_or_default();
                self.image_ingress.add(
                    1,
                    &[
                        KeyValue::new("channel", evt.channel.clone()),
                        KeyValue::new("outcome", format!("{:?}", evt.outcome)),
                        KeyValue::new("reason", reason_str),
                    ],
                );
            }
            ObserverEvent::LlmResponse {
                provider,
                model,
                duration,
                success,
                error_message: _,
            } => {
                let secs = duration.as_secs_f64();
                let attrs = [
                    KeyValue::new("provider", provider.clone()),
                    KeyValue::new("model", model.clone()),
                    KeyValue::new("success", success.to_string()),
                ];
                self.llm_calls.add(1, &attrs);
                self.llm_duration.record(secs, &attrs);

                // Create a completed span for visibility in trace backends.
                let start_time = SystemTime::now()
                    .checked_sub(*duration)
                    .unwrap_or(SystemTime::now());
                let mut span = tracer.build(
                    opentelemetry::trace::SpanBuilder::from_name("llm.call")
                        .with_kind(SpanKind::Internal)
                        .with_start_time(start_time)
                        .with_attributes(vec![
                            KeyValue::new("provider", provider.clone()),
                            KeyValue::new("model", model.clone()),
                            KeyValue::new("success", *success),
                            KeyValue::new("duration_s", secs),
                        ]),
                );
                if *success {
                    span.set_status(Status::Ok);
                } else {
                    span.set_status(Status::error(""));
                }
                span.end();
            }
            ObserverEvent::AgentEnd {
                provider,
                model,
                duration,
                tokens_used,
                cost_usd,
            } => {
                let secs = duration.as_secs_f64();
                let start_time = SystemTime::now()
                    .checked_sub(*duration)
                    .unwrap_or(SystemTime::now());

                // Create a completed span with correct timing
                let mut span = tracer.build(
                    opentelemetry::trace::SpanBuilder::from_name("agent.invocation")
                        .with_kind(SpanKind::Internal)
                        .with_start_time(start_time)
                        .with_attributes(vec![
                            KeyValue::new("provider", provider.clone()),
                            KeyValue::new("model", model.clone()),
                            KeyValue::new("duration_s", secs),
                        ]),
                );
                if let Some(t) = tokens_used {
                    span.set_attribute(KeyValue::new("tokens_used", *t as i64));
                }
                if let Some(c) = cost_usd {
                    span.set_attribute(KeyValue::new("cost_usd", *c));
                    self.cost_usd_last.record(
                        *c,
                        &[
                            KeyValue::new("provider", provider.clone()),
                            KeyValue::new("model", model.clone()),
                        ],
                    );
                }
                span.end();

                self.agent_duration.record(
                    secs,
                    &[
                        KeyValue::new("provider", provider.clone()),
                        KeyValue::new("model", model.clone()),
                    ],
                );
                // Note: tokens are recorded via record_metric(TokensUsed) to avoid
                // double-counting. AgentEnd only records duration.
            }
            ObserverEvent::CodeSessionCompleted {
                status,
                changed_files,
                commands,
                validations,
                blockers,
                delegated,
                ..
            } => {
                let span_status = match status.as_str() {
                    "success" | "completed" | "completed_with_warnings" => Status::Ok,
                    _ => Status::error(status.clone()),
                };
                let mut span = tracer.build(
                    opentelemetry::trace::SpanBuilder::from_name("code_session.completed")
                        .with_kind(SpanKind::Internal)
                        .with_attributes(vec![
                            KeyValue::new("status", status.clone()),
                            KeyValue::new("delegated", delegated.to_string()),
                            KeyValue::new("files", changed_files.len() as i64),
                            KeyValue::new("commands", commands.len() as i64),
                            KeyValue::new("validations", validations.len() as i64),
                            KeyValue::new("blockers", blockers.len() as i64),
                        ]),
                );
                span.set_status(span_status);
                span.end();
            }
            ObserverEvent::ToolCall {
                tool,
                duration,
                success,
            } => {
                let secs = duration.as_secs_f64();
                let start_time = SystemTime::now()
                    .checked_sub(*duration)
                    .unwrap_or(SystemTime::now());

                let status = if *success {
                    Status::Ok
                } else {
                    Status::error("")
                };

                let mut span = tracer.build(
                    opentelemetry::trace::SpanBuilder::from_name("tool.call")
                        .with_kind(SpanKind::Internal)
                        .with_start_time(start_time)
                        .with_attributes(vec![
                            KeyValue::new("tool.name", tool.clone()),
                            KeyValue::new("tool.success", *success),
                            KeyValue::new("duration_s", secs),
                        ]),
                );
                span.set_status(status);
                span.end();

                let attrs = [
                    KeyValue::new("tool", tool.clone()),
                    KeyValue::new("success", success.to_string()),
                ];
                self.tool_calls.add(1, &attrs);
                self.tool_duration
                    .record(secs, &[KeyValue::new("tool", tool.clone())]);
            }
            ObserverEvent::ChannelMessage { channel, direction } => {
                self.channel_messages.add(
                    1,
                    &[
                        KeyValue::new("channel", channel.clone()),
                        KeyValue::new("direction", direction.clone()),
                    ],
                );
            }
            ObserverEvent::HeartbeatTick => {
                self.heartbeat_ticks.add(1, &[]);
            }
            ObserverEvent::Error { component, message } => {
                // Create an error span for visibility in trace backends
                let mut span = tracer.build(
                    opentelemetry::trace::SpanBuilder::from_name("error")
                        .with_kind(SpanKind::Internal)
                        .with_attributes(vec![
                            KeyValue::new("component", component.clone()),
                            KeyValue::new("error.message", message.clone()),
                        ]),
                );
                span.set_status(Status::error(message.clone()));
                span.end();

                self.errors
                    .add(1, &[KeyValue::new("component", component.clone())]);
            }
        }
    }

    fn record_metric(&self, metric: &ObserverMetric) {
        match metric {
            ObserverMetric::RequestLatency(d) => {
                self.request_latency.record(d.as_secs_f64(), &[]);
            }
            ObserverMetric::TokensUsed(t) => {
                self.tokens_used.add(*t as u64, &[]);
            }
            ObserverMetric::ActiveSessions(s) => {
                self.active_sessions.record(*s as u64, &[]);
            }
            ObserverMetric::QueueDepth(d) => {
                self.queue_depth.record(*d as u64, &[]);
            }
        }
    }

    fn flush(&self) {
        if let Err(e) = self.tracer_provider.force_flush() {
            tracing::warn!("OTel trace flush failed: {e}");
        }
        if let Err(e) = self.meter_provider.force_flush() {
            tracing::warn!("OTel metric flush failed: {e}");
        }
    }

    fn name(&self) -> &str {
        "otel"
    }

    fn as_any(&self) -> &dyn Any {
        self
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::cost::{BudgetState, CostOverrideScope, UsagePeriod};
    use crate::observability::BudgetOverrideAction;
    use std::time::Duration;

    // Note: OtelObserver::new() requires an OTLP endpoint.
    // In tests we verify the struct creation fails gracefully
    // when no collector is available, and test the observer interface
    // by constructing with a known-unreachable endpoint (spans/metrics
    // are buffered and exported asynchronously, so recording never panics).

    fn test_observer() -> OtelObserver {
        // Create with a dummy endpoint — exports will silently fail
        // but the observer itself works fine for recording
        OtelObserver::new(Some("http://127.0.0.1:19999"), Some("corvus-test"))
            .expect("observer creation should not fail with valid endpoint format")
    }

    #[test]
    fn otel_observer_name() {
        let obs = test_observer();
        assert_eq!(obs.name(), "otel");
    }

    #[test]
    fn records_all_events_without_panic() {
        let obs = test_observer();
        obs.record_event(&ObserverEvent::AgentStart {
            provider: "openrouter".into(),
            model: "claude-sonnet".into(),
        });
        obs.record_event(&ObserverEvent::LlmRequest {
            provider: "openrouter".into(),
            model: "claude-sonnet".into(),
            messages_count: 2,
        });
        obs.record_event(&ObserverEvent::LlmResponse {
            provider: "openrouter".into(),
            model: "claude-sonnet".into(),
            duration: Duration::from_millis(250),
            success: true,
            error_message: None,
        });
        obs.record_event(&ObserverEvent::AgentEnd {
            provider: "openrouter".into(),
            model: "claude-sonnet".into(),
            duration: Duration::from_millis(500),
            tokens_used: Some(100),
            cost_usd: Some(0.0015),
        });
        obs.record_event(&ObserverEvent::AgentEnd {
            provider: "openrouter".into(),
            model: "claude-sonnet".into(),
            duration: Duration::ZERO,
            tokens_used: None,
            cost_usd: None,
        });
        obs.record_event(&ObserverEvent::ToolCallStart {
            tool: "shell".into(),
        });
        obs.record_event(&ObserverEvent::ToolCall {
            tool: "shell".into(),
            duration: Duration::from_millis(10),
            success: true,
        });
        obs.record_event(&ObserverEvent::ToolCall {
            tool: "file_read".into(),
            duration: Duration::from_millis(5),
            success: false,
        });
        obs.record_event(&ObserverEvent::TurnComplete);
        obs.record_event(&ObserverEvent::ChannelMessage {
            channel: "telegram".into(),
            direction: "inbound".into(),
        });
        obs.record_event(&ObserverEvent::HeartbeatTick);
        obs.record_event(&ObserverEvent::Error {
            component: "provider".into(),
            message: "timeout".into(),
        });
        obs.record_event(&ObserverEvent::CodeSessionCompleted {
            session_id: "sess-1".into(),
            status: "completed".into(),
            summary: "Updated tests".into(),
            changed_files: vec!["src/lib.rs".into()],
            commands: vec!["cargo test".into()],
            validations: vec!["pass:cargo test".into()],
            blockers: vec![],
            pending_work: vec![],
            delegated: false,
        });
    }

    #[test]
    fn records_all_metrics_without_panic() {
        let obs = test_observer();
        obs.record_metric(&ObserverMetric::RequestLatency(Duration::from_secs(2)));
        obs.record_metric(&ObserverMetric::TokensUsed(500));
        obs.record_metric(&ObserverMetric::TokensUsed(0));
        obs.record_metric(&ObserverMetric::ActiveSessions(3));
        obs.record_metric(&ObserverMetric::QueueDepth(42));
    }

    #[test]
    fn flush_does_not_panic() {
        let obs = test_observer();
        obs.record_event(&ObserverEvent::HeartbeatTick);
        obs.flush();
    }

    // ── §8.2 OTel export failure resilience tests ────────────

    #[test]
    fn otel_records_error_event_without_panic() {
        let obs = test_observer();
        // Simulate an error event — should not panic even with unreachable endpoint
        obs.record_event(&ObserverEvent::Error {
            component: "provider".into(),
            message: "connection refused to model endpoint".into(),
        });
    }

    #[test]
    fn otel_records_llm_failure_without_panic() {
        let obs = test_observer();
        obs.record_event(&ObserverEvent::LlmResponse {
            provider: "openrouter".into(),
            model: "missing-model".into(),
            duration: Duration::from_millis(0),
            success: false,
            error_message: Some("404 Not Found".into()),
        });
    }

    #[test]
    fn otel_flush_idempotent_with_unreachable_endpoint() {
        let obs = test_observer();
        // Multiple flushes should not panic even when endpoint is unreachable
        obs.flush();
        obs.flush();
        obs.flush();
    }

    #[test]
    fn otel_records_zero_duration_metrics() {
        let obs = test_observer();
        obs.record_metric(&ObserverMetric::RequestLatency(Duration::ZERO));
        obs.record_metric(&ObserverMetric::TokensUsed(0));
        obs.record_metric(&ObserverMetric::ActiveSessions(0));
        obs.record_metric(&ObserverMetric::QueueDepth(0));
    }

    #[test]
    fn otel_observer_creation_with_valid_endpoint_succeeds() {
        // Even though endpoint is unreachable, creation should succeed
        let result = OtelObserver::new(Some("http://127.0.0.1:12345"), Some("corvus-test"));
        assert!(
            result.is_ok(),
            "observer creation must succeed even with unreachable endpoint"
        );
    }

    #[test]
    fn budget_override_trace_fields_redact_sensitive_values() {
        let event = BudgetOverrideEvent {
            action: BudgetOverrideAction::Granted,
            actor: "paired-admin-token".into(),
            scope: CostOverrideScope::NextRequest,
            reason: Some("token=super-secret".into()),
            session_id: Some("sess-123".into()),
            previous_state: BudgetState::Exceeded,
            period: Some(UsagePeriod::Day),
            override_id: Some("ovr-123".into()),
            surface: Some("gateway_admin".into()),
        };

        let fields = budget_override_trace_fields(&event);
        assert!(fields
            .iter()
            .any(|(key, value)| key == "actor" && value == "***REDACTED***"));
        assert!(fields
            .iter()
            .any(|(key, value)| key == "reason" && value == "***REDACTED***"));
        assert!(fields
            .iter()
            .all(|(_, value)| !value.contains("super-secret")));
    }
}
