use crate::observability::{
    CorrelationIds, DiagnosticDirection, EventDraft, EventFamily, EventOrigin, EventSeverity,
    JournalConfig, OBSERVABILITY_SCHEMA_VERSION, Observability, ObservabilityLimits, OutcomeCode,
    SensitiveDiagnosticPart, StructuredEvent, UnifiedPolicyAudit, replay_journal,
};
use crate::policy::{
    MemoryPolicyAudit, OriginClass, PolicyAuditRecord, PolicyAuditSink, PolicyDisposition,
    PolicyFinalOutcome, PolicyReasonCode, ResourceCost,
};
use crate::types::{BoundedText, MAX_IDENTIFIER_BYTES};
use serde_json::{Value, json};
use std::fs::{self, OpenOptions};
use std::io::Write;
use std::path::PathBuf;
use std::sync::atomic::{AtomicU64, Ordering};
use std::time::Duration;

static NEXT_TEMP: AtomicU64 = AtomicU64::new(1);
const SECRET_CANARY: &str = "OBSERVABILITY-SECRET-CANARY-9f7d2";

fn limits() -> ObservabilityLimits {
    ObservabilityLimits {
        ring_events: 8,
        subscriber_queue: 2,
        max_subscribers: 2,
        journal_queue: 8,
        max_event_bytes: 4_096,
        max_diagnostic_entries: 4,
    }
}

fn temp_dir(label: &str) -> PathBuf {
    let path = std::env::temp_dir().join(format!(
        "metacrate-observability-{label}-{}-{}",
        std::process::id(),
        NEXT_TEMP.fetch_add(1, Ordering::Relaxed)
    ));
    fs::create_dir_all(&path).expect("create test directory");
    path
}

fn draft(family: EventFamily, request: &str) -> EventDraft {
    EventDraft::new(family, EventSeverity::Info, "test", EventOrigin::Service)
        .expect("draft")
        .correlation(CorrelationIds {
            request_id: Some(request.to_owned()),
            ..CorrelationIds::default()
        })
        .expect("correlation")
        .result_code("completed")
        .expect("result")
        .redacted("content_omitted")
        .expect("redaction")
        .field("count", Value::from(1))
        .expect("field")
}

#[test]
fn golden_schema_covers_every_event_family_and_preserves_extensions() {
    let observer = Observability::memory(ObservabilityLimits {
        ring_events: 32,
        ..limits()
    })
    .expect("observer");
    let families = [
        EventFamily::LifecycleTransition,
        EventFamily::InboundMessage,
        EventFamily::OutboundMessage,
        EventFamily::SessionCreated,
        EventFamily::SessionExpired,
        EventFamily::InferenceRequest,
        EventFamily::InferenceResult,
        EventFamily::ModelActionSummary,
        EventFamily::ToolProposed,
        EventFamily::PolicyDecision,
        EventFamily::ApprovalDecision,
        EventFamily::ToolStarted,
        EventFamily::ToolProgress,
        EventFamily::ToolResult,
        EventFamily::BehaviorTransition,
        EventFamily::ScheduledJob,
        EventFamily::ControlCommand,
        EventFamily::Shutdown,
        EventFamily::DiagnosticEnvelope,
    ];
    for (index, family) in families.into_iter().enumerate() {
        observer
            .record(draft(family, &format!("request-{index}")))
            .expect("record family");
    }
    let events = observer.snapshot();
    assert_eq!(events.len(), families.len());
    let event = events.last().expect("last event");
    let value = serde_json::to_value(event).expect("JSON");
    assert_eq!(value["schema_version"], OBSERVABILITY_SCHEMA_VERSION);
    assert_eq!(value["family"], "diagnostic_envelope");
    assert_eq!(value["redaction_flags"], json!(["content_omitted"]));

    let mut value = value;
    value["future_top_level"] = json!({"compatible": true});
    value["fields"]["future_scalar"] = json!(42);
    let decoded: StructuredEvent = serde_json::from_value(value).expect("future event");
    assert_eq!(decoded.extensions["future_top_level"]["compatible"], true);
    assert_eq!(decoded.fields["future_scalar"], 42);
    let encoded = serde_json::to_value(decoded).expect("round trip");
    assert_eq!(encoded["future_top_level"]["compatible"], true);
}

