use bytes::{BufMut, Bytes, BytesMut};
use gritty::protocol::{ErrorCode, Frame, FrameCodec, PROTOCOL_VERSION, SessionEntry};
use tokio_util::codec::{Decoder, Encoder};

#[test]
fn encode_data_frame() {
    let mut codec = FrameCodec;
    let mut buf = BytesMut::new();
    codec.encode(Frame::Data(Bytes::from("hello")), &mut buf).unwrap();
    // type(1) + len(4) + payload(5) = 10
    assert_eq!(buf.len(), 10);
    assert_eq!(buf[0], 0x10);
    assert_eq!(&buf[5..], b"hello");
}

#[test]
fn encode_resize_frame() {
    let mut codec = FrameCodec;
    let mut buf = BytesMut::new();
    codec.encode(Frame::Resize { cols: 80, rows: 24 }, &mut buf).unwrap();
    // type(1) + len(4) + payload(4) = 9
    assert_eq!(buf.len(), 9);
    assert_eq!(buf[0], 0x11);
}

#[test]
fn encode_exit_frame() {
    let mut codec = FrameCodec;
    let mut buf = BytesMut::new();
    codec.encode(Frame::Exit { code: 42 }, &mut buf).unwrap();
    assert_eq!(buf.len(), 9);
    assert_eq!(buf[0], 0x12);
}

#[test]
fn roundtrip_data() {
    let mut codec = FrameCodec;
    let mut buf = BytesMut::new();
    let original = Frame::Data(Bytes::from("hello world"));
    codec.encode(original.clone(), &mut buf).unwrap();
    let decoded = codec.decode(&mut buf).unwrap().unwrap();
    assert_eq!(original, decoded);
}

#[test]
fn roundtrip_resize() {
    let mut codec = FrameCodec;
    let mut buf = BytesMut::new();
    let original = Frame::Resize { cols: 120, rows: 40 };
    codec.encode(original.clone(), &mut buf).unwrap();
    let decoded = codec.decode(&mut buf).unwrap().unwrap();
    assert_eq!(original, decoded);
}

#[test]
fn roundtrip_exit() {
    let mut codec = FrameCodec;
    let mut buf = BytesMut::new();
    let original = Frame::Exit { code: 0 };
    codec.encode(original.clone(), &mut buf).unwrap();
    let decoded = codec.decode(&mut buf).unwrap().unwrap();
    assert_eq!(original, decoded);
}

#[test]
fn decode_incomplete_returns_none() {
    let mut codec = FrameCodec;
    let mut buf = BytesMut::from(&[0x10, 0x00, 0x00][..]);
    assert!(codec.decode(&mut buf).unwrap().is_none());
}

#[test]
fn decode_partial_payload_returns_none() {
    let mut codec = FrameCodec;
    // Header says 5 bytes payload, but only 2 present
    let mut buf = BytesMut::from(&[0x10, 0x00, 0x00, 0x00, 0x05, 0xAA, 0xBB][..]);
    assert!(codec.decode(&mut buf).unwrap().is_none());
}

#[test]
fn decode_invalid_type_returns_error() {
    let mut codec = FrameCodec;
    let mut buf = BytesMut::from(&[0xFF, 0x00, 0x00, 0x00, 0x00][..]);
    assert!(codec.decode(&mut buf).is_err());
}

#[test]
fn roundtrip_new_session() {
    let mut codec = FrameCodec;
    let mut buf = BytesMut::new();
    let original = Frame::NewSession {
        name: "myproject".to_string(),
        command: String::new(),
        cwd: String::new(),
        cols: 0,
        rows: 0,
        client_name: "alice-laptop".to_string(),
    };
    codec.encode(original.clone(), &mut buf).unwrap();
    let decoded = codec.decode(&mut buf).unwrap().unwrap();
    assert_eq!(original, decoded);
}

#[test]
fn roundtrip_new_session_empty_name() {
    let mut codec = FrameCodec;
    let mut buf = BytesMut::new();
    let original = Frame::NewSession {
        name: String::new(),
        command: String::new(),
        cwd: String::new(),
        cols: 0,
        rows: 0,
        client_name: String::new(),
    };
    codec.encode(original.clone(), &mut buf).unwrap();
    let decoded = codec.decode(&mut buf).unwrap().unwrap();
    assert_eq!(original, decoded);
}

#[test]
fn roundtrip_new_session_with_command() {
    let mut codec = FrameCodec;
    let mut buf = BytesMut::new();
    let original = Frame::NewSession {
        name: "worker".to_string(),
        command: "echo hello && sleep 999".to_string(),
        cwd: String::new(),
        cols: 0,
        rows: 0,
        client_name: String::new(),
    };
    codec.encode(original.clone(), &mut buf).unwrap();
    let decoded = codec.decode(&mut buf).unwrap().unwrap();
    assert_eq!(original, decoded);
}

#[test]
fn roundtrip_new_session_command_only() {
    let mut codec = FrameCodec;
    let mut buf = BytesMut::new();
    let original = Frame::NewSession {
        name: String::new(),
        command: "make build".to_string(),
        cwd: String::new(),
        cols: 0,
        rows: 0,
        client_name: String::new(),
    };
    codec.encode(original.clone(), &mut buf).unwrap();
    let decoded = codec.decode(&mut buf).unwrap().unwrap();
    assert_eq!(original, decoded);
}

#[test]
fn roundtrip_rename_session() {
    let mut codec = FrameCodec;
    let mut buf = BytesMut::new();
    let original =
        Frame::RenameSession { session: "0".to_string(), new_name: "myproject".to_string() };
    codec.encode(original.clone(), &mut buf).unwrap();
    let decoded = codec.decode(&mut buf).unwrap().unwrap();
    assert_eq!(original, decoded);
}

#[test]
fn roundtrip_rename_session_by_name() {
    let mut codec = FrameCodec;
    let mut buf = BytesMut::new();
    let original =
        Frame::RenameSession { session: "oldname".to_string(), new_name: "newname".to_string() };
    codec.encode(original.clone(), &mut buf).unwrap();
    let decoded = codec.decode(&mut buf).unwrap().unwrap();
    assert_eq!(original, decoded);
}

#[test]
fn roundtrip_attach() {
    let mut codec = FrameCodec;
    let mut buf = BytesMut::new();
    let original = Frame::Attach {
        session: "0".to_string(),
        client_name: String::new(),
        force: false,
        no_replay: false,
        cols: 80,
        rows: 24,
        attach_token: 0,
        rendered_offset: 0,
        line_dirty: false,
    };
    codec.encode(original.clone(), &mut buf).unwrap();
    let decoded = codec.decode(&mut buf).unwrap().unwrap();
    assert_eq!(original, decoded);
}

