//! Movement pattern enum + built-in patterns.
//!
//! A [`MovementPattern`] describes how an entity moves across the board
//! graph in a single "step" of motion. The enum is server-side only —
//! it is `#[serde(skip)]` on `MovableData` and never crosses the
//! WebSocket boundary. Pattern logic stays on the server; clients
//! receive precomputed reachability frames produced by the reachability
//! presenter.
//!
//! Two built-in patterns ship in this module:
//!
//! - [`Step`] — adjacent neighbors that respect motion-blocking edges
//!   (the default for ordinary on-foot movement).
//! - [`Hop`] — adjacent neighbors regardless of motion-blocking
//!   (flyers, phasers, pieces that leap over obstacles).
//!
//! Custom plugin patterns are not currently supported through this enum;
//! `MovementPatternProvider` (the previous trait-object form) was removed
//! when no caller introduced a third variant. Add a new enum variant if
//! a plugin needs knight-L moves, sliding rooks, etc.
//!
//! ## Edge blocking — Approach B (UC-04 phase C reconciliation)
//!
//! `BoardGraph` carries pure topology (nodes, edges, faces) and does NOT
//! store per-edge `motion_blocked` / `vision_blocked` attributes — that
//! data lives on `LoadedMap` (segment-vs-wall geometry) or, in pure
//! abstract grids, simply doesn't exist. Rather than duplicating it onto
//! the graph (Approach A — a workspace-wide migration affecting every
//! graph-construction site for marginal benefit), we thread a small
//! [`EdgeBlocking`] oracle through pattern evaluation. Plugins (or the
//! engine's reachability presenter) wire up an impl that consults the
//! map; abstract test fixtures use [`AlwaysOpen`].
//!
//! See `docs/superpowers/plans/2026-05-01-uc-04-distance-metrics.md`
//! Phase C "reconciliation" notes.

use crate::board::{BoardGraphView, BoardNodeId};

/// Per-edge blocking oracle consulted by [`MovementPattern::neighbors`].
/// `motion_blocked(from, to)` is true when the entity cannot traverse
/// the edge `(from, to)` for movement; `vision_blocked` is the analogous
/// predicate for line-of-sight.
///
/// Implementors typically wrap `LoadedMap` (the engine ships such a
/// wrapper in the reachability presenter) or model abstract walls. If
/// no blocking semantics apply, use [`AlwaysOpen`].
pub trait EdgeBlocking {
    /// `true` if motion is denied across the edge from `from` to `to`.
    fn motion_blocked(&self, from: BoardNodeId, to: BoardNodeId) -> bool;

    /// `true` if vision is denied across the edge from `from` to `to`.
    /// Default: same as `motion_blocked` — most opaque walls block both.
    fn vision_blocked(&self, from: BoardNodeId, to: BoardNodeId) -> bool {
        self.motion_blocked(from, to)
    }
}

/// Default `EdgeBlocking` for graphs with no walls / no map. Every edge
/// is open. Used by abstract test fixtures and by graph-only games (e.g.
/// Catan road network) where edge-blocking is not modeled.
#[derive(Debug, Default, Clone, Copy)]
pub struct AlwaysOpen;

impl EdgeBlocking for AlwaysOpen {
    fn motion_blocked(&self, _from: BoardNodeId, _to: BoardNodeId) -> bool {
        false
    }
    fn vision_blocked(&self, _from: BoardNodeId, _to: BoardNodeId) -> bool {
        false
    }
}

/// One adjacent edge per move, respecting motion-blocking edges.
///
/// `Step` queries `graph.neighbors(from)` and then drops every neighbor
/// `n` for which `blocking.motion_blocked(from, n)` is true.
#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)]
pub struct Step;

/// One adjacent edge per move, ignoring motion-blocking edges.
///
/// Used for flyers, phasers, teleporters, and other movement that
/// passes through walls. `Hop` returns `graph.neighbors(from)`
/// verbatim — it does not consult `blocking`.
#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)]
pub struct Hop;

/// Describes how an entity reaches its single-step neighbors on a
/// `BoardGraph`. Server-side only — never serialized.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum MovementPattern {
    /// Default on-foot movement: adjacent neighbors honoring edge-blocking.
    Step(Step),
    /// Flying / phasing: adjacent neighbors ignoring edge-blocking.
    Hop(Hop),
}

impl MovementPattern {
    /// Nodes reachable from `from` in one move of this pattern,
    /// honouring edge-blocking via `blocking` as appropriate to the
    /// pattern's semantics.
    pub fn neighbors(
        &self,
        from: BoardNodeId,
        graph: &dyn BoardGraphView,
        blocking: &dyn EdgeBlocking,
    ) -> Vec<BoardNodeId> {
        match self {
            MovementPattern::Step(_) => graph
                .neighbors(from)
                .into_iter()
                .filter(|n| !blocking.motion_blocked(from, *n))
                .collect(),
            MovementPattern::Hop(_) => graph.neighbors(from),
        }
    }
}

impl Default for MovementPattern {
    fn default() -> Self {
        MovementPattern::Step(Step)
    }
}

impl From<Step> for MovementPattern {
    fn from(s: Step) -> Self {
        MovementPattern::Step(s)
    }
}

