"""routers/api_widgets.py -- widget CRUD + grid placement. Bounds/
minimum-footprint/no-overlap validation is server-side and re-checked
regardless of what a client already believes is a valid placement (see
the module's own docstring), so this is exercised at the HTTP layer, not
just against grid.py's pure functions directly (those have no dedicated
coverage of their own -- this file and test_migrations.py's backfill
tests are what actually exercise them end to end).

Frame #1's auto-migrated widget (see migration.py's backfill) is a
single full-panel (0, 0, 8, 5) photos widget -- every test here starts
by shrinking or removing it to free up room, mirroring what a real user
would do on the placement canvas before adding a second widget."""

from __future__ import annotations

from app.models import Frame, FrameButtonAction, Widget

from .conftest import csrf_headers


def _widget_id(db_session, widget_type="photos") -> int:
    return db_session.query(Widget).filter_by(frame_id=1, widget_type=widget_type).one().id


def _shrink_default_widget(client, db_session, w=4, h=5) -> int:
    """Frees up the right-hand side of the grid for a second widget."""
    widget_id = _widget_id(db_session)
    resp = client.patch(f"/api/frames/1/widgets/{widget_id}",
                        json={"x": 0, "y": 0, "w": w, "h": h}, headers=csrf_headers(client))
    assert resp.status_code == 200, resp.text
    return widget_id


def test_list_widgets_returns_grid_and_the_default_widget(client, db_session):
    client.post("/setup", data={"username": "alice", "password": "hunter22"})
    resp = client.get("/api/frames/1/widgets")
    assert resp.status_code == 200
    data = resp.json()
    assert data["orientation"] == "landscape"
    assert data["grid"] == {"cols": 8, "rows": 5}
    assert len(data["widgets"]) == 1
    assert data["widgets"][0]["widget_type"] == "photos"
    assert data["widgets"][0]["w"] == 8 and data["widgets"][0]["h"] == 5


def test_create_with_no_room_left_is_rejected(client, db_session):
    client.post("/setup", data={"username": "alice", "password": "hunter22"})
    resp = client.post("/api/frames/1/widgets", json={"widget_type": "photos"}, headers=csrf_headers(client))
    assert resp.status_code == 400
    assert "no open space" in resp.json()["detail"].lower()


def test_create_auto_places_in_freed_up_space(client, db_session):
    client.post("/setup", data={"username": "alice", "password": "hunter22"})
    _shrink_default_widget(client, db_session, w=4, h=5)

    resp = client.post("/api/frames/1/widgets", json={"widget_type": "whiteboard"}, headers=csrf_headers(client))
    assert resp.status_code == 200, resp.text
    created = resp.json()
    assert created["widget_type"] == "whiteboard"
    assert created["x"] >= 4  # lands in the freed right-hand region, not overlapping the shrunk photos widget
    assert (created["w"], created["h"]) == (2, 2)  # grid.MIN_FOOTPRINT["whiteboard"]

    widget = db_session.get(Widget, created["id"])
    assert widget is not None and widget.frame_id == 1
    # A default config row was created alongside it (see WIDGET_CONFIG_MODELS).
    from app.models import WhiteboardWidgetConfig
    assert db_session.get(WhiteboardWidgetConfig, widget.id) is not None


def test_create_with_explicit_placement_validates_bounds(client, db_session):
    client.post("/setup", data={"username": "alice", "password": "hunter22"})
    _shrink_default_widget(client, db_session, w=4, h=5)

    resp = client.post("/api/frames/1/widgets",
                       json={"widget_type": "photos", "x": 6, "y": 0, "w": 4, "h": 2},
                       headers=csrf_headers(client))
    assert resp.status_code == 400
    assert "out of bounds" in resp.json()["detail"].lower()


def test_create_below_minimum_footprint_is_rejected(client, db_session):
    client.post("/setup", data={"username": "alice", "password": "hunter22"})
    _shrink_default_widget(client, db_session, w=4, h=5)

    resp = client.post("/api/frames/1/widgets",
                       json={"widget_type": "calendar", "x": 4, "y": 0, "w": 2, "h": 1},
                       headers=csrf_headers(client))
    assert resp.status_code == 400
    assert "at least" in resp.json()["detail"].lower()


def test_create_overlapping_existing_widget_is_rejected(client, db_session):
    client.post("/setup", data={"username": "alice", "password": "hunter22"})
    # Default widget still covers the full 8x5 grid -- any explicit placement overlaps it.
    resp = client.post("/api/frames/1/widgets",
                       json={"widget_type": "whiteboard", "x": 0, "y": 0, "w": 2, "h": 2},
                       headers=csrf_headers(client))
    assert resp.status_code == 400
    assert "overlaps" in resp.json()["detail"].lower()


def test_create_unknown_widget_type_is_rejected(client, db_session):
    client.post("/setup", data={"username": "alice", "password": "hunter22"})
    resp = client.post("/api/frames/1/widgets", json={"widget_type": "video"}, headers=csrf_headers(client))
    assert resp.status_code == 400
    assert "unknown widget type" in resp.json()["detail"].lower()


def test_move_widget_to_a_valid_rect(client, db_session):
    client.post("/setup", data={"username": "alice", "password": "hunter22"})
    widget_id = _widget_id(db_session)
    resp = client.patch(f"/api/frames/1/widgets/{widget_id}",
                        json={"x": 1, "y": 1, "w": 3, "h": 2}, headers=csrf_headers(client))
    assert resp.status_code == 200, resp.text
    widget = db_session.get(Widget, widget_id)
    assert (widget.x, widget.y, widget.w, widget.h) == (1, 1, 3, 2)


