//! JSON output types for CLI commands

use serde::{Deserialize, Serialize};

/// Success response wrapper
#[derive(Serialize)]
pub struct JsonResponse<T: Serialize> {
    /// Response status (always "success")
    pub status: &'static str,
    /// Command that was executed
    pub command: &'static str,
    /// Response data
    pub data: T,
}

impl<T: Serialize> JsonResponse<T> {
    /// Create a new success response
    pub fn success(command: &'static str, data: T) -> Self {
        Self {
            status: "success",
            command,
            data,
        }
    }
}

/// Error response wrapper
#[derive(Serialize)]
pub struct JsonErrorResponse {
    /// Response status (always "error")
    pub status: &'static str,
    /// Command that was executed
    pub command: &'static str,
    /// Error details
    pub error: JsonError,
}

/// Error details
#[derive(Serialize)]
pub struct JsonError {
    /// Error code (e.g., "ENV_NOT_FOUND")
    pub code: &'static str,
    /// Human-readable error message
    pub message: String,
    /// Suggested fix (if available)
    #[serde(skip_serializing_if = "Option::is_none")]
    pub suggestion: Option<String>,
}

impl JsonErrorResponse {
    /// Create a new error response
    pub fn error(command: &'static str, code: &'static str, message: String) -> Self {
        Self {
            status: "error",
            command,
            error: JsonError {
                code,
                message,
                suggestion: None,
            },
        }
    }

    /// Add a suggestion to the error
    pub fn with_suggestion(mut self, suggestion: String) -> Self {
        self.error.suggestion = Some(suggestion);
        self
    }
}

// ============================================================================
// Command-specific data types
// ============================================================================

/// List virtualenvs response data
#[derive(Serialize)]
pub struct ListEnvsData {
    pub virtualenvs: Vec<VirtualenvInfo>,
    pub total: usize,
}

/// Virtualenv info for JSON output
#[derive(Serialize)]
pub struct VirtualenvInfo {
    pub name: String,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub python: Option<String>,
    pub path: String,
    pub active: bool,
    /// Creation timestamp (RFC 3339). Omitted when metadata is missing
    /// or doesn't carry `created_at` yet (legacy envs).
    #[serde(skip_serializing_if = "Option::is_none")]
    pub created_at: Option<String>,
    /// Last-use timestamp (RFC 3339). Omitted for envs that haven't
    /// been activated since the field landed.
    #[serde(skip_serializing_if = "Option::is_none")]
    pub last_used: Option<String>,
}

/// List pythons response data
#[derive(Serialize)]
pub struct ListPythonsData {
    pub pythons: Vec<PythonInfo>,
    pub total: usize,
}

/// Python info for JSON output
#[derive(Serialize)]
pub struct PythonInfo {
    pub version: String,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub implementation: Option<String>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub path: Option<String>,
}

/// Create response data
#[derive(Serialize)]
pub struct CreateData {
    pub name: String,
    pub python: String,
    pub path: String,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub python_path: Option<String>,
}

/// Use response data
#[derive(Serialize)]
pub struct UseData {
    pub name: String,
    pub mode: &'static str,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub version_file: Option<String>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub symlink: Option<String>,
}

/// Remove response data
#[derive(Serialize)]
pub struct RemoveData {
    pub name: String,
    pub path: String,
}

/// Install response data
#[derive(Serialize)]
pub struct InstallData {
    pub version: String,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub path: Option<String>,
}

/// Uninstall response data
#[derive(Serialize, Deserialize, PartialEq, Debug)]
pub struct UninstallData {
    pub version: String,
    /// Environments removed by cascade (only present if --cascade used)
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub removed_envs: Option<Vec<String>>,
}

/// Package info for JSON output
#[derive(Serialize)]
pub struct PackageInfo {
    pub name: String,
    pub version: String,
}

/// Packages summary for JSON output
#[derive(Serialize)]
pub struct PackagesInfo {
    pub total: usize,
    pub items: Vec<PackageInfo>,
    pub truncated: bool,
}

