"""Unit tests for atm.main._handle_tick.

Covers the six cases from the arm+prime notification plan:
  1. fresh BUY arm (turquoise)
  2. fresh SELL arm (yellow)
  3. BUY prime (turquoise → dark_green)
  4. silent refresh (turquoise → turquoise)
  5. catchup synth-arm from dark_green + prime alert, audit tagged catchup:true
  6. late-start guard (light_green from IDLE with first_accepted=True)
"""
from __future__ import annotations

from types import SimpleNamespace

from atm.main import _handle_tick
from atm.notifier import Alert
from atm.state_machine import State, StateMachine


class FakeNotifier:
    """Captures alerts instead of fanning out. send() is synchronous."""

    def __init__(self) -> None:
        self.alerts: list[Alert] = []

    def send(self, alert: Alert) -> None:
        self.alerts.append(alert)


class FakeAudit:
    """Captures audit events in-memory."""

    def __init__(self) -> None:
        self.events: list[dict] = []

    def log(self, event: dict) -> None:
        self.events.append(event)


# ---------------------------------------------------------------------------
# 1. fresh BUY arm
# ---------------------------------------------------------------------------

def test_handle_tick_arm_buy():
    fsm = StateMachine(lockout_s=60)
    notif = FakeNotifier()
    audit = FakeAudit()

    tr = _handle_tick(fsm, "turquoise", 1.0, notif, audit, first_accepted=False)

    assert tr is not None
    assert tr.next == State.ARMED_BUY
    assert tr.reason == "arm"
    assert len(notif.alerts) == 1
    assert notif.alerts[0].kind == "arm"
    assert notif.alerts[0].direction == "BUY"
    assert "turquoise" in notif.alerts[0].title


# ---------------------------------------------------------------------------
# 2. fresh SELL arm
# ---------------------------------------------------------------------------

def test_handle_tick_arm_sell():
    fsm = StateMachine(lockout_s=60)
    notif = FakeNotifier()
    audit = FakeAudit()

    tr = _handle_tick(fsm, "yellow", 1.0, notif, audit, first_accepted=False)

    assert tr is not None
    assert tr.next == State.ARMED_SELL
    assert len(notif.alerts) == 1
    assert notif.alerts[0].kind == "arm"
    assert notif.alerts[0].direction == "SELL"
    assert "yellow" in notif.alerts[0].title


# ---------------------------------------------------------------------------
# 3. BUY prime — turquoise then dark_green
# ---------------------------------------------------------------------------

def test_handle_tick_prime_buy():
    fsm = StateMachine(lockout_s=60)
    notif = FakeNotifier()
    audit = FakeAudit()

    _handle_tick(fsm, "turquoise", 1.0, notif, audit, first_accepted=False)
    tr2 = _handle_tick(fsm, "dark_green", 2.0, notif, audit, first_accepted=False)

    assert tr2 is not None
    assert tr2.next == State.PRIMED_BUY
    assert len(notif.alerts) == 2
    assert notif.alerts[0].kind == "arm"
    assert notif.alerts[1].kind == "prime"
    assert notif.alerts[1].direction == "BUY"
    assert "recuperare" not in notif.alerts[1].title.lower()


# ---------------------------------------------------------------------------
# 4. refresh is silent — second turquoise in ARMED_BUY emits no alert
# ---------------------------------------------------------------------------

def test_handle_tick_refresh_no_alert():
    fsm = StateMachine(lockout_s=60)
    notif = FakeNotifier()
    audit = FakeAudit()

    _handle_tick(fsm, "turquoise", 1.0, notif, audit, first_accepted=False)
    tr2 = _handle_tick(fsm, "turquoise", 2.0, notif, audit, first_accepted=False)

    assert tr2 is not None
    assert tr2.reason == "refresh"
    assert len(notif.alerts) == 1  # only the first (real) arm


# ---------------------------------------------------------------------------
# 5. catchup synth-arm from dark_green with first_accepted=True
# ---------------------------------------------------------------------------

