"""Unit tests for lottery_calculator.py — pure calculation logic.""" from __future__ import annotations import pytest from lottery_calculator import ( calculate_annuity, calculate_break_even, calculate_canadian_lottery, calculate_group_split, calculate_us_lottery, ) # --------------------------------------------------------------------------- # calculate_us_lottery # --------------------------------------------------------------------------- class TestCalculateUSLottery: """Tests for the US lottery calculation.""" def test_basic_calculation(self): result = calculate_us_lottery(100_000_000) assert result["country"] == "US" assert result["originalJackpot"] == 100_000_000 def test_cash_sum_is_lump_sum_rate(self): result = calculate_us_lottery(100_000_000, lump_sum_rate=0.52) assert result["cashSum"] == pytest.approx(52_000_000) def test_federal_tax_applied(self): result = calculate_us_lottery(100_000_000, lump_sum_rate=0.52, federal_tax_rate=0.37) assert result["federalTax"] == pytest.approx(52_000_000 * 0.37) def test_state_tax_applied(self): result = calculate_us_lottery(100_000_000, state_tax_rate=0.10) assert result["stateTaxRate"] == 0.10 def test_net_amount_usd(self): result = calculate_us_lottery( 100_000_000, lump_sum_rate=0.50, federal_tax_rate=0.30, state_tax_rate=0.05 ) expected = 50_000_000 * (1 - 0.30 - 0.05) assert result["netAmountUsd"] == pytest.approx(expected) def test_cad_conversion(self): result = calculate_us_lottery(100_000_000, usd_cad_rate=1.40) assert result["netAmountCad"] == pytest.approx(result["netAmountUsd"] * 1.40) def test_investment_split(self): result = calculate_us_lottery(100_000_000, invest_percentage=0.80) total = result["investmentPrincipal"] + result["funMoney"] assert total == pytest.approx(result["netAmountCad"]) assert result["funMoney"] == pytest.approx(result["netAmountCad"] * 0.20) def test_cycles_count(self): result = calculate_us_lottery(100_000_000, cycles=5) assert len(result["cycles"]) == 5 def test_principal_grows(self): result = calculate_us_lottery(100_000_000, cycles=4) for i in range(1, len(result["cycles"])): assert result["cycles"][i]["principalStart"] >= result["cycles"][i - 1]["principalStart"] def test_final_principal_positive(self): result = calculate_us_lottery(500_000_000) assert result["finalPrincipal"] > 0 def test_daily_income_positive(self): result = calculate_us_lottery(500_000_000) assert result["netDailyIncome"] > 0 def test_zero_state_tax(self): """Florida / Texas have 0% state tax.""" result = calculate_us_lottery(100_000_000, state_tax_rate=0.0) assert result["stateTax"] == 0.0 assert result["netAmountUsd"] > calculate_us_lottery(100_000_000, state_tax_rate=0.10)["netAmountUsd"] # --------------------------------------------------------------------------- # calculate_canadian_lottery # --------------------------------------------------------------------------- class TestCalculateCanadianLottery: """Tests for Canadian lottery calculation (tax-free winnings).""" def test_basic_calculation(self): result = calculate_canadian_lottery(50_000_000) assert result["country"] == "Canada" assert result["originalJackpot"] == 50_000_000 def test_tax_free(self): result = calculate_canadian_lottery(50_000_000) assert result["netAmountCad"] == 50_000_000 def test_investment_split(self): result = calculate_canadian_lottery(50_000_000, invest_percentage=0.90) assert result["investmentPrincipal"] == pytest.approx(45_000_000) assert result["funMoney"] == pytest.approx(5_000_000) def test_cycles_count(self): result = calculate_canadian_lottery(50_000_000, cycles=3) assert len(result["cycles"]) == 3 def test_daily_income_positive(self): result = calculate_canadian_lottery(50_000_000) assert result["netDailyIncome"] > 0 # --------------------------------------------------------------------------- # calculate_break_even # --------------------------------------------------------------------------- class TestCalculateBreakEven: """Tests for the break-even expected-value calculator.""" def test_us_break_even(self): result = calculate_break_even(odds=292_201_338, ticket_cost=2.0, country="us") assert result["breakEvenJackpot"] > 0 assert result["breakEvenJackpot"] > 292_201_338 # must be >> odds because of tax def test_canadian_break_even(self): result = calculate_break_even(odds=13_983_816, ticket_cost=3.0, country="canadian") # Canadian take-home fraction = 1.0, so break-even = 3 * 13_983_816 expected = 3.0 * 13_983_816 assert result["breakEvenJackpot"] == pytest.approx(expected) def test_ev_equals_ticket_cost(self): result = calculate_break_even(odds=100, ticket_cost=5.0, country="canadian") # EV at break even = probability * jackpot * 1.0 = 5.0 assert result["expectedValueAtBreakEven"] == pytest.approx(5.0, rel=1e-6) def test_higher_tax_needs_bigger_jackpot(self): r1 = calculate_break_even(odds=100, ticket_cost=2.0, country="us", state_tax_rate=0.0) r2 = calculate_break_even(odds=100, ticket_cost=2.0, country="us", state_tax_rate=0.10) assert r2["breakEvenJackpot"] > r1["breakEvenJackpot"] # --------------------------------------------------------------------------- # calculate_annuity # --------------------------------------------------------------------------- class TestCalculateAnnuity: """Tests for the annuity payout schedule.""" def test_us_annuity_schedule_length(self): result = calculate_annuity(500_000_000, country="us", years=30) assert len(result["schedule"]) == 30 def test_canadian_no_tax(self): result = calculate_annuity(100_000_000, country="canadian", years=10, annual_increase=0.0) for entry in result["schedule"]: assert entry["tax"] == 0.0 assert entry["afterTax"] == entry["preTax"] def test_total_pretax_approximates_jackpot(self): result = calculate_annuity(100_000_000, country="canadian", years=30, annual_increase=0.05) assert result["totalPreTax"] == pytest.approx(100_000_000, rel=1e-6) def test_payments_increase_annually(self): result = calculate_annuity(100_000_000, years=5, annual_increase=0.05) for i in range(1, len(result["schedule"])): assert result["schedule"][i]["preTax"] > result["schedule"][i - 1]["preTax"] def test_zero_increase(self): result = calculate_annuity(100_000_000, years=5, annual_increase=0.0) payments = [e["preTax"] for e in result["schedule"]] assert all(p == pytest.approx(payments[0]) for p in payments) # --------------------------------------------------------------------------- # calculate_group_split # --------------------------------------------------------------------------- class TestCalculateGroupSplit: """Tests for the group play calculator.""" def test_equal_split(self): result = calculate_group_split(100_000_000, members=4, country="canadian") assert len(result["memberResults"]) == 4 for m in result["memberResults"]: assert m["jackpotShare"] == pytest.approx(25_000_000) def test_custom_shares(self): shares = [0.5, 0.3, 0.2] result = calculate_group_split(100_000_000, members=3, shares=shares, country="canadian") assert result["memberResults"][0]["jackpotShare"] == pytest.approx(50_000_000) assert result["memberResults"][1]["jackpotShare"] == pytest.approx(30_000_000) assert result["memberResults"][2]["jackpotShare"] == pytest.approx(20_000_000) def test_shares_normalize(self): """Shares that don't sum to 1.0 should be normalised.""" shares = [2, 2, 1] # sums to 5 result = calculate_group_split(100_000_000, members=3, shares=shares, country="canadian") assert result["memberResults"][0]["share"] == pytest.approx(0.4) assert result["memberResults"][2]["share"] == pytest.approx(0.2) def test_mismatched_shares_falls_back_to_equal(self): result = calculate_group_split(100_000_000, members=3, shares=[0.5, 0.5], country="canadian") for m in result["memberResults"]: assert m["share"] == pytest.approx(1 / 3) def test_each_member_gets_calculation(self): result = calculate_group_split(100_000_000, members=2, country="us") for m in result["memberResults"]: assert "calculation" in m assert m["calculation"]["country"] == "US"