use crate::channels::traits::{Channel, ChannelMessage, SendMessage};
use async_trait::async_trait;
use base64::{engine::general_purpose, Engine as _};
use std::sync::atomic::{AtomicU64, Ordering};
use std::sync::Arc;
use tokio::io::{AsyncBufReadExt, AsyncWriteExt, BufReader};
use tokio::sync::{mpsc, Mutex};
use zeroize::Zeroizing;

// Use tokio_rustls's re-export of rustls types
use tokio_rustls::rustls;

/// Read timeout for IRC — if no data arrives within this duration, the
/// connection is considered dead. IRC servers typically PING every 60-120s.
const READ_TIMEOUT: std::time::Duration = std::time::Duration::from_mins(5);

/// Per-phase timeout applied independently to TCP connect and TLS handshake.
/// Total setup wait can be up to 2x this value.
const CONNECT_TIMEOUT: std::time::Duration = std::time::Duration::from_secs(20);

/// Monotonic counter to ensure unique message IDs under burst traffic.
static MSG_SEQ: AtomicU64 = AtomicU64::new(0);

/// IRC over TLS channel.
///
/// Connects to an IRC server using TLS, joins configured channels,
/// and forwards PRIVMSG messages to the `Corvus` message bus.
/// Supports both channel messages and private messages (DMs).
pub struct IrcChannel {
    server: String,
    port: u16,
    nickname: String,
    username: String,
    channels: Vec<String>,
    allowed_users: Vec<String>,
    server_password: Option<String>,
    nickserv_password: Option<String>,
    sasl_password: Option<String>,
    verify_tls: bool,
    /// Shared write half of the TLS stream for sending messages.
    writer: Arc<Mutex<Option<WriteHalf>>>,
}

type WriteHalf = tokio::io::WriteHalf<tokio_rustls::client::TlsStream<tokio::net::TcpStream>>;

/// Style instruction prepended to every IRC message before it reaches the LLM.
/// IRC clients render plain text only — no markdown, no HTML, no XML.
const IRC_STYLE_PREFIX: &str = "\
[context: you are responding over IRC. \
Plain text only. No markdown, no tables, no XML/HTML tags. \
Never use triple backtick code fences. Use a single blank line to separate blocks instead. \
Be terse and concise. \
Use short lines. Avoid walls of text.]\n";

/// Reserved bytes for the server-prepended sender prefix (`:nick!user@host `).
const SENDER_PREFIX_RESERVE: usize = 64;

/// A parsed IRC message.
#[derive(Debug, Clone, PartialEq, Eq)]
struct IrcMessage {
    prefix: Option<String>,
    command: String,
    params: Vec<String>,
}

impl IrcMessage {
    /// Parse a raw IRC line into an `IrcMessage`.
    ///
    /// IRC format: `[:<prefix>] <command> [<params>] [:<trailing>]`
    fn parse(line: &str) -> Option<Self> {
        let line = line.trim_end_matches(['\r', '\n']);
        if line.is_empty() {
            return None;
        }

        let (prefix, rest) = if let Some(stripped) = line.strip_prefix(':') {
            let space = stripped.find(' ')?;
            (Some(stripped[..space].to_string()), &stripped[space + 1..])
        } else {
            (None, line)
        };

        // Split at trailing (first `:` after command/params)
        let (params_part, trailing) = if let Some(colon_pos) = rest.find(" :") {
            (&rest[..colon_pos], Some(&rest[colon_pos + 2..]))
        } else {
            (rest, None)
        };

        let mut parts: Vec<&str> = params_part.split_whitespace().collect();
        if parts.is_empty() {
            return None;
        }

        let command = parts.remove(0).to_uppercase();
        let mut params: Vec<String> = parts.iter().map(std::string::ToString::to_string).collect();
        if let Some(t) = trailing {
            params.push(t.to_string());
        }

        Some(IrcMessage {
            prefix,
            command,
            params,
        })
    }

    /// Extract the nickname from the prefix (nick!user@host → nick).
    fn nick(&self) -> Option<&str> {
        self.prefix.as_ref().and_then(|p| {
            let end = p.find('!').unwrap_or(p.len());
            let nick = &p[..end];
            if nick.is_empty() {
                None
            } else {
                Some(nick)
            }
        })
    }
}

/// Encode SASL PLAIN credentials: base64(\0nick\0password).
fn encode_sasl_plain(nick: &str, password: &str) -> String {
    let input = Zeroizing::new(format!("\0{nick}\0{password}"));
    general_purpose::STANDARD.encode(input.as_bytes())
}

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum SaslCapAction {
    StartAuthenticatePlain,
    EndCapAndDisable,
}

fn sasl_cap_action(msg: &IrcMessage, sasl_pending: bool) -> Option<SaslCapAction> {
    if !sasl_pending || msg.command != "CAP" || !msg.params.iter().any(|p| p == "sasl") {
        return None;
    }

    if msg.params.iter().any(|p| p == "ACK") {
        Some(SaslCapAction::StartAuthenticatePlain)
    } else if msg.params.iter().any(|p| p == "NAK") {
        Some(SaslCapAction::EndCapAndDisable)
    } else {
        None
    }
}

