"""Covers the Page Designer's free-form-canvas rewrite: PageBlockDef's new
`layout`/`layout_mobile` fields, `Page.canvas_width`, the new block types
added alongside it, and the "html" block's permission gate (POST/PATCH
/pages — app/routers/pages.py)."""

import uuid

from app.core.auth import create_access_token
from tests.conftest import AsyncSessionLocal
from app.models.identity import Role, User, UserRole


async def _create_role_and_token(tenant_id: uuid.UUID, action_matrix: dict[str, list[str]]) -> str:
    """A non-system role scoped to just this test, with `test_tenant`'s own
    teardown deleting it (it deletes every Role/User under tenant_id)."""
    unique = uuid.uuid4().hex[:8]
    async with AsyncSessionLocal() as db:
        async with db.begin():
            role = Role(tenant_id=tenant_id, name=f"Pytest Role {unique}", is_system_role=False, action_matrix=action_matrix)
            db.add(role)
            await db.flush()
            user = User(tenant_id=tenant_id, name=f"Pytest User {unique}", email=f"pytest-{unique}@example.com", status="active")
            db.add(user)
            await db.flush()
            db.add(UserRole(user_id=user.id, role_id=role.id))
            await db.flush()
            user_id = user.id
    return create_access_token(tenant_id=tenant_id, user_id=user_id, vendor_id=None)


async def test_page_layout_and_canvas_width_round_trip(client, auth_headers):
    resp = await client.post("/pages", headers=auth_headers, json={"name": "Layout Test Page"})
    assert resp.status_code == 201, resp.text
    assert resp.json()["canvas_width"] == 1200

    page_id = resp.json()["id"]
    update_resp = await client.patch(
        f"/pages/{page_id}",
        headers=auth_headers,
        json={
            "canvas_width": 1440,
            "blocks": [
                {
                    "id": "b1",
                    "type": "panel",
                    "layout": {"x": 0, "y": 0, "w": 400, "h": 200},
                    "layout_mobile": {"x": 0, "y": 0, "w": 320, "h": 240},
                    "children": [],
                }
            ],
        },
    )
    assert update_resp.status_code == 200, update_resp.text
    body = update_resp.json()
    assert body["canvas_width"] == 1440
    assert body["blocks"][0]["layout"] == {"x": 0, "y": 0, "w": 400, "h": 200}
    assert body["blocks"][0]["layout_mobile"] == {"x": 0, "y": 0, "w": 320, "h": 240}


async def test_page_accepts_every_new_block_type(client, auth_headers):
    blocks = [
        {"id": "divider1", "type": "divider"},
        {"id": "spacer1", "type": "spacer"},
        {"id": "grid1", "type": "grid", "grid_columns": 3, "grid_gap": 8, "children": []},
        {"id": "rich1", "type": "rich_text", "html": "<p>Hello</p>"},
        {"id": "video1", "type": "video", "video_url": "https://youtube.com/watch?v=abc123"},
        {"id": "icon1", "type": "icon", "icon_name": "Star"},
        {"id": "line1", "type": "line_chart", "data_source": "master:00000000-0000-0000-0000-000000000000", "x_field": "status", "series": [{"field": "revenue", "aggregate": "sum"}]},
        {"id": "table1", "type": "table", "table_columns": ["A", "B"], "table_rows": [["1", "2"]]},
        {
            "id": "panel1",
            "type": "panel",
            "background_color": "#f5f5f5",
            "padding": 16,
            "border_color": "#e2e8f0",
            "border_width": 1,
            "children": [{"id": "row_child1", "type": "text", "text": "Ratio'd", "width_ratio": 2.0}],
        },
    ]
    resp = await client.post("/pages", headers=auth_headers, json={"name": "New Blocks Page", "blocks": blocks})
    assert resp.status_code == 201, resp.text
    got_types = {b["type"] for b in resp.json()["blocks"]}
    assert got_types == {b["type"] for b in blocks}


async def test_data_grid_still_requires_master_source(client, auth_headers):
    resp = await client.post(
        "/pages",
        headers=auth_headers,
        json={
            "name": "Bad Data Grid Page",
            "blocks": [{"id": "dg1", "type": "data_grid", "data_source": "form:00000000-0000-0000-0000-000000000000"}],
        },
    )
    assert resp.status_code == 422