#[test]
fn secret_canaries_are_unrepresentable_and_debug_is_redacted() {
    let observer = Observability::memory(limits()).expect("observer");
    for sensitive in [
        SECRET_CANARY,
        "Bearer OBSERVABILITY-SECRET-CANARY-9f7d2",
        "https://grid.example/CAPS/OBSERVABILITY-SECRET-CANARY-9f7d2",
        "password=OBSERVABILITY-SECRET-CANARY-9f7d2",
        "api_key=OBSERVABILITY-SECRET-CANARY-9f7d2",
    ] {
        assert!(
            EventDraft::new(
                EventFamily::InboundMessage,
                EventSeverity::Info,
                "test",
                EventOrigin::Grid
            )
            .expect("draft")
            .field("unsafe", Value::String(sensitive.to_owned()))
            .is_err()
        );
    }
    let part = SensitiveDiagnosticPart {
        role: SECRET_CANARY,
        contents: SECRET_CANARY,
    };
    assert!(!format!("{part:?}").contains(SECRET_CANARY));
    observer
        .capture_diagnostic(DiagnosticDirection::Prompt, "request-safe", &[part])
        .expect("diagnostic metadata");
    let rendered = serde_json::to_string(&observer.snapshot()).expect("render events");
    assert!(!rendered.contains(SECRET_CANARY));
    assert!(!format!("{observer:?}").contains(SECRET_CANARY));
    assert!(rendered.contains("content_digest"));
    assert!(rendered.contains("content_omitted"));
    let mut external = serde_json::to_value(&observer.snapshot()[0]).expect("external event");
    external["component"] = Value::String(SECRET_CANARY.to_owned());
    external["family"] = Value::String(SECRET_CANARY.to_owned());
    external["result_code"] = Value::String(SECRET_CANARY.to_owned());
    external["reason_code"] = Value::String(SECRET_CANARY.to_owned());
    external["redaction_flags"] = serde_json::json!([SECRET_CANARY]);
    external["future_secret"] = Value::String(SECRET_CANARY.to_owned());
    let external: StructuredEvent = serde_json::from_value(external).expect("external event");
    assert!(!format!("{external:?}").contains(SECRET_CANARY));
}

#[tokio::test]
async fn ring_overflow_reports_gap_and_slow_subscribers_are_disconnected() {
    let observer = Observability::memory(ObservabilityLimits {
        ring_events: 3,
        subscriber_queue: 1,
        ..limits()
    })
    .expect("observer");
    for index in 0..5 {
        observer
            .record(draft(
                EventFamily::ToolProgress,
                &format!("request-{index}"),
            ))
            .expect("record");
    }
    assert_eq!(observer.snapshot().len(), 3);
    assert_eq!(observer.metrics().snapshot().dropped_ring_events, 2);
    let mut replay = observer.subscribe(Some(0)).expect("subscription");
    assert_eq!(replay.take_gap(), Some((1, 3)));
    assert_eq!(replay.recv().await.expect("replay").event_id, 3);

    let mut slow = observer.subscribe(None).expect("slow subscription");
    for index in 5..9 {
        observer
            .record(draft(
                EventFamily::ToolProgress,
                &format!("request-{index}"),
            ))
            .expect("record");
    }
    while slow.recv().await.is_some() {}
    assert!(observer.metrics().snapshot().dropped_subscriber_events >= 1);
}

