// Conveniently, that *is* available when tests are run
extern crate alloc;

use crate::inmemory::*;

fn test_read(message: &impl coap_message::ReadableMessage<Code = u8>) {
    use coap_message::*;

    assert!(message.code() == 0x45);

    let expected_options = [
        (11, b"short".to_vec()),
        (11, b"very-long-option".to_vec()),
        (65000, b"".to_vec()),
    ];
    let found_options: alloc::vec::Vec<_> = message
        .options()
        .map(|o| (o.number(), o.value().to_vec()))
        .collect();

    assert!(found_options == expected_options);

    assert!(message.payload() == b"payload");
}

#[test]
fn test_inmemory() {
    let local_body =
        alloc::boxed::Box::new(b"\xb5short\r\x03very-long-option\xe0\xfc\xd0\xffpayload");

    let message = Message::new_from_slice(0x45, local_body.as_slice());

    test_read(&message);
}

#[test]
fn test_inmemory_truncated() {
    // 4 trailing NULs to be disregarded
    let local_body =
        alloc::boxed::Box::new(b"\xb5short\r\x03very-long-option\xe0\xfc\xd0\xffpayload\0\0\0\0");

    let message = Message::new_until(SliceBuffer::new(0x45, *local_body), local_body.len() - 4);

    test_read(&message);
}

#[test]
#[cfg(feature = "downcast")]
fn test_inmemory_downcast() {
    let local_body =
        alloc::boxed::Box::new(b"\xb5short\r\x03very-long-option\xe0\xfc\xd0\xffpayload");

    let message = Message::new_from_slice(0x45, local_body.as_slice());

    fn hide_type<'a>(
        message: &'a impl coap_message::ReadableMessage,
    ) -> &'a (impl coap_message::ReadableMessage + 'a) {
        message
    }

    let hidden = hide_type(&message);

    let message_again = Message::<SliceBuffer<'static>>::downcast_from(hidden).unwrap();

    test_read(message_again);
}

#[test]
fn test_inmemory_write() {
    use coap_message::*;

    let mut code = 0;
    let mut buffer = [0; 100];
    let mut message = MessageMut::new_in_slice(&mut code, &mut buffer);

    message.set_code(0x45);

    // These asserts ensure that the writable message is usable through ReadableMessage too
    assert!(
        message.options().next().is_none(),
        "Nascent message returned options"
    );
    assert!(
        message.payload().is_empty(),
        "Nascent message returned payload"
    );

    message.add_option(11, b"-----").unwrap();
    message.add_option(11, b"very-long-option").unwrap();
    message.add_option(65000, b"").unwrap();

    assert_eq!(
        message.options().count(),
        3,
        "Written options are not read back"
    );

    message.set_payload(b"payload123").unwrap();

    assert_eq!(
        message.payload(),
        b"payload123",
        "Written payload is not read back"
    );

    message.truncate(7).unwrap();

    assert_eq!(
        message.payload(),
        b"payload",
        "Truncated payload is not read back"
    );

    message.mutate_options(|n, v| {
        if n == 11 && v.len() == 5 {
            v.copy_from_slice(b"short");
        }
    });

    let (len, _) = message.finish();

    // These asserts ensure that the writable message actually serializes correctly into the buffer
    assert!(code == 0x45);
    assert!(&buffer[..len] == b"\xb5short\r\x03very-long-option\xe0\xfc\xd0\xffpayload");
}

#[test]
fn test_insert_option_between() {
    use coap_message::*;

    let mut code = 0;
    let mut buffer = [0; 100];
    let mut message = MessageMut::new_in_slice(&mut code, &mut buffer);

    message.set_code(0x45);

    message.add_option(11, b"-----").unwrap();
    message.add_option(65000, b"").unwrap();

    message.set_payload(b"payload123").unwrap();

    assert_eq!(
        message.payload(),
        b"payload123",
        "Written payload is not read back"
    );

    message.insert_option(11, b"very-long-option").unwrap();

    assert_eq!(
        message.options().count(),
        3,
        "Written options are not read back"
    );

    assert_eq!(
        message.payload(),
        b"payload123",
        "Payload is destroyed by option insertion"
    );

    message.truncate(7).unwrap();

    assert_eq!(
        message.payload(),
        b"payload",
        "Truncated payload is not read back"
    );

    message.mutate_options(|n, v| {
        if n == 11 && v.len() == 5 {
            v.copy_from_slice(b"short");
        }
    });

    let (len, _) = message.finish();

    // These asserts ensure that the writable message actually serializes correctly into the buffer
    assert_eq!(code, 0x45);
    assert_eq!(
        &buffer[..len],
        b"\xb5short\r\x03very-long-option\xe0\xfc\xd0\xffpayload"
    );
}