#[test]
fn roundtrip_attach_by_name() {
    let mut codec = FrameCodec;
    let mut buf = BytesMut::new();
    let original = Frame::Attach {
        session: "myproject".to_string(),
        client_name: String::new(),
        force: false,
        no_replay: true,
        cols: 0,
        rows: 0,
        attach_token: 0,
        rendered_offset: 0,
        line_dirty: false,
    };
    codec.encode(original.clone(), &mut buf).unwrap();
    let decoded = codec.decode(&mut buf).unwrap().unwrap();
    assert_eq!(original, decoded);
}

#[test]
fn roundtrip_attach_with_token() {
    let mut codec = FrameCodec;
    let mut buf = BytesMut::new();
    let original = Frame::Attach {
        session: "myproject".to_string(),
        client_name: "alice".to_string(),
        force: true,
        no_replay: false,
        cols: 120,
        rows: 40,
        attach_token: 0xdead_beef_cafe_babe,
        rendered_offset: 0x0011_2233_4455_6677,
        line_dirty: true,
    };
    codec.encode(original.clone(), &mut buf).unwrap();
    let decoded = codec.decode(&mut buf).unwrap().unwrap();
    assert_eq!(original, decoded);
}

#[test]
fn roundtrip_resume() {
    let mut codec = FrameCodec;
    let mut buf = BytesMut::new();
    let original = Frame::Resume { offset: 0xdead_beef_0000_0001 };
    codec.encode(original.clone(), &mut buf).unwrap();
    let decoded = codec.decode(&mut buf).unwrap().unwrap();
    assert_eq!(original, decoded);
}

#[test]
fn roundtrip_notice() {
    let mut codec = FrameCodec;
    let mut buf = BytesMut::new();
    let original = Frame::Notice(Bytes::from("\r\x1b[2m-- replayed --\x1b[0m\r\n"));
    codec.encode(original.clone(), &mut buf).unwrap();
    let decoded = codec.decode(&mut buf).unwrap().unwrap();
    assert_eq!(original, decoded);
}

#[test]
fn roundtrip_notice_empty() {
    let mut codec = FrameCodec;
    let mut buf = BytesMut::new();
    let original = Frame::Notice(Bytes::new());
    codec.encode(original.clone(), &mut buf).unwrap();
    let decoded = codec.decode(&mut buf).unwrap().unwrap();
    assert_eq!(original, decoded);
}

#[test]
fn roundtrip_attach_ack() {
    let mut codec = FrameCodec;
    let mut buf = BytesMut::new();
    let original = Frame::AttachAck { token: 0x0123_4567_89ab_cdef, session_id: 7 };
    codec.encode(original.clone(), &mut buf).unwrap();
    let decoded = codec.decode(&mut buf).unwrap().unwrap();
    assert_eq!(original, decoded);
}

#[test]
fn roundtrip_error_owner_changed() {
    let mut codec = FrameCodec;
    let mut buf = BytesMut::new();
    let original =
        Frame::Error { code: ErrorCode::OwnerChanged, message: "taken over".to_string() };
    codec.encode(original.clone(), &mut buf).unwrap();
    let decoded = codec.decode(&mut buf).unwrap().unwrap();
    assert_eq!(original, decoded);
}

#[test]
fn legacy_attach_without_token_decodes_as_zero() {
    // A v13 client sends an Attach frame without the trailing u64. The new
    // decoder should tolerate the shorter payload and default attach_token to 0.
    let mut codec = FrameCodec;
    let mut buf = BytesMut::new();
    // Manually encode the v13 Attach layout: type + len + (sessLen u16 + "alpha"
    // + cnLen u16 + "" + force u8 + no_replay u8 + cols u16 + rows u16)
    let session = b"alpha";
    #[allow(clippy::identity_op)]
    let payload_len = 2 + session.len() + 2 + 0 + 1 + 1 + 2 + 2;
    buf.put_u8(0x51); // TYPE_ATTACH
    buf.put_u32(payload_len as u32);
    buf.put_u16(session.len() as u16);
    buf.put_slice(session);
    buf.put_u16(0); // client_name
    buf.put_u8(0); // force
    buf.put_u8(0); // no_replay
    buf.put_u16(80); // cols
    buf.put_u16(24); // rows
    let decoded = codec.decode(&mut buf).unwrap().unwrap();
    match decoded {
        Frame::Attach { attach_token, session, cols, rows, .. } => {
            assert_eq!(attach_token, 0);
            assert_eq!(session, "alpha");
            assert_eq!(cols, 80);
            assert_eq!(rows, 24);
        }
        other => panic!("expected Attach, got {other:?}"),
    }
}

#[test]
fn legacy_attach_without_offset_decodes_as_zero() {
    // A v14..v20 client sends an Attach frame with attach_token but without
    // the trailing rendered_offset + line_dirty (added in v21). The decoder
    // should tolerate the shorter payload and default both to 0 / false.
    let mut codec = FrameCodec;
    let mut buf = BytesMut::new();
    let session = b"beta";
    #[allow(clippy::identity_op)]
    let payload_len = 2 + session.len() + 2 + 0 + 1 + 1 + 2 + 2 + 8;
    buf.put_u8(0x51); // TYPE_ATTACH
    buf.put_u32(payload_len as u32);
    buf.put_u16(session.len() as u16);
    buf.put_slice(session);
    buf.put_u16(0); // client_name
    buf.put_u8(0); // force
    buf.put_u8(0); // no_replay
    buf.put_u16(80); // cols
    buf.put_u16(24); // rows
    buf.put_u64(0xabc); // attach_token
    let decoded = codec.decode(&mut buf).unwrap().unwrap();
    match decoded {
        Frame::Attach { attach_token, rendered_offset, line_dirty, .. } => {
            assert_eq!(attach_token, 0xabc);
            assert_eq!(rendered_offset, 0);
            assert!(!line_dirty);
        }
        other => panic!("expected Attach, got {other:?}"),
    }
}

#[test]
fn roundtrip_session_created() {
    let mut codec = FrameCodec;
    let mut buf = BytesMut::new();
    let original = Frame::SessionCreated { id: 42 };
    codec.encode(original.clone(), &mut buf).unwrap();
    // type(1) + len(4) + id(4) = 9
    assert_eq!(buf.len(), 9);
    let decoded = codec.decode(&mut buf).unwrap().unwrap();
    assert_eq!(original, decoded);
}

#[test]
fn roundtrip_list_sessions() {
    let mut codec = FrameCodec;
    let mut buf = BytesMut::new();
    codec.encode(Frame::ListSessions, &mut buf).unwrap();
    let decoded = codec.decode(&mut buf).unwrap().unwrap();
    assert_eq!(Frame::ListSessions, decoded);
}

