//! Protobuf Projection Engine
//!
//! This module provides functionality to project SEA semantic graphs to Protocol Buffer
//! (`.proto`) files. It supports:
//!
//! - Entity and Resource projection to Protobuf messages
//! - Type mapping from SEA types to Protobuf scalar types
//! - Deterministic field numbering for schema stability
//! - Governance message generation
//!
//! # Example
//!
//! ```rust,ignore
//! use sea_core::projection::protobuf::ProtobufEngine;
//! use sea_core::graph::Graph;
//!
//! let graph = build_graph_from_model();
//! let proto_file = ProtobufEngine::project(&graph, "my_namespace", "my.package");
//! println!("{}", proto_file.to_proto_string());
//! ```

use crate::graph::Graph;
use crate::primitives::{Entity, Resource};
use serde::{Deserialize, Serialize};
use serde_json::Value;
use std::collections::{BTreeMap, HashMap, HashSet};
use std::path::{Path, PathBuf};

// ============================================================================
// Protobuf IR Types
// ============================================================================

/// Represents a complete `.proto` file.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ProtoFile {
    /// The package name (e.g., "sea.example")
    pub package: String,
    /// Protobuf syntax version (always "proto3")
    pub syntax: String,
    /// Import statements
    pub imports: Vec<String>,
    /// File-level options
    pub options: ProtoOptions,
    /// Enum definitions
    pub enums: Vec<ProtoEnum>,
    /// Message definitions
    pub messages: Vec<ProtoMessage>,
    /// gRPC service definitions
    pub services: Vec<ProtoService>,
    /// Metadata about the projection
    pub metadata: ProtoMetadata,
}

impl ProtoFile {
    /// Create a new ProtoFile with the given package name.
    pub fn new(package: impl Into<String>) -> Self {
        Self {
            package: package.into(),
            syntax: "proto3".to_string(),
            imports: Vec::new(),
            options: ProtoOptions::default(),
            enums: Vec::new(),
            messages: Vec::new(),
            services: Vec::new(),
            metadata: ProtoMetadata::default(),
        }
    }

    /// Serialize the ProtoFile to `.proto` text format.
    pub fn to_proto_string(&self) -> String {
        let mut out = String::new();

        // Header comments
        out.push_str("// Generated by SEA Projection Framework\n");
        out.push_str(&format!(
            "// Projection: {}\n",
            self.metadata.projection_name
        ));
        out.push_str(&format!(
            "// Source Namespace: {}\n",
            self.metadata.source_namespace
        ));
        if let Some(ref version) = self.metadata.semantic_version {
            out.push_str(&format!("// Version: {}\n", version));
        }
        out.push_str(&format!(
            "// Generated At: {}\n",
            self.metadata.generated_at
        ));
        out.push_str("// DO NOT EDIT - This file is auto-generated\n\n");

        // Syntax
        out.push_str(&format!("syntax = \"{}\";\n\n", self.syntax));

        // Package
        out.push_str(&format!("package {};\n", self.package));

        // Options
        let options = &self.options;

        if let Some(ref pkg) = options.java_package {
            out.push_str(&format!("\noption java_package = \"{}\";", pkg));
        }
        if options.java_multiple_files {
            out.push_str("\noption java_multiple_files = true;");
        }
        if let Some(ref pkg) = options.go_package {
            out.push_str(&format!("\noption go_package = \"{}\";", pkg));
        }
        if let Some(ref ns) = options.csharp_namespace {
            out.push_str(&format!("\noption csharp_namespace = \"{}\";", ns));
        }
        if let Some(ref ns) = options.php_namespace {
            out.push_str(&format!("\noption php_namespace = \"{}\";", ns));
        }
        if let Some(ref pkg) = options.ruby_package {
            out.push_str(&format!("\noption ruby_package = \"{}\";", pkg));
        }
        if let Some(ref prefix) = options.swift_prefix {
            out.push_str(&format!("\noption swift_prefix = \"{}\";", prefix));
        }
        if let Some(ref prefix) = options.objc_class_prefix {
            out.push_str(&format!("\noption objc_class_prefix = \"{}\";", prefix));
        }
        if let Some(ref opt) = options.optimize_for {
            out.push_str(&format!("\noption optimize_for = {};", opt));
        }
        if options.deprecated {
            out.push_str("\noption deprecated = true;");
        }

        // Custom options
        for custom in &options.custom_options {
            out.push_str(&format!("\n{}", custom.to_proto_string()));
        }

        if options.java_package.is_some()
            || options.java_multiple_files
            || options.go_package.is_some()
            || options.csharp_namespace.is_some()
            || options.php_namespace.is_some()
            || options.ruby_package.is_some()
            || options.swift_prefix.is_some()
            || options.objc_class_prefix.is_some()
            || options.optimize_for.is_some()
            || options.deprecated
            || !options.custom_options.is_empty()
        {
            out.push('\n');
        }

        // Imports
        if !self.imports.is_empty() {
            out.push('\n');
            for import in &self.imports {
                out.push_str(&format!("import \"{}\";\n", import));
            }
        }

        // Enums
        for e in &self.enums {
            out.push('\n');
            out.push_str(&e.to_proto_string());
        }

        // Messages
        for m in &self.messages {
            out.push('\n');
            out.push_str(&m.to_proto_string());
        }

        // Services (gRPC)
        for s in &self.services {
            out.push('\n');
            out.push_str(&s.to_proto_string());
        }

        out
    }

    /// Scan all messages and automatically add required Well-Known Type imports.
    ///
    /// This method should be called after all messages are added to ensure
    /// proper imports are included for any WKT fields.
    pub fn add_wkt_imports(&mut self) {
        use std::collections::HashSet;
        let mut required_imports: HashSet<&'static str> = HashSet::new();

        // Scan all message fields for WKT references
        for msg in &self.messages {
            Self::collect_wkt_imports_from_message(msg, &mut required_imports);
        }

        // Add imports that aren't already present
        for import in required_imports {
            if !self.imports.contains(&import.to_string()) {
                self.imports.push(import.to_string());
            }
        }

        // Sort imports for deterministic output
        self.imports.sort();
    }

    fn collect_wkt_imports_from_message(
        msg: &ProtoMessage,
        imports: &mut std::collections::HashSet<&'static str>,
    ) {
        for field in &msg.fields {
            if let ProtoType::Message(ref type_name) = field.proto_type {
                if let Some(wkt) = WellKnownType::from_type_name(type_name) {
                    imports.insert(wkt.import_path());
                }
            }
        }

        // Recurse into nested messages
        for nested in &msg.nested_messages {
            Self::collect_wkt_imports_from_message(nested, imports);
        }
    }
}

/// File-level Protobuf options.
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct ProtoOptions {
    /// Java package for generated code
    pub java_package: Option<String>,
    /// Generate separate files for each message in Java
    pub java_multiple_files: bool,
    /// Go package path
    pub go_package: Option<String>,
    /// C# namespace
    pub csharp_namespace: Option<String>,
    /// PHP namespace
    pub php_namespace: Option<String>,
    /// Ruby package
    pub ruby_package: Option<String>,
    /// Swift prefix
    pub swift_prefix: Option<String>,
    /// Objective-C class prefix
    pub objc_class_prefix: Option<String>,
    /// Optimize for: SPEED, CODE_SIZE, or LITE_RUNTIME
    pub optimize_for: Option<String>,
    /// Mark all messages as deprecated
    pub deprecated: bool,
    /// Custom options (user-defined or extension options)
    pub custom_options: Vec<ProtoCustomOption>,
}

impl ProtoOptions {
    /// Set a standard option by name.
    pub fn set_option(&mut self, name: &str, value: ProtoOptionValue) {
        match name {
            "java_package" => {
                if let ProtoOptionValue::String(s) = value {
                    self.java_package = Some(s);
                }
            }
            "java_multiple_files" => {
                if let ProtoOptionValue::Bool(b) = value {
                    self.java_multiple_files = b;
                }
            }
            "go_package" => {
                if let ProtoOptionValue::String(s) = value {
                    self.go_package = Some(s);
                }
            }
            "csharp_namespace" => {
                if let ProtoOptionValue::String(s) = value {
                    self.csharp_namespace = Some(s);
                }
            }
            "php_namespace" => {
                if let ProtoOptionValue::String(s) = value {
                    self.php_namespace = Some(s);
                }
            }
            "ruby_package" => {
                if let ProtoOptionValue::String(s) = value {
                    self.ruby_package = Some(s);
                }
            }
            "swift_prefix" => {
                if let ProtoOptionValue::String(s) = value {
                    self.swift_prefix = Some(s);
                }
            }
            "objc_class_prefix" => {
                if let ProtoOptionValue::String(s) = value {
                    self.objc_class_prefix = Some(s);
                }
            }
            "optimize_for" => {
                if let ProtoOptionValue::String(s) = value {
                    self.optimize_for = Some(s);
                }
            }
            "deprecated" => {
                if let ProtoOptionValue::Bool(b) = value {
                    self.deprecated = b;
                }
            }
            _ => {
                // Unknown option, add as custom
                self.custom_options.push(ProtoCustomOption {
                    name: name.to_string(),
                    value,
                });
            }
        }
    }
}

/// A custom proto option (user-defined or extension).
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct ProtoCustomOption {
    /// Option name (e.g., "java_package" or "(myopt).field")
    pub name: String,
    /// Option value
    pub value: ProtoOptionValue,
}

impl ProtoCustomOption {
    /// Create a new custom option.
    pub fn new(name: impl Into<String>, value: ProtoOptionValue) -> Self {
        Self {
            name: name.into(),
            value,
        }
    }

    /// Serialize to proto option string.
    pub fn to_proto_string(&self) -> String {
        format!("option {} = {};", self.name, self.value.to_proto_string())
    }
}

/// Value for a proto option.
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub enum ProtoOptionValue {
    /// String value
    String(String),
    /// Integer value
    Int(i64),
    /// Float value
    Float(f64),
    /// Boolean value
    Bool(bool),
    /// Identifier/enum value (unquoted)
    Identifier(String),
}

impl ProtoOptionValue {
    /// Parse option value from JSON Value.
    pub fn from_json(value: &serde_json::Value) -> Self {
        match value {
            serde_json::Value::String(s) => ProtoOptionValue::String(s.clone()),
            serde_json::Value::Bool(b) => ProtoOptionValue::Bool(*b),
            serde_json::Value::Number(n) => {
                if let Some(i) = n.as_i64() {
                    ProtoOptionValue::Int(i)
                } else if let Some(f) = n.as_f64() {
                    ProtoOptionValue::Float(f)
                } else {
                    ProtoOptionValue::String(n.to_string())
                }
            }
            _ => ProtoOptionValue::String(value.to_string()),
        }
    }

    /// Serialize to proto option value string.
    pub fn to_proto_string(&self) -> String {
        match self {
            ProtoOptionValue::String(s) => {
                format!("\"{}\"", s.replace('\\', "\\\\").replace('"', "\\\""))
            }
            ProtoOptionValue::Int(i) => i.to_string(),
            ProtoOptionValue::Float(f) => f.to_string(),
            ProtoOptionValue::Bool(b) => b.to_string(),
            ProtoOptionValue::Identifier(s) => s.clone(),
        }
    }
}

/// Metadata about the projection source.
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct ProtoMetadata {
    /// Name of the projection that generated this file
    pub projection_name: String,
    /// Semantic version if available
    pub semantic_version: Option<String>,
    /// Source namespace from the SEA model
    pub source_namespace: String,
    /// Timestamp of generation
    pub generated_at: String,
}

// ============================================================================
// gRPC Service Types
// ============================================================================

/// Represents a gRPC service definition.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ProtoService {
    /// Service name (e.g., "PaymentProcessorService")
    pub name: String,
    /// RPC methods in this service
    pub methods: Vec<ProtoRpcMethod>,
    /// Documentation comments
    pub comments: Vec<String>,
}

impl ProtoService {
    /// Create a new ProtoService with the given name.
    pub fn new(name: impl Into<String>) -> Self {
        Self {
            name: name.into(),
            methods: Vec::new(),
            comments: Vec::new(),
        }
    }

    /// Serialize the service to `.proto` text format.
    pub fn to_proto_string(&self) -> String {
        let mut out = String::new();

        // Comments
        for comment in &self.comments {
            out.push_str(&format!("// {}\n", comment));
        }

        out.push_str(&format!("service {} {{\n", self.name));

        for method in &self.methods {
            out.push_str(&format!("  {}\n", method.to_proto_string()));
        }

        out.push_str("}\n");
        out
    }
}

