//! Coordinator orchestration unit tests using `MockBackend`.
//!
//! These tests exercise alias detection, move/copy semantics, DAG walks,
//! and cleanup policies without any disk I/O.

#![cfg(test)]

use std::sync::Arc;

use tokio::sync::RwLock;
use uuid::Uuid;

use crate::backend::mock::MockBackend;
use crate::backend::ArchiveBackend;
use crate::coordinator::Archivist;
use crate::registry::{
    cache::ReadCache, ArchiveRegistration, FailureMode, WritePolicy,
};
use crate::types::{
    DagEdge, MessageRecord, RegisterConnectorRequest, RegisterStatus, SessionCompleteness,
    SessionKind, SessionMetadata,
};

/// Construct a blank `SessionMetadata` with the given `scroll_id` and
/// `connector_uid`. Sensible defaults for every other field.
fn blank_session(scroll_id: Uuid, connector_uid: Uuid) -> SessionMetadata {
    let now = chrono::Utc::now();
    SessionMetadata {
        version: 1,
        scroll_id,
        created_at: now,
        updated_at: now,
        title: None,
        connector_uid,
        native_session_id: None,
        agent_id: None,
        parent_scroll_id: None,
        continuation: None,
        tags: Vec::new(),
        metadata: serde_json::Value::Null,
        no_update: false,
        kind: SessionKind::Chat,
        acp_client_id: None,
        is_connected: None,
        current_session_id: None,
        models: None,
        modes: None,
        config_options: None,
        completeness: SessionCompleteness::Complete,
        matrix_room_id: None,
        matrix_sharing_active: false,
        matrix_shared_at: None,
        is_subagent: false,
        subagent_type: None,
        spawning_tool_use_id: None,
    }
}

/// Construct a blank `MessageRecord` scoped to the given session with a
/// freshly generated `message_id` and current timestamp.
fn blank_message(session: Uuid) -> MessageRecord {
    MessageRecord {
        version: 1,
        message_id: Uuid::now_v7(),
        session,
        parent_id: None,
        ts: chrono::Utc::now(),
        role: "user".into(),
        author: None,
        content_md: String::new(),
        content_parts: None,
        attachments: Vec::new(),
        metadata: serde_json::Value::Null,
    }
}

async fn make_coordinator_with_single_mock() -> Archivist {
    let backend: Arc<dyn ArchiveBackend> = Arc::new(MockBackend::new());
    let initial_health = backend.health_check().await;
    let reg = Arc::new(ArchiveRegistration::new(
        "main".into(),
        "mock",
        backend,
        /* write_active */ true,
        FailureMode::Required,
        /* read_priority */ 0,
        /* enabled */ true,
        WritePolicy::Inline,
        /* writer */ None,
        initial_health,
    ));
    Archivist {
        registrations: RwLock::new(vec![reg]),
        read_cache: Arc::new(ReadCache::new()),
        registry_path: std::path::PathBuf::from("/mock/.archives.json"),
    }
}

#[tokio::test]
async fn register_connector_assigns_uid_and_returns_accepted() {
    let coord = make_coordinator_with_single_mock().await;
    let req = RegisterConnectorRequest {
        r#type: "OpenCode".into(),
        title: "test".into(),
        client_native_id: "opencode@localhost".into(),
        custom_uid: None,
        metadata: serde_json::Value::Null,
        fingerprint: None,
    };
    let resp = coord.register_connector(req, None).await.expect("register");
    assert!(matches!(resp.status, RegisterStatus::Accepted));
    assert_ne!(resp.connector_uid, Uuid::nil());
}

#[tokio::test]
async fn register_connector_aliases_on_duplicate_native_id() {
    let coord = make_coordinator_with_single_mock().await;
    let mk_req = || RegisterConnectorRequest {
        r#type: "OpenCode".into(),
        title: "test".into(),
        client_native_id: "opencode@localhost".into(),
        custom_uid: None,
        metadata: serde_json::Value::Null,
        fingerprint: None,
    };
    let first = coord.register_connector(mk_req(), None).await.unwrap();
    let second = coord.register_connector(mk_req(), None).await.unwrap();
    assert_eq!(second.connector_uid, first.connector_uid);
    assert!(matches!(second.status, RegisterStatus::Aliased));
}

#[tokio::test]
async fn get_session_tree_walks_full_dag() {
    let coord = make_coordinator_with_single_mock().await;
    let connector_uid = Uuid::now_v7();
    let root = Uuid::now_v7();
    let child_a = Uuid::now_v7();
    let child_b = Uuid::now_v7();
    let grand = Uuid::now_v7();

    let backend = coord.registrations.read().await[0].backend.clone();
    for id in [root, child_a, child_b, grand] {
        backend
            .put_session(blank_session(id, connector_uid))
            .await
            .unwrap();
    }

    for (p, c) in [(root, child_a), (root, child_b), (child_a, grand)] {
        coord
            .append_dag_edge(
                DagEdge {
                    parent: p,
                    child: c,
                    agent_id: String::new(),
                    subagent_type: None,
                    tool_use_id: None,
                    ts: Some(chrono::Utc::now()),
                },
                None,
            )
            .await
            .unwrap();
    }

    let edges = coord.get_session_tree(root, None).await.unwrap();
    assert_eq!(edges.len(), 3, "expected 3 edges, got {}", edges.len());
}

#[tokio::test]
async fn cleanup_empty_sessions_deletes_only_message_less_sessions() {
    let coord = make_coordinator_with_single_mock().await;

    let connector_uid = Uuid::now_v7();
    let empty = Uuid::now_v7();
    let populated = Uuid::now_v7();

    let backend = coord.registrations.read().await[0].backend.clone();
    for scroll_id in [empty, populated] {
        backend
            .put_session(blank_session(scroll_id, connector_uid))
            .await
            .unwrap();
    }
    backend
        .append_messages(populated, vec![blank_message(populated)])
        .await
        .unwrap();

    let (deleted, total) = coord.cleanup_empty_sessions(None).await.unwrap();
    assert_eq!(deleted, 1);
    assert_eq!(total, 2);
    assert!(backend.get_session(empty).await.unwrap().is_none());
    assert!(backend.get_session(populated).await.unwrap().is_some());
}
