# retoor <retoor@molodetz.nl>

from datetime import datetime, timezone

from devplacepy.database import get_table
from devplacepy.services.quiz import store
from devplacepy.utils import generate_uid, make_combined_slug

_counter = [0]


def _user(prefix):
    _counter[0] += 1
    uid = generate_uid()
    get_table("users").insert(
        {
            "uid": uid,
            "username": f"{prefix}{_counter[0]}",
            "terms_version": "1",
            "email": f"{prefix}{_counter[0]}@t.dev",
            "role": "Member",
            "api_key": generate_uid(),
            "created_at": datetime.now(timezone.utc).isoformat(),
        }
    )
    return get_table("users").find_one(uid=uid)


def _published(owner, points=10):
    uid = generate_uid()
    get_table("quizzes").insert(
        store.born_live(
            {
                "uid": uid,
                "user_uid": owner["uid"],
                "slug": make_combined_slug("Board", uid),
                "title": "Board",
                "description": "",
                "status": "published",
                "published_at": datetime.now(timezone.utc).isoformat(),
                "shuffle_questions": 0,
                "shuffle_options": 0,
                "reveal_answers": 0,
                "allow_review": 1,
                "time_limit_seconds": 0,
                "pass_percent": 0,
                "question_count": 1,
                "total_points": points,
                "attempt_count": 0,
                "stars": 0,
            }
        )
    )
    return store.get_quiz(uid)


def _completed(player, quiz, points, percent):
    uid = generate_uid()
    get_table("quiz_attempts").insert(
        store.born_live(
            {
                "uid": uid,
                "quiz_uid": quiz["uid"],
                "user_uid": player["uid"],
                "status": "completed",
                "question_order": "[]",
                "started_at": datetime.now(timezone.utc).isoformat(),
                "expires_at": "",
                "completed_at": datetime.now(timezone.utc).isoformat(),
                "answered_count": 1,
                "score_points": float(points),
                "max_points": int(quiz["total_points"]),
                "score_percent": float(percent),
                "passed": 0,
            }
        )
    )
    store.clear_cache()
    return uid


def _in_progress(player, quiz):
    uid = generate_uid()
    get_table("quiz_attempts").insert(
        store.born_live(
            {
                "uid": uid,
                "quiz_uid": quiz["uid"],
                "user_uid": player["uid"],
                "status": "in_progress",
                "question_order": "[]",
                "started_at": datetime.now(timezone.utc).isoformat(),
                "expires_at": "",
                "completed_at": "",
                "answered_count": 0,
                "score_points": 0.0,
                "max_points": int(quiz["total_points"]),
                "score_percent": 0.0,
                "passed": 0,
            }
        )
    )
    store.clear_cache()
    return uid


def _entry_for(player):
    for entry in store.scoreboard(100):
        if entry["user"]["uid"] == player["uid"]:
            return entry
    return None


def test_a_completed_attempt_puts_a_member_on_the_board(local_db):
    owner = _user("sb")
    player = _user("sb")
    quiz = _published(owner)
    _completed(player, quiz, 8, 80.0)
    entry = _entry_for(player)
    assert entry["total_points"] == 8.0
    assert entry["quizzes_completed"] == 1


def test_only_the_best_attempt_per_quiz_counts(local_db):
    owner = _user("sb")
    player = _user("sb")
    quiz = _published(owner)
    _completed(player, quiz, 4, 40.0)
    _completed(player, quiz, 9, 90.0)
    _completed(player, quiz, 2, 20.0)
    entry = _entry_for(player)
    assert entry["total_points"] == 9.0
    assert entry["quizzes_completed"] == 1


def test_replaying_a_quiz_can_never_multiply_a_score(local_db):
    owner = _user("sb")
    player = _user("sb")
    quiz = _published(owner)
    _completed(player, quiz, 7, 70.0)
    before = _entry_for(player)["total_points"]
    for _ in range(5):
        _completed(player, quiz, 7, 70.0)
    assert _entry_for(player)["total_points"] == before


def test_a_worse_attempt_never_lowers_the_contribution(local_db):
    owner = _user("sb")
    player = _user("sb")
    quiz = _published(owner)
    _completed(player, quiz, 9, 90.0)
    _completed(player, quiz, 1, 10.0)
    assert _entry_for(player)["total_points"] == 9.0


