#![allow(unused_imports, clippy::all)]
use epics_base_rs::server::snapshot::*;
use epics_base_rs::types::*;
use std::time::{Duration, SystemTime};

const EPICS_UNIX_EPOCH_OFFSET_SECS: u64 = 631_152_000;

#[test]
fn test_double_roundtrip() {
    let val = EpicsValue::Double(3.15);
    let bytes = val.to_bytes();
    let val2 = EpicsValue::from_bytes(DbFieldType::Double, &bytes).unwrap();
    match val2 {
        EpicsValue::Double(v) => assert!((v - 3.15).abs() < 1e-10),
        _ => panic!("wrong type"),
    }
}

#[test]
fn test_string_roundtrip() {
    let val = EpicsValue::String("hello".into());
    let bytes = val.to_bytes();
    assert_eq!(bytes.len(), 40);
    let val2 = EpicsValue::from_bytes(DbFieldType::String, &bytes).unwrap();
    match val2 {
        EpicsValue::String(s) => assert_eq!(s, "hello"),
        _ => panic!("wrong type"),
    }
}

#[test]
fn test_parse_values() {
    match EpicsValue::parse(DbFieldType::Long, "42").unwrap() {
        EpicsValue::Long(v) => assert_eq!(v, 42),
        _ => panic!("wrong type"),
    }
}

#[test]
fn test_serialize_ctrl_double_layout() {
    let val = EpicsValue::Double(42.0);
    let ts = SystemTime::UNIX_EPOCH + Duration::from_secs(EPICS_UNIX_EPOCH_OFFSET_SECS + 100);
    let data = serialize_dbr(34, &val, 0, 0, ts).unwrap();
    assert_eq!(data.len(), 88);
    assert_eq!(&data[8..16], &[0u8; 8]);
    assert_eq!(&data[80..88], &42.0f64.to_be_bytes());
}

#[test]
fn test_serialize_ctrl_long_layout() {
    let val = EpicsValue::Long(99);
    let ts = SystemTime::UNIX_EPOCH;
    let data = serialize_dbr(33, &val, 0, 0, ts).unwrap();
    assert_eq!(data.len(), 48);
    assert_eq!(&data[4..12], &[0u8; 8]);
    assert_eq!(&data[44..48], &99i32.to_be_bytes());
}

#[test]
fn test_serialize_gr_short_layout() {
    let val = EpicsValue::Short(7);
    let ts = SystemTime::UNIX_EPOCH;
    let data = serialize_dbr(22, &val, 0, 0, ts).unwrap();
    assert_eq!(data.len(), 26);
    assert_eq!(&data[24..26], &7i16.to_be_bytes());
}

#[test]
fn test_serialize_ctrl_enum_layout() {
    let val = EpicsValue::Enum(3);
    let ts = SystemTime::UNIX_EPOCH;
    let data = serialize_dbr(31, &val, 0, 0, ts).unwrap();
    assert_eq!(data.len(), 424);
    assert_eq!(&data[422..424], &3u16.to_be_bytes());
}

#[test]
fn test_serialize_ctrl_char_layout() {
    let val = EpicsValue::Char(0xAB);
    let ts = SystemTime::UNIX_EPOCH;
    let data = serialize_dbr(32, &val, 0, 0, ts).unwrap();
    assert_eq!(data.len(), 22);
    assert_eq!(data[21], 0xAB);
}

#[test]
fn test_serialize_ctrl_float_layout() {
    let val = EpicsValue::Float(1.5);
    let ts = SystemTime::UNIX_EPOCH;
    let data = serialize_dbr(30, &val, 0, 0, ts).unwrap();
    assert_eq!(data.len(), 52);
    assert_eq!(&data[48..52], &1.5f32.to_be_bytes());
}

#[test]
fn test_serialize_gr_string_falls_back_to_sts() {
    let val = EpicsValue::String("test".into());
    let ts = SystemTime::UNIX_EPOCH;
    let data = serialize_dbr(21, &val, 0, 0, ts).unwrap();
    assert_eq!(data.len(), 44);
}

// ---- Golden packet tests ----

#[test]
fn test_golden_plain_string() {
    let val = EpicsValue::String("hello".into());
    let data = serialize_dbr(0, &val, 0, 0, SystemTime::UNIX_EPOCH).unwrap();
    assert_eq!(data.len(), 40);
    assert_eq!(&data[..5], b"hello");
    assert_eq!(&data[5..], &[0u8; 35]);
}

#[test]
fn test_golden_plain_short() {
    let val = EpicsValue::Short(42);
    let data = serialize_dbr(1, &val, 0, 0, SystemTime::UNIX_EPOCH).unwrap();
    assert_eq!(data, 42i16.to_be_bytes());
}

#[test]
fn test_golden_plain_float() {
    let val = EpicsValue::Float(1.5);
    let data = serialize_dbr(2, &val, 0, 0, SystemTime::UNIX_EPOCH).unwrap();
    assert_eq!(data, 1.5f32.to_be_bytes());
}

#[test]
fn test_golden_plain_enum() {
    let val = EpicsValue::Enum(7);
    let data = serialize_dbr(3, &val, 0, 0, SystemTime::UNIX_EPOCH).unwrap();
    assert_eq!(data, 7u16.to_be_bytes());
}

#[test]
fn test_golden_plain_char() {
    let val = EpicsValue::Char(0xFF);
    let data = serialize_dbr(4, &val, 0, 0, SystemTime::UNIX_EPOCH).unwrap();
    assert_eq!(data, [0xFF]);
}

#[test]
fn test_golden_plain_long() {
    let val = EpicsValue::Long(-1000);
    let data = serialize_dbr(5, &val, 0, 0, SystemTime::UNIX_EPOCH).unwrap();
    assert_eq!(data, (-1000i32).to_be_bytes());
}