def test_handle_tick_catchup_dark_green():
    fsm = StateMachine(lockout_s=60)
    notif = FakeNotifier()
    audit = FakeAudit()

    tr = _handle_tick(fsm, "dark_green", 1.0, notif, audit, first_accepted=True)

    # Synth arm + real prime feed → FSM lands in PRIMED_BUY
    assert tr is not None
    assert tr.next == State.PRIMED_BUY
    assert tr.reason == "prime"

    # Two alerts: synth-arm (catchup-tagged) + prime
    assert len(notif.alerts) == 2
    assert notif.alerts[0].kind == "arm"
    assert notif.alerts[0].direction == "BUY"
    assert "recuperare" in notif.alerts[0].title.lower() or "recuperare" in notif.alerts[0].body.lower()
    assert notif.alerts[1].kind == "prime"
    assert notif.alerts[1].direction == "BUY"
    assert "recuperare" in notif.alerts[1].title.lower() or "recuperare" in notif.alerts[1].body.lower()

    # Audit: both tick entries tagged catchup:true
    catchup_events = [e for e in audit.events if e.get("catchup")]
    assert len(catchup_events) == 2
    synth = next(e for e in catchup_events if e.get("synthesized_from") == "dark_green")
    assert synth["color"] == "turquoise"
    assert synth["reason"] == "arm"


def test_handle_tick_catchup_dark_red():
    """Mirror of dark_green — SELL side."""
    fsm = StateMachine(lockout_s=60)
    notif = FakeNotifier()
    audit = FakeAudit()

    tr = _handle_tick(fsm, "dark_red", 1.0, notif, audit, first_accepted=True)

    assert tr is not None
    assert tr.next == State.PRIMED_SELL
    assert len(notif.alerts) == 2
    assert notif.alerts[0].direction == "SELL"
    assert notif.alerts[1].direction == "SELL"


# ---------------------------------------------------------------------------
# 6. late-start: light_green from IDLE with first_accepted=True
# ---------------------------------------------------------------------------

def test_handle_tick_late_start():
    fsm = StateMachine(lockout_s=60)
    notif = FakeNotifier()
    audit = FakeAudit()

    tr = _handle_tick(fsm, "light_green", 1.0, notif, audit, first_accepted=True)

    # FSM untouched
    assert tr is None
    assert fsm.state == State.IDLE

    # Exactly one late_start alert
    assert len(notif.alerts) == 1
    assert notif.alerts[0].kind == "late_start"
    assert notif.alerts[0].direction == "BUY"

    # Audit has a late_start event
    assert any(e.get("event") == "late_start" for e in audit.events)


def test_handle_tick_late_start_sell():
    """Mirror of light_green — light_red triggers SELL late_start."""
    fsm = StateMachine(lockout_s=60)
    notif = FakeNotifier()
    audit = FakeAudit()

    tr = _handle_tick(fsm, "light_red", 1.0, notif, audit, first_accepted=True)

    assert tr is None
    assert fsm.state == State.IDLE
    assert len(notif.alerts) == 1
    assert notif.alerts[0].kind == "late_start"
    assert notif.alerts[0].direction == "SELL"


# ---------------------------------------------------------------------------
# Extra guard: first_accepted=True with turquoise is a normal arm (decision 3A)
# ---------------------------------------------------------------------------

def test_handle_tick_first_turquoise_no_catchup_label():
    fsm = StateMachine(lockout_s=60)
    notif = FakeNotifier()
    audit = FakeAudit()

    tr = _handle_tick(fsm, "turquoise", 1.0, notif, audit, first_accepted=True)

    assert tr is not None
    assert tr.next == State.ARMED_BUY
    assert len(notif.alerts) == 1
    assert notif.alerts[0].kind == "arm"
    # Decision 3A: cannot distinguish fresh arm from catchup-at-arm-phase
    assert "recuperare" not in notif.alerts[0].title.lower()
    assert "recuperare" not in notif.alerts[0].body.lower()


# ---------------------------------------------------------------------------
# Regression: user bug 2026-04-16. Catchup must fire whenever IDLE + dark_* is
# observed, regardless of first_accepted. Prior gray noise tick consumed
# first_accepted and the subsequent dark_red was gated out of catchup.
# ---------------------------------------------------------------------------

