//! End-to-end tests for lens resolution behavior.
//!
//! These tests verify:
//! - Queries without a lens return all candidate assertions
//! - Recency lens picks the most recent assertion based on timestamp
//! - Empty queries return proper empty results

#![allow(clippy::expect_used)]

mod common;

use axum::{
    body::Body,
    http::{Request, StatusCode},
};
use ed25519_dalek::{Signer, SigningKey};
use rand::rngs::OsRng;
use serde_json::json;
use std::sync::Arc;
use tokio::sync::{Mutex, Notify};
use tower::ServiceExt;

use stemedb_api::{create_router, AppState};
use stemedb_ingest::worker::IngestWorker;
use stemedb_storage::HybridStore;
use stemedb_wal::Journal;

// Test configuration constants
const INGEST_ITERATIONS: usize = 10;

/// Test environment that keeps temp directories alive.
struct TestEnvironment {
    _temp_dir: tempfile::TempDir,
    state: AppState,
    store: Arc<HybridStore>,
    journal: Arc<Mutex<Journal>>,
}

/// Helper to create a test environment with temporary directories.
async fn create_test_environment() -> TestEnvironment {
    let temp_dir = tempfile::tempdir().expect("Failed to create temp dir");
    let wal_dir = temp_dir.path().join("wal");
    let db_dir = temp_dir.path().join("db");

    std::fs::create_dir_all(&wal_dir).expect("Failed to create WAL dir");
    std::fs::create_dir_all(&db_dir).expect("Failed to create DB dir");

    let store = HybridStore::open(&db_dir).expect("Failed to open store");
    let store_arc = Arc::new(store);

    // Open journals: one for IngestWorker reads, one for AppState (write + read)
    let journal_arc =
        Arc::new(Mutex::new(Journal::open(&wal_dir).expect("Failed to open journal for ingest")));
    let write_journal = Journal::open(&wal_dir).expect("Failed to open write journal");
    let read_journal = Journal::open(&wal_dir).expect("Failed to open read journal");
    let state = AppState::new(write_journal, read_journal, Arc::clone(&store_arc));

    TestEnvironment { _temp_dir: temp_dir, state, store: store_arc, journal: journal_arc }
}

/// Sign a message using Ed25519 and return the signature + public key.
fn sign_message(message: &str) -> ([u8; 32], [u8; 64]) {
    let mut csprng = OsRng;
    let signing_key = SigningKey::generate(&mut csprng);
    let verifying_key = signing_key.verifying_key();

    let signature = signing_key.sign(message.as_bytes());

    (verifying_key.to_bytes(), signature.to_bytes())
}

#[tokio::test]
async fn test_e2e_query_with_no_lens_returns_all_candidates() {
    // ========================================================================
    // Setup
    // ========================================================================

    let env = create_test_environment().await;
    let state = env.state.clone();
    let store = Arc::clone(&env.store);
    let journal = Arc::clone(&env.journal);
    let notify = Arc::new(Notify::new());

    let ingest_notify = Arc::clone(&notify);
    let mut ingest_worker = IngestWorker::new(Arc::clone(&journal), Arc::clone(&store))
        .await
        .expect("Failed to create IngestWorker")
        .with_notify(ingest_notify);

    // ========================================================================
    // Create two competing assertions for the same subject+predicate
    // ========================================================================

    let subject = "Apple_Inc";
    let predicate = "has_revenue";

    // Assertion 1: revenue = 380.0
    let message1 = format!("{}:{}", subject, predicate);
    let (agent_id1, signature1) = sign_message(&message1);
    let timestamp1 = 1000;

    let assertion1 = json!({
        "subject": subject,
        "predicate": predicate,
        "object": {"type": "Number", "value": 380.0},
        "confidence": 0.9,
        "signatures": [{
            "agent_id": hex::encode(agent_id1),
            "signature": hex::encode(signature1),
            "timestamp": timestamp1
        }],
        "source_hash": hex::encode([1u8; 32])
    });

    // Assertion 2: revenue = 385.0 (newer timestamp)
    let message2 = format!("{}:{}", subject, predicate);
    let (agent_id2, signature2) = sign_message(&message2);
    let timestamp2 = 2000;

    let assertion2 = json!({
        "subject": subject,
        "predicate": predicate,
        "object": {"type": "Number", "value": 385.0},
        "confidence": 0.85,
        "signatures": [{
            "agent_id": hex::encode(agent_id2),
            "signature": hex::encode(signature2),
            "timestamp": timestamp2
        }],
        "source_hash": hex::encode([2u8; 32])
    });

    let app = create_router(state.clone());

    // Create both assertions
    for assertion in [&assertion1, &assertion2] {
        let request = Request::builder()
            .uri("/v1/assert")
            .method("POST")
            .header("content-type", "application/json")
            .body(Body::from(serde_json::to_vec(assertion).expect("JSON serialization")))
            .expect("Failed to build request");

        let response = app.clone().oneshot(request).await.expect("Request failed");
        assert_eq!(response.status(), StatusCode::CREATED);
    }

    // Process ingestion
    for _ in 0..INGEST_ITERATIONS {
        if ingest_worker.step().await.expect("Ingestion failed") == 0 {
            break;
        }
    }

    // ========================================================================
    // Query without a lens - should return both assertions
    // ========================================================================

    let query_request = Request::builder()
        .uri(format!("/v1/query?subject={}&predicate={}", subject, predicate))
        .method("GET")
        .body(Body::empty())
        .expect("Failed to build query request");

    let query_response = app.oneshot(query_request).await.expect("Query request failed");

    assert_eq!(query_response.status(), StatusCode::OK);

    let query_body = axum::body::to_bytes(query_response.into_body(), usize::MAX)
        .await
        .expect("Failed to read query response body");
    let query_json: serde_json::Value =
        serde_json::from_slice(&query_body).expect("Failed to parse query JSON");

    let assertions = query_json["assertions"].as_array().expect("Missing assertions array");

    assert_eq!(assertions.len(), 2, "Without a lens, should return all candidate assertions");

    // Verify both values are present
    let values: Vec<f64> =
        assertions.iter().map(|a| a["object"]["value"].as_f64().expect("Missing value")).collect();

    assert!(values.contains(&380.0), "Should contain first assertion value");
    assert!(values.contains(&385.0), "Should contain second assertion value");

    assert_eq!(query_json["total_count"], 2);
}

