""" tests/test_filters.py --------------------- Tests for core/filters.py and the filter integration in core/predictor.py. """ import pytest from db.models import get_game_by_name, insert_draw from core.filters import _has_consecutive_run, sum_range_percentiles, passes_filters from core.predictor import ( hot_numbers, due_numbers, weighted_random, monte_carlo, positional_pick, quick_pick, _swap_filter, _retry_filter, ) # ── _has_consecutive_run ────────────────────────────────────────────────────── def test_no_consecutive(): assert _has_consecutive_run([1, 5, 10, 20, 30]) is False def test_run_of_three_not_flagged(): assert _has_consecutive_run([1, 2, 3, 10, 20]) is False # run=3, threshold=4 def test_run_of_four_flagged(): assert _has_consecutive_run([1, 2, 3, 4, 20]) is True def test_run_of_five_flagged(): assert _has_consecutive_run([10, 11, 12, 13, 14]) is True def test_run_at_end(): assert _has_consecutive_run([5, 20, 30, 31, 32, 33]) is True def test_run_not_contiguous(): assert _has_consecutive_run([1, 3, 5, 7, 9]) is False # ── passes_filters — odd/even ───────────────────────────────────────────────── def test_all_even_rejected(): assert passes_filters([2, 14, 28, 42, 60]) is False def test_all_odd_rejected(): assert passes_filters([1, 7, 13, 29, 69]) is False def test_mixed_odd_even_accepted(): assert passes_filters([1, 14, 28, 42, 60]) is True def test_all_even_under_4_accepted(): # Odd/even filter only kicks in for 4+ numbers assert passes_filters([2, 4, 6]) is True # ── passes_filters — consecutive ───────────────────────────────────────────── def test_four_consecutive_rejected(): assert passes_filters([5, 6, 7, 8, 20]) is False def test_three_consecutive_accepted(): assert passes_filters([5, 6, 7, 20, 35]) is True # ── passes_filters — sum range ──────────────────────────────────────────────── def test_sum_in_range_accepted(): assert passes_filters([1, 14, 28, 42, 60], sum_range=(100, 200)) is True def test_sum_below_range_rejected(): assert passes_filters([1, 2, 3, 4, 10], sum_range=(100, 200)) is False def test_sum_above_range_rejected(): assert passes_filters([60, 62, 64, 66, 69], sum_range=(100, 200)) is False def test_sum_range_none_skips_check(): # All-even but sum would be out of range if range were set — only even check applies assert passes_filters([2, 4, 6, 8, 10], sum_range=None) is False # all-even fails def test_sum_at_boundary_accepted(): assert passes_filters([1, 14, 28, 42, 15], sum_range=(100, 100)) is True # sum=100 # ── sum_range_percentiles ───────────────────────────────────────────────────── def test_sum_range_none_with_few_draws(tmp_db): pb = get_game_by_name("Powerball") for i in range(5): insert_draw(pb["id"], f"2024-01-{i+1:02d}", [1, 2, 3, 4, i+5], bonus=1) result = sum_range_percentiles(pb["id"]) assert result is None # < 10 draws def test_sum_range_returns_tuple_with_enough_draws(tmp_db): pb = get_game_by_name("Powerball") for i in range(20): insert_draw(pb["id"], f"2024-02-{i+1:02d}", [1+i, 2+i, 3+i, 4+i, 5+i], bonus=1) result = sum_range_percentiles(pb["id"]) assert result is not None lo, hi = result assert lo < hi assert isinstance(lo, int) assert isinstance(hi, int) def test_sum_range_bounds_reasonable(tmp_db): pb = get_game_by_name("Powerball") # Insert draws with sums ranging 15–25 for i in range(15, 26): insert_draw(pb["id"], f"2024-03-{i-14:02d}", [1, 2, 3, 4, i-10], bonus=1) lo, hi = sum_range_percentiles(pb["id"]) assert lo >= 10 # 10th pct of sums around 15 assert hi <= 25 # 90th pct of sums around 25 # ── _swap_filter ────────────────────────────────────────────────────────────── def test_swap_filter_passes_already(): numbers = [1, 14, 28, 42, 60] assert _swap_filter(numbers, [2, 3, 5], None) == numbers def test_swap_filter_fixes_all_even(): # all-even → swap last (weakest-ranked) for an odd numbers = [2, 14, 28, 42, 60] pool = [1, 3, 5, 7, 9] # odd alternates result = _swap_filter(numbers, pool, None) assert passes_filters(result), f"Expected filtered result, got {result}" assert len(result) == 5 assert len(set(result)) == 5 def test_swap_filter_fixes_four_consecutive(): numbers = [10, 11, 12, 13, 25] pool = [1, 5, 8, 20, 30, 35] result = _swap_filter(numbers, pool, None) assert passes_filters(result), f"Expected filtered result, got {result}" def test_swap_filter_graceful_fallback(): # If no swap fixes the ticket, return original numbers = [2, 4, 6, 8, 10] # all-even pool = [12, 14, 16] # all-even alternates — can't fix result = _swap_filter(numbers, pool, None) assert result == numbers # graceful fallback # ── Filter integration in strategies ───────────────────────────────────────── def _large_draw_set(game_id): """Insert 30 varied Powerball draws so sum_range_percentiles returns a range.""" import random as rng rng.seed(42) from db.models import get_game_by_id game = get_game_by_id(game_id) for i in range(30): nums = sorted(rng.sample(range(1, game["main_max"] + 1), game["main_count"])) bonus = rng.randint(1, game["bonus_max"]) if game["bonus_count"] > 0 else None insert_draw(game_id, f"2020-{(i//30)+1:02d}-{(i%28)+1:02d}", nums, bonus=bonus) def _assert_filtered(result, game): nums = result["numbers"] assert len(nums) == game["main_count"] assert nums == sorted(nums) assert len(set(nums)) == len(nums) assert all(1 <= n <= game["main_max"] for n in nums) assert not (len(nums) >= 4 and all(n % 2 == 0 for n in nums)), "all-even" assert not (len(nums) >= 4 and all(n % 2 != 0 for n in nums)), "all-odd" assert not _has_consecutive_run(nums, 4), "4+ consecutive" def test_hot_numbers_filter_integration(tmp_db): pb = get_game_by_name("Powerball") _large_draw_set(pb["id"]) for _ in range(10): _assert_filtered(hot_numbers(pb["id"]), pb) def test_due_numbers_filter_integration(tmp_db): pb = get_game_by_name("Powerball") _large_draw_set(pb["id"]) for _ in range(10): _assert_filtered(due_numbers(pb["id"]), pb) def test_weighted_random_filter_integration(tmp_db): pb = get_game_by_name("Powerball") _large_draw_set(pb["id"]) for _ in range(20): _assert_filtered(weighted_random(pb["id"]), pb) def test_monte_carlo_filter_integration(tmp_db): pb = get_game_by_name("Powerball") _large_draw_set(pb["id"]) for _ in range(5): _assert_filtered(monte_carlo(pb["id"], simulations=500), pb) def test_quick_pick_filter_integration(tmp_db): pb = get_game_by_name("Powerball") _large_draw_set(pb["id"]) for _ in range(20): _assert_filtered(quick_pick(pb["id"]), pb) def test_positional_pick_filter_integration(tmp_db): pb = get_game_by_name("Powerball") _large_draw_set(pb["id"]) for _ in range(10): _assert_filtered(positional_pick(pb["id"]), pb)