use crate::{ClientMessage, ResumeStatus, ServerMessage, PROTOCOL_VERSION_V1};
use serde::{Deserialize, Serialize};
use std::collections::BTreeSet;

pub const SUPPORTED_PROTOCOL_VERSIONS: &[&str] = &[PROTOCOL_VERSION_V1];

#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum ProtocolDirection {
    ClientToServer,
    ServerToClient,
}

#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum ProtocolAuthority {
    UntrustedClient,
    TrustedServer,
}

#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum ProtocolInstructionClass {
    Lifecycle,
    Event,
    Render,
    Navigation,
    Upload,
    Diagnostics,
    Stream,
    Chart,
    Notification,
    Grid,
    Interop,
}

#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum ProtocolOrdering {
    Unordered,
    PerSessionOrdered,
}

#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum ProtocolDurability {
    Ephemeral,
    Replayable,
    Resumable,
}

#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum ProtocolRenderEffect {
    None,
    Patch,
    Diff,
    Stream,
    Navigation,
}

#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub struct ProtocolInstructionDescriptor {
    pub class: ProtocolInstructionClass,
    pub direction: ProtocolDirection,
    pub authority: ProtocolAuthority,
    pub ordering: ProtocolOrdering,
    pub durability: ProtocolDurability,
    pub render_effect: ProtocolRenderEffect,
}

#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct ProtocolInvariantViolation {
    pub code: String,
    pub message: String,
}

impl ProtocolInvariantViolation {
    fn new(code: impl Into<String>, message: impl Into<String>) -> Self {
        Self {
            code: code.into(),
            message: message.into(),
        }
    }
}

pub fn is_supported_protocol_version(protocol: &str) -> bool {
    SUPPORTED_PROTOCOL_VERSIONS.contains(&protocol)
}

pub fn describe_client_message(message: &ClientMessage) -> ProtocolInstructionDescriptor {
    let class = match message {
        ClientMessage::Connect { .. } => ProtocolInstructionClass::Lifecycle,
        ClientMessage::Event { .. } => ProtocolInstructionClass::Event,
        ClientMessage::Ping { .. } => ProtocolInstructionClass::Diagnostics,
        ClientMessage::PatchUrl { .. } | ClientMessage::Navigate { .. } => {
            ProtocolInstructionClass::Navigation
        }
        ClientMessage::UploadStart { .. }
        | ClientMessage::UploadChunk { .. }
        | ClientMessage::UploadComplete { .. } => ProtocolInstructionClass::Upload,
    };
    let durability = match message {
        ClientMessage::Connect { .. } => ProtocolDurability::Resumable,
        ClientMessage::Event { .. } => ProtocolDurability::Replayable,
        ClientMessage::PatchUrl { .. } | ClientMessage::Navigate { .. } => {
            ProtocolDurability::Replayable
        }
        ClientMessage::UploadStart { .. }
        | ClientMessage::UploadChunk { .. }
        | ClientMessage::UploadComplete { .. }
        | ClientMessage::Ping { .. } => ProtocolDurability::Ephemeral,
    };
    let render_effect = match message {
        ClientMessage::PatchUrl { .. } | ClientMessage::Navigate { .. } => {
            ProtocolRenderEffect::Navigation
        }
        _ => ProtocolRenderEffect::None,
    };
    ProtocolInstructionDescriptor {
        class,
        direction: ProtocolDirection::ClientToServer,
        authority: ProtocolAuthority::UntrustedClient,
        ordering: ProtocolOrdering::PerSessionOrdered,
        durability,
        render_effect,
    }
}

