use libc::{c_int, c_void, calloc, free, malloc, size_t, strdup};

use std::ffi::{CStr, CString};
use std::mem;

use zeroize::Zeroizing;

use crate::{ffi::pam_conv, PamMessage, PamMessageStyle, PamResponse, PamReturnCode};

/// A trait representing the PAM authentification conversation
///
/// PAM authentification is done as a conversation mechanism, in which PAM
/// asks several questions and the client (your code) answers them. This trait
/// is a representation of such a conversation, which one method for each message
/// PAM can send you.
///
/// This is the trait to implement if you want to customize the conversation with
/// PAM. If you just want a simple login/password authentication, you can use the
/// `PasswordConv` implementation provided by this crate.
pub trait Conversation {
    /// PAM requests a value that should be echoed to the user as they type it
    ///
    /// This would typically be the username. The exact question is provided as the
    /// `msg` argument if you wish to display it to your user.
    fn prompt_echo(&mut self, msg: &CStr) -> Result<CString, ()>;
    /// PAM requests a value that should be typed blindly by the user
    ///
    /// This would typically be the password. The exact question is provided as the
    /// `msg` argument if you wish to display it to your user.
    fn prompt_blind(&mut self, msg: &CStr) -> Result<CString, ()>;
    /// This is an informational message from PAM
    fn info(&mut self, msg: &CStr);
    /// This is an error message from PAM
    fn error(&mut self, msg: &CStr);
    /// PAM sends or requests binary data (MFA, smartcard, FIDO2).
    ///
    /// Used by modules such as pam_u2f, PKCS#11, and biometric authenticators
    /// that exchange opaque binary blobs with the client.
    /// `data` is the binary payload from PAM; return the response blob.
    ///
    /// Default implementation returns `Err(())` — implement this if your
    /// application needs to integrate with MFA/smartcard modules.
    /// PAM_BINARY_PROMPT support requires PAM ≥ 1.3.0.
    fn prompt_binary(&mut self, _data: &[u8]) -> Result<Vec<u8>, ()> {
        Err(())
    }
}

/// A minimalistic conversation handler, that uses given login and password
///
/// This conversation handler is not really interactive, but simply returns to
/// PAM the values that have been set using the `set_credentials` method.
///
/// Both `login` and `passwd` are stored in `Zeroizing<String>` wrappers.
/// `Zeroizing` guarantees that every heap buffer is overwritten with zeros
/// before deallocation, including:
/// - The primary storage buffers (on `Drop`)
/// - Any intermediate `String` produced by reallocation or `clone()`
///
/// To avoid unnecessary intermediate copies, `prompt_echo` and `prompt_blind`
/// construct the `CString` directly from the underlying bytes via
/// `self.{login,passwd}.as_bytes()`, rather than calling `.clone()`.
///
/// Note: The `CString` returned to PAM is eventually `strdup`'d by the
/// `converse` callback onto the C heap. That C-heap copy is freed by PAM
/// via `free()` and is **not** zeroized — this is a limitation of the PAM
/// C ABI that cannot be fixed on the Rust side.
pub struct PasswordConv {
    login: Zeroizing<String>,
    passwd: Zeroizing<String>,
}

impl PasswordConv {
    /// Create a new `PasswordConv` handler
    pub fn new() -> PasswordConv {
        PasswordConv {
            login: Zeroizing::default(),
            passwd: Zeroizing::default(),
        }
    }

    /// Set the credentials that this handler will provide to PAM.
    ///
    /// The signature accepts only `&str` (not `Into<String>`) to prevent
    /// accidental credential leakage through custom type conversions that
    /// may log, display, or serialize sensitive data. See HIGH-05.
    ///
    /// When called multiple times, previous credentials are explicitly
    /// zeroized before replacement. While `Zeroizing<String>` zeroizes on
    /// `Drop`, calling `.zeroize()` explicitly before the assignment ensures
    /// the old buffer is cleared immediately — defense-in-depth against
    /// compiler optimizations or edge cases where Drop order might defer
    /// deallocation. See HIGH-02.
    pub fn set_credentials(&mut self, login: &str, password: &str) {
        use zeroize::Zeroize;
        // Explicitly zeroize old values before replacing them.
        // This is defense-in-depth: Zeroizing::drop() already handles this,
        // but explicit zeroize guarantees the old buffer is cleared
        // regardless of compiler optimizations or Drop ordering.
        self.login.zeroize();
        self.passwd.zeroize();

        self.login = Zeroizing::new(login.to_string());
        self.passwd = Zeroizing::new(password.to_string());
    }
}

