//! Battery 6: Signature Tamper Detection.
//!
//! Tests signature verification and tamper detection in the ingestion pipeline.
//!
//! # Test Coverage
//!
//! | Test | Scenario | Validates |
//! |------|----------|-----------|
//! | `test_valid_signature_accepted` | Valid sig (v1) | Accepted and stored |
//! | `test_tampered_confidence_not_detected_v1` | v1 design limit | Confidence not covered by v1 sig |
//! | `test_tampered_confidence_rejected_v2` | v2 enterprise | Confidence tampering detected |
//! | `test_tampered_subject_rejected` | Subject tamper | Rejected |
//! | `test_wrong_agent_id_rejected` | Agent ID mismatch | Rejected |
//!
//! See also: Battery 10 for multi-sig, v2 signatures, and unknown version rejection.

#![allow(clippy::expect_used)] // Test code uses expect() for clear failure messages

use super::helpers::*;

/// Test 6.1: Valid signature is accepted.
///
/// Agent A signs an assertion correctly. Ingest through IngestWorker.
/// Assert: assertion is stored, index entries exist.
#[tokio::test]
async fn test_valid_signature_accepted() {
    let dir = tempdir().expect("create temp dir");
    let wal_dir = dir.path().join("wal");
    let db_dir = dir.path().join("db");

    let base_ts: u64 = 1_000_000;

    // Create a properly signed assertion
    let assertion = create_signed_assertion_with_source(
        "Subject_A",
        "predicate_test",
        ObjectValue::Text("value".to_string()),
        SourceClass::Clinical,
        0.8,
        base_ts,
    );

    // Write to WAL
    let mut journal = Journal::open(&wal_dir).expect("open journal");
    journal.append(serialize_assertion(&assertion).expect("ser")).expect("append");

    // Ingest via IngestWorker
    let journal = Arc::new(Mutex::new(journal));
    let store = Arc::new(HybridStore::open(&db_dir).expect("open store"));

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

    let bytes = worker.step().await.expect("ingest step");
    assert!(bytes > 0, "should process data from WAL");

    // Verify assertion is stored (H: key exists)
    let h_prefix = key_codec::assertion_key("Subject_A", "");
    let h_entries = store.scan_prefix(&h_prefix).await.expect("scan H:");
    assert_eq!(h_entries.len(), 1, "should have 1 assertion stored");

    // Verify SP: index exists
    let sp_prefix = key_codec::subject_predicate_scan_prefix("Subject_A");
    let sp_entries = store.scan_prefix(&sp_prefix).await.expect("scan SP:");
    assert_eq!(sp_entries.len(), 1, "should have 1 SP: index entry");
}

/// Test 6.2: Tampered confidence is NOT detected (v1 design limitation).
///
/// Agent A signs assertion with confidence=0.8 using v1 signature. The v1 signature
/// only covers `{subject}:{predicate}`, not the confidence field. Modifying confidence
/// after signing does NOT invalidate the signature.
///
/// This test documents the v1 behavior: changing confidence won't fail verification
/// because it's not part of the signed message. Use v2 signatures for full coverage.
#[tokio::test]
async fn test_tampered_confidence_not_detected_v1() {
    let dir = tempdir().expect("create temp dir");
    let wal_dir = dir.path().join("wal");
    let db_dir = dir.path().join("db");

    let base_ts: u64 = 1_000_000;

    // Create a signed assertion with confidence 0.8
    let mut csprng = OsRng;
    let signing_key = SigningKey::generate(&mut csprng);
    let verifying_key = signing_key.verifying_key();

    // Sign for "Subject_B:predicate_test" (v1 format)
    let message = format!("{}:{}", "Subject_B", "predicate_test");
    let signature = signing_key.sign(message.as_bytes());

    // Create assertion with original confidence 0.8
    let mut assertion = AssertionBuilder::new()
        .subject("Subject_B")
        .predicate("predicate_test")
        .object_text("value")
        .source_class(SourceClass::Clinical)
        .confidence(0.8)
        .lifecycle(LifecycleStage::Proposed)
        .timestamp(base_ts)
        .signatures(vec![SignatureEntry {
            agent_id: verifying_key.to_bytes(),
            signature: signature.to_bytes(),
            timestamp: base_ts,
            version: 1,
        }])
        .build();

    // Tamper: Change confidence to 1.0 after signing
    assertion.confidence = 1.0;

    // Write tampered assertion to WAL
    let mut journal = Journal::open(&wal_dir).expect("open journal");
    journal.append(serialize_assertion(&assertion).expect("ser")).expect("append");

    // Ingest via IngestWorker
    let journal = Arc::new(Mutex::new(journal));
    let store = Arc::new(HybridStore::open(&db_dir).expect("open store"));

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

    let bytes = worker.step().await.expect("ingest step");

    // v1 DESIGN LIMITATION: The tampered assertion is accepted because the v1 signature
    // only covers {subject}:{predicate}, not the confidence field.
    assert!(
        bytes > 0,
        "v1: tampered confidence is accepted (signature only covers subject:predicate)"
    );

    // Verify assertion is stored
    let h_prefix = key_codec::assertion_key("Subject_B", "");
    let h_entries = store.scan_prefix(&h_prefix).await.expect("scan H:");
    assert_eq!(
        h_entries.len(),
        1,
        "v1: tampered assertion is stored (confidence not covered by signature)"
    );

    // Verify the stored assertion has the tampered confidence
    let (_key, value) = &h_entries[0];
    let stored: Assertion = stemedb_core::serde::deserialize(value).expect("deserialize");
    assert_eq!(stored.confidence, 1.0, "stored assertion should have tampered confidence 1.0");
}

