from __future__ import annotations

from datetime import UTC, datetime, timedelta
from unittest import TestCase

from app.routers.restaurants import (
    _ALLOWED_TREND_DAYS,
    _REQUIRED_SOURCES,
)


class TestReservationsTrendEndpoint(TestCase):
    def test_allowed_days_presets(self) -> None:
        self.assertEqual(_ALLOWED_TREND_DAYS, {7, 30, 90})

    def test_default_days_is_30(self) -> None:
        self.assertIn(30, _ALLOWED_TREND_DAYS)


class TestSourceBreakdownNormalization(TestCase):
    def test_required_sources_are_complete(self) -> None:
        self.assertEqual(
            _REQUIRED_SOURCES,
            ["widget", "agent", "manual"],
        )

    def test_zero_fill_produces_all_required_sources(self) -> None:
        counts: dict[str, int] = {}
        result = [{"source": s, "count": counts.get(s, 0)} for s in _REQUIRED_SOURCES]
        self.assertEqual(len(result), 3)
        for entry in result:
            self.assertEqual(entry["count"], 0)
            self.assertIn(entry["source"], _REQUIRED_SOURCES)

    def test_zero_fill_with_partial_data(self) -> None:
        counts = {"widget": 10, "agent": 5}
        result = [{"source": s, "count": counts.get(s, 0)} for s in _REQUIRED_SOURCES]
        self.assertEqual(result[0], {"source": "widget", "count": 10})
        self.assertEqual(result[1], {"source": "agent", "count": 5})
        self.assertEqual(result[2], {"source": "manual", "count": 0})


class TestTrendSeriesZeroFill(TestCase):
    def test_zero_fill_covers_full_range(self) -> None:
        days = 7
        now_utc = datetime(2026, 2, 24, 12, 0, tzinfo=UTC)
        start_date = (now_utc - timedelta(days=days)).date()
        end_date = now_utc.date()

        counts_by_date: dict[str, int] = {"2026-02-20": 3, "2026-02-22": 1}
        series: list[dict[str, object]] = []
        cursor = start_date
        while cursor <= end_date:
            iso = cursor.isoformat()
            series.append({"date": iso, "count": counts_by_date.get(iso, 0)})
            cursor += timedelta(days=1)

        self.assertEqual(len(series), 8)
        self.assertEqual(series[0]["date"], "2026-02-17")
        self.assertEqual(series[-1]["date"], "2026-02-24")
        self.assertEqual(series[3]["count"], 3)
        self.assertEqual(series[5]["count"], 1)
        self.assertEqual(series[1]["count"], 0)


class TestStatsEndpointResponseShape(TestCase):
    """Verify the expected response contracts for dashboard stats endpoints."""

    REQUIRED_STATS_KEYS = {
        "reservations_today",
        "reservations_this_week",
        "orders_today",
        "revenue_total_cents",
        "revenue_total_eur",
        "conversations_today",
        "messages_today",
    }

    def test_stats_keys_are_complete(self) -> None:
        """A stats response dict must contain all required keys."""
        sample_stats = {
            "reservations_today": 5,
            "reservations_this_week": 20,
            "orders_today": 3,
            "revenue_total_cents": 15000,
            "revenue_total_eur": "150.00",
            "conversations_today": 8,
            "messages_today": 42,
        }
        self.assertEqual(set(sample_stats.keys()), self.REQUIRED_STATS_KEYS)

    def test_revenue_eur_is_formatted_from_cents(self) -> None:
        """revenue_total_eur must be cents / 100 formatted to 2 decimals."""
        cents = 12345
        eur = f"{cents / 100:.2f}"
        self.assertEqual(eur, "123.45")

    def test_revenue_eur_zero(self) -> None:
        cents = 0
        eur = f"{cents / 100:.2f}"
        self.assertEqual(eur, "0.00")

    def test_stats_values_are_non_negative(self) -> None:
        """All numeric stats fields must be non-negative."""
        sample_stats = {
            "reservations_today": 0,
            "reservations_this_week": 0,
            "orders_today": 0,
            "revenue_total_cents": 0,
            "revenue_total_eur": "0.00",
            "conversations_today": 0,
            "messages_today": 0,
        }
        non_negative_keys = [
            "reservations_today",
            "reservations_this_week",
            "orders_today",
            "revenue_total_cents",
            "conversations_today",
            "messages_today",
        ]
        for key in non_negative_keys:
            self.assertGreaterEqual(int(sample_stats[key]), 0)