#[test]
fn roundtrip_session_info() {
    let mut codec = FrameCodec;
    let mut buf = BytesMut::new();
    let original = Frame::SessionInfo {
        sessions: vec![
            SessionEntry {
                id: 0,
                name: "project-a".to_string(),
                foreground_cmd: "vim".to_string(),
                cwd: "/home/user/project".to_string(),
                client_name: "laptop".to_string(),
                pty_path: "/dev/pts/3".to_string(),
                shell_pid: 1234,
                created_at: 1700000000,
                attached: true,
                last_heartbeat: 1700000005,
                agent_forwarding_active: true,
                is_last_attached: false,
            },
            SessionEntry {
                id: 1,
                name: String::new(),
                foreground_cmd: String::new(),
                cwd: String::new(),
                client_name: String::new(),
                pty_path: "/dev/pts/5".to_string(),
                shell_pid: 5678,
                created_at: 1700000100,
                attached: false,
                last_heartbeat: 0,
                agent_forwarding_active: false,
                is_last_attached: false,
            },
        ],
    };
    codec.encode(original.clone(), &mut buf).unwrap();
    let decoded = codec.decode(&mut buf).unwrap().unwrap();
    assert_eq!(original, decoded);
}

#[test]
fn roundtrip_session_info_empty() {
    let mut codec = FrameCodec;
    let mut buf = BytesMut::new();
    let original = Frame::SessionInfo { sessions: vec![] };
    codec.encode(original.clone(), &mut buf).unwrap();
    let decoded = codec.decode(&mut buf).unwrap().unwrap();
    assert_eq!(original, decoded);
}

#[test]
fn roundtrip_ok() {
    let mut codec = FrameCodec;
    let mut buf = BytesMut::new();
    codec.encode(Frame::Ok, &mut buf).unwrap();
    let decoded = codec.decode(&mut buf).unwrap().unwrap();
    assert_eq!(Frame::Ok, decoded);
}

#[test]
fn roundtrip_error() {
    let mut codec = FrameCodec;
    let mut buf = BytesMut::new();
    let original =
        Frame::Error { code: ErrorCode::UnexpectedFrame, message: "something failed".to_string() };
    codec.encode(original.clone(), &mut buf).unwrap();
    let decoded = codec.decode(&mut buf).unwrap().unwrap();
    assert_eq!(original, decoded);
}

#[test]
fn roundtrip_detached() {
    let mut codec = FrameCodec;
    let mut buf = BytesMut::new();
    codec.encode(Frame::Detached, &mut buf).unwrap();
    let decoded = codec.decode(&mut buf).unwrap().unwrap();
    assert_eq!(Frame::Detached, decoded);
}

#[test]
fn roundtrip_server_shutdown() {
    let mut codec = FrameCodec;
    let mut buf = BytesMut::new();
    codec.encode(Frame::ServerShutdown, &mut buf).unwrap();
    let decoded = codec.decode(&mut buf).unwrap().unwrap();
    assert_eq!(Frame::ServerShutdown, decoded);
}

#[test]
fn roundtrip_kill_session() {
    let mut codec = FrameCodec;
    let mut buf = BytesMut::new();
    let original = Frame::KillSession { session: "0".to_string() };
    codec.encode(original.clone(), &mut buf).unwrap();
    let decoded = codec.decode(&mut buf).unwrap().unwrap();
    assert_eq!(original, decoded);
}

#[test]
fn roundtrip_kill_session_by_name() {
    let mut codec = FrameCodec;
    let mut buf = BytesMut::new();
    let original = Frame::KillSession { session: "myproject".to_string() };
    codec.encode(original.clone(), &mut buf).unwrap();
    let decoded = codec.decode(&mut buf).unwrap().unwrap();
    assert_eq!(original, decoded);
}

#[test]
fn roundtrip_kill_server() {
    let mut codec = FrameCodec;
    let mut buf = BytesMut::new();
    codec.encode(Frame::KillServer, &mut buf).unwrap();
    let decoded = codec.decode(&mut buf).unwrap().unwrap();
    assert_eq!(Frame::KillServer, decoded);
}

#[test]
fn multi_frame_decode() {
    let mut codec = FrameCodec;
    let mut buf = BytesMut::new();
    // Encode three different frames into one buffer
    codec.encode(Frame::Data(Bytes::from("abc")), &mut buf).unwrap();
    codec.encode(Frame::Resize { cols: 80, rows: 24 }, &mut buf).unwrap();
    codec.encode(Frame::Exit { code: 7 }, &mut buf).unwrap();

    // Decode them one by one from the same buffer
    assert_eq!(codec.decode(&mut buf).unwrap().unwrap(), Frame::Data(Bytes::from("abc")));
    assert_eq!(codec.decode(&mut buf).unwrap().unwrap(), Frame::Resize { cols: 80, rows: 24 });
    assert_eq!(codec.decode(&mut buf).unwrap().unwrap(), Frame::Exit { code: 7 });
    // Buffer should be empty now
    assert!(codec.decode(&mut buf).unwrap().is_none());
}

#[test]
fn zero_length_data_roundtrip() {
    let mut codec = FrameCodec;
    let mut buf = BytesMut::new();
    let original = Frame::Data(Bytes::new());
    codec.encode(original.clone(), &mut buf).unwrap();
    let decoded = codec.decode(&mut buf).unwrap().unwrap();
    assert_eq!(original, decoded);
}

#[test]
fn roundtrip_exit_negative_code() {
    let mut codec = FrameCodec;
    let mut buf = BytesMut::new();
    let original = Frame::Exit { code: -1 };
    codec.encode(original.clone(), &mut buf).unwrap();
    let decoded = codec.decode(&mut buf).unwrap().unwrap();
    assert_eq!(original, decoded);
}

#[test]
fn roundtrip_error_empty_message() {
    let mut codec = FrameCodec;
    let mut buf = BytesMut::new();
    let original = Frame::Error { code: ErrorCode::Unknown(0), message: String::new() };
    codec.encode(original.clone(), &mut buf).unwrap();
    let decoded = codec.decode(&mut buf).unwrap().unwrap();
    assert_eq!(original, decoded);
}

#[test]
fn resize_too_short_payload_rejected() {
    let mut codec = FrameCodec;
    // Resize frame type (0x11) with only 3 bytes payload instead of min 4
    let mut buf = BytesMut::from(&[0x11, 0x00, 0x00, 0x00, 0x03, 0x00, 0x50, 0x00][..]);
    let err = codec.decode(&mut buf).unwrap_err();
    assert_eq!(err.kind(), std::io::ErrorKind::InvalidData);
}

