use super::*;

fn sample_event(id: u64) -> EventRecord {
    EventRecord::signal(id, RECORD_TYPE_SIGNAL, 1.0, 1_000_000_000)
}

/// Build a genuine legacy (v1/v2) batch with 21-byte event records, for
/// backward-compatibility decode tests. Mirrors `encode_batch_with_shard` but
/// emits the legacy event layout and a caller-chosen version byte.
fn encode_legacy_batch(
    events: &[EventRecord],
    version: u8,
    first_seq: u64,
    batch_ts: u64,
) -> Vec<u8> {
    let payload_len = events.len() * EVENT_SIZE;
    let mut buf = vec![0u8; HEADER_SIZE + payload_len];
    buf[0..4].copy_from_slice(&MAGIC);
    buf[4] = version;
    buf[5] = 0;
    buf[6..8].copy_from_slice(&(events.len() as u16).to_le_bytes());
    buf[8..16].copy_from_slice(&first_seq.to_le_bytes());
    buf[16..24].copy_from_slice(&batch_ts.to_le_bytes());
    buf[24..28].copy_from_slice(&(payload_len as u32).to_le_bytes());
    // v1 leaves 28-31 zero; v2 would carry shard/region — single-node = zero either way.
    for (i, event) in events.iter().enumerate() {
        let offset = HEADER_SIZE + i * EVENT_SIZE;
        buf[offset..offset + EVENT_SIZE].copy_from_slice(&event.to_bytes_legacy());
    }
    let checksum = compute_checksum(&buf[0..32], &buf[HEADER_SIZE..]);
    buf[32..64].copy_from_slice(checksum.as_bytes());
    buf
}

#[test]
fn event_record_roundtrip() {
    let event = sample_event(42);
    let bytes = event.to_bytes();
    assert_eq!(bytes.len(), EVENT_SIZE_V3);
    let decoded = EventRecord::from_bytes(&bytes).expect("decode should succeed");
    assert_eq!(decoded, event);
}

#[test]
fn event_record_wrong_size() {
    let result = EventRecord::from_bytes(&[0u8; 10]);
    assert!(result.is_err());
    // 21 (legacy) and 32 (v3) are the only accepted sizes.
    assert!(EventRecord::from_bytes(&[0u8; EVENT_SIZE]).is_ok());
    assert!(EventRecord::from_bytes(&[0u8; EVENT_SIZE_V3]).is_ok());
    assert!(EventRecord::from_bytes(&[0u8; 30]).is_err());
}

#[test]
fn v3_nonzero_reserved_bytes_rejected_on_decode() {
    // The v3 reserved bytes [30..32] are written zero by the encoder and must
    // decode as zero. A non-zero reserved field (e.g. a future writer that
    // claimed the space, or a hand-crafted frame whose per-record checksum is
    // not re-checked here) must surface as corruption rather than being
    // silently dropped, so an old reader never decodes a record whose new
    // semantics it cannot represent. The two reserved bytes are checked
    // independently, so verify each in isolation.
    for idx in 30..32 {
        let mut bytes = sample_event(7).to_bytes();
        assert_eq!(bytes[idx], 0, "encoder must write reserved byte {idx} zero");
        bytes[idx] = 0x01;
        let result = EventRecord::from_bytes(&bytes);
        let err_msg = result
            .expect_err("non-zero reserved byte must be rejected")
            .to_string();
        assert!(
            err_msg.contains("reserved"),
            "reserved byte {idx} corruption message should mention 'reserved', got: {err_msg}"
        );
    }
}

#[test]
fn encode_decode_roundtrip_single() {
    let events = vec![sample_event(1)];
    let encoded = encode_batch(&events, 100, 999).expect("encode should succeed");
    assert_eq!(encoded.len(), HEADER_SIZE + EVENT_SIZE_V3);

    let (header, decoded_events) = decode_batch(&encoded).expect("decode should succeed");
    assert_eq!(header.version, FORMAT_VERSION);
    assert_eq!(header.event_count, 1);
    assert_eq!(header.first_seq, 100);
    assert_eq!(header.batch_timestamp, 999);
    assert_eq!(header.payload_len, EVENT_SIZE_V3 as u32);
    assert_eq!(decoded_events.len(), 1);
    assert_eq!(decoded_events[0], events[0]);
}