async def test_data_list_live_pagination_accepts_form_source(client, auth_headers):
    """A "data_list" block (unlike the legacy "data_grid" type above) may
    now turn on live_pagination against a Form, not just a Master — see
    PageBlockRenderer.tsx's PreviewDataGrid Form branch."""
    resp = await client.post(
        "/pages",
        headers=auth_headers,
        json={
            "name": "Form Data List Page",
            "blocks": [
                {
                    "id": "dl1",
                    "type": "data_list",
                    "data_source": "form:00000000-0000-0000-0000-000000000000",
                    "live_pagination": True,
                    "allow_create": True,
                    "allow_edit": True,
                    "allow_delete": True,
                }
            ],
        },
    )
    assert resp.status_code == 201, resp.text


async def test_data_list_live_pagination_still_rejects_dataset_source(client, auth_headers):
    resp = await client.post(
        "/pages",
        headers=auth_headers,
        json={
            "name": "Bad Dataset Data List Page",
            "blocks": [
                {
                    "id": "dl1",
                    "type": "data_list",
                    "data_source": "dataset:00000000-0000-0000-0000-000000000000",
                    "live_pagination": True,
                }
            ],
        },
    )
    assert resp.status_code == 422


async def test_html_block_rejected_without_publish_permission(client, test_tenant):
    token = await _create_role_and_token(test_tenant["tenant_id"], {"pages": ["view", "create", "edit"]})
    headers = {"Authorization": f"Bearer {token}"}
    resp = await client.post(
        "/pages",
        headers=headers,
        json={"name": "Html Page", "blocks": [{"id": "h1", "type": "html", "html": "<script>alert(1)</script>"}]},
    )
    assert resp.status_code == 403


async def test_html_block_allowed_with_publish_permission(client, test_tenant):
    token = await _create_role_and_token(test_tenant["tenant_id"], {"pages": ["view", "create", "edit", "publish"]})
    headers = {"Authorization": f"Bearer {token}"}
    resp = await client.post(
        "/pages",
        headers=headers,
        json={"name": "Html Page 2", "blocks": [{"id": "h1", "type": "html", "html": "<script>alert(1)</script>"}]},
    )
    assert resp.status_code == 201, resp.text


async def test_html_block_permission_checked_on_update_too(client, test_tenant):
    admin_token = await _create_role_and_token(test_tenant["tenant_id"], {"pages": ["view", "create", "edit", "publish"]})
    admin_headers = {"Authorization": f"Bearer {admin_token}"}
    create_resp = await client.post("/pages", headers=admin_headers, json={"name": "Editable Page"})
    page_id = create_resp.json()["id"]

    restricted_token = await _create_role_and_token(test_tenant["tenant_id"], {"pages": ["view", "create", "edit"]})
    restricted_headers = {"Authorization": f"Bearer {restricted_token}"}
    resp = await client.patch(
        f"/pages/{page_id}",
        headers=restricted_headers,
        json={"blocks": [{"id": "h1", "type": "html", "html": "<script>alert(1)</script>"}]},
    )
    assert resp.status_code == 403

    # A non-html update from the same restricted role still works — the gate
    # only fires when an html block is actually present.
    ok_resp = await client.patch(
        f"/pages/{page_id}",
        headers=restricted_headers,
        json={"blocks": [{"id": "t1", "type": "text", "text": "fine"}]},
    )
    assert ok_resp.status_code == 200, ok_resp.text


async def test_data_list_views_and_panel_blocks_round_trip(client, auth_headers):
    """data_list's view_mode/card_* fields, the popup drawer mode, and the
    four Panels block types (page_header/alert/stat_cards/stepper) all
    survive a create + read unchanged."""
    blocks = [
        {"id": "dl", "type": "data_list", "view_mode": "cards", "allow_view_switch": True, "card_title_field": "name", "card_columns": 3},
        {"id": "ph", "type": "page_header", "label": "Cases", "subtitle": "All open cases"},
        {"id": "al", "type": "alert", "variant": "warning", "label": "Heads up", "text": "Two overdue"},
        {
            "id": "sc",
            "type": "stat_cards",
            "data_source": f"master:{uuid.uuid4()}",
            "stat_items": [{"id": "t1", "label": "Overdue", "color": "#dc2626", "param_name": "status", "param_value": "Overdue"}],
        },
        {"id": "st", "type": "stepper", "steps": [{"id": "s1", "label": "Details"}, {"id": "s2", "label": "Review"}], "current_step": "{{step}}"},
        {"id": "pp", "type": "popup", "label": "Details", "popup_mode": "drawer", "children": []},
    ]
    resp = await client.post("/pages", headers=auth_headers, json={"name": "Panels Test Page", "blocks": blocks})
    assert resp.status_code == 201, resp.text
    stored = {b["id"]: b for b in resp.json()["blocks"]}
    assert stored["dl"]["view_mode"] == "cards" and stored["dl"]["allow_view_switch"] is True and stored["dl"]["card_columns"] == 3
    assert stored["sc"]["stat_items"][0]["param_name"] == "status"
    assert stored["st"]["current_step"] == "{{step}}" and len(stored["st"]["steps"]) == 2
    assert stored["pp"]["popup_mode"] == "drawer"

    bad = await client.post("/pages", headers=auth_headers, json={"name": "Bad View", "blocks": [{"id": "x", "type": "data_list", "view_mode": "carousel"}]})
    assert bad.status_code == 422