#[test]
fn exit_too_short_payload_rejected() {
    let mut codec = FrameCodec;
    // Exit frame type (0x12) with 2 bytes payload instead of min 4
    let mut buf = BytesMut::from(&[0x12, 0x00, 0x00, 0x00, 0x02, 0x00, 0x2A][..]);
    let err = codec.decode(&mut buf).unwrap_err();
    assert_eq!(err.kind(), std::io::ErrorKind::InvalidData);
}

#[test]
fn session_info_u32_id_roundtrip() {
    // SessionEntry.id is now u32 -- verify a non-trivial id round-trips
    let mut codec = FrameCodec;
    let mut buf = BytesMut::new();
    let original = Frame::SessionInfo {
        sessions: vec![SessionEntry {
            id: 12345,
            name: "test".to_string(),
            foreground_cmd: String::new(),
            cwd: String::new(),
            client_name: String::new(),
            pty_path: "/dev/pts/3".to_string(),
            shell_pid: 1234,
            created_at: 1700000000,
            attached: true,
            last_heartbeat: 0,
            agent_forwarding_active: false,
            is_last_attached: false,
        }],
    };
    codec.encode(original.clone(), &mut buf).unwrap();
    let decoded = codec.decode(&mut buf).unwrap().unwrap();
    assert_eq!(original, decoded);
    match decoded {
        Frame::SessionInfo { sessions } => {
            assert_eq!(sessions.len(), 1);
            assert_eq!(sessions[0].id, 12345);
        }
        other => panic!("expected SessionInfo, got {other:?}"),
    }
}

#[test]
fn large_data_frame_roundtrip() {
    let mut codec = FrameCodec;
    let mut buf = BytesMut::new();
    let payload = vec![0xABu8; 64 * 1024]; // 64KB
    let original = Frame::Data(Bytes::from(payload));
    codec.encode(original.clone(), &mut buf).unwrap();
    let decoded = codec.decode(&mut buf).unwrap().unwrap();
    assert_eq!(original, decoded);
}

#[test]
fn decode_empty_buffer_returns_none() {
    let mut codec = FrameCodec;
    let mut buf = BytesMut::new();
    assert!(codec.decode(&mut buf).unwrap().is_none());
}

#[test]
fn decode_consumes_only_one_frame() {
    let mut codec = FrameCodec;
    let mut buf = BytesMut::new();
    codec.encode(Frame::Data(Bytes::from("first")), &mut buf).unwrap();
    codec.encode(Frame::Data(Bytes::from("second")), &mut buf).unwrap();
    let total_len = buf.len();

    let first = codec.decode(&mut buf).unwrap().unwrap();
    assert_eq!(first, Frame::Data(Bytes::from("first")));
    // Buffer should still have the second frame
    assert!(buf.len() < total_len);
    assert!(!buf.is_empty());

    let second = codec.decode(&mut buf).unwrap().unwrap();
    assert_eq!(second, Frame::Data(Bytes::from("second")));
    assert!(buf.is_empty());
}

#[test]
fn session_info_preserves_newline_in_name() {
    // Binary encoding preserves arbitrary content -- newlines no longer corrupt the format
    let mut codec = FrameCodec;
    let mut buf = BytesMut::new();
    let original = Frame::SessionInfo {
        sessions: vec![SessionEntry {
            id: 0,
            name: "has\nnewline".to_string(),
            foreground_cmd: String::new(),
            cwd: String::new(),
            client_name: String::new(),
            pty_path: "/dev/pts/3".to_string(),
            shell_pid: 1234,
            created_at: 1700000000,
            attached: true,
            last_heartbeat: 0,
            agent_forwarding_active: false,
            is_last_attached: false,
        }],
    };
    codec.encode(original.clone(), &mut buf).unwrap();
    let decoded = codec.decode(&mut buf).unwrap().unwrap();
    assert_eq!(original, decoded);
    match decoded {
        Frame::SessionInfo { sessions } => {
            assert_eq!(sessions.len(), 1);
            assert_eq!(sessions[0].name, "has\nnewline");
        }
        other => panic!("expected SessionInfo, got {other:?}"),
    }
}

#[test]
fn invalid_utf8_in_string_frame() {
    let mut codec = FrameCodec;
    // NewSession (0x50) with invalid UTF-8 in name field
    // Wire: [name_len: u16][name][cmd_len: u16][cmd][cwd_len: u16][cwd][cols: u16][rows: u16]
    // Minimum valid: 2+0+2+0+2+0+2+2 = 10 bytes, but name content is invalid UTF-8
    let mut buf = BytesMut::new();
    buf.put_u8(0x50); // TYPE_NEW_SESSION
    buf.put_u32(12); // payload length: name_len(2)+name(2)+cmd_len(2)+cmd(0)+cwd_len(2)+cwd(0)+cols(2)+rows(2)
    buf.put_u16(2); // name_len = 2
    buf.put_slice(&[0xFF, 0xFE]); // invalid UTF-8 name
    buf.put_u16(0); // cmd_len = 0
    buf.put_u16(0); // cwd_len = 0
    buf.put_u16(0); // cols
    buf.put_u16(0); // rows
    let err = codec.decode(&mut buf).unwrap_err();
    assert_eq!(err.kind(), std::io::ErrorKind::InvalidData);
}

#[test]
fn roundtrip_ping() {
    let mut codec = FrameCodec;
    let mut buf = BytesMut::new();
    codec.encode(Frame::Ping, &mut buf).unwrap();
    assert_eq!(buf.len(), 5); // type(1) + len(4), zero payload
    assert_eq!(buf[0], 0x14);
    let decoded = codec.decode(&mut buf).unwrap().unwrap();
    assert_eq!(Frame::Ping, decoded);
}

#[test]
fn roundtrip_pong() {
    let mut codec = FrameCodec;
    let mut buf = BytesMut::new();
    codec.encode(Frame::Pong, &mut buf).unwrap();
    assert_eq!(buf.len(), 5); // type(1) + len(4), zero payload
    assert_eq!(buf[0], 0x15);
    let decoded = codec.decode(&mut buf).unwrap().unwrap();
    assert_eq!(Frame::Pong, decoded);
}

#[test]
fn roundtrip_env() {
    let mut codec = FrameCodec;
    let mut buf = BytesMut::new();
    let original = Frame::Env {
        vars: vec![
            ("TERM".to_string(), "xterm-256color".to_string()),
            ("LANG".to_string(), "en_US.UTF-8".to_string()),
            ("COLORTERM".to_string(), "truecolor".to_string()),
        ],
    };
    codec.encode(original.clone(), &mut buf).unwrap();
    assert_eq!(buf[0], 0x16);
    let decoded = codec.decode(&mut buf).unwrap().unwrap();
    assert_eq!(original, decoded);
}

