"""Integration tests for the My Sky dashboard views.

sky_view    — GET /dashboard/sky/  → renders sky template
sky_preview — GET /dashboard/sky/preview  → proxies to PySwiss (no DB write)
sky_save    — POST /dashboard/sky/save    → saves natal data to User model;
              grants Stargazer Note on first save with real chart_data
"""

import json
from unittest.mock import patch, MagicMock

from django.test import TestCase
from django.urls import reverse

from apps.drama.models import Note
from apps.lyric.models import User


class SkyViewTest(TestCase):
    def setUp(self):
        self.user = User.objects.create(email="star@test.io")
        self.client.force_login(self.user)

    def test_sky_view_renders_template(self):
        response = self.client.get(reverse("sky"))
        self.assertEqual(response.status_code, 200)
        self.assertTemplateUsed(response, "apps/dashboard/sky.html")

    def test_sky_view_requires_login(self):
        self.client.logout()
        response = self.client.get(reverse("sky"))
        self.assertEqual(response.status_code, 302)
        self.assertIn("/?next=", response["Location"])

    def test_sky_view_passes_preview_and_save_urls(self):
        response = self.client.get(reverse("sky"))
        self.assertContains(response, reverse("sky_preview"))
        self.assertContains(response, reverse("sky_save"))

    def test_saved_birth_date_renders_in_user_tz_when_set(self):
        """A user w. saved sky_birth_dt + sky_birth_tz hits the astimezone
        branch (views.py L300-306) — saved_birth_date / saved_birth_time
        render in the user's local tz, not UTC."""
        from datetime import datetime
        import zoneinfo
        # 1990-06-15 16:00 UTC = 12:00 PM in America/New_York (EDT, UTC-4)
        self.user.sky_birth_dt = datetime(1990, 6, 15, 16, 0, tzinfo=zoneinfo.ZoneInfo("UTC"))
        self.user.sky_birth_tz = "America/New_York"
        self.user.save()
        response = self.client.get(reverse("sky"))
        self.assertEqual(response.context["saved_birth_date"], "1990-06-15")
        self.assertEqual(response.context["saved_birth_time"], "12:00")

    def test_saved_birth_falls_back_to_utc_when_tz_invalid(self):
        """A garbage sky_birth_tz triggers ZoneInfoNotFoundError — the view
        swallows it (pass) and renders the UTC representation."""
        from datetime import datetime
        import zoneinfo
        self.user.sky_birth_dt = datetime(1990, 6, 15, 16, 0, tzinfo=zoneinfo.ZoneInfo("UTC"))
        self.user.sky_birth_tz = "Not/A/Real_Zone"
        self.user.save()
        response = self.client.get(reverse("sky"))
        # UTC fallback — 16:00 stays 16:00
        self.assertEqual(response.context["saved_birth_time"], "16:00")

    def test_tz_input_is_readonly_and_carries_auto_detect_placeholder(self):
        """Manual TZ edits throw the schedulePreview / PySwiss fetch off (the
        backend gets a stale TZ for the new lat/lon), so the field is render-
        readonly like lat/lon — auto-fills from preview, never from a typed
        override. The old <small id="id_nf_tz_hint"> is gone; its copy lives
        in the placeholder so an empty field is self-explanatory."""
        response = self.client.get(reverse("sky"))
        self.assertContains(response, 'id="id_nf_tz"')
        # readonly + tabindex:-1 mirrors the lat/lon pattern.
        self.assertRegex(
            response.content.decode(),
            r'id="id_nf_tz"[^>]*\breadonly\b',
        )
        self.assertContains(response, 'placeholder="auto-detected from coordinates"')
        self.assertNotContains(response, 'id="id_nf_tz_hint"')