#[test]
fn test_golden_plain_double() {
    let val = EpicsValue::Double(std::f64::consts::PI);
    let data = serialize_dbr(6, &val, 0, 0, SystemTime::UNIX_EPOCH).unwrap();
    assert_eq!(data, std::f64::consts::PI.to_be_bytes());
}

#[test]
fn test_golden_sts_double() {
    let val = EpicsValue::Double(99.9);
    let data = serialize_dbr(13, &val, 3, 2, SystemTime::UNIX_EPOCH).unwrap();
    assert_eq!(data.len(), 14);
    assert_eq!(&data[0..2], &3u16.to_be_bytes());
    assert_eq!(&data[2..4], &2u16.to_be_bytes());
    assert_eq!(&data[4..6], &[0, 0]);
    assert_eq!(&data[6..14], &99.9f64.to_be_bytes());
}

#[test]
fn test_golden_sts_char() {
    let val = EpicsValue::Char(0x42);
    let data = serialize_dbr(11, &val, 1, 1, SystemTime::UNIX_EPOCH).unwrap();
    assert_eq!(data.len(), 6);
    assert_eq!(&data[0..2], &1u16.to_be_bytes());
    assert_eq!(&data[2..4], &1u16.to_be_bytes());
    assert_eq!(data[4], 0);
    assert_eq!(data[5], 0x42);
}

#[test]
fn test_golden_time_double() {
    let ts = SystemTime::UNIX_EPOCH + Duration::from_secs(EPICS_UNIX_EPOCH_OFFSET_SECS + 1000);
    let val = EpicsValue::Double(1.23);
    let data = serialize_dbr(20, &val, 0, 0, ts).unwrap();
    assert_eq!(data.len(), 24);
    assert_eq!(&data[0..2], &0u16.to_be_bytes());
    assert_eq!(&data[2..4], &0u16.to_be_bytes());
    assert_eq!(&data[4..8], &1000u32.to_be_bytes());
    assert_eq!(&data[8..12], &0u32.to_be_bytes());
    assert_eq!(&data[12..16], &[0, 0, 0, 0]);
    assert_eq!(&data[16..24], &1.23f64.to_be_bytes());
}

#[test]
fn test_golden_time_short() {
    let ts = SystemTime::UNIX_EPOCH + Duration::from_secs(EPICS_UNIX_EPOCH_OFFSET_SECS + 500);
    let val = EpicsValue::Short(777);
    let data = serialize_dbr(15, &val, 0, 0, ts).unwrap();
    assert_eq!(data.len(), 16);
    assert_eq!(&data[12..14], &[0, 0]);
    assert_eq!(&data[14..16], &777i16.to_be_bytes());
}

#[test]
fn test_golden_time_char() {
    let ts = SystemTime::UNIX_EPOCH + Duration::from_secs(EPICS_UNIX_EPOCH_OFFSET_SECS + 10);
    let val = EpicsValue::Char(0xBE);
    let data = serialize_dbr(18, &val, 0, 0, ts).unwrap();
    assert_eq!(data.len(), 16);
    assert_eq!(&data[12..15], &[0, 0, 0]);
    assert_eq!(data[15], 0xBE);
}

#[test]
fn test_golden_time_float() {
    let ts = SystemTime::UNIX_EPOCH + Duration::from_secs(EPICS_UNIX_EPOCH_OFFSET_SECS);
    let val = EpicsValue::Float(2.5);
    let data = serialize_dbr(16, &val, 0, 0, ts).unwrap();
    assert_eq!(data.len(), 16);
    assert_eq!(&data[12..16], &2.5f32.to_be_bytes());
}

#[test]
fn test_golden_time_enum() {
    let ts = SystemTime::UNIX_EPOCH + Duration::from_secs(EPICS_UNIX_EPOCH_OFFSET_SECS + 1);
    let val = EpicsValue::Enum(5);
    let data = serialize_dbr(17, &val, 0, 0, ts).unwrap();
    assert_eq!(data.len(), 16);
    assert_eq!(&data[12..14], &[0, 0]);
    assert_eq!(&data[14..16], &5u16.to_be_bytes());
}

#[test]
fn test_golden_time_string() {
    let ts = SystemTime::UNIX_EPOCH + Duration::from_secs(EPICS_UNIX_EPOCH_OFFSET_SECS + 99);
    let val = EpicsValue::String("abc".into());
    let data = serialize_dbr(14, &val, 0, 0, ts).unwrap();
    assert_eq!(data.len(), 52);
    assert_eq!(&data[12..15], b"abc");
    assert_eq!(&data[15..52], &[0u8; 37]);
}

#[test]
fn test_golden_gr_matches_time() {
    let val = EpicsValue::Double(42.0);
    let ts = SystemTime::UNIX_EPOCH;
    let gr = serialize_dbr(27, &val, 0, 0, ts).unwrap();
    let time = serialize_dbr(20, &val, 0, 0, ts).unwrap();
    assert_eq!(gr.len(), 72);
    assert_eq!(time.len(), 24);
    assert_ne!(gr, time);
    assert_eq!(&gr[4..64], &[0u8; 60]);
}

#[test]
fn test_golden_ctrl_matches_gr_pattern() {
    let val = EpicsValue::Double(42.0);
    let ts = SystemTime::UNIX_EPOCH;
    let ctrl = serialize_dbr(34, &val, 0, 0, ts).unwrap();
    let gr = serialize_dbr(27, &val, 0, 0, ts).unwrap();
    assert_eq!(ctrl.len(), gr.len() + 16);
    assert_eq!(&ctrl[0..4], &gr[0..4]);
    assert_eq!(&ctrl[4..64], &[0u8; 60]);
}

