//! Shared subscription-multiplexer wire contract.
//!
//! This protocol is distinct from the single-Worker [`crate::Method`] / [`crate::Event`]
//! protocol. A connection carries many independently managed subscriptions. The
//! connection endpoint owns authorization and maps the typed selectors below to
//! visible resources; clients cannot provide Workspace scope as selector input.

use std::collections::HashSet;
use std::fmt;

use serde::{Deserialize, Deserializer, Serialize};

use crate::Event as WorkerProtocolEvent;

pub const SUBSCRIPTION_PROTOCOL_VERSION: u16 = 1;
pub const MAX_CORRELATION_ID_BYTES: usize = 128;
pub const MAX_RESOURCE_ID_BYTES: usize = 256;
pub const MAX_WORKER_IDS_PER_SELECTOR: usize = 256;
pub const MAX_REJECTION_MESSAGE_BYTES: usize = 1024;

#[derive(Debug, Clone, PartialEq, Eq)]
pub enum SubscriptionValidationError {
    UnsupportedProtocolVersion {
        actual: u16,
    },
    EmptyIdentifier {
        field: &'static str,
    },
    IdentifierTooLong {
        field: &'static str,
        max_bytes: usize,
    },
    InvalidIdentifier {
        field: &'static str,
    },
    EmptyWorkerSelection,
    TooManyWorkers {
        max: usize,
    },
    DuplicateWorkerId {
        worker_id: String,
    },
    EmptyRejectionMessage,
    RejectionMessageTooLong {
        max_bytes: usize,
    },
    SelectorSnapshotMismatch,
    SelectorEventMismatch,
    UnselectedWorker {
        worker_id: String,
    },
}

impl fmt::Display for SubscriptionValidationError {
    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
        match self {
            Self::UnsupportedProtocolVersion { actual } => write!(
                formatter,
                "unsupported subscription protocol version {actual}; expected {SUBSCRIPTION_PROTOCOL_VERSION}"
            ),
            Self::EmptyIdentifier { field } => write!(formatter, "{field} must not be empty"),
            Self::IdentifierTooLong { field, max_bytes } => {
                write!(formatter, "{field} exceeds {max_bytes} bytes")
            }
            Self::InvalidIdentifier { field } => write!(
                formatter,
                "{field} must be trimmed and must not contain control characters"
            ),
            Self::EmptyWorkerSelection => {
                formatter.write_str("worker_lifecycle requires at least one worker id")
            }
            Self::TooManyWorkers { max } => {
                write!(formatter, "worker selector exceeds {max} worker ids")
            }
            Self::DuplicateWorkerId { worker_id } => {
                write!(
                    formatter,
                    "worker selector contains duplicate id {worker_id:?}"
                )
            }
            Self::EmptyRejectionMessage => {
                formatter.write_str("subscription rejection message must not be empty")
            }
            Self::RejectionMessageTooLong { max_bytes } => write!(
                formatter,
                "subscription rejection message exceeds {max_bytes} bytes"
            ),
            Self::SelectorSnapshotMismatch => {
                formatter.write_str("subscription snapshot does not match its selector")
            }
            Self::SelectorEventMismatch => {
                formatter.write_str("subscription event does not match its selector")
            }
            Self::UnselectedWorker { worker_id } => {
                write!(formatter, "worker {worker_id:?} is outside the selector")
            }
        }
    }
}

impl std::error::Error for SubscriptionValidationError {}

macro_rules! bounded_identifier {
    ($name:ident, $field:literal, $max:expr) => {
        #[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
        #[cfg_attr(feature = "typescript", derive(ts_rs::TS))]
        pub struct $name(String);

        impl $name {
            pub fn new(value: impl Into<String>) -> Result<Self, SubscriptionValidationError> {
                let value = value.into();
                validate_identifier($field, &value, $max)?;
                Ok(Self(value))
            }

            pub fn as_str(&self) -> &str {
                &self.0
            }

            pub fn into_inner(self) -> String {
                self.0
            }

            pub fn validate(&self) -> Result<(), SubscriptionValidationError> {
                validate_identifier($field, &self.0, $max)
            }
        }

        impl AsRef<str> for $name {
            fn as_ref(&self) -> &str {
                self.as_str()
            }
        }

        impl fmt::Display for $name {
            fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
                formatter.write_str(self.as_str())
            }
        }
    };
}

