//! Integration test: `Episode::Ops` events emitted via `klieo-ops`
//! project correctly into `RunLog` via the klieo-runlog projector.
//!
//! Verifies:
//! 1. A sqlite-backed `EpisodicMemory` roundtrips `Episode::Ops` payloads.
//! 2. The projector surfaces ops events as `StepKind::OpsEvent` steps.
//! 3. Agent-level steps (LlmCall, ToolCall) and ops steps coexist in
//!    the correct order within the same `RunLog`.
//! 4. All 11 Phase B `OpsEvent` variants (EscalationRaised through
//!    HandoffVerificationFailed) project with the correct snake_case name
//!    and their payload round-trips via `OpsEvent::from_episode_payload`.

use klieo_core::ids::RunId;
use klieo_core::memory::{Episode, EpisodicMemory, ToolResult};
use klieo_memory_sqlite::MemorySqlite;
use klieo_ops::emit::emit_ops_event;
use klieo_ops::types::{AgentId, TenantId};
use klieo_ops::OpsEvent;
use klieo_runlog::projector::project;
use klieo_runlog::types::StepKind;
use std::sync::Arc;

// ── Phase B variant test helpers ─────────────────────────────────────────────

/// Emit a single `OpsEvent`, project it, and assert:
/// - exactly one `StepKind::OpsEvent` step was produced;
/// - `step.name` equals `expected_kind` (the snake_case serde discriminant);
/// - `OpsEvent::from_episode_payload(&step.input)` succeeds and the
///   resulting JSON matches the original payload (round-trip).
async fn assert_phase_b_variant(event: OpsEvent, expected_kind: &str) {
    let memory = in_memory_episodic().await;
    let run_id = RunId::new();

    memory
        .record(
            run_id,
            Episode::Started {
                agent: "b-test".into(),
            },
        )
        .await
        .unwrap();

    emit_ops_event(&*memory, run_id, event.clone())
        .await
        .unwrap_or_else(|e| panic!("emit failed for {expected_kind}: {e}"));

    let episodes = memory.replay(run_id).await.unwrap();
    let log = project(run_id, "b-test", &episodes);

    assert_eq!(
        log.steps.len(),
        1,
        "{expected_kind}: expected 1 step, got {}",
        log.steps.len()
    );

    let step = &log.steps[0];
    assert_eq!(
        step.kind,
        StepKind::OpsEvent,
        "{expected_kind}: wrong StepKind"
    );
    assert_eq!(
        step.name.as_deref(),
        Some(expected_kind),
        "{expected_kind}: step.name mismatch"
    );

    let recovered = OpsEvent::from_episode_payload(&step.input)
        .unwrap_or_else(|| panic!("{expected_kind}: from_episode_payload returned None"));
    let original_json = serde_json::to_value(&event).unwrap();
    let recovered_json = serde_json::to_value(&recovered).unwrap();
    assert_eq!(
        original_json, recovered_json,
        "{expected_kind}: payload did not round-trip cleanly"
    );
}

async fn in_memory_episodic() -> Arc<dyn EpisodicMemory> {
    let mem = MemorySqlite::new(":memory:", Arc::new(klieo_memory_sqlite::DummyEmbedder))
        .await
        .expect("MemorySqlite::new ok");
    mem.episodic
}