class TestReservationsTrendResponseContract(TestCase):
    """Verify reservations-trend endpoint response structure."""

    def test_trend_series_entry_has_required_keys(self) -> None:
        entry = {"date": "2026-02-20", "count": 5}
        self.assertIn("date", entry)
        self.assertIn("count", entry)

    def test_trend_series_date_is_iso_format(self) -> None:
        from datetime import date

        iso = "2026-02-20"
        parsed = date.fromisoformat(iso)
        self.assertEqual(parsed.isoformat(), iso)

    def test_invalid_days_clamps_to_30(self) -> None:
        """Days not in _ALLOWED_TREND_DAYS should be treated as 30."""
        days = 15
        if days not in _ALLOWED_TREND_DAYS:
            days = 30
        self.assertEqual(days, 30)

    def test_valid_days_are_not_clamped(self) -> None:
        for valid in _ALLOWED_TREND_DAYS:
            days = valid
            if days not in _ALLOWED_TREND_DAYS:
                days = 30
            self.assertEqual(days, valid)

    def test_trend_series_length_matches_day_range(self) -> None:
        """A 7-day trend should produce 8 entries (day 0 through day 7 inclusive)."""
        days = 7
        now_utc = datetime(2026, 2, 24, 12, 0, tzinfo=UTC)
        start_date = (now_utc - timedelta(days=days)).date()
        end_date = now_utc.date()

        series: list[dict[str, object]] = []
        cursor = start_date
        while cursor <= end_date:
            series.append({"date": cursor.isoformat(), "count": 0})
            cursor += timedelta(days=1)

        self.assertEqual(len(series), days + 1)


class TestSourceBreakdownResponseContract(TestCase):
    """Verify channel-breakdown endpoint response structure."""

    def test_breakdown_always_returns_three_sources(self) -> None:
        counts: dict[str, int] = {}
        result = [{"source": s, "count": counts.get(s, 0)} for s in _REQUIRED_SOURCES]
        self.assertEqual(len(result), len(_REQUIRED_SOURCES))

    def test_each_entry_has_source_and_count_keys(self) -> None:
        counts: dict[str, int] = {"widget": 1}
        result = [{"source": s, "count": counts.get(s, 0)} for s in _REQUIRED_SOURCES]
        for entry in result:
            self.assertIn("source", entry)
            self.assertIn("count", entry)
            self.assertIsInstance(entry["count"], int)


class TestLocalDayWindowUtcNaive(TestCase):
    """Verify the timezone-aware day window helper.

    Returns are naive-UTC boundaries spanning one restaurant-local
    calendar day — matches the storage convention of
    ``Reservation.reserved_at`` / ``end_time``.
    """

    def test_utc_timezone_produces_correct_boundaries(self) -> None:
        from app.routers.restaurants import _local_day_window

        now = datetime(2026, 2, 25, 14, 30, tzinfo=UTC)
        today_start, today_end, week_start = _local_day_window("UTC", now_utc=now)

        # In UTC, the local day matches the UTC day exactly.
        self.assertEqual(today_start.hour, 0)
        self.assertEqual(today_start.minute, 0)
        self.assertEqual(today_end.hour, 0)  # exclusive next midnight
        self.assertEqual(week_start.weekday(), 0)  # Monday

    def test_invalid_timezone_falls_back_to_utc(self) -> None:
        from app.routers.restaurants import _local_day_window

        # Default fallback is Europe/Brussels (UTC+1 in winter); 14:30 UTC
        # is still within the same local day. Boundaries land 1h before
        # local midnight: 23:00 UTC the previous day and 23:00 UTC same day.
        now = datetime(2026, 2, 25, 14, 30, tzinfo=UTC)
        today_start, today_end, _ = _local_day_window("Invalid/Zone", now_utc=now)
        self.assertEqual(today_start.hour, 23)
        self.assertEqual(today_start.day, 24)
        self.assertEqual(today_end.hour, 23)
        self.assertEqual(today_end.day, 25)

    def test_positive_offset_timezone_shifts_boundaries(self) -> None:
        from app.routers.restaurants import _local_day_window

        # At UTC 23:00 the Amsterdam local clock reads 00:00 next day
        # (winter, CET = UTC+1). The naive-UTC representation of local
        # midnight Feb 26 is therefore 23:00 UTC on Feb 25.
        now = datetime(2026, 2, 25, 23, 0, tzinfo=UTC)
        today_start, today_end, _ = _local_day_window("Europe/Amsterdam", now_utc=now)
        self.assertEqual(today_start.day, 25)
        self.assertEqual(today_start.hour, 23)
        self.assertEqual(today_end.day, 26)
        self.assertEqual(today_end.hour, 23)