def test_a_better_attempt_raises_by_exactly_the_difference(local_db):
    owner = _user("sb")
    player = _user("sb")
    quiz = _published(owner)
    _completed(player, quiz, 4, 40.0)
    _completed(player, quiz, 10, 100.0)
    assert _entry_for(player)["total_points"] == 10.0


def test_two_quizzes_sum_their_best_attempts(local_db):
    owner = _user("sb")
    player = _user("sb")
    first = _published(owner)
    second = _published(owner)
    _completed(player, first, 6, 60.0)
    _completed(player, second, 3, 30.0)
    entry = _entry_for(player)
    assert entry["total_points"] == 9.0
    assert entry["quizzes_completed"] == 2


def test_an_author_scores_on_their_own_quiz(local_db):
    owner = _user("sb")
    quiz = _published(owner)
    _completed(owner, quiz, 10, 100.0)
    entry = _entry_for(owner)
    assert entry["total_points"] == 10.0
    assert entry["quizzes_completed"] == 1
    assert entry["perfect_count"] == 1


def test_an_in_progress_attempt_is_not_counted(local_db):
    owner = _user("sb")
    player = _user("sb")
    quiz = _published(owner)
    _in_progress(player, quiz)
    assert _entry_for(player) is None


def test_a_draft_quiz_is_not_counted(local_db):
    owner = _user("sb")
    player = _user("sb")
    quiz = _published(owner)
    get_table("quizzes").update({"uid": quiz["uid"], "status": "draft"}, ["uid"])
    _completed(player, quiz, 10, 100.0)
    assert _entry_for(player) is None


def test_a_deleted_attempt_is_not_counted(local_db):
    owner = _user("sb")
    player = _user("sb")
    quiz = _published(owner)
    attempt_uid = _completed(player, quiz, 10, 100.0)
    get_table("quiz_attempts").update(
        {"uid": attempt_uid, "deleted_at": datetime.now(timezone.utc).isoformat()}, ["uid"]
    )
    store.clear_cache()
    assert _entry_for(player) is None


def test_ranks_are_a_gapless_total_order(local_db):
    owner = _user("sb")
    quiz = _published(owner)
    for score in (3, 9, 6):
        _completed(_user("sb"), quiz, score, score * 10.0)
    ranks = [entry["rank"] for entry in store.scoreboard(100)]
    assert ranks == list(range(1, len(ranks) + 1))


def test_equal_totals_resolve_deterministically(local_db):
    owner = _user("sb")
    quiz = _published(owner)
    for _ in range(3):
        _completed(_user("sb"), quiz, 5, 50.0)
    first = [entry["user"]["uid"] for entry in store.scoreboard(100)]
    store.clear_cache()
    assert [entry["user"]["uid"] for entry in store.scoreboard(100)] == first


def test_the_limit_bounds_the_board(local_db):
    owner = _user("sb")
    quiz = _published(owner)
    for score in range(1, 6):
        _completed(_user("sb"), quiz, score, score * 10.0)
    assert len(store.scoreboard(3)) == 3


def test_perfect_attempts_are_counted(local_db):
    owner = _user("sb")
    player = _user("sb")
    quiz = _published(owner)
    _completed(player, quiz, 10, 100.0)
    assert _entry_for(player)["perfect_count"] == 1


def test_standing_for_finds_a_member_outside_the_top(local_db):
    owner = _user("sb")
    player = _user("sb")
    quiz = _published(owner)
    _completed(player, quiz, 2, 20.0)
    standing = store.standing_for(player["uid"])
    assert standing["user"]["uid"] == player["uid"]
    assert standing["rank"] >= 1


def test_standing_for_is_none_without_attempts(local_db):
    assert store.standing_for(_user("sb")["uid"]) is None


def test_attempt_states_reports_done_with_the_best_percentage(local_db):
    owner = _user("sb")
    player = _user("sb")
    quiz = _published(owner)
    _completed(player, quiz, 4, 40.0)
    _completed(player, quiz, 9, 90.0)
    states = store.attempt_states_for(player["uid"], [quiz["uid"]])
    assert states[quiz["uid"]]["state"] == "done"
    assert states[quiz["uid"]]["best_percent"] == 90.0