/// Test 6.2b: Tampered confidence IS detected with v2 signatures (enterprise security).
///
/// Agent A signs assertion with confidence=0.8 using v2 signature. The v2 signature
/// covers the BLAKE3 hash of the full serialized assertion. Modifying confidence
/// after signing DOES invalidate the signature.
///
/// This is the security improvement v2 provides over v1.
#[tokio::test]
async fn test_tampered_confidence_rejected_v2() {
    let dir = tempdir().expect("create temp dir");
    let wal_dir = dir.path().join("wal");
    let db_dir = dir.path().join("db");

    let base_ts: u64 = 1_000_000;

    // Create a signed assertion with confidence 0.8 using v2 signing
    let mut csprng = OsRng;
    let signing_key = SigningKey::generate(&mut csprng);
    let verifying_key = signing_key.verifying_key();

    // Build assertion WITHOUT signatures first (for hash computation)
    let mut assertion = AssertionBuilder::new()
        .subject("Subject_V2")
        .predicate("predicate_test")
        .object_text("value")
        .source_class(SourceClass::Clinical)
        .confidence(0.8) // Original confidence
        .lifecycle(LifecycleStage::Proposed)
        .timestamp(base_ts)
        .signatures(vec![])
        .build();

    // Serialize to get content hash (with confidence=0.8)
    let bytes_for_hash = serialize(&assertion).expect("serialize for hash");
    let content_hash = blake3::hash(&bytes_for_hash);

    // Sign the content hash (v2 format)
    let signature = signing_key.sign(content_hash.as_bytes());

    // Add v2 signature
    assertion.signatures = vec![SignatureEntry {
        agent_id: verifying_key.to_bytes(),
        signature: signature.to_bytes(),
        timestamp: base_ts,
        version: 2,
    }];

    // TAMPER: Change confidence to 1.0 after signing
    // This changes the content hash, invalidating the v2 signature
    assertion.confidence = 1.0;

    // Write tampered assertion to WAL
    let mut journal = Journal::open(&wal_dir).expect("open journal");
    journal.append(serialize_assertion(&assertion).expect("ser")).expect("append");

    // Ingest via IngestWorker
    let journal = Arc::new(Mutex::new(journal));
    let store = Arc::new(HybridStore::open(&db_dir).expect("open store"));

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

    // v2 SECURITY: Tampered assertion should be REJECTED
    let result = worker.step().await;
    assert!(result.is_err(), "v2: tampered confidence should be rejected");

    // Verify the error is an invalid signature error
    let err = result.unwrap_err();
    let err_str = err.to_string();
    assert!(
        err_str.contains("Signature") || err_str.contains("verification"),
        "error should be related to signature verification, got: {}",
        err_str
    );

    // Verify no assertion was stored
    let h_prefix = key_codec::assertion_key("Subject_V2", "");
    let h_entries = store.scan_prefix(&h_prefix).await.expect("scan H:");
    assert_eq!(h_entries.len(), 0, "v2: tampered assertion should NOT be stored");
}

