use crate::app_config::ReviewAudioPolicy;
use crate::dialog_support::{is_affirmative_dialog_result, is_dialog_accept_key};
use crate::exam_mode::{ExamAnswer, ExamQuestion, ExamSizeMode, ExamSizeRequest};
use crate::keyboard::is_toggle_review_audio_key;
use crate::localization::Localizer;
use crate::review_audio::{ReviewAudioFile, ReviewAudioPlayer};
use crate::review_report::{exam_report_file, save_review_report};
use crate::{ExamGradeUi, ExamQuestionUi, ExamSetupUi, ExamSummaryUi};
use std::cell::RefCell;
use std::path::Path;
use std::rc::Rc;
use wxdragon::id::{ID_CANCEL, ID_NO, ID_OK, ID_YES};
use wxdragon::prelude::*;

#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) enum ExamQuestionOutcome {
    Submitted(ExamAnswer),
    SubmittedAndEnded(ExamAnswer),
    Ended,
}

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) enum ExamInexactReviewOutcome {
    Next,
    Summary,
}

pub(crate) fn show_exam_setup_dialog(
    parent: Frame,
    localizer: Localizer,
    active_card_count: usize,
) -> Option<ExamSizeRequest> {
    let dialog = ExamSetupUi::new(Some(&parent), false);
    let start_id = ExamSetupUi::xrc_id("button_exam_setup_start");
    let cancel_id = ExamSetupUi::xrc_id("button_exam_setup_cancel");

    dialog
        .dialog_exam_setup
        .set_label(localizer.exam_setup_dialog_title());
    dialog
        .label_exam_setup_status
        .set_label(&localizer.exam_setup_status(active_card_count));
    dialog
        .label_exam_size_mode
        .set_label(localizer.exam_size_mode_label());
    dialog
        .label_exam_size_value
        .set_label(localizer.exam_size_value_label());
    dialog
        .checkbox_exam_exact
        .set_label(localizer.exam_exact_checkbox_label());
    dialog
        .button_exam_setup_cancel
        .set_label(localizer.options_cancel_button());
    dialog
        .button_exam_setup_start
        .set_label(localizer.exam_start_button());
    dialog.choice_exam_size_mode.clear();
    dialog
        .choice_exam_size_mode
        .append(localizer.exam_size_question_count_choice());
    dialog
        .choice_exam_size_mode
        .append(localizer.exam_size_percentage_choice());
    dialog.choice_exam_size_mode.set_selection(0);
    dialog
        .text_exam_size_value
        .set_value(&usize::min(20, active_card_count).max(1).to_string());
    dialog.choice_exam_size_mode.on_selection_changed({
        let value = dialog.text_exam_size_value;
        move |_| {
            value.set_value("");
        }
    });

    let modal_dialog = dialog.dialog_exam_setup;
    dialog
        .button_exam_setup_cancel
        .on_click(move |_| modal_dialog.end_modal(ID_CANCEL));

    let modal_dialog = dialog.dialog_exam_setup;
    dialog
        .button_exam_setup_start
        .on_click(move |_| modal_dialog.end_modal(ID_OK));

    dialog.text_exam_size_value.on_key_down({
        let modal_dialog = dialog.dialog_exam_setup;
        move |event| {
            if let WindowEventData::Keyboard(key_event) = event {
                if is_dialog_accept_key(key_event.get_unicode_key(), key_event.get_key_code()) {
                    modal_dialog.end_modal(ID_OK);
                    key_event.event.skip(false);
                } else {
                    key_event.event.skip(true);
                }
            }
        }
    });

    dialog.dialog_exam_setup.set_affirmative_id(start_id);
    dialog.dialog_exam_setup.set_escape_id(cancel_id);
    dialog.button_exam_setup_start.set_default();
    dialog.text_exam_size_value.set_focus();
    apply_exam_setup_visual_style(
        dialog.label_exam_setup_status,
        dialog.choice_exam_size_mode,
        dialog.text_exam_size_value,
        dialog.checkbox_exam_exact,
        dialog.button_exam_setup_cancel,
        dialog.button_exam_setup_start,
    );
    dialog.panel_exam_setup_root.layout();

    let outcome = loop {
        let result = dialog.dialog_exam_setup.show_modal();
        if !is_affirmative_dialog_result(result, start_id) {
            break None;
        }

        let mode = exam_size_mode_from_selection(dialog.choice_exam_size_mode.get_selection());
        let value_text = dialog.text_exam_size_value.get_value();
        let Some(value) = value_text
            .trim()
            .parse::<usize>()
            .ok()
            .filter(|value| *value > 0)
        else {
            dialog
                .label_exam_setup_status
                .set_label(localizer.exam_invalid_size_status());
            dialog.text_exam_size_value.set_focus();
            continue;
        };
        if mode == ExamSizeMode::Percentage && value > 100 {
            dialog
                .label_exam_setup_status
                .set_label(localizer.exam_invalid_percentage_status());
            dialog.text_exam_size_value.set_focus();
            continue;
        }

        break Some(ExamSizeRequest::new_with_exact_only(
            mode,
            value,
            dialog.checkbox_exam_exact.is_checked(),
        ));
    };
    dialog.dialog_exam_setup.destroy();
    outcome
}