/// Split a base64 SASL payload into RFC-compliant IRCv3 AUTHENTICATE chunks.
///
/// Each chunk must be <= 400 bytes. If payload length is an exact multiple of
/// 400, send a final `+` marker to terminate the exchange.
fn split_sasl_authenticate_payload(encoded: &str) -> Vec<String> {
    const SASL_CHUNK_MAX: usize = 400;
    // `encode_sasl_plain` produces base64 ASCII, so byte slicing is safe here.
    debug_assert!(encoded.is_ascii());

    let mut chunks = Vec::new();
    let mut start = 0;
    while start < encoded.len() {
        let end = (start + SASL_CHUNK_MAX).min(encoded.len());
        chunks.push(encoded[start..end].to_string());
        start = end;
    }

    if encoded.is_empty() || encoded.len().is_multiple_of(SASL_CHUNK_MAX) {
        chunks.push("+".to_string());
    }

    chunks
}

/// Action returned by `dispatch_irc_command` to communicate state changes back to the listen loop.
enum IrcAction {
    None,
    SetRegistered,
    SetNick(String),
    SetSasl(bool),
    ChannelClosed,
    Fatal(String),
}

/// Split a message into lines safe for IRC transmission.
///
/// IRC is a line-based protocol — `\r\n` terminates each command, so any
/// newline inside a PRIVMSG payload would truncate the message and turn the
/// remainder into garbled/invalid IRC commands.
///
/// This function:
/// 1. Splits on `\n` (and strips `\r`) so each logical line becomes its own PRIVMSG.
/// 2. Splits any line that exceeds `max_bytes` at a safe UTF-8 boundary.
/// 3. Skips empty lines to avoid sending blank PRIVMSGs.
fn split_message(message: &str, max_bytes: usize) -> Vec<String> {
    let mut chunks = Vec::new();

    // Guard against max_bytes == 0 to prevent infinite loop
    if max_bytes == 0 {
        chunks.push(collapse_to_single_line(message));
        return chunks;
    }

    for line in message.split('\n') {
        let line = line.trim_end_matches('\r');
        if line.is_empty() {
            continue;
        }

        if line.len() <= max_bytes {
            chunks.push(line.to_string());
        } else {
            split_line_at_utf8_boundaries(line, max_bytes, &mut chunks);
        }
    }

    if chunks.is_empty() {
        chunks.push(String::new());
    }

    chunks
}

/// Collapse all non-empty lines into a single space-separated string.
fn collapse_to_single_line(message: &str) -> String {
    let full: String = message
        .lines()
        .map(|l| l.trim_end_matches('\r'))
        .filter(|l| !l.is_empty())
        .collect::<Vec<_>>()
        .join(" ");
    if full.is_empty() {
        String::new()
    } else {
        full
    }
}

/// Split a single line into chunks at safe UTF-8 char boundaries.
fn split_line_at_utf8_boundaries(line: &str, max_bytes: usize, chunks: &mut Vec<String>) {
    let mut remaining = line;
    while !remaining.is_empty() {
        if remaining.len() <= max_bytes {
            chunks.push(remaining.to_string());
            break;
        }

        let split_at = find_utf8_split_point(remaining, max_bytes);
        chunks.push(remaining[..split_at].to_string());
        remaining = &remaining[split_at..];
    }
}

/// Find a safe UTF-8 boundary to split at, preferring backward search from `max_bytes`.
fn find_utf8_split_point(text: &str, max_bytes: usize) -> usize {
    let mut split_at = max_bytes;
    while split_at > 0 && !text.is_char_boundary(split_at) {
        split_at -= 1;
    }
    if split_at == 0 {
        // No valid boundary found going backward — advance forward instead
        split_at = max_bytes;
        while split_at < text.len() && !text.is_char_boundary(split_at) {
            split_at += 1;
        }
    }
    split_at
}

/// Configuration for constructing an `IrcChannel`.
pub struct IrcChannelConfig {
    pub server: String,
    pub port: u16,
    pub nickname: String,
    pub username: Option<String>,
    pub channels: Vec<String>,
    pub allowed_users: Vec<String>,
    pub server_password: Option<String>,
    pub nickserv_password: Option<String>,
    pub sasl_password: Option<String>,
    pub verify_tls: bool,
}

impl IrcChannel {
    pub fn new(cfg: IrcChannelConfig) -> Self {
        let username = cfg.username.unwrap_or_else(|| cfg.nickname.clone());
        Self {
            server: cfg.server,
            port: cfg.port,
            nickname: cfg.nickname,
            username,
            channels: cfg.channels,
            allowed_users: cfg.allowed_users,
            server_password: cfg.server_password,
            nickserv_password: cfg.nickserv_password,
            sasl_password: cfg.sasl_password,
            verify_tls: cfg.verify_tls,
            writer: Arc::new(Mutex::new(None)),
        }
    }

    fn is_user_allowed(&self, nick: &str) -> bool {
        if self.allowed_users.iter().any(|u| u == "*") {
            return true;
        }
        self.allowed_users
            .iter()
            .any(|u| u.eq_ignore_ascii_case(nick))
    }

    async fn send_raw(writer: &mut WriteHalf, line: &str) -> anyhow::Result<()> {
        let data = format!("{line}\r\n");
        writer.write_all(data.as_bytes()).await?;
        writer.flush().await?;
        Ok(())
    }

    async fn send_to_writer(&self, msg: &str) -> anyhow::Result<()> {
        let mut guard = self.writer.lock().await;
        if let Some(ref mut w) = *guard {
            Self::send_raw(w, msg).await?;
        }
        Ok(())
    }