#[test]
fn encode_decode_roundtrip_multi() {
    let events: Vec<EventRecord> = (0..50).map(sample_event).collect();
    let encoded = encode_batch(&events, 1, 42).expect("encode should succeed");
    let (header, decoded) = decode_batch(&encoded).expect("decode should succeed");
    assert_eq!(header.event_count, 50);
    assert_eq!(decoded.len(), 50);
    for (original, decoded_ev) in events.iter().zip(decoded.iter()) {
        assert_eq!(original, decoded_ev);
    }
}

#[test]
fn encode_empty_batch_fails() {
    let result = encode_batch(&[], 1, 1);
    assert!(result.is_err());
}

#[test]
fn encode_oversized_batch_fails() {
    let events: Vec<EventRecord> = (0..=u64::from(MAX_EVENTS_PER_BATCH))
        .map(sample_event)
        .collect();
    let result = encode_batch(&events, 1, 1);
    assert!(result.is_err());
}

#[test]
fn corrupt_payload_byte_fails_blake3() {
    let events = vec![sample_event(1), sample_event(2)];
    let mut encoded = encode_batch(&events, 1, 1).expect("encode should succeed");
    // Flip a byte in the payload (event data area)
    let payload_offset = HEADER_SIZE + 5;
    encoded[payload_offset] ^= 0xFF;
    let result = decode_batch(&encoded);
    assert!(result.is_err());
    let err_msg = result.expect_err("should fail").to_string();
    assert!(err_msg.contains("checksum"));
}

#[test]
fn corrupt_header_field_fails_blake3() {
    let events = vec![sample_event(1)];
    let mut encoded = encode_batch(&events, 1, 1).expect("encode should succeed");
    // Corrupt the first_seq field in the header (byte 8)
    encoded[10] ^= 0xFF;
    let result = decode_batch(&encoded);
    assert!(result.is_err());
}

#[test]
fn invalid_magic_detected() {
    let events = vec![sample_event(1)];
    let mut encoded = encode_batch(&events, 1, 1).expect("encode should succeed");
    encoded[0] = 0xFF; // corrupt magic
    let result = decode_batch(&encoded);
    assert!(result.is_err());
    let err_msg = result.expect_err("should fail").to_string();
    assert!(err_msg.contains("magic"));
}

#[test]
fn truncated_header_detected() {
    let result = decode_batch(&[0u8; 32]);
    assert!(result.is_err());
}

#[test]
fn truncated_payload_detected() {
    let events = vec![sample_event(1)];
    let encoded = encode_batch(&events, 1, 1).expect("encode should succeed");
    // Truncate: give header but only partial payload
    let result = decode_batch(&encoded[..HEADER_SIZE + 5]);
    assert!(result.is_err());
}

#[test]
fn magic_bytes_are_tidl() {
    // Verify 0x54494C44 LE = "TIDL"
    assert_eq!(u32::from_le_bytes(MAGIC), 0x5449_4C44);
}

#[test]
fn event_content_hash_deterministic() {
    let event = sample_event(42);
    let h1 = event_content_hash(&event);
    let h2 = event_content_hash(&event);
    assert_eq!(h1, h2);
}

#[test]
fn event_content_hash_differs_for_different_events() {
    let h1 = event_content_hash(&sample_event(1));
    let h2 = event_content_hash(&sample_event(2));
    assert_ne!(h1, h2);
}

#[test]
fn header_size_is_cache_line() {
    assert_eq!(HEADER_SIZE, 64);
}

#[test]
fn event_sizes() {
    assert_eq!(EVENT_SIZE, 21);
    assert_eq!(EVENT_SIZE_V3, 32);
    assert_eq!(event_size_for_version(FORMAT_VERSION_V1), 21);
    assert_eq!(event_size_for_version(FORMAT_VERSION_V2), 21);
    assert_eq!(event_size_for_version(FORMAT_VERSION_V3), 32);
}

