//! Component / overlay contracts (port of `src/ui/contracts.ts`).
//!
//! Interface-only surfaces in TS; here they become traits and plain value types.
//! `SizeValue` carries the TS `number | "${number}%"` union as a tagged enum.

/// A renderable, focusable, input-handling unit (≈ TS `Component` interface).
///
/// All lifecycle/focus/input methods are optional in TS; in Rust they get
/// default no-op bodies so implementors only override what they need. `render`
/// is required and returns the painted lines for the given width.
pub trait Component {
    /// Called when the component is attached to the tree.
    fn on_mount(&mut self) {}
    /// Called when the component is detached from the tree.
    fn on_unmount(&mut self) {}
    /// Called when the component gains focus.
    fn on_focus(&mut self) {}
    /// Called when the component loses focus.
    fn on_blur(&mut self) {}
    /// Feed raw input bytes to the component.
    fn handle_input(&mut self, _data: &str) {}
    /// Paint the component to `width` columns, returning one string per row.
    fn render(&mut self, width: usize) -> Vec<String>;
    /// Mark the component dirty so it repaints on the next frame.
    fn invalidate(&mut self);
}

/// Host surface a component can call back into (≈ TS `TUI` interface).
pub trait Tui {
    /// Ask the host to schedule a render pass.
    fn request_render(&self) {}
    /// Move focus to the given component (or clear focus when `None`).
    fn focus(&self, _component: Option<&dyn Component>) {}
}

/// Where an overlay anchors relative to the terminal (≈ TS `OverlayAnchor`).
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum OverlayAnchor {
    Center,
    TopCenter,
    BottomCenter,
    LeftCenter,
    RightCenter,
    TopLeft,
    TopRight,
    BottomLeft,
    BottomRight,
}

/// Per-edge overlay margins (≈ TS `OverlayMargin`; all optional).
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
pub struct OverlayMargin {
    pub left: Option<usize>,
    pub right: Option<usize>,
    pub top: Option<usize>,
    pub bottom: Option<usize>,
}

/// Either an absolute cell count or a percentage of the available extent.
///
/// Ports the TS template-literal union `number | "${number}%"`.
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum SizeValue {
    /// Absolute number of cells.
    Cells(f64),
    /// Percentage `0.0..=100.0` of the available extent.
    Percent(f64),
}

/// A margin that is either uniform (single number) or per-edge.
///
/// Ports the TS `margin?: OverlayMargin | number` union.
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum MarginSpec {
    Uniform(usize),
    PerEdge(OverlayMargin),
}

/// Placement + sizing + conditional-visibility options for an overlay
/// (≈ TS `OverlayOptions`; every field optional).
#[derive(Default)]
pub struct OverlayOptions {
    // Placement
    pub anchor: Option<OverlayAnchor>,
    pub row: Option<SizeValue>,
    pub col: Option<SizeValue>,
    pub offset_x: Option<i64>,
    pub offset_y: Option<i64>,
    pub margin: Option<MarginSpec>,
    // Sizing
    pub width: Option<SizeValue>,
    pub min_width: Option<usize>,
    pub max_height: Option<SizeValue>,
    /// Conditional display: `(term_width, term_height) -> bool`.
    pub visible: Option<VisiblePredicate>,
}

/// Conditional-display predicate for an overlay (`(term_width, term_height) -> bool`).
pub type VisiblePredicate = Box<dyn Fn(usize, usize) -> bool>;

/// Handle for toggling an attached overlay's visibility (≈ TS `OverlayHandle`).
pub trait OverlayHandle {
    /// Whether the overlay is currently hidden.
    fn is_hidden(&self) -> bool;
    /// Set the hidden flag explicitly.
    fn set_hidden(&mut self, hidden: bool);
    /// Hide the overlay (convenience for `set_hidden(true)`).
    fn hide(&mut self);
}