    async fn handle_registration(&self, writer: &mut WriteHalf) -> anyhow::Result<()> {
        let current_nick = self.nickname.clone();

        if let Some(ref pass) = self.server_password {
            Self::send_raw(writer, &format!("PASS {pass}")).await?;
        }

        Self::send_raw(writer, &format!("NICK {current_nick}")).await?;
        Self::send_raw(writer, &format!("USER {} 0 * :Corvus", self.username)).await?;

        Ok(())
    }

    async fn handle_welcome(&self, _current_nick: &str) -> anyhow::Result<()> {
        if let Some(ref pass) = self.nickserv_password {
            self.send_to_writer(&format!("PRIVMSG NickServ :IDENTIFY {pass}"))
                .await?;
        }

        for chan in &self.channels {
            self.send_to_writer(&format!("JOIN {chan}")).await?;
        }

        Ok(())
    }

    fn parse_privmsg(&self, msg: &IrcMessage, sender_nick: &str) -> Option<ChannelMessage> {
        let target = msg.params.first()?;
        let text = msg.params.get(1)?;

        if sender_nick.eq_ignore_ascii_case("NickServ")
            || sender_nick.eq_ignore_ascii_case("ChanServ")
        {
            return None;
        }

        if !self.is_user_allowed(sender_nick) {
            return None;
        }

        let is_channel = target.starts_with('#') || target.starts_with('&');
        let reply_to = if is_channel {
            target.to_string()
        } else {
            sender_nick.to_string()
        };
        let content = if is_channel {
            format!("{IRC_STYLE_PREFIX}<{sender_nick}> {text}")
        } else {
            format!("{IRC_STYLE_PREFIX}{text}")
        };

        let seq = MSG_SEQ.fetch_add(1, Ordering::Relaxed);
        Some(ChannelMessage {
            id: format!("irc_{}_{seq}", chrono::Utc::now().timestamp_millis()),
            sender: sender_nick.to_string(),
            reply_target: reply_to,
            content,
            channel: "irc".to_string(),
            timestamp: std::time::SystemTime::now()
                .duration_since(std::time::UNIX_EPOCH)
                .unwrap_or_default()
                .as_secs(),
            parts: vec![],
        })
    }

    async fn handle_cap_message(
        &self,
        msg: &IrcMessage,
        sasl_pending: bool,
        sasl_active: &mut bool,
    ) -> anyhow::Result<()> {
        match sasl_cap_action(msg, sasl_pending) {
            Some(SaslCapAction::StartAuthenticatePlain) => {
                self.send_to_writer("AUTHENTICATE PLAIN").await?;
            }
            Some(SaslCapAction::EndCapAndDisable) => {
                tracing::warn!("IRC server does not support SASL, continuing without it");
                *sasl_active = false;
                self.send_to_writer("CAP END").await?;
            }
            None => {}
        }
        Ok(())
    }

    async fn handle_authenticate(
        &self,
        msg: &IrcMessage,
        sasl_pending: bool,
        current_nick: &str,
        sasl_active: &mut bool,
    ) -> anyhow::Result<()> {
        if sasl_pending && msg.params.first().is_some_and(|p| p == "+") {
            if let Some(password) = self.sasl_password.as_deref() {
                let encoded = encode_sasl_plain(current_nick, password);
                for chunk in split_sasl_authenticate_payload(&encoded) {
                    self.send_to_writer(&format!("AUTHENTICATE {chunk}"))
                        .await?;
                }
            } else {
                tracing::warn!(
                    "SASL authentication requested but no SASL password is configured; aborting SASL"
                );
                *sasl_active = false;
                self.send_to_writer("CAP END").await?;
            }
        }
        Ok(())
    }

    /// Dispatch a single parsed IRC command, returning an action for the listen loop.
    async fn dispatch_irc_command(
        &self,
        msg: &IrcMessage,
        tx: &mpsc::Sender<ChannelMessage>,
        registered: bool,
        sasl_pending: bool,
        current_nick: &str,
    ) -> anyhow::Result<IrcAction> {
        match msg.command.as_str() {
            "PING" => {
                let token = msg.params.first().map_or("", String::as_str);
                self.send_to_writer(&format!("PONG :{token}")).await?;
            }
            "CAP" => {
                let mut sasl = sasl_pending;
                self.handle_cap_message(msg, sasl_pending, &mut sasl)
                    .await?;
                if sasl != sasl_pending {
                    return Ok(IrcAction::SetSasl(sasl));
                }
            }
            "AUTHENTICATE" => {
                let mut sasl = sasl_pending;
                self.handle_authenticate(msg, sasl_pending, current_nick, &mut sasl)
                    .await?;
                if sasl != sasl_pending {
                    return Ok(IrcAction::SetSasl(sasl));
                }
            }
            "903" => {
                self.send_to_writer("CAP END").await?;
                return Ok(IrcAction::SetSasl(false));
            }
            "904" | "905" | "906" | "907" => {
                tracing::warn!("IRC SASL authentication failed ({})", msg.command);
                self.send_to_writer("CAP END").await?;
                return Ok(IrcAction::SetSasl(false));
            }
            "001" => {
                tracing::info!("IRC registered as {}", current_nick);
                self.handle_welcome(current_nick).await?;
                return Ok(IrcAction::SetRegistered);
            }
            "433" => {
                let alt = format!("{current_nick}_");
                tracing::warn!("IRC nickname {current_nick} is in use, trying {alt}");
                self.send_to_writer(&format!("NICK {alt}")).await?;
                return Ok(IrcAction::SetNick(alt));
            }
            "PRIVMSG" if registered => {
                let sender_nick = msg.nick().unwrap_or("unknown");
                if let Some(channel_msg) = self.parse_privmsg(msg, sender_nick) {
                    if tx.send(channel_msg).await.is_err() {
                        return Ok(IrcAction::ChannelClosed);
                    }
                }
            }
            "464" => {
                return Ok(IrcAction::Fatal("IRC password mismatch".to_string()));
            }
            _ => {}
        }
        Ok(IrcAction::None)
    }

