//! Foca runtime implementation

use crate::identity::GossipId;
use crate::membership::MembershipChange;
use foca::{Notification, Runtime, Timer};
use std::net::SocketAddr;
use std::time::Duration;
use tokio::sync::mpsc;
use tracing::{debug, trace};

/// Foca runtime implementation for async operation
pub struct GossipRuntime {
    /// Channel for outgoing UDP packets
    outgoing_tx: mpsc::Sender<(SocketAddr, Vec<u8>)>,
    /// Channel for timer scheduling
    timer_tx: mpsc::Sender<(Timer<GossipId>, Duration)>,
    /// Channel for membership updates
    membership_tx: mpsc::Sender<MembershipChange>,
}

impl GossipRuntime {
    /// Create a new gossip runtime
    pub fn new(
        outgoing_tx: mpsc::Sender<(SocketAddr, Vec<u8>)>,
        timer_tx: mpsc::Sender<(Timer<GossipId>, Duration)>,
        membership_tx: mpsc::Sender<MembershipChange>,
    ) -> Self {
        Self {
            outgoing_tx,
            timer_tx,
            membership_tx,
        }
    }
}

impl Runtime<GossipId> for GossipRuntime {
    fn notify(&mut self, notification: Notification<GossipId>) {
        match notification {
            Notification::MemberUp(id) => {
                debug!(node_id = id.node_id, addr = %id.addr, "Member up");
                let _ = self
                    .membership_tx
                    .try_send(MembershipChange::MemberUp(id.clone()));
            }
            Notification::MemberDown(id) => {
                debug!(node_id = id.node_id, addr = %id.addr, "Member down");
                let _ = self
                    .membership_tx
                    .try_send(MembershipChange::MemberDown(id.clone()));
            }
            Notification::Idle => {
                trace!("Gossip idle");
            }
            Notification::Rejoin(id) => {
                debug!(node_id = id.node_id, "Member rejoined");
                let _ = self
                    .membership_tx
                    .try_send(MembershipChange::MemberUp(id.clone()));
            }
            Notification::Active => {
                trace!("Gossip active");
            }
            Notification::Defunct => {
                trace!("Member defunct");
            }
            Notification::Rename(old, new) => {
                debug!(old = old.node_id, new = new.node_id, "Member renamed");
                // Treat as down/up sequence
                let _ = self
                    .membership_tx
                    .try_send(MembershipChange::MemberDown(old.clone()));
                let _ = self
                    .membership_tx
                    .try_send(MembershipChange::MemberUp(new.clone()));
            }
        }
    }

    fn send_to(&mut self, to: GossipId, data: &[u8]) {
        trace!(to = %to.addr, len = data.len(), "Sending gossip packet");
        let _ = self.outgoing_tx.try_send((to.addr, data.to_vec()));
    }

    fn submit_after(&mut self, event: Timer<GossipId>, after: Duration) {
        trace!(?event, ?after, "Scheduling timer");
        let _ = self.timer_tx.try_send((event, after));
    }
}

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

    #[tokio::test]
    async fn test_runtime_notifications() {
        let (outgoing_tx, _) = mpsc::channel(10);
        let (timer_tx, _) = mpsc::channel(10);
        let (membership_tx, mut membership_rx) = mpsc::channel(10);

        let mut runtime = GossipRuntime::new(outgoing_tx, timer_tx, membership_tx);

        let id = GossipId::new(1, "127.0.0.1:5000".parse().unwrap(), NodeRole::Worker);

        runtime.notify(Notification::MemberUp(&id));
        let change = membership_rx.try_recv().unwrap();
        assert!(matches!(change, MembershipChange::MemberUp(_)));

        runtime.notify(Notification::MemberDown(&id));
        let change = membership_rx.try_recv().unwrap();
        assert!(matches!(change, MembershipChange::MemberDown(_)));
    }

    #[tokio::test]
    async fn test_runtime_send() {
        let (outgoing_tx, mut outgoing_rx) = mpsc::channel(10);
        let (timer_tx, _) = mpsc::channel(10);
        let (membership_tx, _) = mpsc::channel(10);

        let mut runtime = GossipRuntime::new(outgoing_tx, timer_tx, membership_tx);

        let id = GossipId::new(1, "127.0.0.1:5000".parse().unwrap(), NodeRole::Worker);
        let data = b"test data";

        runtime.send_to(id.clone(), data);

        let (recv_addr, recv_data) = outgoing_rx.try_recv().unwrap();
        assert_eq!(recv_addr, id.addr);
        assert_eq!(recv_data, data);
    }
}
