//! Behavior tests for the lifecycle router skeleton (issue #7).
//!
//! Each acceptance-criterion failure class gets a dedicated test. The
//! adapter-id / adapter-version distinction is exercised against a
//! fixture-style fake [`AdapterRegistry`] so the router can run
//! without the built-in registry — that demonstrates the router has
//! no CCD-specific dependencies.

use std::collections::BTreeMap;

use lifeloop::router::{AdapterRegistry, BuiltinAdapterRegistry, RouteError, RoutingPlan, route};
// `BuiltinAdapterRegistry` is `pub(crate)` from validation; re-exported here
// would be ideal but the public API ships only `AdapterRegistry` + `route`.
// The fake registry below is what tests use; the built-in registry is
// covered separately via a happy-path test that names a real adapter.
use lifeloop::{
    AdapterManifest, AdapterRole, CallbackRequest, ConformanceLevel, FrameClass, FrameContext,
    IntegrationMode, LifecycleEventKind, ManifestContextPressure, ManifestPlacementClass,
    ManifestPlacementSupport, ManifestReceipts, PayloadRef, RegisteredAdapter, SCHEMA_VERSION,
    SupportState,
};

// ---------------------------------------------------------------------------
// Fake adapter manifest fixture
// ---------------------------------------------------------------------------

const FAKE_ADAPTER_ID: &str = "fake-adapter";
const FAKE_ADAPTER_VERSION: &str = "0.0.1";
const FAKE_ADAPTER_OTHER_VERSION: &str = "9.9.9";

fn fake_manifest() -> AdapterManifest {
    AdapterManifest {
        contract_version: SCHEMA_VERSION.to_string(),
        adapter_id: FAKE_ADAPTER_ID.into(),
        adapter_version: FAKE_ADAPTER_VERSION.into(),
        display_name: "Fake".into(),
        role: AdapterRole::PrimaryWorker,
        integration_modes: vec![IntegrationMode::NativeHook],
        lifecycle_events: BTreeMap::new(),
        placement: BTreeMap::from([(
            ManifestPlacementClass::PreSession,
            ManifestPlacementSupport {
                support: SupportState::Native,
                max_bytes: None,
            },
        )]),
        context_pressure: ManifestContextPressure {
            support: SupportState::Unavailable,
            evidence: None,
        },
        receipts: ManifestReceipts {
            native: false,
            lifeloop_synthesized: true,
            receipt_ledger: SupportState::Unavailable,
        },
        session_identity: None,
        session_rename: None,
        renewal: None,
        approval_surface: None,
        failure_modes: Vec::new(),
        telemetry_sources: Vec::new(),
        known_degradations: Vec::new(),
    }
}

/// Fixture-style fake registry. Holds exactly one manifest. Returns
/// matched / unknown-id / version-mismatch in a way that the router's
/// resolution branch can be exercised without the built-in registry.
struct FakeRegistry {
    manifest: AdapterManifest,
}

impl FakeRegistry {
    fn new() -> Self {
        Self {
            manifest: fake_manifest(),
        }
    }
}

impl AdapterRegistry for FakeRegistry {
    fn resolve(
        &self,
        adapter_id: &str,
        adapter_version: &str,
    ) -> lifeloop::router::AdapterResolution {
        use lifeloop::router::AdapterResolution;
        if adapter_id != self.manifest.adapter_id {
            return AdapterResolution::UnknownId;
        }
        if adapter_version != self.manifest.adapter_version {
            return AdapterResolution::VersionMismatch {
                registered_version: self.manifest.adapter_version.clone(),
            };
        }
        AdapterResolution::Found(RegisteredAdapter {
            manifest: self.manifest.clone(),
            conformance: ConformanceLevel::PreConformance,
        })
    }
}

// ---------------------------------------------------------------------------
// Request builder
// ---------------------------------------------------------------------------