def test_attempt_states_reports_in_progress(local_db):
    owner = _user("sb")
    player = _user("sb")
    quiz = _published(owner)
    _in_progress(player, quiz)
    states = store.attempt_states_for(player["uid"], [quiz["uid"]])
    assert states[quiz["uid"]]["state"] == "in_progress"
    assert states[quiz["uid"]]["attempt_uid"]


def test_attempt_states_is_empty_for_a_guest(local_db):
    owner = _user("sb")
    quiz = _published(owner)
    assert store.attempt_states_for("", [quiz["uid"]]) == {}


def test_attempt_states_is_empty_without_quiz_uids(local_db):
    assert store.attempt_states_for(_user("sb")["uid"], []) == {}


def test_completed_quiz_uids_lists_only_finished_quizzes(local_db):
    owner = _user("sb")
    player = _user("sb")
    done = _published(owner)
    open_quiz = _published(owner)
    _completed(player, done, 5, 50.0)
    _in_progress(player, open_quiz)
    completed = store.completed_quiz_uids(player["uid"])
    assert done["uid"] in completed
    assert open_quiz["uid"] not in completed


def test_progress_for_summarises_the_viewer(local_db):
    owner = _user("sb")
    player = _user("sb")
    quiz = _published(owner)
    _completed(player, quiz, 10, 100.0)
    progress = store.progress_for(player["uid"])
    assert progress["completed"] == 1
    assert progress["avg_percent"] == 100.0
    assert progress["perfect_count"] == 1


def test_the_quiz_leaderboard_ranks_by_percentage(local_db):
    owner = _user("sb")
    quiz = _published(owner)
    _completed(_user("sb"), quiz, 3, 30.0)
    _completed(_user("sb"), quiz, 9, 90.0)
    entries = store.quiz_leaderboard(quiz["uid"])
    assert entries[0]["score_percent"] == 90.0
    assert entries[0]["rank"] == 1


def test_the_quiz_leaderboard_ignores_unfinished_attempts(local_db):
    owner = _user("sb")
    quiz = _published(owner)
    _in_progress(_user("sb"), quiz)
    assert store.quiz_leaderboard(quiz["uid"]) == []


def test_progress_agrees_with_the_scoreboard_for_an_author_only_completion(local_db):
    author = _user("sb")
    quiz = _published(author)
    _completed(author, quiz, 10, 100.0)
    progress = store.progress_for(author["uid"])
    entry = _entry_for(author)
    assert progress["completed"] == entry["quizzes_completed"] == 1
    assert progress["avg_percent"] == entry["avg_percent"] == 100.0
    assert progress["total_points"] == entry["total_points"] == 10.0
    assert progress["perfect_count"] == entry["perfect_count"] == 1
    assert progress["rank"] == entry["rank"]


def test_progress_counts_a_ranked_completion(local_db):
    owner = _user("sb")
    player = _user("sb")
    quiz = _published(owner)
    _completed(player, quiz, 8, 80.0)
    progress = store.progress_for(player["uid"])
    entry = _entry_for(player)
    assert progress["completed"] == entry["quizzes_completed"] == 1
    assert progress["avg_percent"] == entry["avg_percent"] == 80.0
    assert progress["total_points"] == entry["total_points"] == 8.0


def test_completing_your_own_quiz_clears_it_from_todo(local_db):
    author = _user("sb")
    before = store.progress_for(author["uid"])["todo"]
    quiz = _published(author)
    assert store.progress_for(author["uid"])["todo"] == before + 1
    _completed(author, quiz, 10, 100.0)
    assert store.progress_for(author["uid"])["todo"] == before


def test_progress_todo_counts_every_published_quiz(local_db):
    author = _user("sb")
    other = _user("sb")
    before = store.progress_for(author["uid"])["todo"]
    _published(author)
    _published(other)
    _published(other)
    assert store.progress_for(author["uid"])["todo"] == before + 3


def test_completing_a_quiz_moves_it_from_todo_to_completed(local_db):
    owner = _user("sb")
    player = _user("sb")
    quiz = _published(owner)
    before = store.progress_for(player["uid"])
    _completed(player, quiz, 5, 50.0)
    after = store.progress_for(player["uid"])
    assert after["completed"] == before["completed"] + 1
    assert after["todo"] == before["todo"] - 1
