# retoor <retoor@molodetz.nl>

import time


from tests.api.quizzes._helpers import (
    CHOICE,
    FREE_TEXT,
    JSON,
    NUMERIC,
    TRUE_FALSE,
    answer_correctly,
    attempt_state,
    build_published,
    signup,
    start_attempt,
)
from tests.conftest import BASE_URL

CACHE_VERSION_TTL_SECONDS = 1.5

FILL_BLANK = {
    "kind": "fill_blank",
    "prompt": "Name the two pragmas",
    "points": 2,
    "options": "First\nSecond",
    "match_values": "journal_mode\nsynchronous",
}

ORDERING = {
    "kind": "ordering",
    "prompt": "Order the steps",
    "points": 3,
    "options": "Open\nRead\nClose",
}

MATCHING = {
    "kind": "matching",
    "prompt": "Match the pragma",
    "points": 2,
    "options": "journal_mode\nsynchronous",
    "match_values": "WAL\nNORMAL",
}


def _single_question(session, question, **settings):
    slug = build_published(session, questions=(question,), title=f"Graded {question['kind']}", **settings)
    return slug


def _drop_api_key(username):
    from devplacepy.database import get_table, refresh_snapshot
    from devplacepy.utils import clear_user_cache

    refresh_snapshot()
    user = get_table("users").find_one(username=username)
    get_table("users").update({"uid": user["uid"], "api_key": ""}, ["uid"])
    clear_user_cache(user["uid"])
    refresh_snapshot()
    time.sleep(CACHE_VERSION_TTL_SECONDS)


def _answer(session, slug, payload):
    attempt_uid = start_attempt(session, slug)
    question = attempt_state(session, slug, attempt_uid)["attempt"]["questions"][0]
    body = {"question_uid": question["uid"], **payload(question)}
    response = session.post(
        f"{BASE_URL}/quizzes/{slug}/attempts/{attempt_uid}/answer", data=body, headers=JSON
    )
    return response, question


def test_a_single_choice_is_graded_deterministically(app_server, seeded_db):
    owner, _ = signup()
    slug = _single_question(owner, CHOICE)
    player, _ = signup()
    response, _ = _answer(
        player, slug, lambda question: {"option_uids": question["options"][1]["uid"]}
    )
    answer = response.json()["answer"]
    assert answer["graded_by"] == "auto"
    assert answer["is_correct"] is True
    assert answer["awarded_points"] == 2.0


def test_a_wrong_single_choice_awards_nothing(app_server, seeded_db):
    owner, _ = signup()
    slug = _single_question(owner, CHOICE)
    player, _ = signup()
    response, _ = _answer(
        player, slug, lambda question: {"option_uids": question["options"][0]["uid"]}
    )
    assert response.json()["answer"]["awarded_points"] == 0.0


def test_a_true_false_is_graded(app_server, seeded_db):
    owner, _ = signup()
    slug = _single_question(owner, TRUE_FALSE)
    player, _ = signup()
    response, _ = _answer(player, slug, lambda question: {"answer_text": "true"})
    assert response.json()["answer"]["is_correct"] is True


def test_a_numeric_within_tolerance_is_correct(app_server, seeded_db):
    owner, _ = signup()
    slug = _single_question(owner, {**NUMERIC, "numeric_tolerance": "1"})
    player, _ = signup()
    response, _ = _answer(player, slug, lambda question: {"answer_text": "1023"})
    assert response.json()["answer"]["is_correct"] is True


def test_a_fill_blank_gives_partial_credit(app_server, seeded_db):
    owner, _ = signup()
    slug = _single_question(owner, FILL_BLANK)
    player, _ = signup()
    response, _ = _answer(
        player, slug, lambda question: {"blanks": "journal_mode,wrong"}
    )
    answer = response.json()["answer"]
    assert answer["awarded_points"] == 1.0
    assert answer["is_correct"] is False


def test_a_full_fill_blank_is_correct(app_server, seeded_db):
    owner, _ = signup()
    slug = _single_question(owner, FILL_BLANK)
    player, _ = signup()
    response, _ = _answer(
        player, slug, lambda question: {"blanks": "journal_mode,synchronous"}
    )
    assert response.json()["answer"]["is_correct"] is True


def test_an_ordering_gives_prefix_credit(app_server, seeded_db):
    owner, _ = signup()
    slug = _single_question(owner, ORDERING)
    player, _ = signup()

    def payload(question):
        uids = [option["uid"] for option in question["options"]]
        return {"option_uids": ",".join([uids[0], uids[2], uids[1]])}

    response, _ = _answer(player, slug, payload)
    assert response.json()["answer"]["awarded_points"] == 1.0


def test_a_matching_gives_pair_credit(app_server, seeded_db):
    owner, _ = signup()
    slug = _single_question(owner, MATCHING)
    player, _ = signup()

    def payload(question):
        uids = [option["uid"] for option in question["options"]]
        return {"option_uids": ",".join(uids), "matches": "WAL,wrong"}

    response, _ = _answer(player, slug, payload)
    assert response.json()["answer"]["awarded_points"] == 1.0


