# retoor <retoor@molodetz.nl>

from tests.conftest import BASE_URL, create_post_with_files, paste_image
import time
import requests
from playwright.sync_api import expect
from devplacepy.database import (
    CUSTOMIZATION_GLOBAL_SCOPE,
    get_customization_prefs,
    get_table,
    list_custom_overrides,
    set_custom_override,
    set_customization_pref,
)
from devplacepy.utils import clear_user_cache
def _user_customization_toggle(username):
    return get_table("users").find_one(username=username)
def _seed_global_css(uid, sentinel):
    set_custom_override(
        "user", uid, CUSTOMIZATION_GLOBAL_SCOPE, "css", f".{sentinel} {{ color: red; }}"
    )
def _profile(page, username):
    page.goto(f"{BASE_URL}/profile/{username}", wait_until="domcontentloaded")
def _click_toggle(page, category):
    page.locator(f"[data-customization-toggle='{category}']").click()
    page.locator(".dialog-overlay.visible .dialog-confirm").click()
def _goto_feed_until(page, predicate, timeout=5.0):
    deadline = time.time() + timeout
    content = ""
    while time.time() < deadline:
        page.goto(f"{BASE_URL}/feed", wait_until="domcontentloaded")
        content = page.content()
        if predicate(content):
            return content
        time.sleep(0.2)
    return content
import re
from uuid import uuid4
from datetime import datetime, timedelta, timezone
from tests.conftest import (
    BASE_URL,
    assert_no_horizontal_overflow,
    assert_share_copies,
)
from devplacepy.database import get_table
from devplacepy.utils import make_combined_slug
def _seed_posts(count):
    suite = uuid4().hex[:8]
    topic = f"pag{suite}"
    users = get_table("users")
    posts = get_table("posts")
    base = datetime(2026, 1, 1, tzinfo=timezone.utc)
    for i in range(count):
        owner = str(uuid4())
        users.insert(
            {
                "uid": owner,
                "username": f"pag_{suite}_{i}",
                "terms_version": "1",
                "email": f"{suite}_{i}@test.devplace",
                "password_hash": "x",
                "role": "Member",
                "is_active": True,
                "created_at": datetime.now(timezone.utc).isoformat(),
            }
        )
        uid = str(uuid4())
        content = f"Paginated post {i}"
        posts.insert(
            {
                "uid": uid,
                "user_uid": owner,
                "slug": make_combined_slug(content, uid),
                "title": None,
                "content": content,
                "topic": topic,
                "project_uid": None,
                "image": None,
                "stars": 0,
                "created_at": (base - timedelta(seconds=i)).isoformat(),
                "deleted_at": None,
                "deleted_by": None,
            }
        )
    return topic
def _seed_post_with_comments(comment_texts, topic="devlog"):
    owner = str(uuid4())
    get_table("users").insert(
        {
            "uid": owner,
            "username": f"seed_{owner[:8]}",
            "terms_version": "1",
            "email": f"{owner[:8]}@seed.devplace",
            "password_hash": "x",
            "role": "Member",
            "is_active": True,
            "created_at": datetime.now(timezone.utc).isoformat(),
        }
    )
    post_uid = str(uuid4())
    marker = f"seedpost-{post_uid[:8]}"
    get_table("posts").insert(
        {
            "deleted_at": None,
            "deleted_by": None,
            "uid": post_uid,
            "user_uid": owner,
            "slug": make_combined_slug(marker, post_uid),
            "title": None,
            "content": marker,
            "topic": topic,
            "project_uid": None,
            "image": None,
            "stars": 0,
            "created_at": datetime.now(timezone.utc).isoformat(),
        }
    )
    comments = get_table("comments")
    base = datetime.now(timezone.utc)
    for i, text in enumerate(comment_texts):
        comments.insert(
            {
                "deleted_at": None,
                "deleted_by": None,
                "uid": str(uuid4()),
                "target_type": "post",
                "target_uid": post_uid,
                "post_uid": post_uid,
                "user_uid": owner,
                "content": text,
                "parent_uid": None,
                "created_at": (base + timedelta(seconds=i)).isoformat(),
            }
        )
    return marker, post_uid
def _create_post_ui(page, content, topic="devlog"):
    page.goto(f"{BASE_URL}/feed", wait_until="domcontentloaded")
    page.locator(".feed-fab").first.wait_for(state="visible", timeout=10000)
    page.locator(".feed-fab").first.click()
    page.check(f"input[value='{topic}']")
    page.fill("#post-content", content)
    page.locator("#create-post-modal button.btn-primary:has-text('Post')").click()
    page.wait_for_url(f"{BASE_URL}/posts/*", wait_until="domcontentloaded")