fn valid_request() -> CallbackRequest {
    CallbackRequest {
        schema_version: SCHEMA_VERSION.to_string(),
        event: LifecycleEventKind::SessionStarted,
        event_id: "evt-1".into(),
        adapter_id: FAKE_ADAPTER_ID.into(),
        adapter_version: FAKE_ADAPTER_VERSION.into(),
        integration_mode: IntegrationMode::NativeHook,
        invocation_id: "inv-1".into(),
        harness_session_id: Some("sess-1".into()),
        harness_run_id: None,
        harness_task_id: None,
        frame_context: None,
        capability_snapshot_ref: None,
        payload_refs: Vec::new(),
        sequence: None,
        idempotency_key: None,
        metadata: serde_json::Map::new(),
    }
}

// ---------------------------------------------------------------------------
// Happy path
// ---------------------------------------------------------------------------

#[test]
fn route_produces_plan_for_valid_request_against_fake_registry() {
    let reg = FakeRegistry::new();
    let req = valid_request();
    let plan: RoutingPlan = route(&req, &reg).expect("route");
    assert_eq!(plan.event, LifecycleEventKind::SessionStarted);
    assert_eq!(plan.event_id, "evt-1");
    assert_eq!(plan.invocation_id, "inv-1");
    assert_eq!(plan.adapter.adapter_id, FAKE_ADAPTER_ID);
    assert_eq!(plan.adapter.adapter_version, FAKE_ADAPTER_VERSION);
    assert_eq!(plan.integration_mode, IntegrationMode::NativeHook);
}

#[test]
fn route_preserves_opaque_payload_refs_unchanged() {
    let reg = FakeRegistry::new();
    let mut req = valid_request();
    let refs = vec![
        PayloadRef {
            payload_id: "p-1".into(),
            payload_kind: "ccd.frame_card".into(),
            content_digest: Some("sha256:abc".into()),
            byte_size: Some(42),
        },
        PayloadRef {
            payload_id: "p-2".into(),
            payload_kind: "client.opaque".into(),
            content_digest: None,
            byte_size: None,
        },
    ];
    req.payload_refs = refs.clone();
    let plan = route(&req, &reg).expect("route");
    assert_eq!(plan.payload_refs, refs);
}

#[test]
fn route_against_builtin_registry_resolves_codex() {
    let req = CallbackRequest {
        adapter_id: "codex".into(),
        adapter_version: "0.1.0".into(),
        ..valid_request()
    };
    let plan = route(&req, &BuiltinAdapterRegistry).expect("route");
    assert_eq!(plan.adapter.adapter_id, "codex");
}

// ---------------------------------------------------------------------------
// Validation failure: schema version mismatch
// ---------------------------------------------------------------------------

#[test]
fn route_rejects_schema_version_mismatch() {
    let reg = FakeRegistry::new();
    let mut req = valid_request();
    req.schema_version = "lifeloop.v0.0".into();
    let err = route(&req, &reg).unwrap_err();
    assert!(
        matches!(err, RouteError::SchemaVersionMismatch { .. }),
        "got {err:?}"
    );
}

// ---------------------------------------------------------------------------
// Validation failure: empty sentinel strings
// ---------------------------------------------------------------------------

#[test]
fn route_rejects_empty_event_id() {
    let reg = FakeRegistry::new();
    let mut req = valid_request();
    req.event_id = String::new();
    match route(&req, &reg).unwrap_err() {
        RouteError::EmptySentinel { field } => assert_eq!(field, "request.event_id"),
        other => panic!("expected EmptySentinel, got {other:?}"),
    }
}

#[test]
fn route_rejects_empty_adapter_id() {
    let reg = FakeRegistry::new();
    let mut req = valid_request();
    req.adapter_id = String::new();
    assert!(matches!(
        route(&req, &reg).unwrap_err(),
        RouteError::EmptySentinel {
            field: "request.adapter_id"
        }
    ));
}

#[test]
fn route_rejects_empty_adapter_version() {
    let reg = FakeRegistry::new();
    let mut req = valid_request();
    req.adapter_version = String::new();
    assert!(matches!(
        route(&req, &reg).unwrap_err(),
        RouteError::EmptySentinel {
            field: "request.adapter_version"
        }
    ));
}