#[test]
fn test_golden_type_conversion() {
    let val = EpicsValue::Double(42.7);
    let ts = SystemTime::UNIX_EPOCH + Duration::from_secs(EPICS_UNIX_EPOCH_OFFSET_SECS);
    let data = serialize_dbr(15, &val, 0, 0, ts).unwrap();
    assert_eq!(data.len(), 16);
    assert_eq!(&data[14..16], &42i16.to_be_bytes());
}

#[test]
fn test_golden_header_read_notify() {
    use epics_ca_rs::protocol::*;
    let mut hdr = CaHeader::new(CA_PROTO_READ_NOTIFY);
    hdr.data_type = 20;
    hdr.set_payload_size(24, 1);
    hdr.cid = ECA_NORMAL;
    hdr.available = 42;

    let bytes = hdr.to_bytes_extended();
    assert_eq!(&bytes[0..2], &CA_PROTO_READ_NOTIFY.to_be_bytes());
    assert_eq!(&bytes[4..6], &20u16.to_be_bytes());
    assert_eq!(&bytes[12..16], &42u32.to_be_bytes());
}

// ---- encode_dbr tests ----

fn bare_snapshot(value: EpicsValue) -> Snapshot {
    Snapshot::new(value, 0, 0, SystemTime::UNIX_EPOCH)
}

fn full_snapshot(value: EpicsValue) -> Snapshot {
    let mut snap = Snapshot::new(value, 3, 2, SystemTime::UNIX_EPOCH);
    snap.display = Some(DisplayInfo {
        units: "degC".to_string(),
        precision: 3,
        upper_disp_limit: 100.0,
        lower_disp_limit: -50.0,
        upper_alarm_limit: 90.0,
        upper_warning_limit: 80.0,
        lower_warning_limit: -20.0,
        lower_alarm_limit: -40.0,
        ..Default::default()
    });
    snap.control = Some(ControlInfo {
        upper_ctrl_limit: 95.0,
        lower_ctrl_limit: -45.0,
    });
    snap
}

#[test]
fn test_encode_plain_matches_serialize() {
    let val = EpicsValue::Double(42.0);
    let ts = SystemTime::UNIX_EPOCH;
    let snap = bare_snapshot(val.clone());
    assert_eq!(
        encode_dbr(6, &snap).unwrap(),
        serialize_dbr(6, &val, 0, 0, ts).unwrap()
    );
}

#[test]
fn test_encode_sts_matches_serialize() {
    let val = EpicsValue::Short(77);
    let ts = SystemTime::UNIX_EPOCH;
    let mut snap = bare_snapshot(val.clone());
    snap.alarm = AlarmInfo {
        status: 5,
        severity: 1,
        ..Default::default()
    };
    assert_eq!(
        encode_dbr(8, &snap).unwrap(),
        serialize_dbr(8, &val, 5, 1, ts).unwrap()
    );
}

#[test]
fn test_encode_time_matches_serialize() {
    let val = EpicsValue::Double(1.23);
    let ts = SystemTime::UNIX_EPOCH + Duration::from_secs(EPICS_UNIX_EPOCH_OFFSET_SECS + 500);
    let mut snap = bare_snapshot(val.clone());
    snap.timestamp = ts;
    snap.alarm = AlarmInfo {
        status: 1,
        severity: 2,
        ..Default::default()
    };
    assert_eq!(
        encode_dbr(20, &snap).unwrap(),
        serialize_dbr(20, &val, 1, 2, ts).unwrap()
    );
}

#[test]
fn test_encode_gr_double_with_metadata() {
    let snap = full_snapshot(EpicsValue::Double(42.0));
    let data = encode_dbr(27, &snap).unwrap();
    assert_eq!(data.len(), 72);
    assert_eq!(&data[0..2], &3u16.to_be_bytes());
    assert_eq!(&data[2..4], &2u16.to_be_bytes());
    assert_eq!(&data[4..6], &3i16.to_be_bytes());
    assert_eq!(&data[6..8], &[0, 0]);
    assert_eq!(&data[8..12], b"degC");
    assert_eq!(&data[12..16], &[0, 0, 0, 0]);
    assert_eq!(&data[16..24], &100.0f64.to_be_bytes());
    assert_eq!(&data[24..32], &(-50.0f64).to_be_bytes());
    assert_eq!(&data[32..40], &90.0f64.to_be_bytes());
    assert_eq!(&data[40..48], &80.0f64.to_be_bytes());
    assert_eq!(&data[48..56], &(-20.0f64).to_be_bytes());
    assert_eq!(&data[56..64], &(-40.0f64).to_be_bytes());
    assert_eq!(&data[64..72], &42.0f64.to_be_bytes());
}

#[test]
fn test_encode_ctrl_double_with_metadata() {
    let snap = full_snapshot(EpicsValue::Double(42.0));
    let data = encode_dbr(34, &snap).unwrap();
    assert_eq!(data.len(), 88);
    assert_eq!(&data[64..72], &95.0f64.to_be_bytes());
    assert_eq!(&data[72..80], &(-45.0f64).to_be_bytes());
    assert_eq!(&data[80..88], &42.0f64.to_be_bytes());
}

