use super::*;

fn sample_event(id: u64) -> EventRecord {
    EventRecord {
        entity_id: id,
        signal_type: RECORD_TYPE_SIGNAL,
        weight: 1.0,
        timestamp_nanos: 1_000_000_000,
    }
}

#[test]
fn event_record_roundtrip() {
    let event = sample_event(42);
    let bytes = event.to_bytes();
    assert_eq!(bytes.len(), EVENT_SIZE);
    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());
}

#[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);

    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 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_size_is_21() {
    assert_eq!(EVENT_SIZE, 21);
}

// ── v2 BatchHeader tests ──────────────────────────────────────────────

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

#[test]
fn v2_batch_has_shard_region_fields() {
    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_V2);
    assert_eq!(header.shard_id, ShardId::SINGLE);
    assert_eq!(header.region_id, RegionId::SINGLE);
}

#[test]
fn v2_shard_region_bytes_are_zero_in_single_node() {
    // Verify the raw wire bytes at positions 28-31 are zeros
    // when encoding in single-node mode.
    let events = vec![sample_event(1)];
    let encoded = encode_batch(&events, 1, 1).expect("encode");
    assert_eq!(encoded[28], 0);
    assert_eq!(encoded[29], 0);
    assert_eq!(encoded[30], 0);
    assert_eq!(encoded[31], 0);
}

#[test]
fn v1_batch_decodes_with_shard_zero_region_zero() {
    // Build a v1-format batch: same layout but version byte = 1.
    // Since bytes 28-31 are zero in both v1 and single-node v2,
    // the only difference is the version byte at offset 4.
    // We must re-sign the checksum after changing the version byte
    // because byte 4 is inside the BLAKE3 input range [0..32].
    let events = vec![sample_event(1)];
    let mut encoded = encode_batch(&events, 100, 999).expect("encode");

    // Downgrade version byte to v1.
    encoded[4] = FORMAT_VERSION_V1;

    // Re-compute BLAKE3 checksum over the modified header prefix + events.
    let checksum = compute_checksum(&encoded[0..32], &encoded[HEADER_SIZE..]);
    encoded[32..64].copy_from_slice(checksum.as_bytes());

    let (header, decoded_events) = 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_events.len(), 1);
    assert_eq!(decoded_events[0], sample_event(1));
}

#[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 v2_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 super::*;
    use proptest::prelude::*;

    fn arb_event() -> impl Strategy<Value = EventRecord> {
        (any::<u64>(), any::<u8>(), any::<f32>(), any::<u64>()).prop_map(
            |(entity_id, signal_type, weight, timestamp_nanos)| EventRecord {
                entity_id,
                signal_type,
                weight,
                timestamp_nanos,
            },
        )
    }

    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);
        }

        #[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_V2);
            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.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);
            }
        }

        #[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.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);
            }
        }

        #[test]
        fn corrupt_any_payload_byte_fails(
            events in proptest::collection::vec(arb_event(), 1..=50),
            corrupt_offset in 0usize..1050,
        ) {
            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());
        }
    }
}
