"""app.widgets.calendar -- unit-level, no HTTP: constructs Widget/
CalendarWidgetConfig rows directly and monkeypatches the underlying
fetch calls (get_or_refresh_*_for_widget), which have their own
dedicated fetch/merge test coverage elsewhere (test_calendar_feed.py
etc.) -- this file is about the widget wiring itself: does render()
produce a correctly-sized image, do advance/back move browse_offset."""

from __future__ import annotations

import time

from app import widgets
from app.db import widget_locked
from app.models import CalendarWidgetConfig, Frame, Widget


def _make_widget(db_session, **cfg_kwargs) -> tuple[Frame, Widget]:
    frame = db_session.get(Frame, 1)
    widget = Widget(frame_id=frame.id, widget_type="calendar", x=0, y=0, w=8, h=5,
                    sort_order=0, created_at=time.time())
    db_session.add(widget)
    db_session.flush()
    db_session.add(CalendarWidgetConfig(widget_id=widget.id, **cfg_kwargs))
    db_session.commit()
    return frame, widget


def _stub_fetches(monkeypatch, events=None, weather=None, tasks=None):
    monkeypatch.setattr(widgets.calendar, "get_or_refresh_calendar_events_for_widget",
                        lambda db, frame, widget: (events or [], ""))
    monkeypatch.setattr(widgets.calendar, "get_or_refresh_weather_for_widget",
                        lambda db, frame, widget: weather or [])
    monkeypatch.setattr(widgets.calendar, "get_or_refresh_tasks_for_widget",
                        lambda db, frame, widget: tasks or [])


def test_render_produces_a_correctly_sized_image(db_session, monkeypatch):
    frame, widget = _make_widget(db_session, view="agenda")
    _stub_fetches(monkeypatch)

    img = widgets.calendar.render(db_session, frame, widget, 400, 300)
    assert img.size == (400, 300)
    assert img.mode == "RGB"


def test_render_produces_a_correctly_sized_image_below_full_panel(db_session, monkeypatch):
    """A calendar widget placed smaller than the full panel must still
    come back at exactly the target box -- render_panel's contract --
    now laid out directly at that size (see calendar_render._size_tier)
    rather than built full-size and resized down after the fact."""
    frame, widget = _make_widget(db_session, view="week")
    _stub_fetches(monkeypatch)

    img = widgets.calendar.render(db_session, frame, widget, 250, 150)
    assert img.size == (250, 150)


def test_render_at_minimum_grid_footprint_for_every_view(db_session, monkeypatch):
    """grid.MIN_FOOTPRINT["calendar"] is (3, 2) cells -- on an 8x5 grid
    against a full 800x480 panel that's a 300x192 box, the smallest a
    calendar widget can actually be placed at. Every view (including
    month, which falls back to agenda below the "small" size tier -- see
    calendar_render._month_view_fits) must still render at exactly that
    size without error."""
    from app.calendar_render import CALENDAR_VIEWS

    _stub_fetches(monkeypatch, events=[
        {"summary": "Standup", "start": "2026-08-01T09:00:00+00:00", "end": "2026-08-01T09:15:00+00:00",
         "all_day": False, "sources": [{"owner_display_name": "Alice", "color_index": None}]},
    ])
    for view in CALENDAR_VIEWS:
        frame, widget = _make_widget(db_session, view=view)
        img = widgets.calendar.render(db_session, frame, widget, 300, 192)
        assert img.size == (300, 192), view


def test_render_at_representative_footprints(db_session, monkeypatch):
    """A handful of footprints spanning all three size tiers (see
    calendar_render._size_tier) -- small (below half-panel), medium
    (roughly half-panel, the old photo-inlay's proportions), and large
    (full panel) -- render without error at exactly the requested size."""
    _stub_fetches(monkeypatch)
    for target_w, target_h in [(300, 192), (400, 480), (800, 480)]:
        frame, widget = _make_widget(db_session, view="agenda")
        img = widgets.calendar.render(db_session, frame, widget, target_w, target_h)
        assert img.size == (target_w, target_h)


