"""Tests for the gematria computation engine (issue #234).

Alexander Whitestone = 222 in Simple English Gematria.
This is not trivia.  It is foundational.
"""

from __future__ import annotations

import pytest

from timmy.gematria import (
    CIPHERS,
    NOTABLE_NUMBERS,
    analyze_number,
    compute_all,
    compute_value,
    factorize,
    gematria,
    letter_breakdown,
    reduce_number,
)

# ── Core cipher computation ──────────────────────────────────────────────────


class TestSimpleCipher:
    """Simple English: A=1, B=2, ..., Z=26."""

    def test_single_letter_a(self):
        assert compute_value("A", "simple") == 1

    def test_single_letter_z(self):
        assert compute_value("Z", "simple") == 26

    def test_alexander_whitestone(self):
        """The foundational identity — Alexander Whitestone = 222."""
        assert compute_value("Alexander Whitestone", "simple") == 222

    def test_case_insensitive(self):
        assert compute_value("hello", "simple") == compute_value("HELLO", "simple")

    def test_ignores_non_alpha(self):
        assert compute_value("A-B!C", "simple") == compute_value("ABC", "simple")

    def test_empty_string(self):
        assert compute_value("", "simple") == 0

    def test_spaces_only(self):
        assert compute_value("   ", "simple") == 0

    def test_numbers_in_text_ignored(self):
        assert compute_value("ABC123", "simple") == compute_value("ABC", "simple")


class TestReductionCipher:
    """Full Reduction: each letter reduced to single digit."""

    def test_a_is_1(self):
        assert compute_value("A", "reduction") == 1

    def test_j_is_1(self):
        # J=10 → 1+0=1
        assert compute_value("J", "reduction") == 1

    def test_s_is_1(self):
        # S=19 → 1+9=10 → 1+0=1
        assert compute_value("S", "reduction") == 1

    def test_z_is_8(self):
        # Z=26 → 2+6=8
        assert compute_value("Z", "reduction") == 8


class TestReverseCipher:
    """Reverse Ordinal: A=26, B=25, ..., Z=1."""

    def test_a_is_26(self):
        assert compute_value("A", "reverse") == 26

    def test_z_is_1(self):
        assert compute_value("Z", "reverse") == 1

    def test_m_is_14(self):
        # M is 13th letter → reverse = 27-13 = 14
        assert compute_value("M", "reverse") == 14


class TestSumerianCipher:
    """Sumerian: Simple × 6."""

    def test_a_is_6(self):
        assert compute_value("A", "sumerian") == 6

    def test_z_is_156(self):
        assert compute_value("Z", "sumerian") == 156

    def test_is_six_times_simple(self):
        text = "Alexander Whitestone"
        assert compute_value(text, "sumerian") == compute_value(text, "simple") * 6


class TestHebrewCipher:
    """Hebrew-mapped: traditional Hebrew values on Latin alphabet."""

    def test_a_is_1(self):
        assert compute_value("A", "hebrew") == 1

    def test_j_is_10(self):
        assert compute_value("J", "hebrew") == 10

    def test_k_is_20(self):
        assert compute_value("K", "hebrew") == 20

    def test_t_is_200(self):
        assert compute_value("T", "hebrew") == 200


class TestUnknownCipher:
    def test_raises_on_unknown(self):
        with pytest.raises(ValueError, match="Unknown cipher"):
            compute_value("test", "klingon")


# ── compute_all ──────────────────────────────────────────────────────────────


class TestComputeAll:
    def test_returns_all_ciphers(self):
        result = compute_all("ABC")
        assert set(result.keys()) == set(CIPHERS.keys())

    def test_values_are_ints(self):
        result = compute_all("test")
        for v in result.values():
            assert isinstance(v, int)


# ── letter_breakdown ─────────────────────────────────────────────────────────


class TestLetterBreakdown:
    def test_simple_breakdown(self):
        result = letter_breakdown("AB", "simple")
        assert result == [("A", 1), ("B", 2)]

    def test_strips_non_alpha(self):
        result = letter_breakdown("A B!", "simple")
        assert result == [("A", 1), ("B", 2)]

    def test_unknown_cipher_raises(self):
        with pytest.raises(ValueError):
            letter_breakdown("test", "nonexistent")