async def test_tree_timeline_and_column_tools_round_trip(client, auth_headers):
    blocks = [
        {"id": "tr", "type": "tree", "data_source": f"master:{uuid.uuid4()}", "tree_parent_field": "parent", "tree_expand_all": True, "card_title_field": "name"},
        {
            "id": "tl",
            "type": "timeline",
            "data_source": f"master:{uuid.uuid4()}",
            "timeline_date_field": "changed_at",
            "timeline_before_field": "before",
            "timeline_after_field": "after",
        },
        {"id": "dl", "type": "data_list", "allow_column_tools": True},
    ]
    resp = await client.post("/pages", headers=auth_headers, json={"name": "Tree Timeline Page", "blocks": blocks})
    assert resp.status_code == 201, resp.text
    stored = {b["id"]: b for b in resp.json()["blocks"]}
    assert stored["tr"]["tree_parent_field"] == "parent" and stored["tr"]["tree_expand_all"] is True
    assert stored["tl"]["timeline_before_field"] == "before" and stored["tl"]["timeline_after_field"] == "after"
    assert stored["dl"]["allow_column_tools"] is True


async def test_label_render_html_flag_round_trips(client, auth_headers):
    blocks = [{"id": "lb", "type": "label", "data_source": f"form:{uuid.uuid4()}", "columns": ["remark"], "label_render_html": True}]
    resp = await client.post("/pages", headers=auth_headers, json={"name": "Label HTML Page", "blocks": blocks})
    assert resp.status_code == 201, resp.text
    assert resp.json()["blocks"][0]["label_render_html"] is True


async def test_list_and_detail_text_display_round_trip(client, auth_headers):
    source = f"form:{uuid.uuid4()}"
    blocks = [
        {"id": "dl", "type": "data_list", "data_source": source, "render_html": True, "max_chars": 120},
        {"id": "dv", "type": "detail_view", "data_source": source, "max_chars": 300},
    ]
    resp = await client.post("/pages", headers=auth_headers, json={"name": "Text Display Page", "blocks": blocks})
    assert resp.status_code == 201, resp.text
    stored = {b["id"]: b for b in resp.json()["blocks"]}
    assert (stored["dl"]["render_html"], stored["dl"]["max_chars"]) == (True, 120)
    assert (stored["dv"].get("render_html", False), stored["dv"]["max_chars"]) == (False, 300)

    bad = [{"id": "dl", "type": "data_list", "data_source": source, "max_chars": 0}]
    resp = await client.post("/pages", headers=auth_headers, json={"name": "Bad Text Display Page", "blocks": bad})
    assert resp.status_code == 422


async def test_context_label_and_form_field_default_round_trip(client, auth_headers):
    blocks = [
        {"id": "lb", "type": "label", "label": "Office", "value_template": "{{session.office_id}}"},
        {
            "id": "ff",
            "type": "form_field",
            "param_name": "office",
            "form_field": {"name": "value", "type": "text", "label": "Office", "default_value": "{{session.office_id}}"},
        },
    ]
    resp = await client.post("/pages", headers=auth_headers, json={"name": "Context Page", "blocks": blocks})
    assert resp.status_code == 201, resp.text
    stored = {b["id"]: b for b in resp.json()["blocks"]}
    assert stored["lb"]["value_template"] == "{{session.office_id}}"
    assert stored["ff"]["form_field"]["default_value"] == "{{session.office_id}}"