#[test]
fn route_rejects_empty_invocation_id() {
    let reg = FakeRegistry::new();
    let mut req = valid_request();
    req.invocation_id = String::new();
    assert!(matches!(
        route(&req, &reg).unwrap_err(),
        RouteError::EmptySentinel {
            field: "request.invocation_id"
        }
    ));
}

#[test]
fn route_rejects_empty_optional_when_present() {
    let reg = FakeRegistry::new();
    let mut req = valid_request();
    req.harness_session_id = Some(String::new());
    assert!(matches!(
        route(&req, &reg).unwrap_err(),
        RouteError::EmptySentinel {
            field: "request.harness_session_id"
        }
    ));
}

// ---------------------------------------------------------------------------
// Validation failure: unknown enum wire names
// ---------------------------------------------------------------------------

#[test]
fn unknown_event_wire_name_is_rejected_at_deserialize_boundary() {
    // serde rejects unknown enum wire names at deserialize time. The
    // router's UnknownEventName variant exists for non-serde call
    // sites; here we pin the deserialize-side behavior so callers
    // know the boundary holds.
    let raw = serde_json::json!({
        "schema_version": SCHEMA_VERSION,
        "event": "session.bogus",
        "event_id": "e",
        "adapter_id": FAKE_ADAPTER_ID,
        "adapter_version": FAKE_ADAPTER_VERSION,
        "integration_mode": "native_hook",
        "invocation_id": "i",
    });
    let r: Result<CallbackRequest, _> = serde_json::from_value(raw);
    assert!(
        r.is_err(),
        "deserialize should reject unknown event wire name"
    );
}

#[test]
fn unknown_integration_mode_wire_name_is_rejected_at_deserialize_boundary() {
    let raw = serde_json::json!({
        "schema_version": SCHEMA_VERSION,
        "event": "session.started",
        "event_id": "e",
        "adapter_id": FAKE_ADAPTER_ID,
        "adapter_version": FAKE_ADAPTER_VERSION,
        "integration_mode": "warp_drive",
        "invocation_id": "i",
    });
    let r: Result<CallbackRequest, _> = serde_json::from_value(raw);
    assert!(
        r.is_err(),
        "deserialize should reject unknown integration_mode wire name"
    );
}

#[test]
fn route_error_unknown_event_name_variant_constructs_and_displays() {
    // Sanity-check the typed variant exists and renders for the
    // non-serde call sites described in its docstring.
    let err = RouteError::UnknownEventName {
        received: "session.bogus".into(),
    };
    let s = format!("{err}");
    assert!(s.contains("session.bogus"), "{s}");
}

// ---------------------------------------------------------------------------
// Validation failure: partial frame context
// ---------------------------------------------------------------------------

#[test]
fn route_rejects_subcall_without_parent_frame_id() {
    let reg = FakeRegistry::new();
    let mut req = valid_request();
    req.event = LifecycleEventKind::FrameOpened;
    req.frame_context = Some(FrameContext {
        frame_id: "f-1".into(),
        parent_frame_id: None,
        frame_class: FrameClass::Subcall,
    });
    assert!(matches!(
        route(&req, &reg).unwrap_err(),
        RouteError::InvalidFrameContext { .. }
    ));
}

#[test]
fn route_rejects_top_level_with_parent_frame_id() {
    let reg = FakeRegistry::new();
    let mut req = valid_request();
    req.event = LifecycleEventKind::FrameOpened;
    req.frame_context = Some(FrameContext {
        frame_id: "f-1".into(),
        parent_frame_id: Some("f-0".into()),
        frame_class: FrameClass::TopLevel,
    });
    assert!(matches!(
        route(&req, &reg).unwrap_err(),
        RouteError::InvalidFrameContext { .. }
    ));
}

