//! Clone orchestration logic.
//!
//! Coordinates the full lifecycle of spawning a child instance on Railway:
//! service creation, environment configuration, Docker image or source-based
//! deployment, volume attachment, domain assignment, and deployment trigger.
//!
//! Supports two deployment modes:
//! - **Docker**: Deploy from a pre-built image (existing behavior)
//! - **Source**: Create a branch on a GitHub repo and build from source on Railway

use crate::railway::{RailwayClient, RailwayError};
use serde::{Deserialize, Serialize};

/// Configuration for clone operations.
#[derive(Clone, Debug)]
pub struct CloneConfig {
    /// Docker image to deploy (e.g., `ghcr.io/compusophy/tempo-x402:latest`).
    /// If None and `source_repo` is set, uses source-based builds.
    pub docker_image: Option<String>,
    /// GitHub repo for source-based builds (e.g., `compusophy-bot/tempo-x402`).
    /// Each clone gets its own `clone/{name}` branch.
    pub source_repo: Option<String>,
    /// GitHub token for branch creation via GitHub REST API.
    /// Required when `source_repo` is set.
    pub github_token: Option<String>,
    /// RPC URL for the Tempo chain
    pub rpc_url: String,
    /// URL of this (parent) instance, so children can register back
    pub self_url: String,
    /// Maximum number of children this instance can spawn
    pub max_children: u32,
    /// CPU limit for child instances in millicores (default: 2000 = 2 vCPU)
    pub clone_cpu_millicores: u32,
    /// Memory limit for child instances in MB (default: 2048 = 2GB)
    pub clone_memory_mb: u32,
    /// Extra env vars to inject into children (e.g., soul config)
    pub child_env_vars: std::collections::HashMap<String, String>,
}

/// Result of a successful clone operation.
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct CloneResult {
    /// Unique ID for the child instance
    pub instance_id: String,
    /// Public URL of the child (Railway domain)
    pub url: String,
    /// Railway service ID
    pub railway_service_id: String,
    /// Railway deployment ID
    pub deployment_id: String,
    /// GitHub branch name (only for source-based clones)
    pub branch: Option<String>,
    /// Railway volume ID — MUST be stored for cleanup on delete.
    pub volume_id: Option<String>,
    /// Borg-style ordinal designation: "one", "two", etc.
    pub designation: String,
    /// Clone's own GitHub repo (e.g., "compusophy-bot/borg-0-2").
    /// None if repo creation failed (falls back to shared source).
    pub clone_repo: Option<String>,
}

#[derive(Debug, thiserror::Error)]
pub enum CloneError {
    #[error("Railway API error: {0}")]
    Railway(#[from] RailwayError),

    #[error("clone limit reached: {current}/{max} children")]
    LimitReached { current: u32, max: u32 },

    #[error("clone error: {0}")]
    Other(String),
}

/// Orchestrates the clone flow: create service, configure, deploy.
pub struct CloneOrchestrator {
    railway: RailwayClient,
    config: CloneConfig,
}

impl CloneOrchestrator {
    pub fn new(railway: RailwayClient, config: CloneConfig) -> Self {
        Self { railway, config }
    }

    /// Spawn a new child clone on Railway.
    ///
    /// Creates a service, then configures and deploys it. If any step after
    /// service creation fails, the service is cleaned up before returning the error.
    ///
    /// The `instance_id` should be pre-generated by the caller (e.g., after
    /// reserving a DB slot) so the same ID is used throughout the flow.
    pub async fn spawn_clone(
        &self,
        instance_id: &str,
        parent_address: &str,
    ) -> Result<CloneResult, CloneError> {
        self.spawn_clone_with_extra_vars(
            instance_id,
            parent_address,
            &std::collections::HashMap::new(),
        )
        .await
    }