def _seed_searchable_posts():
    token = uuid4().hex[:10]
    users = get_table("users")
    posts = get_table("posts")
    base = datetime(2026, 1, 1, tzinfo=timezone.utc)
    match_title = f"Findable post {token}"
    other_title = f"Unrelated post {uuid4().hex[:10]}"
    for offset, title in ((0, match_title), (1, other_title)):
        owner = str(uuid4())
        users.insert(
            {
                "uid": owner,
                "username": f"srch_{owner[:8]}",
                "terms_version": "1",
                "email": f"{owner[:8]}@test.devplace",
                "password_hash": "x",
                "role": "Member",
                "is_active": True,
                "created_at": datetime.now(timezone.utc).isoformat(),
            }
        )
        uid = str(uuid4())
        posts.insert(
            {
                "uid": uid,
                "user_uid": owner,
                "slug": make_combined_slug(title, uid),
                "title": title,
                "content": f"Body for {title}",
                "topic": "devlog",
                "project_uid": None,
                "image": None,
                "stars": 0,
                "created_at": (base - timedelta(seconds=offset)).isoformat(),
                "deleted_at": None,
                "deleted_by": None,
            }
        )
    return token, match_title, other_title
from devplacepy.utils import generate_uid
DEFAULT_MAINTENANCE_MESSAGE = (
    "DevPlace is undergoing scheduled maintenance. Please check back shortly."
)
OPERATIONAL_FIELDS = (
    "rate_limit_per_minute",
    "rate_limit_window_seconds",
    "session_max_age_days",
    "session_remember_days",
    "registration_open",
    "maintenance_mode",
    "maintenance_message",
)
def _save_settings(page, **fields):
    page.goto(f"{BASE_URL}/admin/settings", wait_until="domcontentloaded")
    for name, value in fields.items():
        locator = page.locator(f"#{name}")
        tag = locator.evaluate("el => el.tagName.toLowerCase()")
        if tag == "select":
            page.select_option(f"#{name}", value)
        else:
            locator.fill(value)
    page.click("button:has-text('Save Settings')")
    page.wait_for_url("**/admin/settings", wait_until="domcontentloaded")
def create_post(page, topic="random", content="Test post content", title=None):
    page.goto(f"{BASE_URL}/feed", wait_until="domcontentloaded")
    page.locator(".feed-fab").first.wait_for(state="visible", timeout=10000)
    page.locator(".feed-fab").first.click()
    page.check(f"input[value='{topic}']")
    page.fill("#post-content", content)
    if title:
        page.fill("#post-title", title)
    page.locator("#create-post-modal button.btn-primary:has-text('Post')").click()
    page.wait_for_url(f"{BASE_URL}/posts/*", wait_until="domcontentloaded")
def _profile_stars(page, username):
    page.goto(f"{BASE_URL}/profile/{username}", wait_until="domcontentloaded")
    value = page.locator(
        ".profile-stat:has(.profile-stat-label:has-text('Stars')) .profile-stat-value"
    ).first
    return int(value.text_content().strip())
def _key_role_visibility(username):
    return get_table("users").find_one(username=username)["api_key"]
_author_key = []
def _author():
    # A dedicated Member author so seeding posts never pollutes alice/bob state
    # (notifications, post counts, XP) that other test files assert on.
    if _author_key:
        return _author_key[0]
    name = "rolevis_author"
    requests.post(
        f"{BASE_URL}/auth/signup",
        data={
            "username": name,
            "email": f"{name}@t.dev",
            "password": "secret123",
            "confirm_password": "secret123",
            "birth_date": "1990-01-01",
            "accept_terms": "1",
        },
        allow_redirects=True,
    )
    row = get_table("users").find_one(username=name)
    get_table("users").update({"uid": row["uid"], "role": "Member"}, ["uid"])
    _author_key.append(row["api_key"])
    return _author_key[0]
def _seed_post_role_visibility():
    requests.post(
        f"{BASE_URL}/posts/create",
        headers={"X-API-KEY": _author()},
        data={
            "content": "role visibility seed post body text",
            "title": "RoleVis",
            "topic": "random",
        },
    )
def _seed_news_seo():
    from datetime import datetime, timezone
    from devplacepy.database import get_table
    from devplacepy.utils import generate_uid, make_combined_slug

    uid = generate_uid()
    title = f"SEO Test News Article {uid.split('-')[-1]}"
    slug = make_combined_slug(title, uid)
    get_table("news").insert(
        {
            "deleted_at": None,
            "deleted_by": None,
            "uid": uid,
            "slug": slug,
            "title": title,
            "description": "A seeded news article for SEO tests.",
            "content": "Body content for the seeded article.",
            "url": "https://example.com/article",
            "source_name": "ExampleSource",
            "status": "published",
            "synced_at": datetime.now(timezone.utc).isoformat(),
            "show_on_landing": 0,
            "grade": 8,
        }
    )
    return slug, uid
def _seed_owner():
    from datetime import datetime, timezone
    from uuid import uuid4
    from devplacepy.database import get_table

    uid = str(uuid4())
    get_table("users").insert(
        {
            "uid": uid,
            "username": f"seo_{uid[:8]}",
            "terms_version": "1",
            "email": f"{uid[:8]}@seo.test",
            "password_hash": "x",
            "role": "Member",
            "is_active": True,
            "created_at": datetime.now(timezone.utc).isoformat(),
        }
    )
    return uid