async def test_duplicate_page_copies_design_as_new_draft(client, auth_headers):
    source = await client.post(
        "/pages",
        headers=auth_headers,
        json={
            "name": "Vendor Directory",
            "canvas_width": 1440,
            "parameters": [{"name": "vendorId"}],
            "blocks": [
                {"id": "p1", "type": "panel", "layout": {"x": 10, "y": 20, "w": 400, "h": 200}, "children": [{"id": "t1", "type": "text", "text": "Hello"}]},
            ],
        },
    )
    assert source.status_code == 201, source.text
    src = source.json()
    await client.post(f"/pages/{src['id']}/publish", headers=auth_headers)

    resp = await client.post(f"/pages/{src['id']}/duplicate", headers=auth_headers, json={})
    assert resp.status_code == 201, resp.text
    dup = resp.json()
    assert dup["id"] != src["id"]
    assert dup["name"] == "Vendor Directory (copy)"
    assert dup["status"] == "draft"
    assert dup["blocks"] == src["blocks"]
    assert dup["parameters"] == src["parameters"]
    assert dup["canvas_width"] == 1440

    named = await client.post(f"/pages/{src['id']}/duplicate", headers=auth_headers, json={"name": "  Vendor Directory v2 "})
    assert named.json()["name"] == "Vendor Directory v2"

    # The copy is independent — editing it leaves the source untouched.
    await client.patch(f"/pages/{dup['id']}", headers=auth_headers, json={"blocks": []})
    assert (await client.get(f"/pages/{src['id']}", headers=auth_headers)).json()["blocks"] == src["blocks"]


async def test_duplicate_page_with_html_block_needs_publish_permission(client, test_tenant):
    admin_token = await _create_role_and_token(test_tenant["tenant_id"], {"pages": ["view", "create", "edit", "publish"]})
    admin_headers = {"Authorization": f"Bearer {admin_token}"}
    page_id = (
        await client.post(
            "/pages",
            headers=admin_headers,
            json={"name": "Html Source", "blocks": [{"id": "g1", "type": "panel", "children": [{"id": "h1", "type": "html", "html": "<b>x</b>"}]}]},
        )
    ).json()["id"]

    restricted_token = await _create_role_and_token(test_tenant["tenant_id"], {"pages": ["view", "create", "edit"]})
    resp = await client.post(f"/pages/{page_id}/duplicate", headers={"Authorization": f"Bearer {restricted_token}"}, json={})
    assert resp.status_code == 403

    viewer_token = await _create_role_and_token(test_tenant["tenant_id"], {"pages": ["view"]})
    resp = await client.post(f"/pages/{page_id}/duplicate", headers={"Authorization": f"Bearer {viewer_token}"}, json={})
    assert resp.status_code == 403


async def test_link_block_and_action_params_round_trip(client, auth_headers):
    target = (await client.post("/pages", headers=auth_headers, json={"name": "Application detail", "parameters": [{"name": "applicationNo"}]})).json()
    action = {
        "type": "navigate",
        "target_type": "page",
        "target_id": target["id"],
        "params": [{"name": "applicationNo", "value": "{{recordId}}"}, {"name": "tab", "value": "history"}],
    }
    blocks = [
        {"id": "l1", "type": "link", "label": "View application", "action": action},
        {"id": "b1", "type": "button", "label": "Open", "action": {**action, "params": [{"name": "officeId", "value": "{{session.office_id}}"}]}},
    ]
    resp = await client.post("/pages", headers=auth_headers, json={"name": "Links", "blocks": blocks})
    assert resp.status_code == 201, resp.text
    stored = {b["id"]: b for b in resp.json()["blocks"]}
    assert stored["l1"]["type"] == "link"
    assert stored["l1"]["action"]["params"] == action["params"]
    assert stored["b1"]["action"]["params"] == [{"name": "officeId", "value": "{{session.office_id}}"}]

    # An action saved without params (or before they existed) still saves and has none.
    legacy = await client.post("/pages", headers=auth_headers, json={"name": "Legacy", "blocks": [{"id": "b", "type": "button", "action": {"type": "navigate", "target_type": "page", "target_id": target["id"]}}]})
    assert legacy.status_code == 201 and legacy.json()["blocks"][0]["action"].get("params", []) == []

    bad = await client.post("/pages", headers=auth_headers, json={"name": "Bad", "blocks": [{"id": "l", "type": "link", "action": {**action, "params": [{"name": "", "value": "x"}]}}]})
    assert bad.status_code == 422