def test_move_rejects_overlap_with_another_widget(client, db_session):
    client.post("/setup", data={"username": "alice", "password": "hunter22"})
    photos_id = _shrink_default_widget(client, db_session, w=4, h=5)
    create_resp = client.post("/api/frames/1/widgets", json={"widget_type": "whiteboard"},
                              headers=csrf_headers(client))
    whiteboard_id = create_resp.json()["id"]

    # Try to move photos back over the whiteboard widget's space.
    resp = client.patch(f"/api/frames/1/widgets/{photos_id}",
                        json={"x": 0, "y": 0, "w": 8, "h": 5}, headers=csrf_headers(client))
    assert resp.status_code == 400
    assert "overlaps" in resp.json()["detail"].lower()
    # Original placement is untouched after the rejected move.
    widget = db_session.get(Widget, photos_id)
    assert (widget.x, widget.y, widget.w, widget.h) == (0, 0, 4, 5)
    assert whiteboard_id  # sanity: the other widget really was created


def test_move_unknown_widget_404s(client, db_session):
    client.post("/setup", data={"username": "alice", "password": "hunter22"})
    resp = client.patch("/api/frames/1/widgets/999999",
                        json={"x": 0, "y": 0, "w": 1, "h": 1}, headers=csrf_headers(client))
    assert resp.status_code == 404


def test_delete_widget_and_cascades_button_actions(client, db_session):
    client.post("/setup", data={"username": "alice", "password": "hunter22"})
    widget_id = _widget_id(db_session)
    db_session.add(FrameButtonAction(frame_id=1, button="next", widget_id=widget_id, action="advance", sort_order=0))
    db_session.commit()

    resp = client.delete(f"/api/frames/1/widgets/{widget_id}", headers=csrf_headers(client))
    assert resp.status_code == 200

    assert db_session.get(Widget, widget_id) is None
    remaining = db_session.query(FrameButtonAction).filter_by(widget_id=widget_id).all()
    assert remaining == []


def test_delete_unknown_widget_404s(client, db_session):
    client.post("/setup", data={"username": "alice", "password": "hunter22"})
    resp = client.delete("/api/frames/1/widgets/999999", headers=csrf_headers(client))
    assert resp.status_code == 404


def test_widgets_scoped_to_their_own_frame(client, db_session):
    """A widget id from a different frame must 404, not silently operate
    cross-frame -- same posture as photo_widget_config_or_404 and every
    other frame-scoped lookup in this codebase."""
    client.post("/setup", data={"username": "alice", "password": "hunter22"})
    other_frame = Frame(name="Second frame", manage_token="tok-2", device_id="dev-2", device_token="dtok-2")
    db_session.add(other_frame)
    db_session.commit()
    from app.models import Widget as W
    other_widget = W(frame_id=other_frame.id, widget_type="photos", x=0, y=0, w=8, h=5, sort_order=0,
                     created_at=0)
    db_session.add(other_widget)
    db_session.commit()

    resp = client.patch(f"/api/frames/1/widgets/{other_widget.id}",
                        json={"x": 0, "y": 0, "w": 1, "h": 1}, headers=csrf_headers(client))
    assert resp.status_code == 404


# --- orientation change resets the widget layout (grid.grid_dims transposes) ---

def test_orientation_change_resets_multiple_widgets_to_one_full_panel_widget(client, db_session):
    client.post("/setup", data={"username": "alice", "password": "hunter22"})
    photos_id = _shrink_default_widget(client, db_session, w=4, h=5)
    create_resp = client.post("/api/frames/1/widgets", json={"widget_type": "whiteboard"},
                              headers=csrf_headers(client))
    whiteboard_id = create_resp.json()["id"]
    assert db_session.query(Widget).filter_by(frame_id=1).count() == 2

    resp = client.post("/api/frames/1/config", data={"orientation": "portrait"}, headers=csrf_headers(client))
    assert resp.status_code == 200, resp.text

    remaining = db_session.query(Widget).filter_by(frame_id=1).all()
    assert len(remaining) == 1
    assert remaining[0].id == photos_id  # first by sort_order survives
    assert (remaining[0].x, remaining[0].y, remaining[0].w, remaining[0].h) == (0, 0, 5, 8)  # full portrait panel
    assert db_session.get(Widget, whiteboard_id) is None

    frame = db_session.get(Frame, 1)
    assert frame.orientation == "portrait"


def test_orientation_unchanged_leaves_widget_layout_alone(client, db_session):
    client.post("/setup", data={"username": "alice", "password": "hunter22"})
    _shrink_default_widget(client, db_session, w=4, h=5)

    resp = client.post("/api/frames/1/config", data={"orientation": "landscape"}, headers=csrf_headers(client))
    assert resp.status_code == 200, resp.text

    widget = db_session.query(Widget).filter_by(frame_id=1).one()
    assert (widget.x, widget.y, widget.w, widget.h) == (0, 0, 4, 5)  # untouched -- orientation didn't actually change


def test_orientation_change_with_no_widgets_is_a_no_op(client, db_session):
    client.post("/setup", data={"username": "alice", "password": "hunter22"})
    widget_id = _widget_id(db_session)
    client.delete(f"/api/frames/1/widgets/{widget_id}", headers=csrf_headers(client))
    assert db_session.query(Widget).filter_by(frame_id=1).count() == 0

    resp = client.post("/api/frames/1/config", data={"orientation": "portrait"}, headers=csrf_headers(client))
    assert resp.status_code == 200, resp.text
    assert db_session.query(Widget).filter_by(frame_id=1).count() == 0
