//! Shared validation helpers for marshal verification and certification.
//!
//! This module centralizes pure invariant checks shared across marshal verification
//! and certification flows.

use crate::types::{Epoch, Epocher, Height, Round};
use commonware_utils::sync::Mutex;
use std::sync::Arc;

/// Cache for the last block built during proposal, shared between the
/// proposer task and the broadcast path.
pub(crate) type LastBuilt<B> = Arc<Mutex<Option<(Round, B)>>>;

/// Returns true if the block is at an epoch boundary (last block in its epoch).
#[inline]
fn is_at_epoch_boundary<ES: Epocher>(epocher: &ES, block_height: Height, epoch: Epoch) -> bool {
    epocher.last(epoch).is_some_and(|last| last == block_height)
}

/// Returns true when a verify-time re-proposal is valid for the given epoch.
///
/// Re-proposals are only valid for the last block of the epoch.
#[inline]
pub(crate) fn is_valid_reproposal_at_verify<ES: Epocher>(
    epocher: &ES,
    block_height: Height,
    epoch: Epoch,
) -> bool {
    is_at_epoch_boundary(epocher, block_height, epoch)
}

/// Infers whether a certify-time block should be treated as a re-proposal.
///
/// During certification we may only have the block's embedded round, not the
/// consensus context used at verify time. We treat the block as a re-proposal
/// when it is an epoch-boundary block, the certify view is later than the
/// embedded view, and both rounds are in the same epoch.
#[inline]
pub(crate) fn is_inferred_reproposal_at_certify<ES: Epocher>(
    epocher: &ES,
    block_height: Height,
    embedded_round: Round,
    certify_round: Round,
) -> bool {
    is_at_epoch_boundary(epocher, block_height, embedded_round.epoch())
        && certify_round.view() > embedded_round.view()
        && certify_round.epoch() == embedded_round.epoch()
}

/// Returns true when `block_height` is mapped to `expected_epoch` by `epocher`.
///
/// If the height is not covered by the epoch strategy, this returns false.
#[inline]
pub(crate) fn is_block_in_expected_epoch<ES: Epocher>(
    epocher: &ES,
    block_height: Height,
    expected_epoch: Epoch,
) -> bool {
    epocher
        .containing(block_height)
        .is_some_and(|bounds| bounds.epoch() == expected_epoch)
}

/// Returns true when `block_height` is exactly the successor of `parent_height`.
#[inline]
pub(crate) fn has_contiguous_height(parent_height: Height, block_height: Height) -> bool {
    parent_height.next() == block_height
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::types::{FixedEpocher, View};
    use commonware_utils::NZU64;

    #[test]
    fn test_is_valid_reproposal_at_verify() {
        let epocher = FixedEpocher::new(NZU64!(10));
        assert!(is_valid_reproposal_at_verify(
            &epocher,
            Height::new(9),
            Epoch::new(0)
        ));
        assert!(!is_valid_reproposal_at_verify(
            &epocher,
            Height::new(8),
            Epoch::new(0)
        ));

        // Out-of-range epoch is never a valid re-proposal boundary.
        assert!(!is_valid_reproposal_at_verify(
            &epocher,
            Height::new(0),
            Epoch::new(u64::MAX)
        ));
    }

    #[test]
    fn test_is_inferred_reproposal_at_certify() {
        let epocher = FixedEpocher::new(NZU64!(10));
        let embedded = Round::new(Epoch::new(0), View::new(9));

        // Boundary block, later view, same epoch.
        assert!(is_inferred_reproposal_at_certify(
            &epocher,
            Height::new(9),
            embedded,
            Round::new(Epoch::new(0), View::new(10)),
        ));

        // Same view is not inferred as re-proposal.
        assert!(!is_inferred_reproposal_at_certify(
            &epocher,
            Height::new(9),
            embedded,
            Round::new(Epoch::new(0), View::new(9)),
        ));

        // Cross-epoch is not inferred as re-proposal.
        assert!(!is_inferred_reproposal_at_certify(
            &epocher,
            Height::new(9),
            embedded,
            Round::new(Epoch::new(1), View::new(10)),
        ));

        // Non-boundary block is not inferred as re-proposal, even with later view.
        assert!(!is_inferred_reproposal_at_certify(
            &epocher,
            Height::new(8),
            Round::new(Epoch::new(0), View::new(8)),
            Round::new(Epoch::new(0), View::new(9)),
        ));
    }

    #[test]
    fn test_is_block_in_expected_epoch() {
        let epocher = FixedEpocher::new(NZU64!(10));
        assert!(is_block_in_expected_epoch(
            &epocher,
            Height::new(7),
            Epoch::new(0)
        ));
        assert!(!is_block_in_expected_epoch(
            &epocher,
            Height::new(7),
            Epoch::new(1)
        ));

        // Height at u64::MAX is out of range for this epocher due to last-height overflow.
        assert!(!is_block_in_expected_epoch(
            &epocher,
            Height::new(u64::MAX),
            Epoch::new(0)
        ));
    }

    #[test]
    fn test_has_contiguous_height() {
        assert!(has_contiguous_height(Height::new(6), Height::new(7)));
        assert!(!has_contiguous_height(Height::new(6), Height::new(8)));
    }
}