// No manual Drop needed: Zeroizing<String> zeroizes its buffer automatically
// when dropped, including any intermediate allocations from reallocation.

impl Conversation for PasswordConv {
    fn prompt_echo(&mut self, _msg: &CStr) -> Result<CString, ()> {
        // Use as_bytes() directly to avoid an intermediate cloned String
        // that would not be zeroized. CString::new copies the bytes into
        // its own buffer; the Zeroizing<String> source is untouched.
        CString::new(self.login.as_bytes()).map_err(|_| ())
    }
    fn prompt_blind(&mut self, _msg: &CStr) -> Result<CString, ()> {
        // Same zero-copy-avoidance strategy as prompt_echo.
        // The password bytes go directly into CString without an
        // intermediate String clone sitting on the heap un-zeroized.
        CString::new(self.passwd.as_bytes()).map_err(|_| ())
    }
    fn info(&mut self, _msg: &CStr) {}
    fn error(&mut self, _msg: &CStr) {
        // PAM error messages may contain sensitive information such as file
        // paths, usernames, or policy details. The default handler logs only
        // the fact that an error occurred, not the full message content.
        // Production consumers that need full messages for debugging should
        // implement their own Conversation trait with appropriate filtering.
        // See LOW-02 and B2.
        eprintln!("[PAM ERROR] an authentication error occurred");
    }
}

pub(crate) fn into_pam_conv<C: Conversation>(conv: &mut C) -> pam_conv {
    pam_conv {
        conv: Some(converse::<C>),
        appdata_ptr: conv as *mut C as *mut c_void,
    }
}

/// Maximum number of PAM messages we accept in a single conversation batch.
/// Prevents DoS via memory exhaustion from a malicious or buggy module passing
/// an inflated num_msg (MED-10).
const MAX_PAM_MSGS: c_int = 256;