bounded_identifier!(
    SubscriptionRequestId,
    "request_id",
    MAX_CORRELATION_ID_BYTES
);
bounded_identifier!(SubscriptionId, "subscription_id", MAX_CORRELATION_ID_BYTES);
bounded_identifier!(SubscriptionWorkerId, "worker_id", MAX_RESOURCE_ID_BYTES);
bounded_identifier!(
    SubscriptionWorkdirId,
    "working_directory_id",
    MAX_RESOURCE_ID_BYTES
);

fn validate_identifier(
    field: &'static str,
    value: &str,
    max_bytes: usize,
) -> Result<(), SubscriptionValidationError> {
    if value.is_empty() {
        return Err(SubscriptionValidationError::EmptyIdentifier { field });
    }
    if value.len() > max_bytes {
        return Err(SubscriptionValidationError::IdentifierTooLong { field, max_bytes });
    }
    if value.trim() != value || value.chars().any(char::is_control) {
        return Err(SubscriptionValidationError::InvalidIdentifier { field });
    }
    Ok(())
}

fn validate_rejection_message(message: &str) -> Result<(), SubscriptionValidationError> {
    if message.is_empty() {
        return Err(SubscriptionValidationError::EmptyRejectionMessage);
    }
    if message.len() > MAX_REJECTION_MESSAGE_BYTES {
        return Err(SubscriptionValidationError::RejectionMessageTooLong {
            max_bytes: MAX_REJECTION_MESSAGE_BYTES,
        });
    }
    Ok(())
}

/// A bounded, canonical worker-id set. Ordering on the wire and in equality/hash
/// semantics is lexical, so equivalent selectors aggregate to one upstream key.
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize)]
#[cfg_attr(feature = "typescript", derive(ts_rs::TS))]
pub struct SubscriptionWorkerIds(Vec<SubscriptionWorkerId>);

impl SubscriptionWorkerIds {
    pub fn new(
        worker_ids: impl IntoIterator<Item = SubscriptionWorkerId>,
    ) -> Result<Self, SubscriptionValidationError> {
        let mut worker_ids = worker_ids.into_iter().collect::<Vec<_>>();
        if worker_ids.is_empty() {
            return Err(SubscriptionValidationError::EmptyWorkerSelection);
        }
        if worker_ids.len() > MAX_WORKER_IDS_PER_SELECTOR {
            return Err(SubscriptionValidationError::TooManyWorkers {
                max: MAX_WORKER_IDS_PER_SELECTOR,
            });
        }
        for worker_id in &worker_ids {
            worker_id.validate()?;
        }
        worker_ids.sort_unstable();
        for pair in worker_ids.windows(2) {
            if pair[0] == pair[1] {
                return Err(SubscriptionValidationError::DuplicateWorkerId {
                    worker_id: pair[0].to_string(),
                });
            }
        }
        Ok(Self(worker_ids))
    }

    pub fn as_slice(&self) -> &[SubscriptionWorkerId] {
        &self.0
    }

    pub fn contains(&self, worker_id: &SubscriptionWorkerId) -> bool {
        self.0.binary_search(worker_id).is_ok()
    }

    pub fn validate(&self) -> Result<(), SubscriptionValidationError> {
        Self::new(self.0.clone()).map(|_| ())
    }
}

impl<'de> Deserialize<'de> for SubscriptionWorkerIds {
    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
    where
        D: Deserializer<'de>,
    {
        let worker_ids = Vec::<SubscriptionWorkerId>::deserialize(deserializer)?;
        Self::new(worker_ids).map_err(serde::de::Error::custom)
    }
}