#[tokio::test]
async fn journal_rotates_recovers_truncated_tail_and_replays_without_execution() {
    let directory = temp_dir("journal");
    let config = JournalConfig {
        directory: directory.clone(),
        max_segment_bytes: 700,
        max_segments: 3,
        max_total_bytes: 2_100,
        sync_each_record: false,
    };
    let (observer, runtime) = Observability::journaled(
        ObservabilityLimits {
            max_event_bytes: 512,
            ..limits()
        },
        config,
    )
    .expect("journal observer");
    for index in 0..12 {
        let family = if index == 0 {
            EventFamily::LifecycleTransition
        } else if index == 1 {
            EventFamily::BehaviorTransition
        } else {
            EventFamily::ToolResult
        };
        let mut event = draft(family, &format!("request-{index}"));
        if family == EventFamily::LifecycleTransition || family == EventFamily::BehaviorTransition {
            event = event.code_field("to", "available").expect("state");
        }
        if family == EventFamily::ToolResult {
            event = event
                .correlation(CorrelationIds {
                    action_id: Some(format!("action-{index}")),
                    ..CorrelationIds::default()
                })
                .expect("action");
        }
        observer.record(event).expect("journal event");
    }
    runtime.shutdown(&observer).await.expect("journal shutdown");
    let last_before_restart = observer.snapshot().last().expect("last event").event_id;
    let (restarted, restarted_runtime) = Observability::journaled(
        ObservabilityLimits {
            max_event_bytes: 512,
            ..limits()
        },
        JournalConfig {
            directory: directory.clone(),
            max_segment_bytes: 700,
            max_segments: 3,
            max_total_bytes: 2_100,
            sync_each_record: false,
        },
    )
    .expect("restart journal");
    let restarted_id = restarted
        .record(draft(EventFamily::Shutdown, "restart"))
        .expect("restart event");
    assert!(restarted_id > last_before_restart);
    restarted_runtime
        .shutdown(&restarted)
        .await
        .expect("restart shutdown");
    let active = directory.join("events-current.jsonl.tmp");
    OpenOptions::new()
        .append(true)
        .open(&active)
        .expect("active")
        .write_all(b"{\"schema_version\":1")
        .expect("torn record");
    let replay = replay_journal(&directory, 4_096, 64).expect("recover replay");
    assert!(!replay.timeline.is_empty());
    assert!(replay.tool_outcomes > 0);
    assert!(replay.unavailable_private_content);
    assert!(
        replay
            .actions
            .values()
            .all(|action| action.final_result.is_some())
    );
    // Three retained segments, one active file, and one durable ID reservation.
    assert!(fs::read_dir(&directory).expect("directory").count() <= 5);
    let journal_bytes = fs::read_dir(&directory)
        .expect("directory")
        .filter_map(Result::ok)
        .filter(|entry| {
            let name = entry.file_name();
            let name = name.to_string_lossy();
            name.ends_with(".jsonl") || name == "events-current.jsonl.tmp"
        })
        .map(|entry| entry.metadata().expect("metadata").len())
        .sum::<u64>();
    assert!(journal_bytes <= 2_100);
    fs::remove_dir_all(directory).expect("remove test directory");
}

#[test]
fn metrics_have_fixed_fields_and_saturating_latency_summaries() {
    let observer = Observability::memory(limits()).expect("observer");
    let metrics = observer.metrics();
    metrics.set_health(true, 3);
    metrics.set_load(4, 5, 6, 7);
    metrics.set_rate_limit(8, 9);
    metrics.record_inference_latency(Duration::from_millis(11));
    metrics.record_inference_latency(Duration::from_millis(13));
    metrics.record_tool_latency(Duration::from_millis(17));
    metrics.record_inference_outcome(OutcomeCode::Failed);
    metrics.record_tool_outcome(OutcomeCode::Completed);
    metrics.record_policy_outcome(OutcomeCode::Denied);
    let snapshot = metrics.snapshot();
    assert!(snapshot.ready);
    assert_eq!(snapshot.reconnects, 3);
    assert_eq!(snapshot.active_sessions, 4);
    assert_eq!(snapshot.queue_capacity, 7);
    assert_eq!(snapshot.inference_latency.samples, 2);
    assert_eq!(snapshot.inference_latency.total_millis, 24);
    assert_eq!(snapshot.inference_latency.maximum_millis, 13);
    assert_eq!(snapshot.tool_latency.maximum_millis, 17);
    assert_eq!(snapshot.inference_outcomes.failed, 1);
    assert_eq!(snapshot.tool_outcomes.completed, 1);
    assert_eq!(snapshot.policy_outcomes.denied, 1);
    assert_eq!(
        serde_json::to_value(snapshot)
            .expect("metrics")
            .as_object()
            .expect("object")
            .len(),
        18
    );
}