    /// Spawn a clone with additional env vars merged on top of `child_env_vars`.
    /// Used by `/clone/specialist` to inject specialization without thread-unsafe `set_var`.
    pub async fn spawn_clone_with_extra_vars(
        &self,
        instance_id: &str,
        parent_address: &str,
        extra_vars: &std::collections::HashMap<String, String>,
    ) -> Result<CloneResult, CloneError> {
        // Borg-style ordinal designation: "one", "two", "three", ...
        let designation = extra_vars
            .get("DRONE_DESIGNATION")
            .cloned()
            .unwrap_or_else(|| format!("drone-{}", &instance_id[..8]));
        let service_name = designation.clone();

        tracing::info!(
            instance_id = %instance_id,
            service_name = %service_name,
            "Spawning clone"
        );

        // 1. Create service
        let service_id = self.railway.create_service(&service_name).await?;
        tracing::info!(service_id = %service_id, "Railway service created");

        // All subsequent steps run with cleanup-on-failure
        match self
            .spawn_clone_inner(
                &service_id,
                instance_id,
                &designation,
                parent_address,
                extra_vars,
            )
            .await
        {
            Ok(result) => Ok(result),
            Err(e) => {
                tracing::error!(
                    service_id = %service_id,
                    error = %e,
                    "Clone failed after service creation, cleaning up"
                );
                // Clean up Railway service
                if let Err(cleanup_err) = self.railway.delete_service(&service_id).await {
                    tracing::error!(
                        service_id = %service_id,
                        error = %cleanup_err,
                        "Cleanup of Railway service failed"
                    );
                } else {
                    tracing::info!(service_id = %service_id, "Railway service cleaned up");
                }
                // Clean up GitHub branch (best-effort)
                let branch = format!("clone/{}", &instance_id[..8]);
                if let (Some(ref repo), Some(ref token)) =
                    (&self.config.source_repo, &self.config.github_token)
                {
                    if let Err(branch_err) = delete_github_branch(token, repo, &branch).await {
                        tracing::warn!(
                            branch = %branch,
                            error = %branch_err,
                            "Cleanup of GitHub branch failed (best-effort)"
                        );
                    }
                }
                Err(e)
            }
        }
    }

    /// Inner clone steps after service creation. Separated so spawn_clone()
    /// can clean up the service if any of these fail.
    async fn spawn_clone_inner(
        &self,
        service_id: &str,
        instance_id: &str,
        designation: &str,
        parent_address: &str,
        extra_vars: &std::collections::HashMap<String, String>,
    ) -> Result<CloneResult, CloneError> {
        // 2. Get default environment
        let env_id = self.railway.get_default_environment().await?;

        // Deploy from shared colony fork. Each clone uses the same source repo
        // (compusophy-bot/tempo-x402) and differentiates via vm/{id} branches
        // when it starts making code changes. No per-clone repos — they cause
        // Railway permission issues and unnecessary complexity.
        let clone_repo: Option<String> = None;
        let deploy_repo = self.config.source_repo.as_deref();
        let use_source = deploy_repo.is_some();
        let branch_name = if use_source {
            Some("main".to_string())
        } else {
            None
        };

        // 4. Set environment variables
        let mut env_map = serde_json::Map::new();
        env_map.insert("AUTO_BOOTSTRAP".into(), "true".into());
        env_map.insert("INSTANCE_ID".into(), instance_id.into());
        env_map.insert("PARENT_URL".into(), self.config.self_url.clone().into());
        env_map.insert("PARENT_ADDRESS".into(), parent_address.into());
        env_map.insert("IDENTITY_PATH".into(), "/data/identity.json".into());
        env_map.insert("DB_PATH".into(), "/data/gateway.db".into());
        env_map.insert("NONCE_DB_PATH".into(), "/data/x402-nonces.db".into());
        env_map.insert("RPC_URL".into(), self.config.rpc_url.clone().into());
        env_map.insert("SPA_DIR".into(), "/app/spa".into());
        env_map.insert("PORT".into(), "4023".into());

        // Inject extra env vars (soul config, API keys, etc.)
        for (key, value) in &self.config.child_env_vars {
            env_map.insert(key.clone(), value.clone().into());
        }

        // Inject per-clone overrides (e.g., specialization env vars)
        for (key, value) in extra_vars {
            env_map.insert(key.clone(), value.clone().into());
        }

        // Override SOUL_FORK_REPO with clone's own repo if created
        // This makes the clone push to its own repo instead of the shared colony fork
        if let Some(ref repo) = clone_repo {
            env_map.insert("SOUL_FORK_REPO".into(), repo.clone().into());
            // SOUL_UPSTREAM_REPO stays as the colony fork — PRs flow upstream
            if let Some(ref source) = self.config.source_repo {
                env_map.insert("SOUL_UPSTREAM_REPO".into(), source.clone().into());
            }
        }

        let env_vars = serde_json::Value::Object(env_map);
        self.railway
            .set_variables(service_id, &env_id, env_vars)
            .await?;
        tracing::info!("Environment variables configured");

        // 5. Set deployment source (clone's own repo > shared source > Docker)
        if let (Some(repo), Some(ref branch)) = (deploy_repo, &branch_name) {
            self.railway
                .connect_repo(service_id, &env_id, repo, branch)
                .await?;
            tracing::info!(repo = %repo, branch = %branch, "Source repo connected");
        } else if let Some(ref image) = self.config.docker_image {
            self.railway.set_docker_image(service_id, image).await?;
            tracing::info!(image = %image, "Docker image set");
        } else {
            return Err(CloneError::Other(
                "no deployment source: set DOCKER_IMAGE or CLONE_SOURCE_REPO".to_string(),
            ));
        }

        // 6. Add volume (best-effort — clone works without persistent storage)
        // Capture volume_id so caller can store it for cleanup on delete.
        let volume_id = match self.railway.add_volume(service_id, &env_id, "/data").await {
            Ok(vid) => {
                tracing::info!(volume_id = %vid, "Volume attached at /data");
                Some(vid)
            }
            Err(e) => {
                tracing::warn!(error = %e, "Volume attachment failed (best-effort, continuing)");
                None
            }
        };

        // 7. Set resource limits (best-effort — Railway defaults are fine)
        if self.config.clone_cpu_millicores > 0 || self.config.clone_memory_mb > 0 {
            match self
                .railway
                .update_service_resources(
                    service_id,
                    &env_id,
                    self.config.clone_cpu_millicores,
                    self.config.clone_memory_mb,
                )
                .await
            {
                Ok(_) => tracing::info!(
                    cpu = self.config.clone_cpu_millicores,
                    memory_mb = self.config.clone_memory_mb,
                    "Resource limits configured"
                ),
                Err(e) => {
                    tracing::warn!(error = %e, "Resource limits failed (best-effort, continuing)")
                }
            }
        }

        // 8. Create domain
        let url = self.railway.create_domain(service_id, &env_id).await?;
        tracing::info!(url = %url, "Domain created");

        // 8b. Set ALLOWED_ORIGINS now that we know the domain
        let origins = format!("{},{}", url, self.config.self_url);
        let origins_var = serde_json::Value::Object({
            let mut m = serde_json::Map::new();
            m.insert("ALLOWED_ORIGINS".into(), origins.into());
            m
        });
        let _ = self
            .railway
            .set_variables(service_id, &env_id, origins_var)
            .await;

        // 9. Deploy (skip for source-based — the deployment trigger already builds)
        let deployment_id = if branch_name.is_none() {
            let id = self.railway.deploy_service(service_id, &env_id).await?;
            tracing::info!(deployment_id = %id, "Deployment triggered");
            id
        } else {
            tracing::info!("Source-based clone — deployment trigger will build automatically");
            "trigger-based".to_string()
        };

        Ok(CloneResult {
            instance_id: instance_id.to_string(),
            url,
            railway_service_id: service_id.to_string(),
            deployment_id,
            branch: branch_name,
            volume_id,
            designation: designation.to_string(),
            clone_repo,
        })
    }