#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[cfg_attr(feature = "typescript", derive(ts_rs::TS))]
#[serde(tag = "topic", rename_all = "snake_case")]
pub enum EventSubscriptionSelector {
    RuntimeWorkers,
    WorkerLifecycle {
        worker_ids: SubscriptionWorkerIds,
    },
    WorkerProtocol {
        worker_id: SubscriptionWorkerId,
        #[serde(default, skip_serializing_if = "Option::is_none")]
        runtime_id: Option<String>,
    },
    /// Server-derived Workspace projection. Workspace identity comes from the
    /// authenticated connection and is deliberately absent from this selector.
    WorkspaceWorkers,
    /// Typed extension point for the later Workdir subscription slice.
    WorkspaceWorkdirs,
}

impl EventSubscriptionSelector {
    pub fn validate(&self) -> Result<(), SubscriptionValidationError> {
        match self {
            Self::WorkerLifecycle { worker_ids } => worker_ids.validate(),
            Self::WorkerProtocol {
                worker_id,
                runtime_id,
            } => {
                worker_id.validate()?;
                if let Some(runtime_id) = runtime_id {
                    validate_identifier("runtime_id", runtime_id, MAX_RESOURCE_ID_BYTES)?;
                }
                Ok(())
            }
            Self::RuntimeWorkers | Self::WorkspaceWorkers | Self::WorkspaceWorkdirs => Ok(()),
        }
    }

    pub fn selects_worker(&self, worker_id: &SubscriptionWorkerId) -> bool {
        match self {
            Self::RuntimeWorkers | Self::WorkspaceWorkers => true,
            Self::WorkerLifecycle { worker_ids } => worker_ids.contains(worker_id),
            Self::WorkerProtocol {
                worker_id: selected,
                ..
            } => selected == worker_id,
            Self::WorkspaceWorkdirs => false,
        }
    }
}

#[derive(Debug, Clone, Serialize, Deserialize)]
#[cfg_attr(feature = "typescript", derive(ts_rs::TS))]
pub struct SubscriptionFrame {
    pub protocol_version: u16,
    #[serde(flatten)]
    pub payload: SubscriptionFramePayload,
}

impl SubscriptionFrame {
    pub fn new(payload: SubscriptionFramePayload) -> Self {
        Self {
            protocol_version: SUBSCRIPTION_PROTOCOL_VERSION,
            payload,
        }
    }

    pub fn validate(&self) -> Result<(), SubscriptionValidationError> {
        if self.protocol_version != SUBSCRIPTION_PROTOCOL_VERSION {
            return Err(SubscriptionValidationError::UnsupportedProtocolVersion {
                actual: self.protocol_version,
            });
        }
        self.payload.validate()
    }
}

/// Direction is explicit at the outer envelope, while concrete message kinds
/// remain typed within each lane.
#[derive(Debug, Clone, Serialize, Deserialize)]
#[cfg_attr(feature = "typescript", derive(ts_rs::TS))]
#[serde(tag = "frame", content = "message", rename_all = "snake_case")]
pub enum SubscriptionFramePayload {
    Request(SubscriptionRequest),
    Response(SubscriptionResponse),
    Event(SubscriptionEvent),
    WorkerProtocol(SubscriptionWorkerProtocolMethod),
}

impl SubscriptionFramePayload {
    pub fn validate(&self) -> Result<(), SubscriptionValidationError> {
        match self {
            Self::Request(request) => request.validate(),
            Self::Response(response) => response.validate(),
            Self::Event(event) => event.validate(),
            Self::WorkerProtocol(message) => message.validate(),
        }
    }
}

#[derive(Debug, Clone, Serialize, Deserialize)]
#[cfg_attr(feature = "typescript", derive(ts_rs::TS))]
pub struct SubscriptionWorkerProtocolMethod {
    pub subscription_id: SubscriptionId,
    pub method: crate::Method,
}

impl SubscriptionWorkerProtocolMethod {
    pub fn validate(&self) -> Result<(), SubscriptionValidationError> {
        self.subscription_id.validate()
    }
}

#[derive(Debug, Clone, Serialize, Deserialize)]
#[cfg_attr(feature = "typescript", derive(ts_rs::TS))]
#[serde(tag = "method", content = "params", rename_all = "snake_case")]
pub enum SubscriptionRequest {
    SubscribeEvents {
        request_id: SubscriptionRequestId,
        selector: EventSubscriptionSelector,
    },
    UnsubscribeEvents {
        request_id: SubscriptionRequestId,
        subscription_id: SubscriptionId,
    },
}