#[cfg(test)]
mod tests {
    //! These mirror the TS `contracts.ts` surface. The TS originals are pure
    //! `interface`s / type aliases with no runtime, so the testable behavior
    //! is: the `Component`/`Tui` optional hooks have no-op defaults (TS `?`
    //! methods), the overlay value types carry the right variants, every
    //! `OverlayOptions` field is optional (defaults to `None`), the predicate
    //! closure is invocable, and an `OverlayHandle` impl toggles correctly.
    use super::*;

    /// Minimal `Component` that only overrides the two required methods,
    /// leaving every lifecycle/focus/input hook on its no-op default. This is
    /// the Rust analogue of a TS object implementing only `render`/`invalidate`.
    #[derive(Default)]
    struct CountingComponent {
        renders: usize,
        invalidations: usize,
        last_width: usize,
        input: String,
    }

    impl Component for CountingComponent {
        fn handle_input(&mut self, data: &str) {
            self.input.push_str(data);
        }
        fn render(&mut self, width: usize) -> Vec<String> {
            self.renders += 1;
            self.last_width = width;
            vec![format!("w={width}")]
        }
        fn invalidate(&mut self) {
            self.invalidations += 1;
        }
    }

    #[test]
    fn component_default_hooks_are_no_ops() {
        let mut c = CountingComponent::default();
        // The optional TS hooks (`onMount?`, `onFocus?`, ...) default to no-ops
        // and must not touch state or panic.
        c.on_mount();
        c.on_focus();
        c.on_blur();
        c.on_unmount();
        assert_eq!(c.renders, 0);
        assert_eq!(c.invalidations, 0);
    }

    #[test]
    fn component_required_methods_run() {
        let mut c = CountingComponent::default();
        c.handle_input("ab");
        c.handle_input("c");
        let lines = c.render(42);
        c.invalidate();
        assert_eq!(c.input, "abc");
        assert_eq!(lines, vec!["w=42"]);
        assert_eq!(c.renders, 1);
        assert_eq!(c.last_width, 42);
        assert_eq!(c.invalidations, 1);
    }

    /// A `Tui` host that records callbacks, exercising the no-op-default trait.
    #[derive(Default)]
    struct RecordingTui {
        render_requests: std::cell::Cell<usize>,
    }

    impl Tui for RecordingTui {
        fn request_render(&self) {
            self.render_requests.set(self.render_requests.get() + 1);
        }
    }

    #[test]
    fn tui_default_focus_is_no_op_and_request_render_overridable() {
        let host = RecordingTui::default();
        host.request_render();
        host.request_render();
        // `focus` keeps its no-op default; calling it must not panic.
        host.focus(None);
        assert_eq!(host.render_requests.get(), 2);
    }

    #[test]
    fn overlay_anchor_has_all_nine_variants() {
        // Mirrors the TS `OverlayAnchor` string union (9 members).
        let all = [
            OverlayAnchor::Center,
            OverlayAnchor::TopCenter,
            OverlayAnchor::BottomCenter,
            OverlayAnchor::LeftCenter,
            OverlayAnchor::RightCenter,
            OverlayAnchor::TopLeft,
            OverlayAnchor::TopRight,
            OverlayAnchor::BottomLeft,
            OverlayAnchor::BottomRight,
        ];
        assert_eq!(all.len(), 9);
        // Derived equality holds and distinct variants differ.
        assert_eq!(OverlayAnchor::Center, OverlayAnchor::Center);
        assert_ne!(OverlayAnchor::TopLeft, OverlayAnchor::BottomRight);
    }

    #[test]
    fn size_value_distinguishes_cells_from_percent() {
        // Ports the TS `number | "${number}%"` union as a tagged enum.
        let cells = SizeValue::Cells(80.0);
        let pct = SizeValue::Percent(50.0);
        assert_ne!(cells, pct);
        match cells {
            SizeValue::Cells(n) => assert_eq!(n, 80.0),
            SizeValue::Percent(_) => panic!("expected Cells"),
        }
        match pct {
            SizeValue::Percent(p) => assert_eq!(p, 50.0),
            SizeValue::Cells(_) => panic!("expected Percent"),
        }
    }