impl PackagesInfo {
    /// Creates a new PackagesInfo with truncation applied.
    ///
    /// # Arguments
    /// * `packages` - Full list of (name, version) tuples
    /// * `limit` - Maximum number of packages to include in items
    ///
    /// # Examples
    /// ```
    /// # use scoop_uv::output::PackagesInfo;
    /// let packages = vec![
    ///     ("pkg1".to_string(), "1.0".to_string()),
    ///     ("pkg2".to_string(), "2.0".to_string()),
    /// ];
    /// let info = PackagesInfo::new(&packages, 1);
    /// assert_eq!(info.total, 2);
    /// assert_eq!(info.items.len(), 1);
    /// assert!(info.truncated);
    /// ```
    pub fn new(packages: &[(String, String)], limit: usize) -> Self {
        let total = packages.len();
        let truncated = total > limit;
        Self {
            total,
            items: packages
                .iter()
                .take(limit)
                .map(|(n, v)| PackageInfo {
                    name: n.clone(),
                    version: v.clone(),
                })
                .collect(),
            truncated,
        }
    }

    /// Returns the number of packages not included due to truncation.
    #[inline]
    pub fn remaining(&self) -> usize {
        self.total.saturating_sub(self.items.len())
    }
}

/// `scoop which` response data
#[derive(Serialize)]
pub struct WhichData {
    pub exe: String,
    pub env: String,
    pub path: String,
}

/// `scoop clone` response data
#[derive(Serialize)]
pub struct CloneData {
    pub src: String,
    pub dst: String,
    pub python: String,
    pub path: String,
    pub packages_copied: usize,
    /// `true` when `--no-packages` was passed.
    pub packages_skipped: bool,
}

/// `scoop import` response data
#[derive(Serialize)]
pub struct ImportData {
    pub name: String,
    pub python: String,
    pub packages_installed: usize,
    pub source: String,
}

/// `scoop sync` response data
#[derive(Serialize)]
pub struct SyncData {
    /// Absolute path of the resolved `.scoop.toml`.
    pub manifest_path: String,
    /// Environment name the manifest targets.
    pub environment: String,
    /// Python version specifier from the manifest.
    pub python: String,
    /// All resolved group names (always includes `"default"`).
    pub groups: Vec<String>,
    /// Packages that would be / were installed (deduped, ordered).
    pub packages: Vec<String>,
    /// `true` if `scoop sync` created the env; `false` if it already existed.
    pub env_created: bool,
    /// `true` when `--dry-run` was passed (no side effects performed).
    pub dry_run: bool,
}

/// `scoop status` response data
#[derive(Serialize)]
pub struct StatusData {
    /// `"active"` (shell-activated), `"configured"` (version-file), `"system"`, or `"none"`.
    pub state: &'static str,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub name: Option<String>,
    /// Where the env name came from: `"scoop_active_env"` or `"version_file"`.
    #[serde(skip_serializing_if = "Option::is_none")]
    pub source: Option<&'static str>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub path: Option<String>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub python: Option<String>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub created_at: Option<String>,
    /// RFC 3339 timestamp of the last `scoop activate` / `run` / `shell`
    /// against this env. `None` for legacy envs whose metadata predates
    /// the field, and for freshly created envs that have never been
    /// activated. Omitted from JSON when absent so old consumers don't
    /// have to learn a new key.
    #[serde(skip_serializing_if = "Option::is_none")]
    pub last_used: Option<String>,
    /// Best-effort installed-package count via the venv's own `pip list`.
    /// `None` when the env has no pip (broken / not yet bootstrapped).
    #[serde(skip_serializing_if = "Option::is_none")]
    pub packages: Option<usize>,
}

