use cosimo::DatumKey;

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) enum ExamSizeMode {
    QuestionCount,
    Percentage,
}

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) struct ExamSizeRequest {
    mode: ExamSizeMode,
    value: usize,
    exact_only: bool,
}

impl ExamSizeRequest {
    #[cfg(test)]
    pub(crate) fn new(mode: ExamSizeMode, value: usize) -> Self {
        Self::new_with_exact_only(mode, value, false)
    }

    pub(crate) fn new_with_exact_only(mode: ExamSizeMode, value: usize, exact_only: bool) -> Self {
        Self {
            mode,
            value,
            exact_only,
        }
    }

    pub(crate) fn mode(self) -> ExamSizeMode {
        self.mode
    }

    pub(crate) fn value(self) -> usize {
        self.value
    }

    pub(crate) fn exact_only(self) -> bool {
        self.exact_only
    }
}

pub(crate) fn resolve_exam_question_count(
    request: ExamSizeRequest,
    active_card_count: usize,
) -> usize {
    if active_card_count == 0 {
        return 0;
    }

    match request.mode() {
        ExamSizeMode::QuestionCount => request.value().clamp(1, active_card_count),
        ExamSizeMode::Percentage => {
            let percentage = request.value().clamp(1, 100);
            (active_card_count * percentage).div_ceil(100)
        }
    }
}

#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) struct ExamPass {
    started_at_unix_seconds: i64,
    questions: Vec<ExamQuestion>,
}

impl ExamPass {
    pub(crate) fn new(started_at_unix_seconds: i64, questions: Vec<ExamQuestion>) -> Self {
        Self {
            started_at_unix_seconds,
            questions,
        }
    }

    pub(crate) fn started_at_unix_seconds(&self) -> i64 {
        self.started_at_unix_seconds
    }

    pub(crate) fn questions(&self) -> &[ExamQuestion] {
        &self.questions
    }
}

#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) struct ExamQuestion {
    datum_key: DatumKey,
    prompt: String,
    expected_response: String,
}

impl ExamQuestion {
    pub(crate) fn new(
        datum_key: DatumKey,
        prompt: impl Into<String>,
        expected_response: impl Into<String>,
    ) -> Self {
        Self {
            datum_key,
            prompt: prompt.into(),
            expected_response: expected_response.into(),
        }
    }

    pub(crate) fn datum_key(&self) -> &DatumKey {
        &self.datum_key
    }

    pub(crate) fn prompt(&self) -> &str {
        &self.prompt
    }

    pub(crate) fn expected_response(&self) -> &str {
        &self.expected_response
    }
}

#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) struct ExamAnswer {
    question: ExamQuestion,
    entered_response: String,
}

impl ExamAnswer {
    pub(crate) fn new(question: ExamQuestion, entered_response: impl Into<String>) -> Self {
        Self {
            question,
            entered_response: entered_response.into(),
        }
    }

    pub(crate) fn question(&self) -> &ExamQuestion {
        &self.question
    }

    pub(crate) fn entered_response(&self) -> &str {
        &self.entered_response
    }

    pub(crate) fn is_exact_match(&self) -> bool {
        exam_answer_is_exact_match(&self.entered_response, self.question.expected_response())
    }
}

#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) struct GradedExamAnswer {
    answer: ExamAnswer,
    is_correct: bool,
}

impl GradedExamAnswer {
    pub(crate) fn new(answer: ExamAnswer, is_correct: bool) -> Self {
        Self { answer, is_correct }
    }

    pub(crate) fn answer(&self) -> &ExamAnswer {
        &self.answer
    }

    pub(crate) fn is_correct(&self) -> bool {
        self.is_correct
    }
}

#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) enum ManualExamGrading {
    Exact(GradedExamAnswer),
    NeedsDecision(ExamAnswer),
}

pub(crate) fn classify_exam_answer_for_manual_grading(answer: ExamAnswer) -> ManualExamGrading {
    if answer.is_exact_match() {
        ManualExamGrading::Exact(GradedExamAnswer::new(answer, true))
    } else {
        ManualExamGrading::NeedsDecision(answer)
    }
}

pub(crate) struct ExactExamReviewPlan {
    graded_answers: Vec<GradedExamAnswer>,
    inexact_answers: Vec<ExamAnswer>,
}

impl ExactExamReviewPlan {
    pub(crate) fn graded_answers(self) -> Vec<GradedExamAnswer> {
        self.graded_answers
    }

