//! Interactive user interface components.
//!
//! This module provides:
//! - [`UserInterface`] trait for UI abstraction
//! - [`TerminalUI`] for interactive terminal usage
//! - [`NonInteractiveUI`] for CI/headless environments
//! - Prompts, spinners, progress indicators, and tables
//!
//! # Example
//!
//! ```
//! use bivvy::ui::{create_ui, OutputMode};
//!
//! // Use non-interactive mode for testability
//! let mut ui = create_ui(false, OutputMode::Quiet);
//! ui.show_header("My App");
//! ui.success("Setup complete!");
//! ```

pub mod hints;
pub mod icons;
pub mod mock;
pub mod non_interactive;
pub mod output;
pub mod preflight;
pub mod presenter;
pub mod progress;
pub mod prompts;
pub mod spinner;
pub mod surface;
pub mod table;
pub mod terminal;
pub mod theme;

pub use icons::StatusKind;
pub use mock::{MockSpinner, MockUI};
pub use non_interactive::NonInteractiveUI;
pub use output::{
    check_passed_label, rerun_prompt_label, satisfaction_label, FixOutputSink, Output, OutputMode,
    VerboseStreamSink,
};
pub use preflight::PreflightCollector;
pub use presenter::EventPresenter;
pub use progress::{format_duration, format_relative_time, StepProgress};
pub use prompts::prompt_user;
pub use spinner::{step_spinner, ProgressSpinner};
pub use surface::TerminalSurface;
pub use table::Table;
pub use terminal::{create_ui, TerminalUI};
pub use theme::{should_use_colors, BivvyTheme};

/// Returns true when `TERM=dumb`.
///
/// Dumb terminals don't support cursor movement, so interactive redraws
/// (clear-and-repaint cycles) produce garbage output. When true, prompts
/// render forward-only and `clear_lines` becomes a no-op.
pub(crate) fn is_dumb_term() -> bool {
    std::env::var("TERM").map(|v| v == "dumb").unwrap_or(false)
}

use crate::error::Result;
use indicatif::ProgressBar;

/// Output methods for displaying messages, warnings, errors, and hints.
pub trait OutputWriter {
    /// Display a message to the user.
    fn message(&mut self, msg: &str);

    /// Display a success message.
    fn success(&mut self, msg: &str);

    /// Display a warning message.
    fn warning(&mut self, msg: &str);

    /// Display an error message.
    fn error(&mut self, msg: &str);

    /// Show a contextual hint to the user.
    fn show_hint(&mut self, hint: &str) {
        self.message(hint);
    }

    /// Show a bordered error block with command, output, and optional hint.
    ///
    /// `indent` is the column at which the block should align (typically the
    /// step number width, so error content lines up under the step name).
    /// Implementations that don't render a visual box may ignore it.
    fn show_error_block(&mut self, command: &str, output: &str, hint: Option<&str>, indent: usize) {
        let _ = indent;
        self.error(command);
        if !output.is_empty() {
            self.message(output);
        }
        if let Some(h) = hint {
            self.show_hint(h);
        }
    }
}

/// Interactive prompting.
pub trait Prompter {
    /// Show a prompt and get user input.
    fn prompt(&mut self, prompt: &Prompt) -> Result<PromptResult>;
}

/// Spinner creation and management.
pub trait SpinnerFactory {
    /// Start a spinner for an operation.
    fn start_spinner(&mut self, message: &str) -> Box<dyn SpinnerHandle>;

    /// Start a spinner with leading indentation.
    fn start_spinner_indented(&mut self, message: &str, indent: usize) -> Box<dyn SpinnerHandle> {
        let _ = indent;
        self.start_spinner(message)
    }
}

/// Headers and progress indicators.
pub trait ProgressDisplay {
    /// Show a header/banner.
    fn show_header(&mut self, title: &str);

    /// Show progress (e.g., "Step 3 of 7").
    fn show_progress(&mut self, current: usize, total: usize);
}

/// Output mode and interactivity state.
pub trait UiState {
    /// Get the current output mode.
    fn output_mode(&self) -> OutputMode;

    /// Check if running in interactive mode.
    fn is_interactive(&self) -> bool;

    /// Change the output mode at runtime.
    fn set_output_mode(&mut self, mode: OutputMode);

    /// Attach a [`crate::ui::surface::TerminalSurface`] for the
    /// duration of a run. Default impl is a no-op for UIs that have no
    /// surface concept (mock, non-interactive). Only `TerminalUI`
    /// overrides this.
    fn attach_surface(&mut self, _surface: std::sync::Arc<crate::ui::surface::TerminalSurface>) {}

    /// Detach the surface attached by [`Self::attach_surface`].
    fn detach_surface(&mut self) {}
}

/// Combined trait for all generic user interface interactions.
///
/// Run-path workflow chrome and per-step rendering are *not* part of
/// this trait — see [`crate::runner::display`] for those concerns. This
/// trait is intentionally narrow so that init / status / list / lint
/// commands can mock it without dragging in workflow types.
pub trait UserInterface:
    OutputWriter + Prompter + SpinnerFactory + ProgressDisplay + UiState
{
}

impl<T> UserInterface for T where
    T: OutputWriter + Prompter + SpinnerFactory + ProgressDisplay + UiState
{
}

/// Handle for controlling a spinner.
pub trait SpinnerHandle {
    /// Update the spinner message.
    fn set_message(&mut self, msg: &str);

    /// Mark the operation as successful.
    fn finish_success(&mut self, msg: &str);

    /// Mark the operation as failed.
    fn finish_error(&mut self, msg: &str);

    /// Mark as skipped.
    fn finish_skipped(&mut self, msg: &str);

