"""Mechanical grading: tests 60%, deliverables 20%, content checks 20%.

Axes a challenge doesn't use are reweighted away, so a challenge with no
content_checks is graded 75/25 tests/deliverables. The blend is computed
here, server-side, and stored — the site only ever displays stored numbers.
"""

from __future__ import annotations

from dataclasses import dataclass
from pathlib import Path

from .challenges import Challenge
from .workspace import TestOutcome

WEIGHTS = {"tests": 0.6, "deliverables": 0.2, "content": 0.2}


@dataclass(frozen=True)
class Grade:
    score_tests: float        # 0..1
    score_deliverables: float
    score_content: float | None  # None when the challenge has no content checks
    total: float              # 0..100 blended


def grade(challenge: Challenge, root: Path, tests: TestOutcome) -> Grade:
    score_tests = tests.passed / tests.total if tests.total else 0.0

    present = sum(1 for rel in challenge.expected_files if (root / rel).is_file())
    score_deliverables = present / len(challenge.expected_files)

    score_content: float | None = None
    if challenge.content_checks:
        checks = [
            (root / rel, needle)
            for rel, needles in challenge.content_checks.items()
            for needle in needles
        ]
        hits = sum(1 for path, needle in checks if path.is_file() and needle in path.read_text())
        score_content = hits / len(checks)

    axes = {"tests": score_tests, "deliverables": score_deliverables}
    if score_content is not None:
        axes["content"] = score_content
    weight_sum = sum(WEIGHTS[name] for name in axes)
    total = 100 * sum(WEIGHTS[name] * value for name, value in axes.items()) / weight_sum

    return Grade(score_tests, score_deliverables, score_content, round(total, 1))