/// Detailed environment info for JSON output
#[derive(Serialize)]
pub struct EnvInfoData {
    pub name: String,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub python: Option<String>,
    pub path: String,
    pub active: bool,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub created_at: Option<String>,
    /// RFC 3339 last-use timestamp, omitted when unknown. See
    /// `StatusData::last_used` for the full contract.
    #[serde(skip_serializing_if = "Option::is_none")]
    pub last_used: Option<String>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub size_bytes: Option<u64>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub size_display: Option<String>,
    pub packages: PackagesInfo,
}

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

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

    // ========================================
    // JsonResponse Tests
    // ========================================

    #[test]
    fn test_json_response_success_creates_correct_status() {
        let response = JsonResponse::success("test", "data");
        assert_eq!(response.status, "success");
        assert_eq!(response.command, "test");
        assert_eq!(response.data, "data");
    }

    #[test]
    fn test_json_response_serialization_format() {
        let response = JsonResponse::success(
            "list",
            ListEnvsData {
                virtualenvs: vec![],
                total: 0,
            },
        );
        let json = serde_json::to_string(&response).unwrap();
        assert!(json.contains(r#""status":"success""#));
        assert!(json.contains(r#""command":"list""#));
        assert!(json.contains(r#""total":0"#));
    }

    #[test]
    fn test_json_response_with_string_data() {
        let response = JsonResponse::success("echo", "hello world");
        let json = serde_json::to_string(&response).unwrap();
        let parsed: serde_json::Value = serde_json::from_str(&json).unwrap();
        assert_eq!(parsed["data"], "hello world");
    }

    #[test]
    fn test_json_response_with_struct_data() {
        let response = JsonResponse::success(
            "create",
            CreateData {
                name: "myenv".into(),
                python: "3.12".into(),
                path: "/path/to/env".into(),
                python_path: None,
            },
        );
        let json = serde_json::to_string(&response).unwrap();
        let parsed: serde_json::Value = serde_json::from_str(&json).unwrap();
        assert_eq!(parsed["data"]["name"], "myenv");
        assert_eq!(parsed["data"]["python"], "3.12");
    }

    #[test]
    fn test_json_response_with_vec_data() {
        let response = JsonResponse::success("list", vec!["env1", "env2", "env3"]);
        let json = serde_json::to_string(&response).unwrap();
        let parsed: serde_json::Value = serde_json::from_str(&json).unwrap();
        assert_eq!(parsed["data"].as_array().unwrap().len(), 3);
    }

    // ========================================
    // JsonErrorResponse Tests
    // ========================================

    #[test]
    fn test_json_error_response_creates_error_status() {
        let response = JsonErrorResponse::error("test", "ERR_CODE", "error message".into());
        assert_eq!(response.status, "error");
        assert_eq!(response.command, "test");
        assert_eq!(response.error.code, "ERR_CODE");
        assert_eq!(response.error.message, "error message");
        assert!(response.error.suggestion.is_none());
    }

    #[test]
    fn test_json_error_response_with_suggestion() {
        let response = JsonErrorResponse::error("test", "ERR", "msg".into())
            .with_suggestion("try this".into());
        assert_eq!(response.error.suggestion, Some("try this".into()));
    }

    #[test]
    fn test_json_error_without_suggestion_omits_field() {
        let response = JsonErrorResponse::error("test", "ERR_CODE", "message".into());
        let json = serde_json::to_string(&response).unwrap();
        assert!(!json.contains("suggestion"));
    }

    #[test]
    fn test_json_error_with_suggestion_includes_field() {
        let response = JsonErrorResponse::error("test", "ERR_CODE", "msg".into())
            .with_suggestion("try this".into());
        let json = serde_json::to_string(&response).unwrap();
        assert!(json.contains(r#""suggestion":"try this""#));
    }

    #[test]
    fn test_json_error_response_serialization() {
        let response = JsonErrorResponse::error("create", "ENV_EXISTS", "already exists".into())
            .with_suggestion("use --force".into());
        let json = serde_json::to_string(&response).unwrap();
        let parsed: serde_json::Value = serde_json::from_str(&json).unwrap();

        assert_eq!(parsed["status"], "error");
        assert_eq!(parsed["command"], "create");
        assert_eq!(parsed["error"]["code"], "ENV_EXISTS");
        assert_eq!(parsed["error"]["message"], "already exists");
        assert_eq!(parsed["error"]["suggestion"], "use --force");
    }

    // ========================================
    // ListEnvsData / VirtualenvInfo Tests
    // ========================================

    #[test]
    fn test_list_envs_data_empty() {
        let data = ListEnvsData {
            virtualenvs: vec![],
            total: 0,
        };
        let json = serde_json::to_string(&data).unwrap();
        let parsed: serde_json::Value = serde_json::from_str(&json).unwrap();
        assert_eq!(parsed["virtualenvs"].as_array().unwrap().len(), 0);
        assert_eq!(parsed["total"], 0);
    }

    #[test]
    fn test_list_envs_data_with_envs() {
        let data = ListEnvsData {
            virtualenvs: vec![
                VirtualenvInfo {
                    name: "env1".into(),
                    python: Some("3.12".into()),
                    path: "/path1".into(),
                    active: true,
                    created_at: None,
                    last_used: None,
                },
                VirtualenvInfo {
                    name: "env2".into(),
                    python: None,
                    path: "/path2".into(),
                    active: false,
                    created_at: None,
                    last_used: None,
                },
            ],
            total: 2,
        };
        let json = serde_json::to_string(&data).unwrap();
        let parsed: serde_json::Value = serde_json::from_str(&json).unwrap();
        assert_eq!(parsed["virtualenvs"].as_array().unwrap().len(), 2);
        assert_eq!(parsed["total"], 2);
    }

    #[test]
    fn test_virtualenv_info_without_python_omits_field() {
        let info = VirtualenvInfo {
            name: "test".into(),
            python: None,
            path: "/path".into(),
            active: false,
            created_at: None,
            last_used: None,
        };
        let json = serde_json::to_string(&info).unwrap();
        assert!(!json.contains("python"));
    }

    #[test]
    fn test_virtualenv_info_timestamps_present_when_some() {
        // Pins the additive-schema contract for the new fields: when
        // a value is present, it must serialize as a JSON string, not
        // be hidden by some accidental `skip_serializing_if` change.
        let info = VirtualenvInfo {
            name: "ent".into(),
            python: Some("3.12".into()),
            path: "/p".into(),
            active: false,
            created_at: Some("2024-01-15T10:30:00+00:00".to_string()),
            last_used: Some("2026-06-02T12:00:00+00:00".to_string()),
        };
        let json = serde_json::to_string(&info).unwrap();
        assert!(
            json.contains("\"created_at\":\"2024-01-15T10:30:00+00:00\""),
            "created_at must serialize when Some: {json}"
        );
        assert!(
            json.contains("\"last_used\":\"2026-06-02T12:00:00+00:00\""),
            "last_used must serialize when Some: {json}"
        );
    }

    #[test]
    fn test_virtualenv_info_timestamps_omitted_when_none() {
        // Companion to the Some test: the additive schema requires
        // *both* arms — Some serializes, None omits. Old consumers
        // depend on absent keys, not on `null`.
        let info = VirtualenvInfo {
            name: "ent".into(),
            python: None,
            path: "/p".into(),
            active: false,
            created_at: None,
            last_used: None,
        };
        let json = serde_json::to_string(&info).unwrap();
        assert!(!json.contains("created_at"), "{json}");
        assert!(!json.contains("last_used"), "{json}");
    }

    #[test]
    fn test_virtualenv_info_with_python() {
        let info = VirtualenvInfo {
            name: "test".into(),
            python: Some("3.11".into()),
            path: "/path".into(),
            active: true,
            created_at: None,
            last_used: None,
        };
        let json = serde_json::to_string(&info).unwrap();
        assert!(json.contains(r#""python":"3.11""#));
        assert!(json.contains(r#""active":true"#));
    }

    // ========================================
    // ListPythonsData / PythonInfo Tests
    // ========================================

    #[test]
    fn test_list_pythons_data_empty() {
        let data = ListPythonsData {
            pythons: vec![],
            total: 0,
        };
        let json = serde_json::to_string(&data).unwrap();
        let parsed: serde_json::Value = serde_json::from_str(&json).unwrap();
        assert_eq!(parsed["pythons"].as_array().unwrap().len(), 0);
    }

    #[test]
    fn test_list_pythons_data_with_pythons() {
        let data = ListPythonsData {
            pythons: vec![
                PythonInfo {
                    version: "3.12.0".into(),
                    implementation: Some("cpython".into()),
                    path: Some("/usr/bin/python3.12".into()),
                },
                PythonInfo {
                    version: "3.11.0".into(),
                    implementation: None,
                    path: None,
                },
            ],
            total: 2,
        };
        let json = serde_json::to_string(&data).unwrap();
        let parsed: serde_json::Value = serde_json::from_str(&json).unwrap();
        assert_eq!(parsed["pythons"].as_array().unwrap().len(), 2);
    }

    #[test]
    fn test_python_info_without_path_omits_field() {
        let info = PythonInfo {
            version: "3.12".into(),
            implementation: None,
            path: None,
        };
        let json = serde_json::to_string(&info).unwrap();
        assert!(!json.contains("path"));
        assert!(!json.contains("implementation"));
    }

    #[test]
    fn test_python_info_with_path() {
        let info = PythonInfo {
            version: "3.12".into(),
            implementation: Some("cpython".into()),
            path: Some("/usr/bin/python3".into()),
        };
        let json = serde_json::to_string(&info).unwrap();
        assert!(json.contains(r#""path":"/usr/bin/python3""#));
        assert!(json.contains(r#""implementation":"cpython""#));
    }

    #[test]
    fn test_python_info_without_implementation_omits_field() {
        let info = PythonInfo {
            version: "3.12".into(),
            implementation: None,
            path: Some("/usr/bin/python3".into()),
        };
        let json = serde_json::to_string(&info).unwrap();
        assert!(!json.contains("implementation"));
        assert!(json.contains(r#""version":"3.12""#));
    }

    // ========================================
    // CreateData Tests
    // ========================================

    #[test]
    fn test_create_data_serialization() {
        let data = CreateData {
            name: "myenv".into(),
            python: "3.12".into(),
            path: "/home/user/.scoop/virtualenvs/myenv".into(),
            python_path: None,
        };
        let json = serde_json::to_string(&data).unwrap();
        let parsed: serde_json::Value = serde_json::from_str(&json).unwrap();
        assert_eq!(parsed["name"], "myenv");
        assert_eq!(parsed["python"], "3.12");
        assert!(parsed["path"].as_str().unwrap().contains("myenv"));
    }

    // ========================================
    // UseData Tests
    // ========================================

    #[test]
    fn test_use_data_local_mode() {
        let data = UseData {
            name: "myenv".into(),
            mode: "local",
            version_file: Some("/project/.scoop-version".into()),
            symlink: None,
        };
        let json = serde_json::to_string(&data).unwrap();
        let parsed: serde_json::Value = serde_json::from_str(&json).unwrap();
        assert_eq!(parsed["mode"], "local");
        assert!(parsed["version_file"].is_string());
        assert!(parsed.get("symlink").is_none());
    }

    #[test]
    fn test_use_data_global_mode() {
        let data = UseData {
            name: "myenv".into(),
            mode: "global",
            version_file: None,
            symlink: None,
        };
        let json = serde_json::to_string(&data).unwrap();
        assert_eq!(data.mode, "global");
        assert!(!json.contains("version_file"));
        assert!(!json.contains("symlink"));
    }

    #[test]
    fn test_use_data_with_symlink() {
        let data = UseData {
            name: "myenv".into(),
            mode: "local",
            version_file: Some("/project/.scoop-version".into()),
            symlink: Some("/project/.venv".into()),
        };
        let json = serde_json::to_string(&data).unwrap();
        assert!(json.contains(r#""symlink":"/project/.venv""#));
    }

    // ========================================
    // RemoveData Tests
    // ========================================

    #[test]
    fn test_remove_data_serialization() {
        let data = RemoveData {
            name: "oldenv".into(),
            path: "/home/user/.scoop/virtualenvs/oldenv".into(),
        };
        let json = serde_json::to_string(&data).unwrap();
        let parsed: serde_json::Value = serde_json::from_str(&json).unwrap();
        assert_eq!(parsed["name"], "oldenv");
        assert!(parsed["path"].as_str().unwrap().contains("oldenv"));
    }

    // ========================================
    // InstallData Tests
    // ========================================

    #[test]
    fn test_install_data_with_path() {
        let data = InstallData {
            version: "3.12.0".into(),
            path: Some("/usr/local/bin/python3.12".into()),
        };
        let json = serde_json::to_string(&data).unwrap();
        assert!(json.contains(r#""path""#));
    }

    #[test]
    fn test_install_data_without_path() {
        let data = InstallData {
            version: "3.12.0".into(),
            path: None,
        };
        let json = serde_json::to_string(&data).unwrap();
        assert!(!json.contains("path"));
    }

    // ========================================
    // UninstallData Tests
    // ========================================

    #[test]
    fn test_uninstall_data_serialization() {
        let data = UninstallData {
            version: "3.11.0".into(),
            removed_envs: None,
        };
        let json = serde_json::to_string(&data).unwrap();
        let parsed: serde_json::Value = serde_json::from_str(&json).unwrap();
        assert_eq!(parsed["version"], "3.11.0");
    }

    // ========================================
    // Edge Cases
    // ========================================

    #[test]
    fn test_json_with_unicode_characters() {
        let info = VirtualenvInfo {
            name: "한글환경".into(),
            python: Some("3.12".into()),
            path: "/경로/테스트".into(),
            active: true,
            created_at: None,
            last_used: None,
        };
        let json = serde_json::to_string(&info).unwrap();
        // Should serialize correctly
        let parsed: serde_json::Value = serde_json::from_str(&json).unwrap();
        assert_eq!(parsed["name"], "한글환경");
        assert_eq!(parsed["path"], "/경로/테스트");
    }

    #[test]
    fn test_json_with_empty_strings() {
        let data = CreateData {
            name: "".into(),
            python: "".into(),
            path: "".into(),
            python_path: None,
        };
        let json = serde_json::to_string(&data).unwrap();
        let parsed: serde_json::Value = serde_json::from_str(&json).unwrap();
        assert_eq!(parsed["name"], "");
        assert_eq!(parsed["python"], "");
    }

    #[test]
    fn test_json_with_special_characters() {
        let data = CreateData {
            name: r#"test"env"#.into(),
            python: "3.12".into(),
            path: r#"/path/with\backslash"#.into(),
            python_path: None,
        };
        let json = serde_json::to_string(&data).unwrap();
        // JSON escaping should handle special chars
        let parsed: serde_json::Value = serde_json::from_str(&json).unwrap();
        assert_eq!(parsed["name"], r#"test"env"#);
        assert_eq!(parsed["path"], r#"/path/with\backslash"#);
    }

    #[test]
    fn test_json_with_long_strings() {
        let long_name = "a".repeat(1000);
        let data = CreateData {
            name: long_name.clone(),
            python: "3.12".into(),
            path: "/path".into(),
            python_path: None,
        };
        let json = serde_json::to_string(&data).unwrap();
        let parsed: serde_json::Value = serde_json::from_str(&json).unwrap();
        assert_eq!(parsed["name"].as_str().unwrap().len(), 1000);
    }

    #[test]
    fn test_json_path_with_spaces() {
        let data = CreateData {
            name: "myenv".into(),
            python: "3.12".into(),
            path: "/path/with spaces/to/env".into(),
            python_path: None,
        };
        let json = serde_json::to_string(&data).unwrap();
        let parsed: serde_json::Value = serde_json::from_str(&json).unwrap();
        assert_eq!(parsed["path"], "/path/with spaces/to/env");
    }

    #[test]
    fn test_json_with_newlines_and_tabs() {
        let data = JsonErrorResponse::error("test", "ERR", "line1\nline2\ttab".into());
        let json = serde_json::to_string(&data).unwrap();
        let parsed: serde_json::Value = serde_json::from_str(&json).unwrap();
        assert_eq!(parsed["error"]["message"], "line1\nline2\ttab");
    }

    #[test]
    fn test_json_roundtrip_integrity() {
        let original = ListEnvsData {
            virtualenvs: vec![
                VirtualenvInfo {
                    name: "env1".into(),
                    python: Some("3.12".into()),
                    path: "/path/to/env1".into(),
                    active: true,
                    created_at: None,
                    last_used: None,
                },
                VirtualenvInfo {
                    name: "env2".into(),
                    python: None,
                    path: "/path/to/env2".into(),
                    active: false,
                    created_at: None,
                    last_used: None,
                },
            ],
            total: 2,
        };

        let json = serde_json::to_string(&original).unwrap();
        let parsed: serde_json::Value = serde_json::from_str(&json).unwrap();

        // Verify structure integrity
        assert_eq!(parsed["virtualenvs"].as_array().unwrap().len(), 2);
        assert_eq!(parsed["virtualenvs"][0]["name"], "env1");
        assert_eq!(parsed["virtualenvs"][0]["active"], true);
        assert_eq!(parsed["virtualenvs"][1]["name"], "env2");
        assert!(parsed["virtualenvs"][1].get("python").is_none());
    }

    // ========================================
    // PackagesInfo Tests
    // ========================================

    #[test]
    fn packages_info_new_under_limit_not_truncated() {
        let packages = vec![
            ("pkg1".to_string(), "1.0".to_string()),
            ("pkg2".to_string(), "2.0".to_string()),
        ];
        let info = PackagesInfo::new(&packages, 5);

        assert_eq!(info.total, 2);
        assert_eq!(info.items.len(), 2);
        assert!(!info.truncated);
        assert_eq!(info.remaining(), 0);
    }

    #[test]
    fn packages_info_new_equal_limit_not_truncated() {
        let packages: Vec<(String, String)> = (0..5)
            .map(|i| (format!("pkg{}", i), format!("{}.0", i)))
            .collect();
        let info = PackagesInfo::new(&packages, 5);

        assert_eq!(info.total, 5);
        assert_eq!(info.items.len(), 5);
        assert!(!info.truncated);
        assert_eq!(info.remaining(), 0);
    }

    #[test]
    fn packages_info_new_over_limit_truncated() {
        let packages: Vec<(String, String)> = (0..10)
            .map(|i| (format!("pkg{}", i), format!("{}.0", i)))
            .collect();
        let info = PackagesInfo::new(&packages, 5);

        assert_eq!(info.total, 10);
        assert_eq!(info.items.len(), 5);
        assert!(info.truncated);
        assert_eq!(info.remaining(), 5);
    }

    #[test]
    fn packages_info_new_empty_packages() {
        let packages: Vec<(String, String)> = vec![];
        let info = PackagesInfo::new(&packages, 5);

        assert_eq!(info.total, 0);
        assert_eq!(info.items.len(), 0);
        assert!(!info.truncated);
        assert_eq!(info.remaining(), 0);
    }

    #[test]
    fn packages_info_new_zero_limit_all_truncated() {
        let packages = vec![
            ("pkg1".to_string(), "1.0".to_string()),
            ("pkg2".to_string(), "2.0".to_string()),
        ];
        let info = PackagesInfo::new(&packages, 0);

        assert_eq!(info.total, 2);
        assert_eq!(info.items.len(), 0);
        assert!(info.truncated);
        assert_eq!(info.remaining(), 2);
    }

    #[test]
    fn packages_info_new_preserves_order() {
        let packages = vec![
            ("zeta".to_string(), "3.0".to_string()),
            ("alpha".to_string(), "1.0".to_string()),
            ("beta".to_string(), "2.0".to_string()),
        ];
        let info = PackagesInfo::new(&packages, 2);

        assert_eq!(info.items[0].name, "zeta");
        assert_eq!(info.items[1].name, "alpha");
        // beta is truncated
        assert_eq!(info.remaining(), 1);
    }

    #[test]
    fn packages_info_new_copies_version_correctly() {
        let packages = vec![
            ("numpy".to_string(), "1.24.3".to_string()),
            ("pandas".to_string(), "2.0.1".to_string()),
        ];
        let info = PackagesInfo::new(&packages, 5);

        assert_eq!(info.items[0].version, "1.24.3");
        assert_eq!(info.items[1].version, "2.0.1");
    }

    #[test]
    fn packages_info_serializes_correctly() {
        let packages = vec![
            ("pkg1".to_string(), "1.0".to_string()),
            ("pkg2".to_string(), "2.0".to_string()),
        ];
        let info = PackagesInfo::new(&packages, 1);

        let json = serde_json::to_string(&info).unwrap();
        let parsed: serde_json::Value = serde_json::from_str(&json).unwrap();

        assert_eq!(parsed["total"], 2);
        assert_eq!(parsed["truncated"], true);
        assert_eq!(parsed["items"].as_array().unwrap().len(), 1);
        assert_eq!(parsed["items"][0]["name"], "pkg1");
    }
}
