use crate::scheduler::DurableScheduler;
use chrono::{DateTime, Utc};
use roboticus_core::SurvivalTier;
use serde::{Deserialize, Serialize};
use std::sync::Arc;
use tokio::sync::{RwLock, watch};

/// Cached treasury state, updated by the treasury loop, read by everyone else.
#[derive(Debug, Clone)]
pub struct CachedTreasuryState {
    pub usdc_balance: f64,
    pub native_balance: f64,
    pub survival_tier: SurvivalTier,
    pub last_updated: Option<DateTime<Utc>>,
}

impl Default for CachedTreasuryState {
    fn default() -> Self {
        Self {
            usdc_balance: 0.0,
            native_balance: 0.0,
            survival_tier: SurvivalTier::Critical,
            last_updated: None,
        }
    }
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct TickContext {
    pub credit_balance: f64,
    pub usdc_balance: f64,
    pub survival_tier: SurvivalTier,
    pub timestamp: DateTime<Utc>,
}

#[derive(Debug)]
pub struct HeartbeatDaemon {
    pub interval_ms: u64,
    pub original_interval_ms: u64,
    pub running: bool,
}

impl HeartbeatDaemon {
    pub fn new(interval_ms: u64) -> Self {
        Self {
            interval_ms,
            original_interval_ms: interval_ms,
            running: false,
        }
    }