/// Test 6.3: Tampered subject is rejected.
///
/// Agent A signs assertion with subject="Subject_C". Clone the assertion,
/// change subject to "Subject_D", keep original signature.
/// Assert: ingestion fails with invalid signature.
#[tokio::test]
async fn test_tampered_subject_rejected() {
    let dir = tempdir().expect("create temp dir");
    let wal_dir = dir.path().join("wal");
    let db_dir = dir.path().join("db");

    let base_ts: u64 = 1_000_000;

    // Create a signed assertion with subject "Subject_C"
    let mut csprng = OsRng;
    let signing_key = SigningKey::generate(&mut csprng);
    let verifying_key = signing_key.verifying_key();

    // Sign for "Subject_C:predicate_test"
    let message = format!("{}:{}", "Subject_C", "predicate_test");
    let signature = signing_key.sign(message.as_bytes());

    // Create assertion with original subject "Subject_C"
    let mut assertion = AssertionBuilder::new()
        .subject("Subject_C")
        .predicate("predicate_test")
        .object_text("value")
        .source_class(SourceClass::Clinical)
        .confidence(0.8)
        .lifecycle(LifecycleStage::Proposed)
        .timestamp(base_ts)
        .signatures(vec![SignatureEntry {
            agent_id: verifying_key.to_bytes(),
            signature: signature.to_bytes(),
            timestamp: base_ts,
            version: 1,
        }])
        .build();

    // Tamper: Change subject to "Subject_D" (but keep signature for "Subject_C")
    assertion.subject = "Subject_D".to_string();

    // Write tampered assertion to WAL
    let mut journal = Journal::open(&wal_dir).expect("open journal");
    journal.append(serialize_assertion(&assertion).expect("ser")).expect("append");

    // Ingest via IngestWorker
    let journal = Arc::new(Mutex::new(journal));
    let store = Arc::new(HybridStore::open(&db_dir).expect("open store"));

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

    // Attempt to ingest - should fail due to invalid signature
    let result = worker.step().await;
    assert!(result.is_err(), "tampered subject should fail signature verification");

    // Verify the error is an invalid signature error
    let err = result.unwrap_err();
    let err_str = err.to_string();
    assert!(
        err_str.contains("Signature") || err_str.contains("verification"),
        "error should be related to signature verification, got: {}",
        err_str
    );

    // Verify no assertion was stored
    let h_prefix = key_codec::assertion_key("Subject_D", "");
    let h_entries = store.scan_prefix(&h_prefix).await.expect("scan H:");
    assert_eq!(h_entries.len(), 0, "tampered assertion should NOT be stored");
}

/// Test 6.4: Wrong agent_id is rejected.
///
/// Agent A signs assertion. Replace `agent_id` in the `SignatureEntry` with
/// Agent B's public key (but keep Agent A's signature bytes).
/// Assert: ingestion fails - the signature was made by A's private key but
/// claims to be from B's public key.
#[tokio::test]
async fn test_wrong_agent_id_rejected() {
    let dir = tempdir().expect("create temp dir");
    let wal_dir = dir.path().join("wal");
    let db_dir = dir.path().join("db");

    let base_ts: u64 = 1_000_000;

    // Create Agent A's key pair and sign the assertion
    let mut csprng = OsRng;
    let signing_key_a = SigningKey::generate(&mut csprng);

    // Create Agent B's key pair (we'll use B's public key to tamper)
    let signing_key_b = SigningKey::generate(&mut csprng);
    let verifying_key_b = signing_key_b.verifying_key();

    // Agent A signs for "Subject_E:predicate_test"
    let message = format!("{}:{}", "Subject_E", "predicate_test");
    let signature_a = signing_key_a.sign(message.as_bytes());

    // Create assertion with Agent A's signature but Agent B's public key
    let assertion = AssertionBuilder::new()
        .subject("Subject_E")
        .predicate("predicate_test")
        .object_text("value")
        .source_class(SourceClass::Clinical)
        .confidence(0.8)
        .lifecycle(LifecycleStage::Proposed)
        .timestamp(base_ts)
        .signatures(vec![SignatureEntry {
            agent_id: verifying_key_b.to_bytes(),
            signature: signature_a.to_bytes(),
            timestamp: base_ts,
            version: 1,
        }])
        .build();

    // Write tampered assertion to WAL
    let mut journal = Journal::open(&wal_dir).expect("open journal");
    journal.append(serialize_assertion(&assertion).expect("ser")).expect("append");

    // Ingest via IngestWorker
    let journal = Arc::new(Mutex::new(journal));
    let store = Arc::new(HybridStore::open(&db_dir).expect("open store"));

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

    // Attempt to ingest - should fail because signature was made by A but claims to be from B
    let result = worker.step().await;
    assert!(
        result.is_err(),
        "wrong agent_id should fail signature verification (signature made by A, claims to be from B)"
    );

    // Verify the error is an invalid signature error
    let err = result.unwrap_err();
    let err_str = err.to_string();
    assert!(
        err_str.contains("Signature") || err_str.contains("verification"),
        "error should be related to signature verification, got: {}",
        err_str
    );

    // Verify no assertion was stored
    let h_prefix = key_codec::assertion_key("Subject_E", "");
    let h_entries = store.scan_prefix(&h_prefix).await.expect("scan H:");
    assert_eq!(h_entries.len(), 0, "tampered assertion should NOT be stored");
}
