//! SC-3 — Outbound prompt on new approval, in-process un-gated send.
//!
//! When the daemon observes a new fire-and-return approval (action NOT
//! prefixed `ws.method:`) while enabled and a handle is paired, it sends
//! EXACTLY ONE iMessage to the paired handle, body carries a per-approval
//! code, via the injectable `messages_send` path (here a spy) — NOT the gated
//! `messages.send` WS method, so NO second `approval.requested` is emitted.
//!
//! Edges: feature OFF → zero sends; `ws.method:*` row → zero sends.

mod messaging_harness;
use car_messaging::messaging_orchestrator::MessageSender;
use messaging_harness::{extract_code, FailFirstSender, Harness, PAIRED};
use std::sync::Arc;

#[tokio::test]
async fn outbound_one_send_with_code_no_gate_loop() {
    let h = Harness::enabled_paired();

    // One fire-and-return approval (the v1-target producer).
    let _id = h.seed_fire_and_return("send email to the board").await;
    assert_eq!(
        h.approval_requested_count().await,
        1,
        "seeding raised exactly one approval.requested"
    );

    // Observe → send the prompt.
    h.orch.observe_and_notify().await;

    // Exactly ONE send, to the paired handle.
    let calls = h.spy.calls();
    assert_eq!(calls.len(), 1, "exactly one outbound send, got {calls:?}");
    let (handle, body) = &calls[0];
    assert_eq!(handle, PAIRED, "send went to the paired handle");

    // Body carries the action summary AND a short per-approval code.
    assert!(
        body.contains("send email to the board"),
        "body has the action summary: {body}"
    );
    let code = extract_code(body).expect("body carries a per-approval code");
    assert!(!code.is_empty(), "code is non-empty: {body}");

    // The send went through the spy (the injectable messages_send seam), NOT
    // the gated `messages.send` WS method — so NO second approval.requested
    // was raised by the send itself (the gate-loop signature).
    assert_eq!(
        h.approval_requested_count().await,
        1,
        "the send must NOT raise a second approval.requested (no gate loop)"
    );

    // Idempotency: a second observe does NOT re-send for the same approval.
    h.orch.observe_and_notify().await;
    assert_eq!(
        h.spy.count(),
        1,
        "re-observing the same pending approval does not duplicate the send"
    );
}

#[tokio::test]
async fn feature_off_zero_sends() {
    let h = Harness::disabled();
    h.seed_fire_and_return("dangerous action").await;
    h.orch.observe_and_notify().await;
    assert_eq!(h.spy.count(), 0, "feature OFF ⇒ zero sends");
}

#[tokio::test]
async fn enabled_but_no_paired_handle_zero_sends() {
    let h = Harness::enabled_no_handle();
    h.seed_fire_and_return("dangerous action").await;
    h.orch.observe_and_notify().await;
    assert_eq!(h.spy.count(), 0, "no paired handle ⇒ zero sends");
}

#[tokio::test]
async fn send_failure_rolls_back_code_and_retries_next_tick() {
    // RELIABILITY regression: a transient Messages send failure must NOT
    // permanently suppress the approval. Before the fix the code was inserted
    // into the map BEFORE the send, so on `Err` the idempotency guard skipped
    // the approval forever. The fix rolls back the mapping on `Err` so the next
    // observe tick re-mints and re-sends — landing EXACTLY ONE successful prompt.
    let sender = FailFirstSender::new(1); // fail the first send, then succeed.
    let h = Harness::enabled_paired_with_sender(sender.clone() as Arc<dyn MessageSender>);

    let _id = h.seed_fire_and_return("send email to the board").await;

    // Tick 1: the send fails. Nothing succeeded yet; the code was rolled back.
    h.orch.observe_and_notify().await;
    assert_eq!(sender.attempt_count(), 1, "one send attempted on tick 1");
    assert_eq!(
        sender.count(),
        0,
        "tick 1 send failed ⇒ no successful prompt yet"
    );

    // Tick 2: because the code was rolled back, the approval is re-attempted and
    // this time the send succeeds — exactly one successful prompt lands.
    h.orch.observe_and_notify().await;
    assert_eq!(
        sender.attempt_count(),
        2,
        "the approval was re-attempted on tick 2 (no permanent suppression)"
    );
    assert_eq!(
        sender.count(),
        1,
        "exactly one successful prompt after the retry"
    );
    let calls = sender.calls();
    assert_eq!(calls[0].0, PAIRED, "the retried send went to the paired handle");
    assert!(
        extract_code(&calls[0].1).is_some(),
        "the retried prompt carries a per-approval code: {}",
        calls[0].1
    );

    // Tick 3: idempotency holds after a SUCCESSFUL send — no duplicate prompt.
    h.orch.observe_and_notify().await;
    assert_eq!(
        sender.count(),
        1,
        "a successful prompt is not re-sent on a later tick"
    );
    assert_eq!(
        sender.attempt_count(),
        2,
        "no further send attempt once the prompt has landed"
    );
}

#[tokio::test]
async fn ws_method_row_excluded_zero_sends() {
    let h = Harness::enabled_paired();
    // A blocking-gate (`ws.method:*`) approval — the wrong producer for v1.
    h.seed_ws_method("automation.run_applescript").await;
    h.orch.observe_and_notify().await;
    assert_eq!(
        h.spy.count(),
        0,
        "a ws.method:* blocking-gate row must NOT trigger an outbound prompt"
    );
}
