from datetime import date
from decimal import Decimal

import pytest

from . import (
    Card,
    CardState,
    CardText,
    Deck,
    DeckName,
    DefaultSchedulingPolicy,
    InvalidCardError,
    InvalidDeckError,
    InvalidReviewError,
    InvalidTagError,
    ReviewRating,
    ReviewService,
    Tag,
)


def test_anki_value_objects_validate_and_normalize() -> None:
    assert DeckName("  Japanese  ").value == "Japanese"
    assert CardText("  Front  ").value == "Front"
    assert Tag("  N5  ").value == "n5"

    with pytest.raises(InvalidDeckError):
        DeckName("")

    with pytest.raises(InvalidCardError):
        CardText(" ")

    with pytest.raises(InvalidTagError):
        Tag("")


def test_deck_manages_cards_and_due_filters() -> None:
    deck = Deck(name=DeckName("Japanese"))
    due_card = Card(front=CardText("猫"), back=CardText("cat"), due_at=date(2026, 4, 3))
    future_card = Card(front=CardText("犬"), back=CardText("dog"), due_at=date(2026, 4, 10))

    deck.add_card(due_card)
    deck.add_card(future_card)

    assert due_card.deck_name == DeckName("Japanese")
    assert deck.due_cards(date(2026, 4, 4)) == [due_card]
    assert deck.new_cards() == [due_card, future_card]

    deck.archive()
    with pytest.raises(InvalidDeckError):
        deck.add_card(Card(front=CardText("鳥"), back=CardText("bird")))

    deck.unarchive()
    deck.remove_card(future_card)
    assert future_card.deck_name is None


def test_card_suspend_resume_and_reset() -> None:
    card = Card(
        front=CardText("水"),
        back=CardText("water"),
        due_at=date(2026, 4, 4),
        repetitions=2,
        interval_days=6,
    )

    assert card.is_due(date(2026, 4, 4)) is True
    card.suspend()
    assert card.is_due(date(2026, 4, 4)) is False
    card.resume()
    assert card.state == CardState.REVIEW

    card.reset()
    assert card.state == CardState.NEW
    assert card.interval_days == 0
    assert card.repetitions == 0
    assert card.lapses == 0
    assert card.review_history == []


def test_review_service_updates_card_history_and_schedule() -> None:
    card = Card(front=CardText("火"), back=CardText("fire"), due_at=date(2026, 4, 4))
    service = ReviewService()

    record = service.review(card, ReviewRating.GOOD, reviewed_on=date(2026, 4, 4))

    assert record.previous_state == CardState.NEW
    assert record.next_state == CardState.LEARNING
    assert card.state == CardState.LEARNING
    assert card.interval_days == 1
    assert card.due_at == date(2026, 4, 5)
    assert card.review_history == [record]


def test_scheduling_policy_boundaries() -> None:
    policy = DefaultSchedulingPolicy()
    card = Card(
        front=CardText("木"),
        back=CardText("tree"),
        state=CardState.REVIEW,
        ease_factor=Decimal("1.30"),
        interval_days=10,
        repetitions=3,
        lapses=1,
        due_at=date(2026, 4, 4),
    )

    again_plan = policy.schedule(card, ReviewRating.AGAIN, reviewed_on=date(2026, 4, 4))
    assert again_plan.next_state == CardState.LEARNING
    assert again_plan.interval_days == 1
    assert again_plan.ease_factor == Decimal("1.30")
    assert again_plan.lapses == 2

    easy_plan = policy.schedule(card, ReviewRating.EASY, reviewed_on=date(2026, 4, 4))
    assert easy_plan.next_state == CardState.REVIEW
    assert easy_plan.interval_days == 25
    assert easy_plan.repetitions == 4
    assert easy_plan.due_at == date(2026, 4, 29)


def test_invalid_card_and_review_inputs_are_rejected() -> None:
    with pytest.raises(InvalidCardError):
        Card(front="bad", back=CardText("ok"))  # type: ignore[arg-type]

    with pytest.raises(InvalidCardError):
        Card(front=CardText("front"), back=CardText("back"), ease_factor=Decimal("1.20"))

    with pytest.raises(InvalidReviewError):
        DefaultSchedulingPolicy().schedule(
            Card(front=CardText("front"), back=CardText("back")),
            "bad",  # type: ignore[arg-type]
        )
