"""
Unit tests for calc.py helper functions.

These tests verify pure calculation logic without hitting the database
or the Swiss Ephemeris — all inputs are fixed synthetic data.

Run:
    pyswiss/.venv/Scripts/python pyswiss/manage.py test pyswiss/apps/charts
"""
from django.test import SimpleTestCase

from apps.charts.calc import calculate_aspects, get_element_counts


# ---------------------------------------------------------------------------
# FAKE_PLANETS_ASPECTS — degrees only; used by calculate_aspects tests.
# Each planet also carries a speed (deg/day) for applying_planet tests.
# ---------------------------------------------------------------------------

FAKE_PLANETS = {
    'Sun':     {'degree': 10.0,  'speed':  1.00},  # Aries
    'Moon':    {'degree': 130.0, 'speed': 13.00},  # Leo        — 120° from Sun   → Trine
    'Mercury': {'degree': 250.0, 'speed':  1.50},  # Sagittarius — 120° from Sun   → Trine
    'Venus':   {'degree': 40.0,  'speed':  1.10},  # Taurus      —  90° from Moon  → Square
    'Mars':    {'degree': 160.0, 'speed':  0.50},  # Virgo       —  60° from Neptune → Sextile
    'Jupiter': {'degree': 280.0, 'speed':  0.08},  # Capricorn   — 120° from Mars  → Trine
    'Saturn':  {'degree': 70.0,  'speed':  0.03},  # Gemini      — 120° from Uranus → Trine
    'Uranus':  {'degree': 310.0, 'speed':  0.01},  # Aquarius    —  60° from Sun (wrap) → Sextile
    'Neptune': {'degree': 100.0, 'speed':  0.006}, # Cancer
    'Pluto':   {'degree': 340.0, 'speed':  0.003}, # Pisces
}

# ---------------------------------------------------------------------------
# FAKE_PLANETS_ELEMENTS — sign + degree + speed; used by get_element_counts.
# Designed to produce a known stellium and parade.
#
# Occupied signs: Aries(0), Taurus(1), Gemini(2), Leo(4), Virgo(5),
#                 Scorpio(7), Capricorn(9), Aquarius(10)
# Gaps at Cancer(3), Libra(6), Sagittarius(8), Pisces(11) prevent wrap-around.
#
# Consecutive runs:  Aries→Taurus→Gemini = 3  ← parade (Space = 2)
#                    Leo→Virgo = 2
#                    Capricorn→Aquarius = 2
#
# Time = 2  (Aries has Sun+Mercury+Venus → stellium of 3, bonus = 2)
# Space = 2 (Aries→Taurus→Gemini = 3-sign parade, bonus = 2)
# Classic: Fire=4, Earth=3, Air=2, Water=1
# ---------------------------------------------------------------------------

FAKE_PLANETS_ELEMENTS = {
    'Sun':     {'sign': 'Aries',      'degree':  10.0, 'speed':  1.00},  # Fire, stellium
    'Moon':    {'sign': 'Taurus',     'degree':  40.0, 'speed': 13.00},  # Earth, parade
    'Mercury': {'sign': 'Aries',      'degree':  20.0, 'speed':  1.50},  # Fire, stellium
    'Venus':   {'sign': 'Aries',      'degree':  25.0, 'speed':  1.10},  # Fire, stellium
    'Mars':    {'sign': 'Leo',        'degree': 130.0, 'speed':  0.50},  # Fire
    'Jupiter': {'sign': 'Scorpio',    'degree': 220.0, 'speed':  0.08},  # Water
    'Saturn':  {'sign': 'Gemini',     'degree':  70.0, 'speed':  0.03},  # Air, parade
    'Uranus':  {'sign': 'Aquarius',   'degree': 310.0, 'speed':  0.01},  # Air
    'Neptune': {'sign': 'Capricorn',  'degree': 270.0, 'speed':  0.006}, # Earth
    'Pluto':   {'sign': 'Virgo',      'degree': 160.0, 'speed':  0.003}, # Earth
}


def _aspect_pairs(aspects):
    """Return a set of (planet1, planet2, type) tuples for easy assertion."""
    return {(a['planet1'], a['planet2'], a['type']) for a in aspects}


# ===========================================================================
# get_element_counts — enriched shape
# ===========================================================================