#[test]
fn env_preserves_newlines_in_values() {
    let mut codec = FrameCodec;
    let mut buf = BytesMut::new();
    // Binary encoding preserves arbitrary content -- newlines are no longer special
    let original = Frame::Env {
        vars: vec![("TERM".to_string(), "xterm\nLD_PRELOAD=/tmp/evil.so".to_string())],
    };
    codec.encode(original.clone(), &mut buf).unwrap();
    let decoded = codec.decode(&mut buf).unwrap().unwrap();
    assert_eq!(original, decoded);
}

#[test]
fn env_preserves_newlines_in_keys() {
    let mut codec = FrameCodec;
    let mut buf = BytesMut::new();
    // Binary encoding preserves arbitrary content in keys too
    let original = Frame::Env { vars: vec![("TE\nRM".to_string(), "xterm".to_string())] };
    codec.encode(original.clone(), &mut buf).unwrap();
    let decoded = codec.decode(&mut buf).unwrap().unwrap();
    assert_eq!(original, decoded);
}

#[test]
fn decode_oversized_frame_rejected() {
    let mut codec = FrameCodec;
    let mut buf = BytesMut::new();
    // Header claiming payload of 1 MB + 1 byte (exceeds MAX_FRAME_SIZE)
    buf.put_u8(0x10); // TYPE_DATA
    buf.put_u32((1 << 20) + 1); // 1_048_577 bytes
    let err = codec.decode(&mut buf).unwrap_err();
    assert_eq!(err.kind(), std::io::ErrorKind::InvalidData);
    assert!(err.to_string().contains("too large"));
}

#[test]
fn decode_exactly_max_frame_size_accepted() {
    let mut codec = FrameCodec;
    let mut buf = BytesMut::new();
    let payload = vec![0x42u8; 1 << 20]; // exactly 1 MB
    buf.put_u8(0x10); // TYPE_DATA
    buf.put_u32(1 << 20);
    buf.put_slice(&payload);
    let decoded = codec.decode(&mut buf).unwrap().unwrap();
    assert_eq!(decoded, Frame::Data(Bytes::from(payload)));
}

#[test]
fn roundtrip_env_empty() {
    let mut codec = FrameCodec;
    let mut buf = BytesMut::new();
    let original = Frame::Env { vars: vec![] };
    codec.encode(original.clone(), &mut buf).unwrap();
    assert_eq!(buf.len(), 9); // type(1) + len(4) + count(4)
    assert_eq!(buf[0], 0x16);
    let decoded = codec.decode(&mut buf).unwrap().unwrap();
    assert_eq!(original, decoded);
}

#[test]
fn roundtrip_agent_forward() {
    let mut codec = FrameCodec;
    let mut buf = BytesMut::new();
    codec.encode(Frame::AgentForward, &mut buf).unwrap();
    assert_eq!(buf.len(), 5); // type(1) + len(4), zero payload
    assert_eq!(buf[0], 0x20);
    let decoded = codec.decode(&mut buf).unwrap().unwrap();
    assert_eq!(Frame::AgentForward, decoded);
}

#[test]
fn roundtrip_agent_open() {
    let mut codec = FrameCodec;
    let mut buf = BytesMut::new();
    let original = Frame::AgentOpen { channel_id: 42 };
    codec.encode(original.clone(), &mut buf).unwrap();
    assert_eq!(buf.len(), 9); // type(1) + len(4) + channel_id(4)
    assert_eq!(buf[0], 0x21);
    let decoded = codec.decode(&mut buf).unwrap().unwrap();
    assert_eq!(original, decoded);
}

#[test]
fn roundtrip_agent_data() {
    let mut codec = FrameCodec;
    let mut buf = BytesMut::new();
    let original = Frame::AgentData { channel_id: 7, data: Bytes::from("agent-payload") };
    codec.encode(original.clone(), &mut buf).unwrap();
    // type(1) + len(4) + channel_id(4) + data(13) = 22
    assert_eq!(buf.len(), 22);
    assert_eq!(buf[0], 0x22);
    let decoded = codec.decode(&mut buf).unwrap().unwrap();
    assert_eq!(original, decoded);
}

#[test]
fn roundtrip_agent_data_empty() {
    let mut codec = FrameCodec;
    let mut buf = BytesMut::new();
    let original = Frame::AgentData { channel_id: 0, data: Bytes::new() };
    codec.encode(original.clone(), &mut buf).unwrap();
    assert_eq!(buf.len(), 9); // type(1) + len(4) + channel_id(4)
    let decoded = codec.decode(&mut buf).unwrap().unwrap();
    assert_eq!(original, decoded);
}

#[test]
fn roundtrip_agent_close() {
    let mut codec = FrameCodec;
    let mut buf = BytesMut::new();
    let original = Frame::AgentClose { channel_id: 99 };
    codec.encode(original.clone(), &mut buf).unwrap();
    assert_eq!(buf.len(), 9); // type(1) + len(4) + channel_id(4)
    assert_eq!(buf[0], 0x23);
    let decoded = codec.decode(&mut buf).unwrap().unwrap();
    assert_eq!(original, decoded);
}

#[test]
fn agent_open_wrong_payload_size() {
    let mut codec = FrameCodec;
    let mut buf = BytesMut::new();
    buf.put_u8(0x21); // TYPE_AGENT_OPEN
    buf.put_u32(2); // wrong: should be 4
    buf.put_slice(&[0x00, 0x00]);
    let err = codec.decode(&mut buf).unwrap_err();
    assert_eq!(err.kind(), std::io::ErrorKind::InvalidData);
}

#[test]
fn agent_data_too_short() {
    let mut codec = FrameCodec;
    let mut buf = BytesMut::new();
    buf.put_u8(0x22); // TYPE_AGENT_DATA
    buf.put_u32(2); // less than 4 bytes
    buf.put_slice(&[0x00, 0x00]);
    let err = codec.decode(&mut buf).unwrap_err();
    assert_eq!(err.kind(), std::io::ErrorKind::InvalidData);
}

#[test]
fn agent_close_tolerates_trailing_bytes() {
    let mut codec = FrameCodec;
    let mut buf = BytesMut::new();
    buf.put_u8(0x23); // TYPE_AGENT_CLOSE
    buf.put_u32(8); // 4 known + 4 trailing
    buf.put_slice(&[0x00; 8]);
    let decoded = codec.decode(&mut buf).unwrap().unwrap();
    assert_eq!(decoded, Frame::AgentClose { channel_id: 0 });
}