#[test]
fn test_encode_gr_short_with_metadata() {
    let mut snap = Snapshot::new(EpicsValue::Short(42), 0, 0, SystemTime::UNIX_EPOCH);
    snap.display = Some(DisplayInfo {
        units: "mm".to_string(),
        precision: 0,
        upper_disp_limit: 1000.0,
        lower_disp_limit: -100.0,
        upper_alarm_limit: 900.0,
        upper_warning_limit: 800.0,
        lower_warning_limit: -50.0,
        lower_alarm_limit: -90.0,
        ..Default::default()
    });
    let data = encode_dbr(22, &snap).unwrap();
    assert_eq!(data.len(), 26);
    assert_eq!(&data[4..6], b"mm");
    assert_eq!(&data[12..14], &1000i16.to_be_bytes());
    assert_eq!(&data[24..26], &42i16.to_be_bytes());
}

#[test]
fn test_encode_gr_float_with_metadata() {
    let mut snap = Snapshot::new(EpicsValue::Float(1.5), 0, 0, SystemTime::UNIX_EPOCH);
    snap.display = Some(DisplayInfo {
        units: "V".to_string(),
        precision: 2,
        upper_disp_limit: 10.0,
        lower_disp_limit: 0.0,
        ..Default::default()
    });
    let data = encode_dbr(23, &snap).unwrap();
    assert_eq!(data.len(), 44);
    assert_eq!(&data[4..6], &2i16.to_be_bytes());
    assert_eq!(data[8], b'V');
    assert_eq!(&data[16..20], &10.0f32.to_be_bytes());
}

#[test]
fn test_encode_gr_long_with_metadata() {
    let mut snap = Snapshot::new(EpicsValue::Long(99), 0, 0, SystemTime::UNIX_EPOCH);
    snap.display = Some(DisplayInfo {
        units: "cnt".to_string(),
        upper_disp_limit: 10000.0,
        lower_disp_limit: 0.0,
        ..Default::default()
    });
    let data = encode_dbr(26, &snap).unwrap();
    assert_eq!(data.len(), 40);
    assert_eq!(&data[12..16], &10000i32.to_be_bytes());
    assert_eq!(&data[36..40], &99i32.to_be_bytes());
}

#[test]
fn test_encode_gr_char_with_metadata() {
    // DBF_CHAR limits are epicsInt8 (-128..127) per libca 7cb80d5a1.
    // Pre-fix: f64 → u8 saturated negatives to 0 and large positives
    // round-tripped fine but unsignedly. Post-fix: f64 → i8 → u8
    // bit-pattern preserves signed semantics. 100.0 stays 100;
    // -10.0 round-trips as 0xF6.
    let mut snap = Snapshot::new(EpicsValue::Char(42), 0, 0, SystemTime::UNIX_EPOCH);
    snap.display = Some(DisplayInfo {
        units: "raw".to_string(),
        upper_disp_limit: 100.0,
        lower_disp_limit: -10.0,
        ..Default::default()
    });
    let data = encode_dbr(25, &snap).unwrap();
    assert_eq!(data.len(), 20);
    assert_eq!(data[12], 100u8); // 100 fits i8 → 100u8.
    assert_eq!(data[13], 0xF6u8); // -10i8 bit-pattern.
    assert_eq!(data[19], 42);
}

#[test]
fn test_encode_gr_char_saturates_out_of_range() {
    // 255.0 is out of i8 range — saturates to i8::MAX (127).
    // Before P-8 fix this passed unchecked as u8(255).
    let mut snap = Snapshot::new(EpicsValue::Char(0), 0, 0, SystemTime::UNIX_EPOCH);
    snap.display = Some(DisplayInfo {
        upper_disp_limit: 255.0,
        lower_disp_limit: -200.0,
        ..Default::default()
    });
    let data = encode_dbr(25, &snap).unwrap();
    assert_eq!(data[12], 127u8); // saturated i8::MAX
    assert_eq!(data[13], 0x80u8); // saturated i8::MIN bit-pattern
}

#[test]
fn test_encode_gr_enum_with_strings() {
    let mut snap = Snapshot::new(EpicsValue::Enum(1), 0, 0, SystemTime::UNIX_EPOCH);
    snap.enums = Some(EnumInfo {
        strings: vec!["Off".to_string(), "On".to_string()],
    });
    let data = encode_dbr(24, &snap).unwrap();
    assert_eq!(data.len(), 424);
    assert_eq!(&data[4..6], &2u16.to_be_bytes());
    assert_eq!(&data[6..9], b"Off");
    assert_eq!(data[9], 0);
    assert_eq!(&data[32..34], b"On");
    assert_eq!(&data[422..424], &1u16.to_be_bytes());
}

#[test]
fn test_encode_gr_none_metadata_matches_serialize() {
    let val = EpicsValue::Double(42.0);
    let snap = bare_snapshot(val.clone());
    let encoded = encode_dbr(27, &snap).unwrap();
    let legacy = serialize_dbr(27, &val, 0, 0, SystemTime::UNIX_EPOCH).unwrap();
    assert_eq!(encoded, legacy);
}

#[test]
fn test_encode_ctrl_none_metadata_matches_serialize() {
    let val = EpicsValue::Long(99);
    let snap = bare_snapshot(val.clone());
    let encoded = encode_dbr(33, &snap).unwrap();
    let legacy = serialize_dbr(33, &val, 0, 0, SystemTime::UNIX_EPOCH).unwrap();
    assert_eq!(encoded, legacy);
}

#[test]
fn test_encode_ctrl_short_with_ctrl_limits() {
    let mut snap = Snapshot::new(EpicsValue::Short(10), 0, 0, SystemTime::UNIX_EPOCH);
    snap.display = Some(DisplayInfo {
        units: "mA".to_string(),
        upper_disp_limit: 100.0,
        lower_disp_limit: 0.0,
        ..Default::default()
    });
    snap.control = Some(ControlInfo {
        upper_ctrl_limit: 80.0,
        lower_ctrl_limit: 5.0,
    });
    let data = encode_dbr(29, &snap).unwrap();
    assert_eq!(data.len(), 30);
    assert_eq!(&data[12..14], &100i16.to_be_bytes());
    assert_eq!(&data[24..26], &80i16.to_be_bytes());
    assert_eq!(&data[26..28], &5i16.to_be_bytes());
    assert_eq!(&data[28..30], &10i16.to_be_bytes());
}