    /// Create a TLS connection to the IRC server.
    async fn connect(
        &self,
    ) -> anyhow::Result<tokio_rustls::client::TlsStream<tokio::net::TcpStream>> {
        let addr = format!("{}:{}", self.server, self.port);
        let tcp = tokio::time::timeout(CONNECT_TIMEOUT, tokio::net::TcpStream::connect(&addr))
            .await
            .map_err(|_| {
                anyhow::anyhow!("IRC TCP connect timed out after {CONNECT_TIMEOUT:?} to {addr}")
            })??;

        let tls_config = if self.verify_tls {
            let root_store: rustls::RootCertStore =
                webpki_roots::TLS_SERVER_ROOTS.iter().cloned().collect();
            rustls::ClientConfig::builder()
                .with_root_certificates(root_store)
                .with_no_client_auth()
        } else {
            rustls::ClientConfig::builder()
                .dangerous()
                .with_custom_certificate_verifier(Arc::new(NoVerify))
                .with_no_client_auth()
        };

        let connector = tokio_rustls::TlsConnector::from(Arc::new(tls_config));
        let domain = rustls::pki_types::ServerName::try_from(self.server.clone())?;
        let tls = tokio::time::timeout(CONNECT_TIMEOUT, connector.connect(domain, tcp))
            .await
            .map_err(|_| {
                anyhow::anyhow!(
                    "IRC TLS handshake timed out after {CONNECT_TIMEOUT:?} with {}",
                    self.server
                )
            })??;

        Ok(tls)
    }
}

/// Certificate verifier that accepts any certificate (for `verify_tls=false`).
#[derive(Debug)]
struct NoVerify;

impl rustls::client::danger::ServerCertVerifier for NoVerify {
    fn verify_server_cert(
        &self,
        _end_entity: &rustls::pki_types::CertificateDer<'_>,
        _intermediates: &[rustls::pki_types::CertificateDer<'_>],
        _server_name: &rustls::pki_types::ServerName<'_>,
        _ocsp_response: &[u8],
        _now: rustls::pki_types::UnixTime,
    ) -> Result<rustls::client::danger::ServerCertVerified, rustls::Error> {
        Ok(rustls::client::danger::ServerCertVerified::assertion())
    }

    fn verify_tls12_signature(
        &self,
        _message: &[u8],
        _cert: &rustls::pki_types::CertificateDer<'_>,
        _dss: &rustls::DigitallySignedStruct,
    ) -> Result<rustls::client::danger::HandshakeSignatureValid, rustls::Error> {
        Ok(rustls::client::danger::HandshakeSignatureValid::assertion())
    }

    fn verify_tls13_signature(
        &self,
        _message: &[u8],
        _cert: &rustls::pki_types::CertificateDer<'_>,
        _dss: &rustls::DigitallySignedStruct,
    ) -> Result<rustls::client::danger::HandshakeSignatureValid, rustls::Error> {
        Ok(rustls::client::danger::HandshakeSignatureValid::assertion())
    }

    fn supported_verify_schemes(&self) -> Vec<rustls::SignatureScheme> {
        rustls::crypto::ring::default_provider()
            .signature_verification_algorithms
            .supported_schemes()
    }
}

#[async_trait]
#[allow(clippy::too_many_lines)]
impl Channel for IrcChannel {
    fn name(&self) -> &str {
        "irc"
    }

    async fn send(&self, message: &SendMessage) -> anyhow::Result<()> {
        let mut guard = self.writer.lock().await;
        let writer = guard
            .as_mut()
            .ok_or_else(|| anyhow::anyhow!("IRC not connected"))?;

        // Calculate safe payload size:
        // 512 - sender prefix (~64 bytes for :nick!user@host) - "PRIVMSG " - target - " :" - "\r\n"
        let overhead = SENDER_PREFIX_RESERVE + 10 + message.recipient.len() + 2;
        let max_payload = 512_usize.saturating_sub(overhead);
        let chunks = split_message(&message.content, max_payload);

        for chunk in chunks {
            Self::send_raw(writer, &format!("PRIVMSG {} :{chunk}", message.recipient)).await?;
        }

        Ok(())
    }