    pub fn should_adjust_interval(&self, tier: &SurvivalTier) -> Option<u64> {
        const MAX_HEARTBEAT_INTERVAL_MS: u64 = 3_600_000;
        const MIN_HEARTBEAT_INTERVAL_MS: u64 = 10_000;
        match tier {
            SurvivalTier::Normal | SurvivalTier::High => {
                if self.interval_ms != self.original_interval_ms {
                    Some(self.original_interval_ms)
                } else {
                    None
                }
            }
            tier => {
                let new = match tier {
                    SurvivalTier::LowCompute | SurvivalTier::Critical => self.interval_ms * 2,
                    SurvivalTier::Dead => self.interval_ms * 10,
                    _ => self.interval_ms * 2,
                };
                let max = match tier {
                    SurvivalTier::Dead => MAX_HEARTBEAT_INTERVAL_MS,
                    _ => 300_000,
                };
                Some(new.clamp(MIN_HEARTBEAT_INTERVAL_MS, max))
            }
        }
    }
}

pub fn build_tick_context(credit_balance: f64, usdc_balance: f64) -> TickContext {
    let combined = credit_balance + usdc_balance;
    let survival_tier = SurvivalTier::from_balance(combined, 0.0);
    TickContext {
        credit_balance,
        usdc_balance,
        survival_tier,
        timestamp: Utc::now(),
    }
}

pub fn default_tasks() -> Vec<crate::tasks::HeartbeatTask> {
    use crate::tasks::HeartbeatTask;
    vec![
        HeartbeatTask::SurvivalCheck,
        HeartbeatTask::UsdcMonitor,
        HeartbeatTask::YieldTask,
        HeartbeatTask::MemoryPrune,
        HeartbeatTask::CacheEvict,
        HeartbeatTask::MetricSnapshot,
        HeartbeatTask::AgentCardRefresh,
        HeartbeatTask::SessionGovernor,
    ]
}

fn should_rotate_sessions(
    reset_schedule: Option<&str>,
    last_rotation_at: Option<&str>,
    now: DateTime<Utc>,
) -> bool {
    reset_schedule
        .map(|expr| DurableScheduler::evaluate_cron(expr, last_rotation_at, &now.to_rfc3339()))
        .unwrap_or(false)
}

async fn run_treasury_loop(
    wallet: Arc<roboticus_wallet::WalletService>,
    treasury_state: Arc<RwLock<CachedTreasuryState>>,
    interval_seconds: u64,
    mut shutdown: watch::Receiver<bool>,
) {
    let mut interval = tokio::time::interval(std::time::Duration::from_secs(interval_seconds));
    interval.tick().await;
    tracing::info!(interval_s = interval_seconds, "treasury loop started");
    loop {
        tokio::select! { _ = interval.tick() => {} _ = shutdown.changed() => { break; } }
        let usdc = wallet.wallet.get_usdc_balance().await.unwrap_or(0.0);
        let native = wallet.wallet.get_native_balance().await.unwrap_or(0.0);
        let tier = SurvivalTier::from_balance(usdc, 0.0);
        let mut state = treasury_state.write().await;
        state.usdc_balance = usdc;
        state.native_balance = native;
        state.survival_tier = tier;
        state.last_updated = Some(Utc::now());
        tracing::debug!(usdc, native, ?tier, "treasury tick");
    }
    tracing::info!("treasury loop stopped");
}

async fn run_yield_loop(
    wallet: Arc<roboticus_wallet::WalletService>,
    db: roboticus_db::Database,
    interval_seconds: u64,
    mut shutdown: watch::Receiver<bool>,
) {
    let mut interval = tokio::time::interval(std::time::Duration::from_secs(interval_seconds));
    interval.tick().await;
    tracing::info!(interval_s = interval_seconds, "yield loop started");
    let mut last_atoken_balance: Option<f64> = None;
    loop {
        tokio::select! { _ = interval.tick() => {} _ = shutdown.changed() => { break; } }
        let agent_address = wallet.wallet.address();
        let current = wallet
            .yield_engine
            .get_a_token_balance(agent_address)
            .await
            .ok()
            .unwrap_or(0.0);
        if let Some(prev) = last_atoken_balance
            && current > prev
        {
            let delta = current - prev;
            if let Err(e) = roboticus_db::metrics::record_transaction(
                &db,
                "yield_earned",
                delta,
                "USDC",
                None,
                None,
            ) {
                tracing::warn!(error = %e, "failed to record yield_earned metric");
            }
            tracing::debug!(delta, "recorded yield_earned");
        }
        last_atoken_balance = Some(current);
    }
    tracing::info!("yield loop stopped");
}

async fn run_memory_loop(
    db: roboticus_db::Database,
    obsidian_vault: Option<Arc<tokio::sync::RwLock<roboticus_agent::obsidian::ObsidianVault>>>,
    interval_seconds: u64,
    mut shutdown: watch::Receiver<bool>,
) {
    let mut interval = tokio::time::interval(std::time::Duration::from_secs(interval_seconds));
    interval.tick().await;
    tracing::info!(interval_s = interval_seconds, "memory loop started");
    loop {
        tokio::select! { _ = interval.tick() => {} _ = shutdown.changed() => { break; } }
        let consolidation_db = db.clone();
        let consolidation_vault = obsidian_vault.clone();
        let result = tokio::task::spawn_blocking(move || {
            let vault_guard = consolidation_vault.as_ref().map(|v| v.blocking_read());
            let vault_ref = vault_guard.as_deref();
            roboticus_agent::consolidation::run_consolidation(&consolidation_db, vault_ref)
        })
        .await;
        match result {
            Ok(Ok(report)) if report.total_actions() > 0 => {
                tracing::info!(
                    indexed = report.indexed,
                    obsidian = report.obsidian_indexed,
                    deduped = report.deduped,
                    tier_synced = report.tier_synced,
                    pruned = report.pruned,
                    orphans = report.orphans_cleaned,
                    duration_ms = report.duration_ms,
                    "memory consolidation complete"
                );
            }
            Ok(Ok(_)) => {}
            Ok(Err(e)) => tracing::warn!(error = %e, "memory consolidation failed"),
            Err(e) => tracing::warn!(error = %e, "memory consolidation task panicked"),
        }
    }
    tracing::info!("memory loop stopped");
}

async fn run_maintenance_loop(
    db: roboticus_db::Database,
    treasury_state: Arc<RwLock<CachedTreasuryState>>,
    interval_seconds: u64,
    mut shutdown: watch::Receiver<bool>,
) {
    use crate::tasks::{HeartbeatTask, execute_task};
    let mut interval = tokio::time::interval(std::time::Duration::from_secs(interval_seconds));
    interval.tick().await;
    tracing::info!(interval_s = interval_seconds, "maintenance loop started");
    loop {
        tokio::select! { _ = interval.tick() => {} _ = shutdown.changed() => { break; } }
        let state = treasury_state.read().await;
        let ctx = build_tick_context(0.0, state.usdc_balance);
        drop(state);
        let _ = execute_task(&HeartbeatTask::CacheEvict, &ctx);
        let snapshot = serde_json::json!({"survival_tier": format!("{:?}", ctx.survival_tier), "usdc_balance": ctx.usdc_balance, "credit_balance": ctx.credit_balance, "timestamp": ctx.timestamp.to_rfc3339()});
        roboticus_db::metrics::record_metric_snapshot(&db, &snapshot.to_string())
            .inspect_err(|e| tracing::warn!(error = %e, "failed to record metric snapshot"))
            .ok();
    }
    tracing::info!("maintenance loop stopped");
}

async fn run_session_loop(
    db: roboticus_db::Database,
    session_config: roboticus_core::config::SessionConfig,
    digest_config: roboticus_core::config::DigestConfig,
    agent_id: String,
    interval_seconds: u64,
    mut shutdown: watch::Receiver<bool>,
) {
    let mut interval = tokio::time::interval(std::time::Duration::from_secs(interval_seconds));
    interval.tick().await;
    tracing::info!(interval_s = interval_seconds, "session loop started");
    let governor = roboticus_agent::governor::SessionGovernor::new(session_config.clone())
        .with_digest(digest_config);
    let mut last_rotation_at: Option<String> = None;
    loop {
        tokio::select! { _ = interval.tick() => {} _ = shutdown.changed() => { break; } }
        match governor.tick(&db) {
            Ok(expired) => {
                if expired > 0 {
                    tracing::info!(expired, "session governor expired stale sessions");
                }
            }
            Err(e) => tracing::warn!(error = %e, "session governor tick failed"),
        }
        let now = chrono::Utc::now();
        if should_rotate_sessions(
            session_config.reset_schedule.as_deref(),
            last_rotation_at.as_deref(),
            now,
        ) {
            match governor.rotate_agent_scope_sessions(&db, &agent_id) {
                Ok(rotated) => {
                    if rotated > 0 {
                        tracing::info!(rotated, "session governor rotated agent sessions");
                    }
                    last_rotation_at = Some(now.to_rfc3339());
                }
                Err(e) => tracing::warn!(error = %e, "session rotation failed"),
            }
        }
    }
    tracing::info!("session loop stopped");
}

async fn run_discovery_loop(
    treasury_state: Arc<RwLock<CachedTreasuryState>>,
    interval_seconds: u64,
    mut shutdown: watch::Receiver<bool>,
) {
    use crate::tasks::{HeartbeatTask, execute_task};
    let mut interval = tokio::time::interval(std::time::Duration::from_secs(interval_seconds));
    interval.tick().await;
    tracing::info!(interval_s = interval_seconds, "discovery loop started");
    loop {
        tokio::select! { _ = interval.tick() => {} _ = shutdown.changed() => { break; } }
        let state = treasury_state.read().await;
        let ctx = build_tick_context(0.0, state.usdc_balance);
        drop(state);
        let _ = execute_task(&HeartbeatTask::AgentCardRefresh, &ctx);
    }
    tracing::info!("discovery loop stopped");
}

pub async fn spawn_domain_loops(
    wallet: Arc<roboticus_wallet::WalletService>,
    db: roboticus_db::Database,
    session_config: roboticus_core::config::SessionConfig,
    digest_config: roboticus_core::config::DigestConfig,
    agent_id: String,
    obsidian_vault: Option<Arc<tokio::sync::RwLock<roboticus_agent::obsidian::ObsidianVault>>>,
    heartbeat_config: roboticus_core::config::HeartbeatConfig,
) -> (watch::Sender<bool>, Arc<RwLock<CachedTreasuryState>>) {
    let (shutdown_tx, _) = watch::channel(false);
    let treasury_state = Arc::new(RwLock::new(CachedTreasuryState::default()));
    tokio::spawn(run_treasury_loop(
        Arc::clone(&wallet),
        Arc::clone(&treasury_state),
        heartbeat_config.treasury_interval_seconds,
        shutdown_tx.subscribe(),
    ));
    tokio::spawn(run_yield_loop(
        Arc::clone(&wallet),
        db.clone(),
        heartbeat_config.yield_interval_seconds,
        shutdown_tx.subscribe(),
    ));
    tokio::spawn(run_memory_loop(
        db.clone(),
        obsidian_vault,
        heartbeat_config.memory_interval_seconds,
        shutdown_tx.subscribe(),
    ));
    tokio::spawn(run_maintenance_loop(
        db.clone(),
        Arc::clone(&treasury_state),
        heartbeat_config.maintenance_interval_seconds,
        shutdown_tx.subscribe(),
    ));
    tokio::spawn(run_session_loop(
        db.clone(),
        session_config,
        digest_config,
        agent_id,
        heartbeat_config.session_interval_seconds,
        shutdown_tx.subscribe(),
    ));
    tokio::spawn(run_discovery_loop(
        Arc::clone(&treasury_state),
        heartbeat_config.discovery_interval_seconds,
        shutdown_tx.subscribe(),
    ));
    tracing::info!("all domain loops spawned");
    (shutdown_tx, treasury_state)
}

#[deprecated(note = "Use spawn_domain_loops instead")]
pub async fn run(
    mut daemon: HeartbeatDaemon,
    wallet: std::sync::Arc<roboticus_wallet::WalletService>,
    db: roboticus_db::Database,
    session_config: roboticus_core::config::SessionConfig,
    digest_config: roboticus_core::config::DigestConfig,
    agent_id: String,
    obsidian_vault: Option<
        std::sync::Arc<tokio::sync::RwLock<roboticus_agent::obsidian::ObsidianVault>>,
    >,
) {
    use crate::tasks::{HeartbeatTask, execute_task};
    use std::time::Duration;
    daemon.running = true;
    let tasks = default_tasks();
    let mut interval = tokio::time::interval(Duration::from_millis(daemon.interval_ms));
    let mut last_atoken_balance: Option<f64> = None;
    let governor = roboticus_agent::governor::SessionGovernor::new(session_config.clone())
        .with_digest(digest_config);
    let mut last_rotation_at: Option<String> = None;
    tracing::info!(interval_ms = daemon.interval_ms, "Heartbeat loop started");
    interval.tick().await;
    loop {
        interval.tick().await;
        if !daemon.running {
            break;
        }
        let usdc_balance = wallet.wallet.get_usdc_balance().await.unwrap_or(0.0);
        let credit_balance = 0.0;
        let ctx = build_tick_context(credit_balance, usdc_balance);
        tracing::debug!(tier = ?ctx.survival_tier, usdc = usdc_balance, "Heartbeat tick");
        for task in &tasks {
            let result = execute_task(task, &ctx);
            if !result.success {
                tracing::warn!(task = ?task, msg = %result.message, "Heartbeat task failed");
            }
            if result.should_wake {
                tracing::info!(task = ?task, msg = %result.message, "Heartbeat task triggered wake");
            }
            if *task == HeartbeatTask::YieldTask {
                let agent_address = wallet.wallet.address();
                let current = wallet
                    .yield_engine
                    .get_a_token_balance(agent_address)
                    .await
                    .ok()
                    .unwrap_or(0.0);
                if let Some(prev) = last_atoken_balance
                    && current > prev
                {
                    let delta = current - prev;
                    if delta > 0.0 {
                        if let Err(e) = roboticus_db::metrics::record_transaction(
                            &db,
                            "yield_earned",
                            delta,
                            "USDC",
                            None,
                            None,
                        ) {
                            tracing::warn!(error = %e, "failed to record yield_earned metric");
                        }
                        tracing::debug!(delta, "recorded yield_earned");
                    }
                }
                last_atoken_balance = Some(current);
            }
            if *task == HeartbeatTask::MetricSnapshot && result.success {
                let snapshot = serde_json::json!({"survival_tier": format!("{:?}", ctx.survival_tier), "usdc_balance": ctx.usdc_balance, "credit_balance": ctx.credit_balance, "timestamp": ctx.timestamp.to_rfc3339()});
                roboticus_db::metrics::record_metric_snapshot(&db, &snapshot.to_string())
                    .inspect_err(|e| tracing::warn!(error = %e, "failed to record metric snapshot"))
                    .ok();
            }
            if *task == HeartbeatTask::MemoryPrune {
                let consolidation_db = db.clone();
                let consolidation_vault = obsidian_vault.clone();
                let result = tokio::task::spawn_blocking(move || {
                    let vault_guard = consolidation_vault.as_ref().map(|v| v.blocking_read());
                    let vault_ref = vault_guard.as_deref();
                    roboticus_agent::consolidation::run_consolidation(&consolidation_db, vault_ref)
                })
                .await;
                match result {
                    Ok(Ok(report)) if report.total_actions() > 0 => {
                        tracing::info!(
                            indexed = report.indexed,
                            obsidian = report.obsidian_indexed,
                            deduped = report.deduped,
                            tier_synced = report.tier_synced,
                            pruned = report.pruned,
                            orphans = report.orphans_cleaned,
                            duration_ms = report.duration_ms,
                            "memory consolidation complete"
                        );
                    }
                    Ok(Ok(_)) => {}
                    Ok(Err(e)) => tracing::warn!(error = %e, "memory consolidation failed"),
                    Err(e) => tracing::warn!(error = %e, "memory consolidation task panicked"),
                }
            }
            if *task == HeartbeatTask::SessionGovernor {
                match governor.tick(&db) {
                    Ok(expired) => {
                        if expired > 0 {
                            tracing::info!(expired, "session governor expired stale sessions");
                        }
                    }
                    Err(e) => tracing::warn!(error = %e, "session governor tick failed"),
                }
                let now = chrono::Utc::now();
                if should_rotate_sessions(
                    session_config.reset_schedule.as_deref(),
                    last_rotation_at.as_deref(),
                    now,
                ) {
                    match governor.rotate_agent_scope_sessions(&db, &agent_id) {
                        Ok(rotated) => {
                            if rotated > 0 {
                                tracing::info!(rotated, "session governor rotated agent sessions");
                            }
                            last_rotation_at = Some(now.to_rfc3339());
                        }
                        Err(e) => tracing::warn!(error = %e, "session rotation failed"),
                    }
                }
            }
            if !result.success {
                tracing::debug!(task = ?task, msg = %result.message, "heartbeat task unsuccessful");
            }
        }
        if let Some(new_interval) = daemon.should_adjust_interval(&ctx.survival_tier)
            && new_interval != daemon.interval_ms
        {
            tracing::info!(old_ms = daemon.interval_ms, new_ms = new_interval, tier = ?ctx.survival_tier, "Adjusting heartbeat interval");
            daemon.interval_ms = new_interval;
            interval = tokio::time::interval(Duration::from_millis(new_interval));
            interval.tick().await;
        }
    }
}

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