def test_handle_tick_catchup_dark_red_when_not_first_accepted():
    fsm = StateMachine(lockout_s=60)
    notif = FakeNotifier()
    audit = FakeAudit()

    tr = _handle_tick(fsm, "dark_red", 1.0, notif, audit, first_accepted=False)

    assert tr is not None
    assert tr.next == State.PRIMED_SELL
    assert len(notif.alerts) == 2
    assert notif.alerts[0].kind == "arm"
    assert notif.alerts[0].direction == "SELL"
    assert "recuperare" in (notif.alerts[0].title + notif.alerts[0].body).lower()
    assert notif.alerts[1].kind == "prime"
    assert notif.alerts[1].direction == "SELL"


def test_handle_tick_catchup_dark_green_when_not_first_accepted():
    """Mirror — BUY side."""
    fsm = StateMachine(lockout_s=60)
    notif = FakeNotifier()
    audit = FakeAudit()

    tr = _handle_tick(fsm, "dark_green", 1.0, notif, audit, first_accepted=False)

    assert tr is not None
    assert tr.next == State.PRIMED_BUY
    assert len(notif.alerts) == 2
    assert notif.alerts[0].direction == "BUY"
    assert notif.alerts[1].direction == "BUY"


# ---------------------------------------------------------------------------
# Regression: user bug 2026-04-16 (image.png). After a BUY FIRE, the chart
# shows residual dark_green dots for the rest of the 15m window. Those are
# noise, NOT a new prime signal. Previously triggered false catchup arm+prime
# because FSM returns to IDLE after fire and the catchup branch only checked
# (color, state). Now gated on fsm.fired_in_session(direction).
# ---------------------------------------------------------------------------

def test_handle_tick_dark_green_after_light_green_fire_no_catchup():
    """REGRESSION: post-FIRE dark_green must NOT re-arm catchup."""
    fsm = StateMachine(lockout_s=240)
    notif = FakeNotifier()
    audit = FakeAudit()

    # Full BUY cycle: arm → prime → fire
    _handle_tick(fsm, "turquoise",   1.0, notif, audit, first_accepted=False)
    _handle_tick(fsm, "dark_green",  2.0, notif, audit, first_accepted=False)
    _handle_tick(fsm, "light_green", 3.0, notif, audit, first_accepted=False)
    # After fire: FSM is IDLE, _last_fire[BUY]=3.0
    assert fsm.state == State.IDLE
    assert fsm.fired_in_session("BUY") is True
    baseline_alerts = len(notif.alerts)  # arm + prime (fire alert is handled in main, not here)

    # Residual dark_green post-FIRE — must stay IDLE, must not fire any alert
    tr = _handle_tick(fsm, "dark_green", 10.0, notif, audit, first_accepted=False)

    assert fsm.state == State.IDLE
    assert tr is not None
    assert tr.reason == "noise"
    assert len(notif.alerts) == baseline_alerts, (
        f"post-FIRE dark_green falsely fired: "
        f"new alerts {notif.alerts[baseline_alerts:]}"
    )


def test_handle_tick_dark_red_after_light_red_fire_no_catchup():
    """REGRESSION mirror — SELL side."""
    fsm = StateMachine(lockout_s=240)
    notif = FakeNotifier()
    audit = FakeAudit()

    _handle_tick(fsm, "yellow",    1.0, notif, audit, first_accepted=False)
    _handle_tick(fsm, "dark_red",  2.0, notif, audit, first_accepted=False)
    _handle_tick(fsm, "light_red", 3.0, notif, audit, first_accepted=False)
    assert fsm.state == State.IDLE
    assert fsm.fired_in_session("SELL") is True
    baseline_alerts = len(notif.alerts)

    tr = _handle_tick(fsm, "dark_red", 10.0, notif, audit, first_accepted=False)

    assert fsm.state == State.IDLE
    assert tr is not None
    assert tr.reason == "noise"
    assert len(notif.alerts) == baseline_alerts


