"""Coverage for :mod:`decnet.realism.diurnal`.

Two functions to exercise:

* :func:`in_work_hours` — straightforward window membership including
  the wrap-around (``22:00-06:00``) case and the fail-open behaviour
  on malformed windows.
* :func:`sample_mtime` — must (a) return a ``datetime`` strictly in
  the past, (b) clip to the configured backdate cap, and (c) snap the
  hour-of-day into the persona's window when the unconstrained
  candidate would land outside.
"""
from __future__ import annotations

import random
from datetime import datetime, timedelta, timezone

import pytest

from decnet.realism.diurnal import in_work_hours, sample_mtime


# Fixed 'now' for reproducible tests — Monday 2026-04-27 14:00 UTC.
_NOW = datetime(2026, 4, 27, 14, 0, tzinfo=timezone.utc)


# ---- in_work_hours -----------------------------------------------------

@pytest.mark.parametrize(
    "now_hour,now_min,window,expected",
    [
        (10, 0, "09:00-18:00", True),
        (8, 59, "09:00-18:00", False),
        (9, 0, "09:00-18:00", True),
        (18, 0, "09:00-18:00", False),       # exclusive end
        (17, 59, "09:00-18:00", True),
        (23, 30, "22:00-06:00", True),       # wrap-around: late
        (3, 0, "22:00-06:00", True),         # wrap-around: early
        (12, 0, "22:00-06:00", False),       # wrap-around: middle of day
    ],
)
def test_in_work_hours_window_membership(
    now_hour: int, now_min: int, window: str, expected: bool,
) -> None:
    now = _NOW.replace(hour=now_hour, minute=now_min)
    assert in_work_hours(window, now) is expected


def test_in_work_hours_equal_start_end_means_always_on() -> None:
    # A persona pegged "00:00-00:00" should never be silenced by the
    # diurnal gate — interpreted as "no schedule".
    assert in_work_hours("00:00-00:00", _NOW) is True


@pytest.mark.parametrize(
    "garbage",
    ["not-a-window", "9-18", "09:00", "25:00-26:00", "09:00-18:99", ""],
)
def test_malformed_window_fails_open(garbage: str) -> None:
    # The fleet must not silence on a typo — same fail-open semantics
    # as decnet.realism.personas.in_active_hours.
    assert in_work_hours(garbage, _NOW) is True


# ---- sample_mtime ------------------------------------------------------

def test_sample_mtime_is_in_the_past() -> None:
    rng = random.Random(0)
    for _ in range(20):
        mt = sample_mtime("09:00-18:00", _NOW, rand=rng)
        assert mt < _NOW, f"sample_mtime returned future: {mt} >= {_NOW}"


def test_sample_mtime_respects_backdate_cap() -> None:
    rng = random.Random(0)
    cap_days = 7.0
    for _ in range(50):
        mt = sample_mtime(
            "09:00-18:00", _NOW, rand=rng,
            backdate_max_days=cap_days, backdate_min_hours=0.5,
        )
        assert _NOW - mt <= timedelta(days=cap_days) + timedelta(hours=1)
        assert _NOW - mt >= timedelta(hours=0.5) - timedelta(seconds=1)


def test_sample_mtime_snaps_hour_into_window() -> None:
    # Force a tight window then assert the hour-of-day is always in it.
    rng = random.Random(42)
    window = "09:00-18:00"
    for _ in range(60):
        mt = sample_mtime(window, _NOW, rand=rng)
        assert 9 <= mt.hour < 18, (
            f"hour {mt.hour} fell outside {window} on {mt.isoformat()}"
        )


def test_sample_mtime_handles_wrap_around_window() -> None:
    rng = random.Random(123)
    for _ in range(40):
        mt = sample_mtime("22:00-06:00", _NOW, rand=rng)
        assert mt.hour >= 22 or mt.hour < 6, (
            f"hour {mt.hour} fell outside wrap window on {mt.isoformat()}"
        )


def test_sample_mtime_malformed_window_does_not_snap() -> None:
    # When the window can't be parsed, just return the unconstrained
    # backdate. Belt-and-braces: shouldn't crash, shouldn't future-stamp.
    rng = random.Random(0)
    mt = sample_mtime("garbage", _NOW, rand=rng)
    assert mt < _NOW


def test_sample_mtime_is_deterministic_per_seed() -> None:
    # The diurnal sampler accepts a Random — pinning the seed must
    # produce stable output, otherwise tests can't assert anything
    # tighter than "returns a datetime in the past."
    a = sample_mtime("09:00-18:00", _NOW, rand=random.Random(7))
    b = sample_mtime("09:00-18:00", _NOW, rand=random.Random(7))
    assert a == b
