//! Mock IMAP server stream for integration testing.
//!
//! `MockStream` wraps the server side of a localhost TCP connection paired
//! with an [`ImapConnection`]. Test code enqueues server responses via
//! [`MockStream::send`] and inspects client-written bytes via
//! [`MockStream::receive`].
//!
//! The TCP loopback approach is necessary because `ImapConnection`'s fields
//! are private — integration tests cannot construct one directly and must
//! use the public [`ImapConnection::connect`] method, which expects a real
//! TCP endpoint.

use std::io;
use std::pin::Pin;
use std::task::{Context, Poll};
use std::time::Duration;

use tokio::io::{AsyncRead, AsyncReadExt, AsyncWrite, AsyncWriteExt, ReadBuf};
use tokio::net::{TcpListener, TcpStream};

/// A mock IMAP server backed by the server side of a localhost TCP pair.
///
/// Implements [`AsyncRead`] and [`AsyncWrite`] by delegating to the inner
/// [`TcpStream`], and provides convenience methods for scripting IMAP
/// server responses in tests.
pub struct MockStream {
    /// Server-side TCP stream connected to the `ImapConnection`.
    stream: TcpStream,
    /// Accumulated bytes the client has written, captured by [`receive`](Self::receive).
    client_bytes: Vec<u8>,
}

impl MockStream {
    /// Send raw bytes to the client (simulating server responses).
    ///
    /// Flushes the stream after writing to ensure the client receives the
    /// data immediately.
    pub async fn send(&mut self, data: &[u8]) -> io::Result<()> {
        self.stream.write_all(data).await?;
        self.stream.flush().await
    }

    /// Read bytes the client has written, with a timeout.
    ///
    /// Returns the raw bytes read, or an empty `Vec` if the timeout elapses
    /// before any data arrives or if the stream encounters an error.
    /// Captured bytes are appended to [`client_bytes`](Self::client_bytes).
    pub async fn receive(&mut self, timeout: Duration) -> Vec<u8> {
        let mut buf = vec![0u8; 4096];
        match tokio::time::timeout(timeout, self.stream.read(&mut buf)).await {
            Ok(Ok(n)) if n > 0 => {
                let data = buf[..n].to_vec();
                self.client_bytes.extend_from_slice(&data);
                data
            }
            _ => Vec::new(),
        }
    }

    /// Read a complete IMAP command line (terminated by `\r\n`) from the
    /// client, with a timeout.
    ///
    /// Reads byte-by-byte until `\r\n` is found or the timeout elapses.
    /// Returns the full line including the terminator on success. If the
    /// timeout elapses or EOF is reached before `\r\n`, returns whatever
    /// partial data was accumulated (which may be empty).
    pub async fn receive_line(&mut self, timeout: Duration) -> String {
        let mut line = Vec::new();
        let deadline = tokio::time::Instant::now() + timeout;
        loop {
            let remaining = deadline.saturating_duration_since(tokio::time::Instant::now());
            if remaining.is_zero() {
                break;
            }
            let mut byte = [0u8; 1];
            match tokio::time::timeout(remaining, self.stream.read_exact(&mut byte)).await {
                Ok(Ok(_)) => {
                    line.push(byte[0]);
                    self.client_bytes.push(byte[0]);
                    if line.ends_with(b"\r\n") {
                        break;
                    }
                }
                _ => break,
            }
        }
        String::from_utf8_lossy(&line).into_owned()
    }

    /// All bytes the client has written so far, accumulated across
    /// [`receive`](Self::receive) and [`receive_line`](Self::receive_line)
    /// calls.
    #[allow(dead_code)]
    pub fn client_bytes(&self) -> &[u8] {
        &self.client_bytes
    }

    /// Shut down the write half of the server side, simulating a server
    /// disconnect.
    pub async fn shutdown(&mut self) -> io::Result<()> {
        self.stream.shutdown().await
    }
}

// ---------------------------------------------------------------------------
// AsyncRead / AsyncWrite delegation
// ---------------------------------------------------------------------------

impl AsyncRead for MockStream {
    fn poll_read(
        mut self: Pin<&mut Self>,
        cx: &mut Context<'_>,
        buf: &mut ReadBuf<'_>,
    ) -> Poll<io::Result<()>> {
        Pin::new(&mut self.stream).poll_read(cx, buf)
    }
}

impl AsyncWrite for MockStream {
    fn poll_write(
        mut self: Pin<&mut Self>,
        cx: &mut Context<'_>,
        buf: &[u8],
    ) -> Poll<io::Result<usize>> {
        Pin::new(&mut self.stream).poll_write(cx, buf)
    }

    fn poll_flush(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<io::Result<()>> {
        Pin::new(&mut self.stream).poll_flush(cx)
    }

    fn poll_shutdown(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<io::Result<()>> {
        Pin::new(&mut self.stream).poll_shutdown(cx)
    }
}

// ---------------------------------------------------------------------------
// Tag extraction helper
// ---------------------------------------------------------------------------

/// Extract the IMAP command tag from a raw command line.
///
/// RFC 3501 §2.2.1: the tag is the first whitespace-delimited token in a
/// client command. Used by mock servers to echo the correct tag in their
/// responses.
pub fn extract_tag(cmd_line: &str) -> &str {
    cmd_line.split_whitespace().next().unwrap_or("")
}

// ---------------------------------------------------------------------------
// Connection helper
// ---------------------------------------------------------------------------

/// Create a mock IMAP connection backed by a localhost TCP loopback.
///
/// Binds a TCP listener on `127.0.0.1:0`, spawns a server task that
/// writes `greeting` on accept, then connects an [`ImapConnection`] to
/// it. Returns `(connection, mock_server)`.
///
/// The `greeting` must be a complete IMAP greeting line **including a
/// `[CAPABILITY ...]` response code** (e.g.,
/// `b"* OK [CAPABILITY IMAP4rev1] ready\r\n"`). If the greeting omits
/// capabilities, `ImapConnection::connect` will issue a `CAPABILITY`
/// command and block waiting for a response that the mock server is not
/// prepared to send, causing a timeout.
///
/// # Panics
///
/// Panics if the localhost TCP listener cannot be bound or if the
/// `ImapConnection::connect` call fails. Both indicate a broken test
/// environment, not a recoverable condition.
pub async fn mock_connection(greeting: &[u8]) -> (daaki_imap::ImapConnection, MockStream) {
    let listener = TcpListener::bind("127.0.0.1:0")
        .await
        .expect("test: binding localhost TCP listener");
    let port = listener.local_addr().unwrap().port();

    let greeting_owned = greeting.to_vec();
    let server_task = tokio::spawn(async move {
        let (mut stream, _) = listener.accept().await.unwrap();
        stream.write_all(&greeting_owned).await.unwrap();
        stream.flush().await.unwrap();
        stream
    });

    let conn = daaki_imap::ImapConnection::connect(
        "127.0.0.1",
        port,
        daaki_imap::TlsMode::None,
        Duration::from_secs(5),
    )
    .await
    .expect("test: mock ImapConnection::connect should succeed");

    let server_stream = server_task.await.unwrap();

    (
        conn,
        MockStream {
            stream: server_stream,
            client_bytes: Vec::new(),
        },
    )
}