def test_handle_tick_opposite_direction_catchup_after_fire():
    """After BUY fires, seeing dark_red at IDLE should STILL trigger SELL
    catchup (direction-scoped gate, not session-wide). Proves the gate only
    suppresses same-direction residual, not a genuine opposite-direction cycle
    the user joined mid-way."""
    fsm = StateMachine(lockout_s=240)
    notif = FakeNotifier()
    audit = FakeAudit()

    # Fire BUY cycle
    _handle_tick(fsm, "turquoise",   1.0, notif, audit, first_accepted=False)
    _handle_tick(fsm, "dark_green",  2.0, notif, audit, first_accepted=False)
    _handle_tick(fsm, "light_green", 3.0, notif, audit, first_accepted=False)
    assert fsm.fired_in_session("BUY") is True
    assert fsm.fired_in_session("SELL") is False
    baseline_alerts = len(notif.alerts)

    # Now dark_red at IDLE — SELL hasn't fired, so catchup must still work
    tr = _handle_tick(fsm, "dark_red", 10.0, notif, audit, first_accepted=False)

    assert tr is not None
    assert tr.next == State.PRIMED_SELL
    assert tr.reason == "prime"
    # synth-arm alert + real prime alert = 2 new
    assert len(notif.alerts) == baseline_alerts + 2
    assert notif.alerts[baseline_alerts].kind == "arm"
    assert notif.alerts[baseline_alerts].direction == "SELL"
    assert "recuperare" in (notif.alerts[baseline_alerts].title + notif.alerts[baseline_alerts].body).lower()
    assert notif.alerts[baseline_alerts + 1].kind == "prime"
    assert notif.alerts[baseline_alerts + 1].direction == "SELL"


# ---------------------------------------------------------------------------
# Snapshot callable: _handle_tick should call snapshot(kind, label) for each
# alert and attach the returned path to Alert.image_path. None default keeps
# existing tests oblivious.
# ---------------------------------------------------------------------------

def test_handle_tick_snapshot_called_for_each_alert():
    """snapshot callable invoked with (kind, label); returned path attached."""
    from pathlib import Path

    fsm = StateMachine(lockout_s=60)
    notif = FakeNotifier()
    audit = FakeAudit()
    calls: list[tuple[str, str]] = []

    def snap(kind: str, label: str):
        calls.append((kind, label))
        return Path(f"/tmp/fake_{label}.png")

    # BUY cycle arm + prime (2 alerts, 2 snapshot calls)
    _handle_tick(fsm, "turquoise",  1.0, notif, audit, first_accepted=False, snapshot=snap)
    _handle_tick(fsm, "dark_green", 2.0, notif, audit, first_accepted=False, snapshot=snap)

    assert len(notif.alerts) == 2
    assert calls == [("arm", "arm_buy"), ("prime", "prime_buy")]
    assert notif.alerts[0].image_path == Path("/tmp/fake_arm_buy.png")
    assert notif.alerts[1].image_path == Path("/tmp/fake_prime_buy.png")


def test_handle_tick_snapshot_none_for_gated_kind():
    """snapshot() returning None (config-gated off) → Alert.image_path=None,
    alert still sends text-only."""
    fsm = StateMachine(lockout_s=60)
    notif = FakeNotifier()
    audit = FakeAudit()

    def snap(kind: str, label: str):
        # Simulate cfg.attach_screenshots.arm = False
        return None if kind == "arm" else __import__("pathlib").Path(f"/tmp/{label}.png")

    _handle_tick(fsm, "turquoise",  1.0, notif, audit, first_accepted=False, snapshot=snap)
    _handle_tick(fsm, "dark_green", 2.0, notif, audit, first_accepted=False, snapshot=snap)

    assert notif.alerts[0].image_path is None         # arm gated off
    assert notif.alerts[1].image_path is not None     # prime went through


def test_handle_tick_snapshot_called_for_late_start():
    """late_start path also invokes snapshot."""
    fsm = StateMachine(lockout_s=60)
    notif = FakeNotifier()
    audit = FakeAudit()
    calls: list[tuple[str, str]] = []

    def snap(kind: str, label: str):
        calls.append((kind, label))
        return None

    _handle_tick(fsm, "light_green", 1.0, notif, audit, first_accepted=True, snapshot=snap)

    assert len(notif.alerts) == 1
    assert notif.alerts[0].kind == "late_start"
    assert calls == [("late_start", "late_start_buy")]


def test_handle_tick_snapshot_called_for_catchup_prime():
    """catchup path: arm snapshot uses kind=catchup, prime snapshot uses
    kind=catchup (so user's catchup toggle also gates the catchup prime)."""
    fsm = StateMachine(lockout_s=60)
    notif = FakeNotifier()
    audit = FakeAudit()
    calls: list[tuple[str, str]] = []

    def snap(kind: str, label: str):
        calls.append((kind, label))
        return None

    _handle_tick(fsm, "dark_red", 1.0, notif, audit, first_accepted=False, snapshot=snap)

    # Synth-arm catchup alert + real prime alert (post-synth) — both tagged catchup
    assert len(notif.alerts) == 2
    assert calls == [("catchup", "catchup_arm_sell"), ("catchup", "prime_sell_catchup")]