def _seed_post_seo(image=None):
    from datetime import datetime, timezone
    from uuid import uuid4
    from devplacepy.database import get_table
    from devplacepy.utils import make_combined_slug

    owner = _seed_owner()
    uid = str(uuid4())
    slug = make_combined_slug("SEO Detail Post", uid)
    get_table("posts").insert(
        {
            "deleted_at": None,
            "deleted_by": None,
            "uid": uid,
            "user_uid": owner,
            "slug": slug,
            "title": "SEO Detail Post",
            "content": "Body text for the SEO detail post.",
            "topic": "general",
            "project_uid": None,
            "image": image,
            "stars": 0,
            "created_at": datetime.now(timezone.utc).isoformat(),
        }
    )
    return slug, uid
def _seed_gist():
    from datetime import datetime, timezone
    from uuid import uuid4
    from devplacepy.database import get_table
    from devplacepy.utils import make_combined_slug

    owner = _seed_owner()
    uid = str(uuid4())
    slug = make_combined_slug("SEO Detail Gist", uid)
    get_table("gists").insert(
        {
            "deleted_at": None,
            "deleted_by": None,
            "uid": uid,
            "user_uid": owner,
            "slug": slug,
            "title": "SEO Detail Gist",
            "description": "Gist description for SEO tests.",
            "source_code": "print('seo')",
            "language": "python",
            "stars": 0,
            "created_at": datetime.now(timezone.utc).isoformat(),
        }
    )
    return slug, uid
def _seed_project():
    from datetime import datetime, timezone
    from uuid import uuid4
    from devplacepy.database import get_table
    from devplacepy.utils import make_combined_slug

    owner = _seed_owner()
    uid = str(uuid4())
    slug = make_combined_slug("SEO Detail Project", uid)
    get_table("projects").insert(
        {
            "deleted_at": None,
            "deleted_by": None,
            "uid": uid,
            "user_uid": owner,
            "slug": slug,
            "title": "SEO Detail Project",
            "description": "Project description for SEO tests.",
            "project_type": "software",
            "platforms": "Linux",
            "status": "Released",
            "stars": 0,
            "created_at": datetime.now(timezone.utc).isoformat(),
        }
    )
    return slug, uid
def _seed_news_image(news_uid):
    from devplacepy.database import get_table

    get_table("news_images").insert(
        {
            "deleted_at": None,
            "deleted_by": None,
            "news_uid": news_uid,
            "url": "https://example.com/seo-news-image.jpg",
        }
    )
def _seed_feed_posts(count):
    from datetime import datetime, timezone, timedelta
    from uuid import uuid4
    from devplacepy.database import get_table
    from devplacepy.utils import make_combined_slug

    owner = _seed_owner()
    topic = f"seopag{owner[:8]}"
    base = datetime(2026, 1, 1, tzinfo=timezone.utc)
    posts = get_table("posts")
    for i in range(count):
        uid = str(uuid4())
        posts.insert(
            {
                "deleted_at": None,
                "deleted_by": None,
                "uid": uid,
                "user_uid": owner,
                "slug": make_combined_slug(f"seo pag {i}", uid),
                "title": None,
                "content": f"seo pag post {i}",
                "topic": topic,
                "project_uid": None,
                "image": None,
                "stars": 0,
                "created_at": (base - timedelta(seconds=i)).isoformat(),
            }
        )
    return topic


def test_issues_footer_link(alice):
    page, _ = alice
    page.goto(f"{BASE_URL}/feed", wait_until="domcontentloaded")
    link = page.locator("a:has-text('Issue Report')")
    assert link.is_visible()
    link.click()
    page.wait_for_url(f"{BASE_URL}/issues", wait_until="domcontentloaded")


def test_global_toggle_suppresses_rendering_and_persists(bob):
    page, user = bob
    uid = _user_customization_toggle(user["username"])["uid"]
    set_customization_pref(uid, "global", disabled=False)
    sentinel = f"sentinel-{int(time.time() * 1000)}"
    _seed_global_css(uid, sentinel)

    assert sentinel in _goto_feed_until(page, lambda c: sentinel in c)

    _profile(page, user["username"])
    _click_toggle(page, "global")
    expect(page.locator("[data-customization-toggle='global']")).to_have_text("Enable")

    assert sentinel not in _goto_feed_until(page, lambda c: sentinel not in c)

    _profile(page, user["username"])
    expect(page.locator("[data-customization-toggle='global']")).to_have_text("Enable")
    expect(
        page.locator("form[action$='/customization/global'] input[name='value']")
    ).to_have_value("1")

    _click_toggle(page, "global")
    expect(page.locator("[data-customization-toggle='global']")).to_have_text("Disable")
    assert sentinel in _goto_feed_until(page, lambda c: sentinel in c)