impl From<Hop> for MovementPattern {
    fn from(h: Hop) -> Self {
        MovementPattern::Hop(h)
    }
}

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

    /// Minimal `BoardGraphView` impl for movement tests.
    struct MockGraph { adj: std::collections::HashMap<BoardNodeId, Vec<BoardNodeId>> }

    impl crate::board::BoardGraphView for MockGraph {
        fn node_count(&self) -> usize { self.adj.len() }
        fn edge_count(&self) -> usize { 0 }
        fn face_count(&self) -> usize { 0 }
        fn contains_node(&self, id: BoardNodeId) -> bool { self.adj.contains_key(&id) }
        fn contains_edge(&self, _id: crate::board::EdgeId) -> bool { false }
        fn contains_face(&self, _id: crate::board::FaceId) -> bool { false }
        fn neighbors(&self, n: BoardNodeId) -> Vec<BoardNodeId> { self.adj.get(&n).cloned().unwrap_or_default() }
        fn edges_at(&self, _n: BoardNodeId) -> Vec<crate::board::EdgeId> { vec![] }
        fn endpoints(&self, _e: crate::board::EdgeId) -> Option<(BoardNodeId, BoardNodeId)> { None }
        fn face_bounding(&self, _f: crate::board::FaceId) -> Option<Vec<BoardNodeId>> { None }
        fn faces_at_node(&self, _n: BoardNodeId) -> Vec<crate::board::FaceId> { vec![] }
        fn faces_at_edge(&self, _e: crate::board::EdgeId) -> Vec<crate::board::FaceId> { vec![] }
        fn adjacent_faces(&self, _f: crate::board::FaceId) -> Vec<crate::board::FaceId> { vec![] }
        fn version(&self) -> u64 { 0 }
    }

    fn two_nodes_unblocked() -> (MockGraph, BoardNodeId, BoardNodeId) {
        let a = BoardNodeId(0);
        let b = BoardNodeId(1);
        let mut adj = std::collections::HashMap::new();
        adj.insert(a, vec![b]);
        adj.insert(b, vec![a]);
        (MockGraph { adj }, a, b)
    }

    /// Mock blocker that denies motion across a specific (unordered)
    /// edge. Vision uses the trait default (same set as motion).
    #[derive(Debug, Default)]
    struct WallBetween {
        blocked_pairs: HashSet<(BoardNodeId, BoardNodeId)>,
    }

    impl WallBetween {
        fn between(a: BoardNodeId, b: BoardNodeId) -> Self {
            let mut s = HashSet::new();
            s.insert((a, b));
            s.insert((b, a));
            Self { blocked_pairs: s }
        }
    }

    impl EdgeBlocking for WallBetween {
        fn motion_blocked(&self, from: BoardNodeId, to: BoardNodeId) -> bool {
            self.blocked_pairs.contains(&(from, to))
        }
    }

    #[test]
    fn always_open_blocks_nothing() {
        let (_g, a, b) = two_nodes_unblocked();
        assert!(!AlwaysOpen.motion_blocked(a, b));
        assert!(!AlwaysOpen.vision_blocked(a, b));
    }

    #[test]
    fn step_returns_motion_unblocked_neighbors() {
        let (g, a, b) = two_nodes_unblocked();
        let pattern = MovementPattern::Step(Step);
        let n = pattern.neighbors(a, &g, &AlwaysOpen);
        assert!(n.contains(&b));
    }

    #[test]
    fn step_excludes_motion_blocked_neighbors() {
        // Two connected nodes, but the edge between them is motion-blocked
        // by the oracle. Step must filter b out.
        let (g, a, b) = two_nodes_unblocked();
        let blocker = WallBetween::between(a, b);
        let pattern = MovementPattern::Step(Step);
        let n = pattern.neighbors(a, &g, &blocker);
        assert!(
            n.is_empty(),
            "Step should drop the blocked edge; got {n:?}"
        );
    }

    #[test]
    fn hop_includes_neighbors_with_edge() {
        let (g, a, b) = two_nodes_unblocked();
        let pattern = MovementPattern::Hop(Hop);
        assert!(pattern.neighbors(a, &g, &AlwaysOpen).contains(&b));
    }

    #[test]
    fn hop_ignores_motion_blocking() {
        // Same blocked-edge fixture as `step_excludes_*` — Hop must
        // still report b because it doesn't consult `blocking`.
        let (g, a, b) = two_nodes_unblocked();
        let blocker = WallBetween::between(a, b);
        let pattern = MovementPattern::Hop(Hop);
        assert!(pattern.neighbors(a, &g, &blocker).contains(&b));
    }

    #[test]
    fn pattern_is_cloneable() {
        // Make sure the enum is cloneable like the previous trait object was.
        let original = MovementPattern::Step(Step);
        let cloned = original;
        let (g, a, b) = two_nodes_unblocked();
        assert!(cloned.neighbors(a, &g, &AlwaysOpen).contains(&b));
    }

    #[test]
    fn vision_default_matches_motion() {
        let (_g, a, b) = two_nodes_unblocked();
        let blocker = WallBetween::between(a, b);
        // Default `vision_blocked` impl delegates to `motion_blocked`.
        assert!(blocker.motion_blocked(a, b));
        assert!(blocker.vision_blocked(a, b));
    }
}