    #[test]
    fn tick_context_high_balance() {
        let ctx = build_tick_context(10.0, 5.0);
        assert_eq!(ctx.survival_tier, SurvivalTier::High);
        assert!((ctx.credit_balance - 10.0).abs() < f64::EPSILON);
        assert!((ctx.usdc_balance - 5.0).abs() < f64::EPSILON);
    }

    #[test]
    fn tick_context_critical_balance() {
        let ctx = build_tick_context(0.05, 0.01);
        assert_eq!(ctx.survival_tier, SurvivalTier::Critical);
    }

    #[test]
    fn tick_context_low_compute_balance() {
        let ctx = build_tick_context(0.20, 0.10);
        assert_eq!(ctx.survival_tier, SurvivalTier::LowCompute);
    }

    #[test]
    fn interval_adjustment_by_tier() {
        let daemon = HeartbeatDaemon::new(1000);
        assert_eq!(daemon.should_adjust_interval(&SurvivalTier::High), None);
        assert_eq!(daemon.should_adjust_interval(&SurvivalTier::Normal), None);
        assert_eq!(
            daemon.should_adjust_interval(&SurvivalTier::LowCompute),
            Some(10_000)
        );
        assert_eq!(
            daemon.should_adjust_interval(&SurvivalTier::Critical),
            Some(10_000)
        );
        assert_eq!(
            daemon.should_adjust_interval(&SurvivalTier::Dead),
            Some(10_000)
        );
        let daemon_30s = HeartbeatDaemon::new(30_000);
        assert_eq!(
            daemon_30s.should_adjust_interval(&SurvivalTier::LowCompute),
            Some(60_000)
        );
        assert_eq!(
            daemon_30s.should_adjust_interval(&SurvivalTier::Critical),
            Some(60_000)
        );
        assert_eq!(
            daemon_30s.should_adjust_interval(&SurvivalTier::Dead),
            Some(300_000)
        );
    }