impl SubscriptionRequest {
    pub fn validate(&self) -> Result<(), SubscriptionValidationError> {
        match self {
            Self::SubscribeEvents {
                request_id,
                selector,
            } => {
                request_id.validate()?;
                selector.validate()
            }
            Self::UnsubscribeEvents {
                request_id,
                subscription_id,
            } => {
                request_id.validate()?;
                subscription_id.validate()
            }
        }
    }
}

#[derive(Debug, Clone, Serialize, Deserialize)]
#[cfg_attr(feature = "typescript", derive(ts_rs::TS))]
#[serde(tag = "result", content = "payload", rename_all = "snake_case")]
pub enum SubscriptionResponse {
    Subscribed {
        request_id: SubscriptionRequestId,
        subscription_id: SubscriptionId,
        selector: EventSubscriptionSelector,
        snapshot_revision: u64,
        snapshot: SubscriptionSnapshot,
    },
    Unsubscribed {
        request_id: SubscriptionRequestId,
        subscription_id: SubscriptionId,
    },
    SubscriptionRejected {
        request_id: SubscriptionRequestId,
        #[serde(default, skip_serializing_if = "Option::is_none")]
        subscription_id: Option<SubscriptionId>,
        code: SubscriptionRejectionCode,
        message: String,
    },
}

impl SubscriptionResponse {
    pub fn validate(&self) -> Result<(), SubscriptionValidationError> {
        match self {
            Self::Subscribed {
                request_id,
                subscription_id,
                selector,
                snapshot,
                ..
            } => {
                request_id.validate()?;
                subscription_id.validate()?;
                selector.validate()?;
                snapshot.validate_for_selector(selector)
            }
            Self::Unsubscribed {
                request_id,
                subscription_id,
            } => {
                request_id.validate()?;
                subscription_id.validate()
            }
            Self::SubscriptionRejected {
                request_id,
                subscription_id,
                message,
                ..
            } => {
                request_id.validate()?;
                if let Some(subscription_id) = subscription_id {
                    subscription_id.validate()?;
                }
                validate_rejection_message(message)
            }
        }
    }
}

#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[cfg_attr(feature = "typescript", derive(ts_rs::TS))]
#[serde(rename_all = "snake_case")]
pub enum SubscriptionRejectionCode {
    InvalidRequest,
    UnsupportedProtocolVersion,
    UnsupportedSelector,
    Unauthorized,
    ResourceNotFound,
    CapacityExceeded,
    Internal,
}

#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[cfg_attr(feature = "typescript", derive(ts_rs::TS))]
#[serde(rename_all = "snake_case")]
pub enum SubscriptionTerminationCode {
    Lagged,
    ResourceGone,
    Unauthorized,
    ServerShutdown,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
#[cfg_attr(feature = "typescript", derive(ts_rs::TS))]
#[serde(tag = "event", content = "data", rename_all = "snake_case")]
pub enum SubscriptionEvent {
    Event {
        subscription_id: SubscriptionId,
        subject_revision: u64,
        payload: SubscriptionEventPayload,
    },
    SubscriptionClosed {
        subscription_id: SubscriptionId,
        code: SubscriptionTerminationCode,
        message: String,
    },
}

impl SubscriptionEvent {
    pub fn subscription_id(&self) -> &SubscriptionId {
        match self {
            Self::Event {
                subscription_id, ..
            }
            | Self::SubscriptionClosed {
                subscription_id, ..
            } => subscription_id,
        }
    }

    pub fn validate(&self) -> Result<(), SubscriptionValidationError> {
        match self {
            Self::Event {
                subscription_id,
                payload,
                ..
            } => {
                subscription_id.validate()?;
                payload.validate()
            }
            Self::SubscriptionClosed {
                subscription_id,
                message,
                ..
            } => {
                subscription_id.validate()?;
                validate_rejection_message(message)
            }
        }
    }