pub(crate) fn show_exam_question_dialog(
    parent: Frame,
    localizer: Localizer,
    question: &ExamQuestion,
    current_question: usize,
    total_questions: usize,
    review_audio_policy: ReviewAudioPolicy,
    prompt_audio: Option<ReviewAudioFile>,
) -> ExamQuestionOutcome {
    let dialog = ExamQuestionUi::new(Some(&parent), false);
    let submit_id = ExamQuestionUi::xrc_id("button_exam_submit");
    let end_id = ExamQuestionUi::xrc_id("button_exam_end");
    let prompt_has_audio = prompt_audio.is_some() && review_audio_policy != ReviewAudioPolicy::Off;

    dialog
        .dialog_exam_question
        .set_label(localizer.exam_mode_dialog_title());
    let question_status =
        localizer.exam_question_status(current_question, total_questions, question.prompt());
    dialog
        .label_exam_question_status
        .set_label(&question_status);
    dialog
        .label_exam_prompt_heading
        .set_label(if prompt_has_audio {
            localizer.prompt_heading_with_audio()
        } else {
            localizer.prompt_heading()
        });
    dialog
        .button_exam_end
        .set_label(localizer.end_exam_button());
    dialog
        .button_exam_submit
        .set_label(localizer.submit_exam_answer_button());
    dialog.text_exam_prompt.set_value(question.prompt());
    dialog.text_exam_prompt.set_insertion_point(0);
    dialog.text_exam_answer.set_value("");
    dialog.text_exam_answer.set_name(&question_status);
    let audio_controls = configure_exam_audio_button(
        dialog.button_exam_question_audio,
        dialog.label_exam_question_status,
        localizer,
        review_audio_policy,
        prompt_audio,
    );

    let modal_dialog = dialog.dialog_exam_question;
    dialog
        .button_exam_end
        .on_click(move |_| modal_dialog.end_modal(ID_CANCEL));

    let modal_dialog = dialog.dialog_exam_question;
    dialog
        .button_exam_submit
        .on_click(move |_| modal_dialog.end_modal(ID_OK));

    dialog.text_exam_answer.on_key_down({
        let modal_dialog = dialog.dialog_exam_question;
        let audio_controls = audio_controls.clone();
        move |event| {
            if let WindowEventData::Keyboard(key_event) = event {
                if handle_exam_audio_shortcut(
                    key_event.get_unicode_key(),
                    key_event.get_key_code(),
                    key_event.control_down(),
                    &audio_controls,
                    dialog.label_exam_question_status,
                    localizer,
                ) {
                    key_event.event.skip(false);
                } else if is_dialog_accept_key(
                    key_event.get_unicode_key(),
                    key_event.get_key_code(),
                ) {
                    modal_dialog.end_modal(ID_OK);
                    key_event.event.skip(false);
                } else {
                    key_event.event.skip(true);
                }
            }
        }
    });
    if let Some(audio_controls) = audio_controls.clone() {
        bind_exam_audio_shortcut(
            dialog.text_exam_prompt,
            audio_controls.clone(),
            dialog.label_exam_question_status,
            localizer,
        );
        bind_exam_audio_shortcut(
            dialog.button_exam_question_audio,
            audio_controls.clone(),
            dialog.label_exam_question_status,
            localizer,
        );
        bind_exam_audio_shortcut(
            dialog.button_exam_end,
            audio_controls.clone(),
            dialog.label_exam_question_status,
            localizer,
        );
        bind_exam_audio_shortcut(
            dialog.button_exam_submit,
            audio_controls,
            dialog.label_exam_question_status,
            localizer,
        );
    }

    dialog.dialog_exam_question.set_affirmative_id(submit_id);
    dialog.dialog_exam_question.set_escape_id(end_id);
    dialog.button_exam_submit.set_default();
    dialog.text_exam_answer.set_focus();
    apply_exam_question_visual_style(
        dialog.label_exam_question_status,
        dialog.text_exam_prompt,
        dialog.text_exam_answer,
        dialog.button_exam_question_audio,
        dialog.button_exam_end,
        dialog.button_exam_submit,
    );
    dialog.panel_exam_question_root.layout();

    let result = dialog.dialog_exam_question.show_modal();
    let outcome = exam_question_outcome_from_modal_result(
        result,
        submit_id,
        question,
        dialog.text_exam_answer.get_value(),
    );
    stop_exam_audio(&audio_controls);
    dialog.dialog_exam_question.destroy();
    outcome
}