class SkyPreviewTest(TestCase):
    def setUp(self):
        self.user = User.objects.create(email="star@test.io")
        self.client.force_login(self.user)
        self.url = reverse("sky_preview")

    def test_requires_login(self):
        self.client.logout()
        response = self.client.get(self.url, {"date": "1990-06-15", "lat": "51.5", "lon": "-0.1"})
        self.assertEqual(response.status_code, 302)
        self.assertIn("/?next=", response["Location"])

    def test_missing_params_returns_400(self):
        response = self.client.get(self.url, {"date": "1990-06-15"})
        self.assertEqual(response.status_code, 400)

    def test_invalid_lat_returns_400(self):
        response = self.client.get(self.url, {"date": "1990-06-15", "lat": "999", "lon": "0"})
        self.assertEqual(response.status_code, 400)

    @patch("apps.dashboard.views.http_requests")
    def test_proxies_to_pyswiss_and_returns_chart(self, mock_requests):
        chart_payload = {
            "planets": {"Sun": {"degree": 84.5, "sign": "Gemini", "retrograde": False}},
            "houses":  {"cusps": [0]*12},
            "elements": {"Fire": 1, "Earth": 0, "Air": 0, "Water": 0},
            "house_system": "O",
        }
        tz_response = MagicMock()
        tz_response.json.return_value = {"timezone": "Europe/London"}
        tz_response.raise_for_status = MagicMock()

        chart_response = MagicMock()
        chart_response.json.return_value = chart_payload
        chart_response.raise_for_status = MagicMock()

        mock_requests.get.side_effect = [tz_response, chart_response]

        response = self.client.get(self.url, {
            "date": "1990-06-15", "time": "09:30",
            "lat": "51.5074", "lon": "-0.1278",
        })

        self.assertEqual(response.status_code, 200)
        data = response.json()
        self.assertIn("planets", data)
        # Earth→Stone rename applied
        self.assertIn("Stone", data["elements"])
        self.assertNotIn("Earth", data["elements"])
        self.assertIn("timezone", data)
        self.assertIn("distinctions", data)


class SkySaveTest(TestCase):
    def setUp(self):
        self.user = User.objects.create(email="star@test.io")
        self.client.force_login(self.user)
        self.url = reverse("sky_save")

    def _post(self, payload):
        return self.client.post(
            self.url,
            data=json.dumps(payload),
            content_type="application/json",
        )

    def test_requires_login(self):
        self.client.logout()
        response = self._post({})
        self.assertEqual(response.status_code, 302)
        self.assertIn("/?next=", response["Location"])

    def test_get_not_allowed(self):
        response = self.client.get(self.url)
        self.assertEqual(response.status_code, 405)

    def test_saves_sky_fields_to_user(self):
        payload = {
            "birth_dt":    "1990-06-15T08:30:00",
            "birth_lat":   51.5074,
            "birth_lon":   -0.1278,
            "birth_place": "London, UK",
            "house_system": "O",
            "chart_data":  {},
        }
        response = self._post(payload)
        self.assertEqual(response.status_code, 200)

        self.user.refresh_from_db()
        self.assertEqual(str(self.user.sky_birth_dt), "1990-06-15 08:30:00+00:00")
        self.assertAlmostEqual(float(self.user.sky_birth_lat), 51.5074, places=3)
        self.assertAlmostEqual(float(self.user.sky_birth_lon), -0.1278, places=3)
        self.assertEqual(self.user.sky_birth_place, "London, UK")
        self.assertEqual(self.user.sky_house_system, "O")

    def test_invalid_json_returns_400(self):
        response = self.client.post(
            self.url, data="not json", content_type="application/json"
        )
        self.assertEqual(response.status_code, 400)

    def test_response_contains_saved_flag(self):
        payload = {
            "birth_dt":   "1990-06-15T08:30:00",
            "birth_lat":  51.5,
            "birth_lon":  -0.1,
            "birth_place": "",
            "house_system": "O",
            "chart_data": {},
        }
        data = self._post(payload).json()
        self.assertTrue(data["saved"])

    def test_invalid_birth_dt_string_sets_sky_birth_dt_to_none(self):
        payload = {
            "birth_dt": "not-a-date",
            "birth_lat": 51.5,
            "birth_lon": -0.1,
            "birth_place": "",
            "house_system": "O",
            "chart_data": {},
        }
        response = self._post(payload)
        self.assertEqual(response.status_code, 200)
        self.user.refresh_from_db()
        self.assertIsNone(self.user.sky_birth_dt)