    pub fn validate_for_selector(
        &self,
        selector: &EventSubscriptionSelector,
    ) -> Result<(), SubscriptionValidationError> {
        self.validate()?;
        match self {
            Self::Event { payload, .. } => payload.validate_for_selector(selector),
            Self::SubscriptionClosed { .. } => Ok(()),
        }
    }
}

#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[cfg_attr(feature = "typescript", derive(ts_rs::TS))]
#[serde(rename_all = "snake_case")]
pub enum SubscriptionWorkerState {
    Idle,
    Running,
    Paused,
    Stopped,
    Cancelled,
}

#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[cfg_attr(feature = "typescript", derive(ts_rs::TS))]
pub struct SubscriptionWorker {
    pub worker_id: SubscriptionWorkerId,
    /// Set by the Workspace Server when projecting a Runtime-owned Worker to clients.
    /// Runtime producers leave this unset because the connection identifies the Runtime.
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub runtime_id: Option<String>,
    /// Workspace-scoped canonical resource key. Runtime producers leave this unset;
    /// Workspace-facing projections must populate it before publishing the Worker.
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub resource_key: Option<String>,
    /// Producer-owned monotonic revision for this Worker subject.
    pub subject_revision: u64,
    pub state: SubscriptionWorkerState,
    #[serde(default)]
    pub has_running_internal_workers: bool,
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub workspace_id: Option<String>,
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub display_name: Option<String>,
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub profile: Option<String>,
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub repository_id: Option<String>,
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub working_directory_id: Option<SubscriptionWorkdirId>,
}

impl SubscriptionWorker {
    pub fn validate(&self) -> Result<(), SubscriptionValidationError> {
        self.worker_id.validate()?;
        if let Some(runtime_id) = &self.runtime_id {
            validate_identifier("runtime_id", runtime_id, MAX_RESOURCE_ID_BYTES)?;
        }
        if let Some(resource_key) = &self.resource_key {
            validate_identifier("resource_key", resource_key, MAX_RESOURCE_ID_BYTES)?;
        }
        if let Some(repository_id) = &self.repository_id {
            validate_identifier("repository_id", repository_id, MAX_RESOURCE_ID_BYTES)?;
        }
        if let Some(working_directory_id) = &self.working_directory_id {
            working_directory_id.validate()?;
        }
        Ok(())
    }
}

#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[cfg_attr(feature = "typescript", derive(ts_rs::TS))]
pub struct SubscriptionWorkdir {
    pub working_directory_id: SubscriptionWorkdirId,
    pub repository_id: String,
    pub state: String,
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub primary_worker_id: Option<SubscriptionWorkerId>,
}

impl SubscriptionWorkdir {
    pub fn validate(&self) -> Result<(), SubscriptionValidationError> {
        self.working_directory_id.validate()?;
        validate_identifier("repository_id", &self.repository_id, MAX_RESOURCE_ID_BYTES)?;
        validate_identifier("workdir_state", &self.state, MAX_RESOURCE_ID_BYTES)?;
        if let Some(worker_id) = &self.primary_worker_id {
            worker_id.validate()?;
        }
        Ok(())
    }
}

#[derive(Debug, Clone, Serialize, Deserialize)]
#[cfg_attr(feature = "typescript", derive(ts_rs::TS))]
#[serde(tag = "topic", content = "data", rename_all = "snake_case")]
pub enum SubscriptionSnapshot {
    Workers {
        workers: Vec<SubscriptionWorker>,
    },
    WorkerProtocol {
        worker_id: SubscriptionWorkerId,
        events: Vec<WorkerProtocolEvent>,
    },
    WorkspaceWorkdirs {
        workdirs: Vec<SubscriptionWorkdir>,
    },
}

impl SubscriptionSnapshot {
    pub fn validate_for_selector(
        &self,
        selector: &EventSubscriptionSelector,
    ) -> Result<(), SubscriptionValidationError> {
        selector.validate()?;
        match (selector, self) {
            (
                EventSubscriptionSelector::RuntimeWorkers
                | EventSubscriptionSelector::WorkspaceWorkers,
                Self::Workers { workers },
            ) => validate_workers(workers),
            (
                EventSubscriptionSelector::WorkerLifecycle { worker_ids },
                Self::Workers { workers },
            ) => {
                validate_workers(workers)?;
                for worker in workers {
                    if !worker_ids.contains(&worker.worker_id) {
                        return Err(SubscriptionValidationError::UnselectedWorker {
                            worker_id: worker.worker_id.to_string(),
                        });
                    }
                }
                Ok(())
            }
            (
                EventSubscriptionSelector::WorkerProtocol {
                    worker_id: selected,
                    ..
                },
                Self::WorkerProtocol { worker_id, .. },
            ) if selected == worker_id => worker_id.validate(),
            (
                EventSubscriptionSelector::WorkspaceWorkdirs,
                Self::WorkspaceWorkdirs { workdirs },
            ) => {
                for workdir in workdirs {
                    workdir.validate()?;
                }
                Ok(())
            }
            _ => Err(SubscriptionValidationError::SelectorSnapshotMismatch),
        }
    }
}

#[derive(Debug, Clone, Serialize, Deserialize)]
#[cfg_attr(feature = "typescript", derive(ts_rs::TS))]
#[serde(tag = "event", content = "data", rename_all = "snake_case")]
pub enum SubscriptionEventPayload {
    WorkerUpserted {
        worker: SubscriptionWorker,
    },
    WorkerRemoved {
        worker_id: SubscriptionWorkerId,
        #[serde(default, skip_serializing_if = "Option::is_none")]
        runtime_id: Option<String>,
    },
    WorkerProtocol {
        worker_id: SubscriptionWorkerId,
        event: WorkerProtocolEvent,
    },
    WorkdirUpserted {
        workdir: SubscriptionWorkdir,
    },
    WorkdirRemoved {
        working_directory_id: SubscriptionWorkdirId,
    },
}

impl SubscriptionEventPayload {
    pub fn validate(&self) -> Result<(), SubscriptionValidationError> {
        match self {
            Self::WorkerUpserted { worker } => worker.validate(),
            Self::WorkerRemoved {
                worker_id,
                runtime_id,
            } => {
                worker_id.validate()?;
                if let Some(runtime_id) = runtime_id {
                    validate_identifier("runtime_id", runtime_id, MAX_RESOURCE_ID_BYTES)?;
                }
                Ok(())
            }
            Self::WorkerProtocol { worker_id, .. } => worker_id.validate(),
            Self::WorkdirUpserted { workdir } => workdir.validate(),
            Self::WorkdirRemoved {
                working_directory_id,
            } => working_directory_id.validate(),
        }
    }

