"""Tests for ``compute_weighted_average``.

Covers the canonical happy path (SP2 incubation use case), edge cases
(empty rubric, zero max_marks, zero scores per section), and the
``require_all_sections`` flag that closes the SP2 INC020 audit gap.
"""
from __future__ import annotations

from dataclasses import dataclass
from decimal import Decimal

from apps.smehub._shared.calls import compute_weighted_average


@dataclass
class _Section:
    pk: int
    weight: Decimal
    max_marks: int


@dataclass
class _Score:
    section_id: int
    score: Decimal


def _section(pk: int, weight: str, max_marks: int = 10) -> _Section:
    return _Section(pk=pk, weight=Decimal(weight), max_marks=max_marks)


def _score(section_id: int, value: str) -> _Score:
    return _Score(section_id=section_id, score=Decimal(value))


# ---------------------------------------------------------------------------
# Happy path
# ---------------------------------------------------------------------------

def test_single_section_returns_normalised_percentage():
    sections = [_section(1, "1.0", max_marks=10)]
    scores = [_score(1, "8")]
    assert compute_weighted_average(sections, scores) == Decimal("80.00")


def test_multiple_sections_weighted():
    # 70% weight on section 1 (score 8/10) + 30% on section 2 (score 6/10)
    # = 0.7 * 0.8 + 0.3 * 0.6 = 0.56 + 0.18 = 0.74 → 74.00%
    sections = [_section(1, "0.7"), _section(2, "0.3")]
    scores = [_score(1, "8"), _score(2, "6")]
    assert compute_weighted_average(sections, scores) == Decimal("74.00")


def test_multiple_judges_per_section_average():
    # Section 1 weighted 1.0, max=10. Two judges score 8 and 6 → avg 7 → 70%.
    sections = [_section(1, "1.0", max_marks=10)]
    scores = [_score(1, "8"), _score(1, "6")]
    assert compute_weighted_average(sections, scores) == Decimal("70.00")


# ---------------------------------------------------------------------------
# Edge cases
# ---------------------------------------------------------------------------

def test_empty_sections_returns_zero():
    assert compute_weighted_average([], []) == Decimal("0")


def test_section_with_zero_max_marks_contributes_nothing():
    """Defensive: a misconfigured rubric must not raise ZeroDivisionError."""
    sections = [_section(1, "1.0", max_marks=0)]
    scores = [_score(1, "8")]
    assert compute_weighted_average(sections, scores) == Decimal("0.00")


def test_no_scores_yet_returns_zero_in_lenient_mode():
    """Default behaviour matches SP2: no scores → 0% (so dashboards still render)."""
    sections = [_section(1, "0.5"), _section(2, "0.5")]
    assert compute_weighted_average(sections, []) == Decimal("0.00")


# ---------------------------------------------------------------------------
# require_all_sections — closes the SP2 INC020 audit gap
# ---------------------------------------------------------------------------

def test_require_all_sections_returns_none_when_partial():
    """When ranking gates selection, partial submissions must not silently
    produce a number (FRSME-INC020 audit finding)."""
    sections = [_section(1, "0.5"), _section(2, "0.5")]
    scores = [_score(1, "8")]  # section 2 has no scores
    assert compute_weighted_average(sections, scores, require_all_sections=True) is None


def test_require_all_sections_returns_value_when_complete():
    sections = [_section(1, "0.5"), _section(2, "0.5")]
    scores = [_score(1, "8"), _score(2, "6")]
    # 0.5 * 0.8 + 0.5 * 0.6 = 0.7 → 70.00%
    assert compute_weighted_average(sections, scores, require_all_sections=True) == Decimal("70.00")


def test_lenient_mode_silently_ranks_partial():
    """Backwards-compat with SP2: callers that don't opt in still get the
    historical (wrong) behaviour. Phase 3 will switch SP2's selection gate
    to ``require_all_sections=True``."""
    sections = [_section(1, "0.5"), _section(2, "0.5")]
    scores = [_score(1, "8")]  # only half scored
    # 0.5 * 0.8 = 0.4 → 40.00% (mathematically wrong-ish but matches today)
    assert compute_weighted_average(sections, scores) == Decimal("40.00")
