use std::collections::HashMap;
use std::sync::Arc;

use crate::rate_limit::GlobalRateLimitLayer;
use async_trait::async_trait;
use axum::Json;
use axum::body::Body;
use axum::extract::{Query, State as AxumState};
use axum::http::{Request, StatusCode};
use axum::routing::get;
use roboticus_agent::policy::{AuthorityRule, CommandSafetyRule, PolicyEngine};
use roboticus_agent::subagents::SubagentRegistry;
use roboticus_browser::Browser;
use roboticus_channels::a2a::A2aProtocol;
use roboticus_channels::router::ChannelRouter;
use roboticus_channels::telegram::TelegramAdapter;
use roboticus_channels::whatsapp::WhatsAppAdapter;
use roboticus_channels::{ChannelAdapter, InboundMessage, OutboundMessage};
use roboticus_core::InputAuthority;
use roboticus_db::Database;
use roboticus_llm::LlmService;
use roboticus_llm::OAuthManager;
use roboticus_plugin_sdk::registry::PluginRegistry;
use roboticus_plugin_sdk::{Plugin, ToolDef, ToolResult};
use serde_json::json;
use tokio::net::TcpListener;
use tokio::sync::Mutex;
use tower::ServiceExt;

use roboticus_agent::approvals::ApprovalManager;
use roboticus_agent::tools::ToolRegistry;

use super::*;
#[path = "tests/channel_and_pipeline_tests.rs"]
mod channel_and_pipeline_tests;
#[path = "tests/composition_workflow_tests.rs"]
mod composition_workflow_tests;
#[path = "tests/context.rs"]
mod context;
#[path = "tests/financial_tests.rs"]
mod financial_tests;
#[path = "tests/mock_and_integration_tests.rs"]
mod mock_and_integration_tests;
#[path = "tests/regression_tests.rs"]
mod regression_tests;
#[path = "tests/track1_observability_tests.rs"]
mod track1_observability_tests;

fn test_config_str() -> &'static str {
    r#"
[agent]
name = "TestBot"
id = "test"

[server]
port = 9999

[database]
path = ":memory:"

[session]
scope_mode = "agent"

[models]
primary = "ollama/qwen3:8b"
"#
}