#[test]
fn test_insert_option_last() {
    use coap_message::*;

    // Test insert last.
    let mut code = 0;
    let mut buffer = [0; 100];
    let mut message = MessageMut::new_in_slice(&mut code, &mut buffer);

    message.set_code(0x45);

    message.insert_option(11, b"-----").unwrap();
    message.insert_option(11, b"very-long-option").unwrap();
    message.insert_option(65000, b"").unwrap();

    message.set_payload(b"payload123").unwrap();

    let (len, _) = message.finish();

    // These asserts ensure that the writable message actually serializes correctly into the buffer
    assert_eq!(code, 0x45);
    assert_eq!(
        &buffer[..len],
        b"\xb5-----\r\x03very-long-option\xe0\xfc\xd0\xffpayload123"
    );
}

#[test]
fn test_insert_option_encoding_length_changed() {
    use coap_message::*;

    // Test insert last.
    let mut code = 0;
    let mut buffer = [0; 100];
    let mut message = MessageMut::new_in_slice(&mut code, &mut buffer);

    message.set_code(0x45);

    // First encoding will be b"\xe0\xfc\xdb" (length 3).
    // Changes after insert to b"\x10" (length 1).
    message.add_option(65000, b"").unwrap();
    message.insert_option(65000 - 1, b"").unwrap();

    let (len, _) = message.finish();

    // These asserts ensure that the writable message actually serializes correctly into the buffer
    assert_eq!(code, 0x45);
    assert_eq!(&buffer[..len], b"\xe0\xfc\xda\x10");
}

#[test]
fn test_retain_options() {
    use coap_message::*;

    // Test insert last.
    let mut code = 0;
    let mut buffer = [0; 100];
    let mut message = MessageMut::new_in_slice(&mut code, &mut buffer);

    message.set_code(0x45);

    message.insert_option(10, b"-----").unwrap();
    message.insert_option(10, b"+++++").unwrap();
    message.insert_option(11, b"very-long-option").unwrap();
    message.insert_option(65000, b"").unwrap();

    message.set_payload(b"payload123").unwrap();

    let p = |number, data: &[u8]| number == 10 && data == b"-----" || number == 65000;
    assert!(p(65000, b""));

    message.retain_options(|number, data| number == 10 && data == b"-----" || number == 65000);

    let (len, _) = message.finish();

    // These asserts ensure that the writable message actually serializes correctly into the buffer
    assert_eq!(code, 0x45);
    assert_eq!(&buffer[..len], b"\xa5-----\xe0\xfc\xd1\xffpayload123");
}

#[test]
fn test_retain_options_last() {
    use coap_message::*;

    // Test insert last.
    let mut code = 0;
    let mut buffer = [0; 100];
    let mut message = MessageMut::new_in_slice(&mut code, &mut buffer);

    message.set_code(0x45);

    message.insert_option(11, b"-----").unwrap();
    message.insert_option(11, b"very-long-option").unwrap();
    message.insert_option(65000, b"").unwrap();

    message.set_payload(b"payload123").unwrap();

    message.retain_options(|number, _| number != 65000);

    // Check if `self.latest` has been updated.
    assert!(message.add_option(10, b"").is_err());
    assert!(message.add_option(11, b"").is_ok());

    let (len, _) = message.finish();

    // These asserts ensure that the writable message actually serializes correctly into the buffer
    assert_eq!(code, 0x45);
    assert_eq!(
        &buffer[..len],
        b"\xb5-----\r\x03very-long-option\x00\xffpayload123"
    );
}

#[test]
fn test_retain_options_last_nopayload() {
    use coap_message::*;

    let mut code = 0;
    let mut buffer = [0; 100];
    let mut message = MessageMut::new_in_slice(&mut code, &mut buffer);

    message.set_code(0x45);

    message.insert_option(11, b"-----").unwrap();
    message.insert_option(11, b"very-long-option").unwrap();
    message.insert_option(65000, b"").unwrap();

    message.retain_options(|number, _| number != 65000);

    // Check if `self.latest` has been updated.
    assert!(message.add_option(10, b"").is_err());
    assert!(message.add_option(11, b"").is_ok());

    let (len, _) = message.finish();

    // These asserts ensure that the writable message actually serializes correctly into the buffer
    assert_eq!(code, 0x45);
    assert_eq!(&buffer[..len], b"\xb5-----\r\x03very-long-option\x00");
}