def test_pagetype_toggle_is_independent(bob):
    page, user = bob
    uid = _user_customization_toggle(user["username"])["uid"]
    set_customization_pref(uid, "global", disabled=False)
    set_customization_pref(uid, "pagetype", disabled=False)
    global_sentinel = f"glob-{int(time.time() * 1000)}"
    page_sentinel = f"page-{int(time.time() * 1000)}"
    _seed_global_css(uid, global_sentinel)

    page.goto(f"{BASE_URL}/feed", wait_until="domcontentloaded")
    page_type = page.locator("main[data-page-type]").get_attribute("data-page-type")
    set_custom_override(
        "user", uid, page_type, "css", f".{page_sentinel} {{ color: blue; }}"
    )

    body = _goto_feed_until(
        page, lambda c: global_sentinel in c and page_sentinel in c
    )
    assert global_sentinel in body
    assert page_sentinel in body

    _profile(page, user["username"])
    _click_toggle(page, "pagetype")

    body = _goto_feed_until(page, lambda c: page_sentinel not in c)
    assert global_sentinel in body
    assert page_sentinel not in body


def test_feed_page_loads(alice):
    page, user = alice
    page.goto(f"{BASE_URL}/feed", wait_until="domcontentloaded")
    assert page.is_visible("text=Topics")
    assert page.is_visible("text=All")
    assert page.is_visible("text=Devlog")
    assert page.is_visible("text=Showcase")


def test_feed_nav_tabs(alice):
    page, user = alice
    page.goto(f"{BASE_URL}/feed", wait_until="domcontentloaded")
    assert page.is_visible("a:has-text('All')")
    assert page.is_visible("a:has-text('Trending')")
    assert page.is_visible("a:has-text('Recent')")
    assert page.is_visible("a:has-text('Following')")


def test_feed_tab_switching(alice):
    page, user = alice
    page.goto(f"{BASE_URL}/feed", wait_until="domcontentloaded")
    page.click("a:has-text('Trending')")
    page.wait_for_url(f"{BASE_URL}/feed?tab=trending", wait_until="domcontentloaded")
    page.click("a:has-text('Recent')")
    page.wait_for_url(f"{BASE_URL}/feed?tab=recent", wait_until="domcontentloaded")


def test_feed_topic_filter_sidebar(alice):
    page, user = alice
    page.goto(f"{BASE_URL}/feed", wait_until="domcontentloaded")
    topics = ["Devlog", "Showcase", "Question", "Rant", "Fun"]
    for topic in topics:
        link = page.locator(f".sidebar-nav a:has-text('{topic}')").first
        assert link.is_visible()


def test_feed_topic_filter_navigation(alice):
    page, user = alice
    page.goto(f"{BASE_URL}/feed", wait_until="domcontentloaded")
    page.click("a:has-text('Devlog')")
    page.wait_for_url(f"{BASE_URL}/feed?topic=devlog", wait_until="domcontentloaded")


def test_feed_search_bar_visible(alice):
    page, user = alice
    page.goto(f"{BASE_URL}/feed", wait_until="domcontentloaded")
    search = page.locator("input[name='search'][placeholder='Search posts...']")
    assert search.is_visible()


def test_feed_search_filters(alice):
    page, user = alice
    token, match_title, other_title = _seed_searchable_posts()
    page.goto(f"{BASE_URL}/feed?search={token}", wait_until="domcontentloaded")
    assert page.locator(f".post-title:has-text('{match_title}')").count() == 1
    assert page.locator(f".post-title:has-text('{other_title}')").count() == 0


def test_feed_search_no_match(alice):
    page, user = alice
    _seed_searchable_posts()
    page.goto(
        f"{BASE_URL}/feed?search=zzz{uuid4().hex}", wait_until="domcontentloaded"
    )
    assert page.is_visible(".empty-state")
    assert page.locator(".post-title").count() == 0


def test_feed_search_preserves_topic(alice):
    page, user = alice
    token, match_title, _ = _seed_searchable_posts()
    page.goto(
        f"{BASE_URL}/feed?topic=devlog&search={token}", wait_until="domcontentloaded"
    )
    assert page.locator(f".post-title:has-text('{match_title}')").count() == 1
    preserved = page.locator(
        ".sidebar-card form input[type='hidden'][name='topic'][value='devlog']"
    )
    assert preserved.count() == 1


def test_feed_community_stats(alice):
    page, user = alice
    page.goto(f"{BASE_URL}/feed", wait_until="domcontentloaded")
    assert page.is_visible("text=Total Members")
    assert page.is_visible("text=Posts Today")
    assert page.is_visible("text=Total Projects")


def test_feed_top_authors(alice):
    page, user = alice
    page.goto(f"{BASE_URL}/feed", wait_until="domcontentloaded")
    assert page.is_visible("text=Top Authors")