fn exam_question_outcome_from_modal_result(
    result: i32,
    submit_id: i32,
    question: &ExamQuestion,
    entered_response: String,
) -> ExamQuestionOutcome {
    if result == ID_OK || result == submit_id {
        ExamQuestionOutcome::Submitted(ExamAnswer::new(question.clone(), entered_response))
    } else if !entered_response.trim().is_empty() {
        ExamQuestionOutcome::SubmittedAndEnded(ExamAnswer::new(question.clone(), entered_response))
    } else {
        ExamQuestionOutcome::Ended
    }
}

pub(crate) fn confirm_exam_answer_correct_dialog(
    parent: Frame,
    localizer: Localizer,
    answer: &ExamAnswer,
) -> bool {
    let dialog = ExamGradeUi::new(Some(&parent), false);
    let yes_id = ExamGradeUi::xrc_id("button_exam_grade_yes");
    let no_id = ExamGradeUi::xrc_id("button_exam_grade_no");
    let entered_response = if answer.entered_response().trim().is_empty() {
        localizer.no_answer_entered().to_string()
    } else {
        answer.entered_response().to_string()
    };

    dialog
        .dialog_exam_grade
        .set_label(localizer.exam_grade_answer_caption());
    dialog
        .label_exam_grade_status
        .set_label(localizer.exam_grade_answer_instruction());
    dialog
        .label_exam_grade_prompt_heading
        .set_label(localizer.prompt_heading());
    dialog
        .label_exam_grade_answer_heading
        .set_label(localizer.exam_answer_heading());
    dialog
        .label_exam_grade_expected_heading
        .set_label(localizer.exam_expected_response_heading());
    dialog
        .button_exam_grade_no
        .set_label(localizer.no_button_with_shortcut());
    dialog
        .button_exam_grade_yes
        .set_label(localizer.yes_button_with_shortcut());
    dialog
        .text_exam_grade_prompt
        .set_value(answer.question().prompt());
    dialog.text_exam_grade_prompt.set_insertion_point(0);
    dialog.text_exam_grade_answer.set_value(&entered_response);
    dialog.text_exam_grade_answer.set_insertion_point(0);
    dialog
        .text_exam_grade_expected
        .set_value(answer.question().expected_response());
    dialog.text_exam_grade_expected.set_insertion_point(0);

    let modal_dialog = dialog.dialog_exam_grade;
    dialog
        .button_exam_grade_no
        .on_click(move |_| modal_dialog.end_modal(ID_NO));

    let modal_dialog = dialog.dialog_exam_grade;
    dialog
        .button_exam_grade_yes
        .on_click(move |_| modal_dialog.end_modal(ID_YES));
    bind_exam_grade_shortcuts(dialog.text_exam_grade_prompt, dialog.dialog_exam_grade);
    bind_exam_grade_shortcuts(dialog.text_exam_grade_answer, dialog.dialog_exam_grade);
    bind_exam_grade_shortcuts(dialog.text_exam_grade_expected, dialog.dialog_exam_grade);
    bind_exam_grade_shortcuts(dialog.button_exam_grade_no, dialog.dialog_exam_grade);
    bind_exam_grade_shortcuts(dialog.button_exam_grade_yes, dialog.dialog_exam_grade);

    dialog.dialog_exam_grade.set_affirmative_id(yes_id);
    dialog.dialog_exam_grade.set_escape_id(no_id);
    dialog.button_exam_grade_yes.set_default();
    dialog.text_exam_grade_prompt.set_focus();
    apply_exam_grade_visual_style(
        dialog.label_exam_grade_status,
        dialog.text_exam_grade_prompt,
        dialog.text_exam_grade_answer,
        dialog.text_exam_grade_expected,
        dialog.button_exam_grade_no,
        dialog.button_exam_grade_yes,
    );
    dialog.panel_exam_grade_root.layout();

    let result = dialog.dialog_exam_grade.show_modal();
    let accepted = result == ID_YES || result == yes_id;
    dialog.dialog_exam_grade.destroy();
    accepted
}