    async fn listen(&self, tx: mpsc::Sender<ChannelMessage>) -> anyhow::Result<()> {
        tracing::info!(
            "IRC channel connecting to {}:{} as {}...",
            self.server,
            self.port,
            self.nickname
        );

        let tls = self.connect().await?;
        let (reader, mut writer) = tokio::io::split(tls);

        if self.sasl_password.is_some() {
            Self::send_raw(&mut writer, "CAP REQ :sasl").await?;
        }

        self.handle_registration(&mut writer).await?;

        {
            let mut guard = self.writer.lock().await;
            *guard = Some(writer);
        }

        let mut buf_reader = BufReader::new(reader);
        let mut line = String::new();
        let mut registered = false;
        let mut sasl_pending = self.sasl_password.is_some();
        let mut current_nick = self.nickname.clone();

        loop {
            line.clear();
            let n = tokio::time::timeout(READ_TIMEOUT, buf_reader.read_line(&mut line))
                .await
                .map_err(|_| {
                    anyhow::anyhow!("IRC read timed out (no data for {READ_TIMEOUT:?})")
                })??;
            if n == 0 {
                anyhow::bail!("IRC connection closed by server");
            }

            let Some(msg) = IrcMessage::parse(&line) else {
                continue;
            };

            let action = self
                .dispatch_irc_command(&msg, &tx, registered, sasl_pending, &current_nick)
                .await?;

            match action {
                IrcAction::None => {}
                IrcAction::SetRegistered => {
                    registered = true;
                }
                IrcAction::SetNick(nick) => {
                    current_nick = nick;
                }
                IrcAction::SetSasl(val) => {
                    sasl_pending = val;
                }
                IrcAction::ChannelClosed => {
                    return Ok(());
                }
                IrcAction::Fatal(msg) => {
                    anyhow::bail!("{msg}");
                }
            }
        }
    }

    async fn health_check(&self) -> bool {
        // Lightweight connectivity check: TLS connect + QUIT
        match self.connect().await {
            Ok(tls) => {
                let (_, mut writer) = tokio::io::split(tls);
                let _ = Self::send_raw(&mut writer, "QUIT :health check").await;
                true
            }
            Err(_) => false,
        }
    }
}

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

    // ── IRC message parsing ──────────────────────────────────

    #[test]
    fn parse_privmsg_with_prefix() {
        let msg = IrcMessage::parse(":nick!user@host PRIVMSG #channel :Hello world").unwrap();
        assert_eq!(msg.prefix.as_deref(), Some("nick!user@host"));
        assert_eq!(msg.command, "PRIVMSG");
        assert_eq!(msg.params, vec!["#channel", "Hello world"]);
    }

    #[test]
    fn parse_privmsg_dm() {
        let msg = IrcMessage::parse(":alice!a@host PRIVMSG botname :hi there").unwrap();
        assert_eq!(msg.command, "PRIVMSG");
        assert_eq!(msg.params, vec!["botname", "hi there"]);
        assert_eq!(msg.nick(), Some("alice"));
    }

    #[test]
    fn parse_ping() {
        let msg = IrcMessage::parse("PING :server.example.com").unwrap();
        assert!(msg.prefix.is_none());
        assert_eq!(msg.command, "PING");
        assert_eq!(msg.params, vec!["server.example.com"]);
    }

    #[test]
    fn parse_numeric_reply() {
        let msg = IrcMessage::parse(":server 001 botname :Welcome to the IRC network").unwrap();
        assert_eq!(msg.prefix.as_deref(), Some("server"));
        assert_eq!(msg.command, "001");
        assert_eq!(msg.params, vec!["botname", "Welcome to the IRC network"]);
    }

    #[test]
    fn parse_no_trailing() {
        let msg = IrcMessage::parse(":server 433 * botname").unwrap();
        assert_eq!(msg.command, "433");
        assert_eq!(msg.params, vec!["*", "botname"]);
    }

    #[test]
    fn parse_cap_ack() {
        let msg = IrcMessage::parse(":server CAP * ACK :sasl").unwrap();
        assert_eq!(msg.command, "CAP");
        assert_eq!(msg.params, vec!["*", "ACK", "sasl"]);
    }

    #[test]
    fn parse_empty_line_returns_none() {
        assert!(IrcMessage::parse("").is_none());
        assert!(IrcMessage::parse("\r\n").is_none());
    }

    #[test]
    fn parse_strips_crlf() {
        let msg = IrcMessage::parse("PING :test\r\n").unwrap();
        assert_eq!(msg.params, vec!["test"]);
    }

    #[test]
    fn parse_command_uppercase() {
        let msg = IrcMessage::parse("ping :test").unwrap();
        assert_eq!(msg.command, "PING");
    }

    #[test]
    fn nick_extraction_full_prefix() {
        let msg = IrcMessage::parse(":nick!user@host PRIVMSG #ch :msg").unwrap();
        assert_eq!(msg.nick(), Some("nick"));
    }

    #[test]
    fn nick_extraction_nick_only() {
        let msg = IrcMessage::parse(":server 001 bot :Welcome").unwrap();
        assert_eq!(msg.nick(), Some("server"));
    }

    #[test]
    fn nick_extraction_no_prefix() {
        let msg = IrcMessage::parse("PING :token").unwrap();
        assert_eq!(msg.nick(), None);
    }

    #[test]
    fn parse_authenticate_plus() {
        let msg = IrcMessage::parse("AUTHENTICATE +").unwrap();
        assert_eq!(msg.command, "AUTHENTICATE");
        assert_eq!(msg.params, vec!["+"]);
    }

    #[test]
    fn parse_cap_nak_with_sasl() {
        let msg = IrcMessage::parse(":server CAP * NAK :sasl").unwrap();
        assert_eq!(msg.command, "CAP");
        assert!(msg.params.iter().any(|p| p == "NAK"));
        assert!(msg.params.iter().any(|p| p == "sasl"));
    }

    #[test]
    fn sasl_cap_action_nak_ends_sasl_negotiation() {
        let msg = IrcMessage::parse(":server CAP * NAK :sasl").unwrap();
        assert_eq!(
            sasl_cap_action(&msg, true),
            Some(SaslCapAction::EndCapAndDisable)
        );
    }

    // ── SASL PLAIN encoding ─────────────────────────────────

