""" tests/test_quick_pick.py ------------------------ Tests for the quick_pick strategy and the exclude parameter added to all predictor strategies in Phase 17. """ import pytest from db.models import get_game_by_name, add_game, insert_draw from core.predictor import ( quick_pick, hot_numbers, due_numbers, weighted_random, monte_carlo, positional_pick, ) @pytest.fixture def pb(tmp_db): game = get_game_by_name("Powerball") insert_draw(game["id"], "2024-01-01", [1, 13, 36, 61, 69], bonus=7) insert_draw(game["id"], "2024-01-03", [1, 7, 13, 45, 61], bonus=15) insert_draw(game["id"], "2024-01-05", [2, 13, 22, 36, 55], bonus=3) return game # ── quick_pick — basic validity ─────────────────────────────────────────────── def test_quick_pick_count(pb): assert len(quick_pick(pb["id"])["numbers"]) == pb["main_count"] def test_quick_pick_range(pb): result = quick_pick(pb["id"]) assert all(1 <= n <= pb["main_max"] for n in result["numbers"]) def test_quick_pick_no_duplicates(pb): nums = quick_pick(pb["id"])["numbers"] assert len(nums) == len(set(nums)) def test_quick_pick_sorted(pb): nums = quick_pick(pb["id"])["numbers"] assert nums == sorted(nums) def test_quick_pick_bonus_in_range(pb): bonus = quick_pick(pb["id"])["bonus"] assert bonus is not None assert 1 <= bonus <= pb["bonus_max"] def test_quick_pick_empty_db(tmp_db): game = get_game_by_name("Powerball") result = quick_pick(game["id"]) assert len(result["numbers"]) == 5 assert all(1 <= n <= 69 for n in result["numbers"]) def test_quick_pick_no_bonus_game(tmp_db): gid = add_game("NoBonusLotto", 6, 49, bonus_count=0, bonus_max=0) result = quick_pick(gid) assert result["bonus"] is None assert len(result["numbers"]) == 6 assert all(1 <= n <= 49 for n in result["numbers"]) # ── quick_pick — exclude parameter ─────────────────────────────────────────── def test_quick_pick_exclude_numbers(pb): excl = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10} result = quick_pick(pb["id"], exclude=excl) assert not any(n in excl for n in result["numbers"]) def test_quick_pick_exclude_tight(pb): # Exclude all but exactly main_count numbers — should still pick valid ticket excl = set(range(1, 65)) # leaves 65–69 (exactly 5 for Powerball) result = quick_pick(pb["id"], exclude=excl) assert len(result["numbers"]) == 5 assert all(n >= 65 for n in result["numbers"]) def test_quick_pick_exclude_too_many_falls_back(pb): # Exclude so many that not enough remain — silently falls back excl = set(range(1, 69)) # only [69] left, need 5 result = quick_pick(pb["id"], exclude=excl) assert len(result["numbers"]) == 5 # must always return a full ticket def test_quick_pick_exclude_empty_set(pb): result = quick_pick(pb["id"], exclude=set()) assert len(result["numbers"]) == 5 def test_quick_pick_exclude_none(pb): result = quick_pick(pb["id"], exclude=None) assert len(result["numbers"]) == 5 # ── exclude parameter — all existing strategies ─────────────────────────────── def test_hot_numbers_exclude(pb): excl = {1, 13, 61} # the most frequent numbers in our fixture result = hot_numbers(pb["id"], exclude=excl) assert not any(n in excl for n in result["numbers"]) assert len(result["numbers"]) == 5 def test_due_numbers_exclude(pb): excl = {36, 69} result = due_numbers(pb["id"], exclude=excl) assert not any(n in excl for n in result["numbers"]) assert len(result["numbers"]) == 5 def test_weighted_random_exclude(pb): excl = {1, 7, 13, 22, 36, 45, 55, 61, 69} result = weighted_random(pb["id"], exclude=excl) assert not any(n in excl for n in result["numbers"]) assert len(result["numbers"]) == 5 def test_monte_carlo_exclude(pb): excl = {1, 13} result = monte_carlo(pb["id"], simulations=200, exclude=excl) assert not any(n in excl for n in result["numbers"]) assert len(result["numbers"]) == 5 def test_positional_pick_exclude(pb): excl = {1, 2, 3, 4, 5} result = positional_pick(pb["id"], exclude=excl) assert not any(n in excl for n in result["numbers"]) assert len(result["numbers"]) == 5 def test_exclude_none_unchanged(pb): # All strategies accept exclude=None without breaking for fn in (hot_numbers, due_numbers, weighted_random, positional_pick): result = fn(pb["id"], exclude=None) assert len(result["numbers"]) == 5 def test_exclude_empty_set_unchanged(pb): for fn in (hot_numbers, due_numbers, weighted_random, positional_pick): result = fn(pb["id"], exclude=set()) assert len(result["numbers"]) == 5 def test_monte_carlo_exclude_none(pb): result = monte_carlo(pb["id"], simulations=100, exclude=None) assert len(result["numbers"]) == 5