"""Tests for sim.py: the numbers the lesson and the widget show, and the properties behind them."""
import pytest

from budget import MAX_REPLY, cost, dollars
from sim import (BUDGET, CALL_GAP, HEAVY, ORDINARY_IN, ORDINARY_OUT, RPM, SEED, SESSION_CALLS, SESSIONS, USERS, make_day, random_numbers, simulate_day)


def test_the_same_seed_gives_the_same_day_and_another_seed_a_different_one():
    assert make_day(SEED) == make_day(SEED) and make_day(SEED) != make_day(SEED + 1)
    assert simulate_day("budget") == simulate_day("budget")
    day = make_day(SEED)
    assert len(day) == 394 and day == sorted(day, key=lambda c: (c[0], c[1]))
    assert sum(1 for c in day if c[1] == HEAVY) == 190


def test_the_random_numbers_do_not_change():
    """Pins mulberry32 so the Python and the widget's copy cannot both drift to another stream."""
    gen = random_numbers(1)
    assert [next(gen) for _ in range(3)] == [0.6270739405881613, 0.002735721180215478, 0.5274470399599522]
    assert all(0 <= next(gen) < 1 for _ in range(1000))


@pytest.mark.parametrize("rule, share, bill, heavy_refused", [("none", 94, "$9.93", 0), ("rpm", 94, "$9.93", 0), ("budget", 28, "$0.85", 185)])
def test_the_numbers_the_lesson_names_for_the_day_it_shows(rule, share, bill, heavy_refused):
    day = simulate_day(rule)
    assert (day.percent(HEAVY), dollars(day.bill), day.refused[HEAVY]) == (share, bill, heavy_refused)
    assert (day.ordinary_refused, day.ordinary_asked) == (0, 204)


def test_a_request_limit_does_not_touch_the_heavy_user():
    """The point of the lesson: the heavy user makes one call every four to six minutes, far under 5 a minute, and pays 94% of the bill."""
    none, rpm = simulate_day("none"), simulate_day("rpm")
    assert (rpm.served, rpm.refused, rpm.spent) == (none.served, none.refused, none.spent)


def test_a_very_tight_request_limit_hurts_ordinary_users_and_still_leaves_the_heavy_user_alone():
    """Guards the lesson's claim that lowering the request limit does not help: at 1 call a minute it still misses the heavy user and turns ordinary users away."""
    day = simulate_day("rpm", rpm=1)
    assert day.refused[HEAVY] == 0 and day.ordinary_refused == 69
    assert day.percent(HEAVY) >= 90


def test_over_many_days_the_budget_cuts_the_heavy_share_and_turns_no_ordinary_user_away():
    for seed in range(1, 201):
        none, budget = simulate_day("none", seed), simulate_day("budget", seed)
        assert none.percent(HEAVY) >= 90 and budget.percent(HEAVY) <= 35, seed
        assert budget.ordinary_refused == 0 and simulate_day("rpm", seed).ordinary_refused == 0, seed
        assert budget.bill < none.bill / 5, seed
        for cents in range(15, 55, 5):  # every widget step in the lesson's "size the budget" range: no ordinary call turned away on any day
            assert simulate_day("budget", seed, budget=cents * 10_000).ordinary_refused == 0, (seed, cents)


def test_no_ordinary_user_can_reach_the_default_budget():
    """The most an ordinary user can send in a day, plus the largest set-aside, still fits: so the budget never refuses one of them at its default."""
    most_calls = SESSIONS[1] * SESSION_CALLS[1]
    dearest_call = cost(ORDINARY_IN[1], ORDINARY_OUT[1])
    largest_set_aside = cost(ORDINARY_IN[1], MAX_REPLY)
    assert most_calls * dearest_call + largest_set_aside < BUDGET


def test_under_the_budget_rule_no_user_is_ever_charged_more_than_the_budget():
    """The reason to set aside the most a call can cost: the real cost is never above it, so the total stays under the limit."""
    for seed in range(1, 31):
        assert max(simulate_day("budget", seed).spent) <= BUDGET, seed


def test_every_call_is_either_served_or_refused_and_the_bill_is_the_sum_of_served_costs():
    calls = make_day(SEED)
    for rule in ("none", "rpm", "budget"):
        day = simulate_day(rule)
        assert sum(day.served) + sum(day.refused) == len(calls)
        for user in range(USERS):
            assert day.served[user] + day.refused[user] == sum(1 for c in calls if c[1] == user)
    assert simulate_day("none").spent == tuple(sum(cost(c[2], c[3]) for c in calls if c[1] == u) for u in range(USERS))


def test_a_smaller_budget_cuts_the_bill_and_a_bigger_one_lets_the_heavy_user_pay_more():
    bills = [simulate_day("budget", budget=cents * 10_000).bill for cents in (10, 25, 50)]
    assert bills == sorted(bills) and len(set(bills)) == 3
    assert simulate_day("budget", budget=5 * 10_000).ordinary_refused == 77  # below the reserve of a few calls, ordinary users are hit too


def test_call_gaps_stay_inside_the_day():
    assert max(c[0] for c in make_day(SEED)) < 86_400 and CALL_GAP[1] * SESSION_CALLS[1] <= 8 * 89 and RPM == 5
