// Integration tests for pam_unix — verifies public API behavior.

use pam_unix::{
    Client, Conversation, PamError, PamFlags, PamItemType, PamMessageStyle, PamResult,
    PamReturnCode, PasswordConv,
};
use std::ffi::{CStr, CString};

// ===========================================================================
// PamFlags — bitwise OR support (MED-12)
// ===========================================================================

#[test]
fn pam_flags_combine_with_or() {
    let flags = PamFlags::SILENT | PamFlags::DISALLOW_NULL_AUTHTOK;
    assert_eq!(
        flags.bits(),
        PamFlags::SILENT.bits() | PamFlags::DISALLOW_NULL_AUTHTOK.bits()
    );
}

#[test]
fn pam_flags_none_is_zero() {
    assert_eq!(PamFlags::NONE.bits(), 0);
}

#[test]
fn pam_flags_all_combinations_are_distinct() {
    let all = [
        PamFlags::SILENT,
        PamFlags::DISALLOW_NULL_AUTHTOK,
        PamFlags::ESTABLISH_CRED,
        PamFlags::DELETE_CRED,
        PamFlags::REINITIALIZE_CRED,
        PamFlags::REFRESH_CRED,
        PamFlags::CHANGE_EXPIRED_AUTHTOK,
        PamFlags::UPDATE_AUTHTOK,
        PamFlags::PRELIM_CHECK,
    ];
    // All individual flags must have unique bit values
    for i in 0..all.len() {
        for j in (i + 1)..all.len() {
            assert_ne!(
                all[i].bits(),
                all[j].bits(),
                "Flag collision at indices {i} and {j}"
            );
        }
    }
}

#[test]
fn pam_flags_debug_format() {
    let flags = PamFlags::SILENT | PamFlags::ESTABLISH_CRED;
    let debug = format!("{:?}", flags);
    assert!(debug.contains("PamFlags"), "Debug: {debug}");
}

// ===========================================================================
// PamReturnCode — From<i32> and Display
// ===========================================================================

#[test]
fn pam_return_code_from_known_value() {
    let code: PamReturnCode = 0i32.into();
    assert_eq!(code, PamReturnCode::Success);
}

#[test]
fn pam_return_code_from_auth_err() {
    let code: PamReturnCode = 7i32.into();
    assert_eq!(code, PamReturnCode::Auth_Err);
}

#[test]
fn pam_return_code_unknown_falls_to_system_err() {
    let code: PamReturnCode = 9999i32.into();
    assert_eq!(code, PamReturnCode::System_Err);
}

#[test]
fn pam_return_code_display_includes_name_and_value() {
    let code = PamReturnCode::Perm_Denied;
    let display = format!("{code}");
    assert!(display.contains("Perm_Denied"));
    assert!(display.contains('6'));
}

// ===========================================================================
// PamError — error handling
// ===========================================================================

#[test]
fn pam_error_from_code() {
    let err = PamError::from(PamReturnCode::Auth_Err);
    assert_eq!(err.0, PamReturnCode::Auth_Err);
}

#[test]
fn pam_error_display() {
    let err = PamError(PamReturnCode::Conv_Err);
    let display = format!("{err}");
    assert!(display.contains("Conv_Err"));
}

#[test]
fn pam_result_ok_and_err() {
    let ok: PamResult<i32> = Ok(42);
    assert_eq!(ok.unwrap(), 42);

    let err: PamResult<i32> = Err(PamError(PamReturnCode::Buf_Err));
    assert!(err.is_err());
}

// ===========================================================================
// PamMessageStyle — explicit discriminants (HIGH-04)
// ===========================================================================

#[test]
fn pam_message_style_discriminants_are_distinct() {
    let styles = [
        PamMessageStyle::Prompt_Echo_On as i32,
        PamMessageStyle::Prompt_Echo_Off as i32,
        PamMessageStyle::Error_Msg as i32,
        PamMessageStyle::Text_Info as i32,
        PamMessageStyle::Binary_Prompt as i32,
    ];
    for i in 0..styles.len() {
        for j in (i + 1)..styles.len() {
            assert_ne!(styles[i], styles[j], "Style collision");
        }
    }
}

#[test]
fn pam_message_style_unknown_maps_to_prompt_echo_on() {
    let style: PamMessageStyle = 99i32.into();
    assert_eq!(style, PamMessageStyle::Prompt_Echo_On);
}

// ===========================================================================
// PamItemType
// ===========================================================================

#[test]
fn pam_item_type_display_includes_name() {
    let item = PamItemType::User;
    let display = format!("{item}");
    assert!(display.contains("User"));
}

// ===========================================================================
// PasswordConv — credential storage and zeroization
// ===========================================================================

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

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

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