    #[test]
    fn sasl_plain_encode() {
        let encoded = encode_sasl_plain("jilles", "sesame");
        // \0jilles\0sesame → base64
        assert_eq!(encoded, "AGppbGxlcwBzZXNhbWU=");
    }

    #[test]
    fn sasl_plain_empty_password() {
        let encoded = encode_sasl_plain("nick", "");
        // \0nick\0 → base64
        assert_eq!(encoded, "AG5pY2sA");
    }

    #[test]
    fn sasl_payload_chunks_include_terminator_for_exact_boundary() {
        let payload = "a".repeat(800);
        let chunks = split_sasl_authenticate_payload(&payload);
        assert_eq!(chunks.len(), 3);
        assert_eq!(chunks[0].len(), 400);
        assert_eq!(chunks[1].len(), 400);
        assert_eq!(chunks[2], "+");
    }

    #[test]
    fn sasl_payload_chunks_without_terminator_for_partial_tail() {
        let payload = "a".repeat(401);
        let chunks = split_sasl_authenticate_payload(&payload);
        assert_eq!(chunks.len(), 2);
        assert_eq!(chunks[0].len(), 400);
        assert_eq!(chunks[1].len(), 1);
    }

    #[test]
    fn sasl_payload_chunks_empty_payload_returns_plus() {
        let chunks = split_sasl_authenticate_payload("");
        assert_eq!(chunks, vec!["+"]);
    }

    #[test]
    fn sasl_payload_chunks_exact_single_chunk_has_terminator() {
        let payload = "a".repeat(400);
        let chunks = split_sasl_authenticate_payload(&payload);
        assert_eq!(chunks, vec![payload, "+".to_string()]);
    }

    // ── Message splitting ───────────────────────────────────

    #[test]
    fn split_short_message() {
        let chunks = split_message("hello", 400);
        assert_eq!(chunks, vec!["hello"]);
    }

    #[test]
    fn split_long_message() {
        let msg = "a".repeat(800);
        let chunks = split_message(&msg, 400);
        assert_eq!(chunks.len(), 2);
        assert_eq!(chunks[0].len(), 400);
        assert_eq!(chunks[1].len(), 400);
    }

    #[test]
    fn split_exact_boundary() {
        let msg = "a".repeat(400);
        let chunks = split_message(&msg, 400);
        assert_eq!(chunks.len(), 1);
    }

    #[test]
    fn split_unicode_safe() {
        // 'é' is 2 bytes in UTF-8; splitting at byte 3 would split mid-char
        let msg = "ééé"; // 6 bytes
        let chunks = split_message(msg, 3);
        // Should split at char boundary (2 bytes), not mid-char
        assert_eq!(chunks.len(), 3);
        assert_eq!(chunks[0], "é");
        assert_eq!(chunks[1], "é");
        assert_eq!(chunks[2], "é");
    }

    #[test]
    fn split_empty_message() {
        let chunks = split_message("", 400);
        assert_eq!(chunks, vec![""]);
    }

    #[test]
    fn split_newlines_into_separate_lines() {
        let chunks = split_message("line one\nline two\nline three", 400);
        assert_eq!(chunks, vec!["line one", "line two", "line three"]);
    }

    #[test]
    fn split_crlf_newlines() {
        let chunks = split_message("hello\r\nworld", 400);
        assert_eq!(chunks, vec!["hello", "world"]);
    }

    #[test]
    fn split_skips_empty_lines() {
        let chunks = split_message("hello\n\n\nworld", 400);
        assert_eq!(chunks, vec!["hello", "world"]);
    }

    #[test]
    fn split_trailing_newline() {
        let chunks = split_message("hello\n", 400);
        assert_eq!(chunks, vec!["hello"]);
    }

    #[test]
    fn split_multiline_with_long_line() {
        let long = "a".repeat(800);
        let msg = format!("short\n{long}\nend");
        let chunks = split_message(&msg, 400);
        assert_eq!(chunks.len(), 4);
        assert_eq!(chunks[0], "short");
        assert_eq!(chunks[1].len(), 400);
        assert_eq!(chunks[2].len(), 400);
        assert_eq!(chunks[3], "end");
    }

    #[test]
    fn split_only_newlines() {
        let chunks = split_message("\n\n\n", 400);
        assert_eq!(chunks, vec![""]);
    }

    // ── Allowlist ───────────────────────────────────────────

    #[test]
    fn wildcard_allows_anyone() {
        let ch = make_channel();
        // Default make_channel has wildcard
        assert!(ch.is_user_allowed("anyone"));
        assert!(ch.is_user_allowed("stranger"));
    }

    #[test]
    fn specific_user_allowed() {
        let ch = IrcChannel::new(IrcChannelConfig {
            server: "irc.test".into(),
            port: 6697,
            nickname: "bot".into(),
            username: None,
            channels: vec![],
            allowed_users: vec!["alice".into(), "bob".into()],
            server_password: None,
            nickserv_password: None,
            sasl_password: None,
            verify_tls: true,
        });
        assert!(ch.is_user_allowed("alice"));
        assert!(ch.is_user_allowed("bob"));
        assert!(!ch.is_user_allowed("eve"));
    }

    #[test]
    fn allowlist_case_insensitive() {
        let ch = IrcChannel::new(IrcChannelConfig {
            server: "irc.test".into(),
            port: 6697,
            nickname: "bot".into(),
            username: None,
            channels: vec![],
            allowed_users: vec!["Alice".into()],
            server_password: None,
            nickserv_password: None,
            sasl_password: None,
            verify_tls: true,
        });
        assert!(ch.is_user_allowed("alice"));
        assert!(ch.is_user_allowed("ALICE"));
        assert!(ch.is_user_allowed("Alice"));
    }