    #[cfg(test)]
    pub(crate) fn graded_answers_slice(&self) -> &[GradedExamAnswer] {
        &self.graded_answers
    }

    pub(crate) fn inexact_answers(&self) -> &[ExamAnswer] {
        &self.inexact_answers
    }
}

pub(crate) fn grade_exam_answers_exact_only(answers: Vec<ExamAnswer>) -> ExactExamReviewPlan {
    let mut graded_answers = Vec::with_capacity(answers.len());
    let mut inexact_answers = Vec::new();
    for answer in answers {
        let is_exact = answer.is_exact_match();
        if !is_exact {
            inexact_answers.push(answer.clone());
        }
        graded_answers.push(GradedExamAnswer::new(answer, is_exact));
    }
    ExactExamReviewPlan {
        graded_answers,
        inexact_answers,
    }
}

#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) struct ExamSummary {
    total_questions: usize,
    submitted_answers: usize,
    exact_matches: usize,
    accepted_non_exact: usize,
    incorrect_answers: usize,
    elapsed_seconds: u64,
    ended_early: bool,
}

impl ExamSummary {
    pub(crate) fn from_answers(
        total_questions: usize,
        answers: &[GradedExamAnswer],
        elapsed_seconds: u64,
    ) -> Self {
        let exact_matches = answers
            .iter()
            .filter(|answer| answer.answer().is_exact_match())
            .count();
        let correct_answers = answers.iter().filter(|answer| answer.is_correct()).count();
        let accepted_non_exact = correct_answers.saturating_sub(exact_matches);
        let submitted_answers = answers.len();
        let incorrect_answers = submitted_answers.saturating_sub(correct_answers);

        Self {
            total_questions,
            submitted_answers,
            exact_matches,
            accepted_non_exact,
            incorrect_answers,
            elapsed_seconds,
            ended_early: submitted_answers < total_questions,
        }
    }

    pub(crate) fn total_questions(&self) -> usize {
        self.total_questions
    }

    pub(crate) fn submitted_answers(&self) -> usize {
        self.submitted_answers
    }

    pub(crate) fn exact_matches(&self) -> usize {
        self.exact_matches
    }

    pub(crate) fn accepted_non_exact(&self) -> usize {
        self.accepted_non_exact
    }

    pub(crate) fn correct_answers(&self) -> usize {
        self.exact_matches + self.accepted_non_exact
    }

    pub(crate) fn correct_percentage(&self) -> usize {
        if self.submitted_answers == 0 {
            return 0;
        }
        (self.correct_answers() * 100) / self.submitted_answers
    }

    pub(crate) fn incorrect_answers(&self) -> usize {
        self.incorrect_answers
    }

    pub(crate) fn elapsed_seconds(&self) -> u64 {
        self.elapsed_seconds
    }

    pub(crate) fn seconds_per_submitted_answer(&self) -> u64 {
        if self.submitted_answers == 0 {
            return 0;
        }
        self.elapsed_seconds / self.submitted_answers as u64
    }

    pub(crate) fn ended_early(&self) -> bool {
        self.ended_early
    }
}

pub(crate) fn exam_answer_is_exact_match(entered: &str, expected: &str) -> bool {
    normalize_exam_answer(entered) == normalize_exam_answer(expected)
}