// ── v3 governance envelope tests ──────────────────────────────────────────

#[test]
fn format_version_constants() {
    assert_eq!(FORMAT_VERSION_V1, 1);
    assert_eq!(FORMAT_VERSION_V2, 2);
    assert_eq!(FORMAT_VERSION_V3, 3);
    assert_eq!(FORMAT_VERSION, FORMAT_VERSION_V3);
}

#[test]
fn new_batches_are_v3() {
    let events = vec![sample_event(1)];
    let encoded = encode_batch(&events, 100, 999).expect("encode");
    let (header, _) = decode_batch(&encoded).expect("decode");
    assert_eq!(header.version, FORMAT_VERSION_V3);
    assert_eq!(header.shard_id, ShardId::SINGLE);
    assert_eq!(header.region_id, RegionId::SINGLE);
}

#[test]
fn v3_governance_fields_roundtrip() {
    // scope=Community(1), writer_agent=7, share_policy_version=3, epoch=42
    let event = EventRecord::scoped(99, RECORD_TYPE_SIGNAL, 2.5, 12_345, 1, 7, 3, 42);
    let encoded = encode_batch(std::slice::from_ref(&event), 1, 1).expect("encode");
    let (header, decoded) = decode_batch(&encoded).expect("decode");
    assert_eq!(header.version, FORMAT_VERSION_V3);
    assert_eq!(decoded.len(), 1);
    assert_eq!(decoded[0], event);
    assert_eq!(decoded[0].scope, 1);
    assert_eq!(decoded[0].writer_agent, 7);
    assert_eq!(decoded[0].share_policy_version, 3);
    assert_eq!(decoded[0].membership_epoch, 42);
}

#[test]
fn v3_governance_bytes_covered_by_blake3() {
    let event = EventRecord::scoped(1, RECORD_TYPE_SIGNAL, 1.0, 1, 1, 9, 0, 0);
    let mut encoded = encode_batch(&[event], 1, 1).expect("encode");
    // Corrupt the scope byte (event offset 21 within the payload at HEADER_SIZE).
    let scope_offset = HEADER_SIZE + 21;
    encoded[scope_offset] ^= 0xFF;
    let result = decode_batch(&encoded);
    assert!(result.is_err());
    assert!(
        result
            .expect_err("should fail")
            .to_string()
            .contains("checksum")
    );
}

#[test]
fn v1_batch_decodes_with_governance_defaults() {
    // A genuine v1 batch (21-byte events) must decode under v3 code with the
    // governance envelope defaulting to local/zero.
    let events = vec![sample_event(1), sample_event(2)];
    let encoded = encode_legacy_batch(&events, FORMAT_VERSION_V1, 100, 999);
    let (header, decoded) = decode_batch(&encoded).expect("v1 batch must decode");
    assert_eq!(header.version, FORMAT_VERSION_V1);
    assert_eq!(header.shard_id, ShardId::SINGLE);
    assert_eq!(header.region_id, RegionId::SINGLE);
    assert_eq!(decoded.len(), 2);
    for (i, ev) in decoded.iter().enumerate() {
        assert_eq!(ev.entity_id, events[i].entity_id);
        assert_eq!(ev.scope, 0, "v1 event scope must default to Local");
        assert_eq!(ev.writer_agent, 0);
        assert_eq!(ev.share_policy_version, 0);
        assert_eq!(ev.membership_epoch, 0);
    }
}

#[test]
fn v2_batch_decodes_with_governance_defaults() {
    let events = vec![sample_event(5)];
    let encoded = encode_legacy_batch(&events, FORMAT_VERSION_V2, 1, 1);
    let (header, decoded) = decode_batch(&encoded).expect("v2 batch must decode");
    assert_eq!(header.version, FORMAT_VERSION_V2);
    assert_eq!(decoded.len(), 1);
    assert_eq!(decoded[0], sample_event(5)); // governance all-zero == EventRecord::signal
}

