"""End-to-end HTTP tests for the widget-system cutover in
routers/device.py -- /frame/image, /frame/advance, /frame/back, and
/frame/share against real widget rows (via the migration-backfilled
frame #1, or a purpose-built second frame), a real TestClient, real
render_panel/compose_into. Only Immich itself is mocked (monkeypatched
at the app.widgets.photos module boundary, same pattern as
test_widgets_photos.py) -- everything else in the pipeline is real.

This is the one place in the suite that actually exercises
routers/device.py's dispatch over HTTP; the render-size-invariant tests
exercise the renderers directly, and the widget unit tests exercise
app/widgets/*.py directly, but neither proves device.py's own wiring
(the compositor, the button-action runner, the manage-overlay hookup)
is correct -- that's what this file is for."""

from __future__ import annotations

import time

from app import widgets
from app.models import (
    CalendarWidgetConfig,
    Frame,
    FrameButtonAction,
    PhotoWidgetConfig,
    User,
    Widget,
)

EXPECTED_BYTES = 800 * 480 // 2
_ASSETS = [{"id": "asset-1"}, {"id": "asset-2"}, {"id": "asset-3"}]


def _mock_immich(monkeypatch):
    monkeypatch.setattr(widgets.photos, "immich_client_for", lambda frame: object())
    monkeypatch.setattr(widgets.photos, "list_assets", lambda client, album_id: _ASSETS)
    from PIL import Image
    source = Image.new("RGB", (100, 80), (10, 20, 30))
    monkeypatch.setattr(widgets.photos, "fetch_source_and_faces", lambda client, mode, asset_id: (source, None))


def test_unclaimed_frame_shows_placeholder(client, db_session):
    frame = db_session.get(Frame, 1)
    frame.owner_user_id = None
    db_session.commit()

    resp = client.get("/frame/image")
    assert resp.status_code == 200
    assert len(resp.content) == EXPECTED_BYTES


def test_claimed_frame_with_unconfigured_photo_widget_still_renders(client, db_session):
    client.post("/setup", data={"username": "alice", "password": "hunter22"})
    resp = client.get("/frame/image")
    assert resp.status_code == 200
    assert len(resp.content) == EXPECTED_BYTES

    frame = db_session.get(Frame, 1)
    widget = db_session.query(Widget).filter_by(frame_id=frame.id, widget_type="photos").one()
    cfg = db_session.get(PhotoWidgetConfig, widget.id)
    assert cfg.album_id == ""  # never configured -- still rendered fine, as a placeholder region


def test_configured_photo_widget_renders_and_advances_via_button(client, db_session, monkeypatch):
    client.post("/setup", data={"username": "alice", "password": "hunter22"})
    frame = db_session.get(Frame, 1)
    widget = db_session.query(Widget).filter_by(frame_id=frame.id, widget_type="photos").one()
    cfg = db_session.get(PhotoWidgetConfig, widget.id)
    cfg.album_id = "album-1"
    db_session.commit()

    _mock_immich(monkeypatch)

    resp = client.get("/frame/image")
    assert resp.status_code == 200
    assert len(resp.content) == EXPECTED_BYTES

    db_session.refresh(cfg)
    assert cfg.current_asset_id == "asset-1"

    resp = client.post("/frame/advance")
    assert resp.status_code == 200
    assert len(resp.content) == EXPECTED_BYTES
    db_session.refresh(cfg)
    assert cfg.current_asset_id != "asset-1"  # the default next->advance binding fired

    resp = client.post("/frame/back")
    assert resp.status_code == 200
    db_session.refresh(cfg)
    assert cfg.current_asset_id == "asset-1"  # back undid it


def test_manage_flag_still_returns_a_valid_image(client, db_session, monkeypatch):
    client.post("/setup", data={"username": "alice", "password": "hunter22"})
    frame = db_session.get(Frame, 1)
    widget = db_session.query(Widget).filter_by(frame_id=frame.id, widget_type="photos").one()
    db_session.get(PhotoWidgetConfig, widget.id).album_id = "album-1"
    db_session.commit()
    _mock_immich(monkeypatch)

    plain = client.get("/frame/image").content
    with_manage = client.get("/frame/image?manage=1").content
    assert len(with_manage) == EXPECTED_BYTES
    assert with_manage != plain  # the manage-QR overlay actually got composited in