pub(crate) fn show_exam_inexact_answer_review_dialog(
    parent: Frame,
    localizer: Localizer,
    answer: &ExamAnswer,
) -> ExamInexactReviewOutcome {
    let dialog = ExamGradeUi::new(Some(&parent), false);
    let next_id = ExamGradeUi::xrc_id("button_exam_grade_yes");
    let summary_id = ExamGradeUi::xrc_id("button_exam_grade_no");
    let entered_response = if answer.entered_response().trim().is_empty() {
        localizer.no_answer_entered().to_string()
    } else {
        answer.entered_response().to_string()
    };

    dialog
        .dialog_exam_grade
        .set_label(localizer.exam_review_inexact_caption());
    dialog
        .label_exam_grade_status
        .set_label(localizer.exam_review_inexact_instruction());
    dialog
        .label_exam_grade_prompt_heading
        .set_label(localizer.prompt_heading());
    dialog
        .label_exam_grade_answer_heading
        .set_label(localizer.exam_answer_heading());
    dialog
        .label_exam_grade_expected_heading
        .set_label(localizer.exam_expected_response_heading());
    dialog
        .button_exam_grade_no
        .set_label(localizer.exam_summary_button());
    dialog
        .button_exam_grade_yes
        .set_label(localizer.exam_next_button());
    dialog
        .text_exam_grade_prompt
        .set_value(answer.question().prompt());
    dialog.text_exam_grade_prompt.set_insertion_point(0);
    dialog.text_exam_grade_answer.set_value(&entered_response);
    dialog.text_exam_grade_answer.set_insertion_point(0);
    dialog
        .text_exam_grade_expected
        .set_value(answer.question().expected_response());
    dialog.text_exam_grade_expected.set_insertion_point(0);

    let modal_dialog = dialog.dialog_exam_grade;
    dialog
        .button_exam_grade_no
        .on_click(move |_| modal_dialog.end_modal(ID_CANCEL));

    let modal_dialog = dialog.dialog_exam_grade;
    dialog
        .button_exam_grade_yes
        .on_click(move |_| modal_dialog.end_modal(ID_OK));

    dialog.dialog_exam_grade.set_affirmative_id(next_id);
    dialog.dialog_exam_grade.set_escape_id(summary_id);
    dialog.button_exam_grade_yes.set_default();
    dialog.text_exam_grade_prompt.set_focus();
    apply_exam_grade_visual_style(
        dialog.label_exam_grade_status,
        dialog.text_exam_grade_prompt,
        dialog.text_exam_grade_answer,
        dialog.text_exam_grade_expected,
        dialog.button_exam_grade_no,
        dialog.button_exam_grade_yes,
    );
    dialog.panel_exam_grade_root.layout();

    let result = dialog.dialog_exam_grade.show_modal();
    let outcome = exam_inexact_review_outcome_from_modal_result(result, next_id);
    dialog.dialog_exam_grade.destroy();
    outcome
}