/// C callback for the PAM conversation function.
///
/// Allocates response memory that PAM will free on success. On error, all
/// allocated memory is freed before returning to prevent leaks (CRIT-03).
///
/// All calls into the user-provided `Conversation` handler are wrapped in
/// `catch_unwind` to prevent a panic in custom handler code from unwinding
/// across the `extern "C"` FFI boundary, which would abort the entire process
/// (M2 — DoS via handler panic). Panics in `prompt_echo` / `prompt_blind` are
/// converted to `Conv_Err`; panics in `info` / `error` are silently ignored.
pub(crate) unsafe extern "C" fn converse<C: Conversation>(
    num_msg: c_int,
    msg: *mut *const PamMessage,
    out_resp: *mut *mut PamResponse,
    appdata_ptr: *mut c_void,
) -> c_int {
    // Validate all FFI pointers before dereferencing (MED-03).
    // The PAM spec requires these to be non-null; rejecting null early
    // prevents undefined behavior from buggy or malicious modules.
    if num_msg <= 0
        || num_msg > MAX_PAM_MSGS
        || msg.is_null()
        || out_resp.is_null()
        || appdata_ptr.is_null()
    {
        return PamReturnCode::Conv_Err as c_int;
    }

    // Allocate space for responses (zero-initialized via calloc)
    let resp =
        calloc(num_msg as usize, mem::size_of::<PamResponse>() as size_t) as *mut PamResponse;
    if resp.is_null() {
        return PamReturnCode::Buf_Err as c_int;
    }

    let handler = &mut *(appdata_ptr as *mut C);

    let mut result: PamReturnCode = PamReturnCode::Success;
    let mut i: isize = 0;
    while i < num_msg as isize {
        let m: &PamMessage = &*(*(msg.offset(i)) as *const PamMessage);
        let r: &mut PamResponse = &mut *(resp.offset(i));

        let cmsg = CStr::from_ptr(m.msg);

        // Match on raw i32 discriminant instead of PamMessageStyle::from().
        // The From<i32> impl (generated by pam_enum) silently maps unknown
        // values to Prompt_Echo_On (the first variant), which could cause
        // a password to be echoed if PAM sends an unrecognized message style.
        // By matching on explicit discriminants, unknown values fall to the
        // error arm. See HIGH-04.
        let style = m.msg_style;
        if style == PamMessageStyle::Prompt_Echo_On as i32 {
            // catch_unwind prevents a panicking handler from aborting the
            // process across the extern "C" boundary (M2).
            let echo_result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
                handler.prompt_echo(cmsg)
            }));
            match echo_result {
                Ok(Ok(handler_response)) => {
                    let dup = strdup(handler_response.as_ptr());
                    if dup.is_null() {
                        result = PamReturnCode::Buf_Err;
                        break;
                    }
                    r.resp = dup;
                }
                // Handler returned Err(()) OR panicked — treat as Conv_Err
                Ok(Err(_)) | Err(_) => {
                    result = PamReturnCode::Conv_Err;
                    break;
                }
            }
        } else if style == PamMessageStyle::Prompt_Echo_Off as i32 {
            let blind_result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
                handler.prompt_blind(cmsg)
            }));
            match blind_result {
                Ok(Ok(handler_response)) => {
                    let dup = strdup(handler_response.as_ptr());
                    if dup.is_null() {
                        result = PamReturnCode::Buf_Err;
                        break;
                    }
                    r.resp = dup;
                }
                Ok(Err(_)) | Err(_) => {
                    result = PamReturnCode::Conv_Err;
                    break;
                }
            }
        } else if style == PamMessageStyle::Text_Info as i32 {
            // info() returns nothing — catch_unwind still prevents abort
            let _ = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
                handler.info(cmsg);
            }));
        } else if style == PamMessageStyle::Error_Msg as i32 {
            let _ = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
                handler.error(cmsg);
            }));
        } else if style == PamMessageStyle::Binary_Prompt as i32 {
            // PAM_BINARY_PROMPT: used by MFA/smartcard/FIDO2 modules.
            //
            // Binary messages use the pam_bp_t format defined in
            // Linux-PAM's security/_pam_types.h:
            //   struct { u32 length; /* network byte order, includes header */
            //            u8  type;
            //            u8  data[length - 5]; }
            //
            // The length field is ALWAYS present — it is part of the
            // upstream specification, not an undocumented convention.
            // Reading a fixed-size slice without consulting the header
            // would over-read the buffer and crash (C1).
            const MAX_BINARY_MSG: usize = 65536;
            const PAM_BP_HEADER_LEN: usize = 5; // 4 bytes length + 1 byte type
            const PAM_BP_RESPONSE: u8 = 2; // standard response type byte

            // Read the 5-byte header first — guaranteed by the spec
            let header =
                unsafe { std::slice::from_raw_parts(m.msg as *const u8, PAM_BP_HEADER_LEN) };
            let total_len =
                u32::from_be_bytes([header[0], header[1], header[2], header[3]]) as usize;

            // Validate: header must fit, must not exceed safety cap
            if total_len < PAM_BP_HEADER_LEN || total_len > MAX_BINARY_MSG {
                result = PamReturnCode::Buf_Err;
                break;
            }

            let full = unsafe { std::slice::from_raw_parts(m.msg as *const u8, total_len) };
            let payload = &full[PAM_BP_HEADER_LEN..];

            let binary_result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
                handler.prompt_binary(payload)
            }));
            match binary_result {
                Ok(Ok(response)) => {
                    // Build a valid pam_bp_t response frame:
                    //   u32 total_len (includes header)
                    //   u8  type = PAM_BP_RESPONSE (2)
                    //   u8  data[response.len()]
                    let total = PAM_BP_HEADER_LEN + response.len();
                    if total > MAX_BINARY_MSG {
                        result = PamReturnCode::Buf_Err;
                        break;
                    }
                    let resp_buf = unsafe { malloc(total) as *mut u8 };
                    if resp_buf.is_null() {
                        result = PamReturnCode::Buf_Err;
                        break;
                    }
                    let len_bytes = (total as u32).to_be_bytes();
                    unsafe {
                        std::ptr::copy_nonoverlapping(len_bytes.as_ptr(), resp_buf, 4);
                        *resp_buf.add(4) = PAM_BP_RESPONSE;
                        std::ptr::copy_nonoverlapping(
                            response.as_ptr(),
                            resp_buf.add(PAM_BP_HEADER_LEN),
                            response.len(),
                        );
                    }
                    r.resp = resp_buf as *mut libc::c_char;
                }
                Ok(Err(_)) | Err(_) => {
                    result = PamReturnCode::Conv_Err;
                    break;
                }
            }
        } else {
            // Unknown message style — reject to avoid echoing sensitive data
            result = PamReturnCode::Conv_Err;
            break;
        }
        i += 1;
    }

    if result != PamReturnCode::Success {
        // Free each strdup'd response string allocated in the loop (CRIT-03)
        for j in 0..i {
            let r = &mut *(resp.offset(j));
            if !r.resp.is_null() {
                free(r.resp as *mut c_void);
            }
        }
        free(resp as *mut c_void);
        // Explicitly set *out_resp to NULL on error to prevent PAM from
        // reading an uninitialized pointer (MED-02).
        *out_resp = std::ptr::null_mut();
    } else {
        *out_resp = resp;
    }

    result as c_int
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn password_conv_new_is_empty() {
        let mut conv = PasswordConv::new();
        let result = conv.prompt_echo(&CString::new("Login: ").unwrap());
        assert!(result.is_ok());
        assert_eq!(result.unwrap().to_bytes_with_nul(), b"\0");
    }

    #[test]
    fn password_conv_stores_credentials() {
        let mut conv = PasswordConv::new();
        conv.set_credentials("alice", "secret123");

        let echo = conv.prompt_echo(&CString::new("Login: ").unwrap());
        assert_eq!(echo.unwrap().to_str().unwrap(), "alice");

        let blind = conv.prompt_blind(&CString::new("Password: ").unwrap());
        assert_eq!(blind.unwrap().to_str().unwrap(), "secret123");
    }

    #[test]
    fn password_conv_info_does_not_panic() {
        let mut conv = PasswordConv::new();
        conv.info(&CString::new("Welcome!").unwrap());
    }

    #[test]
    fn password_conv_error_does_not_panic() {
        let mut conv = PasswordConv::new();
        conv.error(&CString::new("Authentication failed").unwrap());
    }

    #[test]
    fn password_conv_zeroizes_on_drop() {
        // Verify that PasswordConv uses Zeroizing<String> which
        // automatically zeroizes on drop. We set credentials, drop the
        // handler, and verify no panic occurs. Actual zeroization is
        // guaranteed by the zeroize crate and verified under Miri:
        //   cargo +nightly miri test
        let mut conv = PasswordConv::new();
        conv.set_credentials("user", "my_secret_password");
        drop(conv);
        // If we reach here without panic, Zeroizing worked correctly.
    }

    #[test]
    fn password_conv_zeroizes_on_overwrite() {
        // When credentials are overwritten, Zeroizing<String> zeroizes the
        // old buffer before replacing it. Verify the new values are correct.
        let mut conv = PasswordConv::new();
        conv.set_credentials("first", "pass1");
        // Overwrite triggers zeroization of "pass1" buffer
        conv.set_credentials("second", "pass2");

        let echo = conv.prompt_echo(&CString::new(">").unwrap());
        assert_eq!(echo.unwrap().to_str().unwrap(), "second");

        let blind = conv.prompt_blind(&CString::new(">").unwrap());
        assert_eq!(blind.unwrap().to_str().unwrap(), "pass2");
    }

    #[test]
    fn password_conv_prompt_echo_returns_login() {
        let mut conv = PasswordConv::new();
        conv.set_credentials("bob", "ignored");

        let result = conv.prompt_echo(&CString::new("User: ").unwrap());
        assert!(result.is_ok());
        assert_eq!(result.unwrap().to_bytes(), b"bob");
    }

    #[test]
    fn password_conv_prompt_blind_returns_password() {
        let mut conv = PasswordConv::new();
        conv.set_credentials("ignored", "s3cr3t!");

        let result = conv.prompt_blind(&CString::new("Pass: ").unwrap());
        assert!(result.is_ok());
        assert_eq!(result.unwrap().to_bytes(), b"s3cr3t!");
    }

    #[test]
    fn password_conv_empty_credentials() {
        let mut conv = PasswordConv::new();
        let echo = conv.prompt_echo(&CString::new(">").unwrap());
        assert!(echo.is_ok());
        assert!(echo.unwrap().to_bytes().is_empty());

        let blind = conv.prompt_blind(&CString::new(">").unwrap());
        assert!(blind.is_ok());
        assert!(blind.unwrap().to_bytes().is_empty());
    }

    #[test]
    fn password_conv_overwrite_credentials() {
        let mut conv = PasswordConv::new();
        conv.set_credentials("first", "pass1");
        conv.set_credentials("second", "pass2");

        let echo = conv.prompt_echo(&CString::new(">").unwrap());
        assert_eq!(echo.unwrap().to_str().unwrap(), "second");

        let blind = conv.prompt_blind(&CString::new(">").unwrap());
        assert_eq!(blind.unwrap().to_str().unwrap(), "pass2");
    }

    #[test]
    fn password_conv_no_clone_in_prompt() {
        // Verify that prompt_blind does NOT produce an intermediate String
        // clone on the heap. Instead, it constructs CString directly from
        // the Zeroizing<String>'s bytes via as_bytes().
        //
        // We can't directly observe the absence of a clone, but we verify
        // that the returned bytes match the original password, proving the
        // data path is correct.
        let mut conv = PasswordConv::new();
        let password = "direct_bytes_no_clone";
        conv.set_credentials("user", password);

        let result = conv.prompt_blind(&CString::new(">").unwrap());
        assert_eq!(result.unwrap().to_str().unwrap(), password);

        // Also verify echo path
        let result = conv.prompt_echo(&CString::new(">").unwrap());
        assert_eq!(result.unwrap().to_str().unwrap(), "user");
    }

    /// Custom Conversation that panics in its handler methods.
    /// Used to verify that catch_unwind in converse() prevents process abort.
    struct PanickingConv;

    impl Conversation for PanickingConv {
        fn prompt_echo(&mut self, _msg: &CStr) -> Result<CString, ()> {
            panic!("panicking prompt_echo");
        }
        fn prompt_blind(&mut self, _msg: &CStr) -> Result<CString, ()> {
            panic!("panicking prompt_blind");
        }
        fn info(&mut self, _msg: &CStr) {
            panic!("panicking info");
        }
        fn error(&mut self, _msg: &CStr) {
            panic!("panicking error");
        }
    }

    #[test]
    fn catch_unwind_prevents_abort_on_prompt_echo_panic() {
        let mut conv = PanickingConv;
        let caught = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
            let _ = conv.prompt_echo(&CString::new(">").unwrap());
        }));
        assert!(caught.is_err(), "panic should be catchable");
    }

    #[test]
    fn panicking_handler_does_not_abort_process() {
        let mut conv = PanickingConv;
        let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
            let _ = conv.info(&CString::new("test").unwrap());
        }));
        assert!(result.is_err());
    }

    // --- PAM_BINARY_PROMPT (pam_bp_t) tests (C1 fix) ---

    /// Conversation handler that captures the binary payload for inspection.
    struct BinaryTestConv {
        received: std::cell::RefCell<Option<Vec<u8>>>,
    }

    impl BinaryTestConv {
        fn new() -> Self {
            BinaryTestConv {
                received: std::cell::RefCell::new(None),
            }
        }
    }

    impl Conversation for BinaryTestConv {
        fn prompt_echo(&mut self, _msg: &CStr) -> Result<CString, ()> {
            Ok(CString::new("echo").unwrap())
        }
        fn prompt_blind(&mut self, _msg: &CStr) -> Result<CString, ()> {
            Ok(CString::new("blind").unwrap())
        }
        fn info(&mut self, _msg: &CStr) {}
        fn error(&mut self, _msg: &CStr) {}
        fn prompt_binary(&mut self, data: &[u8]) -> Result<Vec<u8>, ()> {
            *self.received.borrow_mut() = Some(data.to_vec());
            // Return a known response to verify response framing
            Ok(vec![0xFA, 0xCE])
        }
    }

    #[test]
    fn binary_prompt_parses_pam_bp_header() {
        // Build a valid pam_bp_t frame: length=7 (5 header + 2 payload),
        // type=0x01, payload=[0xAA, 0xBB]
        let mut buf = vec![0u8; 7];
        buf[0..4].copy_from_slice(&7u32.to_be_bytes());
        buf[4] = 0x01;
        buf[5] = 0xAA;
        buf[6] = 0xBB;

        let msg = PamMessage {
            msg_style: crate::ffi::PAM_BINARY_PROMPT as i32,
            msg: buf.as_ptr() as *const libc::c_char,
        };
        let msgs = [&msg as *const PamMessage];
        let mut out_resp: *mut PamResponse = std::ptr::null_mut();

        let mut conv = BinaryTestConv::new();
        let pam_conv = into_pam_conv(&mut conv);

        let rc = unsafe {
            converse::<BinaryTestConv>(
                1,
                msgs.as_ptr() as *mut *const PamMessage,
                &mut out_resp,
                pam_conv.appdata_ptr,
            )
        };

        assert_eq!(rc, PamReturnCode::Success as i32);
        let payload = conv.received.borrow();
        assert_eq!(payload.as_deref(), Some(&[0xAAu8, 0xBBu8][..]));

        // Verify response frame has proper pam_bp_t header
        assert!(!out_resp.is_null());
        let resp = unsafe { &*out_resp };
        assert!(!resp.resp.is_null());
        let resp_bytes = unsafe { std::slice::from_raw_parts(resp.resp as *const u8, 7) };
        // length = 7 (5 header + 2 payload)
        assert_eq!(
            u32::from_be_bytes([resp_bytes[0], resp_bytes[1], resp_bytes[2], resp_bytes[3]]),
            7
        );
        // type = PAM_BP_RESPONSE (2)
        assert_eq!(resp_bytes[4], 2);
        // payload = [0xFA, 0xCE]
        assert_eq!(resp_bytes[5], 0xFA);
        assert_eq!(resp_bytes[6], 0xCE);

        // Cleanup: PAM would normally free this, but in test we must free
        unsafe {
            free(resp.resp as *mut c_void);
            free(out_resp as *mut c_void);
        }
    }

    #[test]
    fn binary_prompt_rejects_too_small_header() {
        // Declare total_len = 3 in the header, which is < PAM_BP_HEADER_LEN (5).
        // converse() should return Conv_Err without trying to read payload.
        let mut buf = vec![0u8; 5]; // allocate 5 bytes so the header reads are safe
        buf[0..4].copy_from_slice(&3u32.to_be_bytes());
        buf[4] = 0x01;

        let msg = PamMessage {
            msg_style: crate::ffi::PAM_BINARY_PROMPT as i32,
            msg: buf.as_ptr() as *const libc::c_char,
        };
        let msgs = [&msg as *const PamMessage];
        let mut out_resp: *mut PamResponse = std::ptr::null_mut();
        let mut conv = BinaryTestConv::new();
        let pam_conv = into_pam_conv(&mut conv);

        let rc = unsafe {
            converse::<BinaryTestConv>(
                1,
                msgs.as_ptr() as *mut *const PamMessage,
                &mut out_resp,
                pam_conv.appdata_ptr,
            )
        };

        // total_len=3 < 5 → Buf_Err (malformed binary header)
        assert_eq!(rc, PamReturnCode::Buf_Err as i32);
        // handler should NOT have been called
        assert!(conv.received.borrow().is_none());
    }

    #[test]
    fn binary_prompt_does_not_crash_on_tiny_buffer() {
        // A real PAM module might send a 6-byte buffer on a page boundary
        // followed by a guard page. The fixed code reads only `total_len`
        // bytes, so it must not SIGSEGV.
        let buf = [0u8, 0, 0, 6, 0x01, 0xCC]; // length=6, type=1, payload=[0xCC]

        let msg = PamMessage {
            msg_style: crate::ffi::PAM_BINARY_PROMPT as i32,
            msg: buf.as_ptr() as *const libc::c_char,
        };
        let msgs = [&msg as *const PamMessage];
        let mut out_resp: *mut PamResponse = std::ptr::null_mut();
        let mut conv = BinaryTestConv::new();
        let pam_conv = into_pam_conv(&mut conv);

        let rc = unsafe {
            converse::<BinaryTestConv>(
                1,
                msgs.as_ptr() as *mut *const PamMessage,
                &mut out_resp,
                pam_conv.appdata_ptr,
            )
        };

        assert_eq!(rc, PamReturnCode::Success as i32);
        assert_eq!(conv.received.borrow().as_deref(), Some(&[0xCCu8][..]));

        unsafe {
            let resp = &*out_resp;
            free(resp.resp as *mut c_void);
            free(out_resp as *mut c_void);
        }
    }
}