pub fn describe_server_message(message: &ServerMessage) -> ProtocolInstructionDescriptor {
    let (class, render_effect, durability) = match message {
        ServerMessage::Hello { .. } => (
            ProtocolInstructionClass::Lifecycle,
            ProtocolRenderEffect::None,
            ProtocolDurability::Resumable,
        ),
        ServerMessage::Patch { .. } => (
            ProtocolInstructionClass::Render,
            ProtocolRenderEffect::Patch,
            ProtocolDurability::Replayable,
        ),
        ServerMessage::Diff { .. } => (
            ProtocolInstructionClass::Render,
            ProtocolRenderEffect::Diff,
            ProtocolDurability::Replayable,
        ),
        ServerMessage::StreamInsert { .. }
        | ServerMessage::StreamDelete { .. }
        | ServerMessage::StreamBatch { .. } => (
            ProtocolInstructionClass::Stream,
            ProtocolRenderEffect::Stream,
            ProtocolDurability::Replayable,
        ),
        ServerMessage::ChartSeriesAppend { .. }
        | ServerMessage::ChartSeriesAppendMany { .. }
        | ServerMessage::ChartSeriesReplace { .. }
        | ServerMessage::ChartReset { .. }
        | ServerMessage::ChartAnnotationUpsert { .. }
        | ServerMessage::ChartAnnotationDelete { .. } => (
            ProtocolInstructionClass::Chart,
            ProtocolRenderEffect::None,
            ProtocolDurability::Replayable,
        ),
        ServerMessage::ToastPush { .. }
        | ServerMessage::ToastDismiss { .. }
        | ServerMessage::InboxUpsert { .. }
        | ServerMessage::InboxDelete { .. } => (
            ProtocolInstructionClass::Notification,
            ProtocolRenderEffect::None,
            ProtocolDurability::Replayable,
        ),
        ServerMessage::GridReplace { .. } | ServerMessage::GridRowsReplace { .. } => (
            ProtocolInstructionClass::Grid,
            ProtocolRenderEffect::None,
            ProtocolDurability::Replayable,
        ),
        ServerMessage::InteropDispatch { .. } => (
            ProtocolInstructionClass::Interop,
            ProtocolRenderEffect::None,
            ProtocolDurability::Ephemeral,
        ),
        ServerMessage::Pong { .. } | ServerMessage::Error { .. } => (
            ProtocolInstructionClass::Diagnostics,
            ProtocolRenderEffect::None,
            ProtocolDurability::Ephemeral,
        ),
        ServerMessage::Redirect { .. }
        | ServerMessage::PatchUrl { .. }
        | ServerMessage::Navigate { .. } => (
            ProtocolInstructionClass::Navigation,
            ProtocolRenderEffect::Navigation,
            ProtocolDurability::Replayable,
        ),
        ServerMessage::UploadProgress { .. }
        | ServerMessage::UploadComplete { .. }
        | ServerMessage::UploadError { .. } => (
            ProtocolInstructionClass::Upload,
            ProtocolRenderEffect::None,
            ProtocolDurability::Ephemeral,
        ),
    };
    ProtocolInstructionDescriptor {
        class,
        direction: ProtocolDirection::ServerToClient,
        authority: ProtocolAuthority::TrustedServer,
        ordering: ProtocolOrdering::PerSessionOrdered,
        durability,
        render_effect,
    }
}

pub fn validate_client_message_invariants(
    message: &ClientMessage,
) -> Result<(), ProtocolInvariantViolation> {
    match message {
        ClientMessage::Connect {
            protocol,
            trace_id,
            span_id,
            parent_span_id,
            ..
        } => {
            if !is_supported_protocol_version(protocol) {
                return Err(ProtocolInvariantViolation::new(
                    "unsupported_protocol_version",
                    format!(
                        "client connect protocol '{}' is unsupported; expected one of {:?}",
                        protocol, SUPPORTED_PROTOCOL_VERSIONS
                    ),
                ));
            }
            validate_hex_id("trace_id", trace_id.as_deref(), 32)?;
            validate_hex_id("span_id", span_id.as_deref(), 16)?;
            validate_hex_id("parent_span_id", parent_span_id.as_deref(), 16)?;
            Ok(())
        }
        ClientMessage::Event { event, .. } => {
            if event.trim().is_empty() {
                return Err(ProtocolInvariantViolation::new(
                    "empty_event_name",
                    "event message must include a non-empty event name",
                ));
            }
            Ok(())
        }
        ClientMessage::PatchUrl { to } => validate_internal_path("patch_url", to),
        ClientMessage::Navigate { to } => validate_internal_path("navigate", to),
        ClientMessage::UploadStart {
            upload_id,
            event,
            name,
            ..
        } => {
            if upload_id.trim().is_empty() {
                return Err(ProtocolInvariantViolation::new(
                    "empty_upload_id",
                    "upload_start.upload_id cannot be empty",
                ));
            }
            if event.trim().is_empty() {
                return Err(ProtocolInvariantViolation::new(
                    "empty_upload_event",
                    "upload_start.event cannot be empty",
                ));
            }
            if name.trim().is_empty() {
                return Err(ProtocolInvariantViolation::new(
                    "empty_upload_name",
                    "upload_start.name cannot be empty",
                ));
            }
            Ok(())
        }
        ClientMessage::UploadChunk { upload_id, .. }
        | ClientMessage::UploadComplete { upload_id } => {
            if upload_id.trim().is_empty() {
                return Err(ProtocolInvariantViolation::new(
                    "empty_upload_id",
                    "upload message upload_id cannot be empty",
                ));
            }
            Ok(())
        }
        ClientMessage::Ping { .. } => Ok(()),
    }
}

