"""Comprehensive tests for Tarot attributes, alphabets, ciphers, and numerology."""

from datetime import datetime

import pytest

from src.tarot.attributes import (
    AstrologicalInfluence,
    CardImage,
    Cipher,
    CipherResult,
    ClockHour,
    Color,
    Correspondences,
    Day,
    Degree,
    Element,
    EnglishAlphabet,
    God,
    GreekAlphabet,
    HebrewAlphabet,
    Hour,
    Letter,
    Meaning,
    Month,
    Number,
    Planet,
    Sephera,
    Suit,
    TreeOfLife,
    Weekday,
    Zodiac,
)
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
