//! Input validation tests.
//!
//! Tests for confidence bounds, vote weight validation, subject/predicate
//! size limits, and timestamp validation.

use super::*;
use crate::error::IngestError;

/// Test: Assertions with confidence > 1.0 are rejected.
#[tokio::test]
async fn test_rejects_high_confidence() {
    let dir = tempdir().expect("Failed to create temp dir");
    let wal_dir = dir.path().join("wal");
    let db_dir = dir.path().join("db");

    let mut csprng = OsRng;
    let signing_key = SigningKey::generate(&mut csprng);
    let verifying_key = signing_key.verifying_key();
    let message = "Test:high";
    let signature = signing_key.sign(message.as_bytes());

    let assertion = Assertion {
        subject: "Test".to_string(),
        predicate: "high".to_string(),
        object: ObjectValue::Text("test".to_string()),
        parent_hash: None,
        source_hash: [0u8; 32],
        source_class: SourceClass::Expert,
        visual_hash: None,
        epoch: None,
        source_metadata: None,
        lifecycle: LifecycleStage::Proposed,
        signatures: vec![SignatureEntry {
            version: 1,
            agent_id: verifying_key.to_bytes(),
            signature: signature.to_bytes(),
            timestamp: 1000,
        }],
        confidence: 1.5, // Invalid: > 1.0
        timestamp: 1000,
        hlc_timestamp: HlcTimestamp::default(),
        vector: None,
    };

    let mut journal = Journal::open(&wal_dir).expect("Failed to open journal");
    let store = HybridStore::open(&db_dir).expect("Failed to open store");

    journal.append(serialize_assertion(&assertion).expect("ser")).expect("append");

    let journal = Arc::new(Mutex::new(journal));
    let store = Arc::new(store);
    let mut worker =
        IngestWorker::new(journal, store.clone()).await.expect("Failed to create worker");

    let result = worker.step().await;
    assert!(result.is_err(), "Should reject confidence > 1.0");

    let err = result.unwrap_err();
    assert!(
        matches!(err, IngestError::InputValidation(_)),
        "Error should be InputValidation, got: {:?}",
        err
    );
    assert!(err.to_string().contains("confidence"));
}
/// Test: Assertions with negative confidence are rejected.
#[tokio::test]
async fn test_rejects_negative_confidence() {
    let dir = tempdir().expect("Failed to create temp dir");
    let wal_dir = dir.path().join("wal");
    let db_dir = dir.path().join("db");

    let mut csprng = OsRng;
    let signing_key = SigningKey::generate(&mut csprng);
    let verifying_key = signing_key.verifying_key();
    let message = "Test:negative";
    let signature = signing_key.sign(message.as_bytes());

    let assertion = Assertion {
        subject: "Test".to_string(),
        predicate: "negative".to_string(),
        object: ObjectValue::Text("test".to_string()),
        parent_hash: None,
        source_hash: [0u8; 32],
        source_class: SourceClass::Expert,
        visual_hash: None,
        epoch: None,
        source_metadata: None,
        lifecycle: LifecycleStage::Proposed,
        signatures: vec![SignatureEntry {
            version: 1,
            agent_id: verifying_key.to_bytes(),
            signature: signature.to_bytes(),
            timestamp: 1000,
        }],
        confidence: -0.5, // Invalid: < 0.0
        timestamp: 1000,
        hlc_timestamp: HlcTimestamp::default(),
        vector: None,
    };

    let mut journal = Journal::open(&wal_dir).expect("Failed to open journal");
    let store = HybridStore::open(&db_dir).expect("Failed to open store");

    journal.append(serialize_assertion(&assertion).expect("ser")).expect("append");

    let journal = Arc::new(Mutex::new(journal));
    let store = Arc::new(store);
    let mut worker =
        IngestWorker::new(journal, store.clone()).await.expect("Failed to create worker");

    let result = worker.step().await;
    assert!(result.is_err(), "Should reject confidence < 0.0");

    let err = result.unwrap_err();
    assert!(matches!(err, IngestError::InputValidation(_)));
}
/// Test: Votes with out-of-range weight are rejected.
#[tokio::test]
async fn test_rejects_invalid_vote_weight() {
    let dir = tempdir().expect("Failed to create temp dir");
    let wal_dir = dir.path().join("wal");
    let db_dir = dir.path().join("db");

    // Create vote with weight > 1.0
    let vote = Vote {
        assertion_hash: [1u8; 32],
        agent_id: [2u8; 32],
        weight: 1.5, // Invalid: > 1.0
        signature: [3u8; 64],
        timestamp: 1000,
        source_url: None,
        observed_context: None,
    };

    let mut journal = Journal::open(&wal_dir).expect("Failed to open journal");
    let store = HybridStore::open(&db_dir).expect("Failed to open store");

    journal.append(serialize_vote(&vote).expect("ser")).expect("append");

    let journal = Arc::new(Mutex::new(journal));
    let store = Arc::new(store);
    let mut worker =
        IngestWorker::new(journal, store.clone()).await.expect("Failed to create worker");

    let result = worker.step().await;
    assert!(result.is_err(), "Should reject vote weight > 1.0");

    let err = result.unwrap_err();
    assert!(
        matches!(err, IngestError::InputValidation(_)),
        "Error should be InputValidation, got: {:?}",
        err
    );
    assert!(err.to_string().contains("weight"));
}
/// Test: Votes with negative weight are rejected.
#[tokio::test]
async fn test_rejects_negative_vote_weight() {
    let dir = tempdir().expect("Failed to create temp dir");
    let wal_dir = dir.path().join("wal");
    let db_dir = dir.path().join("db");

    let vote = Vote {
        assertion_hash: [1u8; 32],
        agent_id: [2u8; 32],
        weight: -0.5, // Invalid: < 0.0
        signature: [3u8; 64],
        timestamp: 1000,
        source_url: None,
        observed_context: None,
    };

    let mut journal = Journal::open(&wal_dir).expect("Failed to open journal");
    let store = HybridStore::open(&db_dir).expect("Failed to open store");

    journal.append(serialize_vote(&vote).expect("ser")).expect("append");

    let journal = Arc::new(Mutex::new(journal));
    let store = Arc::new(store);
    let mut worker =
        IngestWorker::new(journal, store.clone()).await.expect("Failed to create worker");

    let result = worker.step().await;
    assert!(result.is_err(), "Should reject vote weight < 0.0");

    let err = result.unwrap_err();
    assert!(matches!(err, IngestError::InputValidation(_)));
}
/// Test: Assertions with oversized subject are rejected.
#[tokio::test]
async fn test_rejects_oversized_subject() {
    let dir = tempdir().expect("Failed to create temp dir");
    let wal_dir = dir.path().join("wal");
    let db_dir = dir.path().join("db");

    // Create a subject that exceeds MAX_SUBJECT_LEN (1024 bytes)
    let oversized_subject = "x".repeat(1025);

    let mut csprng = OsRng;
    let signing_key = SigningKey::generate(&mut csprng);
    let verifying_key = signing_key.verifying_key();
    let message = format!("{}:pred", oversized_subject);
    let signature = signing_key.sign(message.as_bytes());

    let assertion = Assertion {
        subject: oversized_subject,
        predicate: "pred".to_string(),
        object: ObjectValue::Text("test".to_string()),
        parent_hash: None,
        source_hash: [0u8; 32],
        source_class: SourceClass::Expert,
        visual_hash: None,
        epoch: None,
        source_metadata: None,
        lifecycle: LifecycleStage::Proposed,
        signatures: vec![SignatureEntry {
            version: 1,
            agent_id: verifying_key.to_bytes(),
            signature: signature.to_bytes(),
            timestamp: 1000,
        }],
        confidence: 0.9,
        timestamp: 1000,
        hlc_timestamp: HlcTimestamp::default(),
        vector: None,
    };

    let mut journal = Journal::open(&wal_dir).expect("Failed to open journal");
    let store = HybridStore::open(&db_dir).expect("Failed to open store");

    journal.append(serialize_assertion(&assertion).expect("ser")).expect("append");

    let journal = Arc::new(Mutex::new(journal));
    let store = Arc::new(store);
    let mut worker =
        IngestWorker::new(journal, store.clone()).await.expect("Failed to create worker");

    let result = worker.step().await;
    assert!(result.is_err(), "Should reject oversized subject");

    let err = result.unwrap_err();
    assert!(
        matches!(err, IngestError::InputValidation(_)),
        "Error should be InputValidation, got: {:?}",
        err
    );
    assert!(err.to_string().contains("subject"));
}
/// Test: Assertions with oversized predicate are rejected.
#[tokio::test]
async fn test_rejects_oversized_predicate() {
    let dir = tempdir().expect("Failed to create temp dir");
    let wal_dir = dir.path().join("wal");
    let db_dir = dir.path().join("db");

    // Create a predicate that exceeds MAX_PREDICATE_LEN (256 bytes)
    let oversized_predicate = "y".repeat(257);

    let mut csprng = OsRng;
    let signing_key = SigningKey::generate(&mut csprng);
    let verifying_key = signing_key.verifying_key();
    let message = format!("subj:{}", oversized_predicate);
    let signature = signing_key.sign(message.as_bytes());

    let assertion = Assertion {
        subject: "subj".to_string(),
        predicate: oversized_predicate,
        object: ObjectValue::Text("test".to_string()),
        parent_hash: None,
        source_hash: [0u8; 32],
        source_class: SourceClass::Expert,
        visual_hash: None,
        epoch: None,
        source_metadata: None,
        lifecycle: LifecycleStage::Proposed,
        signatures: vec![SignatureEntry {
            version: 1,
            agent_id: verifying_key.to_bytes(),
            signature: signature.to_bytes(),
            timestamp: 1000,
        }],
        confidence: 0.9,
        timestamp: 1000,
        hlc_timestamp: HlcTimestamp::default(),
        vector: None,
    };

    let mut journal = Journal::open(&wal_dir).expect("Failed to open journal");
    let store = HybridStore::open(&db_dir).expect("Failed to open store");

    journal.append(serialize_assertion(&assertion).expect("ser")).expect("append");

    let journal = Arc::new(Mutex::new(journal));
    let store = Arc::new(store);
    let mut worker =
        IngestWorker::new(journal, store.clone()).await.expect("Failed to create worker");

    let result = worker.step().await;
    assert!(result.is_err(), "Should reject oversized predicate");

    let err = result.unwrap_err();
    assert!(
        matches!(err, IngestError::InputValidation(_)),
        "Error should be InputValidation, got: {:?}",
        err
    );
    assert!(err.to_string().contains("predicate"));
}
/// Test: Assertions with exactly MAX_SUBJECT_LEN bytes are accepted.
/// This boundary test verifies that the validation uses `>` not `>=`,
/// catching off-by-one errors in the comparison.
#[tokio::test]
async fn test_accepts_exact_max_subject_length() {
    let dir = tempdir().expect("Failed to create temp dir");
    let wal_dir = dir.path().join("wal");
    let db_dir = dir.path().join("db");

    // Create a subject with exactly MAX_SUBJECT_LEN (1024 bytes)
    let exact_max_subject = "x".repeat(1024);

    let mut csprng = OsRng;
    let signing_key = SigningKey::generate(&mut csprng);
    let verifying_key = signing_key.verifying_key();
    let message = format!("{}:pred", exact_max_subject);
    let signature = signing_key.sign(message.as_bytes());

    let assertion = Assertion {
        subject: exact_max_subject,
        predicate: "pred".to_string(),
        object: ObjectValue::Text("test".to_string()),
        parent_hash: None,
        source_hash: [0u8; 32],
        source_class: SourceClass::Expert,
        visual_hash: None,
        epoch: None,
        source_metadata: None,
        lifecycle: LifecycleStage::Proposed,
        signatures: vec![SignatureEntry {
            version: 1,
            agent_id: verifying_key.to_bytes(),
            signature: signature.to_bytes(),
            timestamp: 1000,
        }],
        confidence: 0.9,
        timestamp: 1000,
        hlc_timestamp: HlcTimestamp::default(),
        vector: None,
    };

    let mut journal = Journal::open(&wal_dir).expect("Failed to open journal");
    let store = HybridStore::open(&db_dir).expect("Failed to open store");

    journal.append(serialize_assertion(&assertion).expect("ser")).expect("append");

    let journal = Arc::new(Mutex::new(journal));
    let store = Arc::new(store);
    let mut worker =
        IngestWorker::new(journal, store.clone()).await.expect("Failed to create worker");

    let result = worker.step().await;
    assert!(
        result.is_ok(),
        "Should accept subject with exactly 1024 bytes, got error: {:?}",
        result
    );
}
/// Test: Assertions with exactly MAX_PREDICATE_LEN bytes are accepted.
/// This boundary test verifies that the validation uses `>` not `>=`,
/// catching off-by-one errors in the comparison.
#[tokio::test]
async fn test_accepts_exact_max_predicate_length() {
    let dir = tempdir().expect("Failed to create temp dir");
    let wal_dir = dir.path().join("wal");
    let db_dir = dir.path().join("db");

    // Create a predicate with exactly MAX_PREDICATE_LEN (256 bytes)
    let exact_max_predicate = "y".repeat(256);

    let mut csprng = OsRng;
    let signing_key = SigningKey::generate(&mut csprng);
    let verifying_key = signing_key.verifying_key();
    let message = format!("subj:{}", exact_max_predicate);
    let signature = signing_key.sign(message.as_bytes());

    let assertion = Assertion {
        subject: "subj".to_string(),
        predicate: exact_max_predicate,
        object: ObjectValue::Text("test".to_string()),
        parent_hash: None,
        source_hash: [0u8; 32],
        source_class: SourceClass::Expert,
        visual_hash: None,
        epoch: None,
        source_metadata: None,
        lifecycle: LifecycleStage::Proposed,
        signatures: vec![SignatureEntry {
            version: 1,
            agent_id: verifying_key.to_bytes(),
            signature: signature.to_bytes(),
            timestamp: 1000,
        }],
        confidence: 0.9,
        timestamp: 1000,
        hlc_timestamp: HlcTimestamp::default(),
        vector: None,
    };

    let mut journal = Journal::open(&wal_dir).expect("Failed to open journal");
    let store = HybridStore::open(&db_dir).expect("Failed to open store");

    journal.append(serialize_assertion(&assertion).expect("ser")).expect("append");

    let journal = Arc::new(Mutex::new(journal));
    let store = Arc::new(store);
    let mut worker =
        IngestWorker::new(journal, store.clone()).await.expect("Failed to create worker");

    let result = worker.step().await;
    assert!(
        result.is_ok(),
        "Should accept predicate with exactly 256 bytes, got error: {:?}",
        result
    );
}
/// Test: Assertions with NaN confidence are rejected.
#[tokio::test]
async fn test_rejects_nan_confidence() {
    let dir = tempdir().expect("Failed to create temp dir");
    let wal_dir = dir.path().join("wal");
    let db_dir = dir.path().join("db");

    let mut csprng = OsRng;
    let signing_key = SigningKey::generate(&mut csprng);
    let verifying_key = signing_key.verifying_key();
    let message = "Test:nan";
    let signature = signing_key.sign(message.as_bytes());

    let assertion = Assertion {
        subject: "Test".to_string(),
        predicate: "nan".to_string(),
        object: ObjectValue::Text("test".to_string()),
        parent_hash: None,
        source_hash: [0u8; 32],
        source_class: SourceClass::Expert,
        visual_hash: None,
        epoch: None,
        source_metadata: None,
        lifecycle: LifecycleStage::Proposed,
        signatures: vec![SignatureEntry {
            version: 1,
            agent_id: verifying_key.to_bytes(),
            signature: signature.to_bytes(),
            timestamp: 1000,
        }],
        confidence: f32::NAN,
        timestamp: 1000,
        hlc_timestamp: HlcTimestamp::default(),
        vector: None,
    };

    let mut journal = Journal::open(&wal_dir).expect("Failed to open journal");
    let store = HybridStore::open(&db_dir).expect("Failed to open store");

    journal.append(serialize_assertion(&assertion).expect("ser")).expect("append");

    let journal = Arc::new(Mutex::new(journal));
    let store = Arc::new(store);
    let mut worker =
        IngestWorker::new(journal, store.clone()).await.expect("Failed to create worker");

    let result = worker.step().await;
    assert!(result.is_err(), "Should reject NaN confidence");

    let err = result.unwrap_err();
    assert!(matches!(err, IngestError::InputValidation(_)));
}