pub(crate) fn test_state() -> AppState {
    let db = Database::new(":memory:").unwrap();
    db.conn()
        .execute_batch(
            "CREATE TABLE IF NOT EXISTS task_events ( \
                id TEXT PRIMARY KEY, \
                task_id TEXT NOT NULL, \
                parent_task_id TEXT, \
                assigned_to TEXT, \
                event_type TEXT NOT NULL CHECK ( \
                    event_type IN ('pending','assigned','running','progress','completed','failed','cancelled','retry') \
                ), \
                summary TEXT, \
                detail_json TEXT, \
                percentage REAL, \
                retry_count INTEGER NOT NULL DEFAULT 0, \
                created_at TEXT NOT NULL DEFAULT (datetime('now')) \
            ); \
            CREATE INDEX IF NOT EXISTS idx_task_events_task_id ON task_events(task_id); \
            CREATE INDEX IF NOT EXISTS idx_task_events_parent ON task_events(parent_task_id); \
            CREATE INDEX IF NOT EXISTS idx_task_events_assigned_to ON task_events(assigned_to); \
            CREATE INDEX IF NOT EXISTS idx_task_events_created ON task_events(created_at DESC); \
            CREATE INDEX IF NOT EXISTS idx_task_events_type ON task_events(event_type);",
        )
        .unwrap();
    let config = roboticus_core::RoboticusConfig::from_str(test_config_str()).unwrap();
    let llm = LlmService::new(&config).unwrap();
    let a2a = A2aProtocol::new(config.a2a.clone());

    let wallet = roboticus_wallet::Wallet::test_mock();
    let treasury = roboticus_wallet::TreasuryPolicy::new(&config.treasury);
    let yield_engine = roboticus_wallet::YieldEngine::new(&config.r#yield);
    let wallet_svc = roboticus_wallet::WalletService {
        wallet,
        treasury,
        yield_engine,
    };

    let plugins = Arc::new(PluginRegistry::new(
        vec![],
        vec![],
        roboticus_plugin_sdk::registry::PermissionPolicy {
            strict: false,
            allowed: vec![],
        },
    ));
    let mut policy_engine = PolicyEngine::new();
    policy_engine.add_rule(Box::new(AuthorityRule));
    policy_engine.add_rule(Box::new(CommandSafetyRule));
    let policy_engine = Arc::new(policy_engine);
    let browser = Arc::new(Browser::new(
        roboticus_core::config::BrowserConfig::default(),
    ));
    let registry = Arc::new(SubagentRegistry::new(4, vec![]));
    let event_bus = EventBus::new(256);
    let channel_router = Arc::new(ChannelRouter::new());
    let retriever = Arc::new(roboticus_agent::retrieval::MemoryRetriever::new(
        config.memory.clone(),
    ));
    let config_path = std::env::temp_dir().join(format!(
        "roboticus-test-config-{}.toml",
        uuid::Uuid::new_v4()
    ));
    let config_toml = toml::to_string_pretty(&config).expect("serialize test config");
    std::fs::write(&config_path, config_toml).expect("write test config file");
    AppState {
        db,
        config: Arc::new(RwLock::new(config)),
        llm: Arc::new(RwLock::new(llm)),
        wallet: Arc::new(wallet_svc),
        a2a: Arc::new(RwLock::new(a2a)),
        personality: Arc::new(RwLock::new(PersonalityState::empty())),
        hmac_secret: Arc::new(b"test-hmac-secret-key-for-tests!!".to_vec()),
        interviews: Arc::new(RwLock::new(HashMap::new())),
        plugins,
        policy_engine,
        browser,
        registry,
        event_bus,
        channel_router,
        telegram: None,
        whatsapp: None,
        retriever,
        ann_index: roboticus_db::ann::AnnIndex::new(false),
        tools: Arc::new(ToolRegistry::new()),
        capabilities: Arc::new(roboticus_agent::capability::CapabilityRegistry::new()),
        approvals: Arc::new(ApprovalManager::new(
            roboticus_core::config::ApprovalsConfig::default(),
        )),
        discord: None,
        signal: None,
        email: None,
        voice: None,
        discovery: Arc::new(RwLock::new(
            roboticus_agent::discovery::DiscoveryRegistry::new(),
        )),
        devices: Arc::new(RwLock::new(roboticus_agent::device::DeviceManager::new(
            roboticus_agent::device::DeviceIdentity::generate("test-device"),
            5,
        ))),
        mcp_clients: Arc::new(RwLock::new(roboticus_agent::mcp::McpClientManager::new())),
        mcp_server: Arc::new(RwLock::new(roboticus_agent::mcp::McpServerRegistry::new())),
        live_mcp: Arc::new(roboticus_agent::mcp::manager::McpConnectionManager::new()),
        oauth: Arc::new(OAuthManager::new().unwrap()),
        keystore: Arc::new(roboticus_core::keystore::Keystore::new(
            std::env::temp_dir().join(format!("roboticus-test-ks-{}.enc", uuid::Uuid::new_v4())),
        )),
        obsidian: None,
        started_at: std::time::Instant::now(),
        config_path: Arc::new(config_path.clone()),
        config_apply_status: Arc::new(RwLock::new(ConfigApplyStatus::new(&config_path))),
        pending_specialist_proposals: Arc::new(RwLock::new(HashMap::new())),
        ws_tickets: crate::ws_ticket::TicketStore::new(),
        rate_limiter: crate::rate_limit::GlobalRateLimitLayer::new(
            100,
            std::time::Duration::from_secs(60),
        ),
        media_service: None,
        semantic_classifier: Arc::new(roboticus_llm::semantic_classifier::SemanticClassifier::new(
            roboticus_llm::EmbeddingClient::new(None).unwrap(),
        )),
    }
}

/// State with Telegram adapter that has webhook_secret set (for security tests).
fn test_state_with_telegram_webhook_secret(secret: &str) -> AppState {
    let mut state = test_state();
    let adapter = TelegramAdapter::with_config(
        "test-bot-token".into(),
        30,
        vec![],
        Some(secret.to_string()),
        false,
    );
    state.telegram = Some(Arc::new(adapter));
    state
}

/// State with WhatsApp adapter that has app_secret set (for signature verification tests).
fn test_state_with_whatsapp_app_secret(secret: &str) -> AppState {
    let mut state = test_state();
    let adapter = WhatsAppAdapter::with_config(
        "test-token".into(),
        "phone-id".into(),
        "verify-token".into(),
        vec![],
        Some(secret.to_string()),
        false,
    )
    .unwrap();
    state.whatsapp = Some(Arc::new(adapter));
    state
}

fn full_app(state: AppState) -> Router {
    build_router(state.clone()).merge(build_public_router(state))
}

async fn json_body(resp: axum::http::Response<Body>) -> serde_json::Value {
    let bytes = axum::body::to_bytes(resp.into_body(), usize::MAX)
        .await
        .unwrap();
    serde_json::from_slice(&bytes).unwrap()
}

async fn text_body(resp: axum::http::Response<Body>) -> String {
    let bytes = axum::body::to_bytes(resp.into_body(), usize::MAX)
        .await
        .unwrap();
    String::from_utf8(bytes.to_vec()).unwrap()
}

#[tokio::test]
async fn health_returns_ok() {
    let app = build_router(test_state());
    let req = Request::builder()
        .uri("/api/health")
        .body(Body::empty())
        .unwrap();

    let resp = app.oneshot(req).await.unwrap();
    assert_eq!(resp.status(), StatusCode::OK);

    let body = json_body(resp).await;
    assert_eq!(body["status"], "ok");
    assert_eq!(body["version"], env!("CARGO_PKG_VERSION"));
    assert!(
        body["uptime_seconds"].as_u64().is_some(),
        "uptime_seconds should be a number"
    );
}

#[tokio::test]
async fn logs_endpoint_returns_valid_json() {
    let app = build_router(test_state());
    let req = Request::builder()
        .uri("/api/logs")
        .body(Body::empty())
        .unwrap();

    let resp = app.oneshot(req).await.unwrap();
    assert_eq!(resp.status(), StatusCode::OK);

    let body = json_body(resp).await;
    let entries = body
        .get("entries")
        .expect("response must have 'entries' key");
    assert!(entries.is_array(), "entries must be a JSON array");
}

#[tokio::test]
async fn create_and_get_session() {
    let state = test_state();
    let app = build_router(state);

    let req = Request::builder()
        .method("POST")
        .uri("/api/sessions")
        .header("content-type", "application/json")
        .body(Body::from(r#"{"agent_id":"test-agent"}"#))
        .unwrap();

    let resp = app.oneshot(req).await.unwrap();
    assert_eq!(resp.status(), StatusCode::OK);

    let body = json_body(resp).await;
    let session_id = body["id"].as_str().unwrap().to_string();
    assert!(!session_id.is_empty());
}

#[tokio::test]
async fn get_session_not_found() {
    let app = build_router(test_state());
    let req = Request::builder()
        .uri("/api/sessions/nonexistent-id")
        .body(Body::empty())
        .unwrap();

    let resp = app.oneshot(req).await.unwrap();
    assert_eq!(resp.status(), StatusCode::NOT_FOUND);
}

#[tokio::test]
async fn post_and_list_messages() {
    let state = test_state();
    let session_id = roboticus_db::sessions::find_or_create(&state.db, "agent-1", None).unwrap();

    let app = build_router(state.clone());
    let req = Request::builder()
        .method("POST")
        .uri(format!("/api/sessions/{session_id}/messages"))
        .header("content-type", "application/json")
        .body(Body::from(r#"{"role":"user","content":"hello"}"#))
        .unwrap();

    let resp = app.oneshot(req).await.unwrap();
    assert_eq!(resp.status(), StatusCode::OK);

    let app = build_router(state);
    let req = Request::builder()
        .uri(format!("/api/sessions/{session_id}/messages"))
        .body(Body::empty())
        .unwrap();

    let resp = app.oneshot(req).await.unwrap();
    let body = json_body(resp).await;
    let messages = body["messages"].as_array().unwrap();
    assert_eq!(messages.len(), 1);
    assert_eq!(messages[0]["role"], "user");
    assert_eq!(messages[0]["content"], "hello");
}

#[tokio::test]
async fn list_skills_includes_built_ins() {
    let app = build_router(test_state());
    let req = Request::builder()
        .uri("/api/skills")
        .body(Body::empty())
        .unwrap();

    let resp = app.oneshot(req).await.unwrap();
    assert_eq!(resp.status(), StatusCode::OK);

    let body = json_body(resp).await;
    let skills = body["skills"].as_array().unwrap();
    assert!(!skills.is_empty());
    assert!(
        skills
            .iter()
            .all(|s| s["enabled"].as_bool().unwrap_or(false))
    );
    assert!(
        skills
            .iter()
            .any(|s| s["name"].as_str() == Some("supervisor-protocol"))
    );
}

#[tokio::test]
async fn list_skills_exposes_usage_telemetry() {
    let state = test_state();
    let skill_id = roboticus_db::skills::register_skill(
        &state.db,
        "telemetry-skill",
        "instruction",
        Some("Tracks usage"),
        "/tmp/telemetry-skill.md",
        "hash",
        Some("[\"telemetry\"]"),
        None,
        None,
        None,
        Some("Safe"),
    )
    .unwrap();
    roboticus_db::skills::record_skill_usage(&state.db, &skill_id).unwrap();
    let app = build_router(state);
    let resp = app
        .oneshot(
            Request::builder()
                .uri("/api/skills")
                .body(Body::empty())
                .unwrap(),
        )
        .await
        .unwrap();

    assert_eq!(resp.status(), StatusCode::OK);
    let body = json_body(resp).await;
    let skill = body["skills"]
        .as_array()
        .unwrap()
        .iter()
        .find(|s| s["id"] == skill_id)
        .cloned()
        .expect("registered skill should appear in listing");
    assert_eq!(skill["usage_count"], 1);
    assert!(skill["last_used_at"].as_str().is_some());
}

#[tokio::test]
async fn agent_status_returns_running() {
    let app = build_router(test_state());
    let req = Request::builder()
        .uri("/api/agent/status")
        .body(Body::empty())
        .unwrap();

    let resp = app.oneshot(req).await.unwrap();
    assert_eq!(resp.status(), StatusCode::OK);

    let body = json_body(resp).await;
    assert_eq!(body["state"], "running");
}

#[tokio::test]
async fn get_config_returns_config_without_secrets() {
    let app = build_router(test_state());
    let req = Request::builder()
        .uri("/api/config")
        .body(Body::empty())
        .unwrap();

    let resp = app.oneshot(req).await.unwrap();
    assert_eq!(resp.status(), StatusCode::OK);

    let body = json_body(resp).await;
    assert!(body.get("agent").is_some());
    assert!(body.get("server").is_some());
}

#[tokio::test]
async fn put_config_updates_runtime_config() {
    let state = test_state();
    let app = build_router(state);
    let req = Request::builder()
        .method("PUT")
        .uri("/api/config")
        .header("content-type", "application/json")
        .body(Body::from(r#"{"agent":{"name":"UpdatedBot"}}"#))
        .unwrap();

    let resp = app.oneshot(req).await.unwrap();
    assert_eq!(resp.status(), StatusCode::OK);

    let body = json_body(resp).await;
    assert_eq!(body["updated"], true);
    assert_eq!(body["persisted"], true);
    assert!(body["backup_path"].is_string());
}

#[tokio::test]
async fn put_config_routing_weights_persist_round_trip() {
    let state = test_state();
    let app = build_router(state.clone());
    let patch = r#"{
        "models": {
            "routing": {
                "accuracy_floor": 0.42,
                "cost_weight": 0.31,
                "cost_aware": true,
                "confidence_threshold": 0.77,
                "estimated_output_tokens": 640
            }
        }
    }"#;
    let put_resp = app
        .clone()
        .oneshot(
            Request::builder()
                .method("PUT")
                .uri("/api/config")
                .header("content-type", "application/json")
                .body(Body::from(patch))
                .unwrap(),
        )
        .await
        .unwrap();
    assert_eq!(put_resp.status(), StatusCode::OK);
    let put_body = json_body(put_resp).await;
    assert_eq!(put_body["persisted"], true);

    let get_resp = app
        .oneshot(
            Request::builder()
                .uri("/api/config")
                .body(Body::empty())
                .unwrap(),
        )
        .await
        .unwrap();
    assert_eq!(get_resp.status(), StatusCode::OK);
    let cfg = json_body(get_resp).await;
    assert_eq!(cfg["models"]["routing"]["accuracy_floor"], 0.42);
    assert_eq!(cfg["models"]["routing"]["cost_weight"], 0.31);
    assert_eq!(cfg["models"]["routing"]["cost_aware"], true);
    assert_eq!(cfg["models"]["routing"]["confidence_threshold"], 0.77);
    assert_eq!(cfg["models"]["routing"]["estimated_output_tokens"], 640);
}

#[tokio::test]
async fn put_config_context_budget_persist_round_trip() {
    let state = test_state();
    let app = build_router(state.clone());
    let patch = r#"{
        "context_budget": {
            "l0": 4500,
            "l1": 9000,
            "l2": 36000,
            "l3": 72000,
            "channel_minimum": "L2"
        }
    }"#;
    let put_resp = app
        .clone()
        .oneshot(
            Request::builder()
                .method("PUT")
                .uri("/api/config")
                .header("content-type", "application/json")
                .body(Body::from(patch))
                .unwrap(),
        )
        .await
        .unwrap();
    assert_eq!(put_resp.status(), StatusCode::OK);
    let put_body = json_body(put_resp).await;
    assert_eq!(put_body["persisted"], true);
    assert_eq!(put_body["ignored_keys"], json!([]));

    let get_resp = app
        .oneshot(
            Request::builder()
                .uri("/api/config")
                .body(Body::empty())
                .unwrap(),
        )
        .await
        .unwrap();
    assert_eq!(get_resp.status(), StatusCode::OK);
    let cfg = json_body(get_resp).await;
    assert_eq!(cfg["context_budget"]["l0"], 4500);
    assert_eq!(cfg["context_budget"]["l1"], 9000);
    assert_eq!(cfg["context_budget"]["l2"], 36000);
    assert_eq!(cfg["context_budget"]["l3"], 72000);
    assert_eq!(cfg["context_budget"]["channel_minimum"], "L2");
}

#[tokio::test]
async fn get_config_raw_returns_toml_text() {
    let state = test_state();
    let app = build_router(state);
    let resp = app
        .oneshot(
            Request::builder()
                .uri("/api/config/raw")
                .body(Body::empty())
                .unwrap(),
        )
        .await
        .unwrap();
    assert_eq!(resp.status(), StatusCode::OK);
    let content_type = resp
        .headers()
        .get("content-type")
        .unwrap()
        .to_str()
        .unwrap();
    assert!(content_type.starts_with("text/plain"));
    let body = axum::body::to_bytes(resp.into_body(), usize::MAX)
        .await
        .unwrap();
    let text = String::from_utf8(body.to_vec()).unwrap();
    assert!(text.contains("[agent]"));
    assert!(text.contains("[server]"));
}

#[tokio::test]
async fn put_config_raw_persists_round_trip() {
    let state = test_state();
    let app = build_router(state.clone());
    let raw = r#"
[agent]
name = "RawBot"
id = "test-agent"
workspace = "/tmp/workspace"
log_level = "info"

[server]
bind = "127.0.0.1"
port = 18789

[database]
path = ":memory:"

[models]
primary = "ollama/qwen3:8b"
"#;
    let put_resp = app
        .clone()
        .oneshot(
            Request::builder()
                .method("PUT")
                .uri("/api/config/raw")
                .header("content-type", "text/plain")
                .body(Body::from(raw))
                .unwrap(),
        )
        .await
        .unwrap();
    assert_eq!(put_resp.status(), StatusCode::OK);
    let put_body = json_body(put_resp).await;
    assert_eq!(put_body["persisted"], true);

    let get_resp = app
        .oneshot(
            Request::builder()
                .uri("/api/config")
                .body(Body::empty())
                .unwrap(),
        )
        .await
        .unwrap();
    assert_eq!(get_resp.status(), StatusCode::OK);
    let cfg = json_body(get_resp).await;
    assert_eq!(cfg["agent"]["name"], "RawBot");
}

#[tokio::test]
async fn routing_diagnostics_exposes_full_routing_tunables() {
    let state = test_state();
    {
        let mut config = state.config.write().await;
        config.models.routing.confidence_threshold = 0.73;
        config.models.routing.estimated_output_tokens = 777;
    }
    let app = build_router(state);

    let resp = app
        .oneshot(
            Request::builder()
                .uri("/api/models/routing-diagnostics")
                .body(Body::empty())
                .unwrap(),
        )
        .await
        .unwrap();
    assert_eq!(resp.status(), StatusCode::OK);
    let body = json_body(resp).await;
    assert_eq!(body["config"]["confidence_threshold"], 0.73);
    assert_eq!(body["config"]["estimated_output_tokens"], 777);
}

#[tokio::test]
async fn put_config_rejects_invalid() {
    let state = test_state();
    let old_name = state.config.read().await.agent.name.clone();
    let app = build_router(state.clone());
    let req = Request::builder()
        .method("PUT")
        .uri("/api/config")
        .header("content-type", "application/json")
        .body(Body::from(r#"{"memory":{"working_budget_pct":200}}"#))
        .unwrap();

    let resp = app.oneshot(req).await.unwrap();
    assert_eq!(resp.status(), StatusCode::BAD_REQUEST);
    let current_name = state.config.read().await.agent.name.clone();
    assert_eq!(current_name, old_name);
}

#[tokio::test]
async fn get_session_ok() {
    let state = test_state();
    let session_id = roboticus_db::sessions::find_or_create(&state.db, "agent-1", None).unwrap();
    let app = build_router(state);

    let req = Request::builder()
        .uri(format!("/api/sessions/{session_id}"))
        .body(Body::empty())
        .unwrap();

    let resp = app.oneshot(req).await.unwrap();
    assert_eq!(resp.status(), StatusCode::OK);

    let body = json_body(resp).await;
    assert_eq!(body["id"], session_id);
    assert_eq!(body["agent_id"], "agent-1");
}

#[tokio::test]
async fn list_sessions_returns_array() {
    let app = build_router(test_state());
    let req = Request::builder()
        .uri("/api/sessions")
        .body(Body::empty())
        .unwrap();

    let resp = app.oneshot(req).await.unwrap();
    assert_eq!(resp.status(), StatusCode::OK);

    let body = json_body(resp).await;
    let sessions = body["sessions"].as_array().unwrap();
    assert!(sessions.is_empty());
}

#[tokio::test]
async fn get_working_memory_returns_entries() {
    let state = test_state();
    let session_id = roboticus_db::sessions::find_or_create(&state.db, "agent-1", None).unwrap();
    let app = build_router(state);

    let req = Request::builder()
        .uri(format!("/api/memory/working/{session_id}"))
        .body(Body::empty())
        .unwrap();

    let resp = app.oneshot(req).await.unwrap();
    assert_eq!(resp.status(), StatusCode::OK);

    let body = json_body(resp).await;
    assert!(body["entries"].as_array().is_some());
}

#[tokio::test]
async fn get_episodic_memory_returns_entries() {
    let app = build_router(test_state());
    let req = Request::builder()
        .uri("/api/memory/episodic")
        .body(Body::empty())
        .unwrap();

    let resp = app.oneshot(req).await.unwrap();
    assert_eq!(resp.status(), StatusCode::OK);

    let body = json_body(resp).await;
    let entries = body["entries"].as_array().unwrap();
    assert!(entries.is_empty());
}

#[tokio::test]
async fn get_episodic_memory_with_limit() {
    let app = build_router(test_state());
    let req = Request::builder()
        .uri("/api/memory/episodic?limit=5")
        .body(Body::empty())
        .unwrap();

    let resp = app.oneshot(req).await.unwrap();
    assert_eq!(resp.status(), StatusCode::OK);

    let body = json_body(resp).await;
    assert!(body["entries"].as_array().is_some());
}

#[tokio::test]
async fn get_semantic_memory_returns_entries() {
    let app = build_router(test_state());
    let req = Request::builder()
        .uri("/api/memory/semantic/foo")
        .body(Body::empty())
        .unwrap();

    let resp = app.oneshot(req).await.unwrap();
    assert_eq!(resp.status(), StatusCode::OK);

    let body = json_body(resp).await;
    let entries = body["entries"].as_array().unwrap();
    assert!(entries.is_empty());
}

#[tokio::test]
async fn memory_search_with_q_returns_results() {
    let app = build_router(test_state());
    let req = Request::builder()
        .uri("/api/memory/search?q=test")
        .body(Body::empty())
        .unwrap();

    let resp = app.oneshot(req).await.unwrap();
    assert_eq!(resp.status(), StatusCode::OK);

    let body = json_body(resp).await;
    assert!(body["results"].as_array().is_some());
}

#[tokio::test]
async fn memory_search_missing_q_returns_400() {
    let app = build_router(test_state());
    let req = Request::builder()
        .uri("/api/memory/search")
        .body(Body::empty())
        .unwrap();

    let resp = app.oneshot(req).await.unwrap();
    assert_eq!(resp.status(), StatusCode::BAD_REQUEST);

    let body = text_body(resp).await;
    assert!(body.contains("missing"));
}

/// FTS5 operator stripping: queries with AND/OR/NOT are sanitized to phrase search;
/// results for "word AND other" should match results for "word other".
#[tokio::test]
async fn memory_search_fts5_operator_stripping() {
    let app = build_router(test_state());
    let with_ops = Request::builder()
        .uri("/api/memory/search?q=foo+AND+bar+OR+NOT+baz")
        .body(Body::empty())
        .unwrap();
    let without_ops = Request::builder()
        .uri("/api/memory/search?q=foo+bar+baz")
        .body(Body::empty())
        .unwrap();

    let resp_with = app.clone().oneshot(with_ops).await.unwrap();
    let resp_without = app.oneshot(without_ops).await.unwrap();

    assert_eq!(resp_with.status(), StatusCode::OK);
    assert_eq!(resp_without.status(), StatusCode::OK);

    let json_with = json_body(resp_with).await;
    let json_without = json_body(resp_without).await;
    let results_with = json_with["results"].as_array().unwrap();
    let results_without = json_without["results"].as_array().unwrap();
    assert_eq!(
        results_with.len(),
        results_without.len(),
        "FTS5 operator stripping should yield same result count"
    );
}

#[tokio::test]
async fn knowledge_ingest_rejects_path_outside_workspace() {
    let state = test_state();
    let workspace = tempfile::tempdir().unwrap();
    {
        let mut cfg = state.config.write().await;
        cfg.agent.workspace = workspace.path().to_path_buf();
    }
    let app = build_router(state);
    let outside = std::env::temp_dir().join(format!("ic-outside-{}.txt", uuid::Uuid::new_v4()));
    std::fs::write(&outside, b"secret").unwrap();

    let req = Request::builder()
        .method("POST")
        .uri("/api/knowledge/ingest")
        .header("content-type", "application/json")
        .body(Body::from(
            serde_json::json!({ "path": outside.to_string_lossy() }).to_string(),
        ))
        .unwrap();

    let resp = app.oneshot(req).await.unwrap();
    assert_eq!(resp.status(), StatusCode::BAD_REQUEST);
    let body = text_body(resp).await;
    assert!(body.contains("escapes workspace root"));

    let _ = std::fs::remove_file(outside);
}

#[tokio::test]
async fn knowledge_ingest_rejects_missing_workspace_root() {
    let state = test_state();
    let missing =
        std::env::temp_dir().join(format!("ic-missing-workspace-{}", uuid::Uuid::new_v4()));
    {
        let mut cfg = state.config.write().await;
        cfg.agent.workspace = missing.clone();
    }
    let app = build_router(state);
    let req = Request::builder()
        .method("POST")
        .uri("/api/knowledge/ingest")
        .header("content-type", "application/json")
        .body(Body::from(r#"{"path":"README.md"}"#))
        .unwrap();

    let resp = app.oneshot(req).await.unwrap();
    assert_eq!(resp.status(), StatusCode::BAD_REQUEST);
    let body = text_body(resp).await;
    assert!(body.contains("workspace root"));
}

#[tokio::test]
async fn list_cron_jobs_returns_array() {
    let app = build_router(test_state());
    let req = Request::builder()
        .uri("/api/cron/jobs")
        .body(Body::empty())
        .unwrap();

    let resp = app.oneshot(req).await.unwrap();
    assert_eq!(resp.status(), StatusCode::OK);

    let body = json_body(resp).await;
    let jobs = body["jobs"].as_array().unwrap();
    assert!(jobs.is_empty());
}

#[tokio::test]
async fn create_cron_job_returns_job_id() {
    let app = build_router(test_state());
    let req = Request::builder()
        .method("POST")
        .uri("/api/cron/jobs")
        .header("content-type", "application/json")
        .body(Body::from(
            r#"{"name":"test-job","agent_id":"test","schedule_kind":"interval","schedule_expr":"1h"}"#,
        ))
        .unwrap();

    let resp = app.oneshot(req).await.unwrap();
    assert_eq!(resp.status(), StatusCode::OK);

    let body = json_body(resp).await;
    assert!(!body["job_id"].as_str().unwrap().is_empty());
}

#[tokio::test]
async fn create_cron_job_defaults_payload_to_agent_task_when_description_present() {
    let state = test_state();
    let app = build_router(state.clone());
    let req = Request::builder()
        .method("POST")
        .uri("/api/cron/jobs")
        .header("content-type", "application/json")
        .body(Body::from(
            r#"{"name":"morning-briefing","description":"summarize overnight events","agent_id":"test","schedule_kind":"cron","schedule_expr":"0 9 * * *"}"#,
        ))
        .unwrap();

    let resp = app.oneshot(req).await.unwrap();
    assert_eq!(resp.status(), StatusCode::OK);
    let body = json_body(resp).await;
    let job_id = body["job_id"].as_str().unwrap().to_string();

    let job = roboticus_db::cron::get_job(&state.db, &job_id)
        .unwrap()
        .expect("job should exist");
    let payload: serde_json::Value =
        serde_json::from_str(&job.payload_json).expect("payload should be valid JSON");
    assert_eq!(payload["action"], "agent_task");
    assert_eq!(payload["task"], "summarize overnight events");
}

#[tokio::test]
async fn create_cron_job_persists_description_field() {
    let state = test_state();
    let app = build_router(state.clone());
    let req = Request::builder()
        .method("POST")
        .uri("/api/cron/jobs")
        .header("content-type", "application/json")
        .body(Body::from(
            r#"{"name":"morning-briefing","description":"summarize overnight events","agent_id":"test","schedule_kind":"cron","schedule_expr":"0 9 * * *"}"#,
        ))
        .unwrap();

    let resp = app.oneshot(req).await.unwrap();
    assert_eq!(resp.status(), StatusCode::OK);
    let body = json_body(resp).await;
    let job_id = body["job_id"].as_str().unwrap().to_string();

    let job = roboticus_db::cron::get_job(&state.db, &job_id)
        .unwrap()
        .expect("job should exist");
    assert_eq!(
        job.description.as_deref(),
        Some("summarize overnight events")
    );
}

#[tokio::test]
async fn get_cron_job_returns_detail() {
    let state = test_state();
    let job_id = roboticus_db::cron::create_job(
        &state.db,
        "heartbeat",
        "agent-1",
        "every",
        None,
        r#"{"action":"ping"}"#,
    )
    .unwrap();

    let app = build_router(state);
    let req = Request::builder()
        .uri(format!("/api/cron/jobs/{job_id}"))
        .body(Body::empty())
        .unwrap();

    let resp = app.oneshot(req).await.unwrap();
    assert_eq!(resp.status(), StatusCode::OK);

    let body = json_body(resp).await;
    assert_eq!(body["id"], job_id);
    assert_eq!(body["name"], "heartbeat");
    assert_eq!(body["agent_id"], "agent-1");
}

#[tokio::test]
async fn get_cron_job_returns_404_for_missing() {
    let app = build_router(test_state());
    let req = Request::builder()
        .uri("/api/cron/jobs/nonexistent-id")
        .body(Body::empty())
        .unwrap();

    let resp = app.oneshot(req).await.unwrap();
    assert_eq!(resp.status(), StatusCode::NOT_FOUND);
}

#[tokio::test]
async fn run_cron_job_now_executes_and_records_run() {
    let state = test_state();
    let job_id = roboticus_db::cron::create_job(
        &state.db,
        "run-now",
        "agent-1",
        "cron",
        Some("0 * * * *"),
        r#"{"action":"noop"}"#,
    )
    .unwrap();

    let app = build_router(state.clone());
    let req = Request::builder()
        .method("POST")
        .uri(format!("/api/cron/jobs/{job_id}/run"))
        .body(Body::empty())
        .unwrap();

    let resp = app.oneshot(req).await.unwrap();
    assert_eq!(resp.status(), StatusCode::OK);
    let body = json_body(resp).await;
    assert_eq!(body["job_id"], job_id);
    assert_eq!(body["status"], "success");

    let runs = roboticus_db::cron::list_runs(&state.db, None, None, Some(&job_id), 10).unwrap();
    assert_eq!(runs.len(), 1);
    assert_eq!(runs[0].status, "success");
}

#[tokio::test]
async fn run_cron_job_now_returns_output_text_for_log_job() {
    let state = test_state();
    let job_id = roboticus_db::cron::create_job(
        &state.db,
        "run-now-log",
        "agent-1",
        "cron",
        Some("0 * * * *"),
        r#"{"action":"log","message":"hello from cron"}"#,
    )
    .unwrap();

    let app = build_router(state.clone());
    let req = Request::builder()
        .method("POST")
        .uri(format!("/api/cron/jobs/{job_id}/run"))
        .body(Body::empty())
        .unwrap();

    let resp = app.oneshot(req).await.unwrap();
    assert_eq!(resp.status(), StatusCode::OK);
    let body = json_body(resp).await;
    assert_eq!(body["status"], "success");
    assert_eq!(body["output_text"], "hello from cron");

    let runs = roboticus_db::cron::list_runs(&state.db, None, None, Some(&job_id), 10).unwrap();
    assert_eq!(runs[0].output_text.as_deref(), Some("hello from cron"));
}

#[tokio::test]
async fn delete_cron_job_removes_job() {
    let state = test_state();
    let job_id = roboticus_db::cron::create_job(
        &state.db,
        "disposable",
        "agent-1",
        "cron",
        Some("0 * * * *"),
        "{}",
    )
    .unwrap();

    let app = build_router(state);
    let req = Request::builder()
        .method("DELETE")
        .uri(format!("/api/cron/jobs/{job_id}"))
        .body(Body::empty())
        .unwrap();

    let resp = app.oneshot(req).await.unwrap();
    assert_eq!(resp.status(), StatusCode::OK);

    let body = json_body(resp).await;
    assert_eq!(body["deleted"], true);
    assert_eq!(body["id"], job_id);
}

#[tokio::test]
async fn delete_cron_job_returns_404_for_missing() {
    let app = build_router(test_state());
    let req = Request::builder()
        .method("DELETE")
        .uri("/api/cron/jobs/nonexistent-id")
        .body(Body::empty())
        .unwrap();

    let resp = app.oneshot(req).await.unwrap();
    assert_eq!(resp.status(), StatusCode::NOT_FOUND);
}