def _resources_panel(page):
    return page.locator(".sidebar-card .sidebar-section").filter(
        has=page.locator(".sidebar-heading:has-text('Resources')")
    )


def test_feed_resources_panel_visible(alice):
    page, user = alice
    page.goto(f"{BASE_URL}/feed", wait_until="domcontentloaded")
    panel = _resources_panel(page)
    panel.wait_for(state="visible")
    expect(panel.locator(".sidebar-heading")).to_have_text("Resources")
    expect(panel.locator("a.sidebar-link:has-text('Docs')")).to_be_visible()
    expect(panel.locator("a.sidebar-link:has-text('Issues')")).to_be_visible()
    expect(panel.locator("a.sidebar-link[data-devii-open]")).to_be_visible()


def test_feed_resources_docs_link(alice):
    page, user = alice
    page.goto(f"{BASE_URL}/feed", wait_until="domcontentloaded")
    docs = _resources_panel(page).locator("a.sidebar-link:has-text('Docs')")
    assert docs.get_attribute("href") == "/docs"
    docs.click()
    page.wait_for_url("**/docs/index.html", wait_until="domcontentloaded")


def test_feed_resources_issues_link(alice):
    page, user = alice
    page.goto(f"{BASE_URL}/feed", wait_until="domcontentloaded")
    issues = _resources_panel(page).locator("a.sidebar-link:has-text('Issues')")
    assert issues.get_attribute("href") == "/issues"
    issues.click()
    page.wait_for_url(f"{BASE_URL}/issues", wait_until="domcontentloaded")


def test_feed_resources_devii_opens_assistant(alice):
    page, user = alice
    page.goto(f"{BASE_URL}/feed", wait_until="domcontentloaded")
    devii = _resources_panel(page).locator("a.sidebar-link[data-devii-open]")
    expect(devii).to_be_visible()
    assert devii.get_attribute("href") == "/devii/"
    expect(devii).to_have_text(re.compile("Devii"))


def test_feed_daily_topic(alice):
    page, user = alice
    page.goto(f"{BASE_URL}/feed", wait_until="domcontentloaded")
    assert page.is_visible("text=Daily Topic")


def test_create_post_fab(alice):
    page, user = alice
    page.goto(f"{BASE_URL}/feed", wait_until="domcontentloaded")
    fab = page.locator(".feed-fab")
    assert fab.is_visible()
    fab.click()
    assert page.is_visible("text=Create New Post")
    assert page.is_visible("#post-content")
    assert page.is_visible("#create-post-modal button:has-text('Post')")


def test_topnav_notification_bell(alice):
    page, _ = alice
    page.goto(f"{BASE_URL}/feed", wait_until="domcontentloaded")
    bell = page.locator(".topnav-icon[href='/notifications']")
    assert bell.is_visible()


def test_topnav_user_menu(alice):
    page, user = alice
    page.goto(f"{BASE_URL}/feed", wait_until="domcontentloaded")
    assert page.is_visible(f"text={user['username']}")


def test_feed_inline_comment_placeholder(alice):
    page, _ = alice
    marker = f"placeholder-{uuid4().hex[:8]}"
    _create_post_ui(page, f"{marker} placeholder check post")
    page.goto(f"{BASE_URL}/feed", wait_until="domcontentloaded")
    card = page.locator(".post-card").filter(has_text=marker).first
    inline_input = card.locator(".comment-form textarea[name='content']")
    expect(inline_input).to_be_visible()
    assert inline_input.get_attribute("placeholder") == "Your opinion goes here..."


def test_feed_shows_last_three_comments(page, app_server):
    m = uuid4().hex[:6]
    marker, _ = _seed_post_with_comments(
        [f"{m}-one", f"{m}-two", f"{m}-three", f"{m}-four"]
    )
    page.goto(f"{BASE_URL}/feed", wait_until="domcontentloaded")
    card = page.locator(".post-card").filter(has_text=marker).first
    card.wait_for(state="visible")
    previews = card.locator(".post-card-comments .comment-text")
    expect(previews).to_have_count(3)
    expect(previews.nth(0)).to_contain_text(f"{m}-two")
    expect(previews.nth(1)).to_contain_text(f"{m}-three")
    expect(previews.nth(2)).to_contain_text(f"{m}-four")
    expect(card.locator(".post-card-comments")).not_to_contain_text(f"{m}-one")


def test_feed_guest_vote_disabled(page, app_server):
    marker, _ = _seed_post_with_comments([f"{uuid4().hex[:6]}-c"])
    page.goto(f"{BASE_URL}/feed", wait_until="domcontentloaded")
    card = page.locator(".post-card").filter(has_text=marker).first
    card.wait_for(state="visible")
    assert card.locator(".post-action-btn.vote-up").first.is_disabled()