fn exam_inexact_review_outcome_from_modal_result(
    result: i32,
    next_id: i32,
) -> ExamInexactReviewOutcome {
    if result == ID_OK || result == next_id {
        ExamInexactReviewOutcome::Next
    } else {
        ExamInexactReviewOutcome::Summary
    }
}

type ExamAudioControls = (Rc<RefCell<ReviewAudioPlayer>>, ReviewAudioFile);

fn configure_exam_audio_button(
    button: Button,
    status: StaticText,
    localizer: Localizer,
    policy: ReviewAudioPolicy,
    audio_file: Option<ReviewAudioFile>,
) -> Option<ExamAudioControls> {
    button.set_label(localizer.play_stop_review_audio_button_label());
    let Some(audio_file) = audio_file.filter(|_| policy != ReviewAudioPolicy::Off) else {
        button.show(false);
        button.enable(false);
        return None;
    };

    button.show(true);
    button.enable(true);
    let player = Rc::new(RefCell::new(ReviewAudioPlayer::default()));
    if policy == ReviewAudioPolicy::Automatic {
        play_exam_audio_file(&player, status, localizer, &audio_file);
    }

    let controls = (player, audio_file);
    let button_controls = controls.clone();
    button.on_click(move |_| {
        toggle_exam_audio_file(&button_controls, status, localizer);
    });
    Some(controls)
}

fn play_exam_audio_file(
    player: &Rc<RefCell<ReviewAudioPlayer>>,
    status: StaticText,
    localizer: Localizer,
    audio_file: &ReviewAudioFile,
) {
    if let Err(path) = player.borrow_mut().play_audio_file(audio_file.clone()) {
        status.set_label(&localizer.could_not_play_review_audio(&path.display().to_string()));
    }
}

fn toggle_exam_audio_file(controls: &ExamAudioControls, status: StaticText, localizer: Localizer) {
    let (player, audio_file) = controls;
    if let Err(path) = player.borrow_mut().toggle_audio_file(audio_file.clone()) {
        status.set_label(&localizer.could_not_play_review_audio(&path.display().to_string()));
    }
}

fn stop_exam_audio(controls: &Option<ExamAudioControls>) {
    if let Some((player, _)) = controls {
        player.borrow_mut().stop();
    }
}

fn handle_exam_audio_shortcut(
    unicode_key: Option<i32>,
    key_code: Option<i32>,
    control_down: bool,
    controls: &Option<ExamAudioControls>,
    status: StaticText,
    localizer: Localizer,
) -> bool {
    if !is_toggle_review_audio_key(unicode_key, key_code, control_down) {
        return false;
    }

    if let Some(controls) = controls {
        toggle_exam_audio_file(controls, status, localizer);
    }
    true
}

fn bind_exam_audio_shortcut<W>(
    widget: W,
    controls: ExamAudioControls,
    status: StaticText,
    localizer: Localizer,
) where
    W: WxWidget + WindowEvents + Copy + 'static,
{
    widget.on_key_down(move |event| {
        if let WindowEventData::Keyboard(key_event) = event {
            if handle_exam_audio_shortcut(
                key_event.get_unicode_key(),
                key_event.get_key_code(),
                key_event.control_down(),
                &Some(controls.clone()),
                status,
                localizer,
            ) {
                key_event.event.skip(false);
            } else {
                key_event.event.skip(true);
            }
        }
    });
}

fn bind_exam_grade_shortcuts<W>(widget: W, modal_dialog: Dialog)
where
    W: WxWidget + WindowEvents + Copy + 'static,
{
    widget.on_key_down(move |event| {
        if let WindowEventData::Keyboard(key_event) = event {
            match exam_grade_shortcut_result(
                key_event.get_unicode_key(),
                key_event.get_key_code(),
                key_event.control_down(),
            ) {
                Some(true) => {
                    modal_dialog.end_modal(ID_YES);
                    key_event.event.skip(false);
                }
                Some(false) => {
                    modal_dialog.end_modal(ID_NO);
                    key_event.event.skip(false);
                }
                None => key_event.event.skip(true),
            }
        }
    });
}

