"""GET/PUT /api/frames/{id}/buttons -- the button-assignment UI's API
(see routers/api_frames.py's api_buttons_get/api_buttons_save and
static/frame_config.js). Covers the CRUD/validation layer; multi-action
execution order and partial-failure-continues on an actual button press
are already exercised end-to-end in test_device_widget_dispatch.py and
routers/device.py's _run_button_actions -- this file doesn't re-test
device.py's dispatch, just that the assignment API stores/serves/
validates what the UI edits."""

from __future__ import annotations

import time

from app.models import Frame, FrameButtonAction, Widget, WhiteboardWidgetConfig

from .conftest import csrf_headers, link_user, login, make_user


def _add_whiteboard_widget(db_session, frame: Frame) -> Widget:
    widget = Widget(frame_id=frame.id, widget_type="whiteboard", x=0, y=0, w=8, h=5,
                    sort_order=1, created_at=time.time())
    db_session.add(widget)
    db_session.flush()
    db_session.add(WhiteboardWidgetConfig(widget_id=widget.id))
    db_session.commit()
    return widget


def test_get_buttons_reflects_the_default_migration_mapping(client, db_session):
    """Frame #1's auto-migrated photos widget should already have NEXT ->
    advance, BACK -> back from _default_button_actions (see
    migration.py) -- the UI just needs to be able to see that default."""
    client.post("/setup", data={"username": "alice", "password": "hunter22"})
    frame = db_session.get(Frame, 1)
    photo_widget = db_session.query(Widget).filter_by(frame_id=frame.id, widget_type="photos").one()

    resp = client.get("/api/frames/1/buttons")
    assert resp.status_code == 200
    data = resp.json()

    assert {w["id"]: w["widget_type"] for w in data["widgets"]} == {photo_widget.id: "photos"}
    photo_actions = {a["action"] for w in data["widgets"] for a in w["actions"]}
    assert photo_actions == {"advance", "back"}

    assert data["next"] == [{"id": data["next"][0]["id"], "widget_id": photo_widget.id, "action": "advance"}]
    assert data["back"] == [{"id": data["back"][0]["id"], "widget_id": photo_widget.id, "action": "back"}]


def test_get_buttons_includes_placement_and_grid_dims(client, db_session):
    """Two widgets of the same type otherwise look identical in the
    assignment UI ("Photos" / "Photos") -- the client tells them apart
    using x/y/w/h against the frame's grid dims (see
    static/frame_config.js's buildWidgetNames), so the API needs to
    actually hand those over."""
    client.post("/setup", data={"username": "alice", "password": "hunter22"})
    frame = db_session.get(Frame, 1)
    photo_widget = db_session.query(Widget).filter_by(frame_id=frame.id, widget_type="photos").one()
    photo_widget.x, photo_widget.y, photo_widget.w, photo_widget.h = 0, 0, 4, 5
    db_session.commit()
    second = 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(second)
    db_session.flush()
    from app.models import PhotoWidgetConfig
    db_session.add(PhotoWidgetConfig(widget_id=second.id))
    db_session.commit()

    resp = client.get("/api/frames/1/buttons")
    assert resp.status_code == 200
    data = resp.json()

    assert data["grid"] == {"cols": 8, "rows": 5}
    by_id = {w["id"]: w for w in data["widgets"]}
    assert by_id[photo_widget.id]["x"] == 0 and by_id[photo_widget.id]["w"] == 4
    assert by_id[second.id]["x"] == 4 and by_id[second.id]["w"] == 4