    pub fn validate_for_selector(
        &self,
        selector: &EventSubscriptionSelector,
    ) -> Result<(), SubscriptionValidationError> {
        selector.validate()?;
        self.validate()?;
        match (selector, self) {
            (
                EventSubscriptionSelector::RuntimeWorkers
                | EventSubscriptionSelector::WorkspaceWorkers,
                Self::WorkerUpserted { .. } | Self::WorkerRemoved { .. },
            ) => Ok(()),
            (
                EventSubscriptionSelector::WorkerLifecycle { worker_ids },
                Self::WorkerUpserted { worker },
            ) if worker_ids.contains(&worker.worker_id) => Ok(()),
            (
                EventSubscriptionSelector::WorkerLifecycle { worker_ids },
                Self::WorkerRemoved { worker_id, .. },
            ) if worker_ids.contains(worker_id) => Ok(()),
            (
                EventSubscriptionSelector::WorkerProtocol {
                    worker_id: selected,
                    ..
                },
                Self::WorkerProtocol { worker_id, .. },
            ) if selected == worker_id => Ok(()),
            (
                EventSubscriptionSelector::WorkspaceWorkdirs,
                Self::WorkdirUpserted { .. } | Self::WorkdirRemoved { .. },
            ) => Ok(()),
            (
                EventSubscriptionSelector::WorkerLifecycle { .. },
                Self::WorkerUpserted { worker },
            ) => Err(SubscriptionValidationError::UnselectedWorker {
                worker_id: worker.worker_id.to_string(),
            }),
            (
                EventSubscriptionSelector::WorkerLifecycle { .. },
                Self::WorkerRemoved { worker_id, .. },
            ) => Err(SubscriptionValidationError::UnselectedWorker {
                worker_id: worker_id.to_string(),
            }),
            _ => Err(SubscriptionValidationError::SelectorEventMismatch),
        }
    }
}

fn validate_workers(workers: &[SubscriptionWorker]) -> Result<(), SubscriptionValidationError> {
    let mut seen = HashSet::with_capacity(workers.len());
    for worker in workers {
        worker.validate()?;
        if !seen.insert((worker.runtime_id.as_deref(), &worker.worker_id)) {
            return Err(SubscriptionValidationError::DuplicateWorkerId {
                worker_id: worker.worker_id.to_string(),
            });
        }
    }
    Ok(())
}

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