pub fn validate_server_message_invariants(
    message: &ServerMessage,
) -> Result<(), ProtocolInvariantViolation> {
    match message {
        ServerMessage::Hello {
            session_id,
            target,
            protocol,
            revision,
            server_revision,
            resume_status,
            resume_reason,
            resume_token,
            resume_expires_in_ms,
        } => {
            if session_id.trim().is_empty() {
                return Err(ProtocolInvariantViolation::new(
                    "empty_session_id",
                    "hello.session_id cannot be empty",
                ));
            }
            if target.trim().is_empty() {
                return Err(ProtocolInvariantViolation::new(
                    "empty_target",
                    "hello.target cannot be empty",
                ));
            }
            if !is_supported_protocol_version(protocol) {
                return Err(ProtocolInvariantViolation::new(
                    "unsupported_protocol_version",
                    format!(
                        "hello.protocol '{}' is unsupported; expected one of {:?}",
                        protocol, SUPPORTED_PROTOCOL_VERSIONS
                    ),
                ));
            }
            if let Some(server_revision) = server_revision {
                if server_revision < revision {
                    return Err(ProtocolInvariantViolation::new(
                        "server_revision_regression",
                        format!(
                            "hello.server_revision ({server_revision}) cannot be lower than hello.revision ({revision})",
                        ),
                    ));
                }
            }
            if let Some(status) = resume_status {
                if matches!(status, ResumeStatus::Fallback)
                    && resume_reason
                        .as_ref()
                        .map(|reason| reason.trim().is_empty())
                        .unwrap_or(true)
                {
                    return Err(ProtocolInvariantViolation::new(
                        "missing_resume_reason",
                        "hello.resume_reason must be present for fallback resume status",
                    ));
                }
            }
            if let Some(reason) = resume_reason {
                if reason.trim().is_empty() {
                    return Err(ProtocolInvariantViolation::new(
                        "empty_resume_reason",
                        "hello.resume_reason cannot be empty when present",
                    ));
                }
            }
            if resume_expires_in_ms.is_some() && resume_token.is_none() {
                return Err(ProtocolInvariantViolation::new(
                    "missing_resume_token",
                    "hello.resume_token must be present when resume_expires_in_ms is set",
                ));
            }
            Ok(())
        }
        ServerMessage::Patch { target, .. } => validate_non_empty("patch.target", target),
        ServerMessage::Diff { target, slots, .. } => {
            validate_non_empty("diff.target", target)?;
            let mut indices = BTreeSet::new();
            for slot in slots {
                if !indices.insert(slot.index) {
                    return Err(ProtocolInvariantViolation::new(
                        "duplicate_diff_slot",
                        format!("diff contains duplicate slot index {}", slot.index),
                    ));
                }
            }
            Ok(())
        }
        ServerMessage::StreamInsert { target, id, .. }
        | ServerMessage::StreamDelete { target, id } => {
            validate_non_empty("stream.target", target)?;
            validate_non_empty("stream.id", id)
        }
        ServerMessage::StreamBatch { target, operations } => {
            validate_non_empty("stream_batch.target", target)?;
            if operations.is_empty() {
                return Err(ProtocolInvariantViolation::new(
                    "empty_stream_batch",
                    "stream_batch.operations cannot be empty",
                ));
            }
            Ok(())
        }
        ServerMessage::Redirect { to }
        | ServerMessage::PatchUrl { to }
        | ServerMessage::Navigate { to } => validate_internal_path("server_navigation", to),
        ServerMessage::UploadProgress {
            upload_id,
            received,
            total,
        } => {
            validate_non_empty("upload_progress.upload_id", upload_id)?;
            if received > total {
                return Err(ProtocolInvariantViolation::new(
                    "upload_progress_out_of_bounds",
                    format!("upload progress received {received} cannot exceed total {total}"),
                ));
            }
            Ok(())
        }
        ServerMessage::UploadComplete {
            upload_id, name, ..
        } => {
            validate_non_empty("upload_complete.upload_id", upload_id)?;
            validate_non_empty("upload_complete.name", name)
        }
        ServerMessage::UploadError {
            upload_id, message, ..
        } => {
            validate_non_empty("upload_error.upload_id", upload_id)?;
            validate_non_empty("upload_error.message", message)
        }
        ServerMessage::Error { message, .. } => validate_non_empty("error.message", message),
        ServerMessage::ChartSeriesAppend { chart, series, .. }
        | ServerMessage::ChartSeriesAppendMany { chart, series, .. }
        | ServerMessage::ChartSeriesReplace { chart, series, .. } => {
            validate_non_empty("chart", chart)?;
            validate_non_empty("series", series)
        }
        ServerMessage::ChartReset { chart } => validate_non_empty("chart", chart),
        ServerMessage::ChartAnnotationUpsert { chart, annotation } => {
            validate_non_empty("chart", chart)?;
            validate_non_empty("annotation.id", &annotation.id)
        }
        ServerMessage::ChartAnnotationDelete { chart, id } => {
            validate_non_empty("chart", chart)?;
            validate_non_empty("annotation.id", id)
        }
        ServerMessage::ToastPush { toast } => validate_non_empty("toast.id", &toast.id),
        ServerMessage::ToastDismiss { id } => validate_non_empty("toast.id", id),
        ServerMessage::InboxUpsert { item } => validate_non_empty("inbox.id", &item.id),
        ServerMessage::InboxDelete { id } => validate_non_empty("inbox.id", id),
        ServerMessage::GridReplace { grid, .. } | ServerMessage::GridRowsReplace { grid, .. } => {
            validate_non_empty("grid", grid)
        }
        ServerMessage::InteropDispatch { dispatch } => {
            validate_non_empty("interop.event", &dispatch.event)
        }
        ServerMessage::Pong { .. } => Ok(()),
    }
}

