"""common.pool_timing — the ONE source of truth for the heartbeat metronome.

These tests pin the contract that makes the safety invariant hold BY CONSTRUCTION
rather than by two files coincidentally defaulting to 25/30:

  * the shared module REJECTS an unsafe pair (heartbeat >= pingback, or no margin)
    at IMPORT time — a bad config can never start a worker or the pool-service;
  * the defaults are the canonical 25/30 with the documented 1s margin;
  * the env contract: PS_PINGBACK_TIMEOUT (worker) takes precedence, with a
    fallback to the bare PINGBACK_TIMEOUT the pool-service container historically
    receives, so BOTH consumers resolve to the SAME value;
  * pool_worker and pool_service both READ those same values from this module
    (no second declaration to drift).
"""
import importlib
import os
import sys

import pytest


def _reimport_pool_timing(env: dict):
    """Import common.pool_timing FRESH under a given env overlay.

    The invariant is enforced at import time, so each case must re-execute the
    module body. We drop any cached copy, set the env, import, and restore.
    """
    saved = {k: os.environ.get(k) for k in
             ("PS_HEARTBEAT_INTERVAL", "PS_PINGBACK_TIMEOUT",
              "PINGBACK_TIMEOUT", "PS_HEARTBEAT_SAFETY_MARGIN")}
    for k in saved:
        os.environ.pop(k, None)
    os.environ.update({k: str(v) for k, v in env.items()})
    sys.modules.pop("common.pool_timing", None)
    try:
        return importlib.import_module("common.pool_timing")
    finally:
        for k, v in saved.items():
            if v is None:
                os.environ.pop(k, None)
            else:
                os.environ[k] = v
        sys.modules.pop("common.pool_timing", None)


# ── defaults ─────────────────────────────────────────────────────────────────────
def test_defaults_are_25_30_with_margin():
    pt = _reimport_pool_timing({})
    assert pt.HEARTBEAT_INTERVAL == 25
    assert pt.PINGBACK_TIMEOUT == 30
    assert pt.HEARTBEAT_SAFETY_MARGIN >= 1
    # invariant holds by construction
    assert pt.HEARTBEAT_INTERVAL + pt.HEARTBEAT_SAFETY_MARGIN <= pt.PINGBACK_TIMEOUT


# ── invariant enforced at import ──────────────────────────────────────────────────
@pytest.mark.security
def test_rejects_heartbeat_equal_pingback():
    with pytest.raises(RuntimeError, match="SAFETY INVARIANT"):
        _reimport_pool_timing({"PS_HEARTBEAT_INTERVAL": 30, "PS_PINGBACK_TIMEOUT": 30})


@pytest.mark.security
def test_rejects_heartbeat_greater_than_pingback():
    with pytest.raises(RuntimeError, match="SAFETY INVARIANT"):
        _reimport_pool_timing({"PS_HEARTBEAT_INTERVAL": 40, "PS_PINGBACK_TIMEOUT": 30})


@pytest.mark.security
def test_rejects_no_safety_margin():
    # heartbeat < pingback but with no margin (29 < 30, margin 1 -> 29+1 == 30 OK;
    # 30 < 31 with margin 2 -> 32 > 31 -> reject)
    with pytest.raises(RuntimeError, match="SAFETY INVARIANT"):
        _reimport_pool_timing({"PS_HEARTBEAT_INTERVAL": 30, "PS_PINGBACK_TIMEOUT": 31,
                               "PS_HEARTBEAT_SAFETY_MARGIN": 2})


def test_accepts_safe_pair_at_boundary():
    pt = _reimport_pool_timing({"PS_HEARTBEAT_INTERVAL": 29, "PS_PINGBACK_TIMEOUT": 30})
    assert pt.HEARTBEAT_INTERVAL == 29 and pt.PINGBACK_TIMEOUT == 30


def test_accepts_large_per_worker_pingback():
    # docker-compose.step9.yml overrides PS_PINGBACK_TIMEOUT (e.g. 7200) while leaving
    # the global heartbeat at 25 — must stay valid.
    pt = _reimport_pool_timing({"PS_PINGBACK_TIMEOUT": 7200})
    assert pt.HEARTBEAT_INTERVAL == 25 and pt.PINGBACK_TIMEOUT == 7200


# ── env contract: PS_ precedence + bare-name fallback ─────────────────────────────
def test_ps_pingback_takes_precedence_over_bare():
    pt = _reimport_pool_timing({"PS_PINGBACK_TIMEOUT": 120, "PINGBACK_TIMEOUT": 999})
    assert pt.PINGBACK_TIMEOUT == 120


def test_bare_pingback_fallback_for_pool_service():
    # The pool-service container receives the bare PINGBACK_TIMEOUT (compose maps it
    # from PS_PINGBACK_TIMEOUT). With PS_ unset, the bare name must be honored.
    pt = _reimport_pool_timing({"PINGBACK_TIMEOUT": 300})
    assert pt.PINGBACK_TIMEOUT == 300
    assert pt.HEARTBEAT_INTERVAL == 25