    /// Update a clone's branch to main's HEAD and redeploy.
    ///
    /// For source-based clones, fast-forwards the clone's branch to main's latest
    /// commit before triggering a redeploy. For Docker clones, just redeploys.
    pub async fn redeploy_clone(&self, service_id: &str) -> Result<String, CloneError> {
        let env_id = self.railway.get_default_environment().await?;
        let result = self.railway.deploy_service(service_id, &env_id).await?;
        Ok(result)
    }

    /// Update a clone's GitHub branch to main's HEAD before redeploying.
    /// Best-effort: if the branch update fails, the redeploy still triggers
    /// (it'll just use the existing branch state).
    pub async fn update_and_redeploy_clone(
        &self,
        service_id: &str,
        instance_id: &str,
    ) -> Result<String, CloneError> {
        // Update branch to main if source-based
        if let (Some(ref repo), Some(ref token)) =
            (&self.config.source_repo, &self.config.github_token)
        {
            let branch = format!("clone/{}", &instance_id[..8]);
            match update_github_branch_to_main(token, repo, &branch).await {
                Ok(()) => {
                    tracing::info!(
                        instance_id = %instance_id,
                        branch = %branch,
                        "Updated clone branch to main"
                    );
                }
                Err(e) => {
                    tracing::warn!(
                        instance_id = %instance_id,
                        error = %e,
                        "Failed to update clone branch (will redeploy anyway)"
                    );
                }
            }
        }

        self.redeploy_clone(service_id).await
    }