#[test]
fn test_retain_options_empty() {
    use coap_message::*;

    // Test insert last.
    let mut code = 0;
    let mut buffer = [0; 100];
    let mut message = MessageMut::new_in_slice(&mut code, &mut buffer);

    message.set_code(0x45);

    message.retain_options(|_, _| panic!("Got unexpected options!"));

    let _ = message.finish();

    assert_eq!([0; 100], buffer);
}

#[test]
fn test_inmemory_pushout() {
    use coap_message::MinimalWritableMessage as _;
    use coap_message::MutableWritableMessage as _;
    use coap_message::ReadableMessage as _;

    let mut code = 0;
    let mut tail = [0u8; 5];
    let mut message = MessageMut::new_in_slice(&mut code, &mut tail);

    message.set_payload(&[1, 2, 3, 4]).unwrap();
    // This would shift out everything but we shouldn't even be able to shift out anything
    message.add_option(1, &[1, 2, 3, 4]).unwrap_err();
    message.add_option(1, &[]).unwrap_err();
    message.truncate(3).unwrap();
    message.add_option(1, &[]).unwrap();

    assert_eq!(message.payload(), &[1, 2, 3]);
    assert_eq!(message.finish().0, 5);
}

#[test]
#[cfg(feature = "downcast")]
fn test_inmemory_write_downcast() {
    use coap_message::*;

    let mut code = 0;
    let mut buffer = [0; 100];
    let mut message = MessageMut::new_in_slice(&mut code, &mut buffer);

    fn hide_type_mut(
        message: &mut impl coap_message::MinimalWritableMessage,
    ) -> &mut impl coap_message::MinimalWritableMessage {
        message
    }

    let hidden = hide_type_mut(&mut message);

    #[cfg(feature = "alloc")]
    assert!(MessageMut::<BoxBuffer>::downcast_from(hidden).is_none());

    let concrete_again = MessageMut::<SliceBufferMut<'static>>::downcast_from(hidden).unwrap();

    concrete_again.set_code(0x45);

    concrete_again.add_option(11, b"-----").unwrap();
    concrete_again.add_option(11, b"very-long-option").unwrap();

    message.add_option(65000, b"").unwrap();
    message.set_payload(b"payload123").unwrap();
    message.truncate(7).unwrap();
    message.mutate_options(|n, v| {
        if n == 11 && v.len() == 5 {
            v.copy_from_slice(b"short");
        }
    });

    let (len, _) = message.finish();

    assert!(code == 0x45);
    assert!(&buffer[..len] == b"\xb5short\r\x03very-long-option\xe0\xfc\xd0\xffpayload");
}

#[test]
fn test_inmemory_mutable() {
    use coap_message::MutableWritableMessage;

    let mut code = 0x45;
    let mut buffer = *b"\xb5short\r\x03very-long-option\xe0\xfc\xd0\xffpayload";

    let mut message = MessageMut::new_from_encoded_slice(&mut code, &mut buffer).unwrap();

    test_read(&message);
    assert!(message.payload_mut_with_len(7).unwrap() == b"payload");
    message.payload_mut_with_len(7).unwrap()[6] = b'D';
    assert!(&buffer[buffer.len() - 1..] == b"D");
}

#[test]
fn test_inmemory_write_untruncate() {
    use coap_message::*;

    const BUFF_LEN: usize = 100;
    let mut code = 0;
    let mut buffer = [0; BUFF_LEN];
    let mut message = MessageMut::new_in_slice(&mut code, &mut buffer);

    message.set_code(0x45);

    message.add_option(11, b"-----").unwrap();
    message.add_option(11, b"very-long-option").unwrap();
    message.add_option(65000, b"").unwrap();

    let payload = b"payload123";
    message.set_payload(payload).unwrap();

    assert_eq!(message.payload().len(), payload.len());

    let initial_len = BUFF_LEN - message.available_space() + payload.len() + 1;

    // Still can't go beyond the buffer length.
    let too_long = message.untruncate(BUFF_LEN - initial_len + 1);
    assert!(too_long.is_err());

    // But can use the entire tail.
    message.untruncate(BUFF_LEN - initial_len).unwrap();
    assert_eq!(
        message.payload().len(),
        BUFF_LEN - initial_len + payload.len()
    );

    // Truncation works as usual.
    message.truncate(payload.len() - 3).unwrap();
    assert_eq!(message.payload().len(), payload.len() - 3);

    // `untruncate()` can of course be called after truncate.
    message
        .untruncate(payload.len() + 10 - message.payload().len())
        .unwrap();
    assert_eq!(message.payload().len(), payload.len() + 10);

    let _ = message.finish();

    // These asserts ensure that the writable message actually serializes correctly into the
    // buffer and untruncate didn't lose data from `truncate()`.
    assert_eq!(code, 0x45);
    assert_eq!(
        &buffer[..initial_len],
        b"\xb5-----\r\x03very-long-option\xe0\xfc\xd0\xffpayload123"
    );
}