#[test]
fn roundtrip_open_forward() {
    let mut codec = FrameCodec;
    let mut buf = BytesMut::new();
    codec.encode(Frame::OpenForward, &mut buf).unwrap();
    assert_eq!(buf.len(), 5); // type(1) + len(4), zero payload
    assert_eq!(buf[0], 0x28);
    let decoded = codec.decode(&mut buf).unwrap().unwrap();
    assert_eq!(Frame::OpenForward, decoded);
}

#[test]
fn roundtrip_open_url() {
    let mut codec = FrameCodec;
    let mut buf = BytesMut::new();
    let original = Frame::OpenUrl { url: "https://example.com".to_string() };
    codec.encode(original.clone(), &mut buf).unwrap();
    assert_eq!(buf[0], 0x29);
    let decoded = codec.decode(&mut buf).unwrap().unwrap();
    assert_eq!(original, decoded);
}

#[test]
fn roundtrip_tail() {
    let mut codec = FrameCodec;
    let mut buf = BytesMut::new();
    let original = Frame::Tail { session: "myproject".to_string() };
    codec.encode(original.clone(), &mut buf).unwrap();
    assert_eq!(buf[0], 0x55);
    let decoded = codec.decode(&mut buf).unwrap().unwrap();
    assert_eq!(original, decoded);
}

#[test]
fn roundtrip_tail_by_id() {
    let mut codec = FrameCodec;
    let mut buf = BytesMut::new();
    let original = Frame::Tail { session: "0".to_string() };
    codec.encode(original.clone(), &mut buf).unwrap();
    let decoded = codec.decode(&mut buf).unwrap().unwrap();
    assert_eq!(original, decoded);
}

#[test]
fn roundtrip_hello() {
    let mut codec = FrameCodec;
    let mut buf = BytesMut::new();
    let original = Frame::Hello { version: PROTOCOL_VERSION, capabilities: 0, device_id: 42 };
    codec.encode(original.clone(), &mut buf).unwrap();
    // type(1) + len(4) + version(2) + capabilities(4) + device_id(8) = 19
    assert_eq!(buf.len(), 19);
    assert_eq!(buf[0], 0x01);
    let decoded = codec.decode(&mut buf).unwrap().unwrap();
    assert_eq!(original, decoded);
}

#[test]
fn roundtrip_hello_ack() {
    let mut codec = FrameCodec;
    let mut buf = BytesMut::new();
    let original = Frame::HelloAck { version: PROTOCOL_VERSION, capabilities: 0, server_id: 0 };
    codec.encode(original.clone(), &mut buf).unwrap();
    // type(1) + len(4) + version(2) + capabilities(4) + server_id(8) = 19
    assert_eq!(buf.len(), 19);
    assert_eq!(buf[0], 0x02);
    let decoded = codec.decode(&mut buf).unwrap().unwrap();
    assert_eq!(original, decoded);
}

#[test]
fn hello_wrong_payload_size() {
    let mut codec = FrameCodec;
    let mut buf = BytesMut::new();
    buf.put_u8(0x01); // TYPE_HELLO
    buf.put_u32(2); // wrong: should be 6
    buf.put_slice(&[0x00, 0x01]);
    let err = codec.decode(&mut buf).unwrap_err();
    assert_eq!(err.kind(), std::io::ErrorKind::InvalidData);
}

#[test]
fn hello_ack_wrong_payload_size() {
    let mut codec = FrameCodec;
    let mut buf = BytesMut::new();
    buf.put_u8(0x02); // TYPE_HELLO_ACK
    buf.put_u32(1); // wrong: should be 6
    buf.put_slice(&[0x01]);
    let err = codec.decode(&mut buf).unwrap_err();
    assert_eq!(err.kind(), std::io::ErrorKind::InvalidData);
}

#[test]
fn roundtrip_tunnel_listen() {
    let mut codec = FrameCodec;
    let mut buf = BytesMut::new();
    let original = Frame::TunnelListen { port: 8080 };
    codec.encode(original.clone(), &mut buf).unwrap();
    assert_eq!(buf.len(), 7); // type(1) + len(4) + port(2)
    assert_eq!(buf[0], 0x30);
    let decoded = codec.decode(&mut buf).unwrap().unwrap();
    assert_eq!(original, decoded);
}

#[test]
fn roundtrip_tunnel_open() {
    let mut codec = FrameCodec;
    let mut buf = BytesMut::new();
    let original = Frame::TunnelOpen { channel_id: 42 };
    codec.encode(original.clone(), &mut buf).unwrap();
    assert_eq!(buf.len(), 9); // type(1) + len(4) + channel_id(4)
    assert_eq!(buf[0], 0x31);
    let decoded = codec.decode(&mut buf).unwrap().unwrap();
    assert_eq!(original, decoded);
}

#[test]
fn roundtrip_tunnel_data() {
    let mut codec = FrameCodec;
    let mut buf = BytesMut::new();
    let original = Frame::TunnelData { channel_id: 7, data: Bytes::from("tunnel-payload") };
    codec.encode(original.clone(), &mut buf).unwrap();
    assert_eq!(buf[0], 0x32);
    let decoded = codec.decode(&mut buf).unwrap().unwrap();
    assert_eq!(original, decoded);
}

#[test]
fn roundtrip_tunnel_data_empty() {
    let mut codec = FrameCodec;
    let mut buf = BytesMut::new();
    let original = Frame::TunnelData { channel_id: 0, data: Bytes::new() };
    codec.encode(original.clone(), &mut buf).unwrap();
    assert_eq!(buf.len(), 9); // type(1) + len(4) + channel_id(4)
    let decoded = codec.decode(&mut buf).unwrap().unwrap();
    assert_eq!(original, decoded);
}

#[test]
fn roundtrip_tunnel_close() {
    let mut codec = FrameCodec;
    let mut buf = BytesMut::new();
    let original = Frame::TunnelClose { channel_id: 99 };
    codec.encode(original.clone(), &mut buf).unwrap();
    assert_eq!(buf.len(), 9); // type(1) + len(4) + channel_id(4)
    assert_eq!(buf[0], 0x33);
    let decoded = codec.decode(&mut buf).unwrap().unwrap();
    assert_eq!(original, decoded);
}

#[test]
fn roundtrip_send_offer() {
    let mut codec = FrameCodec;
    let mut buf = BytesMut::new();
    let original = Frame::SendOffer { file_count: 3, total_bytes: 1_234_567 };
    codec.encode(original.clone(), &mut buf).unwrap();
    assert_eq!(buf.len(), 17); // type(1) + len(4) + file_count(4) + total_bytes(8)
    assert_eq!(buf[0], 0x38);
    let decoded = codec.decode(&mut buf).unwrap().unwrap();
    assert_eq!(original, decoded);
}

