"""
Integration tests for the PySwiss chart calculation API.

These tests drive the TDD implementation of GET /api/chart/ and GET /api/tz/.
They verify the HTTP contract using Django's test client.

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


# ---------------------------------------------------------------------------
# Helpers
# ---------------------------------------------------------------------------

# J2000.0 — a well-known reference point: Sun at ~280.37° (Capricorn 10°22')
J2000 = '2000-01-01T12:00:00Z'
LONDON = {'lat': 51.5074, 'lon': -0.1278}

# Well-known coordinates with unambiguous timezone results
NEW_YORK  = {'lat': 40.7128,  'lon': -74.0060}   # America/New_York
TOKYO     = {'lat': 35.6762,  'lon': 139.6503}   # Asia/Tokyo
REYKJAVIK = {'lat': 64.1355,  'lon': -21.8954}   # Atlantic/Reykjavik


class ChartApiTest(TestCase):
    """GET /api/chart/ — calculate a natal chart from datetime + coordinates."""

    def _get(self, params):
        return self.client.get('/api/chart/', params)

    # ── guards ────────────────────────────────────────────────────────────

    def test_chart_returns_400_if_dt_missing(self):
        response = self._get({'lat': 51.5074, 'lon': -0.1278})
        self.assertEqual(response.status_code, 400)

    def test_chart_returns_400_if_lat_missing(self):
        response = self._get({'dt': J2000, 'lon': -0.1278})
        self.assertEqual(response.status_code, 400)

    def test_chart_returns_400_if_lon_missing(self):
        response = self._get({'dt': J2000, 'lat': 51.5074})
        self.assertEqual(response.status_code, 400)

    def test_chart_returns_400_for_invalid_dt_format(self):
        response = self._get({'dt': 'not-a-date', **LONDON})
        self.assertEqual(response.status_code, 400)

    def test_chart_returns_400_for_out_of_range_lat(self):
        response = self._get({'dt': J2000, 'lat': 999, 'lon': -0.1278})
        self.assertEqual(response.status_code, 400)

    # ── response shape ────────────────────────────────────────────────────

    def test_chart_returns_200_for_valid_params(self):
        response = self._get({'dt': J2000, **LONDON})
        self.assertEqual(response.status_code, 200)

    def test_chart_response_is_json(self):
        response = self._get({'dt': J2000, **LONDON})
        self.assertIn('application/json', response['Content-Type'])

    def test_chart_returns_all_ten_planets(self):
        data = self._get({'dt': J2000, **LONDON}).json()
        expected = {
            'Sun', 'Moon', 'Mercury', 'Venus', 'Mars',
            'Jupiter', 'Saturn', 'Uranus', 'Neptune', 'Pluto',
        }
        self.assertEqual(set(data['planets'].keys()), expected)

    def test_each_planet_has_sign_degree_and_retrograde(self):
        data = self._get({'dt': J2000, **LONDON}).json()
        for name, planet in data['planets'].items():
            with self.subTest(planet=name):
                self.assertIn('sign', planet)
                self.assertIn('degree', planet)
                self.assertIn('retrograde', planet)

    def test_chart_returns_houses(self):
        data = self._get({'dt': J2000, **LONDON}).json()
        houses = data['houses']
        self.assertEqual(len(houses['cusps']), 12)
        self.assertIn('asc', houses)
        self.assertIn('mc', houses)

    def test_chart_returns_six_element_counts(self):
        """Fire/Water/Earth/Air are sign-based counts; Time/Space are emergent."""
        data = self._get({'dt': J2000, **LONDON}).json()
        for key in ('Fire', 'Water', 'Earth', 'Air', 'Time', 'Space'):
            with self.subTest(element=key):
                self.assertIn(key, data['elements'])

    def test_chart_reports_active_house_system(self):
        data = self._get({'dt': J2000, **LONDON}).json()
        self.assertIn('house_system', data)

    # ── calculation correctness ───────────────────────────────────────────

    def test_sun_is_in_capricorn_at_j2000(self):
        """Regression: Sun at J2000.0 is ~280.37° — Capricorn."""
        data = self._get({'dt': J2000, **LONDON}).json()
        sun = data['planets']['Sun']
        self.assertEqual(sun['sign'], 'Capricorn')
        self.assertAlmostEqual(sun['degree'], 280.37, delta=0.1)

    def test_sun_is_not_retrograde(self):
        """The Sun never goes retrograde."""
        data = self._get({'dt': J2000, **LONDON}).json()
        self.assertFalse(data['planets']['Sun']['retrograde'])

    def test_element_counts_sum_to_ten(self):
        """All 10 planets are assigned to exactly one classical element."""
        data = self._get({'dt': J2000, **LONDON}).json()
        classical = sum(
            data['elements'][e]['count'] for e in ('Fire', 'Water', 'Earth', 'Air')
        )
        self.assertEqual(classical, 10)

    def test_each_element_has_count_key(self):
        data = self._get({'dt': J2000, **LONDON}).json()
        for key in ('Fire', 'Water', 'Earth', 'Air', 'Time', 'Space'):
            with self.subTest(element=key):
                self.assertIn('count', data['elements'][key])

    def test_classic_elements_have_contributors(self):
        data = self._get({'dt': J2000, **LONDON}).json()
        for key in ('Fire', 'Water', 'Earth', 'Air'):
            with self.subTest(element=key):
                self.assertIn('contributors', data['elements'][key])

    def test_time_has_stellia(self):
        data = self._get({'dt': J2000, **LONDON}).json()
        self.assertIn('stellia', data['elements']['Time'])

    def test_space_has_parades(self):
        data = self._get({'dt': J2000, **LONDON}).json()
        self.assertIn('parades', data['elements']['Space'])

    def test_each_planet_has_speed(self):
        data = self._get({'dt': J2000, **LONDON}).json()
        for name, planet in data['planets'].items():
            with self.subTest(planet=name):
                self.assertIn('speed', planet)

    def test_each_aspect_has_applying_planet(self):
        data = self._get({'dt': J2000, **LONDON}).json()
        for aspect in data['aspects']:
            with self.subTest(aspect=aspect):
                self.assertIn('applying_planet', aspect)

    # ── house system ──────────────────────────────────────────────────────

    def test_default_house_system_is_porphyry(self):
        """Porphyry ('O') is the project default — no param needed."""
        data = self._get({'dt': J2000, **LONDON}).json()
        self.assertEqual(data['house_system'], 'O')

    def test_non_superuser_cannot_override_house_system(self):
        """House system override is superuser-only; plain requests get 403."""
        response = self._get({'dt': J2000, **LONDON, 'house_system': 'P'})
        self.assertEqual(response.status_code, 403)

    # ── aspects ───────────────────────────────────────────────────────────

    def test_chart_returns_aspects_list(self):
        data = self._get({'dt': J2000, **LONDON}).json()
        self.assertIn('aspects', data)
        self.assertIsInstance(data['aspects'], list)

    def test_each_aspect_has_required_fields(self):
        data = self._get({'dt': J2000, **LONDON}).json()
        for aspect in data['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_sun_saturn_trine_present_at_j2000(self):
        """Sun ~280.37° (Capricorn) and Saturn ~40.73° (Taurus) are ~120.36° apart — Trine."""
        data = self._get({'dt': J2000, **LONDON}).json()
        pairs = {(a['planet1'], a['planet2'], a['type']) for a in data['aspects']}
        self.assertIn(('Sun', 'Saturn', 'Trine'), pairs)


class TimezoneApiTest(TestCase):
    """GET /api/tz/ — resolve IANA timezone from lat/lon coordinates."""

    def _get(self, params):
        return self.client.get('/api/tz/', params)

    # ── guards ────────────────────────────────────────────────────────────

    def test_returns_400_if_lat_missing(self):
        response = self._get({'lon': -74.0060})
        self.assertEqual(response.status_code, 400)

    def test_returns_400_if_lon_missing(self):
        response = self._get({'lat': 40.7128})
        self.assertEqual(response.status_code, 400)

    def test_returns_400_for_invalid_lat(self):
        response = self._get({'lat': 'abc', 'lon': -74.0060})
        self.assertEqual(response.status_code, 400)

    def test_returns_400_for_out_of_range_lat(self):
        response = self._get({'lat': 999, 'lon': -74.0060})
        self.assertEqual(response.status_code, 400)

    def test_returns_400_for_out_of_range_lon(self):
        response = self._get({'lat': 40.7128, 'lon': 999})
        self.assertEqual(response.status_code, 400)

    # ── response shape ────────────────────────────────────────────────────

    def test_returns_200_for_valid_coords(self):
        response = self._get(NEW_YORK)
        self.assertEqual(response.status_code, 200)

    def test_response_is_json(self):
        response = self._get(NEW_YORK)
        self.assertIn('application/json', response['Content-Type'])

    def test_response_contains_timezone_key(self):
        data = self._get(NEW_YORK).json()
        self.assertIn('timezone', data)

    def test_timezone_is_a_string(self):
        data = self._get(NEW_YORK).json()
        self.assertIsInstance(data['timezone'], str)

    # ── correctness ───────────────────────────────────────────────────────

    def test_new_york_timezone(self):
        data = self._get(NEW_YORK).json()
        self.assertEqual(data['timezone'], 'America/New_York')

    def test_tokyo_timezone(self):
        data = self._get(TOKYO).json()
        self.assertEqual(data['timezone'], 'Asia/Tokyo')

    def test_reykjavik_timezone(self):
        data = self._get(REYKJAVIK).json()
        self.assertEqual(data['timezone'], 'Atlantic/Reykjavik')