def test_weather_only_fetched_when_enabled(db_session, monkeypatch):
    frame, widget = _make_widget(db_session, view="agenda", weather_enabled=False)
    calls = []
    monkeypatch.setattr(widgets.calendar, "get_or_refresh_calendar_events_for_widget",
                        lambda db, frame, widget: ([], ""))
    monkeypatch.setattr(widgets.calendar, "get_or_refresh_weather_for_widget",
                        lambda db, frame, widget: calls.append(1) or [])
    monkeypatch.setattr(widgets.calendar, "get_or_refresh_tasks_for_widget",
                        lambda db, frame, widget: [])

    widgets.calendar.render(db_session, frame, widget, 400, 300)
    assert calls == []


def test_tasks_only_fetched_for_week_view_when_enabled(db_session, monkeypatch):
    frame, widget = _make_widget(db_session, view="agenda", tasks_enabled=True)  # not week view
    calls = []
    monkeypatch.setattr(widgets.calendar, "get_or_refresh_calendar_events_for_widget",
                        lambda db, frame, widget: ([], ""))
    monkeypatch.setattr(widgets.calendar, "get_or_refresh_weather_for_widget",
                        lambda db, frame, widget: [])
    monkeypatch.setattr(widgets.calendar, "get_or_refresh_tasks_for_widget",
                        lambda db, frame, widget: calls.append(1) or [])

    widgets.calendar.render(db_session, frame, widget, 400, 300)
    assert calls == []  # agenda view -- tasks never shown, so never fetched


def test_advance_action_increments_browse_offset(db_session):
    frame, widget = _make_widget(db_session, view="week", browse_offset=0)
    widgets.calendar.ACTIONS["advance"](db_session, frame, widget)
    cfg = db_session.get(CalendarWidgetConfig, widget.id)
    assert cfg.browse_offset == 1

    widgets.calendar.ACTIONS["advance"](db_session, frame, widget)
    cfg = db_session.get(CalendarWidgetConfig, widget.id)
    assert cfg.browse_offset == 2  # accumulates, doesn't reset-then-increment


def test_back_action_decrements_browse_offset(db_session):
    frame, widget = _make_widget(db_session, view="week", browse_offset=3)
    widgets.calendar.ACTIONS["back"](db_session, frame, widget)
    cfg = db_session.get(CalendarWidgetConfig, widget.id)
    assert cfg.browse_offset == 2


def test_normal_wake_resets_browse_offset_to_zero(db_session, monkeypatch):
    """A plain /frame/image GET (is_normal_wake=True, the default) should
    snap browse_offset back to "today" if a previous button press had
    moved it -- mirrors the old _render_calendar_mode's identical
    behavior. A button-triggered render (is_normal_wake=False) must NOT
    do this, or every button press would immediately erase its own
    effect on the very next render."""
    frame, widget = _make_widget(db_session, view="week", browse_offset=5)
    _stub_fetches(monkeypatch)

    widgets.calendar.render(db_session, frame, widget, 400, 300, is_normal_wake=True)
    assert db_session.get(CalendarWidgetConfig, widget.id).browse_offset == 0


def test_button_triggered_render_does_not_reset_browse_offset(db_session, monkeypatch):
    frame, widget = _make_widget(db_session, view="week", browse_offset=0)
    _stub_fetches(monkeypatch)

    widgets.calendar.ACTIONS["advance"](db_session, frame, widget)
    assert db_session.get(CalendarWidgetConfig, widget.id).browse_offset == 1

    widgets.calendar.render(db_session, frame, widget, 400, 300, is_normal_wake=False)
    assert db_session.get(CalendarWidgetConfig, widget.id).browse_offset == 1


# --- calendar_render's size tiers ---

def test_size_tier_thresholds():
    from app.calendar_render import _size_tier

    assert _size_tier(800, 480) == "large"        # full panel
    assert _size_tier(400, 480) == "medium"        # old photo-inlay's half-panel split
    assert _size_tier(300, 192) == "small"         # grid.MIN_FOOTPRINT["calendar"]'s 3x2 cells


def test_month_view_falls_back_to_agenda_layout_below_small_tier(db_session, monkeypatch):
    """Month view needs real column width to stay legible -- at the
    minimum calendar footprint, render() should still succeed (producing
    an agenda-shaped render instead of an unreadable grid), not error or
    silently draw garbage."""
    from app.calendar_render import _month_view_fits

    assert not _month_view_fits(300, 192)
    assert _month_view_fits(800, 480)

    frame, widget = _make_widget(db_session, view="month")
    _stub_fetches(monkeypatch)
    img = widgets.calendar.render(db_session, frame, widget, 300, 192)
    assert img.size == (300, 192)