    #[test]
    fn empty_allowlist_denies_all() {
        let ch = IrcChannel::new(IrcChannelConfig {
            server: "irc.test".into(),
            port: 6697,
            nickname: "bot".into(),
            username: None,
            channels: vec![],
            allowed_users: vec![],
            server_password: None,
            nickserv_password: None,
            sasl_password: None,
            verify_tls: true,
        });
        assert!(!ch.is_user_allowed("anyone"));
    }

    // ── Constructor ─────────────────────────────────────────

    #[test]
    fn new_defaults_username_to_nickname() {
        let ch = IrcChannel::new(IrcChannelConfig {
            server: "irc.test".into(),
            port: 6697,
            nickname: "mybot".into(),
            username: None,
            channels: vec![],
            allowed_users: vec![],
            server_password: None,
            nickserv_password: None,
            sasl_password: None,
            verify_tls: true,
        });
        assert_eq!(ch.username, "mybot");
    }

    #[test]
    fn new_uses_explicit_username() {
        let ch = IrcChannel::new(IrcChannelConfig {
            server: "irc.test".into(),
            port: 6697,
            nickname: "mybot".into(),
            username: Some("customuser".into()),
            channels: vec![],
            allowed_users: vec![],
            server_password: None,
            nickserv_password: None,
            sasl_password: None,
            verify_tls: true,
        });
        assert_eq!(ch.username, "customuser");
        assert_eq!(ch.nickname, "mybot");
    }

    #[test]
    fn name_returns_irc() {
        let ch = make_channel();
        assert_eq!(ch.name(), "irc");
    }

    #[test]
    fn new_stores_all_fields() {
        let ch = IrcChannel::new(IrcChannelConfig {
            server: "irc.example.com".into(),
            port: 6697,
            nickname: "zcbot".into(),
            username: Some("corvus".into()),
            channels: vec!["#test".into()],
            allowed_users: vec!["alice".into()],
            server_password: Some("serverpass".into()),
            nickserv_password: Some("nspass".into()),
            sasl_password: Some("saslpass".into()),
            verify_tls: false,
        });
        assert_eq!(ch.server, "irc.example.com");
        assert_eq!(ch.port, 6697);
        assert_eq!(ch.nickname, "zcbot");
        assert_eq!(ch.username, "corvus");
        assert_eq!(ch.channels, vec!["#test"]);
        assert_eq!(ch.allowed_users, vec!["alice"]);
        assert_eq!(ch.server_password.as_deref(), Some("serverpass"));
        assert_eq!(ch.nickserv_password.as_deref(), Some("nspass"));
        assert_eq!(ch.sasl_password.as_deref(), Some("saslpass"));
        assert!(!ch.verify_tls);
    }

    // ── Config serde ────────────────────────────────────────

    #[test]
    fn irc_config_serde_roundtrip() {
        use crate::config::schema::IrcConfig;

        let config = IrcConfig {
            server: "irc.example.com".into(),
            port: 6697,
            nickname: "zcbot".into(),
            username: Some("corvus".into()),
            channels: vec!["#test".into(), "#dev".into()],
            allowed_users: vec!["alice".into()],
            server_password: None,
            nickserv_password: Some("secret".into()),
            sasl_password: None,
            verify_tls: Some(true),
        };

        let toml_str = toml::to_string(&config).unwrap();
        let parsed: IrcConfig = toml::from_str(&toml_str).unwrap();
        assert_eq!(parsed.server, "irc.example.com");
        assert_eq!(parsed.port, 6697);
        assert_eq!(parsed.nickname, "zcbot");
        assert_eq!(parsed.username.as_deref(), Some("corvus"));
        assert_eq!(parsed.channels, vec!["#test", "#dev"]);
        assert_eq!(parsed.allowed_users, vec!["alice"]);
        assert!(parsed.server_password.is_none());
        assert_eq!(parsed.nickserv_password.as_deref(), Some("secret"));
        assert!(parsed.sasl_password.is_none());
        assert_eq!(parsed.verify_tls, Some(true));
    }

    #[test]
    fn irc_config_minimal_toml() {
        use crate::config::schema::IrcConfig;

        let toml_str = r#"
server = "irc.example.com"
nickname = "bot"
"#;
        let parsed: IrcConfig = toml::from_str(toml_str).unwrap();
        assert_eq!(parsed.server, "irc.example.com");
        assert_eq!(parsed.port, 6697); // default
        assert_eq!(parsed.nickname, "bot");
        assert!(parsed.username.is_none());
        assert!(parsed.channels.is_empty());
        assert!(parsed.allowed_users.is_empty());
        assert!(parsed.server_password.is_none());
        assert!(parsed.nickserv_password.is_none());
        assert!(parsed.sasl_password.is_none());
        assert!(parsed.verify_tls.is_none());
    }

    #[test]
    fn irc_config_default_port() {
        use crate::config::schema::IrcConfig;

        let json = r#"{"server":"irc.test","nickname":"bot"}"#;
        let parsed: IrcConfig = serde_json::from_str(json).unwrap();
        assert_eq!(parsed.port, 6697);
    }

    // ── collapse_to_single_line ─────────────────────────────

    #[test]
    fn collapse_multiline_to_single() {
        assert_eq!(collapse_to_single_line("a\nb\nc"), "a b c");
    }

