from pathlib import Path
import sys


PROJECT_ROOT = Path(__file__).resolve().parents[1]
if str(PROJECT_ROOT) not in sys.path:
    sys.path.insert(0, str(PROJECT_ROOT))

from target_handoff import (
    HandoffDecision,
    compute_fast_handoff_hits,
    evaluate_stale_lock,
    pick_guidance_override_box,
    should_override_guidance,
    update_guidance_override_latch,
)


def test_evaluate_stale_lock_requires_fresh_candidate_and_weak_anchor():
    decision = evaluate_stale_lock(
        confirmed=True,
        have_fresh_candidate=True,
        target_has_fresh_update=False,
        candidate_dist_px=420.0,
        pred_diag_px=70.0,
        miss_streak=8,
        klt_valid=False,
        klt_quality=0.18,
        klt_iou=0.12,
        candidate_motion_ok=True,
        target_motion_ok=False,
        stale_break_dist_diag=3.5,
        stale_break_min_miss=2,
        stale_break_klt_quality=0.35,
        stale_break_klt_iou=0.25,
    )

    assert decision.stale_lock_active is True
    assert "target_not_fresh" in decision.reasons
    assert "weak_klt_quality" in decision.reasons


def test_evaluate_stale_lock_rejects_close_candidate_when_anchor_is_healthy():
    decision = evaluate_stale_lock(
        confirmed=True,
        have_fresh_candidate=True,
        target_has_fresh_update=True,
        candidate_dist_px=90.0,
        pred_diag_px=70.0,
        miss_streak=0,
        klt_valid=True,
        klt_quality=0.92,
        klt_iou=0.71,
        candidate_motion_ok=True,
        target_motion_ok=True,
        stale_break_dist_diag=3.5,
        stale_break_min_miss=2,
        stale_break_klt_quality=0.35,
        stale_break_klt_iou=0.25,
    )

    assert decision.stale_lock_active is False
    assert decision.reasons == []


def test_compute_fast_handoff_hits_reduces_confirmation_under_stale_lock():
    hits = compute_fast_handoff_hits(
        stale_lock_active=True,
        motion_switch_mode=False,
        approach_extra_hits=2,
        fast_maneuver_extra_hits=2,
        default_switch_hits=4,
        fast_handoff_hits=2,
        motion_switch_hits=6,
    )

    assert hits == 2


def test_handoff_decision_repr_is_stable_for_logging():
    decision = HandoffDecision(
        stale_lock_active=True,
        allow_fast_handoff=True,
        required_switch_hits=2,
        disable_prediction_hold=True,
        reasons=["target_not_fresh", "weak_klt_quality"],
    )

    assert decision.reason_text() == "target_not_fresh|weak_klt_quality"


def test_should_override_guidance_when_target_is_missing_but_fresh_candidate_exists():
    assert should_override_guidance(
        confirmed=True,
        target_track_missing=True,
        have_fresh_candidate=True,
        candidate_matches_target=False,
        miss_streak=10,
        override_miss_ge=2,
    )


def test_should_not_override_guidance_before_miss_threshold():
    assert not should_override_guidance(
        confirmed=True,
        target_track_missing=True,
        have_fresh_candidate=True,
        candidate_matches_target=False,
        miss_streak=1,
        override_miss_ge=2,
    )


def test_pick_guidance_override_box_falls_back_to_raw_yolo_box():
    yolo_box = [100.0, 120.0, 180.0, 200.0]

    picked = pick_guidance_override_box(
        track_candidate_box=None,
        raw_yolo_box=yolo_box,
    )

    assert picked == yolo_box


def test_update_guidance_override_latch_keeps_recent_box_when_new_candidate_is_missing():
    prev_box = [100.0, 120.0, 180.0, 200.0]

    latched_box, ttl = update_guidance_override_latch(
        new_box=None,
        prev_box=prev_box,
        prev_ttl=2,
        max_ttl=2,
    )

    assert latched_box == prev_box
    assert ttl == 1


def test_update_guidance_override_latch_replaces_memory_when_new_candidate_arrives():
    new_box = [300.0, 320.0, 380.0, 400.0]

    latched_box, ttl = update_guidance_override_latch(
        new_box=new_box,
        prev_box=None,
        prev_ttl=0,
        max_ttl=2,
    )

    assert latched_box == new_box
    assert ttl == 2