class SkyPreviewErrorPathTest(TestCase):
    def setUp(self):
        self.user = User.objects.create(email="star2@test.io")
        self.client.force_login(self.user)
        self.url = reverse("sky_preview")

    def test_non_numeric_lat_returns_400(self):
        response = self.client.get(self.url, {"date": "1990-06-15", "lat": "abc", "lon": "0"})
        self.assertEqual(response.status_code, 400)

    def test_invalid_tz_string_returns_400(self):
        response = self.client.get(
            self.url, {"date": "1990-06-15", "lat": "51.5", "lon": "-0.1", "tz": "Not/ATimezone"}
        )
        self.assertEqual(response.status_code, 400)

    def test_bad_date_format_returns_400(self):
        response = self.client.get(
            self.url,
            {"date": "not-a-date", "time": "09:00", "lat": "51.5", "lon": "-0.1", "tz": "UTC"},
        )
        self.assertEqual(response.status_code, 400)

    @patch("apps.dashboard.views.http_requests")
    def test_pyswiss_tz_failure_falls_back_to_utc_and_continues(self, mock_requests):
        chart_payload = {
            "planets": {"Sun": {"degree": 84.5, "sign": "Gemini", "retrograde": False}},
            "houses": {"cusps": [0] * 12},
            "elements": {},
            "house_system": "O",
        }
        tz_response = MagicMock()
        tz_response.raise_for_status.side_effect = Exception("tz timeout")

        chart_response = MagicMock()
        chart_response.json.return_value = chart_payload
        chart_response.raise_for_status = MagicMock()

        mock_requests.get.side_effect = [tz_response, chart_response]

        response = self.client.get(self.url, {"date": "1990-06-15", "lat": "51.5", "lon": "-0.1"})
        self.assertEqual(response.status_code, 200)
        self.assertEqual(response.json()["timezone"], "UTC")

    @patch("apps.dashboard.views.http_requests")
    def test_pyswiss_chart_failure_returns_502(self, mock_requests):
        tz_response = MagicMock()
        tz_response.json.return_value = {"timezone": "UTC"}
        tz_response.raise_for_status = MagicMock()

        chart_response = MagicMock()
        chart_response.raise_for_status.side_effect = Exception("chart timeout")

        mock_requests.get.side_effect = [tz_response, chart_response]

        response = self.client.get(self.url, {"date": "1990-06-15", "lat": "51.5", "lon": "-0.1"})
        self.assertEqual(response.status_code, 502)


_REAL_CHART = {
    "planets": {"Sun": {"degree": 66.7, "sign": "Gemini", "retrograde": False}},
    "houses":  {"cusps": [180, 210, 240, 270, 300, 330, 0, 30, 60, 90, 120, 150]},
    "elements": {"Fire": 1, "Stone": 2, "Air": 4, "Water": 0, "Time": 1, "Space": 1},
    "aspects": [],
}