#[tokio::test]
async fn ops_events_project_as_ops_event_steps() {
    let memory = in_memory_episodic().await;
    let run_id = RunId::new();

    memory
        .record(
            run_id,
            Episode::Started {
                agent: "test-agent".into(),
            },
        )
        .await
        .expect("record Started");

    emit_ops_event(
        &*memory,
        run_id,
        OpsEvent::SupervisorStateChange {
            tenant: Some(TenantId("BL_test".into())),
            agent: AgentId("test-agent".into()),
            state: "running".into(),
            reason: None,
        },
    )
    .await
    .expect("emit SupervisorStateChange");

    memory
        .record(run_id, Episode::llm_call(100, 50))
        .await
        .expect("record LlmCall");

    emit_ops_event(
        &*memory,
        run_id,
        OpsEvent::GateDecision {
            tenant: Some(TenantId("BL_test".into())),
            tool: "PayoutTool".into(),
            decision: "allow".into(),
            gate: "AllowlistGate".into(),
            policy_ref: None,
            reason: None,
        },
    )
    .await
    .expect("emit GateDecision");

    memory
        .record(
            run_id,
            Episode::ToolCall {
                name: "PayoutTool".into(),
                args: serde_json::json!({"amount": 100}),
                result: ToolResult::Ok {
                    value: serde_json::json!({"status": "ok"}),
                },
            },
        )
        .await
        .expect("record ToolCall");

    memory
        .record(run_id, Episode::Completed)
        .await
        .expect("record Completed");

    let episodes = memory.replay(run_id).await.expect("replay ok");
    assert_eq!(
        episodes.len(),
        6,
        "6 episodes: Started + ops + LlmCall + ops + ToolCall + Completed"
    );

    let log = project(run_id, "test-agent", &episodes);

    // Steps: LlmCall(idx=0), OpsEvent(SupervisorStateChange, idx=1)
    // Wait — projector processes in order. Let's check what we get:
    // Episode order: Started(skipped), Ops(supervisor), LlmCall, Ops(gate), ToolCall, Completed
    // Steps projected: Ops(idx=0), LlmCall(idx=1), Ops(idx=2), ToolCall(idx=3)
    assert_eq!(
        log.steps.len(),
        4,
        "4 steps: 2 OpsEvent + LlmCall + ToolCall"
    );

    let ops_steps: Vec<_> = log
        .steps
        .iter()
        .filter(|s| s.kind == StepKind::OpsEvent)
        .collect();
    assert_eq!(ops_steps.len(), 2, "two OpsEvent steps");

    let supervisor_step = &ops_steps[0];
    assert_eq!(
        supervisor_step.name.as_deref(),
        Some("supervisor_state_change"),
        "first ops step kind is supervisor_state_change"
    );

    let gate_step = &ops_steps[1];
    assert_eq!(
        gate_step.name.as_deref(),
        Some("gate_decision"),
        "second ops step kind is gate_decision"
    );

    // Verify payload round-trips through OpsEvent.
    let recovered_gate =
        OpsEvent::from_episode_payload(&gate_step.input).expect("gate payload round-trips");
    let gate_json = serde_json::to_value(&recovered_gate).unwrap();
    assert_eq!(gate_json["tool"], "PayoutTool");
    assert_eq!(gate_json["decision"], "allow");
}

#[tokio::test]
async fn ops_and_agent_steps_coexist_in_correct_order() {
    let memory = in_memory_episodic().await;
    let run_id = RunId::new();

    memory
        .record(
            run_id,
            Episode::Started {
                agent: "order-agent".into(),
            },
        )
        .await
        .unwrap();

    emit_ops_event(
        &*memory,
        run_id,
        OpsEvent::KillSwitchTripped {
            tenant: Some(TenantId("BL_test".into())),
            trigger: "programmatic".into(),
            reason: "budget exceeded".into(),
        },
    )
    .await
    .unwrap();

    memory
        .record(
            run_id,
            Episode::Failed {
                error: "halted".into(),
            },
        )
        .await
        .unwrap();

    let episodes = memory.replay(run_id).await.unwrap();
    let log = project(run_id, "order-agent", &episodes);

    // One OpsEvent step (KillSwitchTripped); Failed sets status, not a Step.
    assert_eq!(log.steps.len(), 1);
    assert_eq!(log.steps[0].kind, StepKind::OpsEvent);
    assert_eq!(log.steps[0].name.as_deref(), Some("kill_switch_tripped"));
    assert_eq!(log.status, klieo_runlog::types::RunStatus::Failed);
}

// ── Phase B variant tests (all 11 new OpsEvent variants) ────────────────────

#[tokio::test]
async fn phase_b_escalation_raised_projects_correctly() {
    assert_phase_b_variant(
        OpsEvent::EscalationRaised {
            tenant: Some(TenantId("BL_test".into())),
            ticket: "esc_01HXY0000001".into(),
            severity: "high".into(),
            reason: "amount exceeds cap".into(),
            provenance_root: Some("b3ad7e".into()),
        },
        "escalation_raised",
    )
    .await;
}