/// Represents an RPC method in a gRPC service.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ProtoRpcMethod {
    /// Method name (e.g., "ProcessPayment")
    pub name: String,
    /// Request message type
    pub request_type: String,
    /// Response message type
    pub response_type: String,
    /// Streaming mode
    pub streaming: StreamingMode,
    /// Documentation comments
    pub comments: Vec<String>,
}

impl ProtoRpcMethod {
    /// Create a new unary RPC method.
    pub fn new(
        name: impl Into<String>,
        request_type: impl Into<String>,
        response_type: impl Into<String>,
    ) -> Self {
        Self {
            name: name.into(),
            request_type: request_type.into(),
            response_type: response_type.into(),
            streaming: StreamingMode::Unary,
            comments: Vec::new(),
        }
    }

    /// Serialize the method to `.proto` text format.
    pub fn to_proto_string(&self) -> String {
        let request = match self.streaming {
            StreamingMode::ClientStreaming | StreamingMode::Bidirectional => {
                format!("stream {}", self.request_type)
            }
            _ => self.request_type.clone(),
        };

        let response = match self.streaming {
            StreamingMode::ServerStreaming | StreamingMode::Bidirectional => {
                format!("stream {}", self.response_type)
            }
            _ => self.response_type.clone(),
        };

        format!("rpc {}({}) returns ({});", self.name, request, response)
    }
}

/// gRPC streaming mode for RPC methods.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
pub enum StreamingMode {
    /// Unary RPC (default): single request, single response
    #[default]
    Unary,
    /// Server streaming: single request, stream of responses
    ServerStreaming,
    /// Client streaming: stream of requests, single response
    ClientStreaming,
    /// Bidirectional streaming: stream of requests, stream of responses
    Bidirectional,
}

impl StreamingMode {
    /// Parse streaming mode from a string (e.g., from Flow attributes).
    pub fn parse(s: &str) -> Self {
        match s.to_lowercase().as_str() {
            "streaming" | "server_streaming" | "serverstreaming" => StreamingMode::ServerStreaming,
            "client_streaming" | "clientstreaming" => StreamingMode::ClientStreaming,
            "bidirectional" | "bidi" | "duplex" => StreamingMode::Bidirectional,
            _ => StreamingMode::Unary,
        }
    }
}

impl std::fmt::Display for StreamingMode {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        match self {
            StreamingMode::Unary => write!(f, "unary"),
            StreamingMode::ServerStreaming => write!(f, "server_streaming"),
            StreamingMode::ClientStreaming => write!(f, "client_streaming"),
            StreamingMode::Bidirectional => write!(f, "bidirectional"),
        }
    }
}

/// Represents a Protobuf message definition.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ProtoMessage {
    /// Message name (PascalCase)
    pub name: String,
    /// Field definitions
    pub fields: Vec<ProtoField>,
    /// Nested message definitions
    pub nested_messages: Vec<ProtoMessage>,
    /// Nested enum definitions
    pub nested_enums: Vec<ProtoEnum>,
    /// Reserved field numbers
    pub reserved_numbers: Vec<u32>,
    /// Reserved field names
    pub reserved_names: Vec<String>,
    /// Documentation comments
    pub comments: Vec<String>,
}

impl ProtoMessage {
    /// Create a new empty ProtoMessage.
    pub fn new(name: impl Into<String>) -> Self {
        Self {
            name: name.into(),
            fields: Vec::new(),
            nested_messages: Vec::new(),
            nested_enums: Vec::new(),
            reserved_numbers: Vec::new(),
            reserved_names: Vec::new(),
            comments: Vec::new(),
        }
    }

    /// Serialize the message to `.proto` text format.
    pub fn to_proto_string(&self) -> String {
        let mut out = String::new();

        // Comments
        for comment in &self.comments {
            out.push_str(&format!("// {}\n", comment));
        }

        out.push_str(&format!("message {} {{\n", self.name));

        // Reserved fields
        if !self.reserved_numbers.is_empty() {
            let nums: Vec<String> = self
                .reserved_numbers
                .iter()
                .map(|n| n.to_string())
                .collect();
            out.push_str(&format!("  reserved {};\n", nums.join(", ")));
        }
        if !self.reserved_names.is_empty() {
            let names: Vec<String> = self
                .reserved_names
                .iter()
                .map(|n| format!("\"{}\"", n))
                .collect();
            out.push_str(&format!("  reserved {};\n", names.join(", ")));
        }

        // Nested enums
        for e in &self.nested_enums {
            for line in e.to_proto_string().lines() {
                out.push_str(&format!("  {}\n", line));
            }
        }

        // Nested messages
        for m in &self.nested_messages {
            for line in m.to_proto_string().lines() {
                out.push_str(&format!("  {}\n", line));
            }
        }

        // Fields
        for field in &self.fields {
            out.push_str(&format!("  {}\n", field.to_proto_string()));
        }

        out.push_str("}\n");
        out
    }
}

/// Represents a Protobuf field definition.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ProtoField {
    /// Field name (snake_case)
    pub name: String,
    /// Field number (must be unique within message)
    pub number: u32,
    /// Field type
    pub proto_type: ProtoType,
    /// Whether this is a repeated field
    pub repeated: bool,
    /// Whether this field is optional (proto3 optional)
    pub optional: bool,
    /// Documentation comments
    pub comments: Vec<String>,
}

impl ProtoField {
    /// Serialize the field to `.proto` text format.
    pub fn to_proto_string(&self) -> String {
        let mut parts = Vec::new();

        // Comments as inline
        if !self.comments.is_empty() {
            // We'll add comment at the end
        }

        if self.optional {
            parts.push("optional".to_string());
        }
        if self.repeated {
            parts.push("repeated".to_string());
        }

        parts.push(self.proto_type.to_proto_string());
        parts.push(self.name.clone());

        let mut line = format!("{} = {};", parts.join(" "), self.number);

        if !self.comments.is_empty() {
            line.push_str(&format!(" // {}", self.comments.join("; ")));
        }

        line
    }
}

/// Represents a Protobuf type reference.
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub enum ProtoType {
    /// Scalar types (int32, string, etc.)
    Scalar(ScalarType),
    /// Reference to another message type
    Message(String),
    /// Reference to an enum type
    Enum(String),
    /// Map type (map<key, value>)
    Map {
        key: Box<ProtoType>,
        value: Box<ProtoType>,
    },
}

impl ProtoType {
    /// Serialize the type to `.proto` text format.
    pub fn to_proto_string(&self) -> String {
        match self {
            ProtoType::Scalar(s) => s.to_proto_string(),
            ProtoType::Message(name) => name.clone(),
            ProtoType::Enum(name) => name.clone(),
            ProtoType::Map { key, value } => {
                format!(
                    "map<{}, {}>",
                    key.to_proto_string(),
                    value.to_proto_string()
                )
            }
        }
    }
}

/// Protobuf scalar types.
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
pub enum ScalarType {
    Double,
    Float,
    Int32,
    Int64,
    Uint32,
    Uint64,
    Sint32,
    Sint64,
    Fixed32,
    Fixed64,
    Sfixed32,
    Sfixed64,
    Bool,
    String,
    Bytes,
}

impl ScalarType {
    /// Serialize the scalar type to `.proto` text format.
    pub fn to_proto_string(&self) -> String {
        match self {
            ScalarType::Double => "double",
            ScalarType::Float => "float",
            ScalarType::Int32 => "int32",
            ScalarType::Int64 => "int64",
            ScalarType::Uint32 => "uint32",
            ScalarType::Uint64 => "uint64",
            ScalarType::Sint32 => "sint32",
            ScalarType::Sint64 => "sint64",
            ScalarType::Fixed32 => "fixed32",
            ScalarType::Fixed64 => "fixed64",
            ScalarType::Sfixed32 => "sfixed32",
            ScalarType::Sfixed64 => "sfixed64",
            ScalarType::Bool => "bool",
            ScalarType::String => "string",
            ScalarType::Bytes => "bytes",
        }
        .to_string()
    }
}

/// Represents a Protobuf enum definition.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ProtoEnum {
    /// Enum name (PascalCase)
    pub name: String,
    /// Enum values
    pub values: Vec<ProtoEnumValue>,
    /// Documentation comments
    pub comments: Vec<String>,
}

impl ProtoEnum {
    /// Create a new ProtoEnum with default UNSPECIFIED value.
    pub fn new(name: impl Into<String>) -> Self {
        let name = name.into();
        Self {
            values: vec![ProtoEnumValue {
                name: format!("{}_UNSPECIFIED", to_screaming_snake_case(&name)),
                number: 0,
            }],
            name,
            comments: Vec::new(),
        }
    }

    /// Add a value to the enum.
    pub fn add_value(&mut self, name: impl Into<String>) {
        let number = self.values.len() as i32;
        self.values.push(ProtoEnumValue {
            name: name.into(),
            number,
        });
    }

    /// Serialize the enum to `.proto` text format.
    pub fn to_proto_string(&self) -> String {
        let mut out = String::new();

        for comment in &self.comments {
            out.push_str(&format!("// {}\n", comment));
        }

        out.push_str(&format!("enum {} {{\n", self.name));

        for value in &self.values {
            out.push_str(&format!("  {} = {};\n", value.name, value.number));
        }

        out.push_str("}\n");
        out
    }
}

/// Represents a Protobuf enum value.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ProtoEnumValue {
    /// Value name (SCREAMING_SNAKE_CASE)
    pub name: String,
    /// Numeric value
    pub number: i32,
}

// ============================================================================
// Well-Known Types
// ============================================================================

/// Google Protobuf Well-Known Types.
///
/// These are standard types provided by Google that have special handling
/// in most Protobuf implementations.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub enum WellKnownType {
    /// google.protobuf.Timestamp - for date/time values
    Timestamp,
    /// google.protobuf.Duration - for time spans
    Duration,
    /// google.protobuf.Any - for dynamic typing
    Any,
    /// google.protobuf.Struct - for JSON-like structures
    Struct,
    /// google.protobuf.Value - for dynamic JSON values
    Value,
    /// google.protobuf.ListValue - for JSON arrays
    ListValue,
    /// google.protobuf.Empty - for empty messages
    Empty,
    /// Wrapper types for nullable primitives
    Int32Value,
    Int64Value,
    UInt32Value,
    UInt64Value,
    FloatValue,
    DoubleValue,
    BoolValue,
    StringValue,
    BytesValue,
}

impl WellKnownType {
    /// Get the fully qualified type name.
    pub fn type_name(&self) -> &'static str {
        match self {
            WellKnownType::Timestamp => "google.protobuf.Timestamp",
            WellKnownType::Duration => "google.protobuf.Duration",
            WellKnownType::Any => "google.protobuf.Any",
            WellKnownType::Struct => "google.protobuf.Struct",
            WellKnownType::Value => "google.protobuf.Value",
            WellKnownType::ListValue => "google.protobuf.ListValue",
            WellKnownType::Empty => "google.protobuf.Empty",
            WellKnownType::Int32Value => "google.protobuf.Int32Value",
            WellKnownType::Int64Value => "google.protobuf.Int64Value",
            WellKnownType::UInt32Value => "google.protobuf.UInt32Value",
            WellKnownType::UInt64Value => "google.protobuf.UInt64Value",
            WellKnownType::FloatValue => "google.protobuf.FloatValue",
            WellKnownType::DoubleValue => "google.protobuf.DoubleValue",
            WellKnownType::BoolValue => "google.protobuf.BoolValue",
            WellKnownType::StringValue => "google.protobuf.StringValue",
            WellKnownType::BytesValue => "google.protobuf.BytesValue",
        }
    }

    /// Get the import path for this type.
    pub fn import_path(&self) -> &'static str {
        match self {
            WellKnownType::Timestamp => "google/protobuf/timestamp.proto",
            WellKnownType::Duration => "google/protobuf/duration.proto",
            WellKnownType::Any => "google/protobuf/any.proto",
            WellKnownType::Struct | WellKnownType::Value | WellKnownType::ListValue => {
                "google/protobuf/struct.proto"
            }
            WellKnownType::Empty => "google/protobuf/empty.proto",
            WellKnownType::Int32Value
            | WellKnownType::Int64Value
            | WellKnownType::UInt32Value
            | WellKnownType::UInt64Value
            | WellKnownType::FloatValue
            | WellKnownType::DoubleValue
            | WellKnownType::BoolValue
            | WellKnownType::StringValue
            | WellKnownType::BytesValue => "google/protobuf/wrappers.proto",
        }
    }

    /// Try to parse a type name into a WellKnownType.
    pub fn from_type_name(name: &str) -> Option<Self> {
        match name {
            "google.protobuf.Timestamp" => Some(WellKnownType::Timestamp),
            "google.protobuf.Duration" => Some(WellKnownType::Duration),
            "google.protobuf.Any" => Some(WellKnownType::Any),
            "google.protobuf.Struct" => Some(WellKnownType::Struct),
            "google.protobuf.Value" => Some(WellKnownType::Value),
            "google.protobuf.ListValue" => Some(WellKnownType::ListValue),
            "google.protobuf.Empty" => Some(WellKnownType::Empty),
            "google.protobuf.Int32Value" => Some(WellKnownType::Int32Value),
            "google.protobuf.Int64Value" => Some(WellKnownType::Int64Value),
            "google.protobuf.UInt32Value" => Some(WellKnownType::UInt32Value),
            "google.protobuf.UInt64Value" => Some(WellKnownType::UInt64Value),
            "google.protobuf.FloatValue" => Some(WellKnownType::FloatValue),
            "google.protobuf.DoubleValue" => Some(WellKnownType::DoubleValue),
            "google.protobuf.BoolValue" => Some(WellKnownType::BoolValue),
            "google.protobuf.StringValue" => Some(WellKnownType::StringValue),
            "google.protobuf.BytesValue" => Some(WellKnownType::BytesValue),
            _ => None,
        }
    }
}