    #[test]
    fn interval_recovers_to_original_on_normal_tier() {
        let mut daemon = HeartbeatDaemon::new(30_000);
        daemon.interval_ms = 120_000;
        assert_eq!(
            daemon.should_adjust_interval(&SurvivalTier::Normal),
            Some(30_000)
        );
        assert_eq!(
            daemon.should_adjust_interval(&SurvivalTier::High),
            Some(30_000)
        );
    }

    #[test]
    fn default_tasks_returns_expected_set() {
        use crate::tasks::HeartbeatTask;
        let tasks = default_tasks();
        assert_eq!(tasks.len(), 8);
        assert_eq!(tasks[0], HeartbeatTask::SurvivalCheck);
        assert_eq!(tasks[7], HeartbeatTask::SessionGovernor);
    }

    #[test]
    fn heartbeat_daemon_new_creates_with_correct_interval() {
        let daemon = HeartbeatDaemon::new(5000);
        assert_eq!(daemon.interval_ms, 5000);
        assert!(!daemon.running);
    }

    const MAX_HEARTBEAT_INTERVAL_MS: u64 = 3_600_000;
    const MIN_HEARTBEAT_INTERVAL_MS: u64 = 10_000;

    #[test]
    fn interval_capped_at_max() {
        let daemon = HeartbeatDaemon::new(1_800_000);
        assert!(
            daemon
                .should_adjust_interval(&SurvivalTier::Critical)
                .unwrap()
                <= 300_000
        );
        assert!(
            daemon.should_adjust_interval(&SurvivalTier::Dead).unwrap()
                <= MAX_HEARTBEAT_INTERVAL_MS
        );
    }