class GetElementCountsTest(SimpleTestCase):

    def setUp(self):
        self.counts = get_element_counts(FAKE_PLANETS_ELEMENTS)

    # ── top-level keys ───────────────────────────────────────────────────────

    def test_returns_all_six_elements(self):
        for key in ('Fire', 'Earth', 'Air', 'Water', 'Time', 'Space'):
            with self.subTest(key=key):
                self.assertIn(key, self.counts)

    # ── classic four — count + contributors ──────────────────────────────────

    def test_classic_element_has_count_key(self):
        self.assertIn('count', self.counts['Fire'])

    def test_classic_element_has_contributors_key(self):
        self.assertIn('contributors', self.counts['Fire'])

    def test_fire_count_is_correct(self):
        # Sun + Mercury + Venus (Aries) + Mars (Leo) = 4
        self.assertEqual(self.counts['Fire']['count'], 4)

    def test_earth_count_is_correct(self):
        # Moon (Taurus) + Neptune (Capricorn) + Pluto (Virgo) = 3
        self.assertEqual(self.counts['Earth']['count'], 3)

    def test_air_count_is_correct(self):
        # Saturn (Gemini) + Uranus (Aquarius) = 2
        self.assertEqual(self.counts['Air']['count'], 2)

    def test_water_count_is_correct(self):
        # Jupiter (Scorpio) = 1
        self.assertEqual(self.counts['Water']['count'], 1)

    def test_fire_contributors_contains_expected_planets(self):
        planets = {c['planet'] for c in self.counts['Fire']['contributors']}
        self.assertEqual(planets, {'Sun', 'Mercury', 'Venus', 'Mars'})

    def test_contributor_has_planet_and_sign_keys(self):
        contrib = self.counts['Fire']['contributors'][0]
        self.assertIn('planet', contrib)
        self.assertIn('sign', contrib)

    def test_fire_contributor_signs_are_correct(self):
        sign_map = {c['planet']: c['sign'] for c in self.counts['Fire']['contributors']}
        self.assertEqual(sign_map['Sun'], 'Aries')
        self.assertEqual(sign_map['Mercury'], 'Aries')
        self.assertEqual(sign_map['Venus'], 'Aries')
        self.assertEqual(sign_map['Mars'], 'Leo')

    # ── Time — count + stellia ───────────────────────────────────────────────

    def test_time_has_count_key(self):
        self.assertIn('count', self.counts['Time'])

    def test_time_has_stellia_key(self):
        self.assertIn('stellia', self.counts['Time'])

    def test_time_count_is_correct(self):
        # Aries has 3 planets → bonus = 2
        self.assertEqual(self.counts['Time']['count'], 2)

    def test_time_stellia_is_a_list(self):
        self.assertIsInstance(self.counts['Time']['stellia'], list)

    def test_time_stellia_contains_one_entry(self):
        self.assertEqual(len(self.counts['Time']['stellia']), 1)

    def test_time_stellium_sign_is_aries(self):
        self.assertEqual(self.counts['Time']['stellia'][0]['sign'], 'Aries')

    def test_time_stellium_planets_are_correct(self):
        planet_names = {p['planet'] for p in self.counts['Time']['stellia'][0]['planets']}
        self.assertEqual(planet_names, {'Sun', 'Mercury', 'Venus'})

    def test_time_stellium_planet_entries_have_sign(self):
        for entry in self.counts['Time']['stellia'][0]['planets']:
            with self.subTest(planet=entry['planet']):
                self.assertEqual(entry['sign'], 'Aries')

    # ── Space — count + parades ──────────────────────────────────────────────

    def test_space_has_count_key(self):
        self.assertIn('count', self.counts['Space'])

    def test_space_has_parades_key(self):
        self.assertIn('parades', self.counts['Space'])

    def test_space_count_is_correct(self):
        # Aries→Taurus→Gemini = 3 consecutive → bonus = 2
        self.assertEqual(self.counts['Space']['count'], 2)

    def test_space_parades_is_a_list(self):
        self.assertIsInstance(self.counts['Space']['parades'], list)

    def test_space_parades_contains_one_entry(self):
        self.assertEqual(len(self.counts['Space']['parades']), 1)

    def test_space_parade_signs_are_correct(self):
        self.assertEqual(
            self.counts['Space']['parades'][0]['signs'],
            ['Aries', 'Taurus', 'Gemini'],
        )

    def test_space_parade_planets_are_correct(self):
        planet_names = {p['planet'] for p in self.counts['Space']['parades'][0]['planets']}
        self.assertEqual(planet_names, {'Sun', 'Mercury', 'Venus', 'Moon', 'Saturn'})

    def test_space_parade_planet_entries_have_planet_and_sign(self):
        for entry in self.counts['Space']['parades'][0]['planets']:
            with self.subTest(planet=entry['planet']):
                self.assertIn('planet', entry)
                self.assertIn('sign', entry)


# ===========================================================================
# calculate_aspects
# ===========================================================================