// ============================================================================
// Type Mapping
// ============================================================================

/// Map SEA type strings to Protobuf types.
///
/// This function handles the common type names used in SEA attributes,
/// including mapping to Google Well-Known Types where appropriate.
pub fn map_sea_type_to_proto(sea_type: &str) -> ProtoType {
    match sea_type.to_lowercase().as_str() {
        // Scalar types
        "string" | "text" | "varchar" => ProtoType::Scalar(ScalarType::String),
        "int" | "integer" | "int64" | "long" => ProtoType::Scalar(ScalarType::Int64),
        "int32" | "short" => ProtoType::Scalar(ScalarType::Int32),
        "uint32" => ProtoType::Scalar(ScalarType::Uint32),
        "uint64" | "ulong" => ProtoType::Scalar(ScalarType::Uint64),
        "float" | "double" | "decimal" | "number" => ProtoType::Scalar(ScalarType::Double),
        "float32" => ProtoType::Scalar(ScalarType::Float),
        "bool" | "boolean" => ProtoType::Scalar(ScalarType::Bool),
        "bytes" | "binary" | "blob" => ProtoType::Scalar(ScalarType::Bytes),
        "uuid" | "guid" => ProtoType::Scalar(ScalarType::String),

        // Well-Known Types
        "date" | "datetime" | "timestamp" => {
            ProtoType::Message(WellKnownType::Timestamp.type_name().to_string())
        }
        "duration" | "timespan" | "interval" => {
            ProtoType::Message(WellKnownType::Duration.type_name().to_string())
        }
        "any" | "dynamic" | "object" => {
            ProtoType::Message(WellKnownType::Any.type_name().to_string())
        }
        "struct" | "json" | "jsonobject" => {
            ProtoType::Message(WellKnownType::Struct.type_name().to_string())
        }
        "value" | "jsonvalue" => ProtoType::Message(WellKnownType::Value.type_name().to_string()),
        "empty" | "void" | "unit" => {
            ProtoType::Message(WellKnownType::Empty.type_name().to_string())
        }

        // Nullable wrapper types
        "optional_int" | "nullable_int" | "int?" => {
            ProtoType::Message(WellKnownType::Int64Value.type_name().to_string())
        }
        "optional_int32" | "nullable_int32" | "int32?" => {
            ProtoType::Message(WellKnownType::Int32Value.type_name().to_string())
        }
        "optional_string" | "nullable_string" | "string?" => {
            ProtoType::Message(WellKnownType::StringValue.type_name().to_string())
        }
        "optional_bool" | "nullable_bool" | "bool?" => {
            ProtoType::Message(WellKnownType::BoolValue.type_name().to_string())
        }
        "optional_double" | "nullable_double" | "double?" => {
            ProtoType::Message(WellKnownType::DoubleValue.type_name().to_string())
        }
        "optional_float" | "nullable_float" | "float?" => {
            ProtoType::Message(WellKnownType::FloatValue.type_name().to_string())
        }
        "optional_bytes" | "nullable_bytes" | "bytes?" => {
            ProtoType::Message(WellKnownType::BytesValue.type_name().to_string())
        }

        // Custom types become messages
        _ => ProtoType::Message(sanitize_proto_ident(sea_type)),
    }
}

/// Infer a ProtoType from a serde_json::Value.
pub fn infer_proto_type_from_value(value: &Value) -> ProtoType {
    match value {
        Value::Null => ProtoType::Scalar(ScalarType::String),
        Value::Bool(_) => ProtoType::Scalar(ScalarType::Bool),
        Value::Number(n) => {
            if n.is_f64() {
                ProtoType::Scalar(ScalarType::Double)
            } else {
                ProtoType::Scalar(ScalarType::Int64)
            }
        }
        Value::String(_) => ProtoType::Scalar(ScalarType::String),
        Value::Array(_) => ProtoType::Scalar(ScalarType::String), // Arrays need special handling
        Value::Object(_) => ProtoType::Scalar(ScalarType::String), // Objects need special handling
    }
}

// ============================================================================
// Protobuf Engine
// ============================================================================

/// The Protobuf projection engine.
///
/// This struct provides methods to convert a SEA Graph into Protobuf IR
/// which can then be serialized to `.proto` text format.
pub struct ProtobufEngine;

impl ProtobufEngine {
    /// Project a SEA Graph to a ProtoFile.
    ///
    /// # Arguments
    ///
    /// * `graph` - The semantic graph to project
    /// * `namespace` - Filter to only include entities/resources from this namespace (empty = all)
    /// * `package` - The Protobuf package name to use
    ///
    /// # Returns
    ///
    /// A `ProtoFile` containing the generated Protobuf IR.
    pub fn project(graph: &Graph, namespace: &str, package: &str) -> ProtoFile {
        let mut proto = ProtoFile::new(package);
        proto.metadata.source_namespace = namespace.to_string();
        proto.metadata.generated_at = std::env::var("SOURCE_DATE_EPOCH")
            .ok()
            .and_then(|epoch| epoch.parse::<i64>().ok())
            .and_then(|epoch| chrono::DateTime::from_timestamp(epoch, 0))
            .map(|dt| dt.to_rfc3339())
            .unwrap_or_default();

        // Convert entities to messages
        for entity in graph.all_entities() {
            if namespace.is_empty() || entity.namespace() == namespace {
                proto.messages.push(Self::entity_to_message(entity));
            }
        }

        // Convert resources to messages
        for resource in graph.all_resources() {
            if namespace.is_empty() || resource.namespace() == namespace {
                proto.messages.push(Self::resource_to_message(resource));
            }
        }

        // Sort messages by name for deterministic output
        proto.messages.sort_by(|a, b| a.name.cmp(&b.name));

        // Auto-detect and add Well-Known Type imports
        proto.add_wkt_imports();

        proto
    }

    /// Project with options for a projection contract.
    pub fn project_with_options(
        graph: &Graph,
        namespace: &str,
        package: &str,
        projection_name: &str,
        include_governance: bool,
    ) -> ProtoFile {
        Self::project_with_full_options(
            graph,
            namespace,
            package,
            projection_name,
            include_governance,
            false, // include_services
        )
    }

    /// Project with all options including gRPC service generation.
    pub fn project_with_full_options(
        graph: &Graph,
        namespace: &str,
        package: &str,
        projection_name: &str,
        include_governance: bool,
        include_services: bool,
    ) -> ProtoFile {
        let mut proto = Self::project(graph, namespace, package);
        proto.metadata.projection_name = projection_name.to_string();

        if include_governance {
            proto.messages.extend(Self::generate_governance_messages());
        }

        if include_services {
            proto.services = Self::flows_to_services(graph, namespace);
            let response_msgs = Self::collect_response_messages(&proto.services);
            proto.messages.extend(response_msgs);
            proto.messages.sort_by(|a, b| a.name.cmp(&b.name));
        }

        // Re-add WKT imports in case governance messages need them
        proto.add_wkt_imports();

        proto
    }

    /// Project a SEA Graph to multiple ProtoFiles (one per namespace).
    ///
    /// This method partitions the graph by namespace, creating a separate `.proto` file
    /// for each. Cross-namespace references are automatically resolved by adding imports
    /// and fully qualifying type names.
    ///
    /// # Arguments
    ///
    /// * `graph` - The semantic graph to project
    /// * `base_package` - The root package name (e.g. "sea.generated")
    /// * `include_governance` - Whether to include governance messages
    /// * `include_services` - Whether to generate gRPC services
    ///
    /// # Returns
    ///
    /// A map of relative file paths to ProtoFile definitions.
    pub fn project_multi_file(
        graph: &Graph,
        base_package: &str,
        include_governance: bool,
        include_services: bool,
    ) -> BTreeMap<PathBuf, ProtoFile> {
        let mut files: BTreeMap<String, ProtoFile> = BTreeMap::new();
        // Index: TypeName -> (Namespace, FullPackage)
        let mut type_index: HashMap<String, (String, String)> = HashMap::new();

        // 1. Collect all namespaces and entities
        for entity in graph.all_entities() {
            let ns = entity.namespace();
            let package = if ns.is_empty() {
                base_package.to_string()
            } else {
                format!("{}.{}", base_package, ns)
            };

            type_index.insert(
                sanitize_proto_ident(entity.name()),
                (ns.to_string(), package.clone()),
            );

            files
                .entry(ns.to_string())
                .or_insert_with(|| ProtoFile::new(package))
                .messages
                .push(Self::entity_to_message(entity));
        }

        for resource in graph.all_resources() {
            let ns = resource.namespace();
            let package = if ns.is_empty() {
                base_package.to_string()
            } else {
                format!("{}.{}", base_package, ns)
            };

            type_index.insert(
                sanitize_proto_ident(resource.name()),
                (ns.to_string(), package.clone()),
            );

            files
                .entry(ns.to_string())
                .or_insert_with(|| ProtoFile::new(package))
                .messages
                .push(Self::resource_to_message(resource));
        }

        // 2. Add Services
        if include_services {
            let services = Self::flows_to_services(graph, "");
            let response_msgs = Self::collect_response_messages(&services);
            for service in services {
                let entity_name = service
                    .name
                    .strip_suffix("Service")
                    .unwrap_or(&service.name);

                if let Some((ns, package)) = type_index.get(entity_name) {
                    files
                        .entry(ns.clone())
                        .or_insert_with(|| ProtoFile::new(package.clone()))
                        .services
                        .push(service);
                } else {
                    let package = base_package.to_string();
                    files
                        .entry("".to_string())
                        .or_insert_with(|| ProtoFile::new(package))
                        .services
                        .push(service);
                }
            }
            for resp in response_msgs {
                type_index.insert(
                    resp.name.clone(),
                    ("".to_string(), base_package.to_string()),
                );
                files
                    .entry("".to_string())
                    .or_insert_with(|| ProtoFile::new(base_package.to_string()))
                    .messages
                    .push(resp);
            }
        }

        // 3. Add Governance (in root namespace usually)
        if include_governance {
            let root_file = files
                .entry("".to_string())
                .or_insert_with(|| ProtoFile::new(base_package.to_string()));

            let governance_msgs = Self::generate_governance_messages();
            for msg in &governance_msgs {
                type_index.insert(msg.name.clone(), ("".to_string(), base_package.to_string()));
            }
            root_file.messages.extend(governance_msgs);
        }

        // 4. Resolve Imports and Finalize
        for (ns, file) in files.iter_mut() {
            let mut imports_to_add = HashSet::new();

            // Check messages for cross-references
            for msg in &mut file.messages {
                Self::resolve_imports_in_message(msg, ns, &type_index, &mut imports_to_add);
            }

            // Check services
            for svc in &mut file.services {
                for method in &mut svc.methods {
                    // Check request types
                    if let Some((target_ns, target_pkg)) = type_index.get(&method.request_type) {
                        if target_ns != ns {
                            imports_to_add.insert(target_ns.clone());
                            method.request_type = format!("{}.{}", target_pkg, method.request_type);
                        }
                    }
                    // Check response types
                    if let Some((target_ns, target_pkg)) = type_index.get(&method.response_type) {
                        if target_ns != ns {
                            imports_to_add.insert(target_ns.clone());
                            method.response_type =
                                format!("{}.{}", target_pkg, method.response_type);
                        }
                    }
                }
            }

            // Add collected imports
            for target_ns in imports_to_add {
                // self-import is prevented by check above
                // root namespace usually doesn't need path prefix if files in same dir,
                // but assume we strictly follow directory structure.
                let import_path = if target_ns.is_empty() {
                    "projection.proto".to_string()
                } else {
                    format!("{}.proto", target_ns.replace('.', "/"))
                };
                file.imports.push(import_path);
            }

            // Sort and deduplicate imports (including WKTs)
            file.add_wkt_imports();
        }

        // Convert map to PathBuf keys
        let mut results = BTreeMap::new();
        for (ns, file) in files {
            let path = if ns.is_empty() {
                PathBuf::from("projection.proto")
            } else {
                PathBuf::from(format!("{}.proto", ns.replace('.', "/")))
            };
            results.insert(path, file);
        }

        results
    }