#[test]
fn password_conv_overwrites_credentials_safely() {
    let mut conv = PasswordConv::new();
    conv.set_credentials("old_user", "old_pass");
    // Overwrite — old credentials are zeroized by Zeroizing<String>
    conv.set_credentials("new_user", "new_pass");

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

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

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

// ===========================================================================
// Conversation trait — custom implementation
// ===========================================================================

struct TestConv {
    login: String,
    password: String,
}

impl Conversation for TestConv {
    fn prompt_echo(&mut self, _msg: &CStr) -> Result<CString, ()> {
        CString::new(self.login.clone()).map_err(|_| ())
    }
    fn prompt_blind(&mut self, _msg: &CStr) -> Result<CString, ()> {
        CString::new(self.password.clone()).map_err(|_| ())
    }
    fn info(&mut self, _msg: &CStr) {}
    fn error(&mut self, _msg: &CStr) {}
}

#[test]
fn custom_conversation_trait_works() {
    let mut conv = TestConv {
        login: "bob".into(),
        password: "pass".into(),
    };
    let echo = conv.prompt_echo(&CString::new(">").unwrap());
    assert_eq!(echo.unwrap().to_str().unwrap(), "bob");
}

// ===========================================================================
// Client — API surface (compilation + basic behavior checks)
// ===========================================================================

#[test]
fn client_with_password_creates_client() {
    // This test verifies the Client API compiles and constructs without panic.
    // We use "login" service which is commonly available, but don't actually
    // authenticate to avoid requiring real credentials.
    let client = Client::with_password("login");
    assert!(
        client.is_ok(),
        "Failed to create Client: {:?}",
        client.err()
    );
}

#[test]
fn client_close_on_drop_defaults_to_true() {
    let mut client = Client::with_password("login").unwrap();
    // Verify set_close_on_drop is accessible and works
    client.set_close_on_drop(false);
    // The field is private, so we can only test through the setter
}

#[test]
fn client_conversation_accessors() {
    let mut client = Client::with_password("login").unwrap();
    // Can access conversation mutably and immutably
    let _ = client.conversation();
    let conv = client.conversation_mut();
    conv.set_credentials("test", "test");
}

// ===========================================================================
// Stress / edge-case tests
// ===========================================================================

#[test]
fn password_conv_many_overwrites() {
    let mut conv = PasswordConv::new();
    for i in 0..100 {
        conv.set_credentials(&format!("user{i}"), &format!("pass{i}"));
    }
    let login = conv.prompt_echo(&CString::new(">").unwrap());
    assert_eq!(login.unwrap().to_str().unwrap(), "user99");
}

#[test]
fn password_conv_long_password() {
    let mut conv = PasswordConv::new();
    let long_pass = "A".repeat(10_000);
    conv.set_credentials("user", &long_pass);
    let result = conv.prompt_blind(&CString::new(">").unwrap());
    assert_eq!(result.unwrap().to_bytes().len(), 10_000);
}

#[test]
fn pam_flags_triple_combination() {
    let flags = PamFlags::ESTABLISH_CRED | PamFlags::REINITIALIZE_CRED | PamFlags::REFRESH_CRED;
    assert_eq!(
        flags.bits(),
        PamFlags::ESTABLISH_CRED.bits()
            | PamFlags::REINITIALIZE_CRED.bits()
            | PamFlags::REFRESH_CRED.bits()
    );
}

#[test]
fn pam_return_code_all_known_values_are_unique() {
    // Verify all known PAM return codes have unique i32 values
    let codes = [
        PamReturnCode::System_Err,
        PamReturnCode::Success,
        PamReturnCode::Open_Err,
        PamReturnCode::Symbol_Err,
        PamReturnCode::Service_Err,
        PamReturnCode::Buf_Err,
        PamReturnCode::Perm_Denied,
        PamReturnCode::Auth_Err,
        PamReturnCode::Cred_Insufficient,
        PamReturnCode::Authinfo_Unavail,
        PamReturnCode::User_Unknown,
        PamReturnCode::MaxTries,
        PamReturnCode::New_Authtok_Reqd,
        PamReturnCode::Acct_Expired,
        PamReturnCode::Session_Err,
        PamReturnCode::Cred_Unavail,
        PamReturnCode::Cred_Expired,
        PamReturnCode::Cred_Err,
        PamReturnCode::No_Module_Data,
        PamReturnCode::Conv_Err,
        PamReturnCode::AuthTok_Err,
        PamReturnCode::AuthTok_Recovery_Err,
        PamReturnCode::AuthTok_Lock_Busy,
        PamReturnCode::AuthTok_Disable_Aging,
        PamReturnCode::Try_Again,
        PamReturnCode::Ignore,
        PamReturnCode::AuthTok_Expired,
        PamReturnCode::Abort,
        PamReturnCode::Module_Unknown,
        PamReturnCode::Bad_Item,
        PamReturnCode::Conv_Again,
        PamReturnCode::Incomplete,
    ];
    let values: Vec<i32> = codes.iter().map(|c| *c as i32).collect();
    for i in 0..values.len() {
        for j in (i + 1)..values.len() {
            assert_ne!(values[i], values[j], "Duplicate PAM code at {i} and {j}");
        }
    }
}