#[test]
fn route_rejects_empty_frame_id() {
    let reg = FakeRegistry::new();
    let mut req = valid_request();
    req.event = LifecycleEventKind::FrameOpened;
    req.frame_context = Some(FrameContext {
        frame_id: String::new(),
        parent_frame_id: None,
        frame_class: FrameClass::TopLevel,
    });
    assert!(matches!(
        route(&req, &reg).unwrap_err(),
        RouteError::InvalidFrameContext { .. }
    ));
}

#[test]
fn route_rejects_frame_event_missing_frame_context() {
    let reg = FakeRegistry::new();
    let mut req = valid_request();
    req.event = LifecycleEventKind::FrameOpened;
    req.frame_context = None;
    assert!(matches!(
        route(&req, &reg).unwrap_err(),
        RouteError::InvalidFrameContext { .. }
    ));
}

// ---------------------------------------------------------------------------
// Validation failure: invalid event envelope
// ---------------------------------------------------------------------------

#[test]
fn route_rejects_receipt_emitted_with_idempotency_key() {
    let reg = FakeRegistry::new();
    let mut req = valid_request();
    req.event = LifecycleEventKind::ReceiptEmitted;
    req.idempotency_key = Some("idem-1".into());
    assert!(matches!(
        route(&req, &reg).unwrap_err(),
        RouteError::InvalidEventEnvelope { .. }
    ));
}

// ---------------------------------------------------------------------------
// Validation failure: invalid payload ref
// ---------------------------------------------------------------------------

#[test]
fn route_rejects_payload_ref_with_empty_payload_id() {
    let reg = FakeRegistry::new();
    let mut req = valid_request();
    req.payload_refs = vec![PayloadRef {
        payload_id: String::new(),
        payload_kind: "k".into(),
        content_digest: None,
        byte_size: None,
    }];
    match route(&req, &reg).unwrap_err() {
        RouteError::InvalidPayloadRef { index, .. } => assert_eq!(index, 0),
        other => panic!("expected InvalidPayloadRef, got {other:?}"),
    }
}

#[test]
fn route_rejects_payload_ref_with_empty_payload_kind_at_correct_index() {
    let reg = FakeRegistry::new();
    let mut req = valid_request();
    req.payload_refs = vec![
        PayloadRef {
            payload_id: "ok".into(),
            payload_kind: "k".into(),
            content_digest: None,
            byte_size: None,
        },
        PayloadRef {
            payload_id: "p2".into(),
            payload_kind: String::new(),
            content_digest: None,
            byte_size: None,
        },
    ];
    match route(&req, &reg).unwrap_err() {
        RouteError::InvalidPayloadRef { index, .. } => assert_eq!(index, 1),
        other => panic!("expected InvalidPayloadRef, got {other:?}"),
    }
}

// ---------------------------------------------------------------------------
// Adapter resolution: id vs version distinction
// ---------------------------------------------------------------------------

#[test]
fn route_rejects_unknown_adapter_id() {
    let reg = FakeRegistry::new();
    let mut req = valid_request();
    req.adapter_id = "ghost".into();
    match route(&req, &reg).unwrap_err() {
        RouteError::AdapterIdNotFound { adapter_id } => assert_eq!(adapter_id, "ghost"),
        other => panic!("expected AdapterIdNotFound, got {other:?}"),
    }
}

#[test]
fn route_distinguishes_adapter_version_mismatch_from_unknown_id() {
    let reg = FakeRegistry::new();
    let mut req = valid_request();
    // id matches the fake registry, version does not.
    req.adapter_version = FAKE_ADAPTER_OTHER_VERSION.into();
    match route(&req, &reg).unwrap_err() {
        RouteError::AdapterVersionMismatch {
            adapter_id,
            requested,
            registered,
        } => {
            assert_eq!(adapter_id, FAKE_ADAPTER_ID);
            assert_eq!(requested, FAKE_ADAPTER_OTHER_VERSION);
            assert_eq!(registered, FAKE_ADAPTER_VERSION);
        }
        other => panic!("expected AdapterVersionMismatch, got {other:?}"),
    }
}