def test_feed_guest_reply_disabled(page, app_server):
    marker, _ = _seed_post_with_comments([f"{uuid4().hex[:6]}-c"])
    page.goto(f"{BASE_URL}/feed", wait_until="domcontentloaded")
    card = page.locator(".post-card").filter(has_text=marker).first
    card.wait_for(state="visible")
    assert card.locator(".post-card-comments [data-action='reply']").first.is_disabled()


def test_feed_politics_topic(alice):
    page, _ = alice
    page.goto(f"{BASE_URL}/feed", wait_until="domcontentloaded")
    politics_link = page.locator(".sidebar-nav a:has-text('Politics')")
    assert politics_link.is_visible()


def test_paste_image_attaches_in_post_composer(alice):
    page, _ = alice
    page.goto(f"{BASE_URL}/feed", wait_until="domcontentloaded")
    page.locator(".feed-fab").first.wait_for(state="visible", timeout=10000)
    page.locator(".feed-fab").first.click()
    page.locator("#post-content").wait_for(state="visible", timeout=10000)
    page.locator("#create-post-modal dp-upload .dp-upload-btn").first.wait_for(
        state="visible", timeout=10000
    )
    paste_image(page, "#post-content")
    page.locator("#create-post-modal dp-upload .dp-upload-count").first.wait_for(
        state="visible", timeout=15000
    )
    expect(
        page.locator("#create-post-modal dp-upload input[name='attachment_uids']")
    ).to_have_value(re.compile(r".+"))


def test_feed_card_shows_the_post_image(alice):
    import io
    from PIL import Image

    page, _ = alice
    buf = io.BytesIO()
    Image.new("RGB", (600, 400), (28, 120, 200)).save(buf, "PNG")
    create_post_with_files(
        page,
        "Feed card image rendering check",
        [{"name": "card.png", "mimeType": "image/png", "buffer": buf.getvalue()}],
    )
    page.goto(f"{BASE_URL}/feed", wait_until="domcontentloaded")
    card = page.locator(
        ".post-card:has-text('Feed card image rendering check')"
    ).first
    card.wait_for(state="visible", timeout=10000)
    image = card.locator(".attachment-gallery.single .gallery-thumb").first
    image.wait_for(state="visible", timeout=10000)
    assert "_thumb" not in image.get_attribute("src")


def test_create_post_cancel_modal(alice):
    page, _ = alice
    page.goto(f"{BASE_URL}/feed", wait_until="domcontentloaded")
    page.locator(".feed-fab").click()
    cancel = page.locator("#create-post-modal button:has-text('Cancel')").first
    cancel.click()
    modal = page.locator("#create-post-modal")
    assert not modal.is_visible()


def test_feed_pagination_first_page(alice):
    page, _ = alice
    topic = _seed_posts(26)
    page.goto(f"{BASE_URL}/feed?topic={topic}", wait_until="domcontentloaded")
    assert page.locator(".post-card").count() == 25
    assert page.is_visible(".load-more-wrap")


def test_feed_pagination_load_more(alice):
    page, _ = alice
    topic = _seed_posts(26)
    page.goto(f"{BASE_URL}/feed?topic={topic}", wait_until="domcontentloaded")
    page.click(".load-more-wrap a")
    page.wait_for_url(lambda url: "before=" in url, wait_until="domcontentloaded")
    assert f"topic={topic}" in page.url
    assert page.locator(".post-card").count() == 1
    assert not page.is_visible(".load-more-wrap")


def test_feed_public_access(page, app_server):
    page.goto(f"{BASE_URL}/feed", wait_until="domcontentloaded")
    assert page.is_visible("text=Topics")
    assert page.is_visible("text=All")
    assert page.is_visible("text=Login")


def test_feed_guest_fab_links_to_login(page, app_server):
    page.goto(f"{BASE_URL}/feed", wait_until="domcontentloaded")
    fab = page.locator(".feed-fab")
    assert fab.count() == 1
    assert "/auth/login" in (fab.first.get_attribute("href") or "")


def test_feed_guest_inline_comment_disabled(page, app_server):
    _seed_post_with_comments([f"{uuid4().hex[:6]}-c"])
    page.goto(f"{BASE_URL}/feed", wait_until="domcontentloaded")
    assert page.locator(".post-card").count() > 0
    form = page.locator(".comment-form.comment-form-guest").first
    form.wait_for(state="attached")
    assert form.locator("textarea").first.is_disabled()


def test_leaderboard_nav_link(alice):
    page, _ = alice
    page.goto(f"{BASE_URL}/feed", wait_until="domcontentloaded")
    assert page.is_visible("a.topnav-icon[href='/leaderboard']")


def test_mobile_hamburger_nav(alice):
    page, _ = alice
    page.set_viewport_size({"width": 390, "height": 844})
    page.goto(f"{BASE_URL}/feed", wait_until="domcontentloaded")
    btn = page.locator("#hamburger-btn")
    btn.wait_for(state="visible")
    btn.click()
    page.locator("#mobile-panel.open").wait_for(state="visible")
    assert "open" in (page.locator("#mobile-panel").get_attribute("class") or "")