class SkySaveNoteTest(TestCase):
    """sky_save grants the Stargazer Note on the first save with real chart_data."""

    def setUp(self):
        self.user = User.objects.create(email="star@test.io")
        self.client.force_login(self.user)
        self.url = reverse("sky_save")

    def _post(self, chart_data=_REAL_CHART):
        return self.client.post(
            self.url,
            data=json.dumps({
                "birth_dt":    "1990-06-15T08:30:00",
                "birth_lat":   51.5074,
                "birth_lon":   -0.1278,
                "birth_place": "London, UK",
                "birth_tz":    "Europe/London",
                "house_system": "O",
                "chart_data":  chart_data,
            }),
            content_type="application/json",
        )

    def test_first_save_with_chart_data_returns_stargazer_brief(self):
        data = self._post().json()
        self.assertIn("brief", data)
        brief = data["brief"]
        self.assertEqual(brief["kind"], "note_unlock")
        self.assertEqual(brief["title"], "Stargazer")
        self.assertIn("Stargazer", brief["line_text"])
        self.assertIn("/billboard/post/", brief["post_url"])
        self.assertEqual(brief["square_url"], "/billboard/my-notes/")
        self.assertIn("created_at", brief)

    def test_first_save_creates_note_in_db(self):
        self._post()
        self.assertEqual(Note.objects.filter(user=self.user, slug="stargazer").count(), 1)

    def test_second_save_returns_null_brief(self):
        self._post()
        data = self._post().json()
        self.assertIsNone(data["brief"])

    def test_second_save_does_not_create_duplicate_note(self):
        self._post()
        self._post()
        self.assertEqual(Note.objects.filter(user=self.user, slug="stargazer").count(), 1)

    def test_save_with_empty_chart_data_does_not_grant_note(self):
        data = self._post(chart_data={}).json()
        self.assertIsNone(data["brief"])
        self.assertEqual(Note.objects.filter(user=self.user, slug="stargazer").count(), 0)

    def test_save_with_null_chart_data_does_not_grant_note(self):
        data = self._post(chart_data=None).json()
        self.assertIsNone(data["brief"])
        self.assertEqual(Note.objects.filter(user=self.user, slug="stargazer").count(), 0)


class SkyDeleteTest(TestCase):
    """POST /dashboard/sky/delete clears all sky fields on the User model and
    redirects back to /dashboard/sky/. The Stargazer Note is preserved (it's
    earned, not stateful)."""

    def setUp(self):
        from datetime import datetime
        import zoneinfo

        self.user = User.objects.create(email="star@test.io")
        self.user.sky_chart_data  = {"planets": {"Sun": {"sign": "Gemini"}}}
        self.user.sky_birth_place = "Baltimore, MD, US"
        self.user.sky_birth_tz    = "America/New_York"
        self.user.sky_birth_lat   = 39.2904
        self.user.sky_birth_lon   = -76.6122
        self.user.sky_birth_dt    = datetime(
            1990, 6, 15, 12, 0, tzinfo=zoneinfo.ZoneInfo("UTC")
        )
        self.user.sky_house_system = "O"
        self.user.save()
        self.client.force_login(self.user)
        self.url = reverse("sky_delete")

    def test_post_clears_all_sky_fields(self):
        self.client.post(self.url)
        self.user.refresh_from_db()
        self.assertIsNone(self.user.sky_chart_data)
        self.assertIsNone(self.user.sky_birth_dt)
        self.assertIsNone(self.user.sky_birth_lat)
        self.assertIsNone(self.user.sky_birth_lon)
        self.assertEqual(self.user.sky_birth_place, "")
        self.assertEqual(self.user.sky_birth_tz, "")

    def test_post_redirects_to_sky_view(self):
        response = self.client.post(self.url)
        self.assertEqual(response.status_code, 302)
        self.assertEqual(response["Location"], reverse("sky"))

    def test_get_returns_405(self):
        response = self.client.get(self.url)
        self.assertEqual(response.status_code, 405)

    def test_requires_login(self):
        self.client.logout()
        response = self.client.post(self.url)
        self.assertEqual(response.status_code, 302)
        self.assertIn("/?next=", response["Location"])

    def test_post_preserves_unrelated_user_fields(self):
        self.user.username = "stargazer-keepme"
        self.user.save()
        self.client.post(self.url)
        self.user.refresh_from_db()
        self.assertEqual(self.user.username, "stargazer-keepme")