#[test]
fn legacy_local_event_hash_matches_v3_local() {
    // Dedup version-stability: a legacy 21-byte event and a fresh v3 local event
    // with the same base fields must hash identically (both governance-zero).
    let legacy = EventRecord::from_bytes(&sample_event(7).to_bytes_legacy()).unwrap();
    let v3_local = sample_event(7);
    assert_eq!(legacy, v3_local);
    assert_eq!(event_content_hash(&legacy), event_content_hash(&v3_local));
}

#[test]
fn content_hash_distinguishes_scope() {
    // Same base fields, different scope => distinct dedup identity.
    let local = EventRecord::signal(1, RECORD_TYPE_SIGNAL, 1.0, 1000);
    let community = EventRecord::scoped(1, RECORD_TYPE_SIGNAL, 1.0, 1000, 1, 0, 0, 0);
    assert_ne!(event_content_hash(&local), event_content_hash(&community));
}

#[test]
fn unsupported_version_rejected() {
    let events = vec![sample_event(1)];
    let mut encoded = encode_batch(&events, 1, 1).expect("encode");

    // Set version to 99 (unsupported).
    encoded[4] = 99;
    let checksum = compute_checksum(&encoded[0..32], &encoded[HEADER_SIZE..]);
    encoded[32..64].copy_from_slice(checksum.as_bytes());

    let result = decode_batch(&encoded);
    assert!(result.is_err());
    let err_msg = result.expect_err("should fail").to_string();
    assert!(err_msg.contains("unsupported format version"));
}

#[test]
fn encode_with_shard_roundtrips() {
    let events = vec![sample_event(1), sample_event(2)];
    let sid = ShardId(7);
    let rid = RegionId(3);
    let encoded =
        encode_batch_with_shard(&events, 100, 999, sid, rid).expect("encode should succeed");
    let (header, decoded) = decode_batch(&encoded).expect("decode should succeed");
    assert_eq!(header.shard_id, sid);
    assert_eq!(header.region_id, rid);
    assert_eq!(header.first_seq, 100);
    assert_eq!(decoded.len(), 2);
    // Verify raw wire bytes at shard/region positions
    assert_eq!(u16::from_le_bytes([encoded[28], encoded[29]]), 7);
    assert_eq!(u16::from_le_bytes([encoded[30], encoded[31]]), 3);
}

#[test]
fn shard_region_covered_by_blake3() {
    // Corrupt a shard_id byte (byte 28) and verify BLAKE3 catches it.
    let events = vec![sample_event(1)];
    let mut encoded = encode_batch(&events, 1, 1).expect("encode");
    encoded[28] ^= 0xFF; // corrupt shard_id
    let result = decode_batch(&encoded);
    assert!(result.is_err());
    let err_msg = result.expect_err("should fail").to_string();
    assert!(err_msg.contains("checksum"));
}

mod proptests {
    use proptest::prelude::*;

    use super::*;

    fn arb_event() -> impl Strategy<Value = EventRecord> {
        (
            any::<u64>(),
            any::<u8>(),
            any::<f32>(),
            any::<u64>(),
            0u8..4,
            any::<u16>(),
            any::<u16>(),
            any::<u32>(),
        )
            .prop_map(
                |(entity_id, signal_type, weight, ts, scope, writer, spv, epoch)| {
                    EventRecord::scoped(
                        entity_id,
                        signal_type,
                        weight,
                        ts,
                        scope,
                        writer,
                        spv,
                        epoch,
                    )
                },
            )
    }