    #[test]
    fn interval_floored_at_min() {
        let daemon = HeartbeatDaemon::new(1);
        assert!(
            daemon
                .should_adjust_interval(&SurvivalTier::LowCompute)
                .unwrap()
                >= MIN_HEARTBEAT_INTERVAL_MS
        );
    }

    #[test]
    fn should_rotate_sessions_respects_cron_slots() {
        let now = DateTime::parse_from_rfc3339("2025-01-01T12:00:10+00:00")
            .unwrap()
            .with_timezone(&Utc);
        assert!(should_rotate_sessions(Some("0 12 * * *"), None, now));
        assert!(!should_rotate_sessions(Some("15 12 * * *"), None, now));
    }

    #[test]
    fn should_rotate_sessions_deduplicates_same_slot() {
        let now = DateTime::parse_from_rfc3339("2025-01-01T12:00:20+00:00")
            .unwrap()
            .with_timezone(&Utc);
        assert!(!should_rotate_sessions(
            Some("0 12 * * *"),
            Some("2025-01-01T12:00:05+00:00"),
            now
        ));
    }

    #[test]
    fn should_rotate_sessions_none_schedule_returns_false() {
        let now = DateTime::parse_from_rfc3339("2025-01-01T12:00:10+00:00")
            .unwrap()
            .with_timezone(&Utc);
        assert!(!should_rotate_sessions(None, None, now));
    }