    /// Helper to resolve cross-namespace imports within a message.
    fn resolve_imports_in_message(
        msg: &mut ProtoMessage,
        current_ns: &str,
        index: &HashMap<String, (String, String)>,
        imports: &mut HashSet<String>,
    ) {
        for field in &mut msg.fields {
            match &mut field.proto_type {
                ProtoType::Message(name) | ProtoType::Enum(name) => {
                    // Ignore WKTs
                    if WellKnownType::from_type_name(name).is_some() {
                        continue;
                    }

                    if let Some((target_ns, target_pkg)) = index.get(name.as_str()) {
                        if target_ns != current_ns {
                            imports.insert(target_ns.clone());
                            *name = format!("{}.{}", target_pkg, name);
                        }
                    }
                }
                ProtoType::Map { key: _, value } => {
                    // Check recursively (simplified for now assuming only value can be message)
                    if let ProtoType::Message(name) = &mut **value {
                        if WellKnownType::from_type_name(name).is_some() {
                            continue;
                        }
                        if let Some((target_ns, target_pkg)) = index.get(name.as_str()) {
                            if target_ns != current_ns {
                                imports.insert(target_ns.clone());
                                *name = format!("{}.{}", target_pkg, name);
                            }
                        }
                    }
                }
                _ => {}
            }
        }

        // Recurse into nested messages
        for nested in &mut msg.nested_messages {
            Self::resolve_imports_in_message(nested, current_ns, index, imports);
        }
    }
    ///
    /// This method groups flows by their destination entity (service provider)
    /// and creates RPC methods for each flow. The naming convention follows
    /// gRPC best practices: `{DestinationEntity}Service`.
    ///
    /// # Example
    ///
    /// A flow `Customer -> PaymentProcessor of PaymentRequest` generates:
    /// ```protobuf
    /// service PaymentProcessorService {
    ///   rpc ProcessPaymentRequest(PaymentRequest) returns (PaymentRequestResponse);
    /// }
    /// ```
    pub fn flows_to_services(graph: &Graph, namespace: &str) -> Vec<ProtoService> {
        let mut services: BTreeMap<String, ProtoService> = BTreeMap::new();

        for flow in graph.all_flows() {
            if !namespace.is_empty() && flow.namespace() != namespace {
                continue;
            }

            let to_entity = match graph.get_entity(flow.to_id()) {
                Some(e) => e,
                None => continue,
            };

            let resource = match graph.get_resource(flow.resource_id()) {
                Some(r) => r,
                None => continue,
            };

            let service_name = format!("{}Service", sanitize_proto_ident(to_entity.name()));
            let method_name = format!("Process{}", sanitize_proto_ident(resource.name()));
            let request_type = sanitize_proto_ident(resource.name());
            let response_type = format!("{}Response", sanitize_proto_ident(resource.name()));

            let streaming = flow
                .get_attribute("streaming")
                .and_then(|v| v.as_str())
                .map(StreamingMode::parse)
                .unwrap_or(StreamingMode::Unary);

            let mut method = ProtoRpcMethod::new(&method_name, &request_type, &response_type);
            method.streaming = streaming;

            if let Some(from_entity) = graph.get_entity(flow.from_id()) {
                method.comments.push(format!(
                    "Flow: {} -> {} of {}",
                    from_entity.name(),
                    to_entity.name(),
                    resource.name()
                ));
            }

            services
                .entry(service_name.clone())
                .or_insert_with(|| {
                    let mut svc = ProtoService::new(&service_name);
                    svc.comments
                        .push(format!("gRPC service for {}", to_entity.name()));
                    svc
                })
                .methods
                .push(method);
        }

        services.into_values().collect()
    }

    pub fn collect_response_messages(services: &[ProtoService]) -> Vec<ProtoMessage> {
        let mut seen: HashSet<String> = HashSet::new();
        let mut messages = Vec::new();
        for service in services {
            for method in &service.methods {
                if !seen.contains(&method.response_type) {
                    seen.insert(method.response_type.clone());
                    let mut msg = ProtoMessage::new(&method.response_type);
                    msg.comments
                        .push(format!("Response message for {}", method.name));
                    msg.fields.push(ProtoField {
                        name: "success".to_string(),
                        number: 1,
                        proto_type: ProtoType::Scalar(ScalarType::Bool),
                        repeated: false,
                        optional: false,
                        comments: vec![],
                    });
                    msg.fields.push(ProtoField {
                        name: "message".to_string(),
                        number: 2,
                        proto_type: ProtoType::Scalar(ScalarType::String),
                        repeated: false,
                        optional: true,
                        comments: vec![],
                    });
                    messages.push(msg);
                }
            }
        }
        messages
    }

    /// Convert an Entity to a ProtoMessage.
    fn entity_to_message(entity: &Entity) -> ProtoMessage {
        let mut msg = ProtoMessage::new(sanitize_proto_ident(entity.name()));
        msg.comments.push(format!("SEA Entity: {}", entity.name()));
        msg.comments
            .push(format!("Namespace: {}", entity.namespace()));

        let mut field_number = 1u32;

        // Add id field (always first)
        msg.fields.push(ProtoField {
            name: "id".to_string(),
            number: field_number,
            proto_type: ProtoType::Scalar(ScalarType::String),
            repeated: false,
            optional: false,
            comments: vec!["Unique identifier".to_string()],
        });
        field_number += 1;

        // Add name field
        msg.fields.push(ProtoField {
            name: "name".to_string(),
            number: field_number,
            proto_type: ProtoType::Scalar(ScalarType::String),
            repeated: false,
            optional: false,
            comments: vec!["Entity name".to_string()],
        });
        field_number += 1;

        // Add attributes sorted alphabetically for deterministic output
        let mut sorted_attrs: BTreeMap<&String, &Value> = BTreeMap::new();
        for (key, value) in entity.attributes() {
            sorted_attrs.insert(key, value);
        }

        for (key, value) in sorted_attrs {
            msg.fields.push(ProtoField {
                name: to_snake_case(key),
                number: field_number,
                proto_type: infer_proto_type_from_value(value),
                repeated: matches!(value, Value::Array(_)),
                optional: true,
                comments: vec![],
            });
            field_number += 1;
        }

        msg
    }

    /// Convert a Resource to a ProtoMessage.
    fn resource_to_message(resource: &Resource) -> ProtoMessage {
        let mut msg = ProtoMessage::new(sanitize_proto_ident(resource.name()));
        msg.comments
            .push(format!("SEA Resource: {}", resource.name()));
        msg.comments
            .push(format!("Unit: {}", resource.unit().symbol()));

        let mut field_number = 1u32;

        // Add id field
        msg.fields.push(ProtoField {
            name: "id".to_string(),
            number: field_number,
            proto_type: ProtoType::Scalar(ScalarType::String),
            repeated: false,
            optional: false,
            comments: vec!["Unique identifier".to_string()],
        });
        field_number += 1;

        // Add name field
        msg.fields.push(ProtoField {
            name: "name".to_string(),
            number: field_number,
            proto_type: ProtoType::Scalar(ScalarType::String),
            repeated: false,
            optional: false,
            comments: vec!["Resource name".to_string()],
        });
        field_number += 1;

        // Add quantity field with unit comment
        msg.fields.push(ProtoField {
            name: "quantity".to_string(),
            number: field_number,
            proto_type: ProtoType::Scalar(ScalarType::Double),
            repeated: false,
            optional: true,
            comments: vec![format!("Quantity in {}", resource.unit().symbol())],
        });
        field_number += 1;

        // Add unit field
        msg.fields.push(ProtoField {
            name: "unit".to_string(),
            number: field_number,
            proto_type: ProtoType::Scalar(ScalarType::String),
            repeated: false,
            optional: false,
            comments: vec!["Unit of measurement".to_string()],
        });
        field_number += 1;

        // Add attributes
        let mut sorted_attrs: BTreeMap<&String, &Value> = BTreeMap::new();
        for (key, value) in resource.attributes() {
            sorted_attrs.insert(key, value);
        }

        for (key, value) in sorted_attrs {
            msg.fields.push(ProtoField {
                name: to_snake_case(key),
                number: field_number,
                proto_type: infer_proto_type_from_value(value),
                repeated: matches!(value, Value::Array(_)),
                optional: true,
                comments: vec![],
            });
            field_number += 1;
        }

        msg
    }

    /// Generate standard governance messages.
    fn generate_governance_messages() -> Vec<ProtoMessage> {
        let mut messages = Vec::new();

        // PolicyViolation message
        let mut violation = ProtoMessage::new("PolicyViolation");
        violation
            .comments
            .push("Represents a policy violation event".to_string());
        violation.fields = vec![
            ProtoField {
                name: "policy_name".to_string(),
                number: 1,
                proto_type: ProtoType::Scalar(ScalarType::String),
                repeated: false,
                optional: false,
                comments: vec!["Name of the violated policy".to_string()],
            },
            ProtoField {
                name: "entity_id".to_string(),
                number: 2,
                proto_type: ProtoType::Scalar(ScalarType::String),
                repeated: false,
                optional: false,
                comments: vec!["ID of the entity that violated the policy".to_string()],
            },
            ProtoField {
                name: "severity".to_string(),
                number: 3,
                proto_type: ProtoType::Scalar(ScalarType::String),
                repeated: false,
                optional: false,
                comments: vec!["Severity level (error, warn, info)".to_string()],
            },
            ProtoField {
                name: "message".to_string(),
                number: 4,
                proto_type: ProtoType::Scalar(ScalarType::String),
                repeated: false,
                optional: false,
                comments: vec!["Human-readable violation message".to_string()],
            },
            ProtoField {
                name: "timestamp".to_string(),
                number: 5,
                proto_type: ProtoType::Scalar(ScalarType::String),
                repeated: false,
                optional: false,
                comments: vec!["When the violation occurred".to_string()],
            },
        ];
        messages.push(violation);

        // MetricEvent message
        let mut metric = ProtoMessage::new("MetricEvent");
        metric
            .comments
            .push("Represents a metric measurement event".to_string());
        metric.fields = vec![
            ProtoField {
                name: "metric_name".to_string(),
                number: 1,
                proto_type: ProtoType::Scalar(ScalarType::String),
                repeated: false,
                optional: false,
                comments: vec!["Name of the metric".to_string()],
            },
            ProtoField {
                name: "value".to_string(),
                number: 2,
                proto_type: ProtoType::Scalar(ScalarType::Double),
                repeated: false,
                optional: false,
                comments: vec!["Measured value".to_string()],
            },
            ProtoField {
                name: "unit".to_string(),
                number: 3,
                proto_type: ProtoType::Scalar(ScalarType::String),
                repeated: false,
                optional: true,
                comments: vec!["Unit of measurement".to_string()],
            },
            ProtoField {
                name: "timestamp".to_string(),
                number: 4,
                proto_type: ProtoType::Scalar(ScalarType::String),
                repeated: false,
                optional: false,
                comments: vec!["When the measurement was taken".to_string()],
            },
        ];
        messages.push(metric);

        messages
    }
}

// ============================================================================
// Helper Functions
// ============================================================================

/// Convert a string to PascalCase.
///
/// Preserves original casing of characters after the first letter of each word,
/// so "PaymentProcessor" stays "PaymentProcessor" (not "Paymentprocessor").
fn to_pascal_case(s: &str) -> String {
    s.split(|c: char| !c.is_alphanumeric())
        .filter(|part| !part.is_empty())
        .map(|part| {
            let mut chars = part.chars();
            match chars.next() {
                Some(first) => first.to_uppercase().to_string() + chars.as_str(),
                None => String::new(),
            }
        })
        .collect()
}

fn sanitize_proto_ident(raw: &str) -> String {
    let pascal = to_pascal_case(raw);
    if pascal.is_empty() {
        return "SeaUnnamed".to_string();
    }
    let mut result = String::new();
    for (i, c) in pascal.chars().enumerate() {
        if i == 0 && !c.is_ascii_alphabetic() && c != '_' {
            result.push_str("Sea");
        }
        if c.is_ascii_alphanumeric() || c == '_' {
            result.push(c);
        }
    }
    if result.is_empty() {
        return "SeaUnnamed".to_string();
    }
    let reserved = [
        "syntax", "package", "message", "service", "rpc", "enum", "import", "option", "returns",
        "stream", "reserved", "bool", "string", "bytes", "double", "float", "int32", "int64",
        "uint32", "uint64", "true", "false",
    ];
    if reserved.contains(&result.to_lowercase().as_str()) {
        result = format!("Sea{}", result);
    }
    result
}