# ---------------------------------------------------------------------------
# _save_annotated_frame — audit-log failures instead of swallowing silently.
# ---------------------------------------------------------------------------

def test_save_annotated_frame_logs_audit_on_failure(tmp_path, monkeypatch):
    """cv2.imwrite raising → return None AND audit.log event=snapshot_fail."""
    import atm.main as main_mod

    # Force the lazy cv2 import to succeed but fail on imwrite
    class _FakeCv2:
        @staticmethod
        def rectangle(*a, **kw): pass
        @staticmethod
        def imwrite(*a, **kw):
            raise OSError("disk full")

    import sys
    monkeypatch.setitem(sys.modules, "cv2", _FakeCv2)

    cfg = type("C", (), {"dot_roi": type("R", (), {"x": 0, "y": 0, "w": 10, "h": 10})()})()
    frame = type("F", (), {"copy": lambda self: self})()  # dummy with .copy()
    audit = FakeAudit()

    result = main_mod._save_annotated_frame(frame, cfg, tmp_path, "test_label", 123.0, audit=audit)

    assert result is None
    assert any(e.get("event") == "snapshot_fail" and e.get("label") == "test_label"
               for e in audit.events)


def test_save_annotated_frame_succeeds(tmp_path, monkeypatch):
    """Happy path: cv2 present + imwrite succeeds → returns path."""
    import atm.main as main_mod

    written: list[str] = []

    class _FakeCv2:
        @staticmethod
        def rectangle(*a, **kw): pass
        @staticmethod
        def imwrite(path, _img):
            written.append(path)

    import sys
    monkeypatch.setitem(sys.modules, "cv2", _FakeCv2)

    cfg = type("C", (), {"dot_roi": type("R", (), {"x": 0, "y": 0, "w": 10, "h": 10})()})()
    frame = type("F", (), {"copy": lambda self: self})()
    audit = FakeAudit()

    result = main_mod._save_annotated_frame(frame, cfg, tmp_path, "BUY", 1700000000.0, audit=audit)

    assert result is not None
    assert result.parent == tmp_path
    assert "BUY" in result.name
    assert len(written) == 1
    assert not any(e.get("event") == "snapshot_fail" for e in audit.events)


# ---------------------------------------------------------------------------
# Commit 3: fire_on_phase_skip backstop
# ---------------------------------------------------------------------------

def _cfg_with_flag(enabled: bool):
    return SimpleNamespace(alerts=SimpleNamespace(fire_on_phase_skip=enabled))


def test_phase_skip_fire_when_flag_on():
    """ARMED_SELL → light_red directly with flag=True → phase_skip_fire alert."""
    fsm = StateMachine(lockout_s=240)
    notif = FakeNotifier()
    audit = FakeAudit()

    # Arm SELL (yellow from IDLE)
    _handle_tick(fsm, "yellow", 1.0, notif, audit, first_accepted=False,
                 cfg=_cfg_with_flag(True))
    assert fsm.state == State.ARMED_SELL
    notif.alerts.clear()

    # ARMED_SELL → light_red (skips dark_red) → phase_skip_fire
    tr = _handle_tick(fsm, "light_red", 2.0, notif, audit, first_accepted=False,
                      cfg=_cfg_with_flag(True))
    assert tr is not None and tr.reason == "phase_skip"

    ps_alerts = [a for a in notif.alerts if a.kind == "phase_skip_fire"]
    assert len(ps_alerts) == 1
    assert ps_alerts[0].direction == "SELL"
    assert "SELL" in ps_alerts[0].title


def test_phase_skip_no_fire_when_flag_off():
    """Same scenario, flag=False → no phase_skip_fire emitted."""
    fsm = StateMachine(lockout_s=240)
    notif = FakeNotifier()
    audit = FakeAudit()

    _handle_tick(fsm, "yellow", 1.0, notif, audit, first_accepted=False,
                 cfg=_cfg_with_flag(False))
    notif.alerts.clear()

    _handle_tick(fsm, "light_red", 2.0, notif, audit, first_accepted=False,
                 cfg=_cfg_with_flag(False))

    ps_alerts = [a for a in notif.alerts if a.kind == "phase_skip_fire"]
    assert ps_alerts == []


