from __future__ import annotations import math import pytest from funkyjuicerecipes.logic.calculations import ( compute_breakdown, breakdown_for_view, ) def test_readme_style_example_120ml_nic_in_pg(): bd = compute_breakdown( size_ml=120, base_pg_pct=30.0, base_vg_pct=70.0, nic_target_mg_per_ml=3.0, nic_strength_mg_per_ml=100.0, nic_base="PG", flavors=[ {"name": "Vanilla", "base": "PG", "pct": 8.0}, ("Strawberry", "VG", 4.0), ], ) # Percentages assert bd.pg_pct == pytest.approx(19.0) assert bd.vg_pct == pytest.approx(66.0) assert bd.nic_pct == pytest.approx(3.0) # mL calculations assert bd.pg_ml == pytest.approx(22.8) assert bd.vg_ml == pytest.approx(79.2) assert bd.nic_ml == pytest.approx(3.6) names = [f.name for f in bd.flavors] assert names == ["Vanilla", "Strawberry"] amounts = {f.name: f.ml for f in bd.flavors} assert amounts["Vanilla"] == pytest.approx(9.6) assert amounts["Strawberry"] == pytest.approx(4.8) assert bd.flavor_prep_ml == pytest.approx(14.4) # Rounded view (1 decimal) should match obvious roundings v = breakdown_for_view(bd) assert v.pg_ml == pytest.approx(22.8) assert v.vg_ml == pytest.approx(79.2) assert v.nic_ml == pytest.approx(3.6) assert {f.name: f.ml for f in v.flavors}["Vanilla"] == pytest.approx(9.6) assert {f.name: f.ml for f in v.flavors}["Strawberry"] == pytest.approx(4.8) assert v.flavor_prep_ml == pytest.approx(14.4) def test_nic_in_vg_changes_pg_vg_split(): bd = compute_breakdown( size_ml=120, base_pg_pct=30.0, base_vg_pct=70.0, nic_target_mg_per_ml=3.0, nic_strength_mg_per_ml=100.0, nic_base="VG", flavors=[ {"name": "Vanilla", "base": "PG", "pct": 8.0}, ("Strawberry", "VG", 4.0), ], ) assert bd.pg_pct == pytest.approx(22.0) assert bd.vg_pct == pytest.approx(63.0) assert bd.pg_ml == pytest.approx(26.4) assert bd.vg_ml == pytest.approx(75.6) @pytest.mark.parametrize( "base_pg,base_vg", [ (70.0, 30.0), (70.0000001, 29.9999999), # tolerance ], ) def test_base_pg_vg_sum_to_100_with_tolerance(base_pg, base_vg): bd = compute_breakdown( size_ml=60, base_pg_pct=base_pg, base_vg_pct=base_vg, nic_target_mg_per_ml=0.0, nic_strength_mg_per_ml=100.0, nic_base="PG", flavors=[], ) assert bd.pg_pct + bd.vg_pct + bd.nic_pct + sum(f.pct for f in bd.flavors) == pytest.approx(100.0) def test_excessive_flavor_or_negative_pg_raises(): # This combination would push PG negative (base PG 10, nic 10 PG, flavor PG 5 => PG becomes -5) with pytest.raises(ValueError): compute_breakdown( size_ml=30, base_pg_pct=10.0, base_vg_pct=90.0, nic_target_mg_per_ml=10.0, nic_strength_mg_per_ml=100.0, nic_base="PG", flavors=[("Whatever", "PG", 5.0)], ) # Flavors sum > 100 should also fail via negative VG/PG after deduction with pytest.raises(ValueError): compute_breakdown( size_ml=30, base_pg_pct=50.0, base_vg_pct=50.0, nic_target_mg_per_ml=0.0, nic_strength_mg_per_ml=100.0, nic_base="PG", flavors=[("A", "PG", 60.0), ("B", "VG", 50.0)], ) def test_input_variants_tuple_and_mapping_supported(): bd = compute_breakdown( size_ml=10, base_pg_pct=50.0, base_vg_pct=50.0, nic_target_mg_per_ml=0.0, nic_strength_mg_per_ml=100.0, nic_base="PG", flavors=[ {"name": "X", "base": "PG", "pct": 5.0}, ("Y", "VG", 5.0), ], ) def test_50mg_stock_requires_more_nic_volume_and_changes_pg_vg_split(): # With 50 mg/mL stock, to reach 3 mg/mL target we need 6% nic volume. bd = compute_breakdown( size_ml=120, base_pg_pct=30.0, base_vg_pct=70.0, nic_target_mg_per_ml=3.0, nic_strength_mg_per_ml=50.0, nic_base="PG", flavors=[ {"name": "Vanilla", "base": "PG", "pct": 8.0}, ("Strawberry", "VG", 4.0), ], ) # Now nic_pct (volume) is 6% assert bd.nic_pct == pytest.approx(6.0) # PG is further reduced by extra 3% vs the 100 mg/mL case # Base pg 30 - flavor PG 8 - nic 6 = 16 assert bd.pg_pct == pytest.approx(16.0) # VG remains 70 - flavor VG 4 = 66 assert bd.vg_pct == pytest.approx(66.0) # mL assert bd.nic_ml == pytest.approx(7.2) # 6% of 120 assert bd.pg_ml == pytest.approx(19.2) # 16% of 120 assert bd.vg_ml == pytest.approx(79.2) # 66% of 120 assert {f.name for f in bd.flavors} == {"Vanilla", "Strawberry"}