#[test]
fn test_inmemory_write_untruncate_no_payload() {
    use coap_message::*;

    const BUFF_LEN: usize = 100;
    let mut code = 0;
    let mut buffer = [0; BUFF_LEN];
    let mut message = MessageMut::new_in_slice(&mut code, &mut buffer);

    message.set_code(0x45);

    message.add_option(11, b"-----").unwrap();
    message.add_option(11, b"very-long-option").unwrap();
    message.add_option(65000, b"").unwrap();

    assert_eq!(message.payload().len(), 0);

    let initial_len = BUFF_LEN - message.available_space();

    // Need to account for payload marker.
    let too_long = message.untruncate(message.available_space());
    assert!(too_long.is_err());

    message.untruncate(message.available_space() - 1).unwrap();
    assert_eq!(message.payload().len(), message.available_space() - 1);

    let _ = message.finish();

    // These asserts ensure that the payload marker is correctly set.
    assert_eq!(code, 0x45);
    #[expect(clippy::range_plus_one, reason = "the +1 here is an extra byte")]
    {
        assert_eq!(
            &buffer[..initial_len + 1],
            b"\xb5-----\r\x03very-long-option\xe0\xfc\xd0\xff"
        );
    }
}

#[test]
fn test_inmemory_errors() {
    use coap_message::*;

    let bodies = [
        // Truncated option
        (b"\xb5sh".as_slice(), 0),
        // Payload-marker only in one of the two nibbles
        (b"\xbf".as_slice(), 0),
        // Option length overflows, being 269 + 0xffff (the overflow triggers before the truncated-option, even though we can't test that precisely that happens here).
        (b"\x0e\xff\xff".as_slice(), 0),
        // Zero-long option Option whose number barely does not overflow, and then one that does
        (b"\xe0\xfe\x00\xd0\xff".as_slice(), 1),
    ];

    for (body, discard_options) in bodies {
        let message = Message::new_from_slice(0x45, body);

        // We don't guarantee the precise value, but still test to what we expect from the
        // implementation
        assert!(b"" == message.payload());

        let mut optit = message.options();

        for _ in 0..discard_options {
            assert!(optit.next().is_some_and(|o| o.number() != OPTION_INVALID));
        }

        assert!(optit.next().map(|o| o.number()) == Some(OPTION_INVALID));
    }
}

// While it's technically a unit test, being so similar to the above makes it prefereable to have
// it here.
#[test]
fn test_encoding() {
    use crate::option_extension::encode_extensions;

    let example_options = [
        (11, &b"short"[..]),
        (11, &b"very-long-option"[..]),
        (65000, &b""[..]),
    ];

    let expected_encoding = b"\xb5short\r\x03very-long-option\xe0\xfc\xd0";

    let mut encoded = alloc::vec::Vec::new();
    let mut option = 0u16;
    for e in &example_options {
        let delta = e.0 - option;
        option = e.0;
        encoded.extend_from_slice(encode_extensions(delta, e.1.len() as _).as_ref());
        encoded.extend_from_slice(e.1);
    }

    assert_eq!(&expected_encoding.as_ref(), &encoded);
}

#[test]
fn test_truncate() {
    use coap_message::*;

    let mut code = 0;
    let mut buffer = [0; 100];
    let mut message = MessageMut::new_in_slice(&mut code, &mut buffer);

    message.set_code(0x45);

    message.add_option(11, b"-----").unwrap();
    message.add_option(11, b"very-long-option").unwrap();
    message.add_option(65000, b"").unwrap();

    message.truncate(0).unwrap();

    assert_eq!(&[0u8; 0], message.payload());

    let payload = b"payload123";

    message.set_payload(payload).unwrap();

    message.truncate(0).unwrap();

    assert_eq!(&[0u8; 0], message.payload());

    message.untruncate(payload.len()).unwrap();

    message.truncate(payload.len() - 3).unwrap();

    assert_eq!(&payload[..payload.len() - 3], message.payload());
}