fn exam_grade_shortcut_result(
    unicode_key: Option<i32>,
    key_code: Option<i32>,
    control_down: bool,
) -> Option<bool> {
    if !control_down {
        return None;
    }
    let character = unicode_key
        .and_then(|key| u32::try_from(key).ok())
        .and_then(char::from_u32);
    if matches!(character, Some('y' | 'Y')) || matches!(key_code, Some(89) | Some(25)) {
        return Some(true);
    }
    if matches!(character, Some('n' | 'N')) || matches!(key_code, Some(78) | Some(14)) {
        return Some(false);
    }
    None
}

pub(crate) fn show_exam_summary_dialog(
    parent: Frame,
    localizer: Localizer,
    summary: &str,
    deck_path: &Path,
    review_db_path: &Path,
    started_at_unix_seconds: i64,
    finished_at_unix_seconds: i64,
) {
    let dialog = ExamSummaryUi::new(Some(&parent), false);
    let ok_id = ExamSummaryUi::xrc_id("button_exam_summary_ok");

    dialog
        .dialog_exam_summary
        .set_label(localizer.exam_summary_caption());
    dialog
        .label_exam_summary_status
        .set_label(localizer.exam_summary_caption());
    dialog
        .button_exam_summary_ok
        .set_label(localizer.options_ok_button());
    dialog
        .button_exam_summary_save_report
        .set_label(localizer.save_report_button());
    dialog.text_exam_summary.set_value(summary);
    dialog.text_exam_summary.set_insertion_point(0);
    dialog.button_exam_summary_save_report.on_click({
        let status = dialog.label_exam_summary_status;
        let deck_path = deck_path.to_path_buf();
        let review_db_path = review_db_path.to_path_buf();
        let summary = summary.to_string();
        move |_| {
            let report = exam_report_file(
                localizer,
                &deck_path,
                &review_db_path,
                started_at_unix_seconds,
                finished_at_unix_seconds,
                &summary,
            );
            match save_review_report(&report) {
                Ok(path) => {
                    status.set_label(
                        &localizer.saved_exam_report_status(&path.display().to_string()),
                    );
                }
                Err(error) => {
                    status.set_label(&localizer.could_not_save_exam_report_status(
                        &report.path().display().to_string(),
                        &error.to_string(),
                    ));
                }
            }
        }
    });

    let modal_dialog = dialog.dialog_exam_summary;
    dialog
        .button_exam_summary_ok
        .on_click(move |_| modal_dialog.end_modal(ID_OK));

    dialog.dialog_exam_summary.set_affirmative_id(ok_id);
    dialog.dialog_exam_summary.set_escape_id(ok_id);
    dialog.button_exam_summary_ok.set_default();
    dialog.text_exam_summary.set_focus();
    apply_exam_summary_visual_style(
        dialog.label_exam_summary_status,
        dialog.text_exam_summary,
        dialog.button_exam_summary_save_report,
        dialog.button_exam_summary_ok,
    );
    dialog.panel_exam_summary_root.layout();

    let _ = dialog.dialog_exam_summary.show_modal();
    dialog.dialog_exam_summary.destroy();
}

fn exam_size_mode_from_selection(selection: Option<u32>) -> ExamSizeMode {
    match selection {
        Some(1) => ExamSizeMode::Percentage,
        _ => ExamSizeMode::QuestionCount,
    }
}

fn apply_exam_setup_visual_style(
    status: StaticText,
    size_mode: Choice,
    size_value: TextCtrl,
    exact_checkbox: CheckBox,
    cancel_button: Button,
    start_button: Button,
) {
    increase_font_to_at_least(status, 11);
    increase_font_to_at_least(size_mode, 11);
    increase_font_to_at_least(size_value, 11);
    increase_font_to_at_least(exact_checkbox, 11);
    for button in [cancel_button, start_button] {
        increase_font_to_at_least(button, 11);
        button.set_min_size(Size::new(140, 40));
    }
}