def test_put_replaces_the_whole_list_in_order(client, db_session):
    client.post("/setup", data={"username": "alice", "password": "hunter22"})
    frame = db_session.get(Frame, 1)
    photo_widget = db_session.query(Widget).filter_by(frame_id=frame.id, widget_type="photos").one()
    board_widget = _add_whiteboard_widget(db_session, frame)

    resp = client.put("/api/frames/1/buttons/next", json={
        "actions": [
            {"widget_id": board_widget.id, "action": "check_now"},
            {"widget_id": photo_widget.id, "action": "advance"},
        ],
    }, headers=csrf_headers(client))
    assert resp.status_code == 200, resp.text

    rows = db_session.query(FrameButtonAction).filter_by(
        frame_id=frame.id, button="next"
    ).order_by(FrameButtonAction.sort_order).all()
    assert [(r.widget_id, r.action) for r in rows] == [
        (board_widget.id, "check_now"), (photo_widget.id, "advance"),
    ]

    # BACK's own default mapping (photos -> back) is untouched by a PUT to next.
    back_rows = db_session.query(FrameButtonAction).filter_by(frame_id=frame.id, button="back").all()
    assert len(back_rows) == 1
    assert back_rows[0].action == "back"


def test_put_empty_list_clears_the_button(client, db_session):
    client.post("/setup", data={"username": "alice", "password": "hunter22"})
    frame = db_session.get(Frame, 1)

    resp = client.put("/api/frames/1/buttons/next", json={"actions": []}, headers=csrf_headers(client))
    assert resp.status_code == 200, resp.text
    assert db_session.query(FrameButtonAction).filter_by(frame_id=frame.id, button="next").count() == 0


def test_put_rejects_unknown_button_name(client, db_session):
    client.post("/setup", data={"username": "alice", "password": "hunter22"})
    resp = client.put("/api/frames/1/buttons/sideways", json={"actions": []}, headers=csrf_headers(client))
    assert resp.status_code == 404


def test_put_rejects_widget_from_another_frame(client, db_session):
    client.post("/setup", data={"username": "alice", "password": "hunter22"})
    other = Frame(name="Other", device_token="tok-other", manage_token="mtok-other", created_at=time.time())
    db_session.add(other)
    db_session.flush()
    other_widget = Widget(frame_id=other.id, widget_type="photos", x=0, y=0, w=8, h=5,
                          sort_order=0, created_at=time.time())
    db_session.add(other_widget)
    db_session.commit()

    resp = client.put("/api/frames/1/buttons/next", json={
        "actions": [{"widget_id": other_widget.id, "action": "advance"}],
    }, headers=csrf_headers(client))
    assert resp.status_code == 400
    # Nothing partially applied -- the whole request is validated before any write.
    assert db_session.query(FrameButtonAction).filter_by(frame_id=1, button="next").count() == 1


def test_put_rejects_action_the_widget_type_does_not_support(client, db_session):
    client.post("/setup", data={"username": "alice", "password": "hunter22"})
    frame = db_session.get(Frame, 1)
    photo_widget = db_session.query(Widget).filter_by(frame_id=frame.id, widget_type="photos").one()

    resp = client.put("/api/frames/1/buttons/next", json={
        "actions": [{"widget_id": photo_widget.id, "action": "check_now"}],
    }, headers=csrf_headers(client))
    assert resp.status_code == 400


def test_deleting_a_widget_cascades_its_button_bindings(client, db_session):
    client.post("/setup", data={"username": "alice", "password": "hunter22"})
    frame = db_session.get(Frame, 1)
    photo_widget = db_session.query(Widget).filter_by(frame_id=frame.id, widget_type="photos").one()

    resp = client.delete(f"/api/frames/1/widgets/{photo_widget.id}", headers=csrf_headers(client))
    assert resp.status_code == 200, resp.text
    assert db_session.query(FrameButtonAction).filter_by(frame_id=frame.id).count() == 0


def test_linked_user_can_view_and_control_can_save(client, db_session):
    client.post("/setup", data={"username": "alice", "password": "hunter22"})
    bob = make_user(db_session, "bob")
    frame = db_session.get(Frame, 1)
    link_user(db_session, bob, frame)

    client.cookies.clear()
    login(client, "bob")
    resp = client.get("/api/frames/1/buttons")
    assert resp.status_code == 200


def test_unrelated_user_cannot_view(client, db_session):
    client.post("/setup", data={"username": "alice", "password": "hunter22"})
    make_user(db_session, "mallory")

    client.cookies.clear()
    login(client, "mallory")
    resp = client.get("/api/frames/1/buttons")
    assert resp.status_code == 404