async def test_responsive_layout_block_round_trips_and_validates(client, auth_headers):
    layout = {
        "id": "L1", "type": "layout", "flow_mode": "columns", "flow_columns": 4, "flow_columns_tablet": 2, "flow_columns_mobile": 1,
        "flow_gap": 12, "flow_align_x": "center", "flow_align_y": "start", "flow_stack_below": "tablet",
        "layout": {"x": 0, "y": 0, "w": 960, "h": 240},
        "children": [
            {"id": "k1", "type": "kpi_card", "flow_span": 2, "flow_span_mobile": 1, "flow_hide_on": ["mobile"], "flow_order_mobile": -1, "flow_height": "auto"},
            {"id": "k2", "type": "text", "text": "Hi", "width_ratio": 2},
        ],
    }
    resp = await client.post("/pages", headers=auth_headers, json={"name": "Responsive", "blocks": [layout]})
    assert resp.status_code == 201, resp.text
    saved = resp.json()["blocks"][0]
    assert saved["type"] == "layout" and saved["flow_columns"] == 4 and saved["flow_align_x"] == "center"
    assert saved["children"][0]["flow_hide_on"] == ["mobile"] and saved["children"][0]["flow_height"] == "auto"

    for bad in ({"flow_mode": "zigzag"}, {"flow_columns": 0}, {"flow_columns": 13}, {"flow_hide_on": ["watch"]}):
        resp = await client.post("/pages", headers=auth_headers, json={"name": "Bad", "blocks": [{**layout, **bad}]})
        assert resp.status_code == 422, (bad, resp.text)

async def test_page_fit_mode(client, auth_headers):
    created = await client.post("/pages", headers=auth_headers, json={"name": "Fit"})
    assert created.json()["fit_mode"] == "scale"  # existing behaviour by default
    page_id = created.json()["id"]
    patched = await client.patch(f"/pages/{page_id}", headers=auth_headers, json={"fit_mode": "fluid"})
    assert patched.status_code == 200 and patched.json()["fit_mode"] == "fluid"
    dup = await client.post(f"/pages/{page_id}/duplicate", headers=auth_headers, json={})
    assert dup.json()["fit_mode"] == "fluid"
    bad = await client.patch(f"/pages/{page_id}", headers=auth_headers, json={"fit_mode": "stretch"})
    assert bad.status_code == 422


async def test_data_list_work_settings_round_trip(client, auth_headers):
    block = {
        "id": "dl",
        "type": "data_list",
        "data_source": f"master:{uuid.uuid4()}",
        "format_rules": [{"id": "r", "field": "status", "operator": "equals", "value": "Closed", "color": "grey"}],
        "totals": {"fee": "sum"},
        "group_rows_by": "status",
        "allow_select": True,
        "bulk_actions": [{"id": "b", "label": "Close", "kind": "set_field", "field": "status", "value": "Closed"}],
        "inline_edit": True,
        "freeze_first_column": True,
        "allow_saved_views": True,
    }
    resp = await client.post("/pages", headers=auth_headers, json={"name": "Work list", "blocks": [block]})
    assert resp.status_code == 201, resp.text
    stored = resp.json()["blocks"][0]
    assert stored["format_rules"][0].get("target", "row") == "row" and stored["totals"] == {"fee": "sum"}
    assert stored["bulk_actions"][0]["kind"] == "set_field" and stored["inline_edit"] and stored["freeze_first_column"]

    for bad in ({"totals": {"fee": "median"}}, {"bulk_actions": [{"id": "x", "kind": "launch"}]}, {"format_rules": [{"id": "r", "field": "s", "operator": "between", "color": "grey"}]}):
        resp = await client.post("/pages", headers=auth_headers, json={"name": "Bad", "blocks": [{**block, **bad}]})
        assert resp.status_code == 422, bad


async def test_work_view_blocks_round_trip(client, auth_headers):
    source = f"master:{uuid.uuid4()}"
    blocks = [
        {"id": "k", "type": "kanban", "data_source": source, "kanban_field": "status", "kanban_columns": ["New", "Done"], "allow_edit": True},
        {"id": "c", "type": "calendar", "data_source": source, "calendar_date_field": "due", "calendar_end_field": "until", "calendar_view": "week"},
        {"id": "r", "type": "repeater", "data_source": source, "repeater_columns": 3, "children": [{"id": "t", "type": "text", "text": "{{item.title}}"}]},
        {"id": "a", "type": "approvals", "approvals_workflow_ids": [str(uuid.uuid4())], "approvals_fields": ["days"]},
    ]
    resp = await client.post("/pages", headers=auth_headers, json={"name": "Work", "blocks": blocks})
    assert resp.status_code == 201, resp.text
    stored = {b["id"]: b for b in resp.json()["blocks"]}
    assert stored["k"]["kanban_columns"] == ["New", "Done"] and stored["c"]["calendar_view"] == "week"
    assert stored["r"]["children"][0]["text"] == "{{item.title}}" and stored["a"]["approvals_fields"] == ["days"]
    bad = await client.post("/pages", headers=auth_headers, json={"name": "Bad", "blocks": [{**blocks[2], "repeater_columns": 9}]})
    assert bad.status_code == 422