    #[test]
    fn collapse_skips_empty_lines() {
        assert_eq!(collapse_to_single_line("a\n\nb"), "a b");
    }

    #[test]
    fn collapse_empty_input() {
        assert_eq!(collapse_to_single_line(""), "");
    }

    #[test]
    fn collapse_strips_cr() {
        assert_eq!(collapse_to_single_line("a\r\nb\r\n"), "a b");
    }

    // ── find_utf8_split_point ────────────────────────────────

    #[test]
    fn split_point_ascii() {
        assert_eq!(find_utf8_split_point("hello", 3), 3);
    }

    #[test]
    fn split_point_advances_past_mid_char() {
        // 'é' is 2 bytes; splitting at byte 1 backs up to 0, then advances forward to 2
        assert_eq!(find_utf8_split_point("é", 1), 2);
    }

    #[test]
    fn split_point_advances_when_backup_fails() {
        // 4-byte emoji: splitting at 0 would fail backward, must advance
        let text = "🌍";
        let point = find_utf8_split_point(text, 0);
        // Should advance to the first valid boundary (0 or 4)
        assert!(text.is_char_boundary(point));
    }

    // ── sasl_cap_action ──────────────────────────────────────

    #[test]
    fn sasl_cap_action_ack_starts_auth() {
        let msg = IrcMessage::parse(":server CAP * ACK :sasl").unwrap();
        assert_eq!(
            sasl_cap_action(&msg, true),
            Some(SaslCapAction::StartAuthenticatePlain)
        );
    }

    #[test]
    fn sasl_cap_action_not_pending_returns_none() {
        let msg = IrcMessage::parse(":server CAP * ACK :sasl").unwrap();
        assert_eq!(sasl_cap_action(&msg, false), None);
    }

    #[test]
    fn sasl_cap_action_non_cap_returns_none() {
        let msg = IrcMessage::parse("PING :test").unwrap();
        assert_eq!(sasl_cap_action(&msg, true), None);
    }

    #[test]
    fn sasl_cap_action_no_sasl_param_returns_none() {
        let msg = IrcMessage::parse(":server CAP * ACK :multi-prefix").unwrap();
        assert_eq!(sasl_cap_action(&msg, true), None);
    }

    // ── parse_privmsg ────────────────────────────────────────

    #[test]
    fn parse_privmsg_channel_message_includes_style_prefix() {
        let ch = IrcChannel::new(IrcChannelConfig {
            server: "irc.test".into(),
            port: 6697,
            nickname: "bot".into(),
            username: None,
            channels: vec!["#test".into()],
            allowed_users: vec!["*".into()],
            server_password: None,
            nickserv_password: None,
            sasl_password: None,
            verify_tls: true,
        });
        let msg = IrcMessage::parse(":alice!a@host PRIVMSG #test :hello").unwrap();
        let result = ch.parse_privmsg(&msg, "alice");
        assert!(result.is_some());
        let cm = result.unwrap();
        assert_eq!(cm.reply_target, "#test");
        assert!(cm.content.contains("hello"));
        assert!(cm.content.contains("[context:"));
        assert_eq!(cm.channel, "irc");
    }

    #[test]
    fn parse_privmsg_dm_uses_sender_as_reply_target() {
        let ch = IrcChannel::new(IrcChannelConfig {
            server: "irc.test".into(),
            port: 6697,
            nickname: "bot".into(),
            username: None,
            channels: vec![],
            allowed_users: vec!["*".into()],
            server_password: None,
            nickserv_password: None,
            sasl_password: None,
            verify_tls: true,
        });
        let msg = IrcMessage::parse(":alice!a@host PRIVMSG bot :hi there").unwrap();
        let result = ch.parse_privmsg(&msg, "alice");
        assert!(result.is_some());
        let cm = result.unwrap();
        assert_eq!(cm.reply_target, "alice");
    }

    #[test]
    fn parse_privmsg_filters_nickserv() {
        let ch = make_channel();
        let msg = IrcMessage::parse(":NickServ!s@host PRIVMSG bot :identify now").unwrap();
        assert!(ch.parse_privmsg(&msg, "NickServ").is_none());
    }

    #[test]
    fn parse_privmsg_filters_unauthorized_user() {
        let ch = IrcChannel::new(IrcChannelConfig {
            server: "irc.test".into(),
            port: 6697,
            nickname: "bot".into(),
            username: None,
            channels: vec![],
            allowed_users: vec!["alice".into()],
            server_password: None,
            nickserv_password: None,
            sasl_password: None,
            verify_tls: true,
        });
        let msg = IrcMessage::parse(":eve!e@host PRIVMSG #test :spam").unwrap();
        assert!(ch.parse_privmsg(&msg, "eve").is_none());
    }

    // ── split_message zero max_bytes ─────────────────────────

    #[test]
    fn split_message_zero_max_collapses() {
        let chunks = split_message("a\nb\nc", 0);
        assert_eq!(chunks, vec!["a b c"]);
    }

    // ── Helpers ─────────────────────────────────────────────

    fn make_channel() -> IrcChannel {
        IrcChannel::new(IrcChannelConfig {
            server: "irc.example.com".into(),
            port: 6697,
            nickname: "zcbot".into(),
            username: None,
            channels: vec!["#corvus".into()],
            allowed_users: vec!["*".into()],
            server_password: None,
            nickserv_password: None,
            sasl_password: None,
            verify_tls: true,
        })
    }
}