#[test]
fn test_encode_invalid_type() {
    let snap = bare_snapshot(EpicsValue::Double(0.0));
    // 35..=38 are PUT_ACKT/PUT_ACKS/STSACK_STRING/CLASS_NAME (valid).
    // Anything beyond 38 is unallocated.
    assert!(encode_dbr(39, &snap).is_err());
    assert!(encode_dbr(100, &snap).is_err());
}

#[test]
fn test_encode_stsack_string_layout() {
    // STSACK_STRING wire layout: status(2) severity(2) ackt(2) acks(2) value(40) = 48 bytes
    let mut snap = bare_snapshot(EpicsValue::String("warn".to_string()));
    snap.alarm = AlarmInfo {
        status: 5,
        severity: 1,
        ackt: Some(1),
        acks: Some(2),
    };
    let data = encode_dbr(epics_base_rs::types::DBR_STSACK_STRING, &snap).unwrap();
    assert_eq!(data.len(), 48);
    assert_eq!(&data[0..2], &5u16.to_be_bytes());
    assert_eq!(&data[2..4], &1u16.to_be_bytes());
    assert_eq!(&data[4..6], &1u16.to_be_bytes());
    assert_eq!(&data[6..8], &2u16.to_be_bytes());
    assert_eq!(&data[8..12], b"warn");
}

#[test]
fn test_encode_stsack_string_default_ackt_acks() {
    // ackt/acks default to None → encoded as zero so SimplePvs without
    // record-backed alarm fields still produce a valid response.
    let snap = bare_snapshot(EpicsValue::String("ok".to_string()));
    let data = encode_dbr(epics_base_rs::types::DBR_STSACK_STRING, &snap).unwrap();
    assert_eq!(data.len(), 48);
    assert_eq!(&data[4..6], &0u16.to_be_bytes());
    assert_eq!(&data[6..8], &0u16.to_be_bytes());
}

#[test]
fn test_parse_menu_string_alarm_sevr() {
    assert_eq!(
        EpicsValue::parse(DbFieldType::Short, "NO_ALARM").unwrap(),
        EpicsValue::Short(0)
    );
    assert_eq!(
        EpicsValue::parse(DbFieldType::Short, "MINOR").unwrap(),
        EpicsValue::Short(1)
    );
    assert_eq!(
        EpicsValue::parse(DbFieldType::Short, "MAJOR").unwrap(),
        EpicsValue::Short(2)
    );
    assert_eq!(
        EpicsValue::parse(DbFieldType::Short, "INVALID").unwrap(),
        EpicsValue::Short(3)
    );
}

#[test]
fn test_parse_menu_string_omsl() {
    assert_eq!(
        EpicsValue::parse(DbFieldType::Short, "supervisory").unwrap(),
        EpicsValue::Short(0)
    );
    assert_eq!(
        EpicsValue::parse(DbFieldType::Short, "closed_loop").unwrap(),
        EpicsValue::Short(1)
    );
}

#[test]
fn test_parse_menu_string_enum_type() {
    assert_eq!(
        EpicsValue::parse(DbFieldType::Enum, "NO_ALARM").unwrap(),
        EpicsValue::Enum(0)
    );
    assert_eq!(
        EpicsValue::parse(DbFieldType::Enum, "MAJOR").unwrap(),
        EpicsValue::Enum(2)
    );
}

#[test]
fn test_parse_menu_string_numeric_priority() {
    assert_eq!(
        EpicsValue::parse(DbFieldType::Short, "0").unwrap(),
        EpicsValue::Short(0)
    );
    assert_eq!(
        EpicsValue::parse(DbFieldType::Short, "42").unwrap(),
        EpicsValue::Short(42)
    );
    assert_eq!(
        EpicsValue::parse(DbFieldType::Enum, "3").unwrap(),
        EpicsValue::Enum(3)
    );
}

#[test]
fn test_parse_menu_string_unknown() {
    assert!(EpicsValue::parse(DbFieldType::Short, "UNKNOWN_MENU").is_err());
    assert!(EpicsValue::parse(DbFieldType::Enum, "UNKNOWN_MENU").is_err());
}

// ---- decode_dbr roundtrip tests ----

#[test]
fn test_decode_plain_double() {
    let data = 42.0f64.to_be_bytes();
    let snap = decode_dbr(6, &data, 1).unwrap();
    assert_eq!(snap.value, EpicsValue::Double(42.0));
    assert_eq!(snap.alarm.status, 0);
}

#[test]
fn test_decode_sts_double_roundtrip() {
    let val = EpicsValue::Double(99.9);
    let data = serialize_dbr(13, &val, 3, 2, SystemTime::UNIX_EPOCH).unwrap();
    let snap = decode_dbr(13, &data, 1).unwrap();
    assert_eq!(snap.value, EpicsValue::Double(99.9));
    assert_eq!(snap.alarm.status, 3);
    assert_eq!(snap.alarm.severity, 2);
}

#[test]
fn test_decode_time_double_roundtrip() {
    let ts = SystemTime::UNIX_EPOCH + Duration::from_secs(EPICS_UNIX_EPOCH_OFFSET_SECS + 1000);
    let val = EpicsValue::Double(1.23);
    let data = serialize_dbr(20, &val, 5, 1, ts).unwrap();
    let snap = decode_dbr(20, &data, 1).unwrap();
    assert_eq!(snap.value, EpicsValue::Double(1.23));
    assert_eq!(snap.alarm.status, 5);
    assert_eq!(snap.alarm.severity, 1);
    let orig_secs = ts.duration_since(SystemTime::UNIX_EPOCH).unwrap().as_secs();
    let decoded_secs = snap
        .timestamp
        .duration_since(SystemTime::UNIX_EPOCH)
        .unwrap()
        .as_secs();
    assert_eq!(orig_secs, decoded_secs);
}

