"""Tests of the StudySession use case.""" import itertools from collections.abc import Callable, Sequence from flash_card.application.dto.card_dto import CardDto from flash_card.application.use_cases.study_session import StudySession from flash_card.domain.entities.card import Card from flash_card.domain.entities.deck import Deck from flash_card.domain.value_objects.language_pair import LanguagePair PAIR = LanguagePair("ms", "da") A = Card("aku", "mig") B = Card("kau", "du") def _first(cards: Sequence[Card]) -> Card: return cards[0] class _RecordingProgress: def __init__(self) -> None: self.saved: list[tuple[Card, ...]] = [] def load(self) -> Deck | None: return None def save(self, deck: Deck) -> None: self.saved.append(deck.cards) class _RecordingSpeech: def __init__(self) -> None: self.spoken: list[tuple[str, str]] = [] def speak(self, text: str, language_code: str) -> None: self.spoken.append((text, language_code)) def _session( *cards: Card, progress: _RecordingProgress | None = None, speech: _RecordingSpeech | None = None, ) -> StudySession: store = _RecordingProgress() if progress is None else progress voice = _RecordingSpeech() if speech is None else speech return StudySession(Deck(PAIR, cards), _first, store, voice) def test_current_card_is_none_before_first_draw() -> None: assert _session(A).current_card is None def test_next_card_returns_chosen_card_as_dto() -> None: assert _session(A, B).next_card() == CardDto("aku", "mig", "ms", "da") def test_next_card_returns_different_card_when_two_or_more() -> None: session = _session(A, B) first = session.next_card() second = session.next_card() assert first != second assert second == CardDto("kau", "du", "ms", "da") def test_next_card_returns_same_card_when_one() -> None: session = _session(A) assert session.next_card() == session.next_card() def test_next_card_returns_none_when_deck_is_empty() -> None: session = _session() assert session.next_card() is None assert session.current_card is None def test_current_card_follows_last_draw() -> None: session = _session(A, B) shown = session.next_card() assert session.current_card == shown def test_remaining_counts_cards_in_deck() -> None: session = _session(A, B) session.next_card() assert session.remaining == 2 def test_mark_known_removes_current_card_and_saves_remaining_once() -> None: progress = _RecordingProgress() session = _session(A, B, progress=progress) session.next_card() session.mark_known() assert session.remaining == 1 assert session.current_card is None assert progress.saved == [(B,)] def test_mark_known_saves_nothing_when_no_current_card() -> None: progress = _RecordingProgress() session = _session(A, progress=progress) session.mark_known() assert session.remaining == 1 assert progress.saved == [] def test_mark_known_on_last_card_leaves_session_finished() -> None: progress = _RecordingProgress() session = _session(A, progress=progress) session.next_card() session.mark_known() assert progress.saved == [()] assert session.next_card() is None def test_next_card_never_saves() -> None: progress = _RecordingProgress() session = _session(A, B, progress=progress) session.next_card() session.next_card() assert progress.saved == [] def test_next_card_speaks_front_text_once_in_front_language() -> None: speech = _RecordingSpeech() session = _session(A, B, speech=speech) session.next_card() assert speech.spoken == [("aku", "ms")] def test_next_card_does_not_speak_when_deck_is_empty() -> None: speech = _RecordingSpeech() session = _session(speech=speech) session.next_card() assert speech.spoken == [] def test_next_card_speaks_again_on_every_draw() -> None: speech = _RecordingSpeech() session = _session(A, B, speech=speech) session.next_card() session.next_card() assert speech.spoken == [("aku", "ms"), ("kau", "ms")] def test_mark_known_does_not_speak_but_following_draw_does() -> None: speech = _RecordingSpeech() session = _session(A, B, speech=speech) session.next_card() session.mark_known() assert speech.spoken == [("aku", "ms")] session.next_card() assert speech.spoken == [("aku", "ms"), ("kau", "ms")] C = Card("ini", "denne") D = Card("itu", "den") def _level_session( deck_cards: Sequence[Card], level_cards: Sequence[Card] | None, choose: Callable[[Sequence[Card]], Card] = _first, progress: _RecordingProgress | None = None, speech: _RecordingSpeech | None = None, ) -> StudySession: store = _RecordingProgress() if progress is None else progress voice = _RecordingSpeech() if speech is None else speech return StudySession(Deck(PAIR, deck_cards), choose, store, voice, level_cards) def _last(cards: Sequence[Card]) -> Card: return cards[-1] def test_next_card_draws_only_cards_of_the_level_over_many_draws() -> None: chooser = itertools.cycle([_first, _last]) session = _level_session( [A, B, C, D], [A, B], choose=lambda cards: next(chooser)(cards) ) drawn = {session.next_card() for _ in range(40)} assert drawn == { CardDto("aku", "mig", "ms", "da"), CardDto("kau", "du", "ms", "da"), } def test_next_card_never_draws_a_card_outside_the_level() -> None: session = _level_session([A, B, C], [B], choose=_last) for _ in range(5): assert session.next_card() == CardDto("kau", "du", "ms", "da") def test_remaining_counts_whole_deck_and_remaining_in_level_counts_level() -> None: session = _level_session([A, B, C], [A, B]) assert session.remaining == 3 assert session.remaining_in_level == 2 def test_remaining_in_level_counts_whole_deck_without_level() -> None: assert _level_session([A, B, C], None).remaining_in_level == 3 def test_remaining_in_level_ignores_level_cards_missing_from_deck() -> None: assert _level_session([B], [A, B]).remaining_in_level == 1 def test_mark_known_keeps_cards_outside_the_level_and_saves_whole_deck() -> None: progress = _RecordingProgress() session = _level_session([A, B, C], [A], progress=progress) session.next_card() session.mark_known() assert progress.saved == [(B, C)] assert session.remaining == 2 assert session.remaining_in_level == 0 def test_next_card_is_none_when_nothing_of_the_level_is_left() -> None: session = _level_session([B, C], [A]) assert session.next_card() is None assert session.current_card is None def test_session_finishes_after_last_card_of_level_is_known() -> None: session = _level_session([A, B], [A]) session.next_card() session.mark_known() assert session.next_card() is None assert session.remaining == 1 def test_next_card_speaks_only_cards_of_the_level() -> None: speech = _RecordingSpeech() session = _level_session([A, B, C], [B], choose=_last, speech=speech) session.next_card() session.next_card() assert speech.spoken == [("kau", "ms"), ("kau", "ms")] def test_next_card_speaks_nothing_when_level_is_finished() -> None: speech = _RecordingSpeech() session = _level_session([B], [A], speech=speech) session.next_card() assert speech.spoken == []