    fn worker_id(value: &str) -> SubscriptionWorkerId {
        SubscriptionWorkerId::new(value).unwrap()
    }

    fn request_id() -> SubscriptionRequestId {
        SubscriptionRequestId::new("request-1").unwrap()
    }

    fn subscription_id() -> SubscriptionId {
        SubscriptionId::new("subscription-1").unwrap()
    }

    fn worker(value: &str) -> SubscriptionWorker {
        SubscriptionWorker {
            worker_id: worker_id(value),
            runtime_id: None,
            resource_key: None,
            subject_revision: 0,
            state: SubscriptionWorkerState::Idle,
            has_running_internal_workers: false,
            workspace_id: Some("workspace-1".to_string()),
            display_name: Some(format!("Worker {value}")),
            profile: Some("builtin:coder".to_string()),
            repository_id: None,
            working_directory_id: None,
        }
    }

    #[test]
    fn subscribe_frame_has_stable_versioned_json_shape() {
        let frame = SubscriptionFrame::new(SubscriptionFramePayload::Request(
            SubscriptionRequest::SubscribeEvents {
                request_id: request_id(),
                selector: EventSubscriptionSelector::WorkerLifecycle {
                    worker_ids: SubscriptionWorkerIds::new([
                        worker_id("worker-2"),
                        worker_id("worker-1"),
                    ])
                    .unwrap(),
                },
            },
        ));

        let json = serde_json::to_value(&frame).unwrap();
        assert_eq!(
            json,
            serde_json::json!({
                "protocol_version": 1,
                "frame": "request",
                "message": {
                    "method": "subscribe_events",
                    "params": {
                        "request_id": "request-1",
                        "selector": {
                            "topic": "worker_lifecycle",
                            "worker_ids": ["worker-1", "worker-2"]
                        }
                    }
                }
            })
        );
        frame.validate().unwrap();
        let decoded: SubscriptionFrame = serde_json::from_value(json).unwrap();
        decoded.validate().unwrap();
    }

    #[test]
    fn subscribed_snapshot_round_trips_and_matches_selector() {
        let frame = SubscriptionFrame::new(SubscriptionFramePayload::Response(
            SubscriptionResponse::Subscribed {
                request_id: request_id(),
                subscription_id: subscription_id(),
                selector: EventSubscriptionSelector::RuntimeWorkers,
                snapshot_revision: 7,
                snapshot: SubscriptionSnapshot::Workers {
                    workers: vec![worker("worker-1")],
                },
            },
        ));

        frame.validate().unwrap();
        let json = serde_json::to_string(&frame).unwrap();
        let decoded: SubscriptionFrame = serde_json::from_str(&json).unwrap();
        decoded.validate().unwrap();
    }

    #[test]
    fn worker_selector_is_canonical_and_rejects_duplicates() {
        let first = SubscriptionWorkerIds::new([worker_id("b"), worker_id("a")]).unwrap();
        let second = SubscriptionWorkerIds::new([worker_id("a"), worker_id("b")]).unwrap();
        assert_eq!(first, second);

        let error = SubscriptionWorkerIds::new([worker_id("a"), worker_id("a")]).unwrap_err();
        assert!(matches!(
            error,
            SubscriptionValidationError::DuplicateWorkerId { .. }
        ));
        assert!(serde_json::from_str::<SubscriptionWorkerIds>("[]").is_err());
    }