    #[test]
    fn should_rotate_sessions_none_schedule_with_last_rotation() {
        let now = DateTime::parse_from_rfc3339("2025-01-01T12:00:10+00:00")
            .unwrap()
            .with_timezone(&Utc);
        assert!(!should_rotate_sessions(
            None,
            Some("2025-01-01T00:00:00+00:00"),
            now
        ));
    }

    #[test]
    fn should_rotate_sessions_invalid_cron_returns_false() {
        let now = DateTime::parse_from_rfc3339("2025-01-01T12:00:10+00:00")
            .unwrap()
            .with_timezone(&Utc);
        assert!(!should_rotate_sessions(Some("this is invalid"), None, now));
    }

    #[test]
    fn should_rotate_sessions_no_last_rotation_at_matches() {
        let now = DateTime::parse_from_rfc3339("2025-01-01T06:00:10+00:00")
            .unwrap()
            .with_timezone(&Utc);
        assert!(should_rotate_sessions(Some("0 6 * * *"), None, now));
    }

    #[test]
    fn should_rotate_sessions_different_slot_after_last_rotation() {
        let now = DateTime::parse_from_rfc3339("2025-01-02T06:00:10+00:00")
            .unwrap()
            .with_timezone(&Utc);
        assert!(should_rotate_sessions(
            Some("0 6 * * *"),
            Some("2025-01-01T06:00:05+00:00"),
            now
        ));
    }

    #[test]
    fn heartbeat_daemon_new_sets_both_intervals() {
        let daemon = HeartbeatDaemon::new(60_000);
        assert_eq!(daemon.interval_ms, 60_000);
        assert_eq!(daemon.original_interval_ms, 60_000);
        assert!(!daemon.running);
    }

