//! Shared in-process harness for the iMessage approval-transport orchestrator
//! gate tests (SC-3/4/5/6/7). Builds an `Arc<HostState>` the test seeds with
//! approvals, a `MessagingConfigStore` over a temp `~/.car/`, and a capturing
//! send spy — NO live daemon, NO Messages.app, NO chat.db.
//!
//! Allowing dead_code: each integration-test binary only uses a subset of
//! these helpers, so the unused ones would otherwise warn per-binary.
#![allow(dead_code)]

use car_proto::{CreateHostApprovalRequest, HostApprovalRequest, HostApprovalStatus};
use car_server_types::host::HostState;
use car_messaging::messaging_config::MessagingConfigStore;
use car_messaging::messaging_orchestrator::{MessageSender, MessagingOrchestrator, SendOutcome};
use std::sync::{Arc, Mutex};
use tempfile::TempDir;

/// A capturing send spy. Records every `(handle, body)` the orchestrator
/// sends so tests can assert the count, recipient, and body content.
#[derive(Default)]
pub struct SpySender {
    pub sent: Mutex<Vec<(String, String)>>,
}

impl SpySender {
    pub fn new() -> Arc<Self> {
        Arc::new(Self::default())
    }

    /// Snapshot of all `(handle, body)` sends recorded so far.
    pub fn calls(&self) -> Vec<(String, String)> {
        self.sent.lock().unwrap().clone()
    }

    /// How many sends were recorded.
    pub fn count(&self) -> usize {
        self.sent.lock().unwrap().len()
    }
}

impl MessageSender for SpySender {
    fn send(&self, handle: &str, body: &str) -> Result<SendOutcome, String> {
        self.sent
            .lock()
            .unwrap()
            .push((handle.to_string(), body.to_string()));
        Ok(SendOutcome::ok())
    }
}

/// A send spy that FAILS its first `fail_first` sends (returning `Err`) and
/// then succeeds, recording only the sends that succeeded. Models a transient
/// Messages.app failure so the retry path can be asserted: a send that errs
/// must not permanently suppress the approval — the next observe tick must
/// re-mint and re-send, landing exactly one successful prompt.
pub struct FailFirstSender {
    /// Remaining sends to fail before the first success.
    fail_remaining: Mutex<usize>,
    /// `(handle, body)` for every send that SUCCEEDED (failed sends excluded).
    pub sent: Mutex<Vec<(String, String)>>,
    /// Total send attempts (successful + failed), so a test can assert a retry
    /// actually happened.
    pub attempts: Mutex<usize>,
}

impl FailFirstSender {
    pub fn new(fail_first: usize) -> Arc<Self> {
        Arc::new(Self {
            fail_remaining: Mutex::new(fail_first),
            sent: Mutex::new(Vec::new()),
            attempts: Mutex::new(0),
        })
    }

    /// Snapshot of all `(handle, body)` sends that SUCCEEDED.
    pub fn calls(&self) -> Vec<(String, String)> {
        self.sent.lock().unwrap().clone()
    }

    /// How many sends SUCCEEDED.
    pub fn count(&self) -> usize {
        self.sent.lock().unwrap().len()
    }

    /// How many send attempts were made (successful + failed).
    pub fn attempt_count(&self) -> usize {
        *self.attempts.lock().unwrap()
    }
}

impl MessageSender for FailFirstSender {
    fn send(&self, handle: &str, body: &str) -> Result<SendOutcome, String> {
        *self.attempts.lock().unwrap() += 1;
        {
            let mut rem = self.fail_remaining.lock().unwrap();
            if *rem > 0 {
                *rem -= 1;
                return Err("transient Messages failure (spy)".to_string());
            }
        }
        self.sent
            .lock()
            .unwrap()
            .push((handle.to_string(), body.to_string()));
        Ok(SendOutcome::ok())
    }
}

/// The standard handle the harness pairs (the single allowlisted approver).
pub const PAIRED: &str = "+15551234567";

/// A fully-wired in-process harness: a host, a temp-rooted config store, the
/// send spy, and the orchestrator over all three.
pub struct Harness {
    pub host: Arc<HostState>,
    pub store: MessagingConfigStore,
    pub spy: Arc<SpySender>,
    pub orch: MessagingOrchestrator,
    _dir: TempDir,
}

impl Harness {
    /// Build a harness with the feature ENABLED and the standard handle paired.
    pub fn enabled_paired() -> Self {
        let dir = TempDir::new().expect("tempdir");
        let store = MessagingConfigStore::with_base_dir(dir.path());
        store.set_enabled(true).unwrap();
        store.add_handle(PAIRED).unwrap();
        Self::from_parts(dir, store)
    }

    /// Build a harness with the feature DISABLED (default), no handle paired.
    pub fn disabled() -> Self {
        let dir = TempDir::new().expect("tempdir");
        let store = MessagingConfigStore::with_base_dir(dir.path());
        Self::from_parts(dir, store)
    }

    /// Build an ENABLED harness with NO paired handle (empty allowlist).
    pub fn enabled_no_handle() -> Self {
        let dir = TempDir::new().expect("tempdir");
        let store = MessagingConfigStore::with_base_dir(dir.path());
        store.set_enabled(true).unwrap();
        Self::from_parts(dir, store)
    }