pub fn validate_server_message_sequence(
    messages: &[ServerMessage],
) -> Result<(), ProtocolInvariantViolation> {
    let mut seen_hello = false;
    let mut last_render_revision: Option<u64> = None;

    for (index, message) in messages.iter().enumerate() {
        validate_server_message_invariants(message)?;
        if matches!(message, ServerMessage::Hello { .. }) {
            if seen_hello {
                return Err(ProtocolInvariantViolation::new(
                    "duplicate_hello",
                    "server transcript cannot contain multiple hello messages",
                ));
            }
            if index != 0 {
                return Err(ProtocolInvariantViolation::new(
                    "hello_not_first",
                    "hello message must be first in a server transcript",
                ));
            }
            seen_hello = true;
            continue;
        }

        let current_render_revision = match message {
            ServerMessage::Patch { revision, .. } | ServerMessage::Diff { revision, .. } => {
                Some(*revision)
            }
            _ => None,
        };
        if let Some(current_render_revision) = current_render_revision {
            if let Some(previous) = last_render_revision {
                if current_render_revision <= previous {
                    return Err(ProtocolInvariantViolation::new(
                        "non_monotonic_revision",
                        format!(
                            "render revisions must increase monotonically (previous={previous}, next={current_render_revision})"
                        ),
                    ));
                }
            }
            last_render_revision = Some(current_render_revision);
        }
    }
    Ok(())
}

fn validate_internal_path(label: &str, path: &str) -> Result<(), ProtocolInvariantViolation> {
    if path.trim().is_empty() {
        return Err(ProtocolInvariantViolation::new(
            "empty_path",
            format!("{label} path cannot be empty"),
        ));
    }
    let normalized = path.trim();
    if !normalized.starts_with('/') {
        return Err(ProtocolInvariantViolation::new(
            "non_internal_path",
            format!("{label} path must start with '/'"),
        ));
    }
    if normalized.starts_with("//")
        || normalized.starts_with("/\\")
        || normalized.contains("://")
        || normalized.starts_with("/http")
    {
        return Err(ProtocolInvariantViolation::new(
            "external_path",
            format!("{label} path must stay internal to the current application"),
        ));
    }
    Ok(())
}

fn validate_non_empty(label: &str, value: &str) -> Result<(), ProtocolInvariantViolation> {
    if value.trim().is_empty() {
        return Err(ProtocolInvariantViolation::new(
            "empty_field",
            format!("{label} cannot be empty"),
        ));
    }
    Ok(())
}