#[test]
fn roundtrip_send_done() {
    let mut codec = FrameCodec;
    let mut buf = BytesMut::new();
    codec.encode(Frame::SendDone, &mut buf).unwrap();
    assert_eq!(buf.len(), 5); // type(1) + len(4), zero payload
    assert_eq!(buf[0], 0x39);
    let decoded = codec.decode(&mut buf).unwrap().unwrap();
    assert_eq!(Frame::SendDone, decoded);
}

#[test]
fn roundtrip_send_cancel() {
    let mut codec = FrameCodec;
    let mut buf = BytesMut::new();
    let original = Frame::SendCancel { reason: "receiver disconnected".to_string() };
    codec.encode(original.clone(), &mut buf).unwrap();
    assert_eq!(buf[0], 0x3A);
    let decoded = codec.decode(&mut buf).unwrap().unwrap();
    assert_eq!(original, decoded);
}

#[test]
fn roundtrip_send_cancel_empty() {
    let mut codec = FrameCodec;
    let mut buf = BytesMut::new();
    let original = Frame::SendCancel { reason: String::new() };
    codec.encode(original.clone(), &mut buf).unwrap();
    let decoded = codec.decode(&mut buf).unwrap().unwrap();
    assert_eq!(original, decoded);
}

#[test]
fn roundtrip_send_file() {
    let mut codec = FrameCodec;
    let mut buf = BytesMut::new();
    let original = Frame::SendFile { session: "myproject".to_string() };
    codec.encode(original.clone(), &mut buf).unwrap();
    assert_eq!(buf[0], 0x3B);
    let decoded = codec.decode(&mut buf).unwrap().unwrap();
    assert_eq!(original, decoded);
}

#[test]
fn send_offer_wrong_payload_size() {
    let mut codec = FrameCodec;
    let mut buf = BytesMut::new();
    buf.put_u8(0x38); // TYPE_SEND_OFFER
    buf.put_u32(4); // wrong: should be 12
    buf.put_slice(&[0x00; 4]);
    let err = codec.decode(&mut buf).unwrap_err();
    assert_eq!(err.kind(), std::io::ErrorKind::InvalidData);
}

#[test]
fn send_file_empty_session() {
    let mut codec = FrameCodec;
    let mut buf = BytesMut::new();
    buf.put_u8(0x3B); // TYPE_SEND_FILE
    buf.put_u32(0); // empty session
    let decoded = codec.decode(&mut buf).unwrap().unwrap();
    assert_eq!(decoded, Frame::SendFile { session: String::new() });
}

#[test]
fn tunnel_listen_tolerates_trailing_bytes() {
    let mut codec = FrameCodec;
    let mut buf = BytesMut::new();
    buf.put_u8(0x30); // TYPE_TUNNEL_LISTEN
    buf.put_u32(4); // 2 known + 2 trailing
    buf.put_slice(&[0x00, 0x01, 0x00, 0x00]);
    let decoded = codec.decode(&mut buf).unwrap().unwrap();
    assert_eq!(decoded, Frame::TunnelListen { port: 1 });
}

#[test]
fn roundtrip_port_forward_listen() {
    let mut codec = FrameCodec;
    let mut buf = BytesMut::new();
    let original = Frame::PortForwardListen { forward_id: 1, listen_port: 8080, target_port: 3000 };
    codec.encode(original.clone(), &mut buf).unwrap();
    assert_eq!(buf.len(), 13); // type(1) + len(4) + forward_id(4) + listen(2) + target(2)
    assert_eq!(buf[0], 0x40);
    let decoded = codec.decode(&mut buf).unwrap().unwrap();
    assert_eq!(original, decoded);
}

#[test]
fn roundtrip_port_forward_ready() {
    let mut codec = FrameCodec;
    let mut buf = BytesMut::new();
    let original = Frame::PortForwardReady { forward_id: 42 };
    codec.encode(original.clone(), &mut buf).unwrap();
    assert_eq!(buf.len(), 9); // type(1) + len(4) + forward_id(4)
    assert_eq!(buf[0], 0x41);
    let decoded = codec.decode(&mut buf).unwrap().unwrap();
    assert_eq!(original, decoded);
}

#[test]
fn roundtrip_port_forward_open() {
    let mut codec = FrameCodec;
    let mut buf = BytesMut::new();
    let original = Frame::PortForwardOpen { forward_id: 1, channel_id: 7, target_port: 3000 };
    codec.encode(original.clone(), &mut buf).unwrap();
    assert_eq!(buf.len(), 15); // type(1) + len(4) + forward_id(4) + channel_id(4) + target(2)
    assert_eq!(buf[0], 0x42);
    let decoded = codec.decode(&mut buf).unwrap().unwrap();
    assert_eq!(original, decoded);
}

#[test]
fn roundtrip_port_forward_data() {
    let mut codec = FrameCodec;
    let mut buf = BytesMut::new();
    let original = Frame::PortForwardData { channel_id: 3, data: Bytes::from("pf-payload") };
    codec.encode(original.clone(), &mut buf).unwrap();
    // type(1) + len(4) + channel_id(4) + data(10) = 19
    assert_eq!(buf.len(), 19);
    assert_eq!(buf[0], 0x43);
    let decoded = codec.decode(&mut buf).unwrap().unwrap();
    assert_eq!(original, decoded);
}

#[test]
fn roundtrip_port_forward_data_empty() {
    let mut codec = FrameCodec;
    let mut buf = BytesMut::new();
    let original = Frame::PortForwardData { channel_id: 0, data: Bytes::new() };
    codec.encode(original.clone(), &mut buf).unwrap();
    assert_eq!(buf.len(), 9); // type(1) + len(4) + channel_id(4)
    let decoded = codec.decode(&mut buf).unwrap().unwrap();
    assert_eq!(original, decoded);
}

#[test]
fn roundtrip_port_forward_close() {
    let mut codec = FrameCodec;
    let mut buf = BytesMut::new();
    let original = Frame::PortForwardClose { channel_id: 99 };
    codec.encode(original.clone(), &mut buf).unwrap();
    assert_eq!(buf.len(), 9); // type(1) + len(4) + channel_id(4)
    assert_eq!(buf[0], 0x44);
    let decoded = codec.decode(&mut buf).unwrap().unwrap();
    assert_eq!(original, decoded);
}