    fn from_parts(dir: TempDir, store: MessagingConfigStore) -> Self {
        let host = Arc::new(HostState::new());
        let spy = SpySender::new();
        let orch = MessagingOrchestrator::new(
            host.clone(),
            MessagingConfigStore::with_base_dir(dir.path()),
            spy.clone() as Arc<dyn MessageSender>,
            dir.path().to_path_buf(),
        );
        Self {
            host,
            store,
            spy,
            orch,
            _dir: dir,
        }
    }

    /// Build an ENABLED, paired harness whose orchestrator sends through a
    /// caller-supplied `MessageSender` (e.g. a [`FailFirstSender`]) instead of
    /// the default capturing spy. Used by the send-retry test, which needs the
    /// first send to fail. The default `spy` field is still present but unused.
    pub fn enabled_paired_with_sender(sender: Arc<dyn MessageSender>) -> Self {
        let dir = TempDir::new().expect("tempdir");
        let store = MessagingConfigStore::with_base_dir(dir.path());
        store.set_enabled(true).unwrap();
        store.add_handle(PAIRED).unwrap();
        let host = Arc::new(HostState::new());
        let spy = SpySender::new();
        let orch = MessagingOrchestrator::new(
            host.clone(),
            MessagingConfigStore::with_base_dir(dir.path()),
            sender,
            dir.path().to_path_buf(),
        );
        Self {
            host,
            store,
            spy,
            orch,
            _dir: dir,
        }
    }

    /// The base dir where the watermark + config live (the temp `~/.car/`).
    pub fn watermark_dir(&self) -> &std::path::Path {
        self._dir.path()
    }

    /// Seed a fire-and-return (system-level) approval and return its id.
    pub async fn seed_fire_and_return(&self, action: &str) -> String {
        let req = CreateHostApprovalRequest {
            agent_id: None,
            action: action.to_string(),
            details: serde_json::Value::Null,
            options: vec![],
            system_level: true,
        };
        let approval = self.host.create_approval(None, req).await.unwrap();
        approval.id
    }

    /// Seed a SESSION-OWNED fire-and-return approval (`client_id: Some(...)`,
    /// `system_level: false`) and return its id. This is what
    /// `host.request_approval` produces by default (`handler.rs:2008-2012`):
    /// the action carries no `ws.method:` prefix, but the row is owned by a WS
    /// session, so the in-process orchestrator (which is NOT a session and
    /// cannot own it) must NEVER prompt for it — `resolve_approval` would hit
    /// the cross-session fan-out branch and come back still-Pending, silently
    /// dropping the code. The orchestrator's eligibility predicate excludes
    /// these (it requires `client_id.is_none()`).
    pub async fn seed_session_owned_fire_and_return(&self, action: &str) -> String {
        let req = CreateHostApprovalRequest {
            agent_id: None,
            action: action.to_string(),
            details: serde_json::Value::Null,
            options: vec![],
            system_level: false,
        };
        // A session id owns the row. The orchestrator is not this session.
        let approval = self
            .host
            .create_approval(Some("owning-ws-session"), req)
            .await
            .unwrap();
        approval.id
    }

    /// Seed a blocking-gate (`ws.method:*`) approval and return its id. These
    /// are v2-deferred and must never be touched by the transport.
    pub async fn seed_ws_method(&self, method: &str) -> String {
        let req = CreateHostApprovalRequest {
            agent_id: None,
            action: format!("ws.method:{method}"),
            details: serde_json::Value::Null,
            options: vec![],
            system_level: true,
        };
        let approval = self.host.create_approval(None, req).await.unwrap();
        approval.id
    }

    /// Look up a single approval row by id (panics if absent).
    pub async fn approval(&self, id: &str) -> HostApprovalRequest {
        self.host
            .approvals()
            .await
            .into_iter()
            .find(|a| a.id == id)
            .unwrap_or_else(|| panic!("approval {id} not found"))
    }

    /// Whether an approval is Resolved.
    pub async fn is_resolved(&self, id: &str) -> bool {
        self.approval(id).await.status == HostApprovalStatus::Resolved
    }

    /// Count `approval.requested` events the host emitted (one per created
    /// approval; the transport's own send must NOT add another).
    pub async fn approval_requested_count(&self) -> usize {
        self.host
            .events(500)
            .await
            .into_iter()
            .filter(|e| e.kind == "approval.requested")
            .count()
    }

    /// Count `approval.resolved` events.
    pub async fn approval_resolved_count(&self) -> usize {
        self.host
            .events(500)
            .await
            .into_iter()
            .filter(|e| e.kind == "approval.resolved")
            .count()
    }
}

/// Build a synthetic inbound row as Unit 1's reader would emit. No chat.db.
pub fn inbound(handle: &str, body: &str, rowid: i64) -> car_ffi_common::integrations::InboundMessage {
    car_ffi_common::integrations::InboundMessage {
        rowid,
        handle_id: handle.to_string(),
        body: body.to_string(),
        date: 0,
    }
}

/// Extract the per-approval code the orchestrator put in an outbound prompt
/// body (`Reply \`A7 approve\` or \`A7 deny\`.`). Returns the first
/// backtick-delimited code token's leading word.
pub fn extract_code(body: &str) -> Option<String> {
    // The body contains "Reply `<code> approve`" — grab the token after the
    // first backtick.
    let after = body.split('`').nth(1)?;
    after.split_whitespace().next().map(|s| s.to_string())
}
