use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
use uuid::Uuid;

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Project {
    pub id: Uuid,
    pub name: String,
    pub created_at: DateTime<Utc>,
}

#[derive(Debug, Serialize, Deserialize)]
pub struct CreateProjectRequest {
    pub name: String,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ProjectStats {
    pub node_count: usize,
    pub edge_count: usize,
}

#[derive(Debug, Serialize, Deserialize)]
pub struct ProjectResponse {
    pub id: Uuid,
    pub name: String,
    pub created_at: DateTime<Utc>,
}

impl From<Project> for ProjectResponse {
    fn from(p: Project) -> Self {
        Self {
            id: p.id,
            name: p.name,
            created_at: p.created_at,
        }
    }
}

#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
#[serde(rename_all = "snake_case")]
pub enum NodeType {
    Host,
    Service,
    Vulnerability,
    PrivilegeEscalation,
    Credential,
    Finding,
    Note,
    EntryPoint,
    AttackStep,
    Impact,
    Objective,
    #[serde(untagged)]
    Custom(String),
}

impl std::fmt::Display for NodeType {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        match self {
            NodeType::Host => write!(f, "host"),
            NodeType::Service => write!(f, "service"),
            NodeType::Vulnerability => write!(f, "vulnerability"),
            NodeType::PrivilegeEscalation => write!(f, "privilege_escalation"),
            NodeType::Credential => write!(f, "credential"),
            NodeType::Finding => write!(f, "finding"),
            NodeType::Note => write!(f, "note"),
            NodeType::EntryPoint => write!(f, "entry_point"),
            NodeType::AttackStep => write!(f, "attack_step"),
            NodeType::Impact => write!(f, "impact"),
            NodeType::Objective => write!(f, "objective"),
            NodeType::Custom(s) => write!(f, "{s}"),
        }
    }
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct NodePosition {
    pub x: f64,
    pub y: f64,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Node {
    pub id: Uuid,
    pub project_id: Uuid,
    pub label: String,
    pub node_type: NodeType,
    pub position: Option<NodePosition>,
    pub properties: serde_json::Value,
    pub created_at: DateTime<Utc>,
}

#[derive(Debug, Serialize, Deserialize)]
pub struct CreateNodeRequest {
    pub label: String,
    #[serde(default = "default_node_type")]
    pub node_type: NodeType,
    #[serde(default)]
    pub position: Option<NodePosition>,
    #[serde(default = "default_properties")]
    pub properties: serde_json::Value,
    /// Auto-create an edge from this existing node to the new node
    /// Accepts full UUID or short prefix (≥6 chars)
    #[serde(default)]
    pub after: Option<String>,
    /// Label for the auto-created edge (only used with `after`)
    #[serde(default)]
    pub edge_label: Option<String>,
}

fn default_node_type() -> NodeType {
    NodeType::Note
}
fn default_properties() -> serde_json::Value {
    serde_json::Value::Object(serde_json::Map::new())
}

#[derive(Debug, Serialize, Deserialize)]
pub struct UpdateNodeRequest {
    pub label: Option<String>,
    pub node_type: Option<NodeType>,
    pub position: Option<NodePosition>,
    pub properties: Option<serde_json::Value>,
    /// Replace the entire Markdown content in properties["content"]
    pub content: Option<String>,
    /// Append text to existing properties["content"]
    #[serde(default)]
    pub append_content: Option<String>,
    /// Set verdict field in properties (e.g. "success", "failed", "in_progress")
    #[serde(default)]
    pub set_verdict: Option<String>,
    /// Set evidence field in properties (e.g. "confirmed", "unverified", "hypothesis")
    #[serde(default)]
    pub set_evidence: Option<String>,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct NodeResponse {
    pub id: Uuid,
    pub project_id: Uuid,
    pub label: String,
    pub node_type: String,
    pub position: Option<NodePosition>,
    pub properties: serde_json::Value,
    pub created_at: DateTime<Utc>,
}

impl From<Node> for NodeResponse {
    fn from(n: Node) -> Self {
        Self {
            id: n.id,
            project_id: n.project_id,
            label: n.label,
            node_type: n.node_type.to_string(),
            position: n.position,
            properties: n.properties,
            created_at: n.created_at,
        }
    }
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Edge {
    pub id: Uuid,
    pub project_id: Uuid,
    pub source_node_id: Uuid,
    pub target_node_id: Uuid,
    pub label: Option<String>,
    pub created_at: DateTime<Utc>,
}

#[derive(Debug, Serialize, Deserialize)]
pub struct CreateEdgeRequest {
    #[serde(default)]
    pub source_node_id: Option<Uuid>,
    #[serde(default)]
    pub target_node_id: Option<Uuid>,
    pub label: Option<String>,
    /// Auto-create source node with this label if it doesn't exist
    #[serde(default)]
    pub source_label: Option<String>,
    /// Auto-create target node with this label if it doesn't exist
    #[serde(default)]
    pub target_label: Option<String>,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct EdgeResponse {
    pub id: Uuid,
    pub project_id: Uuid,
    pub source_node_id: Uuid,
    pub target_node_id: Uuid,
    pub label: Option<String>,
    pub created_at: DateTime<Utc>,
}

impl From<Edge> for EdgeResponse {
    fn from(e: Edge) -> Self {
        Self {
            id: e.id,
            project_id: e.project_id,
            source_node_id: e.source_node_id,
            target_node_id: e.target_node_id,
            label: e.label,
            created_at: e.created_at,
        }
    }
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct EdgeUpdateRequest {
    pub label: Option<String>,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct NodeSummary {
    pub id: Uuid,
    pub label: String,
    pub node_type: String,
    pub verdict: Option<String>,
    pub evidence: Option<String>,
}

impl From<&Node> for NodeSummary {
    fn from(n: &Node) -> Self {
        Self {
            id: n.id,
            label: n.label.clone(),
            node_type: n.node_type.to_string(),
            verdict: n
                .properties
                .get("verdict")
                .and_then(|v| v.as_str())
                .map(String::from),
            evidence: n
                .properties
                .get("evidence")
                .and_then(|v| v.as_str())
                .map(String::from),
        }
    }
}

#[derive(Debug, Serialize, Deserialize)]
#[serde(bound(serialize = "T: Serialize", deserialize = "T: Deserialize<'de>"))]
pub struct ApiResponse<T> {
    pub success: bool,
    pub data: Option<T>,
    pub error: Option<String>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub error_code: Option<String>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub error_details: Option<ApiErrorDetails>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub error_messages: Option<LocalizedErrorMessages>,
}

#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct ApiErrorDetails {
    pub operation: String,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub entity: Option<String>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub field: Option<String>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub hint: Option<String>,
}

#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct LocalizedErrorMessages {
    pub en: String,
    pub zh: String,
}

impl<T: Serialize> ApiResponse<T> {
    pub fn ok(data: T) -> Self {
        Self {
            success: true,
            data: Some(data),
            error: None,
            error_code: None,
            error_details: None,
            error_messages: None,
        }
    }

    pub fn err(msg: impl Into<String>) -> Self {
        let msg = msg.into();
        Self {
            success: false,
            data: None,
            error: Some(msg.clone()),
            error_code: Some("error".to_string()),
            error_details: None,
            error_messages: Some(LocalizedErrorMessages {
                en: msg.clone(),
                zh: msg,
            }),
        }
    }

    pub fn err_with_details(
        msg: impl Into<String>,
        code: impl Into<String>,
        details: ApiErrorDetails,
        messages: LocalizedErrorMessages,
    ) -> Self {
        Self {
            success: false,
            data: None,
            error: Some(msg.into()),
            error_code: Some(code.into()),
            error_details: Some(details),
            error_messages: Some(messages),
        }
    }
}

// ============================================================================
// 1. Credential 凭据管理
// ============================================================================

#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
#[serde(rename_all = "snake_case")]
pub enum CredentialType {
    Password,
    SshKey,
    Aksk,
    Token,
}

impl std::fmt::Display for CredentialType {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        match self {
            CredentialType::Password => write!(f, "password"),
            CredentialType::SshKey => write!(f, "ssh_key"),
            CredentialType::Aksk => write!(f, "aksk"),
            CredentialType::Token => write!(f, "token"),
        }
    }
}

#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
#[serde(rename_all = "snake_case")]
pub enum CredentialStatus {
    Unverified,
    Verified,
    Expired,
}

impl std::fmt::Display for CredentialStatus {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        match self {
            CredentialStatus::Unverified => write!(f, "unverified"),
            CredentialStatus::Verified => write!(f, "verified"),
            CredentialStatus::Expired => write!(f, "expired"),
        }
    }
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Credential {
    pub id: Uuid,
    pub project_id: Uuid,
    pub label: String,
    pub credential_type: CredentialType,
    pub username: Option<String>,
    pub secret: String,
    pub source_node_id: Option<Uuid>,
    pub status: CredentialStatus,
    pub notes: Option<String>,
    pub created_at: DateTime<Utc>,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CreateCredentialRequest {
    pub label: String,
    pub credential_type: CredentialType,
    pub username: Option<String>,
    pub secret: String,
    pub source_node_id: Option<Uuid>,
    pub status: CredentialStatus,
    pub notes: Option<String>,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct UpdateCredentialRequest {
    pub label: Option<String>,
    pub credential_type: Option<CredentialType>,
    pub username: Option<String>,
    pub secret: Option<String>,
    pub source_node_id: Option<Option<Uuid>>,
    pub status: Option<CredentialStatus>,
    pub notes: Option<Option<String>>,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CredentialResponse {
    pub id: Uuid,
    pub project_id: Uuid,
    pub label: String,
    pub credential_type: String,
    pub username: Option<String>,
    pub secret: String,
    pub source_node_id: Option<Uuid>,
    pub status: String,
    pub notes: Option<String>,
    pub created_at: DateTime<Utc>,
}

impl From<Credential> for CredentialResponse {
    fn from(c: Credential) -> Self {
        Self {
            id: c.id,
            project_id: c.project_id,
            label: c.label,
            credential_type: c.credential_type.to_string(),
            username: c.username,
            secret: c.secret,
            source_node_id: c.source_node_id,
            status: c.status.to_string(),
            notes: c.notes,
            created_at: c.created_at,
        }
    }
}

// ============================================================================
// 2. Timeline 时间线视图
// ============================================================================

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct TimelineItem {
    pub node_id: Uuid,
    pub project_id: Uuid,
    pub node_label: String,
    pub node_type: String,
    pub created_at: DateTime<Utc>,
    pub properties: serde_json::Value,
    pub incoming: Vec<TimelineEdgeInfo>,
    pub outgoing: Vec<TimelineEdgeInfo>,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct TimelineEdgeInfo {
    pub edge_id: Uuid,
    pub label: String,
    pub other_node_id: Uuid,
    pub other_node_label: String,
    pub direction: String,
}

// ============================================================================
// 3. Global Search 全局搜索
// ============================================================================

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SearchResult {
    pub project_id: Uuid,
    pub project_name: String,
    pub node_id: Uuid,
    pub node_label: String,
    pub node_type: String,
    pub match_context: String,
}

// ============================================================================
// 4. Evidence 证据附件
// ============================================================================

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct EvidenceFile {
    pub id: Uuid,
    pub project_id: Uuid,
    pub node_id: Uuid,
    pub original_name: String,
    pub stored_name: String,
    pub file_size: i64,
    pub mime_type: Option<String>,
    pub created_at: DateTime<Utc>,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct EvidenceFileResponse {
    pub id: Uuid,
    pub project_id: Uuid,
    pub node_id: Uuid,
    pub original_name: String,
    pub stored_name: String,
    pub file_size: i64,
    pub mime_type: Option<String>,
    pub created_at: DateTime<Utc>,
}

impl From<EvidenceFile> for EvidenceFileResponse {
    fn from(e: EvidenceFile) -> Self {
        Self {
            id: e.id,
            project_id: e.project_id,
            node_id: e.node_id,
            original_name: e.original_name,
            stored_name: e.stored_name,
            file_size: e.file_size,
            mime_type: e.mime_type,
            created_at: e.created_at,
        }
    }
}

// ============================================================================
// 5. Import 工具导入
// ============================================================================

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ImportRequest {
    pub nodes: Vec<ImportNode>,
    pub edges: Vec<ImportEdge>,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ImportNode {
    pub label: String,
    #[serde(default = "default_import_node_type")]
    pub node_type: Option<String>,
    pub content: Option<String>,
    pub properties: Option<serde_json::Value>,
}

fn default_import_node_type() -> Option<String> {
    Some("note".to_string())
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ImportEdge {
    pub source_label: String,
    pub target_label: String,
    pub label: Option<String>,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ImportResult {
    pub nodes_created: usize,
    pub nodes_skipped: usize,
    pub edges_created: usize,
    pub edges_skipped: usize,
    pub errors: Vec<String>,
}

// ============================================================================
// 6. Agent 系统
// ============================================================================

/// Agent 运行入口点
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum AgentEntry {
    Tell,
    Ask,
    Analysis,
    Report,
    /// 全局分析：读整个图，输出建议的探索方向（Intent）
    Reason,
    /// 执行探索：拿一个具体 Intent 去执行
    Explore,
}

impl AgentEntry {
    pub fn as_str(&self) -> &'static str {
        match self {
            Self::Tell => "tell",
            Self::Ask => "ask",
            Self::Analysis => "analysis",
            Self::Report => "report",
            Self::Reason => "reason",
            Self::Explore => "explore",
        }
    }
}

/// Agent 运行请求
#[derive(Debug, Serialize, Deserialize)]
pub struct AgentRunRequest {
    pub entry: AgentEntry,
    pub project_id: Uuid,
    pub message: String,
    #[serde(default = "default_max_turns")]
    pub max_turns: u32,
}

fn default_max_turns() -> u32 {
    5
}

/// Agent 运行响应
#[derive(Debug, Serialize, Deserialize)]
pub struct AgentRunResponse {
    pub answer: String,
    pub turns: u32,
    pub tool_calls: Vec<ToolCallRecord>,
    pub created_nodes: Vec<NodeSummary>,
    pub created_edges: Vec<EdgeResponse>,
    pub created_intents: Vec<ExplorationIntent>,
}

/// 工具调用记录
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ToolCallRecord {
    pub id: String,
    pub name: String,
    pub arguments: serde_json::Value,
    pub result: Option<serde_json::Value>,
    pub error: Option<String>,
}

/// LLM 工具调用请求
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct LlmToolCall {
    pub id: String,
    pub name: String,
    pub arguments: serde_json::Value,
}

/// LLM 响应
#[derive(Debug, Serialize, Deserialize)]
pub struct LlmResponse {
    pub content: Option<String>,
    pub tool_calls: Vec<LlmToolCall>,
    pub finish_reason: String,
}

/// 探索意图状态
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
#[serde(rename_all = "lowercase")]
pub enum IntentStatus {
    Open,
    Claimed,
    Completed,
    Abandoned,
}

impl IntentStatus {
    pub fn as_str(&self) -> &'static str {
        match self {
            Self::Open => "open",
            Self::Claimed => "claimed",
            Self::Completed => "completed",
            Self::Abandoned => "abandoned",
        }
    }
    pub fn from_str(s: &str) -> Self {
        match s {
            "claimed" => Self::Claimed,
            "completed" => Self::Completed,
            "abandoned" => Self::Abandoned,
            _ => Self::Open,
        }
    }
}

/// 探索意图
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ExplorationIntent {
    pub id: String,
    pub project_id: Uuid,
    pub node_id: String,
    pub direction: String,
    pub description: Option<String>,
    pub status: IntentStatus,
    pub worker: Option<String>,
    pub created_at: String,
    pub updated_at: String,
}

/// 创建意图请求
#[derive(Debug, Serialize, Deserialize)]
pub struct CreateIntentRequest {
    pub project_id: Uuid,
    pub node_id: String,
    pub direction: String,
    pub description: Option<String>,
}

/// 更新意图请求
#[derive(Debug, Serialize, Deserialize)]
pub struct UpdateIntentRequest {
    pub status: Option<IntentStatus>,
    pub description: Option<String>,
}

/// 工具定义（供 LLM 使用）
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ToolDefinition {
    pub name: String,
    pub description: String,
    pub parameters: serde_json::Value,
}

/// 工具执行结果
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ToolResult {
    pub output: serde_json::Value,
    pub side_effects: Vec<SideEffect>,
}

/// 工具副作用
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(tag = "type")]
pub enum SideEffect {
    NodeCreated(NodeResponse),
    EdgeCreated(EdgeResponse),
    CredentialCreated(CredentialResponse),
}

/// Agent 记忆消息
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct AgentMemoryMessage {
    pub id: String,
    pub project_id: Uuid,
    pub role: String,
    pub content: String,
    pub tool_calls: Option<String>,
    pub run_id: String,
    pub created_at: DateTime<Utc>,
}


// ============================================================================
// 7. Analysis 分析类型
// ============================================================================

#[derive(Debug, serde::Serialize)]
pub struct AttackPath {
    pub nodes: Vec<PathNode>,
    pub edge_labels: Vec<String>,
}

#[derive(Debug, serde::Serialize)]
pub struct PathNode {
    pub id: String,
    pub label: String,
}

#[derive(Debug, serde::Serialize)]
pub struct TaggedNode {
    pub id: String,
    pub label: String,
    pub node_type: String,
    #[serde(default)]
    pub content: String,
    #[serde(default)]
    pub properties: serde_json::Value,
}

#[derive(Debug, serde::Serialize)]
pub struct NextStep {
    pub node_id: String,
    pub label: String,
    pub node_type: String,
    pub suggestion: String,
    pub from_path: Vec<String>,
    pub evidence: String,
    pub chain_status: String,
}

#[derive(Debug, serde::Serialize)]
pub struct CrossConnection {
    pub from_id: String,
    pub from_label: String,
    pub to_id: String,
    pub to_label: String,
    pub reason: String,
}

#[derive(Debug, serde::Serialize)]
pub struct HealthIssue {
    pub severity: String,
    pub check_type: String,
    pub description: String,
    pub node_ids: Vec<String>,
    pub node_labels: Vec<String>,
}

#[derive(Debug, serde::Serialize)]
pub struct CheckIssue {
    pub rule: String,
    pub severity: String,
    pub node_id: String,
    pub node_label: String,
    pub description: String,
}

#[derive(Debug, serde::Serialize)]
pub struct CheckResult {
    pub total_nodes: usize,
    pub total_edges: usize,
    pub issues: Vec<CheckIssue>,
}

#[derive(Debug, serde::Serialize)]
pub struct AttackAnalysis {
    pub attack_paths: Vec<AttackPath>,
    pub unexploited_vulns: Vec<TaggedNode>,
    pub dead_ends: Vec<TaggedNode>,
    pub next_steps: Vec<NextStep>,
    pub cross_connections: Vec<CrossConnection>,
    pub isolated_nodes: Vec<TaggedNode>,
    pub health_checks: Vec<HealthIssue>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub check_summary: Option<CheckResult>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub ai_hint: Option<String>,
}
#[cfg(test)]
mod tests {
    use super::*;
    use chrono::TimeZone;
    use serde::de::DeserializeOwned;
    use serde_json::{json, Value};

    fn sample_uuid() -> Uuid {
        Uuid::parse_str("123e4567-e89b-12d3-a456-426614174000").unwrap()
    }

    fn another_uuid() -> Uuid {
        Uuid::parse_str("123e4567-e89b-12d3-a456-426614174001").unwrap()
    }

    fn third_uuid() -> Uuid {
        Uuid::parse_str("123e4567-e89b-12d3-a456-426614174002").unwrap()
    }

    fn sample_time() -> DateTime<Utc> {
        Utc.with_ymd_and_hms(2026, 5, 28, 12, 34, 56).unwrap()
    }

    fn assert_roundtrip<T>(value: &T)
    where
        T: Serialize + DeserializeOwned,
    {
        let serialized = serde_json::to_value(value).unwrap();
        let deserialized: T = serde_json::from_value(serialized.clone()).unwrap();
        let reserialized = serde_json::to_value(deserialized).unwrap();
        assert_eq!(serialized, reserialized);
    }

    fn sample_node() -> Node {
        Node {
            id: sample_uuid(),
            project_id: another_uuid(),
            label: "Initial foothold".to_string(),
            node_type: NodeType::PrivilegeEscalation,
            position: Some(NodePosition { x: 12.5, y: 64.0 }),
            properties: json!({
                "content": "Found sudo misconfig",
                "verdict": "success",
                "evidence": "confirmed"
            }),
            created_at: sample_time(),
        }
    }

    fn sample_edge() -> Edge {
        Edge {
            id: third_uuid(),
            project_id: another_uuid(),
            source_node_id: sample_uuid(),
            target_node_id: another_uuid(),
            label: Some("pivoted".to_string()),
            created_at: sample_time(),
        }
    }

    fn sample_credential() -> Credential {
        Credential {
            id: sample_uuid(),
            project_id: another_uuid(),
            label: "VPN token".to_string(),
            credential_type: CredentialType::Token,
            username: Some("alice".to_string()),
            secret: "secret-value".to_string(),
            source_node_id: Some(third_uuid()),
            status: CredentialStatus::Verified,
            notes: Some("Pulled from browser storage".to_string()),
            created_at: sample_time(),
        }
    }

    fn sample_evidence_file() -> EvidenceFile {
        EvidenceFile {
            id: sample_uuid(),
            project_id: another_uuid(),
            node_id: third_uuid(),
            original_name: "screen.png".to_string(),
            stored_name: "uuid-screen.png".to_string(),
            file_size: 2048,
            mime_type: Some("image/png".to_string()),
            created_at: sample_time(),
        }
    }

    #[test]
    fn project_and_project_response_roundtrip_and_from() {
        let project = Project {
            id: sample_uuid(),
            name: "Engagement A".to_string(),
            created_at: sample_time(),
        };

        assert_roundtrip(&project);

        let response = ProjectResponse::from(project.clone());
        assert_eq!(response.id, project.id);
        assert_eq!(response.name, project.name);
        assert_eq!(response.created_at, project.created_at);
        assert_roundtrip(&response);
        assert_roundtrip(&CreateProjectRequest {
            name: "Engagement A".to_string(),
        });
        assert_roundtrip(&ProjectStats {
            node_count: 3,
            edge_count: 2,
        });
    }

    #[test]
    fn node_type_serialization_and_display_cover_builtin_and_custom() {
        let builtins = [
            (NodeType::Host, "host"),
            (NodeType::Service, "service"),
            (NodeType::Vulnerability, "vulnerability"),
            (NodeType::PrivilegeEscalation, "privilege_escalation"),
            (NodeType::Credential, "credential"),
            (NodeType::Finding, "finding"),
            (NodeType::Note, "note"),
            (NodeType::EntryPoint, "entry_point"),
            (NodeType::AttackStep, "attack_step"),
            (NodeType::Impact, "impact"),
            (NodeType::Objective, "objective"),
        ];

        for (node_type, expected) in builtins {
            assert_eq!(node_type.to_string(), expected);
            assert_eq!(serde_json::to_value(&node_type).unwrap(), json!(expected));
            assert_eq!(
                serde_json::from_value::<NodeType>(json!(expected)).unwrap(),
                node_type
            );
        }

        let custom = NodeType::Custom("domain_controller".to_string());
        assert_eq!(custom.to_string(), "domain_controller");
        assert_eq!(
            serde_json::to_value(&custom).unwrap(),
            json!("domain_controller")
        );
        assert_eq!(
            serde_json::from_value::<NodeType>(json!("domain_controller")).unwrap(),
            custom
        );
    }

    #[test]
    fn node_related_types_roundtrip_from_and_defaults() {
        let node = sample_node();
        assert_roundtrip(&node);

        let response = NodeResponse::from(node.clone());
        assert_eq!(response.id, node.id);
        assert_eq!(response.project_id, node.project_id);
        assert_eq!(response.label, node.label);
        assert_eq!(response.node_type, "privilege_escalation");
        assert_eq!(response.position.as_ref().unwrap().x, 12.5);
        assert_eq!(response.properties, node.properties);
        assert_roundtrip(&response);

        let summary = NodeSummary::from(&node);
        assert_eq!(summary.id, node.id);
        assert_eq!(summary.label, node.label);
        assert_eq!(summary.node_type, "privilege_escalation");
        assert_eq!(summary.verdict.as_deref(), Some("success"));
        assert_eq!(summary.evidence.as_deref(), Some("confirmed"));
        assert_roundtrip(&summary);

        let create_default: CreateNodeRequest = serde_json::from_value(json!({
            "label": "Quick note"
        }))
        .unwrap();
        assert_eq!(create_default.label, "Quick note");
        assert_eq!(create_default.node_type, NodeType::Note);
        assert!(create_default.position.is_none());
        assert_eq!(create_default.properties, json!({}));
        assert!(create_default.after.is_none());
        assert!(create_default.edge_label.is_none());

        assert_roundtrip(&CreateNodeRequest {
            label: "New node".to_string(),
            node_type: NodeType::Service,
            position: Some(NodePosition { x: 1.0, y: 2.0 }),
            properties: json!({"content": "hello"}),
            after: Some("abc123".to_string()),
            edge_label: Some("discovered".to_string()),
        });

        assert_roundtrip(&UpdateNodeRequest {
            label: Some("Updated".to_string()),
            node_type: Some(NodeType::Impact),
            position: Some(NodePosition { x: 9.0, y: 8.0 }),
            properties: Some(json!({"tag": "important"})),
            content: Some("replaced".to_string()),
            append_content: Some(" appended".to_string()),
            set_verdict: Some("failed".to_string()),
            set_evidence: Some("hypothesis".to_string()),
        });
    }

    #[test]
    fn edge_related_types_roundtrip_and_from() {
        let edge = sample_edge();
        assert_roundtrip(&edge);

        let response = EdgeResponse::from(edge.clone());
        assert_eq!(response.id, edge.id);
        assert_eq!(response.project_id, edge.project_id);
        assert_eq!(response.source_node_id, edge.source_node_id);
        assert_eq!(response.target_node_id, edge.target_node_id);
        assert_eq!(response.label, edge.label);
        assert_eq!(response.created_at, edge.created_at);
        assert_roundtrip(&response);

        assert_roundtrip(&CreateEdgeRequest {
            source_node_id: Some(sample_uuid()),
            target_node_id: Some(another_uuid()),
            label: Some("connected".to_string()),
            source_label: Some("Source".to_string()),
            target_label: Some("Target".to_string()),
        });

        assert_roundtrip(&EdgeUpdateRequest {
            label: Some("updated".to_string()),
        });
    }

    #[test]
    fn credential_enums_display_and_serde_are_consistent() {
        let credential_types = [
            (CredentialType::Password, "password"),
            (CredentialType::SshKey, "ssh_key"),
            (CredentialType::Aksk, "aksk"),
            (CredentialType::Token, "token"),
        ];
        for (credential_type, expected) in credential_types {
            assert_eq!(credential_type.to_string(), expected);
            assert_eq!(
                serde_json::to_value(&credential_type).unwrap(),
                json!(expected)
            );
            assert_eq!(
                serde_json::from_value::<CredentialType>(json!(expected)).unwrap(),
                credential_type
            );
        }

        let credential_statuses = [
            (CredentialStatus::Unverified, "unverified"),
            (CredentialStatus::Verified, "verified"),
            (CredentialStatus::Expired, "expired"),
        ];
        for (credential_status, expected) in credential_statuses {
            assert_eq!(credential_status.to_string(), expected);
            assert_eq!(
                serde_json::to_value(&credential_status).unwrap(),
                json!(expected)
            );
            assert_eq!(
                serde_json::from_value::<CredentialStatus>(json!(expected)).unwrap(),
                credential_status
            );
        }
    }

    #[test]
    fn credential_related_types_roundtrip_and_from() {
        let credential = sample_credential();
        assert_roundtrip(&credential);

        let response = CredentialResponse::from(credential.clone());
        assert_eq!(response.id, credential.id);
        assert_eq!(response.project_id, credential.project_id);
        assert_eq!(response.label, credential.label);
        assert_eq!(response.credential_type, "token");
        assert_eq!(response.username, credential.username);
        assert_eq!(response.secret, credential.secret);
        assert_eq!(response.source_node_id, credential.source_node_id);
        assert_eq!(response.status, "verified");
        assert_eq!(response.notes, credential.notes);
        assert_eq!(response.created_at, credential.created_at);
        assert_roundtrip(&response);

        assert_roundtrip(&CreateCredentialRequest {
            label: "SSH key".to_string(),
            credential_type: CredentialType::SshKey,
            username: Some("root".to_string()),
            secret: "-----BEGIN OPENSSH PRIVATE KEY-----".to_string(),
            source_node_id: Some(sample_uuid()),
            status: CredentialStatus::Unverified,
            notes: Some("Need to test against bastion".to_string()),
        });

        assert_roundtrip(&UpdateCredentialRequest {
            label: Some("Updated secret".to_string()),
            credential_type: Some(CredentialType::Password),
            username: Some("admin".to_string()),
            secret: Some("new-secret".to_string()),
            source_node_id: Some(Some(another_uuid())),
            status: Some(CredentialStatus::Expired),
            notes: Some(Some("rotated".to_string())),
        });

        let clearable: UpdateCredentialRequest = serde_json::from_value(json!({
            "source_node_id": null,
            "notes": null
        }))
        .unwrap();
        assert!(clearable.source_node_id.is_none());
        assert!(clearable.notes.is_none());
    }

    #[test]
    fn evidence_and_api_response_helpers_are_serializable() {
        let evidence = sample_evidence_file();
        assert_roundtrip(&evidence);

        let response = EvidenceFileResponse::from(evidence.clone());
        assert_eq!(response.id, evidence.id);
        assert_eq!(response.project_id, evidence.project_id);
        assert_eq!(response.node_id, evidence.node_id);
        assert_eq!(response.original_name, evidence.original_name);
        assert_eq!(response.stored_name, evidence.stored_name);
        assert_eq!(response.file_size, evidence.file_size);
        assert_eq!(response.mime_type, evidence.mime_type);
        assert_eq!(response.created_at, evidence.created_at);
        assert_roundtrip(&response);

        let ok = ApiResponse::ok(ProjectStats {
            node_count: 5,
            edge_count: 4,
        });
        assert!(ok.success);
        assert_eq!(ok.error, None);
        assert_eq!(ok.data.as_ref().unwrap().node_count, 5);
        assert_roundtrip(&ok);

        let err: ApiResponse<Value> = ApiResponse::err("bad request");
        assert!(!err.success);
        assert!(err.data.is_none());
        assert_eq!(err.error.as_deref(), Some("bad request"));
        assert_roundtrip(&err);
    }

    #[test]
    fn import_types_and_defaults_roundtrip() {
        let import_node: ImportNode = serde_json::from_value(json!({
            "label": "Imported note"
        }))
        .unwrap();
        assert_eq!(import_node.label, "Imported note");
        assert_eq!(import_node.node_type.as_deref(), Some("note"));
        assert!(import_node.content.is_none());
        assert!(import_node.properties.is_none());

        assert_roundtrip(&ImportRequest {
            nodes: vec![ImportNode {
                label: "Imported service".to_string(),
                node_type: Some("service".to_string()),
                content: Some("imported markdown".to_string()),
                properties: Some(json!({"content": "imported"})),
            }],
            edges: vec![ImportEdge {
                source_label: "Imported service".to_string(),
                target_label: "Imported vuln".to_string(),
                label: Some("exposes".to_string()),
            }],
        });

        assert_roundtrip(&ImportResult {
            nodes_created: 1,
            nodes_skipped: 2,
            edges_created: 3,
            edges_skipped: 4,
            errors: vec!["duplicate label".to_string()],
        });
    }

    #[test]
    fn timeline_search_and_agent_types_roundtrip() {
        assert_roundtrip(&TimelineItem {
            node_id: sample_uuid(),
            project_id: another_uuid(),
            node_label: "Edge host".to_string(),
            node_type: "host".to_string(),
            created_at: sample_time(),
            properties: json!({"content": "first seen via nmap"}),
            incoming: vec![TimelineEdgeInfo {
                edge_id: third_uuid(),
                label: "discovered".to_string(),
                other_node_id: another_uuid(),
                other_node_label: "Internet".to_string(),
                direction: "incoming".to_string(),
            }],
            outgoing: vec![TimelineEdgeInfo {
                edge_id: sample_uuid(),
                label: "leads_to".to_string(),
                other_node_id: third_uuid(),
                other_node_label: "Admin panel".to_string(),
                direction: "outgoing".to_string(),
            }],
        });

        assert_roundtrip(&SearchResult {
            project_id: sample_uuid(),
            project_name: "Prod target".to_string(),
            node_id: another_uuid(),
            node_label: "SQL injection".to_string(),
            node_type: "vulnerability".to_string(),
            match_context: "Found in notes body".to_string(),
        });

        assert_eq!(
            serde_json::to_value(&AgentEntry::Tell).unwrap(),
            json!("tell")
        );
        assert!(matches!(
            serde_json::from_value::<AgentEntry>(json!("analysis")).unwrap(),
            AgentEntry::Analysis
        ));

        let run_request: AgentRunRequest = serde_json::from_value(json!({
            "entry": "ask",
            "project_id": sample_uuid(),
            "message": "What should I test next?"
        }))
        .unwrap();
        assert_eq!(run_request.max_turns, 5);
        assert_eq!(
            serde_json::to_value(&run_request).unwrap()["entry"],
            json!("ask")
        );

        assert_roundtrip(&AgentRunResponse {
            answer: "Try the admin panel".to_string(),
            turns: 2,
            tool_calls: vec![ToolCallRecord {
                id: "call-1".to_string(),
                name: "create_node".to_string(),
                arguments: json!({"label": "Admin panel"}),
                result: Some(json!({"id": sample_uuid()})),
                error: None,
            }],
            created_nodes: vec![NodeSummary {
                id: sample_uuid(),
                label: "Admin panel".to_string(),
                node_type: "service".to_string(),
                verdict: Some("pending".to_string()),
                evidence: Some("confirmed".to_string()),
            }],
            created_edges: vec![EdgeResponse::from(sample_edge())],
            created_intents: vec![],
        });

        assert_roundtrip(&LlmToolCall {
            id: "tool-1".to_string(),
            name: "search_nodes".to_string(),
            arguments: json!({"q": "sql"}),
        });

        assert_roundtrip(&LlmResponse {
            content: Some("Found likely match".to_string()),
            tool_calls: vec![LlmToolCall {
                id: "tool-2".to_string(),
                name: "list_nodes".to_string(),
                arguments: json!({}),
            }],
            finish_reason: "stop".to_string(),
        });

        assert_roundtrip(&ToolDefinition {
            name: "create_edge".to_string(),
            description: "Create a graph edge".to_string(),
            parameters: json!({"type": "object"}),
        });

        assert_roundtrip(&ToolResult {
            output: json!({"status": "ok"}),
            side_effects: vec![
                SideEffect::NodeCreated(NodeResponse::from(sample_node())),
                SideEffect::EdgeCreated(EdgeResponse::from(sample_edge())),
                SideEffect::CredentialCreated(CredentialResponse::from(sample_credential())),
            ],
        });

        assert_roundtrip(&AgentMemoryMessage {
            id: "mem-1".to_string(),
            project_id: sample_uuid(),
            role: "assistant".to_string(),
            content: "Summarized the chain".to_string(),
            tool_calls: Some("[]".to_string()),
            run_id: "run-1".to_string(),
            created_at: sample_time(),
        });
    }
}
