Documentation
use std::io::{BufRead, Write};
use std::path::Path;

use crate::init::evidence::Evidence;
use crate::init::fit::measure;
use crate::init::report::summary_line;
use crate::init::settings::{to_configuration, Confidence, SettingId, Settings, ALL_SETTINGS};

pub enum Outcome {
    Write(Settings),
    Quit,
}

pub fn run(
    root: &Path,
    evidence: &Evidence,
    detected: &Settings,
    defaults: &Settings,
    input: &mut impl BufRead,
    output: &mut impl Write,
) -> Outcome {
    let decisions: Vec<SettingId> = detected
        .differing(defaults)
        .into_iter()
        .filter(|id| detected.confidence(*id) != Confidence::Overridden)
        .collect();

    if decisions.is_empty() {
        let _ = writeln!(output, "{}", summary_line(evidence));
        let _ = writeln!(
            output,
            "detection matches the iwe defaults — nothing to choose"
        );
        return Outcome::Write(detected.clone());
    }

    draw(root, evidence, detected, defaults, &decisions, output);

    loop {
        let mut line = String::new();
        if input.read_line(&mut line).unwrap_or(0) == 0 {
            let _ = writeln!(output);
            return Outcome::Quit;
        }

        match line.trim() {
            "" | "y" => return Outcome::Write(detected.clone()),
            "n" => {
                let mut chosen = detected.clone();
                for id in &decisions {
                    chosen.adopt(*id, defaults);
                }
                return Outcome::Write(chosen);
            }
            "q" => return Outcome::Quit,
            _ => {
                let _ = writeln!(output, "answer y, n or q");
                prompt(output);
            }
        }
    }
}

fn draw(
    root: &Path,
    evidence: &Evidence,
    detected: &Settings,
    defaults: &Settings,
    decisions: &[SettingId],
    output: &mut impl Write,
) {
    let _ = writeln!(output);
    let _ = writeln!(output, "{}", summary_line(evidence));
    let _ = writeln!(output);
    let _ = writeln!(output, "    {:<18} {:<20} DEFAULT:", "", "DETECTED:");

    for id in ALL_SETTINGS {
        let on_offer = decisions.contains(&id);
        let mut detected_cell = detected.get(id).to_string();
        if detected.is_mixed(id) {
            detected_cell.push_str(" ?");
        }
        let default_cell = if on_offer {
            defaults.get(id).to_string()
        } else {
            String::new()
        };
        let _ = writeln!(
            output,
            "  {} {:<18} {:<20} {}",
            if on_offer { "" } else { " " },
            id.label(),
            detected_cell,
            default_cell
        );
    }

    let _ = writeln!(output);
    let _ = writeln!(
        output,
        "    {:<18} {:<20} {}",
        "normalize",
        measure(root, &to_configuration(detected)).render(),
        measure(root, &to_configuration(defaults)).render()
    );
    let _ = writeln!(output);
    prompt(output);
}