#[test]
fn test_decode_time_short_roundtrip() {
    let ts = SystemTime::UNIX_EPOCH + Duration::from_secs(EPICS_UNIX_EPOCH_OFFSET_SECS + 500);
    let val = EpicsValue::Short(777);
    let data = serialize_dbr(15, &val, 0, 0, ts).unwrap();
    let snap = decode_dbr(15, &data, 1).unwrap();
    assert_eq!(snap.value, EpicsValue::Short(777));
}

#[test]
fn test_decode_time_char_roundtrip() {
    let ts = SystemTime::UNIX_EPOCH + Duration::from_secs(EPICS_UNIX_EPOCH_OFFSET_SECS + 10);
    let val = EpicsValue::Char(0xBE);
    let data = serialize_dbr(18, &val, 0, 0, ts).unwrap();
    let snap = decode_dbr(18, &data, 1).unwrap();
    assert_eq!(snap.value, EpicsValue::Char(0xBE));
}

#[test]
fn test_decode_time_float_roundtrip() {
    let ts = SystemTime::UNIX_EPOCH + Duration::from_secs(EPICS_UNIX_EPOCH_OFFSET_SECS);
    let val = EpicsValue::Float(2.5);
    let data = serialize_dbr(16, &val, 0, 0, ts).unwrap();
    let snap = decode_dbr(16, &data, 1).unwrap();
    assert_eq!(snap.value, EpicsValue::Float(2.5));
}

#[test]
fn test_decode_time_enum_roundtrip() {
    let ts = SystemTime::UNIX_EPOCH + Duration::from_secs(EPICS_UNIX_EPOCH_OFFSET_SECS + 1);
    let val = EpicsValue::Enum(5);
    let data = serialize_dbr(17, &val, 0, 0, ts).unwrap();
    let snap = decode_dbr(17, &data, 1).unwrap();
    assert_eq!(snap.value, EpicsValue::Enum(5));
}

#[test]
fn test_decode_time_string_roundtrip() {
    let ts = SystemTime::UNIX_EPOCH + Duration::from_secs(EPICS_UNIX_EPOCH_OFFSET_SECS + 99);
    let val = EpicsValue::String("abc".into());
    let data = serialize_dbr(14, &val, 0, 0, ts).unwrap();
    let snap = decode_dbr(14, &data, 1).unwrap();
    assert_eq!(snap.value, EpicsValue::String("abc".into()));
}

#[test]
fn test_decode_ctrl_double_roundtrip() {
    let snap_orig = full_snapshot(EpicsValue::Double(42.0));
    let data = encode_dbr(34, &snap_orig).unwrap();
    let snap = decode_dbr(34, &data, 1).unwrap();
    assert_eq!(snap.value, EpicsValue::Double(42.0));
    assert_eq!(snap.alarm.status, 3);
    assert_eq!(snap.alarm.severity, 2);
    let disp = snap.display.unwrap();
    assert_eq!(disp.units, "degC");
    assert_eq!(disp.precision, 3);
    assert_eq!(disp.upper_disp_limit, 100.0);
    assert_eq!(disp.lower_disp_limit, -50.0);
    assert_eq!(disp.upper_alarm_limit, 90.0);
    assert_eq!(disp.upper_warning_limit, 80.0);
    assert_eq!(disp.lower_warning_limit, -20.0);
    assert_eq!(disp.lower_alarm_limit, -40.0);
    let ctrl = snap.control.unwrap();
    assert_eq!(ctrl.upper_ctrl_limit, 95.0);
    assert_eq!(ctrl.lower_ctrl_limit, -45.0);
}

#[test]
fn test_decode_ctrl_float_roundtrip() {
    let snap_orig = full_snapshot(EpicsValue::Float(1.5));
    let data = encode_dbr(30, &snap_orig).unwrap();
    let snap = decode_dbr(30, &data, 1).unwrap();
    assert_eq!(snap.value, EpicsValue::Float(1.5));
    let disp = snap.display.unwrap();
    assert_eq!(disp.units, "degC");
    assert_eq!(disp.precision, 3);
    assert!((disp.upper_disp_limit - 100.0).abs() < 0.01);
    let ctrl = snap.control.unwrap();
    assert!((ctrl.upper_ctrl_limit - 95.0).abs() < 0.01);
}

#[test]
fn test_decode_ctrl_long_roundtrip() {
    let snap_orig = full_snapshot(EpicsValue::Long(99));
    let data = encode_dbr(33, &snap_orig).unwrap();
    let snap = decode_dbr(33, &data, 1).unwrap();
    assert_eq!(snap.value, EpicsValue::Long(99));
    let disp = snap.display.unwrap();
    assert_eq!(disp.units, "degC");
    assert_eq!(disp.upper_disp_limit, 100.0);
    assert_eq!(disp.lower_disp_limit, -50.0);
    let ctrl = snap.control.unwrap();
    assert_eq!(ctrl.upper_ctrl_limit, 95.0);
    assert_eq!(ctrl.lower_ctrl_limit, -45.0);
}

#[test]
fn test_decode_ctrl_short_roundtrip() {
    let snap_orig = full_snapshot(EpicsValue::Short(7));
    let data = encode_dbr(29, &snap_orig).unwrap();
    let snap = decode_dbr(29, &data, 1).unwrap();
    assert_eq!(snap.value, EpicsValue::Short(7));
    let disp = snap.display.unwrap();
    assert_eq!(disp.units, "degC");
}

