"""Risk scoring for AI Testing Swarm.

Batch1 additions:
- Compute a numeric risk_score per test result (0..100)
- Aggregate endpoint risk and drive gate thresholds (PASS/WARN/BLOCK)

Design goals:
- Backward compatible: consumers can ignore risk_score fields.
- Deterministic and explainable.
"""

from __future__ import annotations

from dataclasses import dataclass


@dataclass(frozen=True)
class RiskThresholds:
    """Thresholds for the endpoint gate.

    Gate is computed from endpoint_risk_score (currently max test risk).

    - PASS: score < warn
    - WARN: warn <= score < block
    - BLOCK: score >= block
    """

    warn: int = 30
    block: int = 80


# Keep aligned with orchestrator/release gate semantics
EXPECTED_FAILURES: set[str] = {
    "success",
    "missing_param",
    "invalid_param",
    "security",
    "method_not_allowed",
    "not_found",
    "content_negotiation",
}

RISKY_FAILURES: set[str] = {
    "unknown",
    "missing_param_accepted",
    "null_param_accepted",
    "invalid_param_accepted",
    "headers_accepted",
    "method_risk",
}

BLOCKING_FAILURES: set[str] = {
    "auth_issue",
    "infra",
    "security_risk",
    "server_error",
}


def clamp_int(x: int, lo: int, hi: int) -> int:
    return lo if x < lo else hi if x > hi else x


def compute_test_risk_score(result: dict, *, sla_ms: int | None = None) -> int:
    """Compute a risk score for a single test result.

    Inputs expected (best-effort):
      - result['failure_type']
      - result['status']
      - result['mutation']['strategy'] (optional)
      - result['response']['status_code']
      - result['response']['elapsed_ms']
      - result['response']['openapi_validation'] (list)

    Returns: int in range 0..100.

    Batch2: strategy-aware weighting.
    The same failure_type can be more/less severe depending on the test strategy.
    """

    ft = str(result.get("failure_type") or "unknown")
    status = str(result.get("status") or "")
    mutation = result.get("mutation") or {}
    strategy = str(mutation.get("strategy") or "").strip().lower()

    resp = result.get("response") or {}
    sc = resp.get("status_code")

    # Base score from semantic classification.
    if ft in EXPECTED_FAILURES:
        base = 0
    elif ft in RISKY_FAILURES:
        base = 35
    elif ft in BLOCKING_FAILURES:
        base = 90
    else:
        # Unknown failure types are treated as high risk but not always a hard blocker.
        base = 60

    # Strategy-aware overrides (only when the strategy is known).
    # These are designed to stay deterministic and explainable.
    if strategy == "security" and ft == "security_risk":
        base = max(base, 100)
    if strategy in {"missing_param", "null_param", "invalid_param"} and ft.endswith("_accepted"):
        # Validation bypass signals.
        base = max(base, 80)
    if strategy == "headers" and ft == "headers_accepted":
        base = max(base, 55)
    if strategy == "method_misuse" and ft == "method_risk":
        base = max(base, 85)
    if strategy == "auth" and ft == "auth_issue":
        # Often indicates environment/config drift rather than product risk.
        base = min(base, 70)

    # Status-code adjustments (defense in depth)
    if isinstance(sc, int):
        if 500 <= sc:
            base = max(base, 90)
        elif sc in (401, 403):
            base = max(base, 80)
        elif 400 <= sc < 500:
            # Client errors for negative tests are expected; keep base.
            base = max(base, base)

    # Explicit FAILED status implies something unexpected.
    if status.upper() == "FAILED":
        base = max(base, 70)

    # OpenAPI validation issues are a small additive risk.
    issues = resp.get("openapi_validation") or []
    if isinstance(issues, list) and issues:
        base += 10

    # SLA breach is a small additive risk.
    if sla_ms is not None:
        elapsed = resp.get("elapsed_ms")
        if isinstance(elapsed, int) and elapsed > sla_ms:
            base += 5

    return clamp_int(int(base), 0, 100)


def compute_endpoint_risk_score(results: list[dict]) -> int:
    """Aggregate endpoint risk score.

    Current policy: endpoint risk is the max risk_score across tests.
    (This makes gating stable and easy to interpret.)
    """

    scores = [r.get("risk_score") for r in (results or []) if isinstance(r.get("risk_score"), int)]
    if not scores:
        return 0
    return int(max(scores))


def gate_from_score(score: int, thresholds: RiskThresholds) -> str:
    if score >= thresholds.block:
        return "BLOCK"
    if score >= thresholds.warn:
        return "WARN"
    return "PASS"