fn normalize_exam_answer(value: &str) -> String {
    value.trim().replace("\r\n", "\n").replace('\r', "\n")
}

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

    #[test]
    fn exact_exam_answer_matching_ignores_surrounding_space_and_line_ending_style() {
        assert!(exam_answer_is_exact_match(" Madrid \r\n", "Madrid\n"));
    }

    #[test]
    fn exact_exam_answer_matching_keeps_case_and_accents_significant() {
        assert!(!exam_answer_is_exact_match("madrid", "Madrid"));
        assert!(!exam_answer_is_exact_match("si", "sí"));
    }

    #[test]
    fn exam_question_count_clamps_requested_question_count_to_active_cards() {
        assert_eq!(
            resolve_exam_question_count(ExamSizeRequest::new(ExamSizeMode::QuestionCount, 10), 4,),
            4
        );
        assert_eq!(
            resolve_exam_question_count(ExamSizeRequest::new(ExamSizeMode::QuestionCount, 0), 4),
            1
        );
    }

    #[test]
    fn exam_question_count_uses_ceiling_for_percentages() {
        assert_eq!(
            resolve_exam_question_count(ExamSizeRequest::new(ExamSizeMode::Percentage, 10), 21),
            3
        );
        assert_eq!(
            resolve_exam_question_count(ExamSizeRequest::new(ExamSizeMode::Percentage, 100), 21),
            21
        );
        assert_eq!(
            resolve_exam_question_count(ExamSizeRequest::new(ExamSizeMode::Percentage, 0), 21),
            1
        );
    }

    #[test]
    fn exam_size_request_preserves_exact_only_choice() {
        let request = ExamSizeRequest::new_with_exact_only(ExamSizeMode::Percentage, 50, true);

        assert_eq!(request.mode(), ExamSizeMode::Percentage);
        assert_eq!(request.value(), 50);
        assert!(request.exact_only());
    }

    #[test]
    fn graded_exam_answers_store_correctness_without_scheduler_feedback() {
        let question = ExamQuestion::new(DatumKey::for_card_id(1), "Prompt", "Response");
        let answer = ExamAnswer::new(question.clone(), "Response");

        assert!(GradedExamAnswer::new(answer.clone(), true).is_correct());
        assert!(!GradedExamAnswer::new(answer, false).is_correct());
    }

    #[test]
    fn manual_exam_grading_auto_accepts_exact_answers_and_defers_inexact_answers() {
        let exact_question = ExamQuestion::new(DatumKey::for_card_id(1), "Prompt 1", "Madrid");
        let inexact_question = ExamQuestion::new(DatumKey::for_card_id(2), "Prompt 2", "Paris");

        let exact =
            classify_exam_answer_for_manual_grading(ExamAnswer::new(exact_question, " Madrid "));
        let inexact =
            classify_exam_answer_for_manual_grading(ExamAnswer::new(inexact_question, "París"));

        assert!(matches!(
            exact,
            ManualExamGrading::Exact(answer) if answer.is_correct() && answer.answer().is_exact_match()
        ));
        assert!(matches!(
            inexact,
            ManualExamGrading::NeedsDecision(answer)
                if answer.entered_response() == "París" && !answer.is_exact_match()
        ));
    }

    #[test]
    fn exact_only_exam_grading_marks_inexact_answers_incorrect_and_reviews_them() {
        let exact_question = ExamQuestion::new(DatumKey::for_card_id(1), "Prompt 1", "Madrid");
        let inexact_question = ExamQuestion::new(DatumKey::for_card_id(2), "Prompt 2", "Paris");

        let plan = grade_exam_answers_exact_only(vec![
            ExamAnswer::new(exact_question, "Madrid"),
            ExamAnswer::new(inexact_question, "París"),
        ]);

        assert_eq!(plan.graded_answers_slice().len(), 2);
        assert!(plan.graded_answers_slice()[0].is_correct());
        assert!(!plan.graded_answers_slice()[1].is_correct());
        assert_eq!(plan.inexact_answers().len(), 1);
        assert_eq!(plan.inexact_answers()[0].entered_response(), "París");
        assert_eq!(
            plan.inexact_answers()[0].question().expected_response(),
            "Paris"
        );
    }

    #[test]
    fn exam_summary_counts_exact_accepted_and_incorrect_answers() {
        let exact_question = ExamQuestion::new(DatumKey::for_card_id(1), "Prompt 1", "Madrid");
        let accepted_question = ExamQuestion::new(DatumKey::for_card_id(2), "Prompt 2", "Paris");
        let incorrect_question = ExamQuestion::new(DatumKey::for_card_id(3), "Prompt 3", "Rome");
        let answers = vec![
            GradedExamAnswer::new(ExamAnswer::new(exact_question, " Madrid "), true),
            GradedExamAnswer::new(ExamAnswer::new(accepted_question, "París"), true),
            GradedExamAnswer::new(ExamAnswer::new(incorrect_question, "Milan"), false),
        ];

        let summary = ExamSummary::from_answers(4, &answers, 12);

        assert_eq!(summary.total_questions(), 4);
        assert_eq!(summary.submitted_answers(), 3);
        assert_eq!(summary.exact_matches(), 1);
        assert_eq!(summary.accepted_non_exact(), 1);
        assert_eq!(summary.correct_answers(), 2);
        assert_eq!(summary.correct_percentage(), 66);
        assert_eq!(summary.incorrect_answers(), 1);
        assert_eq!(summary.elapsed_seconds(), 12);
        assert_eq!(summary.seconds_per_submitted_answer(), 4);
        assert!(summary.ended_early());
    }
}