#[test]
fn unified_policy_audit_correlates_outcomes_without_exposing_inputs() {
    let probe_observer = Observability::memory(limits()).expect("probe observer");
    let probe = EventDraft::new(
        EventFamily::ToolResult,
        EventSeverity::Info,
        "policy",
        EventOrigin::Policy,
    )
    .and_then(|event| {
        event.correlation(CorrelationIds {
            session_id: Some(SECRET_CANARY.to_owned()),
            request_id: Some(SECRET_CANARY.to_owned()),
            action_id: Some("authorization-7".to_owned()),
            ..CorrelationIds::default()
        })
    })
    .and_then(|event| event.result_code("completed"))
    .and_then(|event| event.reason_code("allowed"))
    .and_then(|event| event.identifier_field("tool", SECRET_CANARY))
    .and_then(|event| event.code_field("disposition", "allowed"))
    .and_then(|event| event.field("tool_calls", Value::from(1)))
    .and_then(|event| event.field("movement_millimeters", Value::from(0)))
    .and_then(|event| event.redacted("tool_arguments"))
    .expect("policy event draft");
    probe_observer.record(probe).expect("policy event record");

    let observer = Observability::memory(limits()).expect("observer");
    let memory = std::sync::Arc::new(MemoryPolicyAudit::new(8).expect("policy audit"));
    let audit = UnifiedPolicyAudit::new(memory.clone(), observer.clone());
    audit
        .emit(PolicyAuditRecord {
            recorded_unix_millis: 1,
            authorization_id: Some(7),
            approval_id: None,
            origin: OriginClass::AuthorizedIm,
            origin_avatar_id: None,
            principal: BoundedText::<MAX_IDENTIFIER_BYTES>::new("principal", SECRET_CANARY)
                .expect("principal"),
            session_id: BoundedText::<MAX_IDENTIFIER_BYTES>::new("session", SECRET_CANARY)
                .expect("session"),
            correlation_id: BoundedText::<MAX_IDENTIFIER_BYTES>::new("correlation", SECRET_CANARY)
                .expect("correlation"),
            tool: BoundedText::<MAX_IDENTIFIER_BYTES>::new("tool", SECRET_CANARY).expect("tool"),
            disposition: PolicyDisposition::Allowed,
            reason: PolicyReasonCode::Allowed,
            applied_budget: ResourceCost::one_call(),
            arguments_hash: BoundedText::<64>::new("hash", SECRET_CANARY).expect("hash"),
            final_outcome: PolicyFinalOutcome::Completed,
        })
        .expect("emit audit");
    assert_eq!(memory.snapshot().len(), 1);
    let rendered = serde_json::to_string(&observer.snapshot()).expect("events");
    assert!(!rendered.contains(SECRET_CANARY));
    let families = observer
        .snapshot()
        .iter()
        .map(|event| event.family.clone())
        .collect::<Vec<_>>();
    assert!(
        families.iter().any(|family| family == "tool_result"),
        "{families:?}"
    );
    assert_eq!(observer.metrics().snapshot().policy_outcomes.completed, 1);
}

#[tokio::test]
async fn nonblocking_flood_remains_bounded_when_journal_queue_is_saturated() {
    let directory = temp_dir("flood");
    let flood_limits = ObservabilityLimits {
        ring_events: 16,
        subscriber_queue: 1,
        max_subscribers: 1,
        journal_queue: 1,
        max_event_bytes: 4_096,
        max_diagnostic_entries: 0,
    };
    let (observer, runtime) = Observability::journaled(
        flood_limits,
        JournalConfig {
            directory: directory.clone(),
            max_segment_bytes: 4_096,
            max_segments: 2,
            max_total_bytes: 8_192,
            sync_each_record: true,
        },
    )
    .expect("journal observer");
    let _slow = observer.subscribe(None).expect("subscriber");
    for index in 0..10_000 {
        observer
            .record(draft(EventFamily::ToolProgress, &format!("flood-{index}")))
            .expect("bounded record");
    }
    assert_eq!(observer.snapshot().len(), flood_limits.ring_events);
    let snapshot = observer.metrics().snapshot();
    assert_eq!(snapshot.recorded_events, 10_000);
    assert!(snapshot.dropped_journal_events > 0);
    assert!(snapshot.dropped_subscriber_events > 0);
    runtime.shutdown(&observer).await.expect("shutdown");
    fs::remove_dir_all(directory).expect("remove test directory");
}
