"""Image-backed regression scenarios.

Each scenario in `calibration/scenarios.json` is a sequence of real PNG frames
fed through the full Detector → _handle_tick pipeline. Asserts per step:
  - detector classifies the exact expected color (accepted=True)
  - FSM transition reason/state + trigger match
  - notifier receives exactly the expected new alert kinds
  - scheduler-running flag (mirroring _handle_fsm_result) matches

Frames live in calibration/frames/ (self-contained, survives logs/fires/ purges).
"""
from __future__ import annotations

import json
from pathlib import Path

import cv2
import pytest

from atm.config import Config
from atm.detector import Detector
from atm.main import _handle_tick
from atm.state_machine import StateMachine

from tests.test_handle_tick import FakeNotifier, FakeAudit


_SCENARIOS_PATH = Path("calibration/scenarios.json")
_CONFIGS_DIR = Path("configs")

# Reasons that stop the screenshot scheduler (mirrors main.py:_handle_fsm_result).
_SCHEDULER_STOP_REASONS = {"fire", "cooled", "phase_skip", "opposite_rearm"}


def _load_scenarios() -> list[dict]:
    return json.loads(_SCENARIOS_PATH.read_text(encoding="utf-8"))


@pytest.fixture(scope="module")
def cfg() -> Config:
    return Config.load_current(_CONFIGS_DIR)


@pytest.mark.parametrize(
    "scenario", _load_scenarios(), ids=lambda s: s["id"]
)
def test_scenario(scenario: dict, cfg: Config) -> None:
    fsm = StateMachine(lockout_s=cfg.lockout_s)
    notif = FakeNotifier()
    audit = FakeAudit()
    detector = Detector(cfg=cfg, capture=lambda: None)
    scheduler_running = False
    first_accepted = True

    for i, step in enumerate(scenario["steps"]):
        frame_path = Path(step["frame"])
        assert frame_path.exists(), f"{scenario['id']}[{i}]: missing frame {frame_path}"
        frame = cv2.imread(str(frame_path))
        assert frame is not None, f"{scenario['id']}[{i}]: cv2.imread failed"

        res = detector.step(ts=float(i), frame=frame)
        assert res.accepted, (
            f"{scenario['id']}[{i}]: detector rejected {frame_path.name} "
            f"(match={res.match.name if res.match else None}, "
            f"d={res.match.distance if res.match else None}, rgb={res.rgb})"
        )
        assert res.color == step["expected_color"], (
            f"{scenario['id']}[{i}]: color mismatch — expected "
            f"{step['expected_color']}, got {res.color}"
        )

        alerts_before = len(notif.alerts)
        tr = _handle_tick(
            fsm, res.color, float(i), notif, audit,
            first_accepted=first_accepted, cfg=cfg,
        )
        first_accepted = False

        assert tr is not None, f"{scenario['id']}[{i}]: _handle_tick returned None"
        assert tr.reason == step["expected_reason"], (
            f"{scenario['id']}[{i}]: reason mismatch — expected "
            f"{step['expected_reason']}, got {tr.reason}"
        )
        assert tr.next.value == step["expected_state"], (
            f"{scenario['id']}[{i}]: state mismatch — expected "
            f"{step['expected_state']}, got {tr.next.value}"
        )
        assert tr.trigger == step["expected_trigger"], (
            f"{scenario['id']}[{i}]: trigger mismatch — expected "
            f"{step['expected_trigger']}, got {tr.trigger}"
        )

        new_alerts = [a.kind for a in notif.alerts[alerts_before:]]
        assert new_alerts == step["expected_new_alerts"], (
            f"{scenario['id']}[{i}]: alert mismatch — expected "
            f"{step['expected_new_alerts']}, got {new_alerts}"
        )

        # Scheduler lifecycle (mirrors _handle_fsm_result main.py:953-957)
        if tr.reason == "prime" and not scheduler_running:
            scheduler_running = True
        elif tr.reason in _SCHEDULER_STOP_REASONS and scheduler_running:
            scheduler_running = False
        # Also stops on trigger fire (main.py:960-964)
        if tr.trigger and not tr.locked and scheduler_running:
            scheduler_running = False

        assert scheduler_running == step["expected_scheduler_running"], (
            f"{scenario['id']}[{i}]: scheduler_running mismatch — expected "
            f"{step['expected_scheduler_running']}, got {scheduler_running}"
        )
