//! Interactive prompt helpers for the CLI.
//!
//! `prompt_secret` reads a masked secret from the TTY (honouring
//! `RELICARIO_TEST_ITEM_SECRET` so integration tests without a TTY can inject
//! secrets); the `prompt_keep`/`prompt_keep_opt`/`prompt_yesno` variants are
//! used by the edit handlers to keep current values when the user hits enter
//! at a blank prompt. `prompt_or_flag` and `prompt_or_flag_optional` thread a
//! CLI-flag value through the same path so command handlers can use one call
//! site whether the value came from the command line or an interactive prompt.

use anyhow::Result;
use std::io::BufRead;

/// `rpassword::prompt_password` wrapper that honours `RELICARIO_TEST_ITEM_SECRET`
/// for integration-test use (rpassword reads /dev/tty by default, which is
/// unavailable in assert_cmd-spawned children).
pub(crate) fn prompt_secret(label: &str) -> Result<String> {
    if let Some(s) = crate::test_item_secret_override() {
        return Ok(s);
    }
    rpassword::prompt_password(label).map_err(Into::into)
}

fn read_required_line<R: BufRead>(reader: &mut R, label: &str) -> Result<String> {
    eprint!("{label}: ");
    std::io::Write::flush(&mut std::io::stderr())?;
    let mut s = String::new();
    reader.read_line(&mut s)?;
    let trimmed = s.trim().to_string();
    if trimmed.is_empty() { anyhow::bail!("{label} required"); }
    Ok(trimmed)
}

fn read_optional_line<R: BufRead>(reader: &mut R, label: &str) -> Result<Option<String>> {
    eprint!("{label} (leave blank to skip): ");
    std::io::Write::flush(&mut std::io::stderr())?;
    let mut s = String::new();
    reader.read_line(&mut s)?;
    let trimmed = s.trim().to_string();
    Ok(if trimmed.is_empty() { None } else { Some(trimmed) })
}

pub(crate) fn prompt_keep(label: &str, current: &str) -> Result<Option<String>> {
    eprint!("{label} [{current}]: ");
    std::io::Write::flush(&mut std::io::stderr())?;
    let mut s = String::new();
    std::io::stdin().read_line(&mut s)?;
    let trimmed = s.trim().to_string();
    Ok(if trimmed.is_empty() { None } else { Some(trimmed) })
}

pub(crate) fn prompt_keep_opt(label: &str, current: Option<&str>) -> Result<Option<String>> {
    let display = current.unwrap_or("(none)");
    eprint!("{label} [{display}]: ");
    std::io::Write::flush(&mut std::io::stderr())?;
    let mut s = String::new();
    std::io::stdin().read_line(&mut s)?;
    let trimmed = s.trim().to_string();
    Ok(if trimmed.is_empty() { None } else { Some(trimmed) })
}

pub(crate) fn prompt_yesno(label: &str) -> Result<bool> {
    eprint!("{label} [y/N] ");
    std::io::Write::flush(&mut std::io::stderr())?;
    let mut s = String::new();
    std::io::stdin().read_line(&mut s)?;
    Ok(matches!(s.trim().to_ascii_lowercase().as_str(), "y" | "yes"))
}

pub(crate) fn prompt_or_flag<T>(
    flag: Option<T>,
    label: &str,
    parser: impl FnOnce(&str) -> Result<T>,
) -> Result<T> {
    let stdin = std::io::stdin();
    let mut reader = std::io::BufReader::new(stdin.lock());
    prompt_or_flag_with_reader(flag, label, parser, &mut reader)
}

pub(crate) fn prompt_or_flag_optional<T>(
    flag: Option<T>,
    label: &str,
    parser: impl FnOnce(&str) -> Result<T>,
) -> Result<Option<T>> {
    let stdin = std::io::stdin();
    let mut reader = std::io::BufReader::new(stdin.lock());
    prompt_or_flag_optional_with_reader(flag, label, parser, &mut reader)
}

pub(crate) fn prompt_or_flag_with_reader<T, R: BufRead>(
    flag: Option<T>,
    label: &str,
    parser: impl FnOnce(&str) -> Result<T>,
    reader: &mut R,
) -> Result<T> {
    if let Some(t) = flag {
        return Ok(t);
    }
    let line = read_required_line(reader, label)?;
    parser(&line)
}

pub(crate) fn prompt_or_flag_optional_with_reader<T, R: BufRead>(
    flag: Option<T>,
    label: &str,
    parser: impl FnOnce(&str) -> Result<T>,
    reader: &mut R,
) -> Result<Option<T>> {
    if let Some(t) = flag {
        return Ok(Some(t));
    }
    match read_optional_line(reader, label)? {
        None => Ok(None),
        Some(line) => parser(&line).map(Some),
    }
}

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

    #[test]
    fn prompt_or_flag_uses_flag_value_when_some() {
        let mut reader = Cursor::new(Vec::<u8>::new());
        let got = prompt_or_flag_with_reader::<String, _>(
            Some("from-flag".to_string()),
            "Title",
            |_| panic!("parser must not run when flag is Some"),
            &mut reader,
        ).expect("flag value path should succeed");
        assert_eq!(got, "from-flag");
    }

    #[test]
    fn prompt_or_flag_prompts_when_none() {
        let mut reader = Cursor::new(b"prompted\n".to_vec());
        let got = prompt_or_flag_with_reader::<String, _>(
            None,
            "Title",
            |s| Ok(s.to_string()),
            &mut reader,
        ).expect("prompt path should succeed");
        assert_eq!(got, "prompted");
    }

    #[test]
    fn prompt_or_flag_optional_returns_some_from_flag_without_reading() {
        let mut reader = Cursor::new(Vec::<u8>::new());
        let got = prompt_or_flag_optional_with_reader::<String, _>(
            Some("flag-val".to_string()),
            "URL",
            |_| panic!("parser must not run when flag is Some"),
            &mut reader,
        ).expect("flag value path should succeed");
        assert_eq!(got, Some("flag-val".to_string()));
    }

    #[test]
    fn prompt_or_flag_optional_prompts_and_blank_yields_none() {
        let mut reader = Cursor::new(b"\n".to_vec());
        let got = prompt_or_flag_optional_with_reader::<String, _>(
            None,
            "URL",
            |_| panic!("parser must not run on blank input"),
            &mut reader,
        ).expect("blank prompt should succeed with None");
        assert_eq!(got, None);
    }

    #[test]
    fn prompt_or_flag_optional_prompts_and_value_runs_parser() {
        let mut reader = Cursor::new(b"  42  \n".to_vec());
        let got = prompt_or_flag_optional_with_reader::<u32, _>(
            None,
            "Number",
            |s| s.parse::<u32>().map_err(Into::into),
            &mut reader,
        ).expect("value should parse");
        assert_eq!(got, Some(42));
    }
}
