"""Comprehensive tests for Tarot attributes, alphabets, ciphers, and numerology.""" from datetime import datetime import pytest from src.tarot.attributes import ( Month, Day, Weekday, Hour, ClockHour, Zodiac, Suit, Meaning, Letter, Sephera, Degree, Element, AstrologicalInfluence, TreeOfLife, Correspondences, CardImage, EnglishAlphabet, GreekAlphabet, HebrewAlphabet, Number, Color, Planet, God, Cipher, CipherResult, ) from src.tarot.card.data import CardDataLoader, calculate_digital_root # ============================================================================ # Basic Attribute Tests # ============================================================================ class TestMonth: def test_month_creation(self): month = Month(1, "January", "Capricorn", "Aquarius") assert month.number == 1 assert month.name == "January" assert month.zodiac_start == "Capricorn" def test_month_all_months(self): months = [ Month(i, f"Month_{i}", "Sign_1", "Sign_2") for i in range(1, 13) ] assert len(months) == 12 assert months[0].number == 1 assert months[11].number == 12 class TestDay: def test_day_creation(self): day = Day(1, "Sunday", "Sun") assert day.number == 1 assert day.name == "Sunday" assert day.planetary_correspondence == "Sun" def test_all_weekdays(self): days = [ Day(i, f"Day_{i}", f"Planet_{i}") for i in range(1, 8) ] assert len(days) == 7 class TestWeekday: def test_weekday_creation(self): weekday = Weekday(1, "Monday", "Moon") assert weekday.name == "Monday" assert weekday.planetary_correspondence == "Moon" assert weekday.is_weekend is False def test_weekday_invalid_number(self): with pytest.raises(ValueError): Weekday(0, "Zero", "Sun") class TestClockHour: def test_clock_hour_creation(self): hour = ClockHour(13, 1, "PM", "Mars", "Afternoon surge") assert hour.hour_24 == 13 assert hour.hour_12 == 1 assert hour.time_of_day == "Night" def test_clock_hour_invalid_period(self): with pytest.raises(ValueError): ClockHour(12, 12, "XX", "Sun") class TestZodiac: def test_zodiac_creation(self): zodiac = Zodiac("Aries", "♈", "Fire", "Mars", "Mar 21 - Apr 19") assert zodiac.name == "Aries" assert zodiac.element == "Fire" assert zodiac.ruling_planet == "Mars" class TestSuit: def test_suit_creation(self): suit = Suit("Cups", "Water", "Chalice", 1) assert suit.name == "Cups" assert suit.element == "Water" assert suit.number == 1 class TestMeaning: def test_meaning_creation(self): meaning = Meaning("This is positive", "This is negative") assert meaning.upright == "This is positive" assert meaning.reversed == "This is negative" # ============================================================================ # Sepheric Tests # ============================================================================ class TestSephera: def test_sephera_creation(self): sephera = Sephera(1, "Kether", "כתר", "Crown", "Metatron", "Chaioth", "Primum") assert sephera.number == 1 assert sephera.name == "Kether" assert sephera.hebrew_name == "כתר" def test_all_sephera(self): sephera_list = [ Sephera(i, f"Sephera_{i}", "Hebrew", "Meaning", "Angel", "Order", "Chakra") for i in range(1, 11) ] assert len(sephera_list) == 10 # ============================================================================ # Alphabet Tests # ============================================================================ class TestEnglishAlphabet: def test_english_letter_creation(self): letter = EnglishAlphabet("A", 1, "ay") assert letter.letter == "A" assert letter.position == 1 assert letter.sound == "ay" def test_english_invalid_position_low(self): with pytest.raises(ValueError): EnglishAlphabet("A", 0, "ay") def test_english_invalid_position_high(self): with pytest.raises(ValueError): EnglishAlphabet("A", 27, "ay") def test_english_invalid_letter(self): with pytest.raises(ValueError): EnglishAlphabet("AB", 1, "ay") def test_english_all_letters(self): letters = [EnglishAlphabet(chr(65 + i), i + 1, "sound") for i in range(26)] assert len(letters) == 26 assert letters[0].letter == "A" assert letters[25].letter == "Z" class TestGreekAlphabet: def test_greek_letter_creation(self): letter = GreekAlphabet("Α", 1, "alpha") assert letter.letter == "Α" assert letter.position == 1 assert letter.transliteration == "alpha" def test_greek_invalid_position_low(self): with pytest.raises(ValueError): GreekAlphabet("Α", 0, "alpha") def test_greek_invalid_position_high(self): with pytest.raises(ValueError): GreekAlphabet("Α", 25, "alpha") def test_greek_all_letters(self): letters = [GreekAlphabet(f"Α{i}", i + 1, f"greek_{i}") for i in range(24)] assert len(letters) == 24 class TestHebrewAlphabet: def test_hebrew_letter_creation(self): letter = HebrewAlphabet("א", 1, "aleph", "Start") assert letter.letter == "א" assert letter.position == 1 assert letter.transliteration == "aleph" assert letter.meaning == "Start" def test_hebrew_invalid_position_low(self): with pytest.raises(ValueError): HebrewAlphabet("א", 0, "aleph", "Start") def test_hebrew_invalid_position_high(self): with pytest.raises(ValueError): HebrewAlphabet("א", 23, "aleph", "Start") def test_hebrew_all_letters(self): letters = [HebrewAlphabet(f"א{i}", i + 1, f"hebrew_{i}", f"meaning_{i}") for i in range(22)] assert len(letters) == 22 # ============================================================================ # Number Tests # ============================================================================ class TestNumber: def test_number_creation(self): num = Number(1, "Kether", "Spirit", 0) # compliment is auto-calculated assert num.value == 1 assert num.sephera == "Kether" assert num.compliment == 8 # 10 - 1 = 9, but 9->9, so 1->8 def test_number_compliments(self): """Test that compliments are auto-calculated correctly.""" test_cases = [(1, 8), (2, 7), (3, 6), (4, 5), (5, 4), (6, 3), (7, 2), (8, 1), (9, 9)] for value, expected_compliment in test_cases: num = Number(value, f"Sephera_{value}", "Element", 0) assert num.compliment == expected_compliment def test_number_invalid_value_low(self): with pytest.raises(ValueError): Number(0, "Sephera", "Element", 0) def test_number_invalid_value_high(self): with pytest.raises(ValueError): Number(10, "Sephera", "Element", 0) def test_all_numbers(self): numbers = [Number(i, f"Sephera_{i}", "Element", 0) for i in range(1, 10)] assert len(numbers) == 9 # ============================================================================ # Color Tests # ============================================================================ class TestColor: def test_color_creation(self): color = Color("Red", "#FF0000", (255, 0, 0), "Gevurah", 5, "Fire", "Briah", "Power") assert color.name == "Red" assert color.hex_value == "#FF0000" assert color.rgb == (255, 0, 0) assert color.number == 5 def test_color_invalid_hex_format(self): with pytest.raises(ValueError): Color("Red", "FF0000", (255, 0, 0), "Gevurah", 5, "Fire", "Briah", "Power") def test_color_invalid_hex_length(self): with pytest.raises(ValueError): Color("Red", "#FF00", (255, 0, 0), "Gevurah", 5, "Fire", "Briah", "Power") def test_color_invalid_rgb_low(self): with pytest.raises(ValueError): Color("Red", "#FF0000", (-1, 0, 0), "Gevurah", 5, "Fire", "Briah", "Power") def test_color_invalid_rgb_high(self): with pytest.raises(ValueError): Color("Red", "#FF0000", (256, 0, 0), "Gevurah", 5, "Fire", "Briah", "Power") def test_color_all_valid_rgb(self): for r in [0, 128, 255]: for g in [0, 128, 255]: for b in [0, 128, 255]: color = Color("Test", "#000000", (r, g, b), "Sephera", 1, "Element", "Scale", "Meaning") assert color.rgb == (r, g, b) # ============================================================================ # Planet Tests # ============================================================================ class TestPlanet: def test_planet_creation(self): number = Number(6, "Tiphareth", "Fire", 0) color = Color("Gold", "#FFD700", (255, 215, 0), "Tiphareth", 6, "Fire", "Yetzirah", "Beauty") planet = Planet( name="Sun", symbol="☉", element="Fire", ruling_zodiac=["Leo"], associated_numbers=[number], associated_letters=["Resh"], keywords=["Vitality"], color=color, description="Center", ) assert planet.name == "Sun" assert planet.color is color assert planet.associated_numbers[0].value == 6 class TestGod: def test_god_creation(self): number = Number(6, "Tiphareth", "Fire", 0) planet = Planet( name="Sun", symbol="☉", element="Fire", ruling_zodiac=["Leo"], associated_numbers=[number], associated_letters=["Resh"], keywords=["Vitality"], color=None, description="Center", ) god = God( name="Ra", culture="Egyptian", pantheon="Solar", domains=["Consciousness"], epithets=["Sun God"], mythology="Solar deity", sephera_numbers=[6], path_numbers=[15], planets=["Sun"], elements=["Fire"], zodiac_signs=["Leo"], associated_planet=planet, associated_element=None, associated_numbers=[number], tarot_trumps=["XIX - The Sun"], keywords=["Vitality"], description="Solar current", ) assert god.culture_key() == "egyptian" assert god.primary_number() is number assert 6 in god.sephera_numbers def test_god_without_planet(self): god = God( name="Hecate", culture="Greek", pantheon="Chthonic", domains=["Magic"], epithets=["Triple Goddess"], mythology="Guardian of crossroads", sephera_numbers=[], path_numbers=[29], planets=[], elements=["Water"], zodiac_signs=["Pisces"], associated_planet=None, associated_element=None, associated_numbers=[], tarot_trumps=["XVIII - The Moon"], keywords=["Mystery"], description="Night guide", ) assert god.primary_number() is None assert "water" in [elem.lower() for elem in god.elements] # ============================================================================ # Cipher Tests # ============================================================================ class TestCipher: def test_cipher_mapping_basic(self): cipher = Cipher("Test", "test", [1, 2, 3]) mapping = cipher.mapping_for_alphabet(["A", "B", "C"]) assert mapping == {"A": 1, "B": 2, "C": 3} def test_cipher_cycle_expansion(self): cipher = Cipher("Cycle", "cycle", [1, 2], cycle=True) mapping = cipher.mapping_for_alphabet(["A", "B", "C", "D"]) assert mapping["C"] == 1 assert mapping["D"] == 2 def test_cipher_subset(self): cipher = Cipher("Subset", "subset", [5, 6], letter_subset={"A", "C"}) mapping = cipher.mapping_for_alphabet(["A", "B", "C"]) assert "B" not in mapping assert mapping["C"] == 6 class TestCipherResult: def test_cipher_result_totals(self): cipher = Cipher("Test", "test", [1, 2, 3]) result = CipherResult("abc", cipher, "english", (1, 2, 3)) assert result.total == 6 assert result.as_string("-") == "1-2-3" assert str(result) == "123" # ============================================================================ # Digital Root Tests # ============================================================================ class TestDigitalRoot: def test_digital_root_single_digit(self): """Single digits should return themselves.""" for i in range(1, 10): assert calculate_digital_root(i) == i def test_digital_root_two_digit(self): """Test two-digit numbers.""" assert calculate_digital_root(10) == 1 # 1+0 = 1 assert calculate_digital_root(14) == 5 # 1+4 = 5 assert calculate_digital_root(19) == 1 # 1+9 = 10, 1+0 = 1 assert calculate_digital_root(18) == 9 # 1+8 = 9 def test_digital_root_large_numbers(self): """Test large numbers.""" assert calculate_digital_root(99) == 9 # 9+9 = 18, 1+8 = 9 assert calculate_digital_root(100) == 1 # 1+0+0 = 1 assert calculate_digital_root(123) == 6 # 1+2+3 = 6 def test_digital_root_tarot_cards(self): """Test digital root for Tarot card numbers.""" # Major Arcana cards 0-21 assert calculate_digital_root(14) == 5 # Card 14 (Temperance) -> 5 assert calculate_digital_root(21) == 3 # Card 21 (The World) -> 3 assert calculate_digital_root(1) == 1 # Card 1 (Magician) -> 1 def test_digital_root_invalid_input(self): """Test that invalid inputs raise errors.""" with pytest.raises(ValueError): calculate_digital_root(0) with pytest.raises(ValueError): calculate_digital_root(-5) # ============================================================================ # CardDataLoader Tests # ============================================================================ class TestCardDataLoader: @pytest.fixture def loader(self): return CardDataLoader() def test_loader_initialization(self, loader): """Test that loader initializes correctly.""" assert loader is not None def test_load_numbers(self, loader): """Test loading numbers.""" for i in range(1, 10): num = loader.number(i) assert num is not None assert num.value == i assert num.compliment == (9 - i if i != 9 else 9) def test_load_colors(self, loader): """Test loading colors.""" for i in range(1, 11): color = loader.color(i) assert color is not None assert color.number == i def test_load_sephera(self, loader): """Test loading individual Sephiroth.""" for i in range(1, 11): sephera = loader.sephera(i) assert sephera is not None assert sephera.number == i def test_load_ciphers(self, loader): """Ensure cipher catalog is populated.""" ciphers = loader.cipher() assert "english_simple" in ciphers assert ciphers["english_simple"].default_alphabet == "english" def test_word_cipher_request(self, loader): """word().cipher() should return meaningful totals.""" result = loader.word("tarot").cipher("english_simple") assert isinstance(result, CipherResult) assert result.total == 74 assert result.alphabet_name == "english" def test_word_cipher_custom_alphabet(self, loader): result = loader.word("אמש").cipher("kabbalah_three_mother") assert result.values == (1, 40, 300) def test_trigram_line_diagram(self, loader): from letter import trigram tri = trigram.trigram.name("Zhen") # Thunder assert tri is not None assert tri.data.line_diagram == "|::" def test_hexagram_line_diagram(self, loader): from letter import hexagram hex_result = hexagram.hexagram.filter('number:1').first() assert hex_result is not None assert hex_result.data.line_diagram == "||||||" def test_color_by_number(self, loader): """Test getting color by number with digital root.""" # 14 -> digital root 5 -> Gevurah -> Red Scarlet color = loader.color_by_number(14) assert color is not None assert color.number == 5 assert "Red" in color.name def test_number_by_digital_root(self, loader): """Test getting number by digital root.""" num = loader.number_by_digital_root(14) assert num is not None assert num.value == 5 def test_digital_root_method(self, loader): """Test the digital root method on loader.""" assert loader.digital_root(14) == 5 assert loader.digital_root(99) == 9 assert loader.digital_root(5) == 5 def test_cipher_lookup(self, loader): """Test loading individual cipher definitions.""" systems = ["hebrew_standard", "english_simple", "greek_isopsephy", "reduction"] for system_name in systems: cipher = loader.cipher(system_name) assert cipher is not None assert cipher.key == system_name def test_planet_lookup(self, loader): """Test loading planetary correspondences.""" planets = loader.planet() assert "mercury" in planets mercury = loader.planet("Mercury") assert mercury is not None assert mercury.associated_letters[0] == "Beth" assert mercury.associated_numbers and mercury.associated_numbers[0].value == 8 def test_weekday_lookup(self, loader): weekdays = loader.weekday() assert "monday" in weekdays sunday = loader.weekday("Sunday") assert sunday is not None assert sunday.is_weekend is True assert sunday.planetary_correspondence == "Sun" def test_clock_hour_lookup(self, loader): hours = loader.clock_hour() assert len(hours) == 24 midnight = loader.clock_hour(0) assert midnight is not None assert midnight.period == "AM" assert midnight.time_of_day == "Day" afternoon = loader.clock_hour(15) assert afternoon is not None assert afternoon.period == "PM" assert afternoon.time_of_day == "Night" def test_god_registry(self, loader): gods = loader.god() assert "apollo" in gods assert "ra" in gods apollo = gods["apollo"] assert apollo.culture == "Greek" assert apollo.associated_planet is not None assert apollo.associated_planet.name == "Sun" def test_gods_by_culture(self, loader): greek_gods = loader.gods_by_culture("Greek") assert greek_gods assert all(god.culture == "Greek" for god in greek_gods.values()) assert "apollo" in greek_gods assert "ra" not in greek_gods def test_god_with_culture_filter(self, loader): apollo = loader.god("Apollo", culture="Greek") assert apollo is not None assert apollo.culture == "Greek" assert apollo.associated_planet is not None assert loader.god("Apollo", culture="Roman") is None def test_temporal_correspondence(self, loader): moment = datetime(2024, 3, 21, 15, 30) snapshot = loader.temporal_correspondence(moment) assert snapshot.timestamp == moment assert snapshot.weekday is not None assert snapshot.weekday.name == "Thursday" assert snapshot.clock_hour is not None assert snapshot.clock_hour.hour_24 == 15 assert snapshot.planet is not None assert snapshot.planet.name == snapshot.clock_hour.planetary_ruler if snapshot.number is not None: assert snapshot.color is snapshot.number.color assert snapshot.card.name # deterministic card returned assert snapshot.hexagram is not None def test_alphabet_catalog(self, loader): """Test loading alphabets.""" alphabets = loader.alphabet() assert "english" in alphabets assert "greek" in alphabets assert "hebrew" in alphabets assert len(alphabets["english"]) == 26 assert len(alphabets["greek"]) == 24 assert len(alphabets["hebrew"]) == 22 def test_sephera_loaded(self, loader): """Test that all 10 Sephiroth are loaded.""" sephera_count = 0 for i in range(1, 11): color = loader.color(i) if color is not None and color.sephera: sephera_count += 1 assert sephera_count == 10 class TestDigitalRootIntegration: """Integration tests for digital root with Tarot cards.""" def test_all_major_arcana_digital_roots(self): """Test digital root for all Major Arcana cards (0-21).""" loader = CardDataLoader() # All Major Arcana cards should map to colors 1-9 for card_num in range(22): if card_num == 0: continue # Skip The Fool (0) color = loader.color_by_number(card_num) assert color is not None assert 1 <= color.number <= 9 def test_color_consistency(self): """Test that equivalent numbers map to same color.""" loader = CardDataLoader() # 5 and 14 should map to same color (both have digital root 5) color_5 = loader.color_by_number(5) color_14 = loader.color_by_number(14) assert color_5 is not None assert color_14 is not None assert color_5.number == color_14.number assert color_5.hex_value == color_14.hex_value