def test_phase_skip_lockout_suppresses_spam():
    """Two phase_skip events within lockout_s → only the first emits an alert."""
    fsm = StateMachine(lockout_s=240)
    notif = FakeNotifier()
    audit = FakeAudit()
    cfg = _cfg_with_flag(True)

    # First cycle
    _handle_tick(fsm, "yellow", 1.0, notif, audit, first_accepted=False, cfg=cfg)
    _handle_tick(fsm, "light_red", 2.0, notif, audit, first_accepted=False, cfg=cfg)
    # Second arm + phase_skip well within 240s
    _handle_tick(fsm, "yellow", 60.0, notif, audit, first_accepted=False, cfg=cfg)
    _handle_tick(fsm, "light_red", 61.0, notif, audit, first_accepted=False, cfg=cfg)

    ps_alerts = [a for a in notif.alerts if a.kind == "phase_skip_fire"]
    assert len(ps_alerts) == 1, (
        f"expected 1 phase_skip_fire (lockout), got {len(ps_alerts)}"
    )


def test_state_machine_is_locked_and_record_fire_public_api():
    """Public lockout helpers mirror the private _is_locked / _last_fire behavior."""
    fsm = StateMachine(lockout_s=100)
    assert fsm.is_locked("BUY", 0.0) is False

    fsm.record_fire("BUY", 10.0)
    assert fsm.is_locked("BUY", 50.0) is True   # within 100s
    assert fsm.is_locked("BUY", 150.0) is False  # past lockout
    assert fsm.is_locked("SELL", 50.0) is False  # other direction unaffected


# ---------------------------------------------------------------------------
# opposite_rearm — bug observat 2026-04-21 17:45
# PRIMED_BUY + yellow → ARMED_SELL; reason=opposite_rearm; must emit alert
# with screenshot attached. CRITICAL regression.
# ---------------------------------------------------------------------------

def test_opposite_rearm_primed_buy_to_armed_sell_emits_alert():
    """REGRESSION (2026-04-21): PRIMED_BUY + yellow → ARMED_SELL silently."""
    from pathlib import Path

    fsm = StateMachine(lockout_s=60)
    notif = FakeNotifier()
    audit = FakeAudit()

    def snap(kind, label):
        return Path(f"/tmp/{label}.png")

    # Drive to PRIMED_BUY
    _handle_tick(fsm, "turquoise",  1.0, notif, audit, first_accepted=False, snapshot=snap)
    _handle_tick(fsm, "dark_green", 2.0, notif, audit, first_accepted=False, snapshot=snap)
    assert fsm.state == State.PRIMED_BUY
    notif.alerts.clear()

    # Yellow → ARMED_SELL via opposite_rearm
    tr = _handle_tick(fsm, "yellow", 3.0, notif, audit, first_accepted=False, snapshot=snap)

    assert tr is not None
    assert tr.next == State.ARMED_SELL
    assert tr.reason == "opposite_rearm"
    assert len(notif.alerts) == 1
    a = notif.alerts[0]
    assert a.kind == "opposite_rearm"
    assert a.direction == "SELL"
    assert "yellow" in a.title
    assert "opus" in a.title.lower()
    assert a.image_path == Path("/tmp/opposite_rearm_sell.png")


def test_opposite_rearm_primed_sell_to_armed_buy_emits_alert():
    """Mirror: PRIMED_SELL + turquoise → ARMED_BUY."""
    fsm = StateMachine(lockout_s=60)
    notif = FakeNotifier()
    audit = FakeAudit()

    _handle_tick(fsm, "yellow",   1.0, notif, audit, first_accepted=False)
    _handle_tick(fsm, "dark_red", 2.0, notif, audit, first_accepted=False)
    assert fsm.state == State.PRIMED_SELL
    notif.alerts.clear()

    tr = _handle_tick(fsm, "turquoise", 3.0, notif, audit, first_accepted=False)

    assert tr is not None
    assert tr.next == State.ARMED_BUY
    assert tr.reason == "opposite_rearm"
    assert len(notif.alerts) == 1
    assert notif.alerts[0].kind == "opposite_rearm"
    assert notif.alerts[0].direction == "BUY"