/// Convert a string to snake_case.
fn to_snake_case(s: &str) -> String {
    let mut result = String::new();
    let mut prev_is_uppercase = false;

    for (i, c) in s.chars().enumerate() {
        if c.is_uppercase() {
            if i > 0 && !prev_is_uppercase {
                result.push('_');
            }
            result.push(c.to_lowercase().next().unwrap_or(c));
            prev_is_uppercase = true;
        } else if c.is_alphanumeric() {
            result.push(c);
            prev_is_uppercase = false;
        } else {
            if !result.is_empty() && !result.ends_with('_') {
                result.push('_');
            }
            prev_is_uppercase = false;
        }
    }

    result.trim_matches('_').to_string()
}

/// Convert a string to SCREAMING_SNAKE_CASE.
fn to_screaming_snake_case(s: &str) -> String {
    to_snake_case(s).to_uppercase()
}

pub fn validate_proto_package_namespace(ns: &str) -> Result<(), String> {
    if ns.is_empty() {
        return Ok(());
    }
    if ns.contains('/')
        || ns.contains('\\')
        || ns.contains("..")
        || ns.starts_with('.')
        || ns.starts_with('/')
    {
        return Err(format!(
            "Invalid protobuf namespace '{}': must not contain path separators or traversal",
            ns
        ));
    }
    let mut chars = ns.chars().peekable();
    while let Some(first) = chars.next() {
        if !first.is_ascii_alphabetic() && first != '_' {
            return Err(format!(
                "Invalid protobuf namespace '{}': must be dotted identifiers (e.g., com.example.api)",
                ns
            ));
        }
        loop {
            match chars.peek() {
                Some('.') => {
                    chars.next();
                    break;
                }
                Some(c) if c.is_ascii_alphanumeric() || *c == '_' => {
                    chars.next();
                }
                Some(_) => {
                    return Err(format!(
                        "Invalid protobuf namespace '{}': must be dotted identifiers (e.g., com.example.api)",
                        ns
                    ));
                }
                None => break,
            }
        }
    }
    Ok(())
}

pub fn validate_output_path(output_root: &Path, target: &Path) -> Result<(), String> {
    let canonical_output = output_root.canonicalize().map_err(|e| {
        format!(
            "Cannot resolve output root '{}': {}",
            output_root.display(),
            e
        )
    })?;

    let rel = target
        .strip_prefix(output_root)
        .or_else(|_| target.strip_prefix(&canonical_output))
        .map_err(|_| {
            format!(
                "Security: output path '{}' escapes output directory",
                target.display()
            )
        })?;

    // Lexically reject traversal even when the file does not exist yet.
    if rel.components().any(|c| {
        matches!(
            c,
            std::path::Component::ParentDir
                | std::path::Component::RootDir
                | std::path::Component::Prefix(_)
        )
    }) {
        return Err(format!(
            "Security: output path '{}' escapes output directory",
            target.display()
        ));
    }

    let canonical_target_base = canonical_output.join(rel);
    let target_for_canonical_checks = if target.is_absolute() {
        target
    } else {
        canonical_target_base.as_path()
    };

    if let Some(canonical_parent) = canonical_target_base
        .parent()
        .filter(|p| p.exists())
        .and_then(|p| p.canonicalize().ok())
    {
        if !canonical_parent.starts_with(&canonical_output) {
            return Err(format!(
                "Security: output path '{}' escapes output directory",
                target.display()
            ));
        }
    }

    if let Some(canonical_parent) = target_for_canonical_checks
        .parent()
        .filter(|p| p.exists())
        .and_then(|p| p.canonicalize().ok())
    {
        if !canonical_parent.starts_with(&canonical_output) {
            return Err(format!(
                "Security: output path '{}' escapes output directory",
                target.display()
            ));
        }
    }

    // Best-effort canonicalization for existing paths catches symlink escapes.
    if let Ok(canonical_target) = target_for_canonical_checks.canonicalize() {
        if !canonical_target.starts_with(&canonical_output) {
            return Err(format!(
                "Security: output path '{}' escapes output directory",
                target.display()
            ));
        }
    }

    Ok(())
}

// ============================================================================
// Compatibility Enforcement
// ============================================================================

/// Compatibility mode for schema evolution.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
pub enum CompatibilityMode {
    /// Only additions allowed - strictest mode for public APIs
    Additive,
    /// Removals become reserved fields - default for internal APIs
    #[default]
    Backward,
    /// All changes allowed - for breaking releases
    Breaking,
}

impl std::fmt::Display for CompatibilityMode {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        match self {
            CompatibilityMode::Additive => write!(f, "additive"),
            CompatibilityMode::Backward => write!(f, "backward"),
            CompatibilityMode::Breaking => write!(f, "breaking"),
        }
    }
}

impl std::str::FromStr for CompatibilityMode {
    type Err = String;

    fn from_str(s: &str) -> Result<Self, Self::Err> {
        match s.to_lowercase().as_str() {
            "additive" | "strict" => Ok(CompatibilityMode::Additive),
            "backward" | "backwards" | "default" => Ok(CompatibilityMode::Backward),
            "breaking" | "none" => Ok(CompatibilityMode::Breaking),
            _ => Err(format!("Unknown compatibility mode: {}", s)),
        }
    }
}

/// A compatibility violation found during schema comparison.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CompatibilityViolation {
    /// The message name where the violation occurred
    pub message_name: String,
    /// The field name involved (if applicable)
    pub field_name: Option<String>,
    /// The field number involved (if applicable)
    pub field_number: Option<u32>,
    /// Type of violation
    pub violation_type: ViolationType,
    /// Human-readable description
    pub description: String,
}

/// Types of compatibility violations.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum ViolationType {
    /// A field was removed
    FieldRemoved,
    /// A field number was reused with a different name/type
    FieldNumberReused,
    /// A field type was changed
    FieldTypeChanged,
    /// A field was renamed (same number, different name)
    FieldRenamed,
    /// A message was removed
    MessageRemoved,
    /// A required field was added (breaking in proto3)
    RequiredFieldAdded,
}

impl std::fmt::Display for ViolationType {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        match self {
            ViolationType::FieldRemoved => write!(f, "field_removed"),
            ViolationType::FieldNumberReused => write!(f, "field_number_reused"),
            ViolationType::FieldTypeChanged => write!(f, "field_type_changed"),
            ViolationType::FieldRenamed => write!(f, "field_renamed"),
            ViolationType::MessageRemoved => write!(f, "message_removed"),
            ViolationType::RequiredFieldAdded => write!(f, "required_field_added"),
        }
    }
}

/// Result of a compatibility check.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CompatibilityResult {
    /// Whether the schemas are compatible under the given mode
    pub is_compatible: bool,
    /// The mode used for checking
    pub mode: CompatibilityMode,
    /// List of violations found
    pub violations: Vec<CompatibilityViolation>,
    /// Suggested fixes (reserved fields to add)
    pub suggested_reserved_numbers: BTreeMap<String, Vec<u32>>,
    /// Suggested reserved names
    pub suggested_reserved_names: BTreeMap<String, Vec<String>>,
}

impl CompatibilityResult {
    /// Create a compatible (empty) result.
    pub fn compatible(mode: CompatibilityMode) -> Self {
        Self {
            is_compatible: true,
            mode,
            violations: Vec::new(),
            suggested_reserved_numbers: BTreeMap::new(),
            suggested_reserved_names: BTreeMap::new(),
        }
    }

    /// Check if there are any violations.
    pub fn has_violations(&self) -> bool {
        !self.violations.is_empty()
    }

    /// Format violations as a human-readable report.
    pub fn to_report(&self) -> String {
        let mut out = String::new();
        out.push_str(&format!("Compatibility Check (mode: {})\n", self.mode));
        out.push_str(&format!(
            "Result: {}\n",
            if self.is_compatible { "PASS" } else { "FAIL" }
        ));

        if !self.violations.is_empty() {
            out.push_str(&format!("\nViolations ({}):\n", self.violations.len()));
            for v in &self.violations {
                out.push_str(&format!(
                    "  - [{}] {}: {}\n",
                    v.violation_type, v.message_name, v.description
                ));
            }
        }

        if !self.suggested_reserved_numbers.is_empty() {
            out.push_str("\nSuggested Reserved Fields:\n");
            for (msg, nums) in &self.suggested_reserved_numbers {
                let nums_str: Vec<String> = nums.iter().map(|n| n.to_string()).collect();
                out.push_str(&format!(
                    "  message {}: reserved {};\n",
                    msg,
                    nums_str.join(", ")
                ));
            }
        }

        out
    }
}

/// Schema compatibility checker.
pub struct CompatibilityChecker;

impl CompatibilityChecker {
    /// Check compatibility between an old and new ProtoFile.
    ///
    /// # Arguments
    /// * `old` - The previous schema version
    /// * `new` - The new schema version  
    /// * `mode` - The compatibility mode to enforce
    ///
    /// # Returns
    /// A CompatibilityResult with violations and suggested fixes.
    pub fn check(old: &ProtoFile, new: &ProtoFile, mode: CompatibilityMode) -> CompatibilityResult {
        let mut result = CompatibilityResult::compatible(mode);

        // Build lookup maps for old schema
        let old_messages: BTreeMap<&str, &ProtoMessage> =
            old.messages.iter().map(|m| (m.name.as_str(), m)).collect();

        let new_messages: BTreeMap<&str, &ProtoMessage> =
            new.messages.iter().map(|m| (m.name.as_str(), m)).collect();

        // Check for removed messages
        for name in old_messages.keys() {
            if !new_messages.contains_key(name) {
                result.violations.push(CompatibilityViolation {
                    message_name: name.to_string(),
                    field_name: None,
                    field_number: None,
                    violation_type: ViolationType::MessageRemoved,
                    description: format!("Message '{}' was removed", name),
                });
            }
        }

        // Check each message that exists in both
        for (name, old_msg) in &old_messages {
            if let Some(new_msg) = new_messages.get(name) {
                Self::check_message(old_msg, new_msg, &mut result);
            }
        }

        // Determine if compatible based on mode
        result.is_compatible = match mode {
            CompatibilityMode::Breaking => true, // Always compatible in breaking mode
            CompatibilityMode::Backward => {
                // Compatible if no field number reuse or type changes
                !result.violations.iter().any(|v| {
                    matches!(
                        v.violation_type,
                        ViolationType::FieldNumberReused | ViolationType::FieldTypeChanged
                    )
                })
            }
            CompatibilityMode::Additive => {
                // Any removal or change is incompatible
                result.violations.is_empty()
            }
        };

        result
    }

    /// Check compatibility between two messages.
    fn check_message(old: &ProtoMessage, new: &ProtoMessage, result: &mut CompatibilityResult) {
        // Build field maps by number and by name
        let old_by_number: BTreeMap<u32, &ProtoField> =
            old.fields.iter().map(|f| (f.number, f)).collect();

        let new_by_number: BTreeMap<u32, &ProtoField> =
            new.fields.iter().map(|f| (f.number, f)).collect();

        let old_by_name: BTreeMap<&str, &ProtoField> =
            old.fields.iter().map(|f| (f.name.as_str(), f)).collect();

        // Check for removed fields
        for (number, old_field) in &old_by_number {
            if !new_by_number.contains_key(number) {
                result.violations.push(CompatibilityViolation {
                    message_name: old.name.clone(),
                    field_name: Some(old_field.name.clone()),
                    field_number: Some(*number),
                    violation_type: ViolationType::FieldRemoved,
                    description: format!(
                        "Field '{}' (number {}) was removed",
                        old_field.name, number
                    ),
                });

                // Suggest reserving this field number
                result
                    .suggested_reserved_numbers
                    .entry(old.name.clone())
                    .or_default()
                    .push(*number);

                result
                    .suggested_reserved_names
                    .entry(old.name.clone())
                    .or_default()
                    .push(old_field.name.clone());
            }
        }

        // Check for field number reuse with different name/type
        for (number, new_field) in &new_by_number {
            if let Some(old_field) = old_by_number.get(number) {
                // Check name change
                if old_field.name != new_field.name {
                    result.violations.push(CompatibilityViolation {
                        message_name: old.name.clone(),
                        field_name: Some(new_field.name.clone()),
                        field_number: Some(*number),
                        violation_type: ViolationType::FieldRenamed,
                        description: format!(
                            "Field number {} renamed from '{}' to '{}'",
                            number, old_field.name, new_field.name
                        ),
                    });
                }

                // Check type change
                if old_field.proto_type != new_field.proto_type {
                    result.violations.push(CompatibilityViolation {
                        message_name: old.name.clone(),
                        field_name: Some(new_field.name.clone()),
                        field_number: Some(*number),
                        violation_type: ViolationType::FieldTypeChanged,
                        description: format!(
                            "Field '{}' type changed from {} to {}",
                            new_field.name,
                            old_field.proto_type.to_proto_string(),
                            new_field.proto_type.to_proto_string()
                        ),
                    });
                }
            } else {
                // New field - check if it reuses a previously removed name
                if old_by_name.contains_key(new_field.name.as_str()) {
                    let old_field = old_by_name[new_field.name.as_str()];
                    if old_field.number != *number {
                        result.violations.push(CompatibilityViolation {
                            message_name: old.name.clone(),
                            field_name: Some(new_field.name.clone()),
                            field_number: Some(*number),
                            violation_type: ViolationType::FieldNumberReused,
                            description: format!(
                                "Field '{}' changed number from {} to {}",
                                new_field.name, old_field.number, number
                            ),
                        });
                    }
                }
            }
        }
    }