# ── reduce_number ────────────────────────────────────────────────────────────


class TestReduceNumber:
    def test_single_digit(self):
        assert reduce_number(7) == 7

    def test_double_digit_to_master(self):
        # 29 → 2+9=11 → master number, preserved
        assert reduce_number(29) == 11

    def test_double_digit_non_master(self):
        # 28 → 2+8=10 → 1+0=1
        assert reduce_number(28) == 1

    def test_222_reduces_to_6(self):
        assert reduce_number(222) == 6  # 2+2+2=6

    def test_master_11(self):
        assert reduce_number(11) == 11

    def test_master_22(self):
        assert reduce_number(22) == 22

    def test_master_33(self):
        assert reduce_number(33) == 33

    def test_zero(self):
        assert reduce_number(0) == 0

    def test_negative(self):
        assert reduce_number(-42) == 6  # abs(-42)=42 → 4+2=6

    def test_large_number(self):
        assert reduce_number(9999) == 9  # 9+9+9+9=36 → 3+6=9


# ── factorize ────────────────────────────────────────────────────────────────


class TestFactorize:
    def test_prime(self):
        assert factorize(7) == [7]

    def test_composite(self):
        assert factorize(12) == [2, 2, 3]

    def test_222(self):
        assert factorize(222) == [2, 3, 37]

    def test_one(self):
        assert factorize(1) == [1]

    def test_zero(self):
        assert factorize(0) == []

    def test_large_prime(self):
        assert factorize(997) == [997]


# ── analyze_number ───────────────────────────────────────────────────────────


class TestAnalyzeNumber:
    def test_222(self):
        result = analyze_number(222)
        assert result["value"] == 222
        assert result["numerological_reduction"] == 6
        assert result["prime_factors"] == [2, 3, 37]
        assert result["is_prime"] is False
        assert result["significance"] == NOTABLE_NUMBERS[222]

    def test_prime_detection(self):
        assert analyze_number(17)["is_prime"] is True
        assert analyze_number(18)["is_prime"] is False

    def test_perfect_square(self):
        assert analyze_number(144)["is_perfect_square"] is True
        assert analyze_number(143)["is_perfect_square"] is False

    def test_triangular(self):
        assert analyze_number(6)["is_triangular"] is True  # 1+2+3
        assert analyze_number(7)["is_triangular"] is False

    def test_angel_number(self):
        assert analyze_number(333).get("angel_number") is True
        assert analyze_number(123).get("angel_number") is None

    def test_master_number(self):
        assert analyze_number(22).get("master_number") is True


# ── gematria tool function (the main interface) ─────────────────────────────


class TestGematriaTool:
    """Test the main gematria() tool function that Timmy calls."""

    def test_phrase_mode(self):
        result = gematria("Alexander Whitestone")
        assert "222" in result
        assert "Simple" in result

    def test_number_mode(self):
        result = gematria("222")
        assert "Analysis of 222" in result
        assert "Alexander Whitestone" in result

    def test_compare_mode(self):
        result = gematria("compare: ABC, DEF")
        assert "Comparison" in result
        assert "ABC" in result
        assert "DEF" in result

    def test_compare_needs_two(self):
        result = gematria("compare: alone")
        assert "at least two" in result.lower()

    def test_empty_input(self):
        result = gematria("123 456")
        assert "No alphabetic" in result

    def test_whitespace_handling(self):
        result = gematria("  ABC  ")
        assert "Simple" in result

    def test_letter_breakdown_in_output(self):
        result = gematria("ABC")
        assert "Breakdown" in result
        assert "A(1)" in result

    def test_notable_number_flagged(self):
        result = gematria("Alexander Whitestone")
        assert "★" in result or "222" in result

    def test_numerological_root_shown(self):
        result = gematria("ABC")
        assert "root" in result.lower() or "reduction" in result.lower()