#[tokio::test]
async fn phase_b_escalation_state_changed_projects_correctly() {
    assert_phase_b_variant(
        OpsEvent::EscalationStateChanged {
            tenant: Some(TenantId("BL_test".into())),
            ticket: "esc_01HXY0000002".into(),
            from: "raised".into(),
            to: "acknowledged".into(),
            reason: Some("ops team alerted".into()),
        },
        "escalation_state_changed",
    )
    .await;
}

#[tokio::test]
async fn phase_b_escalation_resolved_projects_correctly() {
    assert_phase_b_variant(
        OpsEvent::EscalationResolved {
            tenant: Some(TenantId("BL_test".into())),
            ticket: "esc_01HXY0000003".into(),
            outcome: "resolved".into(),
            reason: Some("approved by second approver".into()),
        },
        "escalation_resolved",
    )
    .await;
}

#[tokio::test]
async fn phase_b_work_planned_projects_correctly() {
    assert_phase_b_variant(
        OpsEvent::WorkPlanned {
            tenant: Some(TenantId("BL_test".into())),
            work_id: "wrk_01JXABCDEF".into(),
            title: "triage claim".into(),
            depends_on: vec!["wrk_01JXA00001".into()],
        },
        "work_planned",
    )
    .await;
}

#[tokio::test]
async fn phase_b_work_dependency_added_projects_correctly() {
    assert_phase_b_variant(
        OpsEvent::WorkDependencyAdded {
            tenant: Some(TenantId("BL_test".into())),
            child: "wrk_child_01".into(),
            on: "wrk_parent_01".into(),
        },
        "work_dependency_added",
    )
    .await;
}

#[tokio::test]
async fn phase_b_work_dispatched_projects_correctly() {
    assert_phase_b_variant(
        OpsEvent::WorkDispatched {
            tenant: Some(TenantId("BL_test".into())),
            work_id: "wrk_01JXABCDEF".into(),
        },
        "work_dispatched",
    )
    .await;
}

#[tokio::test]
async fn phase_b_work_transition_projects_correctly() {
    assert_phase_b_variant(
        OpsEvent::WorkTransition {
            tenant: Some(TenantId("BL_test".into())),
            work_id: "wrk_01JXABCDEF".into(),
            from: "in_progress".into(),
            to: "done".into(),
            reason: None,
        },
        "work_transition",
    )
    .await;
}

#[tokio::test]
async fn phase_b_handoff_packaged_projects_correctly() {
    assert_phase_b_variant(
        OpsEvent::HandoffPackaged {
            tenant: Some(TenantId("BL_test".into())),
            from_run: "rn_01HXX0000001".into(),
            merkle_root: "deadbeefcafe".into(),
        },
        "handoff_packaged",
    )
    .await;
}

#[tokio::test]
async fn phase_b_handoff_delivered_projects_correctly() {
    assert_phase_b_variant(
        OpsEvent::HandoffDelivered {
            tenant: Some(TenantId("BL_test".into())),
            from_run: "rn_01HXX0000001".into(),
            to_agent: "agent_receiver_01".into(),
        },
        "handoff_delivered",
    )
    .await;
}

#[tokio::test]
async fn phase_b_handoff_verified_projects_correctly() {
    assert_phase_b_variant(
        OpsEvent::HandoffVerified {
            tenant: Some(TenantId("BL_test".into())),
            from_run: "rn_01HXX0000001".into(),
        },
        "handoff_verified",
    )
    .await;
}

#[tokio::test]
async fn phase_b_handoff_verification_failed_projects_correctly() {
    assert_phase_b_variant(
        OpsEvent::HandoffVerificationFailed {
            tenant: Some(TenantId("BL_test".into())),
            from_run: Some("rn_01HXX0000001".into()),
            reason: "signature math failed".into(),
        },
        "handoff_verification_failed",
    )
    .await;
}