    /// Delete a Railway volume. MUST be called BEFORE delete_service.
    pub async fn delete_volume(&self, volume_id: &str) -> Result<(), CloneError> {
        self.railway.delete_volume(volume_id).await?;
        Ok(())
    }

    /// Delete a Railway service. Delegates to the Railway client.
    pub async fn delete_service(&self, service_id: &str) -> Result<(), CloneError> {
        self.railway.delete_service(service_id).await?;
        Ok(())
    }

    pub fn config(&self) -> &CloneConfig {
        &self.config
    }
}

/// Create a dedicated GitHub repo for a clone — the stem cell differentiation model.
///
/// Each clone gets its own repo (e.g., `compusophy-bot/borg-0-2`), initialized
/// from the colony baseline. The clone pushes to its own repo, Railway builds
/// from it, and the clone evolves independently. Good changes flow upstream via PRs.
///
/// Returns the new repo's full name (e.g., "compusophy-bot/borg-0-2").
pub async fn create_clone_repo(
    token: &str,
    source_repo: &str,
    designation: &str,
) -> Result<String, CloneError> {
    let http = reqwest::Client::builder()
        .timeout(std::time::Duration::from_secs(30))
        .redirect(reqwest::redirect::Policy::limited(5))
        .build()
        .map_err(|e| CloneError::Other(format!("HTTP client error: {e}")))?;

    // Extract the org from source_repo (e.g., "compusophy-bot" from "compusophy-bot/tempo-x402")
    let org = source_repo
        .split('/')
        .next()
        .ok_or_else(|| CloneError::Other("invalid source_repo format".to_string()))?;

    let repo_name = designation.to_string();
    let full_name = format!("{org}/{repo_name}");

    // 1. Create the repo under the org (or user)
    // Try org endpoint first, fall back to user endpoint
    let create_url = format!("https://api.github.com/orgs/{org}/repos");
    let body = serde_json::json!({
        "name": repo_name,
        "description": format!("x402 autonomous agent: {designation}"),
        "private": false,
        "auto_init": false,
    });

    let resp = http
        .post(&create_url)
        .header("Authorization", format!("Bearer {token}"))
        .header("Accept", "application/vnd.github+json")
        .header("User-Agent", "x402-node")
        .header("X-GitHub-Api-Version", "2022-11-28")
        .json(&body)
        .send()
        .await
        .map_err(|e| CloneError::Other(format!("GitHub create repo error: {e}")))?;

    if !resp.status().is_success() {
        // Maybe it's a user account, not an org — try user endpoint
        let user_url = "https://api.github.com/user/repos";
        let resp2 = http
            .post(user_url)
            .header("Authorization", format!("Bearer {token}"))
            .header("Accept", "application/vnd.github+json")
            .header("User-Agent", "x402-node")
            .header("X-GitHub-Api-Version", "2022-11-28")
            .json(&body)
            .send()
            .await
            .map_err(|e| CloneError::Other(format!("GitHub create repo (user) error: {e}")))?;

        if !resp2.status().is_success() {
            let status = resp2.status();
            let body = resp2.text().await.unwrap_or_default();
            // 422 = repo already exists — that's fine, we can reuse it
            if status.as_u16() == 422 && body.contains("already exists") {
                tracing::info!(repo = %full_name, "Clone repo already exists — reusing");
                // Continue to push code below
            } else {
                return Err(CloneError::Other(format!(
                    "GitHub create repo failed (HTTP {status}): {}",
                    body.chars().take(200).collect::<String>()
                )));
            }
        }
    }

    tracing::info!(repo = %full_name, "Created clone repo");

    // 2. Mirror the source repo into the new repo
    // Use git CLI since it handles auth via the token in the URL
    let source_url = format!("https://x-access-token:{token}@github.com/{source_repo}.git");
    let target_url = format!("https://x-access-token:{token}@github.com/{full_name}.git");

    // Clone bare from source
    let tmp_dir = format!("/tmp/clone-mirror-{designation}");
    let _ = tokio::fs::remove_dir_all(&tmp_dir).await; // clean up any previous attempt

    let clone_result = tokio::process::Command::new("git")
        .args(["clone", "--bare", &source_url, &tmp_dir])
        .output()
        .await
        .map_err(|e| CloneError::Other(format!("git clone --bare failed: {e}")))?;

    if !clone_result.status.success() {
        let stderr = String::from_utf8_lossy(&clone_result.stderr);
        let _ = tokio::fs::remove_dir_all(&tmp_dir).await;
        return Err(CloneError::Other(format!(
            "git clone --bare failed: {}",
            stderr.chars().take(200).collect::<String>()
        )));
    }

    // Push mirror to new repo
    let push_result = tokio::process::Command::new("git")
        .args(["push", "--mirror", &target_url])
        .current_dir(&tmp_dir)
        .output()
        .await
        .map_err(|e| CloneError::Other(format!("git push --mirror failed: {e}")))?;

    // Clean up temp dir
    let _ = tokio::fs::remove_dir_all(&tmp_dir).await;

    if !push_result.status.success() {
        let stderr = String::from_utf8_lossy(&push_result.stderr);
        return Err(CloneError::Other(format!(
            "git push --mirror failed: {}",
            stderr.chars().take(200).collect::<String>()
        )));
    }

    tracing::info!(
        source = %source_repo,
        target = %full_name,
        "Mirrored colony baseline into clone repo"
    );

    Ok(full_name)
}

/// Create a branch on a GitHub repo via the REST API.
///
/// 1. GET /repos/{repo}/git/ref/heads/main → get SHA
/// 2. POST /repos/{repo}/git/refs → create refs/heads/{branch}
async fn create_github_branch(token: &str, repo: &str, branch: &str) -> Result<(), CloneError> {
    let http = reqwest::Client::builder()
        .timeout(std::time::Duration::from_secs(15))
        .redirect(reqwest::redirect::Policy::limited(5))
        .build()
        .map_err(|e| CloneError::Other(format!("HTTP client error: {e}")))?;

    // 1. Get main branch SHA
    let ref_url = format!("https://api.github.com/repos/{repo}/git/ref/heads/main");
    let ref_resp = http
        .get(&ref_url)
        .header("Authorization", format!("Bearer {token}"))
        .header("Accept", "application/vnd.github+json")
        .header("User-Agent", "x402-node")
        .header("X-GitHub-Api-Version", "2022-11-28")
        .send()
        .await
        .map_err(|e| CloneError::Other(format!("GitHub API error (get ref): {e}")))?;

    if !ref_resp.status().is_success() {
        let status = ref_resp.status();
        let body = ref_resp.text().await.unwrap_or_default();
        return Err(CloneError::Other(format!(
            "GitHub GET ref failed (HTTP {status}): {body}"
        )));
    }

    let ref_json: serde_json::Value = ref_resp
        .json()
        .await
        .map_err(|e| CloneError::Other(format!("GitHub API parse error: {e}")))?;

    let sha = ref_json["object"]["sha"]
        .as_str()
        .ok_or_else(|| CloneError::Other("missing SHA in GitHub ref response".to_string()))?;

    // 2. Create new branch
    let create_url = format!("https://api.github.com/repos/{repo}/git/refs");
    let create_resp = http
        .post(&create_url)
        .header("Authorization", format!("Bearer {token}"))
        .header("Accept", "application/vnd.github+json")
        .header("User-Agent", "x402-node")
        .header("X-GitHub-Api-Version", "2022-11-28")
        .json(&serde_json::json!({
            "ref": format!("refs/heads/{branch}"),
            "sha": sha,
        }))
        .send()
        .await
        .map_err(|e| CloneError::Other(format!("GitHub API error (create ref): {e}")))?;

    if !create_resp.status().is_success() {
        let status = create_resp.status();
        let body = create_resp.text().await.unwrap_or_default();
        return Err(CloneError::Other(format!(
            "GitHub create branch failed (HTTP {status}): {body}"
        )));
    }

    Ok(())
}

/// Fast-forward a branch to main's HEAD via the GitHub REST API.
/// Used by the health probe to update clone branches before redeploying.
pub async fn update_github_branch_to_main(
    token: &str,
    repo: &str,
    branch: &str,
) -> Result<(), CloneError> {
    let http = reqwest::Client::builder()
        .timeout(std::time::Duration::from_secs(15))
        .redirect(reqwest::redirect::Policy::limited(5))
        .build()
        .map_err(|e| CloneError::Other(format!("HTTP client error: {e}")))?;

    // 1. Get main branch SHA
    let ref_url = format!("https://api.github.com/repos/{repo}/git/ref/heads/main");
    let ref_resp = http
        .get(&ref_url)
        .header("Authorization", format!("Bearer {token}"))
        .header("Accept", "application/vnd.github+json")
        .header("User-Agent", "x402-node")
        .header("X-GitHub-Api-Version", "2022-11-28")
        .send()
        .await
        .map_err(|e| CloneError::Other(format!("GitHub API error (get ref): {e}")))?;

    if !ref_resp.status().is_success() {
        let status = ref_resp.status();
        let body = ref_resp.text().await.unwrap_or_default();
        return Err(CloneError::Other(format!(
            "GitHub GET ref failed (HTTP {status}): {}",
            body.chars().take(200).collect::<String>()
        )));
    }

    let ref_json: serde_json::Value = ref_resp
        .json()
        .await
        .map_err(|e| CloneError::Other(format!("GitHub API parse error: {e}")))?;

    let main_sha = ref_json["object"]["sha"]
        .as_str()
        .ok_or_else(|| CloneError::Other("missing SHA in GitHub ref response".to_string()))?;

    // 2. Force-update the branch ref to point to main's SHA
    let update_url = format!("https://api.github.com/repos/{repo}/git/refs/heads/{branch}");
    let update_resp = http
        .patch(&update_url)
        .header("Authorization", format!("Bearer {token}"))
        .header("Accept", "application/vnd.github+json")
        .header("User-Agent", "x402-node")
        .header("X-GitHub-Api-Version", "2022-11-28")
        .json(&serde_json::json!({
            "sha": main_sha,
            "force": true,
        }))
        .send()
        .await
        .map_err(|e| CloneError::Other(format!("GitHub API error (update ref): {e}")))?;

    if !update_resp.status().is_success() {
        let status = update_resp.status();
        let body = update_resp.text().await.unwrap_or_default();
        return Err(CloneError::Other(format!(
            "GitHub update branch failed (HTTP {status}): {}",
            body.chars().take(200).collect::<String>()
        )));
    }

    Ok(())
}

/// Delete a branch on a GitHub repo via the REST API (best-effort cleanup).
pub async fn delete_github_branch(token: &str, repo: &str, branch: &str) -> Result<(), CloneError> {
    let http = reqwest::Client::builder()
        .timeout(std::time::Duration::from_secs(15))
        .redirect(reqwest::redirect::Policy::limited(5))
        .build()
        .map_err(|e| CloneError::Other(format!("HTTP client error: {e}")))?;

    let url = format!("https://api.github.com/repos/{repo}/git/refs/heads/{branch}");
    let resp = http
        .delete(&url)
        .header("Authorization", format!("Bearer {token}"))
        .header("Accept", "application/vnd.github+json")
        .header("User-Agent", "x402-node")
        .header("X-GitHub-Api-Version", "2022-11-28")
        .send()
        .await
        .map_err(|e| CloneError::Other(format!("GitHub API error (delete ref): {e}")))?;

    if !resp.status().is_success() && resp.status().as_u16() != 404 {
        let status = resp.status();
        let body = resp.text().await.unwrap_or_default();
        return Err(CloneError::Other(format!(
            "GitHub delete branch failed (HTTP {status}): {body}"
        )));
    }

    Ok(())
}

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