def test_notifications_bell_visible(alice):
    page, _ = alice
    page.goto(f"{BASE_URL}/feed", wait_until="domcontentloaded")
    bell = page.locator(".topnav-icon[href='/notifications']")
    assert bell.is_visible()


def test_maintenance_mode_blocks_guests(alice):
    page, _ = alice
    try:
        _save_settings(page, maintenance_mode="1", maintenance_message="Down for tests")
        deadline = time.time() + 5.0
        response = requests.get(f"{BASE_URL}/feed")
        while response.status_code != 503 and time.time() < deadline:
            time.sleep(0.2)
            response = requests.get(f"{BASE_URL}/feed")
        assert response.status_code == 503
        assert "Down for tests" in response.text
    finally:
        _save_settings(
            page, maintenance_mode="0", maintenance_message=DEFAULT_MAINTENANCE_MESSAGE
        )


def test_back_to_feed_link(alice):
    page, _ = alice
    create_post(page, "random", "Back link test")
    back = page.locator("a:has-text('Back to Feed')")
    assert back.is_visible()
    back.click()
    page.wait_for_url(f"{BASE_URL}/feed", wait_until="domcontentloaded")


def test_delete_own_post(alice):
    page, _ = alice
    create_post(page, "random", "Post to be deleted permanently here")
    post_url = page.url
    page.locator(".post-action-btn:has-text('Delete')").click()
    page.locator(".dialog-overlay.visible .dialog-confirm").click()
    page.wait_for_url(f"{BASE_URL}/feed", wait_until="domcontentloaded")
    resp = page.goto(post_url, wait_until="domcontentloaded")
    assert resp.status == 404


def test_profile_nav_from_topbar(alice):
    page, user = alice
    page.goto(f"{BASE_URL}/feed", wait_until="domcontentloaded")
    page.locator(".topnav-user").click()
    profile_link = page.locator(".dropdown-menu a:has-text('Profile')")
    profile_link.wait_for(state="visible")
    profile_link.click()
    page.wait_for_url(
        f"**/profile/{user['username']}", wait_until="domcontentloaded"
    )
    assert page.is_visible(f"text={user['username']}")


def test_guest_has_no_admin_nav(page, app_server):
    page.goto(f"{BASE_URL}/feed", wait_until="domcontentloaded")
    assert page.locator("a[href='/admin']").count() == 0
    assert page.locator("a[href='/auth/login']").count() >= 1


def test_guest_sees_no_role_badges(page, seeded_db):
    _seed_post_role_visibility()
    page.goto(f"{BASE_URL}/feed", wait_until="domcontentloaded")
    page.locator(".post-card").first.wait_for(state="visible")
    assert page.locator(".post-author-role").count() == 0


def test_guest_action_controls_disabled(page, seeded_db):
    _seed_post_role_visibility()
    page.goto(f"{BASE_URL}/feed", wait_until="domcontentloaded")
    card = page.locator(".post-card").first
    card.wait_for(state="visible")
    assert card.locator(".vote-up").first.is_disabled()
    fab = page.locator(".feed-fab").first
    assert "/auth/login" in (fab.get_attribute("href") or "")


def test_member_has_no_admin_nav(bob):
    page, _ = bob
    page.goto(f"{BASE_URL}/feed", wait_until="domcontentloaded")
    assert page.locator("a[href='/admin']").count() == 0


def test_member_sees_no_role_badges(bob):
    page, _ = bob
    _seed_post_role_visibility()
    page.goto(f"{BASE_URL}/feed", wait_until="domcontentloaded")
    page.locator(".post-card").first.wait_for(state="visible")
    assert page.locator(".post-author-role").count() == 0


def test_member_can_act(bob):
    page, _ = bob
    _seed_post_role_visibility()
    page.goto(f"{BASE_URL}/feed", wait_until="domcontentloaded")
    fab = page.locator(".feed-fab").first
    fab.wait_for(state="visible")
    assert (fab.get_attribute("data-modal") or "") == "create-post-modal"
    card = page.locator(".post-card").first
    card.wait_for(state="visible")
    assert not card.locator(".vote-up").first.is_disabled()


def test_admin_has_admin_nav(alice):
    page, _ = alice
    page.goto(f"{BASE_URL}/feed", wait_until="domcontentloaded")
    assert page.locator("a[href='/admin']").count() >= 1


def test_feed_shows_author_level(alice):
    page, _ = alice
    _seed_post_role_visibility()
    page.goto(f"{BASE_URL}/feed", wait_until="domcontentloaded")
    page.locator(".post-card").first.wait_for(state="visible")
    assert page.locator(".post-author-level").count() >= 1


def test_feed_canonical_preserves_pagination(page, app_server):
    page.goto(f"{BASE_URL}/feed?page=2", wait_until="domcontentloaded")
    href = page.locator('link[rel="canonical"]').get_attribute("href")
    assert href.endswith("?page=2"), href