fn prompt(output: &mut impl Write) {
    let _ = writeln!(
        output,
        "write the detected settings? ⏎/y detected · n defaults · q quit"
    );
    let _ = write!(output, "> ");
    let _ = output.flush();
}

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

    use super::{run, Outcome};
    use crate::init::evidence::Evidence;
    use crate::init::settings::{defaults, Confidence, SettingId, Settings, Value};

    fn bundles() -> (Settings, Settings) {
        let base = defaults();
        let mut detected = base.clone();
        detected.set(
            SettingId::LibraryPath,
            Value::text("notes"),
            Confidence::Detected,
            "3 of 3 files live under notes/",
        );
        detected.set(
            SettingId::LinkFormat,
            Value::text("wiki"),
            Confidence::Detected,
            "3 wiki links vs 0 markdown links",
        );
        (detected, base)
    }

    fn drive(input: &str) -> (Option<Settings>, String) {
        let (detected, base) = bundles();
        drive_bundles(input, &detected, &base)
    }

    fn drive_bundles(
        input: &str,
        detected: &Settings,
        base: &Settings,
    ) -> (Option<Settings>, String) {
        let mut reader = Cursor::new(input.as_bytes().to_vec());
        let mut output = Vec::new();

        let outcome = run(
            Path::new("/iwe-nonexistent-corpus"),
            &Evidence::default(),
            detected,
            base,
            &mut reader,
            &mut output,
        );

        let selected = match outcome {
            Outcome::Write(settings) => Some(settings),
            Outcome::Quit => None,
        };

        (
            selected,
            String::from_utf8(output).expect("valid UTF-8 output"),
        )
    }

    fn rows_of(transcript: &str) -> Vec<String> {
        transcript
            .lines()
            .map(|line| {
                line.split_whitespace()
                    .filter(|token| *token != "")
                    .collect::<Vec<_>>()
                    .join(" ")
            })
            .filter(|line| {
                line.starts_with("library")
                    || line.starts_with("links")
                    || line.starts_with("format")
            })
            .collect()
    }

    #[test]
    fn empty_line_writes_the_detected_bundle() {
        let (selected, _) = drive("\n");
        let selected = selected.expect("screen writes on Enter");

        assert_eq!(Value::text("notes"), selected.get(SettingId::LibraryPath));
        assert_eq!(Value::text("wiki"), selected.get(SettingId::LinkFormat));
    }

    #[test]
    fn yes_writes_the_detected_bundle() {
        let (selected, _) = drive("y\n");
        let selected = selected.expect("screen writes on y");

        assert_eq!(Value::text("notes"), selected.get(SettingId::LibraryPath));
        assert_eq!(Value::text("wiki"), selected.get(SettingId::LinkFormat));
    }

    #[test]
    fn no_writes_the_default_bundle() {
        let (selected, _) = drive("n\n");
        let selected = selected.expect("screen writes on n");

        assert_eq!(Value::text(""), selected.get(SettingId::LibraryPath));
        assert_eq!(Value::text("markdown"), selected.get(SettingId::LinkFormat));
    }

    #[test]
    fn quit_writes_nothing() {
        let (selected, _) = drive("q\n");

        assert!(selected.is_none());
    }

    #[test]
    fn end_of_input_writes_nothing() {
        let (selected, _) = drive("");

        assert!(selected.is_none());
    }

    #[test]
    fn unknown_input_asks_again() {
        let (selected, transcript) = drive("x\ny\n");
        let selected = selected.expect("screen writes on y");
        let asked: Vec<&str> = transcript
            .lines()
            .filter(|line| *line == "> answer y, n or q")
            .collect();

        assert_eq!(vec!["> answer y, n or q"], asked);
        assert_eq!(Value::text("notes"), selected.get(SettingId::LibraryPath));
    }

    #[test]
    fn the_screen_lists_every_setting_and_offers_the_differing_ones() {
        let (_, transcript) = drive("\n");

        assert_eq!(
            vec![
                "library notes none".to_string(),
                "format markdown".to_string(),
                "links wiki markdown".to_string(),
            ],
            rows_of(&transcript)
        );
    }

    #[test]
    fn an_overridden_setting_keeps_its_value_when_defaults_are_chosen() {
        let base = defaults();
        let mut detected = base.clone();
        detected.set(
            SettingId::LibraryPath,
            Value::text("notes"),
            Confidence::Detected,
            "3 of 3 files live under notes/",
        );
        detected.set(
            SettingId::LinkFormat,
            Value::text("wiki"),
            Confidence::Overridden,
            "set on the command line",
        );

        let (selected, transcript) = drive_bundles("n\n", &detected, &base);
        let selected = selected.expect("screen writes on n");

        assert_eq!(
            vec![
                "library notes none".to_string(),
                "format markdown".to_string(),
                "links wiki".to_string(),
            ],
            rows_of(&transcript)
        );
        assert_eq!(Value::text(""), selected.get(SettingId::LibraryPath));
        assert_eq!(Value::text("wiki"), selected.get(SettingId::LinkFormat));
    }
}