    #[test]
    fn validation_rejects_invalid_identifiers_and_versions() {
        assert!(matches!(
            SubscriptionRequestId::new(" "),
            Err(SubscriptionValidationError::InvalidIdentifier { .. })
        ));
        let mut frame = SubscriptionFrame::new(SubscriptionFramePayload::Request(
            SubscriptionRequest::UnsubscribeEvents {
                request_id: request_id(),
                subscription_id: subscription_id(),
            },
        ));
        frame.protocol_version = 2;
        assert!(matches!(
            frame.validate(),
            Err(SubscriptionValidationError::UnsupportedProtocolVersion { actual: 2 })
        ));
    }

    #[test]
    fn snapshot_and_events_must_match_selector() {
        let selected = EventSubscriptionSelector::WorkerLifecycle {
            worker_ids: SubscriptionWorkerIds::new([worker_id("worker-1")]).unwrap(),
        };
        let snapshot = SubscriptionSnapshot::Workers {
            workers: vec![worker("worker-2")],
        };
        assert!(matches!(
            snapshot.validate_for_selector(&selected),
            Err(SubscriptionValidationError::UnselectedWorker { .. })
        ));

        let event = SubscriptionEvent::Event {
            subscription_id: subscription_id(),
            subject_revision: 8,
            payload: SubscriptionEventPayload::WorkerRemoved {
                worker_id: worker_id("worker-2"),
                runtime_id: None,
            },
        };
        assert!(matches!(
            event.validate_for_selector(&selected),
            Err(SubscriptionValidationError::UnselectedWorker { .. })
        ));
        assert!(matches!(
            event.validate_for_selector(&EventSubscriptionSelector::WorkspaceWorkdirs),
            Err(SubscriptionValidationError::SelectorEventMismatch)
        ));
    }

    #[test]
    fn worker_protocol_method_uses_subscription_lane() {
        let frame = SubscriptionFrame::new(SubscriptionFramePayload::WorkerProtocol(
            SubscriptionWorkerProtocolMethod {
                subscription_id: subscription_id(),
                method: crate::Method::ListCompletions {
                    kind: crate::CompletionKind::File,
                    prefix: "src/".to_string(),
                },
            },
        ));
        frame.validate().unwrap();
        assert_eq!(
            serde_json::to_value(frame).unwrap(),
            serde_json::json!({
                "protocol_version": 1,
                "frame": "worker_protocol",
                "message": {
                    "subscription_id": "subscription-1",
                    "method": {
                        "method": "list_completions",
                        "params": {
                            "kind": "file",
                            "prefix": "src/"
                        }
                    }
                }
            })
        );
    }

    #[test]
    fn workspace_snapshot_allows_equal_local_worker_ids_from_distinct_runtimes() {
        let mut first = worker("1");
        first.runtime_id = Some("runtime-a".to_string());
        let mut second = worker("1");
        second.runtime_id = Some("runtime-b".to_string());
        SubscriptionSnapshot::Workers {
            workers: vec![first, second],
        }
        .validate_for_selector(&EventSubscriptionSelector::WorkspaceWorkers)
        .unwrap();
    }

    #[test]
    fn subscription_closed_is_a_typed_server_event() {
        let frame = SubscriptionFrame::new(SubscriptionFramePayload::Event(
            SubscriptionEvent::SubscriptionClosed {
                subscription_id: subscription_id(),
                code: SubscriptionTerminationCode::Lagged,
                message: "resubscribe".to_string(),
            },
        ));
        frame.validate().unwrap();
        assert_eq!(
            serde_json::to_value(frame).unwrap(),
            serde_json::json!({
                "protocol_version": 1,
                "frame": "event",
                "message": {
                    "event": "subscription_closed",
                    "data": {
                        "subscription_id": "subscription-1",
                        "code": "lagged",
                        "message": "resubscribe"
                    }
                }
            })
        );
    }

    #[test]
    fn client_selector_has_no_workspace_scope_field() {
        let json = serde_json::to_value(EventSubscriptionSelector::WorkspaceWorkers).unwrap();
        assert_eq!(json, serde_json::json!({ "topic": "workspace_workers" }));
        assert!(json.get("workspace_id").is_none());
    }
}