    /// Apply compatibility fixes to a new ProtoFile based on an old one.
    ///
    /// This adds reserved field numbers and names for removed fields.
    pub fn apply_backward_compatibility(old: &ProtoFile, new: &mut ProtoFile) {
        let result = Self::check(old, new, CompatibilityMode::Backward);

        // Apply suggested reserved numbers
        for msg in &mut new.messages {
            if let Some(reserved_nums) = result.suggested_reserved_numbers.get(&msg.name) {
                for num in reserved_nums {
                    if !msg.reserved_numbers.contains(num) {
                        msg.reserved_numbers.push(*num);
                    }
                }
                msg.reserved_numbers.sort();
            }

            if let Some(reserved_names) = result.suggested_reserved_names.get(&msg.name) {
                for name in reserved_names {
                    if !msg.reserved_names.contains(name) {
                        msg.reserved_names.push(name.clone());
                    }
                }
                msg.reserved_names.sort();
            }
        }
    }
}

/// File-based schema history storage.
pub struct SchemaHistory {
    /// Directory where schema history is stored
    history_dir: std::path::PathBuf,
}

impl SchemaHistory {
    /// Create a new SchemaHistory with the given directory.
    pub fn new(history_dir: impl Into<std::path::PathBuf>) -> Self {
        Self {
            history_dir: history_dir.into(),
        }
    }

    /// Get the path for a schema file.
    fn schema_path(&self, package: &str) -> std::path::PathBuf {
        let filename = format!("{}.json", package.replace('.', "_"));
        self.history_dir.join(filename)
    }

    /// Load the previous schema for a package.
    pub fn load(&self, package: &str) -> Result<Option<ProtoFile>, String> {
        let path = self.schema_path(package);
        if !path.exists() {
            return Ok(None);
        }

        let content = std::fs::read_to_string(&path)
            .map_err(|e| format!("Failed to read schema history: {}", e))?;

        let proto: ProtoFile = serde_json::from_str(&content)
            .map_err(|e| format!("Failed to parse schema history: {}", e))?;

        Ok(Some(proto))
    }

    /// Save a schema to history.
    pub fn save(&self, proto: &ProtoFile) -> Result<(), String> {
        // Ensure directory exists
        std::fs::create_dir_all(&self.history_dir)
            .map_err(|e| format!("Failed to create history directory: {}", e))?;

        let path = self.schema_path(&proto.package);
        let content = serde_json::to_string_pretty(proto)
            .map_err(|e| format!("Failed to serialize schema: {}", e))?;

        std::fs::write(&path, content)
            .map_err(|e| format!("Failed to write schema history: {}", e))?;

        Ok(())
    }

    /// Check compatibility and optionally apply fixes.
    pub fn check_and_update(
        &self,
        new: &mut ProtoFile,
        mode: CompatibilityMode,
        apply_fixes: bool,
    ) -> Result<CompatibilityResult, String> {
        let old = self.load(&new.package)?;

        let result = match old {
            Some(ref old_proto) => {
                if apply_fixes && mode == CompatibilityMode::Backward {
                    CompatibilityChecker::apply_backward_compatibility(old_proto, new);
                }
                CompatibilityChecker::check(old_proto, new, mode)
            }
            None => CompatibilityResult::compatible(mode),
        };

        // Save the new schema if compatible (or in breaking mode)
        if result.is_compatible || mode == CompatibilityMode::Breaking {
            self.save(new)?;
        }

        Ok(result)
    }
}