#[tokio::test]
async fn test_e2e_lens_resolution_picks_most_recent() {
    // ========================================================================
    // Setup
    // ========================================================================

    let env = create_test_environment().await;
    let state = env.state.clone();
    let store = Arc::clone(&env.store);
    let journal = Arc::clone(&env.journal);
    let notify = Arc::new(Notify::new());

    let ingest_notify = Arc::clone(&notify);
    let mut ingest_worker = IngestWorker::new(Arc::clone(&journal), Arc::clone(&store))
        .await
        .expect("Failed to create IngestWorker")
        .with_notify(ingest_notify);

    // ========================================================================
    // Create two assertions with different timestamps
    // ========================================================================

    let subject = "Microsoft_Corp";
    let predicate = "has_ceo";

    // Older assertion: Satya (timestamp 1000)
    let message1 = format!("{}:{}", subject, predicate);
    let (agent_id1, signature1) = sign_message(&message1);

    let old_assertion = json!({
        "subject": subject,
        "predicate": predicate,
        "object": {"type": "Text", "value": "Satya_Nadella"},
        "confidence": 0.9,
        "signatures": [{
            "agent_id": hex::encode(agent_id1),
            "signature": hex::encode(signature1),
            "timestamp": 1000
        }],
        "source_hash": hex::encode([1u8; 32])
    });

    // Newer assertion: Bill (timestamp 5000) - hypothetically outdated info
    let message2 = format!("{}:{}", subject, predicate);
    let (agent_id2, signature2) = sign_message(&message2);

    let new_assertion = json!({
        "subject": subject,
        "predicate": predicate,
        "object": {"type": "Text", "value": "Bill_Gates"},
        "confidence": 0.8,
        "signatures": [{
            "agent_id": hex::encode(agent_id2),
            "signature": hex::encode(signature2),
            "timestamp": 5000
        }],
        "source_hash": hex::encode([2u8; 32])
    });

    let app = create_router(state.clone());

    // Create both assertions
    for assertion in [&old_assertion, &new_assertion] {
        let request = Request::builder()
            .uri("/v1/assert")
            .method("POST")
            .header("content-type", "application/json")
            .body(Body::from(serde_json::to_vec(assertion).expect("JSON serialization")))
            .expect("Failed to build request");

        let response = app.clone().oneshot(request).await.expect("Request failed");
        assert_eq!(response.status(), StatusCode::CREATED);
    }

    // Process ingestion
    for _ in 0..INGEST_ITERATIONS {
        if ingest_worker.step().await.expect("Ingestion failed") == 0 {
            break;
        }
    }

    // ========================================================================
    // Query with Recency lens - should pick the newer one
    // ========================================================================

    let query_request = Request::builder()
        .uri(format!("/v1/query?subject={}&predicate={}&lens=Recency", subject, predicate))
        .method("GET")
        .body(Body::empty())
        .expect("Failed to build query request");

    let query_response = app.oneshot(query_request).await.expect("Query request failed");

    assert_eq!(query_response.status(), StatusCode::OK);

    let query_body = axum::body::to_bytes(query_response.into_body(), usize::MAX)
        .await
        .expect("Failed to read query response body");
    let query_json: serde_json::Value =
        serde_json::from_slice(&query_body).expect("Failed to parse query JSON");

    let assertions = query_json["assertions"].as_array().expect("Missing assertions array");

    assert_eq!(assertions.len(), 1, "Recency lens should return exactly one assertion");

    // Verify it picked the newer one (Bill_Gates at timestamp 5000)
    let winner = &assertions[0];
    assert_eq!(
        winner["object"]["value"].as_str().expect("Missing value"),
        "Bill_Gates",
        "Recency lens should pick the most recent assertion"
    );
    assert_eq!(winner["signatures"][0]["timestamp"].as_u64().expect("Missing timestamp"), 5000);
}

#[tokio::test]
async fn test_e2e_empty_query_returns_no_results() {
    // ========================================================================
    // Setup with no data
    // ========================================================================

    let env = create_test_environment().await;
    let app = create_router(env.state);

    // ========================================================================
    // Query for non-existent subject
    // ========================================================================

    let query_request = Request::builder()
        .uri("/v1/query?subject=Nonexistent_Entity&predicate=some_property")
        .method("GET")
        .body(Body::empty())
        .expect("Failed to build query request");

    let query_response = app.oneshot(query_request).await.expect("Query request failed");

    assert_eq!(query_response.status(), StatusCode::OK);

    let query_body = axum::body::to_bytes(query_response.into_body(), usize::MAX)
        .await
        .expect("Failed to read query response body");
    let query_json: serde_json::Value =
        serde_json::from_slice(&query_body).expect("Failed to parse query JSON");

    assert_eq!(query_json["assertions"], json!([]));
    assert_eq!(query_json["total_count"], 0);
    assert_eq!(query_json["has_more"], false);
}