def test_feed_canonical_drops_non_pagination_params(page, app_server):
    page.goto(f"{BASE_URL}/feed?sort=new", wait_until="domcontentloaded")
    href = page.locator('link[rel="canonical"]').get_attribute("href")
    assert href.endswith("/feed"), href


def test_default_og_image_is_raster(page, app_server):
    page.goto(f"{BASE_URL}/feed", wait_until="domcontentloaded")
    og = page.locator('meta[property="og:image"]').get_attribute("content")
    assert og.endswith(".png"), og


def test_feed_pagination_emits_rel_next(page, app_server):
    topic = _seed_feed_posts(26)
    page.goto(f"{BASE_URL}/feed?topic={topic}", wait_until="domcontentloaded")
    nxt = page.locator('link[rel="next"]')
    assert nxt.count() == 1
    href = nxt.get_attribute("href")
    assert "before=" in href and f"topic={topic}" in href, href


def test_feed_online_now_widget_lists_current_user(alice):
    page, user = alice
    page.goto(f"{BASE_URL}/feed", wait_until="domcontentloaded")
    widget = page.locator(".online-users")
    expect(widget).to_be_visible()
    expect(
        page.locator(f".online-user[title='{user['username']}']")
    ).to_have_count(1)
    expect(page.locator(".online-users .presence-dot.online").first).to_be_visible()


def _scroll_and_open_post(page):
    _seed_feed_posts(30)
    page.goto(f"{BASE_URL}/feed", wait_until="domcontentloaded")
    page.locator(".post-card").first.wait_for(state="visible")
    page.evaluate("window.scrollTo(0, 1200)")
    page.wait_for_function("window.scrollY > 1000")
    card_link = page.locator(".post-card .card-link").nth(5)
    card_link.scroll_into_view_if_needed()
    page.wait_for_timeout(400)
    y_before = page.evaluate("window.scrollY")
    assert y_before > 0
    card_link.click()
    page.wait_for_url(f"{BASE_URL}/posts/*", wait_until="domcontentloaded")
    return y_before


def test_feed_scroll_restored_via_back_link(alice):
    page, user = alice
    y_before = _scroll_and_open_post(page)
    back = page.locator("a.back-link")
    back.wait_for(state="visible")
    back.click()
    page.wait_for_url(f"{BASE_URL}/feed*", wait_until="domcontentloaded")
    page.wait_for_function(f"Math.abs(window.scrollY - {y_before}) < 60")


def test_feed_scroll_restored_via_browser_back(alice):
    page, user = alice
    y_before = _scroll_and_open_post(page)
    page.go_back(wait_until="domcontentloaded")
    page.wait_for_function(f"Math.abs(window.scrollY - {y_before}) < 60")


def test_feed_scroll_not_restored_on_fresh_visit(alice):
    page, user = alice
    _scroll_and_open_post(page)
    page.goto(f"{BASE_URL}/gists", wait_until="domcontentloaded")
    page.goto(f"{BASE_URL}/feed", wait_until="domcontentloaded")
    page.wait_for_timeout(600)
    assert page.evaluate("window.scrollY") < 60


def _seed_post_with_long_code_line(topic="devlog"):
    owner = str(uuid4())
    get_table("users").insert(
        {
            "uid": owner,
            "username": f"code_{owner[:8]}",
            "email": f"{owner[:8]}@code.seed",
            "password_hash": "x",
            "role": "Member",
            "is_active": True,
            "created_at": datetime.now(timezone.utc).isoformat(),
        }
    )
    uid = str(uuid4())
    marker = f"longcode-{uid[:8]}"
    line = "unbreakable_identifier_" * 10
    get_table("posts").insert(
        {
            "deleted_at": None,
            "deleted_by": None,
            "uid": uid,
            "user_uid": owner,
            "slug": make_combined_slug(marker, uid),
            "title": marker,
            "content": f"```python\n{line}\n```",
            "topic": topic,
            "project_uid": None,
            "image": None,
            "stars": 0,
            "created_at": datetime.now(timezone.utc).isoformat(),
        }
    )
    return marker


def test_feed_long_code_line_does_not_widen_layout(alice):
    page, user = alice
    marker = _seed_post_with_long_code_line()
    page.goto(f"{BASE_URL}/feed", wait_until="domcontentloaded")
    card = page.locator(".post-card").filter(has_text=marker).first
    card.wait_for(state="visible")
    assert card.locator("pre").count() == 1
    assert_no_horizontal_overflow(page, ".feed-layout")


def test_dashboard_long_code_line_does_not_widen_layout(alice):
    page, user = alice
    marker = _seed_post_with_long_code_line()
    page.goto(f"{BASE_URL}/", wait_until="domcontentloaded")
    card = page.locator(".dashboard-post-card").filter(has_text=marker).first
    card.wait_for(state="visible")
    assert card.locator("pre").count() == 1
    assert_no_horizontal_overflow(page, ".dashboard-main")