    /// Clear the spinner without leaving any output.
    fn finish_and_clear(&mut self);

    /// Get a clone of the underlying progress bar, if available.
    ///
    /// This is used for live output streaming: the cloned bar can be shared
    /// with an `OutputCallback` to update the spinner message in real-time.
    /// Returns `None` for mock/hidden spinners.
    fn progress_bar(&self) -> Option<ProgressBar> {
        None
    }
}

/// A prompt to show to the user.
#[derive(Debug, Clone)]
pub struct Prompt {
    /// Unique key for the prompt (used for caching/lookup).
    pub key: String,
    /// The question to display.
    pub question: String,
    /// The type of prompt.
    pub prompt_type: PromptType,
    /// Default value if user just presses enter.
    pub default: Option<String>,
}

/// The type of prompt.
#[derive(Debug, Clone)]
pub enum PromptType {
    /// Yes/no confirmation.
    Confirm,
    /// Free-form text input.
    Input,
    /// Select one from a list of options.
    Select { options: Vec<PromptOption> },
    /// Select multiple from a list of options.
    MultiSelect { options: Vec<PromptOption> },
}

/// An option in a select prompt.
#[derive(Debug, Clone)]
pub struct PromptOption {
    /// Display label.
    pub label: String,
    /// Value returned when selected.
    pub value: String,
}

/// Result of a prompt.
#[derive(Debug, Clone)]
pub enum PromptResult {
    /// Boolean result from confirm.
    Bool(bool),
    /// String result from input or select.
    String(String),
    /// Multiple string results from multi-select.
    Strings(Vec<String>),
}

impl PromptResult {
    /// Get as string, suitable for interpolation.
    pub fn as_string(&self) -> String {
        match self {
            Self::Bool(b) => b.to_string(),
            Self::String(s) => s.clone(),
            Self::Strings(v) => v.join(","),
        }
    }

    /// Get as bool if this is a Bool result.
    pub fn as_bool(&self) -> Option<bool> {
        match self {
            Self::Bool(b) => Some(*b),
            _ => None,
        }
    }
}

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

    #[test]
    fn prompt_result_as_string_bool() {
        assert_eq!(PromptResult::Bool(true).as_string(), "true");
        assert_eq!(PromptResult::Bool(false).as_string(), "false");
    }

    #[test]
    fn prompt_result_as_string_string() {
        assert_eq!(
            PromptResult::String("hello".to_string()).as_string(),
            "hello"
        );
    }

    #[test]
    fn prompt_result_as_string_strings() {
        assert_eq!(
            PromptResult::Strings(vec!["a".to_string(), "b".to_string()]).as_string(),
            "a,b"
        );
    }

    #[test]
    fn prompt_result_as_bool() {
        assert_eq!(PromptResult::Bool(true).as_bool(), Some(true));
        assert_eq!(PromptResult::String("test".to_string()).as_bool(), None);
    }

    #[test]
    fn prompt_option_creation() {
        let opt = PromptOption {
            label: "Display Text".to_string(),
            value: "value".to_string(),
        };
        assert_eq!(opt.label, "Display Text");
        assert_eq!(opt.value, "value");
    }

    #[test]
    fn prompt_type_variants() {
        let confirm = PromptType::Confirm;
        let input = PromptType::Input;
        let select = PromptType::Select { options: vec![] };
        let multi = PromptType::MultiSelect { options: vec![] };

        // Just verify they can be created
        assert!(matches!(confirm, PromptType::Confirm));
        assert!(matches!(input, PromptType::Input));
        assert!(matches!(select, PromptType::Select { .. }));
        assert!(matches!(multi, PromptType::MultiSelect { .. }));
    }

    #[test]
    fn multiselect_empty_selection_returns_empty_vec() {
        let result = PromptResult::Strings(vec![]);
        assert_eq!(result.as_string(), "");
    }

    #[test]
    fn multiselect_single_selection() {
        let result = PromptResult::Strings(vec!["db".to_string()]);
        assert_eq!(result.as_string(), "db");
    }

    #[test]
    fn multiselect_multiple_selections() {
        let result = PromptResult::Strings(vec![
            "db".to_string(),
            "cache".to_string(),
            "search".to_string(),
        ]);
        assert_eq!(result.as_string(), "db,cache,search");
    }

    #[test]
    fn prompt_type_select_stores_options() {
        let options = vec![
            PromptOption {
                label: "Option A".to_string(),
                value: "a".to_string(),
            },
            PromptOption {
                label: "Option B".to_string(),
                value: "b".to_string(),
            },
        ];

        let prompt_type = PromptType::Select {
            options: options.clone(),
        };

        if let PromptType::Select { options: stored } = prompt_type {
            assert_eq!(stored.len(), 2);
            assert_eq!(stored[0].value, "a");
        } else {
            panic!("Expected Select variant");
        }
    }

    #[test]
    fn prompt_type_multiselect_stores_options() {
        let options = vec![PromptOption {
            label: "Feature 1".to_string(),
            value: "f1".to_string(),
        }];

        let prompt_type = PromptType::MultiSelect { options };

        if let PromptType::MultiSelect { options: stored } = prompt_type {
            assert_eq!(stored.len(), 1);
        } else {
            panic!("Expected MultiSelect variant");
        }
    }

    #[test]
    fn prompt_result_strings_preserves_order() {
        let result = PromptResult::Strings(vec![
            "first".to_string(),
            "second".to_string(),
            "third".to_string(),
        ]);
        assert_eq!(result.as_string(), "first,second,third");
    }

    #[test]
    fn prompt_result_as_bool_returns_none_for_strings() {
        let result = PromptResult::Strings(vec!["true".to_string()]);
        assert_eq!(result.as_bool(), None);
    }
}