    #[test]
    fn test_clone_config_docker() {
        let config = CloneConfig {
            docker_image: Some("ghcr.io/compusophy/tempo-x402:latest".to_string()),
            source_repo: None,
            github_token: None,
            rpc_url: "https://rpc.moderato.tempo.xyz".to_string(),
            self_url: "https://my-instance.up.railway.app".to_string(),
            max_children: 10,
            clone_cpu_millicores: 2000,
            clone_memory_mb: 2048,
            child_env_vars: std::collections::HashMap::new(),
        };
        assert_eq!(config.max_children, 10);
        assert_eq!(config.clone_cpu_millicores, 2000);
        assert!(config.docker_image.is_some());
        assert!(config.source_repo.is_none());
    }

    #[test]
    fn test_clone_config_source() {
        let config = CloneConfig {
            docker_image: None,
            source_repo: Some("compusophy-bot/tempo-x402".to_string()),
            github_token: Some("ghp_test".to_string()),
            rpc_url: "https://rpc.moderato.tempo.xyz".to_string(),
            self_url: "https://my-instance.up.railway.app".to_string(),
            max_children: 5,
            clone_cpu_millicores: 2000,
            clone_memory_mb: 2048,
            child_env_vars: std::collections::HashMap::new(),
        };
        assert!(config.docker_image.is_none());
        assert_eq!(
            config.source_repo.as_deref(),
            Some("compusophy-bot/tempo-x402")
        );
    }

    #[test]
    fn test_clone_error_display() {
        let err = CloneError::LimitReached {
            current: 10,
            max: 10,
        };
        assert!(err.to_string().contains("10/10"));
    }
}