    #[test]
    fn should_adjust_interval_dead_tier_caps_at_1_hour() {
        let daemon = HeartbeatDaemon::new(200_000);
        assert!(daemon.should_adjust_interval(&SurvivalTier::Dead).unwrap() <= 3_600_000);
    }

    #[test]
    fn should_adjust_interval_critical_caps_at_5_minutes() {
        let daemon = HeartbeatDaemon::new(200_000);
        assert!(
            daemon
                .should_adjust_interval(&SurvivalTier::Critical)
                .unwrap()
                <= 300_000
        );
    }

    #[test]
    fn should_adjust_interval_low_compute_caps_at_5_minutes() {
        let daemon = HeartbeatDaemon::new(200_000);
        assert!(
            daemon
                .should_adjust_interval(&SurvivalTier::LowCompute)
                .unwrap()
                <= 300_000
        );
    }

    #[test]
    fn should_adjust_interval_already_at_original_no_change() {
        let daemon = HeartbeatDaemon::new(60_000);
        assert!(
            daemon
                .should_adjust_interval(&SurvivalTier::Normal)
                .is_none()
        );
        assert!(daemon.should_adjust_interval(&SurvivalTier::High).is_none());
    }

    #[test]
    fn should_adjust_interval_degraded_then_recover() {
        let mut daemon = HeartbeatDaemon::new(30_000);
        daemon.interval_ms = 300_000;
        assert_eq!(
            daemon.should_adjust_interval(&SurvivalTier::Normal),
            Some(30_000)
        );
        assert_eq!(
            daemon.should_adjust_interval(&SurvivalTier::High),
            Some(30_000)
        );
    }

    #[test]
    fn build_tick_context_dead_tier() {
        let ctx = build_tick_context(0.001, 0.001);
        assert_eq!(ctx.survival_tier, SurvivalTier::Critical);
    }

    #[test]
    fn build_tick_context_normal_tier() {
        let ctx = build_tick_context(1.0, 0.0);
        assert_eq!(ctx.survival_tier, SurvivalTier::Normal);
    }

    #[test]
    fn build_tick_context_zero_balances() {
        let ctx = build_tick_context(0.0, 0.0);
        assert_eq!(ctx.survival_tier, SurvivalTier::Critical);
    }

    #[test]
    fn build_tick_context_boundary_low_compute() {
        assert_eq!(
            build_tick_context(0.10, 0.0).survival_tier,
            SurvivalTier::LowCompute
        );
        assert_eq!(
            build_tick_context(0.49, 0.0).survival_tier,
            SurvivalTier::LowCompute
        );
    }

    #[test]
    fn build_tick_context_boundary_normal() {
        assert_eq!(
            build_tick_context(0.50, 0.0).survival_tier,
            SurvivalTier::Normal
        );
        assert_eq!(
            build_tick_context(4.99, 0.0).survival_tier,
            SurvivalTier::Normal
        );
    }

    #[test]
    fn build_tick_context_boundary_high() {
        assert_eq!(
            build_tick_context(5.00, 0.0).survival_tier,
            SurvivalTier::High
        );
    }

    #[test]
    fn build_tick_context_timestamp_is_recent() {
        let before = Utc::now();
        let ctx = build_tick_context(1.0, 1.0);
        let after = Utc::now();
        assert!(ctx.timestamp >= before && ctx.timestamp <= after);
    }

    #[test]
    fn default_tasks_last_is_session_governor() {
        use crate::tasks::HeartbeatTask;
        assert_eq!(
            *default_tasks().last().unwrap(),
            HeartbeatTask::SessionGovernor
        );
    }

    #[test]
    fn cached_treasury_state_default() {
        let state = CachedTreasuryState::default();
        assert!((state.usdc_balance - 0.0).abs() < f64::EPSILON);
        assert!((state.native_balance - 0.0).abs() < f64::EPSILON);
        assert_eq!(state.survival_tier, SurvivalTier::Critical);
        assert!(state.last_updated.is_none());
    }