#[test]
fn roundtrip_port_forward_stop() {
    let mut codec = FrameCodec;
    let mut buf = BytesMut::new();
    let original = Frame::PortForwardStop { forward_id: 5 };
    codec.encode(original.clone(), &mut buf).unwrap();
    assert_eq!(buf.len(), 9); // type(1) + len(4) + forward_id(4)
    assert_eq!(buf[0], 0x45);
    let decoded = codec.decode(&mut buf).unwrap().unwrap();
    assert_eq!(original, decoded);
}

#[test]
fn roundtrip_port_forward_request() {
    let mut codec = FrameCodec;
    let mut buf = BytesMut::new();
    let original = Frame::PortForwardRequest {
        forward_id: 42,
        direction: 1,
        listen_port: 8080,
        target_port: 3000,
    };
    codec.encode(original.clone(), &mut buf).unwrap();
    assert_eq!(buf.len(), 14); // type(1) + len(4) + forward_id(4) + direction(1) + listen_port(2) + target_port(2)
    assert_eq!(buf[0], 0x46);
    let decoded = codec.decode(&mut buf).unwrap().unwrap();
    assert_eq!(original, decoded);
}

#[test]
fn port_forward_request_wrong_payload_size() {
    let mut codec = FrameCodec;
    let mut buf = BytesMut::new();
    buf.put_u8(0x46); // TYPE_PORT_FORWARD_REQUEST
    buf.put_u32(4); // wrong: should be 9
    buf.put_slice(&[0x00; 4]);
    let err = codec.decode(&mut buf).unwrap_err();
    assert_eq!(err.kind(), std::io::ErrorKind::InvalidData);
}

#[test]
fn port_forward_listen_wrong_payload_size() {
    let mut codec = FrameCodec;
    let mut buf = BytesMut::new();
    buf.put_u8(0x40); // TYPE_PORT_FORWARD_LISTEN
    buf.put_u32(4); // wrong: should be 8
    buf.put_slice(&[0x00; 4]);
    let err = codec.decode(&mut buf).unwrap_err();
    assert_eq!(err.kind(), std::io::ErrorKind::InvalidData);
}

#[test]
fn port_forward_open_wrong_payload_size() {
    let mut codec = FrameCodec;
    let mut buf = BytesMut::new();
    buf.put_u8(0x42); // TYPE_PORT_FORWARD_OPEN
    buf.put_u32(4); // wrong: should be 10
    buf.put_slice(&[0x00; 4]);
    let err = codec.decode(&mut buf).unwrap_err();
    assert_eq!(err.kind(), std::io::ErrorKind::InvalidData);
}

#[test]
fn port_forward_data_too_short() {
    let mut codec = FrameCodec;
    let mut buf = BytesMut::new();
    buf.put_u8(0x43); // TYPE_PORT_FORWARD_DATA
    buf.put_u32(2); // less than 4 bytes
    buf.put_slice(&[0x00, 0x00]);
    let err = codec.decode(&mut buf).unwrap_err();
    assert_eq!(err.kind(), std::io::ErrorKind::InvalidData);
}

#[test]
fn roundtrip_clipboard_set() {
    let mut codec = FrameCodec;
    let mut buf = BytesMut::new();
    let original = Frame::ClipboardSet { data: Bytes::from("copied text") };
    codec.encode(original.clone(), &mut buf).unwrap();
    assert_eq!(buf[0], 0x2A);
    let decoded = codec.decode(&mut buf).unwrap().unwrap();
    assert_eq!(original, decoded);
}

#[test]
fn roundtrip_clipboard_get() {
    let mut codec = FrameCodec;
    let mut buf = BytesMut::new();
    let original = Frame::ClipboardGet;
    codec.encode(original.clone(), &mut buf).unwrap();
    assert_eq!(buf.len(), 5); // type(1) + len(4) + no payload
    assert_eq!(buf[0], 0x2B);
    let decoded = codec.decode(&mut buf).unwrap().unwrap();
    assert_eq!(original, decoded);
}

#[test]
fn roundtrip_clipboard_data() {
    let mut codec = FrameCodec;
    let mut buf = BytesMut::new();
    let original = Frame::ClipboardData { data: Bytes::from("pasted text") };
    codec.encode(original.clone(), &mut buf).unwrap();
    assert_eq!(buf[0], 0x2C);
    let decoded = codec.decode(&mut buf).unwrap().unwrap();
    assert_eq!(original, decoded);
}

#[test]
fn roundtrip_clipboard_set_empty() {
    let mut codec = FrameCodec;
    let mut buf = BytesMut::new();
    let original = Frame::ClipboardSet { data: Bytes::new() };
    codec.encode(original.clone(), &mut buf).unwrap();
    let decoded = codec.decode(&mut buf).unwrap().unwrap();
    assert_eq!(original, decoded);
}

#[test]
fn resize_tolerates_trailing_bytes() {
    let mut codec = FrameCodec;
    let mut buf = BytesMut::new();
    codec.encode(Frame::Resize { cols: 80, rows: 24 }, &mut buf).unwrap();
    let old_len = u32::from_be_bytes([buf[1], buf[2], buf[3], buf[4]]);
    buf[1..5].copy_from_slice(&(old_len + 2).to_be_bytes());
    buf.extend_from_slice(&[0xFF, 0xFF]);
    let decoded = codec.decode(&mut buf).unwrap().unwrap();
    assert_eq!(decoded, Frame::Resize { cols: 80, rows: 24 });
}

#[test]
fn hello_tolerates_trailing_bytes() {
    let mut codec = FrameCodec;
    let mut buf = BytesMut::new();
    codec.encode(Frame::Hello { version: 9, capabilities: 1, device_id: 0x42 }, &mut buf).unwrap();
    let old_len = u32::from_be_bytes([buf[1], buf[2], buf[3], buf[4]]);
    buf[1..5].copy_from_slice(&(old_len + 4).to_be_bytes());
    buf.extend_from_slice(&[0x00, 0x00, 0x00, 0x42]);
    let decoded = codec.decode(&mut buf).unwrap().unwrap();
    assert_eq!(decoded, Frame::Hello { version: 9, capabilities: 1, device_id: 0x42 });
}

#[test]
fn session_created_tolerates_trailing_bytes() {
    let mut codec = FrameCodec;
    let mut buf = BytesMut::new();
    codec.encode(Frame::SessionCreated { id: 7 }, &mut buf).unwrap();
    let old_len = u32::from_be_bytes([buf[1], buf[2], buf[3], buf[4]]);
    buf[1..5].copy_from_slice(&(old_len + 2).to_be_bytes());
    buf.extend_from_slice(&[0xAB, 0xCD]);
    let decoded = codec.decode(&mut buf).unwrap().unwrap();
    assert_eq!(decoded, Frame::SessionCreated { id: 7 });
}