def test_opposite_rearm_armed_buy_to_armed_sell_emits_alert():
    """Flip direct ARMED_BUY → ARMED_SELL (fără prime între)."""
    fsm = StateMachine(lockout_s=60)
    notif = FakeNotifier()
    audit = FakeAudit()

    _handle_tick(fsm, "turquoise", 1.0, notif, audit, first_accepted=False)
    assert fsm.state == State.ARMED_BUY
    notif.alerts.clear()

    tr = _handle_tick(fsm, "yellow", 2.0, notif, audit, first_accepted=False)

    assert tr is not None
    assert tr.next == State.ARMED_SELL
    assert tr.reason == "opposite_rearm"
    assert len(notif.alerts) == 1
    assert notif.alerts[0].kind == "opposite_rearm"
    assert notif.alerts[0].direction == "SELL"


# ---------------------------------------------------------------------------
# rearm — PRIMED_* + arm-color aceeași direcție → ARMED_* (reset ciclu)
# ---------------------------------------------------------------------------

def test_rearm_primed_buy_to_armed_buy_emits_alert():
    from pathlib import Path

    fsm = StateMachine(lockout_s=60)
    notif = FakeNotifier()
    audit = FakeAudit()

    def snap(kind, label):
        return Path(f"/tmp/{label}.png")

    _handle_tick(fsm, "turquoise",  1.0, notif, audit, first_accepted=False, snapshot=snap)
    _handle_tick(fsm, "dark_green", 2.0, notif, audit, first_accepted=False, snapshot=snap)
    assert fsm.state == State.PRIMED_BUY
    notif.alerts.clear()

    tr = _handle_tick(fsm, "turquoise", 3.0, notif, audit, first_accepted=False, snapshot=snap)

    assert tr is not None
    assert tr.next == State.ARMED_BUY
    assert tr.reason == "rearm"
    assert len(notif.alerts) == 1
    a = notif.alerts[0]
    assert a.kind == "rearm"
    assert a.direction == "BUY"
    assert "reluat" in a.title.lower()
    assert a.image_path == Path("/tmp/rearm_buy.png")


def test_rearm_primed_sell_to_armed_sell_emits_alert():
    fsm = StateMachine(lockout_s=60)
    notif = FakeNotifier()
    audit = FakeAudit()

    _handle_tick(fsm, "yellow",   1.0, notif, audit, first_accepted=False)
    _handle_tick(fsm, "dark_red", 2.0, notif, audit, first_accepted=False)
    assert fsm.state == State.PRIMED_SELL
    notif.alerts.clear()

    tr = _handle_tick(fsm, "yellow", 3.0, notif, audit, first_accepted=False)

    assert tr is not None
    assert tr.next == State.ARMED_SELL
    assert tr.reason == "rearm"
    assert len(notif.alerts) == 1
    assert notif.alerts[0].kind == "rearm"
    assert notif.alerts[0].direction == "SELL"


# ---------------------------------------------------------------------------
# _emit_arm_alert helper — unit test
# ---------------------------------------------------------------------------

def test_emit_arm_alert_helper_builds_expected_alert():
    from pathlib import Path
    from atm.main import _emit_arm_alert

    notif = FakeNotifier()
    calls: list[tuple[str, str]] = []

    def snap(kind, label):
        calls.append((kind, label))
        return Path(f"/tmp/{label}.png")

    _emit_arm_alert(
        notif,
        kind="opposite_rearm",
        direction="SELL",
        now=1700000000.0,
        title="SELL re-armat (yellow) — ciclu opus",
        snap=snap,
        snap_kind="opposite_rearm",
        snap_label="opposite_rearm_sell",
    )

    assert len(notif.alerts) == 1
    a = notif.alerts[0]
    assert a.kind == "opposite_rearm"
    assert a.direction == "SELL"
    assert a.title == "SELL re-armat (yellow) — ciclu opus"
    assert a.image_path == Path("/tmp/opposite_rearm_sell.png")
    assert calls == [("opposite_rearm", "opposite_rearm_sell")]