def test_a_matching_exposes_its_right_hand_choices(app_server, seeded_db):
    owner, _ = signup()
    slug = _single_question(owner, MATCHING)
    player, _ = signup()
    attempt_uid = start_attempt(player, slug)
    question = attempt_state(player, slug, attempt_uid)["attempt"]["questions"][0]
    assert sorted(question["match_choices"]) == ["NORMAL", "WAL"]
    assert all(option["match_value"] is None for option in question["options"])


def test_a_free_text_answer_is_still_graded_without_the_gateway(app_server, seeded_db):
    owner, _ = signup()
    slug = _single_question(owner, FREE_TEXT)
    player, player_name = signup()
    _drop_api_key(player_name)
    response, _ = _answer(
        player,
        slug,
        lambda question: {"answer_text": "the planner picks a bad index for live rows"},
    )
    assert response.status_code == 200
    assert response.json()["answer"]["answered"] is True


def test_the_degraded_grade_is_stamped_as_a_fallback(app_server, seeded_db):
    owner, _ = signup()
    slug = _single_question(owner, FREE_TEXT)
    player, player_name = signup()
    _drop_api_key(player_name)
    response, _ = _answer(
        player, slug, lambda question: {"answer_text": "a one bucket index for live rows"}
    )
    assert response.json()["answer"]["graded_by"] == "fallback"


def test_the_fallback_explains_itself_to_the_learner(app_server, seeded_db):
    owner, _ = signup()
    slug = _single_question(owner, FREE_TEXT)
    player, player_name = signup()
    _drop_api_key(player_name)
    response, _ = _answer(player, slug, lambda question: {"answer_text": "a one bucket index"})
    assert "unavailable" in response.json()["answer"]["feedback"]


def test_the_fallback_score_stays_within_the_question_points(app_server, seeded_db):
    owner, _ = signup()
    slug = _single_question(owner, FREE_TEXT)
    player, player_name = signup()
    _drop_api_key(player_name)
    response, _ = _answer(
        player,
        slug,
        lambda question: {"answer_text": "It keeps the live query off a one bucket index."},
    )
    answer = response.json()["answer"]
    assert 0.0 <= answer["awarded_points"] <= 3.0
    assert 0.0 <= answer["confidence"] <= 1.0


def test_an_empty_free_text_answer_scores_zero(app_server, seeded_db):
    owner, _ = signup()
    slug = _single_question(owner, FREE_TEXT)
    player, player_name = signup()
    _drop_api_key(player_name)
    response, _ = _answer(player, slug, lambda question: {"answer_text": "completely unrelated"})
    assert response.json()["answer"]["awarded_points"] == 0.0


def test_the_results_page_surfaces_the_fallback_note(app_server, seeded_db):
    owner, _ = signup()
    slug = _single_question(owner, FREE_TEXT, allow_review="1")
    player, player_name = signup()
    _drop_api_key(player_name)
    attempt_uid = start_attempt(player, slug)
    question = attempt_state(player, slug, attempt_uid)["attempt"]["questions"][0]
    player.post(
        f"{BASE_URL}/quizzes/{slug}/attempts/{attempt_uid}/answer",
        data={"question_uid": question["uid"], "answer_text": "a one bucket index"},
        headers=JSON,
    )
    player.post(f"{BASE_URL}/quizzes/{slug}/attempts/{attempt_uid}/finish", headers=JSON)
    payload = player.get(
        f"{BASE_URL}/quizzes/{slug}/attempts/{attempt_uid}/results", headers=JSON
    ).json()
    assert payload["fallback_count"] == 1
    page = player.get(f"{BASE_URL}/quizzes/{slug}/attempts/{attempt_uid}/results").text
    assert "quiz-grade-fallback" in page


def test_a_degraded_grade_records_a_system_audit_failure(app_server, seeded_db):
    from devplacepy.database import get_table, refresh_snapshot

    owner, _ = signup()
    slug = _single_question(owner, FREE_TEXT)
    player, player_name = signup()
    _drop_api_key(player_name)
    _answer(player, slug, lambda question: {"answer_text": "a one bucket index"})
    refresh_snapshot()
    count = get_table("audit_log").count(event_key="quiz.grade.failed", result="failure")
    assert count >= 1


def test_only_free_text_ever_reaches_the_grader(app_server, seeded_db):
    owner, _ = signup()
    slug = build_published(owner, questions=(CHOICE, NUMERIC, TRUE_FALSE), title="No gateway quiz")
    player, _ = signup()
    attempt_uid = start_attempt(player, slug)
    state = attempt_state(player, slug, attempt_uid)
    for question in state["attempt"]["questions"]:
        payload = answer_correctly(player, slug, attempt_uid, question).json()
        assert payload["answer"]["graded_by"] == "auto"