    #[test]
    fn overlay_margin_defaults_all_none() {
        // TS `OverlayMargin` has every edge optional.
        let m = OverlayMargin::default();
        assert!(m.left.is_none());
        assert!(m.right.is_none());
        assert!(m.top.is_none());
        assert!(m.bottom.is_none());

        let m2 = OverlayMargin {
            left: Some(1),
            top: Some(2),
            ..Default::default()
        };
        assert_eq!(m2.left, Some(1));
        assert_eq!(m2.top, Some(2));
        assert!(m2.right.is_none());
        assert!(m2.bottom.is_none());
    }

    #[test]
    fn margin_spec_covers_uniform_and_per_edge() {
        // Ports the TS `margin?: OverlayMargin | number` union.
        let uniform = MarginSpec::Uniform(3);
        let per_edge = MarginSpec::PerEdge(OverlayMargin {
            left: Some(4),
            ..Default::default()
        });
        match uniform {
            MarginSpec::Uniform(n) => assert_eq!(n, 3),
            MarginSpec::PerEdge(_) => panic!("expected Uniform"),
        }
        match per_edge {
            MarginSpec::PerEdge(m) => assert_eq!(m.left, Some(4)),
            MarginSpec::Uniform(_) => panic!("expected PerEdge"),
        }
    }

    #[test]
    fn overlay_options_default_is_all_none() {
        // Every TS `OverlayOptions` field is optional; default must be empty.
        let o = OverlayOptions::default();
        assert!(o.anchor.is_none());
        assert!(o.row.is_none());
        assert!(o.col.is_none());
        assert!(o.offset_x.is_none());
        assert!(o.offset_y.is_none());
        assert!(o.margin.is_none());
        assert!(o.width.is_none());
        assert!(o.min_width.is_none());
        assert!(o.max_height.is_none());
        assert!(o.visible.is_none());
    }

    #[test]
    fn overlay_options_populated_fields_round_trip() {
        let o = OverlayOptions {
            anchor: Some(OverlayAnchor::TopRight),
            width: Some(SizeValue::Percent(80.0)),
            min_width: Some(20),
            max_height: Some(SizeValue::Cells(10.0)),
            offset_x: Some(-2),
            offset_y: Some(3),
            margin: Some(MarginSpec::Uniform(1)),
            ..Default::default()
        };
        assert_eq!(o.anchor, Some(OverlayAnchor::TopRight));
        assert_eq!(o.width, Some(SizeValue::Percent(80.0)));
        assert_eq!(o.min_width, Some(20));
        assert_eq!(o.max_height, Some(SizeValue::Cells(10.0)));
        assert_eq!(o.offset_x, Some(-2));
        assert_eq!(o.offset_y, Some(3));
        assert_eq!(o.margin, Some(MarginSpec::Uniform(1)));
    }

    #[test]
    fn visible_predicate_is_invocable() {
        // TS `visible?: (termWidth, termHeight) => boolean`.
        let pred: VisiblePredicate = Box::new(|w, h| w >= 80 && h >= 24);
        let o = OverlayOptions {
            visible: Some(pred),
            ..Default::default()
        };
        let v = o.visible.as_ref().unwrap();
        assert!(v(100, 30));
        assert!(!v(40, 30));
        assert!(!v(100, 10));
    }

    /// An `OverlayHandle` impl over a simple flag, mirroring the TS interface
    /// where `hide()` is sugar for `setHidden(true)`.
    #[derive(Default)]
    struct FlagOverlay {
        hidden: bool,
    }

    impl OverlayHandle for FlagOverlay {
        fn is_hidden(&self) -> bool {
            self.hidden
        }
        fn set_hidden(&mut self, hidden: bool) {
            self.hidden = hidden;
        }
        fn hide(&mut self) {
            self.set_hidden(true);
        }
    }

    #[test]
    fn overlay_handle_toggles_and_hides() {
        let mut h = FlagOverlay::default();
        assert!(!h.is_hidden());
        h.set_hidden(true);
        assert!(h.is_hidden());
        h.set_hidden(false);
        assert!(!h.is_hidden());
        h.hide();
        assert!(h.is_hidden());
    }
}