#[test]
fn test_decode_ctrl_char_roundtrip() {
    let snap_orig = full_snapshot(EpicsValue::Char(0xAB));
    let data = encode_dbr(32, &snap_orig).unwrap();
    let snap = decode_dbr(32, &data, 1).unwrap();
    assert_eq!(snap.value, EpicsValue::Char(0xAB));
    let disp = snap.display.unwrap();
    assert_eq!(disp.units, "degC");
}

#[test]
fn test_decode_ctrl_enum_roundtrip() {
    let mut snap_orig = full_snapshot(EpicsValue::Enum(2));
    snap_orig.enums = Some(EnumInfo {
        strings: vec!["Off".into(), "On".into(), "Reset".into()],
    });
    let data = encode_dbr(31, &snap_orig).unwrap();
    let snap = decode_dbr(31, &data, 1).unwrap();
    assert_eq!(snap.value, EpicsValue::Enum(2));
    let ei = snap.enums.unwrap();
    assert_eq!(ei.strings.len(), 3);
    assert_eq!(ei.strings[0], "Off");
    assert_eq!(ei.strings[1], "On");
    assert_eq!(ei.strings[2], "Reset");
}

#[test]
fn test_decode_gr_double_roundtrip() {
    let snap_orig = full_snapshot(EpicsValue::Double(3.15));
    let data = encode_dbr(27, &snap_orig).unwrap();
    let snap = decode_dbr(27, &data, 1).unwrap();
    assert_eq!(snap.value, EpicsValue::Double(3.15));
    let disp = snap.display.unwrap();
    assert_eq!(disp.units, "degC");
    assert_eq!(disp.precision, 3);
    assert_eq!(disp.upper_disp_limit, 100.0);
    assert!(snap.control.is_none());
}

#[test]
fn test_dbr_type_helpers() {
    assert_eq!(DbFieldType::Double.time_dbr_type(), 20);
    assert_eq!(DbFieldType::Short.time_dbr_type(), 15);
    assert_eq!(DbFieldType::Double.ctrl_dbr_type(), 34);
    assert_eq!(DbFieldType::Long.ctrl_dbr_type(), 33);
    assert_eq!(DbFieldType::String.time_dbr_type(), 14);
    assert_eq!(DbFieldType::Char.ctrl_dbr_type(), 32);
    // CA-261/263: STS / GR helpers (added with per-variant constants)
    assert_eq!(DbFieldType::String.sts_dbr_type(), 7);
    assert_eq!(DbFieldType::Double.sts_dbr_type(), 13);
    assert_eq!(DbFieldType::String.gr_dbr_type(), 21);
    assert_eq!(DbFieldType::Double.gr_dbr_type(), 27);
    // Int64 has no CA wire type — collapses to Double in every layer
    assert_eq!(DbFieldType::Int64.sts_dbr_type(), 13);
    assert_eq!(DbFieldType::Int64.time_dbr_type(), 20);
    assert_eq!(DbFieldType::Int64.gr_dbr_type(), 27);
    assert_eq!(DbFieldType::Int64.ctrl_dbr_type(), 34);
}

#[test]
fn test_dbr_per_variant_constants_match_layer_arithmetic() {
    use epics_base_rs::types::{
        DBR_CHAR, DBR_CTRL_CHAR, DBR_CTRL_DOUBLE, DBR_CTRL_ENUM, DBR_CTRL_FLOAT, DBR_CTRL_LONG,
        DBR_CTRL_SHORT, DBR_CTRL_STRING, DBR_DOUBLE, DBR_ENUM, DBR_FLOAT, DBR_GR_CHAR,
        DBR_GR_DOUBLE, DBR_GR_ENUM, DBR_GR_FLOAT, DBR_GR_LONG, DBR_GR_SHORT, DBR_GR_STRING,
        DBR_INT, DBR_LONG, DBR_SHORT, DBR_STRING, DBR_STS_CHAR, DBR_STS_DOUBLE, DBR_STS_ENUM,
        DBR_STS_FLOAT, DBR_STS_LONG, DBR_STS_SHORT, DBR_STS_STRING,
    };
    // Native (CA-260)
    assert_eq!(DBR_STRING, 0);
    assert_eq!(DBR_DOUBLE, 6);
    // Aliases
    assert_eq!(DBR_INT, DBR_SHORT);
    // STS
    assert_eq!(DBR_STS_STRING, 7);
    assert_eq!(DBR_STS_SHORT, 8);
    assert_eq!(DBR_STS_FLOAT, 9);
    assert_eq!(DBR_STS_ENUM, 10);
    assert_eq!(DBR_STS_CHAR, 11);
    assert_eq!(DBR_STS_LONG, 12);
    assert_eq!(DBR_STS_DOUBLE, 13);
    // GR (CA-263) — main gap closure
    assert_eq!(DBR_GR_STRING, 21);
    assert_eq!(DBR_GR_SHORT, 22);
    assert_eq!(DBR_GR_FLOAT, 23);
    assert_eq!(DBR_GR_ENUM, 24);
    assert_eq!(DBR_GR_CHAR, 25);
    assert_eq!(DBR_GR_LONG, 26);
    assert_eq!(DBR_GR_DOUBLE, 27);
    // CTRL (CA-264) — main gap closure
    assert_eq!(DBR_CTRL_STRING, 28);
    assert_eq!(DBR_CTRL_SHORT, 29);
    assert_eq!(DBR_CTRL_FLOAT, 30);
    assert_eq!(DBR_CTRL_ENUM, 31);
    assert_eq!(DBR_CTRL_CHAR, 32);
    assert_eq!(DBR_CTRL_LONG, 33);
    assert_eq!(DBR_CTRL_DOUBLE, 34);
    // Round-trip: each per-variant constant matches the helper output
    for ft in [
        DbFieldType::String,
        DbFieldType::Short,
        DbFieldType::Float,
        DbFieldType::Enum,
        DbFieldType::Char,
        DbFieldType::Long,
        DbFieldType::Double,
    ] {
        assert_eq!(ft.sts_dbr_type(), ft as u16 + 7);
        assert_eq!(ft.time_dbr_type(), ft as u16 + 14);
        assert_eq!(ft.gr_dbr_type(), ft as u16 + 21);
        assert_eq!(ft.ctrl_dbr_type(), ft as u16 + 28);
    }
}