fn apply_exam_question_visual_style(
    status: StaticText,
    prompt: TextCtrl,
    answer: TextCtrl,
    audio_button: Button,
    end_button: Button,
    submit_button: Button,
) {
    increase_font_to_at_least(status, 11);
    increase_font_to_at_least(prompt, 12);
    increase_font_to_at_least(answer, 12);
    for button in [audio_button, end_button, submit_button] {
        increase_font_to_at_least(button, 11);
        button.set_min_size(Size::new(140, 40));
    }
}

fn apply_exam_grade_visual_style(
    status: StaticText,
    prompt: TextCtrl,
    answer: TextCtrl,
    expected: TextCtrl,
    no_button: Button,
    yes_button: Button,
) {
    increase_font_to_at_least(status, 11);
    for text in [prompt, answer, expected] {
        increase_font_to_at_least(text, 12);
    }
    for button in [no_button, yes_button] {
        increase_font_to_at_least(button, 11);
        button.set_min_size(Size::new(120, 40));
    }
}

fn apply_exam_summary_visual_style(
    status: StaticText,
    summary: TextCtrl,
    save_button: Button,
    ok_button: Button,
) {
    increase_font_to_at_least(status, 11);
    increase_font_to_at_least(summary, 12);
    for button in [save_button, ok_button] {
        increase_font_to_at_least(button, 11);
        button.set_min_size(Size::new(120, 40));
    }
}

fn increase_font_to_at_least<W>(widget: W, minimum_point_size: i32)
where
    W: WxWidget,
{
    let Some(mut font) = widget.get_font() else {
        return;
    };
    let point_size = font.get_point_size();
    if point_size > 0 && point_size < minimum_point_size {
        font.set_point_size(minimum_point_size);
        widget.set_font(&font);
    }
}

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

    #[test]
    fn exam_grade_shortcuts_accept_control_y_and_control_n() {
        assert_eq!(
            exam_grade_shortcut_result(Some('y' as i32), Some(89), true),
            Some(true)
        );
        assert_eq!(
            exam_grade_shortcut_result(Some('N' as i32), Some(78), true),
            Some(false)
        );
        assert_eq!(exam_grade_shortcut_result(None, Some(25), true), Some(true));
        assert_eq!(
            exam_grade_shortcut_result(None, Some(14), true),
            Some(false)
        );
    }

    #[test]
    fn exam_grade_shortcuts_require_control() {
        assert_eq!(
            exam_grade_shortcut_result(Some('y' as i32), Some(89), false),
            None
        );
        assert_eq!(
            exam_grade_shortcut_result(Some('n' as i32), Some(78), false),
            None
        );
    }

    #[test]
    fn ending_exam_question_with_text_submits_answer_and_ends() {
        let question = ExamQuestion::new(DatumKey::for_card_id(1), "Prompt", "Response");

        let outcome = exam_question_outcome_from_modal_result(
            ID_CANCEL,
            50_000,
            &question,
            "typed".to_string(),
        );

        assert!(matches!(
            outcome,
            ExamQuestionOutcome::SubmittedAndEnded(answer)
                if answer.entered_response() == "typed"
        ));
    }

    #[test]
    fn ending_exam_question_without_text_ends_without_answer() {
        let question = ExamQuestion::new(DatumKey::for_card_id(1), "Prompt", "Response");

        let outcome =
            exam_question_outcome_from_modal_result(ID_CANCEL, 50_000, &question, "  ".to_string());

        assert_eq!(outcome, ExamQuestionOutcome::Ended);
    }

    #[test]
    fn inexact_exam_review_modal_result_maps_next_and_summary() {
        let next_id = 50_000;

        assert_eq!(
            exam_inexact_review_outcome_from_modal_result(ID_OK, next_id),
            ExamInexactReviewOutcome::Next
        );
        assert_eq!(
            exam_inexact_review_outcome_from_modal_result(next_id, next_id),
            ExamInexactReviewOutcome::Next
        );
        assert_eq!(
            exam_inexact_review_outcome_from_modal_result(ID_CANCEL, next_id),
            ExamInexactReviewOutcome::Summary
        );
    }
}
