"""Unit tests for the services layer (families, naming utilities).""" import pytest from app.services.families import build_families, resolve_base_form from app.services.naming import strip_roman_suffix, to_roman # --------------------------------------------------------------------------- # Minimal Evolution stand-in — only the two fields the services touch # --------------------------------------------------------------------------- class Evo: """Lightweight stand-in for app.models.evolution.Evolution.""" def __init__(self, from_id: int, to_id: int) -> None: self.from_pokemon_id = from_id self.to_pokemon_id = to_id # --------------------------------------------------------------------------- # build_families # --------------------------------------------------------------------------- class TestBuildFamilies: def test_empty_evolutions_returns_empty_dict(self): assert build_families([]) == {} def test_linear_chain(self): # A(1) → B(2) → C(3) evos = [Evo(1, 2), Evo(2, 3)] families = build_families(evos) assert set(families[1]) == {1, 2, 3} assert set(families[2]) == {1, 2, 3} assert set(families[3]) == {1, 2, 3} def test_branching_evolutions(self): # Eevee-like: 1 → 2, 1 → 3, 1 → 4 evos = [Evo(1, 2), Evo(1, 3), Evo(1, 4)] families = build_families(evos) assert set(families[1]) == {1, 2, 3, 4} assert set(families[2]) == {1, 2, 3, 4} assert set(families[4]) == {1, 2, 3, 4} def test_disjoint_chains_are_separate_families(self): # Chain 1→2 and independent chain 3→4 evos = [Evo(1, 2), Evo(3, 4)] families = build_families(evos) assert set(families[1]) == {1, 2} assert set(families[3]) == {3, 4} assert 3 not in set(families[1]) assert 1 not in set(families[3]) def test_shedinja_case(self): # Nincada(1) → Ninjask(2) and Nincada(1) → Shedinja(3) evos = [Evo(1, 2), Evo(1, 3)] families = build_families(evos) assert set(families[1]) == {1, 2, 3} assert set(families[3]) == {1, 2, 3} def test_pokemon_not_in_any_evolution_not_in_result(self): evos = [Evo(1, 2)] families = build_families(evos) assert 99 not in families def test_all_family_members_have_identical_family_list(self): evos = [Evo(10, 11), Evo(11, 12)] families = build_families(evos) assert set(families[10]) == set(families[11]) == set(families[12]) # --------------------------------------------------------------------------- # resolve_base_form # --------------------------------------------------------------------------- class TestResolveBaseForm: def test_pokemon_not_in_any_evolution_returns_itself(self): assert resolve_base_form(99, []) == 99 def test_base_form_returns_itself(self): # A(1) → B(2): base of 1 is still 1 evos = [Evo(1, 2)] assert resolve_base_form(1, evos) == 1 def test_final_form_returns_base(self): # A(1) → B(2) → C(3): base of 3 is 1 evos = [Evo(1, 2), Evo(2, 3)] assert resolve_base_form(3, evos) == 1 def test_middle_form_returns_base(self): # A(1) → B(2) → C(3): base of 2 is 1 evos = [Evo(1, 2), Evo(2, 3)] assert resolve_base_form(2, evos) == 1 def test_branching_evolution_base(self): # 1 → 2, 1 → 3: base of both 2 and 3 is 1 evos = [Evo(1, 2), Evo(1, 3)] assert resolve_base_form(2, evos) == 1 assert resolve_base_form(3, evos) == 1 def test_shedinja_resolves_to_nincada(self): # Nincada(1) → Ninjask(2), Nincada(1) → Shedinja(3) evos = [Evo(1, 2), Evo(1, 3)] assert resolve_base_form(3, evos) == 1 def test_empty_evolutions_returns_self(self): assert resolve_base_form(42, []) == 42 # --------------------------------------------------------------------------- # to_roman # --------------------------------------------------------------------------- class TestToRoman: @pytest.mark.parametrize( "n, expected", [ (1, "I"), (2, "II"), (3, "III"), (4, "IV"), (5, "V"), (6, "VI"), (9, "IX"), (10, "X"), (11, "XI"), (14, "XIV"), (40, "XL"), (50, "L"), (90, "XC"), (100, "C"), ], ) def test_converts_integer_to_roman(self, n: int, expected: str): assert to_roman(n) == expected def test_typical_genlocke_sequence(self): # Lineage names: Heracles I, II, III, IV, V assert [to_roman(i) for i in range(1, 6)] == ["I", "II", "III", "IV", "V"] # --------------------------------------------------------------------------- # strip_roman_suffix # --------------------------------------------------------------------------- class TestStripRomanSuffix: def test_strips_roman_numeral_ii(self): assert strip_roman_suffix("Heracles II") == "Heracles" def test_strips_roman_numeral_iii(self): assert strip_roman_suffix("Athena III") == "Athena" def test_strips_roman_numeral_iv(self): assert strip_roman_suffix("Nova IV") == "Nova" def test_strips_roman_numeral_x(self): assert strip_roman_suffix("Zeus X") == "Zeus" def test_no_suffix_returns_unchanged(self): assert strip_roman_suffix("Apollo") == "Apollo" def test_name_with_i_suffix(self): # Single "I" at end is a valid roman numeral suffix assert strip_roman_suffix("Heracles I") == "Heracles" def test_round_trip_with_to_roman(self): base = "Heracles" for n in range(1, 6): suffixed = f"{base} {to_roman(n)}" assert strip_roman_suffix(suffixed) == base