def test_two_widget_frame_composites_both_and_next_targets_calendar(client, db_session, monkeypatch):
    """A second frame (not frame #1) with two independent widgets -- a
    calendar widget and a photo widget, addressed directly the way the
    migration's calendar_photo_inlay backfill shapes a frame -- exercised
    end to end over HTTP, not just via the migration's own unit tests."""
    _mock_immich(monkeypatch)

    frame = Frame(
        name="Two Widget Frame", device_id="aabbccddeeff", device_token="devtok-2",
        manage_token="mtok-2", orientation="landscape", created_at=time.time(),
    )
    db_session.add(frame)
    db_session.flush()

    cal_widget = Widget(frame_id=frame.id, widget_type="calendar", x=0, y=0, w=4, h=5,
                        sort_order=0, created_at=time.time())
    photo_widget = Widget(frame_id=frame.id, widget_type="photos", x=4, y=0, w=4, h=5,
                          sort_order=1, created_at=time.time())
    db_session.add_all([cal_widget, photo_widget])
    db_session.flush()
    db_session.add(CalendarWidgetConfig(widget_id=cal_widget.id, view="agenda", browse_offset=0))
    db_session.add(PhotoWidgetConfig(widget_id=photo_widget.id, album_id="album-1"))
    db_session.add_all([
        FrameButtonAction(frame_id=frame.id, button="next", widget_id=cal_widget.id, action="advance"),
        FrameButtonAction(frame_id=frame.id, button="back", widget_id=cal_widget.id, action="back"),
    ])
    db_session.commit()

    resp = client.get(f"/frame/image?id={frame.device_id}&token={frame.device_token}")
    assert resp.status_code == 200
    assert len(resp.content) == EXPECTED_BYTES

    # NEXT is bound only to the calendar widget -- pressing it should
    # move the calendar's browse_offset, not touch the photo widget.
    resp = client.post(f"/frame/advance?id={frame.device_id}&token={frame.device_token}")
    assert resp.status_code == 200

    cal_cfg = db_session.get(CalendarWidgetConfig, cal_widget.id)
    photo_cfg = db_session.get(PhotoWidgetConfig, photo_widget.id)
    assert cal_cfg.browse_offset == 1
    assert photo_cfg.current_asset_id == ""  # untouched -- NEXT was never bound to it


def test_frame_share_checks_widget_scoped_state(client, db_session, monkeypatch):
    client.post("/setup", data={"username": "alice", "password": "hunter22"})
    frame = db_session.get(Frame, 1)
    frame.owner_user_id = db_session.query(User).filter_by(username="alice").one().id
    widget = db_session.query(Widget).filter_by(frame_id=frame.id, widget_type="photos").one()
    cfg = db_session.get(PhotoWidgetConfig, widget.id)
    cfg.album_id = "album-1"
    cfg.current_asset_id = "asset-1"
    cfg.queue = ["asset-2"]
    frame.immich_url = "http://immich.example.com"
    frame.immich_api_key = "key"
    db_session.commit()

    # Not showing/queued -- rejected before ever touching Immich
    resp = client.get("/frame/share/asset-not-on-this-frame")
    assert resp.status_code == 404

    # Currently showing -- allowed through to the (mocked) Immich call
    monkeypatch.setattr(
        "app.routers.device.immich_client_for",
        lambda frame: type("C", (), {"create_share_link": lambda self, asset_id, expires_in_s: "https://immich.example.com/share/abc"})(),
    )
    resp = client.get("/frame/share/asset-1", follow_redirects=False)
    assert resp.status_code in (302, 303, 307)

    # Queued (not current) -- also allowed
    resp = client.get("/frame/share/asset-2", follow_redirects=False)
    assert resp.status_code in (302, 303, 307)