fn validate_hex_id(
    label: &str,
    value: Option<&str>,
    expected_len: usize,
) -> Result<(), ProtocolInvariantViolation> {
    let Some(value) = value else {
        return Ok(());
    };
    if value.len() != expected_len {
        return Err(ProtocolInvariantViolation::new(
            "invalid_hex_id_length",
            format!("{label} must contain exactly {expected_len} hex chars"),
        ));
    }
    if !value.chars().all(|char| char.is_ascii_hexdigit()) {
        return Err(ProtocolInvariantViolation::new(
            "invalid_hex_id_charset",
            format!("{label} must contain only ascii hex chars"),
        ));
    }
    Ok(())
}

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

    fn valid_connect() -> ClientMessage {
        ClientMessage::Connect {
            protocol: PROTOCOL_VERSION_V1.to_string(),
            session_id: None,
            last_revision: None,
            resume_token: None,
            tenant_id: None,
            trace_id: Some("4bf92f3577b34da6a3ce929d0e0e4736".to_string()),
            span_id: Some("00f067aa0ba902b7".to_string()),
            parent_span_id: Some("89abcdef01234567".to_string()),
            correlation_id: None,
            request_id: None,
        }
    }

    fn valid_hello() -> ServerMessage {
        ServerMessage::Hello {
            session_id: "sid".to_string(),
            target: "root".to_string(),
            revision: 0,
            protocol: PROTOCOL_VERSION_V1.to_string(),
            server_revision: Some(0),
            resume_status: Some(ResumeStatus::Fresh),
            resume_reason: None,
            resume_token: None,
            resume_expires_in_ms: None,
        }
    }

    #[test]
    fn client_connect_rejects_unsupported_protocol() {
        let invalid = ClientMessage::Connect {
            protocol: "shelly/2".to_string(),
            session_id: None,
            last_revision: None,
            resume_token: None,
            tenant_id: None,
            trace_id: None,
            span_id: None,
            parent_span_id: None,
            correlation_id: None,
            request_id: None,
        };
        let err = validate_client_message_invariants(&invalid).expect_err("unsupported protocol");
        assert_eq!(err.code, "unsupported_protocol_version");
    }

    #[test]
    fn client_invariants_cover_hex_event_navigation_and_upload_rules() {
        assert!(validate_client_message_invariants(&valid_connect()).is_ok());

        let mut invalid_trace = valid_connect();
        if let ClientMessage::Connect { trace_id, .. } = &mut invalid_trace {
            *trace_id = Some("abcd".to_string());
        }
        let err = validate_client_message_invariants(&invalid_trace).expect_err("trace length");
        assert_eq!(err.code, "invalid_hex_id_length");

        let mut invalid_span = valid_connect();
        if let ClientMessage::Connect { span_id, .. } = &mut invalid_span {
            *span_id = Some("zzzzzzzzzzzzzzzz".to_string());
        }
        let err = validate_client_message_invariants(&invalid_span).expect_err("span charset");
        assert_eq!(err.code, "invalid_hex_id_charset");

        let err = validate_client_message_invariants(&ClientMessage::Event {
            event: "   ".to_string(),
            target: None,
            value: json!({}),
            metadata: serde_json::Map::new(),
        })
        .expect_err("empty event should fail");
        assert_eq!(err.code, "empty_event_name");

        let err = validate_client_message_invariants(&ClientMessage::PatchUrl {
            to: "users/1".to_string(),
        })
        .expect_err("patch_url must stay internal");
        assert_eq!(err.code, "non_internal_path");

        let err = validate_client_message_invariants(&ClientMessage::Navigate {
            to: "/http://evil.example".to_string(),
        })
        .expect_err("navigate must stay internal");
        assert_eq!(err.code, "external_path");

        let err = validate_client_message_invariants(&ClientMessage::UploadStart {
            upload_id: "".to_string(),
            event: "uploaded".to_string(),
            target: None,
            name: "file.txt".to_string(),
            size: 1,
            content_type: None,
        })
        .expect_err("upload id is required");
        assert_eq!(err.code, "empty_upload_id");

        let err = validate_client_message_invariants(&ClientMessage::UploadStart {
            upload_id: "u1".to_string(),
            event: "   ".to_string(),
            target: None,
            name: "file.txt".to_string(),
            size: 1,
            content_type: None,
        })
        .expect_err("upload event is required");
        assert_eq!(err.code, "empty_upload_event");

        let err = validate_client_message_invariants(&ClientMessage::UploadStart {
            upload_id: "u1".to_string(),
            event: "uploaded".to_string(),
            target: None,
            name: "   ".to_string(),
            size: 1,
            content_type: None,
        })
        .expect_err("upload name is required");
        assert_eq!(err.code, "empty_upload_name");

        let err = validate_client_message_invariants(&ClientMessage::UploadChunk {
            upload_id: "  ".to_string(),
            offset: 0,
            data: "AA==".to_string(),
        })
        .expect_err("upload chunk id is required");
        assert_eq!(err.code, "empty_upload_id");

        let err = validate_client_message_invariants(&ClientMessage::UploadComplete {
            upload_id: "  ".to_string(),
        })
        .expect_err("upload complete id is required");
        assert_eq!(err.code, "empty_upload_id");

        assert!(validate_client_message_invariants(&ClientMessage::Ping { nonce: None }).is_ok());
    }

    #[test]
    fn server_hello_invariants_cover_resume_and_protocol_rules() {
        assert!(validate_server_message_invariants(&valid_hello()).is_ok());

        let mut message = valid_hello();
        if let ServerMessage::Hello { session_id, .. } = &mut message {
            *session_id = " ".to_string();
        }
        let err = validate_server_message_invariants(&message).expect_err("session id required");
        assert_eq!(err.code, "empty_session_id");

        let mut message = valid_hello();
        if let ServerMessage::Hello { target, .. } = &mut message {
            *target = " ".to_string();
        }
        let err = validate_server_message_invariants(&message).expect_err("target required");
        assert_eq!(err.code, "empty_target");

        let mut message = valid_hello();
        if let ServerMessage::Hello { protocol, .. } = &mut message {
            *protocol = "shelly/9".to_string();
        }
        let err = validate_server_message_invariants(&message).expect_err("protocol unsupported");
        assert_eq!(err.code, "unsupported_protocol_version");

        let mut message = valid_hello();
        if let ServerMessage::Hello {
            revision,
            server_revision,
            ..
        } = &mut message
        {
            *revision = 3;
            *server_revision = Some(2);
        }
        let err = validate_server_message_invariants(&message)
            .expect_err("server revision cannot regress below revision");
        assert_eq!(err.code, "server_revision_regression");

        let mut message = valid_hello();
        if let ServerMessage::Hello {
            resume_status,
            resume_reason,
            ..
        } = &mut message
        {
            *resume_status = Some(ResumeStatus::Fallback);
            *resume_reason = None;
        }
        let err =
            validate_server_message_invariants(&message).expect_err("fallback reason is required");
        assert_eq!(err.code, "missing_resume_reason");

        let mut message = valid_hello();
        if let ServerMessage::Hello {
            resume_status,
            resume_reason,
            ..
        } = &mut message
        {
            *resume_status = Some(ResumeStatus::Fresh);
            *resume_reason = Some("   ".to_string());
        }
        let err = validate_server_message_invariants(&message)
            .expect_err("resume reason cannot be blank when present");
        assert_eq!(err.code, "empty_resume_reason");

        let mut message = valid_hello();
        if let ServerMessage::Hello {
            resume_token,
            resume_expires_in_ms,
            ..
        } = &mut message
        {
            *resume_token = None;
            *resume_expires_in_ms = Some(60_000);
        }
        let err = validate_server_message_invariants(&message)
            .expect_err("resume expiry requires resume token");
        assert_eq!(err.code, "missing_resume_token");
    }

    #[test]
    fn server_message_invariants_cover_render_stream_upload_and_navigation() {
        let err = validate_server_message_invariants(&ServerMessage::Patch {
            target: "".to_string(),
            html: "<p>x</p>".to_string(),
            revision: 1,
        })
        .expect_err("patch target is required");
        assert_eq!(err.code, "empty_field");

        let err = validate_server_message_invariants(&ServerMessage::Diff {
            target: "root".to_string(),
            revision: 1,
            slots: vec![
                crate::DynamicSlotPatch {
                    index: 1,
                    html: "a".to_string(),
                },
                crate::DynamicSlotPatch {
                    index: 1,
                    html: "b".to_string(),
                },
            ],
        })
        .expect_err("duplicate diff slots should fail");
        assert_eq!(err.code, "duplicate_diff_slot");

        let err = validate_server_message_invariants(&ServerMessage::StreamInsert {
            target: "root".to_string(),
            id: "".to_string(),
            html: "<li>x</li>".to_string(),
            at: crate::StreamPosition::Append,
        })
        .expect_err("stream id is required");
        assert_eq!(err.code, "empty_field");

        let err = validate_server_message_invariants(&ServerMessage::StreamBatch {
            target: "root".to_string(),
            operations: vec![],
        })
        .expect_err("stream batch must not be empty");
        assert_eq!(err.code, "empty_stream_batch");

        let err = validate_server_message_invariants(&ServerMessage::Redirect {
            to: "/http://evil.example".to_string(),
        })
        .expect_err("redirect must stay internal");
        assert_eq!(err.code, "external_path");

        let err = validate_server_message_invariants(&ServerMessage::UploadProgress {
            upload_id: "u1".to_string(),
            received: 2,
            total: 1,
        })
        .expect_err("upload progress bounds");
        assert_eq!(err.code, "upload_progress_out_of_bounds");

        let err = validate_server_message_invariants(&ServerMessage::UploadComplete {
            upload_id: "u1".to_string(),
            name: "".to_string(),
            size: 1,
            content_type: None,
        })
        .expect_err("upload name required");
        assert_eq!(err.code, "empty_field");

        let err = validate_server_message_invariants(&ServerMessage::UploadError {
            upload_id: "u1".to_string(),
            message: "".to_string(),
            code: Some("bad".to_string()),
        })
        .expect_err("upload error message required");
        assert_eq!(err.code, "empty_field");

        let err = validate_server_message_invariants(&ServerMessage::Error {
            message: "".to_string(),
            code: None,
        })
        .expect_err("error message required");
        assert_eq!(err.code, "empty_field");
    }

    #[test]
    fn server_message_invariants_cover_chart_notification_grid_and_interop() {
        let err = validate_server_message_invariants(&ServerMessage::ChartSeriesAppend {
            chart: "".to_string(),
            series: "s1".to_string(),
            point: crate::ChartPoint { x: 1.0, y: 2.0 },
        })
        .expect_err("chart id required");
        assert_eq!(err.code, "empty_field");

        let err = validate_server_message_invariants(&ServerMessage::ChartAnnotationUpsert {
            chart: "chart-1".to_string(),
            annotation: crate::ChartAnnotation {
                id: "".to_string(),
                x: 1.0,
                label: "L".to_string(),
            },
        })
        .expect_err("annotation id required");
        assert_eq!(err.code, "empty_field");

        let err = validate_server_message_invariants(&ServerMessage::ChartAnnotationDelete {
            chart: "chart-1".to_string(),
            id: "".to_string(),
        })
        .expect_err("annotation delete id required");
        assert_eq!(err.code, "empty_field");

        let err = validate_server_message_invariants(&ServerMessage::ToastPush {
            toast: crate::Toast {
                id: "".to_string(),
                level: crate::ToastLevel::Info,
                title: None,
                message: "hello".to_string(),
                ttl_ms: None,
            },
        })
        .expect_err("toast id required");
        assert_eq!(err.code, "empty_field");

        let err = validate_server_message_invariants(&ServerMessage::InboxUpsert {
            item: crate::InboxItem {
                id: "".to_string(),
                title: "t".to_string(),
                body: "b".to_string(),
                read: false,
                inserted_at: None,
            },
        })
        .expect_err("inbox id required");
        assert_eq!(err.code, "empty_field");

        let err = validate_server_message_invariants(&ServerMessage::GridReplace {
            grid: "".to_string(),
            state: crate::GridState {
                columns: vec![],
                rows: vec![],
                total_rows: 0,
                offset: 0,
                limit: 20,
                views: vec![],
                active_view: None,
                group_by: None,
                query: None,
                sort: None,
            },
        })
        .expect_err("grid id required");
        assert_eq!(err.code, "empty_field");

        let err = validate_server_message_invariants(&ServerMessage::InteropDispatch {
            dispatch: crate::JsInteropDispatch {
                target: None,
                event: "".to_string(),
                detail: json!({}),
                bubbles: true,
            },
        })
        .expect_err("interop event required");
        assert_eq!(err.code, "empty_field");

        assert!(validate_server_message_invariants(&ServerMessage::Pong { nonce: None }).is_ok());
    }

    #[test]
    fn server_sequence_requires_monotonic_revisions() {
        let messages = vec![
            ServerMessage::Patch {
                target: "root".to_string(),
                html: "<p>1</p>".to_string(),
                revision: 2,
            },
            ServerMessage::Diff {
                target: "root".to_string(),
                revision: 2,
                slots: vec![],
            },
        ];
        let err =
            validate_server_message_sequence(&messages).expect_err("non monotonic should fail");
        assert_eq!(err.code, "non_monotonic_revision");
    }

    #[test]
    fn server_sequence_rejects_hello_order_and_duplicates() {
        let err = validate_server_message_sequence(&[
            ServerMessage::Patch {
                target: "root".to_string(),
                html: "<p>1</p>".to_string(),
                revision: 1,
            },
            valid_hello(),
        ])
        .expect_err("hello must be first");
        assert_eq!(err.code, "hello_not_first");

        let err = validate_server_message_sequence(&[valid_hello(), valid_hello()])
            .expect_err("duplicate hello should fail");
        assert_eq!(err.code, "duplicate_hello");

        assert!(validate_server_message_sequence(&[
            valid_hello(),
            ServerMessage::Patch {
                target: "root".to_string(),
                html: "<p>1</p>".to_string(),
                revision: 1,
            },
            ServerMessage::Diff {
                target: "root".to_string(),
                revision: 2,
                slots: vec![],
            },
        ])
        .is_ok());
    }

    #[test]
    fn hello_fallback_requires_reason() {
        let message = ServerMessage::Hello {
            session_id: "sid".to_string(),
            target: "root".to_string(),
            revision: 0,
            protocol: PROTOCOL_VERSION_V1.to_string(),
            server_revision: None,
            resume_status: Some(ResumeStatus::Fallback),
            resume_reason: None,
            resume_token: Some("token".to_string()),
            resume_expires_in_ms: Some(120_000),
        };
        let err =
            validate_server_message_invariants(&message).expect_err("missing reason should fail");
        assert_eq!(err.code, "missing_resume_reason");
    }

    #[test]
    fn descriptors_map_to_expected_axes() {
        let client = ClientMessage::Event {
            event: "save".to_string(),
            target: None,
            value: json!({"id": 1}),
            metadata: serde_json::Map::new(),
        };
        let server = ServerMessage::Patch {
            target: "root".to_string(),
            html: "<p>ok</p>".to_string(),
            revision: 1,
        };
        let c = describe_client_message(&client);
        let s = describe_server_message(&server);
        assert_eq!(c.direction, ProtocolDirection::ClientToServer);
        assert_eq!(c.authority, ProtocolAuthority::UntrustedClient);
        assert_eq!(c.class, ProtocolInstructionClass::Event);
        assert_eq!(s.direction, ProtocolDirection::ServerToClient);
        assert_eq!(s.authority, ProtocolAuthority::TrustedServer);
        assert_eq!(s.render_effect, ProtocolRenderEffect::Patch);
    }

    #[test]
    fn describe_client_message_covers_every_instruction_class_branch() {
        let descriptors = vec![
            describe_client_message(&ClientMessage::Connect {
                protocol: PROTOCOL_VERSION_V1.to_string(),
                session_id: None,
                last_revision: None,
                resume_token: None,
                tenant_id: None,
                trace_id: None,
                span_id: None,
                parent_span_id: None,
                correlation_id: None,
                request_id: None,
            }),
            describe_client_message(&ClientMessage::Event {
                event: "save".to_string(),
                target: None,
                value: json!({"ok": true}),
                metadata: serde_json::Map::new(),
            }),
            describe_client_message(&ClientMessage::Ping { nonce: None }),
            describe_client_message(&ClientMessage::PatchUrl {
                to: "/users".to_string(),
            }),
            describe_client_message(&ClientMessage::Navigate {
                to: "/users/1".to_string(),
            }),
            describe_client_message(&ClientMessage::UploadStart {
                upload_id: "u1".to_string(),
                event: "upload".to_string(),
                target: None,
                name: "avatar.png".to_string(),
                size: 1,
                content_type: Some("image/png".to_string()),
            }),
            describe_client_message(&ClientMessage::UploadChunk {
                upload_id: "u1".to_string(),
                offset: 0,
                data: "AA==".to_string(),
            }),
            describe_client_message(&ClientMessage::UploadComplete {
                upload_id: "u1".to_string(),
            }),
        ];

        assert!(descriptors.iter().all(|descriptor| {
            descriptor.direction == ProtocolDirection::ClientToServer
                && descriptor.authority == ProtocolAuthority::UntrustedClient
                && descriptor.ordering == ProtocolOrdering::PerSessionOrdered
        }));
        assert!(descriptors
            .iter()
            .any(|descriptor| descriptor.class == ProtocolInstructionClass::Lifecycle));
        assert!(descriptors
            .iter()
            .any(|descriptor| descriptor.class == ProtocolInstructionClass::Event));
        assert!(descriptors
            .iter()
            .any(|descriptor| descriptor.class == ProtocolInstructionClass::Diagnostics));
        assert!(descriptors
            .iter()
            .any(|descriptor| descriptor.class == ProtocolInstructionClass::Navigation));
        assert!(descriptors
            .iter()
            .any(|descriptor| descriptor.class == ProtocolInstructionClass::Upload));
    }

    #[test]
    fn describe_server_message_and_invariants_cover_remaining_variant_matrix() {
        let messages = vec![
            valid_hello(),
            ServerMessage::Patch {
                target: "root".to_string(),
                html: "<p>ok</p>".to_string(),
                revision: 1,
            },
            ServerMessage::Diff {
                target: "root".to_string(),
                revision: 2,
                slots: vec![crate::DynamicSlotPatch {
                    index: 0,
                    html: "ok".to_string(),
                }],
            },
            ServerMessage::StreamInsert {
                target: "items".to_string(),
                id: "item-1".to_string(),
                html: "<li>One</li>".to_string(),
                at: crate::StreamPosition::Append,
            },
            ServerMessage::StreamDelete {
                target: "items".to_string(),
                id: "item-1".to_string(),
            },
            ServerMessage::StreamBatch {
                target: "items".to_string(),
                operations: vec![
                    crate::StreamBatchOperation::Insert {
                        id: "item-2".to_string(),
                        html: "<li>Two</li>".to_string(),
                        at: crate::StreamPosition::Append,
                    },
                    crate::StreamBatchOperation::Delete {
                        id: "item-1".to_string(),
                    },
                ],
            },
            ServerMessage::ChartSeriesAppend {
                chart: "throughput".to_string(),
                series: "p95".to_string(),
                point: crate::ChartPoint { x: 1.0, y: 2.0 },
            },
            ServerMessage::ChartSeriesAppendMany {
                chart: "throughput".to_string(),
                series: "p95".to_string(),
                points: vec![crate::ChartPoint { x: 2.0, y: 3.0 }],
            },
            ServerMessage::ChartSeriesReplace {
                chart: "throughput".to_string(),
                series: "p95".to_string(),
                points: vec![crate::ChartPoint { x: 3.0, y: 4.0 }],
            },
            ServerMessage::ChartReset {
                chart: "throughput".to_string(),
            },
            ServerMessage::ChartAnnotationUpsert {
                chart: "throughput".to_string(),
                annotation: crate::ChartAnnotation {
                    id: "a-1".to_string(),
                    x: 4.0,
                    label: "deploy".to_string(),
                },
            },
            ServerMessage::ChartAnnotationDelete {
                chart: "throughput".to_string(),
                id: "a-1".to_string(),
            },
            ServerMessage::ToastPush {
                toast: crate::Toast {
                    id: "toast-1".to_string(),
                    level: crate::ToastLevel::Info,
                    title: Some("Info".to_string()),
                    message: "ok".to_string(),
                    ttl_ms: Some(2_000),
                },
            },
            ServerMessage::ToastDismiss {
                id: "toast-1".to_string(),
            },
            ServerMessage::InboxUpsert {
                item: crate::InboxItem {
                    id: "inbox-1".to_string(),
                    title: "Ready".to_string(),
                    body: "Done".to_string(),
                    read: false,
                    inserted_at: None,
                },
            },
            ServerMessage::InboxDelete {
                id: "inbox-1".to_string(),
            },
            ServerMessage::GridReplace {
                grid: "accounts".to_string(),
                state: crate::GridState {
                    columns: vec![],
                    rows: vec![],
                    total_rows: 0,
                    offset: 0,
                    limit: 25,
                    views: vec![],
                    active_view: None,
                    group_by: None,
                    query: None,
                    sort: None,
                },
            },
            ServerMessage::GridRowsReplace {
                grid: "accounts".to_string(),
                window: crate::GridRowsWindow {
                    offset: 0,
                    total_rows: 0,
                    rows: vec![],
                },
            },
            ServerMessage::InteropDispatch {
                dispatch: crate::JsInteropDispatch {
                    target: Some("#root".to_string()),
                    event: "interop:event".to_string(),
                    detail: json!({"k": "v"}),
                    bubbles: true,
                },
            },
            ServerMessage::Pong { nonce: None },
            ServerMessage::Redirect {
                to: "/dashboard".to_string(),
            },
            ServerMessage::PatchUrl {
                to: "/dashboard?page=2".to_string(),
            },
            ServerMessage::Navigate {
                to: "/settings".to_string(),
            },
            ServerMessage::UploadProgress {
                upload_id: "u1".to_string(),
                received: 1,
                total: 2,
            },
            ServerMessage::UploadComplete {
                upload_id: "u1".to_string(),
                name: "avatar.png".to_string(),
                size: 2,
                content_type: Some("image/png".to_string()),
            },
            ServerMessage::UploadError {
                upload_id: "u1".to_string(),
                message: "rejected".to_string(),
                code: Some("too_large".to_string()),
            },
            ServerMessage::Error {
                message: "oops".to_string(),
                code: Some("runtime".to_string()),
            },
        ];

        for message in &messages {
            let descriptor = describe_server_message(message);
            assert_eq!(descriptor.direction, ProtocolDirection::ServerToClient);
            assert_eq!(descriptor.authority, ProtocolAuthority::TrustedServer);
            assert_eq!(descriptor.ordering, ProtocolOrdering::PerSessionOrdered);
            assert!(validate_server_message_invariants(message).is_ok());
        }
    }
}
