//! Integration smoke tests: OpsRouterBuilder with all stores + auth.

use async_trait::async_trait;
use axum::{
    body::Body,
    http::{Request, StatusCode},
};
use bytes::Bytes;
use chrono::Utc;
use klieo_a2a::task_store::{A2aTaskStore, DEFAULT_BUCKET};
use klieo_auth_common::{AuthError, Authenticator, Headers, Identity};
use klieo_bus_memory::MemoryBus;
use klieo_core::{
    ids::RunId,
    resume::{KvResumeBuffer, ResumeBuffer},
};
use klieo_ops_api::OpsRouterBuilder;
use klieo_runlog::{
    types::{RunStatus, Usage},
    InMemoryRunLogStore, RunLog, RunLogStore,
};
use std::collections::HashSet;
use std::sync::Arc;
use tower::ServiceExt;

struct ScopedOpsAuth;

#[async_trait]
impl Authenticator for ScopedOpsAuth {
    async fn authenticate(&self, _: &dyn Headers, _: &[u8]) -> Result<Identity, AuthError> {
        Ok(Identity::with_scopes(
            "test-operator",
            HashSet::from(["klieo.ops.read".to_string()]),
        ))
    }
}

fn make_run(agent: &str) -> RunLog {
    RunLog {
        run_id: RunId::new(),
        agent: agent.to_string(),
        started_at: Utc::now(),
        finished_at: None,
        status: RunStatus::Completed,
        steps: vec![],
        tokens: Usage::default(),
        cost_estimate: None,
    }
}

#[tokio::test]
async fn smoke_all_stores_wired() {
    let run_store = Arc::new(InMemoryRunLogStore::new());
    run_store.put(&make_run("smoke-agent")).await.unwrap();

    let bus = MemoryBus::new();
    let task_store = Arc::new(A2aTaskStore::new(bus.kv.clone(), DEFAULT_BUCKET.into()));
    let resume_buf = Arc::new(KvResumeBuffer::new(bus.kv.clone()));
    resume_buf
        .record("stream-smoke", 1, Bytes::from_static(b"e"))
        .await
        .unwrap();

    let app = OpsRouterBuilder::new()
        .with_authenticator(Arc::new(ScopedOpsAuth))
        .with_run_log_store(Arc::clone(&run_store) as Arc<dyn RunLogStore>)
        .with_task_store(task_store)
        .with_resume_buffer(resume_buf)
        .build()
        .expect("build failed");

    // GET /runs → 200, one entry
    let resp = app
        .clone()
        .oneshot(Request::builder().uri("/runs").body(Body::empty()).unwrap())
        .await
        .unwrap();
    assert_eq!(resp.status(), StatusCode::OK);
    let body = axum::body::to_bytes(resp.into_body(), usize::MAX)
        .await
        .unwrap();
    let runs: Vec<serde_json::Value> = serde_json::from_slice(&body).unwrap();
    assert_eq!(runs.len(), 1);
    assert_eq!(runs[0]["agent"], "smoke-agent");

    // GET /agents → 200, one entry
    let resp = app
        .clone()
        .oneshot(
            Request::builder()
                .uri("/agents")
                .body(Body::empty())
                .unwrap(),
        )
        .await
        .unwrap();
    assert_eq!(resp.status(), StatusCode::OK);
    let body = axum::body::to_bytes(resp.into_body(), usize::MAX)
        .await
        .unwrap();
    let agents: Vec<serde_json::Value> = serde_json::from_slice(&body).unwrap();
    assert_eq!(agents.len(), 1);
    assert_eq!(agents[0]["agent"], "smoke-agent");

    // GET /tasks?context_id=ctx-smoke → 200, empty list (no tasks seeded)
    let resp = app
        .clone()
        .oneshot(
            Request::builder()
                .uri("/tasks?context_id=ctx-smoke")
                .body(Body::empty())
                .unwrap(),
        )
        .await
        .unwrap();
    assert_eq!(resp.status(), StatusCode::OK);

    // GET /streams → 200, one stream entry
    let resp = app
        .clone()
        .oneshot(
            Request::builder()
                .uri("/streams")
                .body(Body::empty())
                .unwrap(),
        )
        .await
        .unwrap();
    assert_eq!(resp.status(), StatusCode::OK);
    let body = axum::body::to_bytes(resp.into_body(), usize::MAX)
        .await
        .unwrap();
    let streams: Vec<serde_json::Value> = serde_json::from_slice(&body).unwrap();
    assert_eq!(streams.len(), 1);
    assert_eq!(streams[0]["stream_id"], "stream-smoke");
}

#[tokio::test]
async fn smoke_no_stores_all_endpoints_return_501() {
    let app = OpsRouterBuilder::new()
        .with_authenticator(Arc::new(ScopedOpsAuth))
        .build()
        .expect("build failed");

    for uri in &["/runs", "/agents", "/streams"] {
        let resp = app
            .clone()
            .oneshot(Request::builder().uri(*uri).body(Body::empty()).unwrap())
            .await
            .unwrap();
        assert_eq!(
            resp.status(),
            StatusCode::NOT_IMPLEMENTED,
            "expected 501 for {uri}"
        );
    }
    let resp = app
        .clone()
        .oneshot(
            Request::builder()
                .uri("/tasks?context_id=x")
                .body(Body::empty())
                .unwrap(),
        )
        .await
        .unwrap();
    assert_eq!(resp.status(), StatusCode::NOT_IMPLEMENTED);
}

#[test]
fn build_without_authenticator_returns_err() {
    let result = OpsRouterBuilder::new().build();
    assert!(
        matches!(
            result,
            Err(klieo_ops_api::OpsBuilderError::MissingAuthenticator)
        ),
        "expected MissingAuthenticator, got: {:?}",
        result
    );
}

#[tokio::test]
async fn smoke_forbidden_when_scope_missing() {
    struct NoScopeAuth;
    #[async_trait]
    impl Authenticator for NoScopeAuth {
        async fn authenticate(&self, _: &dyn Headers, _: &[u8]) -> Result<Identity, AuthError> {
            // Authenticated but carries no ops scope.
            Ok(Identity::new("user-without-ops-scope"))
        }
    }

    let app = OpsRouterBuilder::new()
        .with_authenticator(Arc::new(NoScopeAuth))
        .build()
        .expect("build failed");

    let resp = app
        .oneshot(Request::builder().uri("/runs").body(Body::empty()).unwrap())
        .await
        .unwrap();
    assert_eq!(resp.status(), StatusCode::FORBIDDEN);

    let body = axum::body::to_bytes(resp.into_body(), usize::MAX)
        .await
        .unwrap();
    let json: serde_json::Value = serde_json::from_slice(&body).unwrap();
    assert_eq!(json["error"], "forbidden");
}

#[tokio::test]
async fn smoke_auth_rejects_when_authenticator_denies() {
    use async_trait::async_trait;
    use klieo_auth_common::{AuthError, Authenticator, Headers, Identity};

    struct AlwaysDeny;
    #[async_trait]
    impl Authenticator for AlwaysDeny {
        async fn authenticate(&self, _h: &dyn Headers, _p: &[u8]) -> Result<Identity, AuthError> {
            Err(AuthError::Rejected("denied".into()))
        }
    }

    let app = OpsRouterBuilder::new()
        .with_authenticator(Arc::new(AlwaysDeny))
        .build()
        .expect("build failed");

    let resp = app
        .oneshot(Request::builder().uri("/runs").body(Body::empty()).unwrap())
        .await
        .unwrap();
    assert_eq!(resp.status(), StatusCode::UNAUTHORIZED);
}