#[test]
fn test_dbr_class_name_round_trip() {
    use epics_base_rs::types::{DBR_CLASS_NAME, decode_dbr};
    let mut snap = bare_snapshot(EpicsValue::String(String::new()));
    snap.class_name = Some("ai".to_string());
    let data = encode_dbr(DBR_CLASS_NAME, &snap).unwrap();
    // Always 40 bytes regardless of the actual string length
    assert_eq!(data.len(), 40);
    // Wire form is null-padded "ai\0\0…"
    assert_eq!(&data[..2], b"ai");
    assert!(data[2..].iter().all(|&b| b == 0));

    let decoded = decode_dbr(DBR_CLASS_NAME, &data, 1).unwrap();
    assert_eq!(decoded.class_name.as_deref(), Some("ai"));
}

#[test]
fn test_dbr_class_name_truncates_long_record_type() {
    use epics_base_rs::types::DBR_CLASS_NAME;
    let mut snap = bare_snapshot(EpicsValue::String(String::new()));
    // 50 chars; 40-byte wire layout truncates to 39 + NUL
    snap.class_name = Some("a".repeat(50));
    let data = encode_dbr(DBR_CLASS_NAME, &snap).unwrap();
    assert_eq!(data.len(), 40);
    assert_eq!(&data[..39], &b"a".repeat(39)[..]);
    assert_eq!(data[39], 0); // last byte is NUL terminator
}

#[test]
fn test_dbr_class_name_empty_when_unpopulated() {
    use epics_base_rs::types::DBR_CLASS_NAME;
    let snap = bare_snapshot(EpicsValue::String(String::new()));
    // class_name is None — server emits an all-zero (empty) response,
    // matching what C IOC does for non-record-backed channels.
    let data = encode_dbr(DBR_CLASS_NAME, &snap).unwrap();
    assert_eq!(data.len(), 40);
    assert!(data.iter().all(|&b| b == 0));
}

// ── P-1 (BUG_ARCHAEOLOGY libca 8cc20393f / a7bf59079): empty-array
// COUNT=0 round-trip. Pre-fix the `count <= 1` short-circuit raised
// CaError::Protocol("...too small") on GET and silently degraded
// array WRITE to a scalar PUT.

#[test]
fn p1_empty_array_double_decodes_as_empty_doublearray() {
    let v = EpicsValue::from_bytes_array(DbFieldType::Double, &[], 0).unwrap();
    assert_eq!(v, EpicsValue::DoubleArray(vec![]));
    assert_eq!(v.count(), 0);
}

#[test]
fn p1_count_one_double_falls_through_to_scalar() {
    // count == 1 still routes through the scalar decoder (the
    // legitimate "scalar shaped as array of one" CA case).
    let bytes = 7.5_f64.to_be_bytes();
    let v = EpicsValue::from_bytes_array(DbFieldType::Double, &bytes, 1).unwrap();
    assert_eq!(v, EpicsValue::Double(7.5));
}

#[test]
fn p1_empty_array_string_decodes_as_empty_stringarray() {
    // DBR_STRING uses 40-byte fixed-width slots — count=0 means no
    // bytes consumed; the decoder must produce StringArray(vec![])
    // not error on zero-length input.
    let v = EpicsValue::from_bytes_array(DbFieldType::String, &[], 0).unwrap();
    assert_eq!(v, EpicsValue::StringArray(vec![]));
    assert_eq!(v.count(), 0);
}

#[test]
fn p1_empty_array_all_dbr_types_round_trip() {
    // Every DBR type's count=0 path must produce its typed empty
    // variant. Catches future variants added without an arm in
    // from_bytes_array's count=0 dispatch.
    let cases: &[(DbFieldType, EpicsValue)] = &[
        (DbFieldType::Short, EpicsValue::ShortArray(vec![])),
        (DbFieldType::Float, EpicsValue::FloatArray(vec![])),
        (DbFieldType::Enum, EpicsValue::EnumArray(vec![])),
        (DbFieldType::Char, EpicsValue::CharArray(vec![])),
        (DbFieldType::Long, EpicsValue::LongArray(vec![])),
        (DbFieldType::Double, EpicsValue::DoubleArray(vec![])),
        (DbFieldType::String, EpicsValue::StringArray(vec![])),
    ];
    for (t, expected) in cases {
        let v = EpicsValue::from_bytes_array(*t, &[], 0).unwrap();
        assert_eq!(&v, expected, "DBF type {t:?}");
        // to_bytes round-trip: empty array encodes to empty payload,
        // which decodes back to the same empty array variant.
        let bytes = v.to_bytes();
        assert_eq!(bytes.len(), 0, "encoded length for empty {t:?}");
        let back = EpicsValue::from_bytes_array(*t, &bytes, 0).unwrap();
        assert_eq!(back, *expected, "round-trip {t:?}");
    }
}