// ============================================================================
// Tests
// ============================================================================

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

    #[test]
    fn test_to_pascal_case() {
        assert_eq!(to_pascal_case("hello_world"), "HelloWorld");
        assert_eq!(to_pascal_case("my-entity"), "MyEntity");
        assert_eq!(to_pascal_case("already PascalCase"), "AlreadyPascalCase");
        assert_eq!(to_pascal_case("UPPERCASE"), "UPPERCASE");
        assert_eq!(to_pascal_case("PaymentProcessor"), "PaymentProcessor");
    }

    #[test]
    fn test_sanitize_proto_ident() {
        assert_eq!(sanitize_proto_ident("PaymentProcessor"), "PaymentProcessor");
        assert_eq!(sanitize_proto_ident("123Invalid"), "Sea123Invalid");
        assert_eq!(sanitize_proto_ident("message"), "SeaMessage");
        assert_eq!(sanitize_proto_ident("String"), "SeaString");
        assert_eq!(sanitize_proto_ident("hello_world"), "HelloWorld");
        assert_eq!(sanitize_proto_ident(""), "SeaUnnamed");
        assert_eq!(sanitize_proto_ident("rpc"), "SeaRpc");
        assert_eq!(sanitize_proto_ident("service"), "SeaService");
    }

    #[cfg(unix)]
    #[test]
    fn test_validate_output_path_accepts_symlinked_output_root() {
        let tmp = tempfile::tempdir().unwrap();
        let real_output = tmp.path().join("real-output");
        let linked_output = tmp.path().join("linked-output");
        std::fs::create_dir_all(&real_output).unwrap();
        std::os::unix::fs::symlink(&real_output, &linked_output).unwrap();

        let target = linked_output.join("generated.proto");

        assert!(validate_output_path(&linked_output, &target).is_ok());
    }

    #[test]
    fn test_validate_output_path_rejects_traversal() {
        let tmp = tempfile::tempdir().unwrap();
        let output = tmp.path().join("out");
        std::fs::create_dir_all(&output).unwrap();
        let target = output.join("..").join("escape.proto");

        assert!(validate_output_path(&output, &target).is_err());
    }

    #[test]
    fn test_to_snake_case() {
        assert_eq!(to_snake_case("HelloWorld"), "hello_world");
        assert_eq!(to_snake_case("myEntity"), "my_entity");
        assert_eq!(to_snake_case("already_snake"), "already_snake");
        // Consecutive uppercase chars become lowercase without underscore separation
        assert_eq!(to_snake_case("XMLParser"), "xmlparser");
        assert_eq!(to_snake_case("PaymentID"), "payment_id");
    }

    #[test]
    fn test_to_screaming_snake_case() {
        assert_eq!(to_screaming_snake_case("MyEnum"), "MY_ENUM");
        assert_eq!(to_screaming_snake_case("StatusCode"), "STATUS_CODE");
    }

    #[test]
    fn test_map_sea_type_to_proto() {
        assert_eq!(
            map_sea_type_to_proto("string"),
            ProtoType::Scalar(ScalarType::String)
        );
        assert_eq!(
            map_sea_type_to_proto("int"),
            ProtoType::Scalar(ScalarType::Int64)
        );
        assert_eq!(
            map_sea_type_to_proto("boolean"),
            ProtoType::Scalar(ScalarType::Bool)
        );
        assert_eq!(
            map_sea_type_to_proto("timestamp"),
            ProtoType::Message("google.protobuf.Timestamp".to_string())
        );
        assert_eq!(
            map_sea_type_to_proto("CustomType"),
            ProtoType::Message("CustomType".to_string())
        );
    }

    #[test]
    fn test_proto_field_to_string() {
        let field = ProtoField {
            name: "my_field".to_string(),
            number: 1,
            proto_type: ProtoType::Scalar(ScalarType::String),
            repeated: false,
            optional: false,
            comments: vec![],
        };
        assert_eq!(field.to_proto_string(), "string my_field = 1;");

        let optional_field = ProtoField {
            name: "optional_field".to_string(),
            number: 2,
            proto_type: ProtoType::Scalar(ScalarType::Int64),
            repeated: false,
            optional: true,
            comments: vec![],
        };
        assert_eq!(
            optional_field.to_proto_string(),
            "optional int64 optional_field = 2;"
        );

        let repeated_field = ProtoField {
            name: "items".to_string(),
            number: 3,
            proto_type: ProtoType::Scalar(ScalarType::String),
            repeated: true,
            optional: false,
            comments: vec![],
        };
        assert_eq!(
            repeated_field.to_proto_string(),
            "repeated string items = 3;"
        );
    }

    #[test]
    fn test_proto_enum_to_string() {
        let mut e = ProtoEnum::new("Status");
        e.add_value("STATUS_ACTIVE");
        e.add_value("STATUS_INACTIVE");

        let output = e.to_proto_string();
        assert!(output.contains("enum Status {"));
        assert!(output.contains("STATUS_UNSPECIFIED = 0;"));
        assert!(output.contains("STATUS_ACTIVE = 1;"));
        assert!(output.contains("STATUS_INACTIVE = 2;"));
    }

    #[test]
    fn test_proto_message_to_string() {
        let mut msg = ProtoMessage::new("Person");
        msg.fields.push(ProtoField {
            name: "name".to_string(),
            number: 1,
            proto_type: ProtoType::Scalar(ScalarType::String),
            repeated: false,
            optional: false,
            comments: vec![],
        });
        msg.fields.push(ProtoField {
            name: "age".to_string(),
            number: 2,
            proto_type: ProtoType::Scalar(ScalarType::Int32),
            repeated: false,
            optional: true,
            comments: vec![],
        });

        let output = msg.to_proto_string();
        assert!(output.contains("message Person {"));
        assert!(output.contains("string name = 1;"));
        assert!(output.contains("optional int32 age = 2;"));
    }

    #[test]
    fn test_proto_file_to_string() {
        let mut proto = ProtoFile::new("test.package");
        proto.metadata.projection_name = "TestProjection".to_string();
        proto.metadata.source_namespace = "test".to_string();

        let mut msg = ProtoMessage::new("TestMessage");
        msg.fields.push(ProtoField {
            name: "id".to_string(),
            number: 1,
            proto_type: ProtoType::Scalar(ScalarType::String),
            repeated: false,
            optional: false,
            comments: vec![],
        });
        proto.messages.push(msg);

        let output = proto.to_proto_string();
        assert!(output.contains("syntax = \"proto3\";"));
        assert!(output.contains("package test.package;"));
        assert!(output.contains("message TestMessage {"));
        assert!(output.contains("string id = 1;"));
    }

    #[test]
    fn test_entity_to_message() {
        use serde_json::json;

        let mut entity = Entity::new_with_namespace("Warehouse", "logistics");
        entity.set_attribute("capacity", json!(5000));
        entity.set_attribute("location", json!("Building A"));

        let msg = ProtobufEngine::entity_to_message(&entity);

        assert_eq!(msg.name, "Warehouse");
        assert!(msg.fields.iter().any(|f| f.name == "id"));
        assert!(msg.fields.iter().any(|f| f.name == "name"));
        assert!(msg.fields.iter().any(|f| f.name == "capacity"));
        assert!(msg.fields.iter().any(|f| f.name == "location"));

        // Check field numbers are sequential
        let numbers: Vec<u32> = msg.fields.iter().map(|f| f.number).collect();
        assert_eq!(numbers, vec![1, 2, 3, 4]); // id, name, capacity, location (sorted)
    }

    #[test]
    fn test_governance_messages() {
        let messages = ProtobufEngine::generate_governance_messages();
        assert_eq!(messages.len(), 2);

        let violation = messages.iter().find(|m| m.name == "PolicyViolation");
        assert!(violation.is_some());

        let metric = messages.iter().find(|m| m.name == "MetricEvent");
        assert!(metric.is_some());
    }

    // ========================================================================
    // Well-Known Type Tests
    // ========================================================================

    #[test]
    fn test_wkt_type_name() {
        assert_eq!(
            WellKnownType::Timestamp.type_name(),
            "google.protobuf.Timestamp"
        );
        assert_eq!(
            WellKnownType::Duration.type_name(),
            "google.protobuf.Duration"
        );
        assert_eq!(WellKnownType::Any.type_name(), "google.protobuf.Any");
        assert_eq!(WellKnownType::Struct.type_name(), "google.protobuf.Struct");
        assert_eq!(WellKnownType::Empty.type_name(), "google.protobuf.Empty");
        assert_eq!(
            WellKnownType::Int64Value.type_name(),
            "google.protobuf.Int64Value"
        );
    }

    #[test]
    fn test_wkt_import_path() {
        assert_eq!(
            WellKnownType::Timestamp.import_path(),
            "google/protobuf/timestamp.proto"
        );
        assert_eq!(
            WellKnownType::Duration.import_path(),
            "google/protobuf/duration.proto"
        );
        assert_eq!(
            WellKnownType::Any.import_path(),
            "google/protobuf/any.proto"
        );
        assert_eq!(
            WellKnownType::Struct.import_path(),
            "google/protobuf/struct.proto"
        );
        assert_eq!(
            WellKnownType::Value.import_path(),
            "google/protobuf/struct.proto"
        );
        assert_eq!(
            WellKnownType::Empty.import_path(),
            "google/protobuf/empty.proto"
        );
        assert_eq!(
            WellKnownType::Int64Value.import_path(),
            "google/protobuf/wrappers.proto"
        );
        assert_eq!(
            WellKnownType::StringValue.import_path(),
            "google/protobuf/wrappers.proto"
        );
    }

    #[test]
    fn test_wkt_from_type_name() {
        assert_eq!(
            WellKnownType::from_type_name("google.protobuf.Timestamp"),
            Some(WellKnownType::Timestamp)
        );
        assert_eq!(
            WellKnownType::from_type_name("google.protobuf.Duration"),
            Some(WellKnownType::Duration)
        );
        assert_eq!(
            WellKnownType::from_type_name("google.protobuf.Any"),
            Some(WellKnownType::Any)
        );
        assert_eq!(
            WellKnownType::from_type_name("google.protobuf.StringValue"),
            Some(WellKnownType::StringValue)
        );
        assert_eq!(WellKnownType::from_type_name("SomeOtherType"), None);
    }

    #[test]
    fn test_map_sea_type_to_wkt() {
        // Timestamp types
        assert_eq!(
            map_sea_type_to_proto("timestamp"),
            ProtoType::Message("google.protobuf.Timestamp".to_string())
        );
        assert_eq!(
            map_sea_type_to_proto("datetime"),
            ProtoType::Message("google.protobuf.Timestamp".to_string())
        );

        // Duration types
        assert_eq!(
            map_sea_type_to_proto("duration"),
            ProtoType::Message("google.protobuf.Duration".to_string())
        );
        assert_eq!(
            map_sea_type_to_proto("timespan"),
            ProtoType::Message("google.protobuf.Duration".to_string())
        );

        // Dynamic types
        assert_eq!(
            map_sea_type_to_proto("any"),
            ProtoType::Message("google.protobuf.Any".to_string())
        );
        assert_eq!(
            map_sea_type_to_proto("json"),
            ProtoType::Message("google.protobuf.Struct".to_string())
        );

        // Empty type
        assert_eq!(
            map_sea_type_to_proto("void"),
            ProtoType::Message("google.protobuf.Empty".to_string())
        );

        // Nullable/optional types
        assert_eq!(
            map_sea_type_to_proto("optional_string"),
            ProtoType::Message("google.protobuf.StringValue".to_string())
        );
        assert_eq!(
            map_sea_type_to_proto("nullable_int"),
            ProtoType::Message("google.protobuf.Int64Value".to_string())
        );
    }

    #[test]
    fn test_add_wkt_imports() {
        let mut proto = ProtoFile::new("test");

        // Add a message with a Timestamp field
        let mut msg = ProtoMessage::new("Event");
        msg.fields.push(ProtoField {
            name: "created_at".to_string(),
            number: 1,
            proto_type: ProtoType::Message("google.protobuf.Timestamp".to_string()),
            repeated: false,
            optional: false,
            comments: vec![],
        });
        msg.fields.push(ProtoField {
            name: "duration".to_string(),
            number: 2,
            proto_type: ProtoType::Message("google.protobuf.Duration".to_string()),
            repeated: false,
            optional: false,
            comments: vec![],
        });
        proto.messages.push(msg);

        proto.add_wkt_imports();

        assert!(proto
            .imports
            .contains(&"google/protobuf/timestamp.proto".to_string()));
        assert!(proto
            .imports
            .contains(&"google/protobuf/duration.proto".to_string()));
    }

    #[test]
    fn test_resolve_imports_in_message() {
        // Setup index with a target message
        let mut index = HashMap::new();
        index.insert(
            "TargetType".to_string(),
            ("other.ns".to_string(), "base.other.ns".to_string()),
        );

        let mut msg = ProtoMessage::new("SourceMessage");
        msg.fields.push(ProtoField {
            name: "field1".to_string(),
            number: 1,
            proto_type: ProtoType::Message("TargetType".to_string()),
            repeated: false,
            optional: false,
            comments: vec![],
        });

        let mut imports = HashSet::new();

        // Resolve imports
        ProtobufEngine::resolve_imports_in_message(&mut msg, "current.ns", &index, &mut imports);

        // Should find import for other.ns
        assert!(imports.contains("other.ns"));

        // Should update type name to fully qualified
        let field_type = if let ProtoType::Message(ref name) = msg.fields[0].proto_type {
            name.clone()
        } else {
            panic!("Wrong type");
        };
        assert_eq!(field_type, "base.other.ns.TargetType");
    }

    #[test]
    fn test_resolve_imports_in_nested_message() {
        let mut index = HashMap::new();
        index.insert(
            "NestedTarget".to_string(),
            ("other.ns".to_string(), "base.other.ns".to_string()),
        );

        let mut msg = ProtoMessage::new("Outer");
        let mut inner = ProtoMessage::new("Inner");
        inner.fields.push(ProtoField {
            name: "field".to_string(),
            number: 1,
            proto_type: ProtoType::Message("NestedTarget".to_string()),
            repeated: false,
            optional: false,
            comments: vec![],
        });
        msg.nested_messages.push(inner);

        let mut imports = HashSet::new();
        ProtobufEngine::resolve_imports_in_message(&mut msg, "current.ns", &index, &mut imports);

        assert!(imports.contains("other.ns"));
    }

    #[test]
    fn test_wkt_imports_in_proto_string() {
        let mut proto = ProtoFile::new("test.wkt");

        let mut msg = ProtoMessage::new("AuditLog");
        msg.fields.push(ProtoField {
            name: "timestamp".to_string(),
            number: 1,
            proto_type: ProtoType::Message("google.protobuf.Timestamp".to_string()),
            repeated: false,
            optional: false,
            comments: vec![],
        });
        proto.messages.push(msg);
        proto.add_wkt_imports();

        let output = proto.to_proto_string();
        assert!(output.contains("import \"google/protobuf/timestamp.proto\";"));
    }

    #[test]
    fn test_wkt_no_duplicate_imports() {
        let mut proto = ProtoFile::new("test");

        // Add multiple messages using the same WKT
        let mut msg1 = ProtoMessage::new("Event1");
        msg1.fields.push(ProtoField {
            name: "time1".to_string(),
            number: 1,
            proto_type: ProtoType::Message("google.protobuf.Timestamp".to_string()),
            repeated: false,
            optional: false,
            comments: vec![],
        });

        let mut msg2 = ProtoMessage::new("Event2");
        msg2.fields.push(ProtoField {
            name: "time2".to_string(),
            number: 1,
            proto_type: ProtoType::Message("google.protobuf.Timestamp".to_string()),
            repeated: false,
            optional: false,
            comments: vec![],
        });

        proto.messages.push(msg1);
        proto.messages.push(msg2);
        proto.add_wkt_imports();

        // Should only have one timestamp import
        let timestamp_count = proto
            .imports
            .iter()
            .filter(|i| i.contains("timestamp"))
            .count();
        assert_eq!(timestamp_count, 1);
    }

    // ========================================================================
    // Custom Options Tests
    // ========================================================================

    #[test]
    fn test_proto_option_value_string() {
        let val = ProtoOptionValue::String("com.example.api".to_string());
        assert_eq!(val.to_proto_string(), "\"com.example.api\"");
    }

    #[test]
    fn test_proto_option_value_string_escaping() {
        let val = ProtoOptionValue::String("path\\to\\file".to_string());
        assert_eq!(val.to_proto_string(), "\"path\\\\to\\\\file\"");

        let val2 = ProtoOptionValue::String("say \"hello\"".to_string());
        assert_eq!(val2.to_proto_string(), "\"say \\\"hello\\\"\"");
    }

    #[test]
    fn test_proto_option_value_int() {
        let val = ProtoOptionValue::Int(42);
        assert_eq!(val.to_proto_string(), "42");

        let neg = ProtoOptionValue::Int(-100);
        assert_eq!(neg.to_proto_string(), "-100");
    }

    #[test]
    fn test_proto_option_value_float() {
        let val = ProtoOptionValue::Float(3.15);
        assert_eq!(val.to_proto_string(), "3.15");
    }

    #[test]
    fn test_proto_option_value_bool() {
        assert_eq!(ProtoOptionValue::Bool(true).to_proto_string(), "true");
        assert_eq!(ProtoOptionValue::Bool(false).to_proto_string(), "false");
    }

    #[test]
    fn test_proto_option_value_identifier() {
        let val = ProtoOptionValue::Identifier("SPEED".to_string());
        assert_eq!(val.to_proto_string(), "SPEED");
    }

    #[test]
    fn test_proto_custom_option_to_string() {
        let opt = ProtoCustomOption::new(
            "java_package",
            ProtoOptionValue::String("com.example".to_string()),
        );
        assert_eq!(
            opt.to_proto_string(),
            "option java_package = \"com.example\";"
        );
    }

    #[test]
    fn test_proto_custom_option_extension() {
        // Extension option with parentheses
        let opt = ProtoCustomOption::new("(mycompany.api_version)", ProtoOptionValue::Int(2));
        assert_eq!(opt.to_proto_string(), "option (mycompany.api_version) = 2;");
    }

    #[test]
    fn test_proto_options_set_standard_options() {
        let mut opts = ProtoOptions::default();

        opts.set_option(
            "java_package",
            ProtoOptionValue::String("com.example".to_string()),
        );
        opts.set_option("java_multiple_files", ProtoOptionValue::Bool(true));
        opts.set_option(
            "go_package",
            ProtoOptionValue::String("github.com/example".to_string()),
        );
        opts.set_option(
            "csharp_namespace",
            ProtoOptionValue::String("Example.Api".to_string()),
        );
        opts.set_option("deprecated", ProtoOptionValue::Bool(true));

        assert_eq!(opts.java_package, Some("com.example".to_string()));
        assert!(opts.java_multiple_files);
        assert_eq!(opts.go_package, Some("github.com/example".to_string()));
        assert_eq!(opts.csharp_namespace, Some("Example.Api".to_string()));
        assert!(opts.deprecated);
    }

    #[test]
    fn test_proto_options_set_custom_option() {
        let mut opts = ProtoOptions::default();

        opts.set_option(
            "my_custom_option",
            ProtoOptionValue::String("custom_value".to_string()),
        );

        assert_eq!(opts.custom_options.len(), 1);
        assert_eq!(opts.custom_options[0].name, "my_custom_option");
    }

    #[test]
    fn test_proto_file_with_all_options() {
        let mut proto = ProtoFile::new("test.api");
        proto.options.java_package = Some("com.example.api".to_string());
        proto.options.java_multiple_files = true;
        proto.options.go_package = Some("github.com/example/api".to_string());
        proto.options.csharp_namespace = Some("Example.Api".to_string());
        proto.options.optimize_for = Some("SPEED".to_string());
        proto.options.custom_options.push(ProtoCustomOption::new(
            "(api.version)",
            ProtoOptionValue::Int(1),
        ));

        let output = proto.to_proto_string();
        assert!(output.contains("option java_package = \"com.example.api\";"));
        assert!(output.contains("option java_multiple_files = true;"));
        assert!(output.contains("option go_package = \"github.com/example/api\";"));
        assert!(output.contains("option csharp_namespace = \"Example.Api\";"));
        assert!(output.contains("option optimize_for = SPEED;"));
        assert!(output.contains("option (api.version) = 1;"));
    }

    #[test]
    fn test_proto_option_value_from_json() {
        use serde_json::json;

        assert_eq!(
            ProtoOptionValue::from_json(&json!("hello")),
            ProtoOptionValue::String("hello".to_string())
        );
        assert_eq!(
            ProtoOptionValue::from_json(&json!(true)),
            ProtoOptionValue::Bool(true)
        );
        assert_eq!(
            ProtoOptionValue::from_json(&json!(42)),
            ProtoOptionValue::Int(42)
        );
        assert_eq!(
            ProtoOptionValue::from_json(&json!(3.15)),
            ProtoOptionValue::Float(3.15)
        );
    }

    // ========================================================================
    // gRPC Service Tests
    // ========================================================================

    #[test]
    fn test_proto_service_to_string() {
        let mut service = ProtoService::new("PaymentService");
        service
            .comments
            .push("Payment processing service".to_string());

        service.methods.push(ProtoRpcMethod::new(
            "ProcessPayment",
            "PaymentRequest",
            "PaymentResponse",
        ));

        let output = service.to_proto_string();
        assert!(output.contains("service PaymentService {"));
        assert!(output.contains("rpc ProcessPayment(PaymentRequest) returns (PaymentResponse);"));
        assert!(output.contains("// Payment processing service"));
    }

    #[test]
    fn test_proto_rpc_method_unary() {
        let method = ProtoRpcMethod::new("GetUser", "GetUserRequest", "User");
        let output = method.to_proto_string();
        assert_eq!(output, "rpc GetUser(GetUserRequest) returns (User);");
    }

    #[test]
    fn test_proto_rpc_method_server_streaming() {
        let mut method = ProtoRpcMethod::new("ListEvents", "ListEventsRequest", "Event");
        method.streaming = StreamingMode::ServerStreaming;
        let output = method.to_proto_string();
        assert_eq!(
            output,
            "rpc ListEvents(ListEventsRequest) returns (stream Event);"
        );
    }

    #[test]
    fn test_proto_rpc_method_client_streaming() {
        let mut method = ProtoRpcMethod::new("UploadChunks", "DataChunk", "UploadResult");
        method.streaming = StreamingMode::ClientStreaming;
        let output = method.to_proto_string();
        assert_eq!(
            output,
            "rpc UploadChunks(stream DataChunk) returns (UploadResult);"
        );
    }

    #[test]
    fn test_proto_rpc_method_bidirectional() {
        let mut method = ProtoRpcMethod::new("Chat", "ChatMessage", "ChatMessage");
        method.streaming = StreamingMode::Bidirectional;
        let output = method.to_proto_string();
        assert_eq!(
            output,
            "rpc Chat(stream ChatMessage) returns (stream ChatMessage);"
        );
    }

    #[test]
    fn test_streaming_mode_from_str() {
        assert_eq!(
            StreamingMode::parse("streaming"),
            StreamingMode::ServerStreaming
        );
        assert_eq!(
            StreamingMode::parse("server_streaming"),
            StreamingMode::ServerStreaming
        );
        assert_eq!(
            StreamingMode::parse("client_streaming"),
            StreamingMode::ClientStreaming
        );
        assert_eq!(
            StreamingMode::parse("bidirectional"),
            StreamingMode::Bidirectional
        );
        assert_eq!(StreamingMode::parse("bidi"), StreamingMode::Bidirectional);
        assert_eq!(StreamingMode::parse("duplex"), StreamingMode::Bidirectional);
        assert_eq!(StreamingMode::parse("unary"), StreamingMode::Unary);
        assert_eq!(StreamingMode::parse(""), StreamingMode::Unary);
    }

    #[test]
    fn test_streaming_mode_display() {
        assert_eq!(format!("{}", StreamingMode::Unary), "unary");
        assert_eq!(
            format!("{}", StreamingMode::ServerStreaming),
            "server_streaming"
        );
        assert_eq!(
            format!("{}", StreamingMode::ClientStreaming),
            "client_streaming"
        );
        assert_eq!(format!("{}", StreamingMode::Bidirectional), "bidirectional");
    }

    #[test]
    fn test_proto_file_with_services() {
        let mut proto = ProtoFile::new("test.api");

        let mut service = ProtoService::new("GreeterService");
        service.methods.push(ProtoRpcMethod::new(
            "SayHello",
            "HelloRequest",
            "HelloResponse",
        ));
        proto.services.push(service);

        let output = proto.to_proto_string();
        assert!(output.contains("service GreeterService {"));
        assert!(output.contains("rpc SayHello(HelloRequest) returns (HelloResponse);"));
    }

    // ========================================================================
    // Compatibility Tests
    // ========================================================================

    fn make_test_proto(messages: Vec<ProtoMessage>) -> ProtoFile {
        ProtoFile {
            package: "test.package".to_string(),
            syntax: "proto3".to_string(),
            imports: vec![],
            options: ProtoOptions::default(),
            enums: vec![],
            messages,
            services: vec![],
            metadata: ProtoMetadata::default(),
        }
    }

    fn make_test_message(name: &str, fields: Vec<ProtoField>) -> ProtoMessage {
        ProtoMessage {
            name: name.to_string(),
            fields,
            nested_messages: vec![],
            nested_enums: vec![],
            reserved_numbers: vec![],
            reserved_names: vec![],
            comments: vec![],
        }
    }

    fn make_test_field(name: &str, number: u32, proto_type: ProtoType) -> ProtoField {
        ProtoField {
            name: name.to_string(),
            number,
            proto_type,
            repeated: false,
            optional: false,
            comments: vec![],
        }
    }

    #[test]
    fn test_compatibility_no_changes() {
        let old = make_test_proto(vec![make_test_message(
            "Person",
            vec![
                make_test_field("id", 1, ProtoType::Scalar(ScalarType::String)),
                make_test_field("name", 2, ProtoType::Scalar(ScalarType::String)),
            ],
        )]);

        let new = old.clone();

        let result = CompatibilityChecker::check(&old, &new, CompatibilityMode::Additive);
        assert!(result.is_compatible);
        assert!(result.violations.is_empty());
    }

    #[test]
    fn test_compatibility_field_added() {
        let old = make_test_proto(vec![make_test_message(
            "Person",
            vec![make_test_field(
                "id",
                1,
                ProtoType::Scalar(ScalarType::String),
            )],
        )]);

        let new = make_test_proto(vec![make_test_message(
            "Person",
            vec![
                make_test_field("id", 1, ProtoType::Scalar(ScalarType::String)),
                make_test_field("name", 2, ProtoType::Scalar(ScalarType::String)),
            ],
        )]);

        // Adding fields is compatible in all modes
        let result = CompatibilityChecker::check(&old, &new, CompatibilityMode::Additive);
        assert!(result.is_compatible);
        assert!(result.violations.is_empty());
    }

    #[test]
    fn test_compatibility_field_removed_additive() {
        let old = make_test_proto(vec![make_test_message(
            "Person",
            vec![
                make_test_field("id", 1, ProtoType::Scalar(ScalarType::String)),
                make_test_field("name", 2, ProtoType::Scalar(ScalarType::String)),
            ],
        )]);

        let new = make_test_proto(vec![make_test_message(
            "Person",
            vec![make_test_field(
                "id",
                1,
                ProtoType::Scalar(ScalarType::String),
            )],
        )]);

        // Removing fields is NOT compatible in additive mode
        let result = CompatibilityChecker::check(&old, &new, CompatibilityMode::Additive);
        assert!(!result.is_compatible);
        assert_eq!(result.violations.len(), 1);
        assert_eq!(
            result.violations[0].violation_type,
            ViolationType::FieldRemoved
        );
    }

    #[test]
    fn test_compatibility_field_removed_backward() {
        let old = make_test_proto(vec![make_test_message(
            "Person",
            vec![
                make_test_field("id", 1, ProtoType::Scalar(ScalarType::String)),
                make_test_field("name", 2, ProtoType::Scalar(ScalarType::String)),
            ],
        )]);

        let new = make_test_proto(vec![make_test_message(
            "Person",
            vec![make_test_field(
                "id",
                1,
                ProtoType::Scalar(ScalarType::String),
            )],
        )]);

        // Removing fields IS compatible in backward mode (with warnings)
        let result = CompatibilityChecker::check(&old, &new, CompatibilityMode::Backward);
        assert!(result.is_compatible); // Still compatible, just has violations
        assert!(!result.violations.is_empty());

        // Should suggest reserving the field
        assert!(result.suggested_reserved_numbers.contains_key("Person"));
        assert!(result.suggested_reserved_numbers["Person"].contains(&2));
    }

    #[test]
    fn test_compatibility_type_change() {
        let old = make_test_proto(vec![make_test_message(
            "Person",
            vec![make_test_field(
                "age",
                1,
                ProtoType::Scalar(ScalarType::Int32),
            )],
        )]);

        let new = make_test_proto(vec![make_test_message(
            "Person",
            vec![make_test_field(
                "age",
                1,
                ProtoType::Scalar(ScalarType::String),
            )],
        )]);

        // Type changes are NOT compatible in backward mode
        let result = CompatibilityChecker::check(&old, &new, CompatibilityMode::Backward);
        assert!(!result.is_compatible);
        assert!(result
            .violations
            .iter()
            .any(|v| v.violation_type == ViolationType::FieldTypeChanged));
    }

    #[test]
    fn test_compatibility_breaking_mode() {
        let old = make_test_proto(vec![make_test_message(
            "Person",
            vec![make_test_field(
                "id",
                1,
                ProtoType::Scalar(ScalarType::String),
            )],
        )]);

        let new = make_test_proto(vec![make_test_message(
            "Person",
            vec![make_test_field(
                "uuid",
                1,
                ProtoType::Scalar(ScalarType::Int64),
            )],
        )]);

        // Breaking mode allows everything
        let result = CompatibilityChecker::check(&old, &new, CompatibilityMode::Breaking);
        assert!(result.is_compatible);
        // Violations are still reported for informational purposes
        assert!(!result.violations.is_empty());
    }

    #[test]
    fn test_apply_backward_compatibility() {
        let old = make_test_proto(vec![make_test_message(
            "Person",
            vec![
                make_test_field("id", 1, ProtoType::Scalar(ScalarType::String)),
                make_test_field("name", 2, ProtoType::Scalar(ScalarType::String)),
                make_test_field("email", 3, ProtoType::Scalar(ScalarType::String)),
            ],
        )]);

        let mut new = make_test_proto(vec![make_test_message(
            "Person",
            vec![
                make_test_field("id", 1, ProtoType::Scalar(ScalarType::String)),
                // name (2) removed
                // email (3) removed
                make_test_field("phone", 4, ProtoType::Scalar(ScalarType::String)),
            ],
        )]);

        CompatibilityChecker::apply_backward_compatibility(&old, &mut new);

        // Should have added reserved numbers
        let person = &new.messages[0];
        assert!(person.reserved_numbers.contains(&2));
        assert!(person.reserved_numbers.contains(&3));
        assert!(person.reserved_names.contains(&"name".to_string()));
        assert!(person.reserved_names.contains(&"email".to_string()));
    }

    #[test]
    fn test_compatibility_message_removed() {
        let old = make_test_proto(vec![
            make_test_message("Person", vec![]),
            make_test_message("Address", vec![]),
        ]);

        let new = make_test_proto(vec![make_test_message("Person", vec![])]);

        let result = CompatibilityChecker::check(&old, &new, CompatibilityMode::Additive);
        assert!(!result.is_compatible);
        assert!(result.violations.iter().any(|v| {
            v.violation_type == ViolationType::MessageRemoved && v.message_name == "Address"
        }));
    }

    #[test]
    fn test_compatibility_result_report() {
        let old = make_test_proto(vec![make_test_message(
            "Person",
            vec![make_test_field(
                "name",
                1,
                ProtoType::Scalar(ScalarType::String),
            )],
        )]);

        let new = make_test_proto(vec![make_test_message("Person", vec![])]);

        let result = CompatibilityChecker::check(&old, &new, CompatibilityMode::Backward);
        let report = result.to_report();

        assert!(report.contains("Compatibility Check"));
        assert!(report.contains("field_removed"));
        assert!(report.contains("Person"));
    }

    #[test]
    fn test_compatibility_mode_parsing() {
        assert_eq!(
            "additive".parse::<CompatibilityMode>().unwrap(),
            CompatibilityMode::Additive
        );
        assert_eq!(
            "backward".parse::<CompatibilityMode>().unwrap(),
            CompatibilityMode::Backward
        );
        assert_eq!(
            "breaking".parse::<CompatibilityMode>().unwrap(),
            CompatibilityMode::Breaking
        );
        assert!("invalid".parse::<CompatibilityMode>().is_err());
    }
}