class CalculateAspectsTest(SimpleTestCase):

    def setUp(self):
        self.aspects = calculate_aspects(FAKE_PLANETS)

    # ── return shape ──────────────────────────────────────────────────────

    def test_returns_a_list(self):
        self.assertIsInstance(self.aspects, list)

    def test_each_aspect_has_required_keys(self):
        for aspect in self.aspects:
            with self.subTest(aspect=aspect):
                self.assertIn('planet1', aspect)
                self.assertIn('planet2', aspect)
                self.assertIn('type', aspect)
                self.assertIn('angle', aspect)
                self.assertIn('orb', aspect)

    def test_each_aspect_has_applying_planet_key(self):
        for aspect in self.aspects:
            with self.subTest(aspect=aspect):
                self.assertIn('applying_planet', aspect)

    def test_applying_planet_is_one_of_the_pair(self):
        for aspect in self.aspects:
            with self.subTest(aspect=aspect):
                self.assertIn(
                    aspect['applying_planet'],
                    (aspect['planet1'], aspect['planet2']),
                )

    def test_applying_planet_is_the_faster_body(self):
        """Moon (13.0°/day) applies to Sun (1.0°/day) in their Trine."""
        sun_moon = next(
            a for a in self.aspects
            if {a['planet1'], a['planet2']} == {'Sun', 'Moon'}
        )
        self.assertEqual(sun_moon['applying_planet'], 'Moon')

    def test_each_aspect_type_is_a_known_name(self):
        known = {
            'Conjunction', 'Semisextile', 'Semisquare', 'Sextile', 'Square',
            'Trine', 'Sesquiquadrate', 'Quincunx', 'Opposition',
        }
        for aspect in self.aspects:
            with self.subTest(aspect=aspect):
                self.assertIn(aspect['type'], known)

    def test_angle_and_orb_are_floats(self):
        for aspect in self.aspects:
            with self.subTest(aspect=aspect):
                self.assertIsInstance(aspect['angle'], float)
                self.assertIsInstance(aspect['orb'], float)

    def test_no_self_aspects(self):
        for aspect in self.aspects:
            self.assertNotEqual(aspect['planet1'], aspect['planet2'])

    def test_no_duplicate_pairs(self):
        pairs = [(a['planet1'], a['planet2']) for a in self.aspects]
        self.assertEqual(len(pairs), len(set(pairs)))

    # ── known aspects in FAKE_PLANETS ────────────────────────────────────

    def test_sun_moon_trine(self):
        """Moon at 130° is exactly 120° from Sun at 10°."""
        pairs = _aspect_pairs(self.aspects)
        self.assertIn(('Sun', 'Moon', 'Trine'), pairs)

    def test_sun_mercury_trine(self):
        """Mercury at 250° wraps to 120° from Sun at 10° (360-250+10=120)."""
        pairs = _aspect_pairs(self.aspects)
        self.assertIn(('Sun', 'Mercury', 'Trine'), pairs)

    def test_moon_mercury_trine(self):
        """Moon 130° → Mercury 250° = 120°."""
        pairs = _aspect_pairs(self.aspects)
        self.assertIn(('Moon', 'Mercury', 'Trine'), pairs)

    def test_moon_venus_square(self):
        """Moon 130° → Venus 40° = 90°."""
        pairs = _aspect_pairs(self.aspects)
        self.assertIn(('Moon', 'Venus', 'Square'), pairs)

    def test_venus_neptune_sextile(self):
        """Venus 40° → Neptune 100° = 60°."""
        pairs = _aspect_pairs(self.aspects)
        self.assertIn(('Venus', 'Neptune', 'Sextile'), pairs)

    def test_mars_neptune_sextile(self):
        """Mars 160° → Neptune 100° = 60°."""
        pairs = _aspect_pairs(self.aspects)
        self.assertIn(('Mars', 'Neptune', 'Sextile'), pairs)

    def test_sun_uranus_sextile(self):
        """Sun 10° → Uranus 310° — angle = |10-310| = 300° → 360-300 = 60°."""
        pairs = _aspect_pairs(self.aspects)
        self.assertIn(('Sun', 'Uranus', 'Sextile'), pairs)

    def test_mars_jupiter_trine(self):
        """Mars 160° → Jupiter 280° = 120°."""
        pairs = _aspect_pairs(self.aspects)
        self.assertIn(('Mars', 'Jupiter', 'Trine'), pairs)

    def test_saturn_uranus_trine(self):
        """Saturn 70° → Uranus 310° = |70-310| = 240° → 360-240 = 120°."""
        pairs = _aspect_pairs(self.aspects)
        self.assertIn(('Saturn', 'Uranus', 'Trine'), pairs)

    # ── orb bounds ────────────────────────────────────────────────────────

    def test_orb_is_within_allowed_maximum(self):
        max_orbs = {
            'Conjunction':     8.0,
            'Semisextile':     4.0,
            'Semisquare':      4.0,
            'Sextile':         6.0,
            'Square':          8.0,
            'Trine':           8.0,
            'Sesquiquadrate':  4.0,
            'Quincunx':        5.0,
            'Opposition':     10.0,
        }
        for aspect in self.aspects:
            with self.subTest(aspect=aspect):
                self.assertLessEqual(
                    aspect['orb'], max_orbs[aspect['type']],
                    msg=f"{aspect['planet1']}-{aspect['planet2']} orb exceeds maximum",
                )

    def test_exact_trine_has_zero_orb(self):
        """Sun-Moon at exactly 120° should report orb of 0.0."""
        sun_moon = next(
            a for a in self.aspects
            if a['planet1'] == 'Sun' and a['planet2'] == 'Moon'
        )
        self.assertAlmostEqual(sun_moon['orb'], 0.0, places=5)