    proptest! {
        #[test]
        fn event_roundtrip(event in arb_event()) {
            let bytes = event.to_bytes();
            let decoded = EventRecord::from_bytes(&bytes)?;
            prop_assert_eq!(decoded.entity_id, event.entity_id);
            prop_assert_eq!(decoded.signal_type, event.signal_type);
            // f32 NaN != NaN, so compare bits
            prop_assert_eq!(decoded.weight.to_bits(), event.weight.to_bits());
            prop_assert_eq!(decoded.timestamp_nanos, event.timestamp_nanos);
            prop_assert_eq!(decoded.scope, event.scope);
            prop_assert_eq!(decoded.writer_agent, event.writer_agent);
            prop_assert_eq!(decoded.share_policy_version, event.share_policy_version);
            prop_assert_eq!(decoded.membership_epoch, event.membership_epoch);
        }

        #[test]
        fn batch_roundtrip(
            events in proptest::collection::vec(arb_event(), 1..=100),
            first_seq in any::<u64>(),
            batch_ts in any::<u64>(),
        ) {
            let encoded = encode_batch(&events, first_seq, batch_ts)?;
            let (header, decoded) = decode_batch(&encoded)?;
            prop_assert_eq!(header.first_seq, first_seq);
            prop_assert_eq!(header.batch_timestamp, batch_ts);
            prop_assert_eq!(header.version, FORMAT_VERSION_V3);
            prop_assert_eq!(header.shard_id, ShardId::SINGLE);
            prop_assert_eq!(header.region_id, RegionId::SINGLE);
            prop_assert_eq!(decoded.len(), events.len());
            for (orig, dec) in events.iter().zip(decoded.iter()) {
                prop_assert_eq!(orig, dec);
            }
        }

        #[test]
        fn batch_roundtrip_with_shard_region(
            events in proptest::collection::vec(arb_event(), 1..=50),
            first_seq in any::<u64>(),
            batch_ts in any::<u64>(),
            shard_id in any::<u16>(),
            region_id in any::<u16>(),
        ) {
            let sid = ShardId(shard_id);
            let rid = RegionId(region_id);
            let encoded = encode_batch_with_shard(&events, first_seq, batch_ts, sid, rid)?;
            let (header, decoded) = decode_batch(&encoded)?;
            prop_assert_eq!(header.first_seq, first_seq);
            prop_assert_eq!(header.batch_timestamp, batch_ts);
            prop_assert_eq!(header.shard_id, sid);
            prop_assert_eq!(header.region_id, rid);
            prop_assert_eq!(decoded.len(), events.len());
            for (orig, dec) in events.iter().zip(decoded.iter()) {
                prop_assert_eq!(orig, dec);
            }
        }

        /// A genuine legacy v1 batch always decodes with zeroed governance.
        #[test]
        fn v1_batch_roundtrip_governance_zero(
            events in proptest::collection::vec(arb_event(), 1..=50),
        ) {
            let encoded = encode_legacy_batch(&events, FORMAT_VERSION_V1, 1, 1);
            let (header, decoded) = decode_batch(&encoded)?;
            prop_assert_eq!(header.version, FORMAT_VERSION_V1);
            prop_assert_eq!(decoded.len(), events.len());
            for (orig, dec) in events.iter().zip(decoded.iter()) {
                prop_assert_eq!(orig.entity_id, dec.entity_id);
                prop_assert_eq!(orig.signal_type, dec.signal_type);
                prop_assert_eq!(orig.weight.to_bits(), dec.weight.to_bits());
                prop_assert_eq!(orig.timestamp_nanos, dec.timestamp_nanos);
                prop_assert_eq!(dec.scope, 0u8);
                prop_assert_eq!(dec.writer_agent, 0u16);
                prop_assert_eq!(dec.membership_epoch, 0u32);
            }
        }

        #[test]
        fn corrupt_any_payload_byte_fails(
            events in proptest::collection::vec(arb_event(), 1..=50),
            corrupt_offset in 0usize..2000,
        ) {
            let encoded = encode_batch(&events, 1, 1)?;
            let payload_start = HEADER_SIZE;
            let payload_end = encoded.len();
            let payload_size = payload_end - payload_start;
            if payload_size == 0 {
                return Ok(());
            }
            let actual_offset = payload_start + (corrupt_offset % payload_size);
            let mut corrupted = encoded;
            corrupted[actual_offset] ^= 0xFF;
            let result = decode_batch(&corrupted);
            prop_assert!(result.is_err());
        }
    }
}