    #[test]
    fn cached_treasury_state_update() {
        let state = CachedTreasuryState {
            usdc_balance: 5.0,
            native_balance: 0.01,
            survival_tier: SurvivalTier::High,
            last_updated: Some(Utc::now()),
        };
        assert!((state.usdc_balance - 5.0).abs() < f64::EPSILON);
        assert!((state.native_balance - 0.01).abs() < f64::EPSILON);
        assert_eq!(state.survival_tier, SurvivalTier::High);
        assert!(state.last_updated.is_some());
    }

    #[test]
    fn cached_treasury_state_clone() {
        let state = CachedTreasuryState {
            usdc_balance: 3.0,
            native_balance: 0.005,
            survival_tier: SurvivalTier::Normal,
            last_updated: Some(Utc::now()),
        };
        let cloned = state.clone();
        assert!((cloned.usdc_balance - 3.0).abs() < f64::EPSILON);
        assert_eq!(cloned.survival_tier, SurvivalTier::Normal);
    }

    #[tokio::test(start_paused = true)]
    async fn maintenance_loop_reads_from_treasury_cache() {
        let db = roboticus_db::Database::new(":memory:").unwrap();
        let treasury_state = Arc::new(RwLock::new(CachedTreasuryState {
            usdc_balance: 10.0,
            native_balance: 0.05,
            survival_tier: SurvivalTier::High,
            last_updated: Some(Utc::now()),
        }));
        let (shutdown_tx, _) = watch::channel(false);

        tokio::spawn(run_maintenance_loop(
            db,
            Arc::clone(&treasury_state),
            1,
            shutdown_tx.subscribe(),
        ));

        // Advance past first tick + one interval
        tokio::time::advance(std::time::Duration::from_secs(2)).await;
        tokio::task::yield_now().await;

        // Maintenance loop should have read from cache (no crash = success)
        let _ = shutdown_tx.send(true);
    }

    #[tokio::test(start_paused = true)]
    async fn discovery_loop_reads_treasury_state() {
        let treasury_state = Arc::new(RwLock::new(CachedTreasuryState {
            usdc_balance: 2.0,
            native_balance: 0.0,
            survival_tier: SurvivalTier::Normal,
            last_updated: Some(Utc::now()),
        }));
        let (shutdown_tx, _) = watch::channel(false);

        tokio::spawn(run_discovery_loop(
            Arc::clone(&treasury_state),
            1,
            shutdown_tx.subscribe(),
        ));

        tokio::time::advance(std::time::Duration::from_secs(2)).await;
        tokio::task::yield_now().await;

        let _ = shutdown_tx.send(true);
    }

    #[tokio::test(start_paused = true)]
    async fn session_loop_runs_governor_tick() {
        let db = roboticus_db::Database::new(":memory:").unwrap();
        let (shutdown_tx, _) = watch::channel(false);

        tokio::spawn(run_session_loop(
            db,
            roboticus_core::config::SessionConfig::default(),
            roboticus_core::config::DigestConfig::default(),
            "test-agent".into(),
            1,
            shutdown_tx.subscribe(),
        ));

        tokio::time::advance(std::time::Duration::from_secs(2)).await;
        tokio::task::yield_now().await;

        let _ = shutdown_tx.send(true);
    }

    #[tokio::test(start_paused = true)]
    async fn memory_loop_runs_consolidation() {
        let db = roboticus_db::Database::new(":memory:").unwrap();
        let (shutdown_tx, _) = watch::channel(false);

        tokio::spawn(run_memory_loop(db, None, 1, shutdown_tx.subscribe()));

        tokio::time::advance(std::time::Duration::from_secs(2)).await;
        tokio::task::yield_now().await;

        let _ = shutdown_tx.send(true);
    }

    #[tokio::test(start_paused = true)]
    async fn shutdown_signal_stops_maintenance_loop() {
        let db = roboticus_db::Database::new(":memory:").unwrap();
        let treasury_state = Arc::new(RwLock::new(CachedTreasuryState::default()));
        let (shutdown_tx, _) = watch::channel(false);

        let handle = tokio::spawn(run_maintenance_loop(
            db,
            treasury_state,
            60, // long interval so loop is waiting
            shutdown_tx.subscribe(),
        ));

        // Send shutdown before first tick completes
        let _ = shutdown_tx.send(true);
        tokio::time::advance(std::time::Duration::from_secs(61)).await;
        tokio::task::yield_now().await;

        // Loop should have exited
        assert!(handle.is_finished());
    }
}
