# retoor <retoor@molodetz.nl>

import base64
import time
import requests
from tests.conftest import BASE_URL
from devplacepy.database import get_table
_counter_api_auth = [0]
def _signup_api_auth(password="secret123"):
    _counter_api_auth[0] += 1
    name = f"apiauth{int(time.time() * 1000)}{_counter_api_auth[0]}"
    email = f"{name}@t.dev"
    session = requests.Session()
    session.post(
        f"{BASE_URL}/auth/signup",
        data={
            "username": name,
            "email": email,
            "password": password,
            "confirm_password": password,
            "birth_date": "1990-01-01",
            "accept_terms": "1",
        },
        allow_redirects=True,
    )
    return session, name, email, password
def _user_api_auth(name):
    return get_table("users").find_one(username=name)
def _key_api_auth(name):
    return _user_api_auth(name)["api_key"]
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()
import re
from uuid import uuid4
from datetime import datetime, timedelta, timezone
from tests.conftest import BASE_URL, assert_share_copies
from devplacepy.utils import make_combined_slug
DPBOT_SIZE = 112147
SOURCE_LENGTH_LIMIT = 400000
def _make_source(length):
    unit = "def handler():\n    return 0\n"
    return (unit * (length // len(unit) + 1))[:length]
def _register_session(prefix):
    name = f"{prefix}{int(time.time() * 1000)}"
    session = requests.Session()
    session.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,
    )
    return session, name
def _seed_gists(count):
    owner = str(uuid4())
    get_table("users").insert(
        {
            "uid": owner,
            "username": f"pag_{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(),
        }
    )
    base = datetime(2026, 1, 1, tzinfo=timezone.utc)
    gists = get_table("gists")
    for i in range(count):
        uid = str(uuid4())
        title = f"Pag Gist {i}"
        gists.insert(
            {
                "deleted_at": None,
                "deleted_by": None,
                "uid": uid,
                "user_uid": owner,
                "slug": make_combined_slug(title, uid),
                "title": title,
                "language": "python",
                "description": "paginated",
                "stars": 0,
                "created_at": (base - timedelta(seconds=i)).isoformat(),
            }
        )
    return owner
def _set_cm_value(page, value):
    page.evaluate(f"""() => {{
        const cm = document.querySelector(".CodeMirror")?.CodeMirror;
        if (cm) {{
            cm.setValue({repr(value)});
            cm.save();
        }}
    }}""")
def _create_gist(
    page,
    title="Test Gist",
    description="Test description",
    language="python",
    source_code="print('hello')",
):
    page.goto(f"{BASE_URL}/gists", wait_until="domcontentloaded")
    page.locator("#create-gist-btn").wait_for(state="visible", timeout=10000)
    page.locator("#create-gist-btn").click()
    page.wait_for_timeout(800)
    page.fill("#gist-title", title)
    page.fill("#gist-description", description)
    page.select_option("#gist-language", language)
    page.wait_for_timeout(300)
    _set_cm_value(page, source_code)
    page.wait_for_timeout(300)
    page.locator("button.btn-primary:has-text('Create Gist')").click()
    page.wait_for_timeout(2000)
    current = page.url
    if "/gists/" in current and current != f"{BASE_URL}/gists":
        return
    page.wait_for_url("**/gists/*", timeout=15000, wait_until="domcontentloaded")
def _seed_searchable_gists(language="python"):
    token = uuid4().hex[:10]
    owner = str(uuid4())
    get_table("users").insert(
        {
            "uid": owner,
            "username": f"gsrch_{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(),
        }
    )
    base = datetime(2026, 1, 1, tzinfo=timezone.utc)
    gists = get_table("gists")
    match_title = f"Findable gist {token}"
    other_title = f"Unrelated gist {uuid4().hex[:10]}"
    for offset, title in ((0, match_title), (1, other_title)):
        uid = str(uuid4())
        gists.insert(
            {
                "uid": uid,
                "user_uid": owner,
                "slug": make_combined_slug(title, uid),
                "title": title,
                "language": language,
                "description": f"Description for {title}",
                "source_code": "x = 1",
                "stars": 0,
                "created_at": (base - timedelta(seconds=offset)).isoformat(),
                "deleted_at": None,
                "deleted_by": None,
            }
        )
    return token, match_title, other_title
import io
import uuid
from PIL import Image
JSON_media = {"Accept": "application/json"}
def _png_bytes_media(color=(200, 30, 30)):
    buf = io.BytesIO()
    Image.new("RGB", (8, 8), color).save(buf, "PNG")
    return buf.getvalue()
def _signup_media(prefix="media"):
    s = requests.Session()
    name = f"{prefix}_{uuid.uuid4().hex[:10]}"
    s.post(
        f"{BASE_URL}/auth/signup",
        data={
            "username": name,
            "email": f"{name}@test.devplace",
            "password": "secret123",
            "confirm_password": "secret123",
            "birth_date": "1990-01-01",
            "accept_terms": "1",
        },
        allow_redirects=True,
    )
    return s, name
def _login_media(seeded_db, who):
    s = requests.Session()
    creds = seeded_db[who]
    s.post(
        f"{BASE_URL}/auth/login",
        data={"email": creds["email"], "password": creds["password"]},
        allow_redirects=True,
    )
    return s
def _upload_media(s, name="pic.png", content=None, mime="image/png"):
    files = {"file": (name, content if content is not None else _png_bytes_media(), mime)}
    r = s.post(f"{BASE_URL}/uploads/upload", files=files)
    assert r.status_code == 201, r.text
    return r.json()["uid"]
def _create_post_media(s, attachment_uids, title="media post"):
    r = s.post(
        f"{BASE_URL}/posts/create",
        data={
            "content": "A post that carries media for the gallery tests.",
            "title": title,
            "topic": "random",
            "attachment_uids": attachment_uids,
        },
        allow_redirects=True,
    )
    assert r.status_code == 200, r.text[:300]
    assert "/posts/" in r.url, r.url
    return r.url
def _create_project_media(s, attachment_uids, title="Media Project"):
    r = s.post(
        f"{BASE_URL}/projects/create",
        data={
            "title": title,
            "description": "Project with media for gallery tests.",
            "project_type": "software",
            "platforms": "linux",
            "status": "In Development",
            "attachment_uids": attachment_uids,
        },
        allow_redirects=True,
    )
    assert r.status_code == 200, r.text[:300]
    assert "/projects/" in r.url, r.url
    return r.url
def _media_json(session, username):
    r = session.get(f"{BASE_URL}/profile/{username}?tab=media", headers=JSON_media)
    assert r.status_code == 200, r.text[:300]
    return r.json()
def _media_uids(session, username):
    return [m["uid"] for m in _media_json(session, username)["media"]]
def _seed_media_via_browser(page, title="ui media"):
    up = page.request.post(
        f"{BASE_URL}/uploads/upload",
        multipart={
            "file": {
                "name": "pic.png",
                "mimeType": "image/png",
                "buffer": _png_bytes_media(),
            }
        },
    )
    assert up.ok, up.text()
    uid = up.json()["uid"]
    post = page.request.post(
        f"{BASE_URL}/posts/create",
        form={
            "content": "media for the ui gallery test",
            "title": title,
            "topic": "random",
            "attachment_uids": uid,
        },
    )
    assert post.ok, post.text()
    return uid
def _media_uids_via_page(page, username):
    resp = page.request.get(f"{BASE_URL}/profile/{username}?tab=media", headers=JSON_media)
    return [m["uid"] for m in resp.json()["media"]]
from devplacepy.database import (
    get_notification_default,
    get_notification_prefs,
    get_table,
    notification_enabled,
    reset_notification_prefs,
    set_notification_default,
    set_notification_pref,
)
from devplacepy.utils import create_notification, generate_uid
def _user_notification_prefs(username):
    return get_table("users").find_one(username=username)
def _pref(prefs, key):
    return next(p for p in prefs if p["key"] == key)
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_admin_sees_other_users_key(alice):
    page, _ = alice
    _, member, _, _ = _signup_api_auth()
    page.goto(f"{BASE_URL}/profile/{member}", wait_until="domcontentloaded")
    assert page.locator("[data-api-key-card]").count() == 1
    assert _key_api_auth(member) in page.content()


def test_non_owner_non_admin_hidden_and_guarded(bob):
    page, user = bob
    member = _user_customization_toggle(user["username"])
    get_table("users").update({"uid": member["uid"], "role": "Member"}, ["uid"])
    clear_user_cache(member["uid"])
    target = _user_customization_toggle("alice_test")

    _profile(page, target["username"])
    assert page.locator("[data-customization]").count() == 0

    response = requests.post(
        f"{BASE_URL}/profile/{target['username']}/customization/global",
        data={"value": "1"},
        headers={"Accept": "application/json", "X-API-KEY": member["api_key"]},
        allow_redirects=False,
    )
    assert response.status_code == 403


def test_profile_gists_tab(alice, app_server):
    page, alice_user = alice
    title = f"Profile Tab Test {int(time.time())}"
    _create_gist(page, title=title, source_code="profile_tab = True")
    page.goto(
        f"{BASE_URL}/profile/{alice_user['username']}?tab=gists",
        wait_until="domcontentloaded",
    )
    assert page.is_visible(f"text={title}")


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


def test_ui_media_tab_link_visible(alice):
    page, user = alice
    page.goto(f"{BASE_URL}/profile/{user['username']}", wait_until="domcontentloaded")
    assert page.locator(".profile-tab:has-text('Media')").is_visible()


def test_ui_media_tab_shows_thumbnail(alice):
    page, user = alice
    _seed_media_via_browser(page)
    page.goto(
        f"{BASE_URL}/profile/{user['username']}?tab=media",
        wait_until="domcontentloaded",
    )
    assert page.locator(".profile-tab.active:has-text('Media')").is_visible()
    thumb = page.locator(".media-grid img.gallery-thumb[data-lightbox]").first
    thumb.wait_for(state="visible")
    assert thumb.get_attribute("data-full")


def test_ui_media_lightbox_opens(alice):
    page, user = alice
    _seed_media_via_browser(page)
    page.goto(
        f"{BASE_URL}/profile/{user['username']}?tab=media",
        wait_until="domcontentloaded",
    )
    thumb = page.locator(".media-grid img.gallery-thumb[data-lightbox]").first
    thumb.wait_for(state="visible")
    thumb.click()
    overlay = page.locator(".lightbox-overlay.visible")
    overlay.wait_for(state="visible")
    expect(page.locator(".lightbox-overlay.visible .lightbox-image")).to_be_visible()
    page.locator(".lightbox-overlay.visible .lightbox-close").click()
    overlay.wait_for(state="hidden")


def test_ui_owner_delete_removes_tile(alice):
    page, user = alice
    uid = _seed_media_via_browser(page, title="to delete")
    page.goto(
        f"{BASE_URL}/profile/{user['username']}?tab=media",
        wait_until="domcontentloaded",
    )
    tile = page.locator(f".media-tile[data-media-tile='{uid}']")
    tile.wait_for(state="attached")
    page.locator(f".media-tile[data-media-tile='{uid}'] .media-delete-btn").click()
    page.locator(".dialog-overlay.visible .dialog-confirm").click()
    tile.wait_for(state="detached")
    assert page.locator(f".media-tile[data-media-tile='{uid}']").count() == 0


def test_ui_member_sees_no_delete_button_on_other_profile(bob, alice):
    # alice (admin) seeds media on her own profile
    page_a, user_a = alice
    _seed_media_via_browser(page_a)
    # bob (member) views alice's gallery: no delete controls
    page_b, _ = bob
    page_b.goto(
        f"{BASE_URL}/profile/{user_a['username']}?tab=media",
        wait_until="domcontentloaded",
    )
    page_b.locator(".media-grid img.gallery-thumb").first.wait_for(state="visible")
    assert page_b.locator(".media-delete-btn").count() == 0


def test_ui_mobile_media_grid(mobile_page):
    page, user = mobile_page
    _seed_media_via_browser(page)
    page.goto(
        f"{BASE_URL}/profile/{user['username']}?tab=media",
        wait_until="domcontentloaded",
    )
    grid = page.locator(".media-grid")
    grid.wait_for(state="visible")
    assert page.locator(".media-grid .media-tile").count() >= 1


def test_profile_tab_renders_for_owner(bob):
    page, user = bob
    page.goto(
        f"{BASE_URL}/profile/{user['username']}?tab=notifications",
        wait_until="domcontentloaded",
    )
    expect(page.locator("[data-notif-prefs]")).to_be_visible()
    expect(
        page.locator("[data-notif-toggle][data-type='vote'][data-channel='push']")
    ).to_have_count(1)
    assert page.is_visible("text=Direct messages")


def test_owner_toggle_via_http_persists(bob):
    _, user = bob
    target = _user_notification_prefs(user["username"])
    reset_notification_prefs(target["uid"])
    try:
        response = requests.post(
            f"{BASE_URL}/profile/{target['username']}/notifications",
            data={"notification_type": "mention", "channel": "push", "value": "false"},
            headers={"Accept": "application/json", "X-API-KEY": target["api_key"]},
            allow_redirects=False,
        )
        assert response.status_code == 200
        assert response.json()["ok"] is True
        assert notification_enabled(target["uid"], "mention", "push") is False
        assert notification_enabled(target["uid"], "mention", "in_app") is True
    finally:
        reset_notification_prefs(target["uid"])


def test_non_owner_non_admin_forbidden(bob):
    _, user = bob
    member = _user_notification_prefs(user["username"])
    target = _user_notification_prefs("alice_test")
    response = requests.post(
        f"{BASE_URL}/profile/{target['username']}/notifications",
        data={"notification_type": "vote", "channel": "in_app", "value": "false"},
        headers={"Accept": "application/json", "X-API-KEY": member["api_key"]},
        allow_redirects=False,
    )
    assert response.status_code == 403


def test_admin_edits_other_user(alice):
    _, admin_user = alice
    admin = _user_notification_prefs(admin_user["username"])
    target = _user_notification_prefs("bob_test")
    reset_notification_prefs(target["uid"])
    try:
        response = requests.post(
            f"{BASE_URL}/profile/{target['username']}/notifications",
            data={"notification_type": "comment", "channel": "push", "value": "false"},
            headers={"Accept": "application/json", "X-API-KEY": admin["api_key"]},
            allow_redirects=False,
        )
        assert response.status_code == 200
        assert notification_enabled(target["uid"], "comment", "push") is False
    finally:
        reset_notification_prefs(target["uid"])


def _await_notification_enabled(uid, notification_type, channel, expected):
    import time as _time

    deadline = _time.time() + 5.0
    current = notification_enabled(uid, notification_type, channel)
    while current != expected and _time.time() < deadline:
        _time.sleep(0.2)
        current = notification_enabled(uid, notification_type, channel)
    return current


def test_owner_reset_via_http_clears_custom_prefs(bob):
    _, user = bob
    target = _user_notification_prefs(user["username"])
    set_notification_pref(target["uid"], "mention", "push", False)
    try:
        assert _await_notification_enabled(target["uid"], "mention", "push", False) is False

        from devplacepy.database import get_table, refresh_snapshot

        audit_table = get_table("audit_log")
        before = audit_table.count(
            event_key="profile.notification.reset", target_uid=target["uid"]
        )

        response = requests.post(
            f"{BASE_URL}/profile/{target['username']}/notifications/reset",
            headers={"Accept": "application/json", "X-API-KEY": target["api_key"]},
            allow_redirects=False,
        )
        assert response.status_code == 200
        body = response.json()
        assert body["ok"] is True
        assert body["redirect"] == f"/profile/{target['username']}?tab=notifications"

        assert _await_notification_enabled(target["uid"], "mention", "push", True) is True

        refresh_snapshot()
        after = audit_table.count(
            event_key="profile.notification.reset", target_uid=target["uid"]
        )
        assert after == before + 1
    finally:
        reset_notification_prefs(target["uid"])


def test_non_owner_non_admin_reset_forbidden(bob):
    _, user = bob
    member = _user_notification_prefs(user["username"])
    target = _user_notification_prefs("alice_test")
    response = requests.post(
        f"{BASE_URL}/profile/{target['username']}/notifications/reset",
        headers={"Accept": "application/json", "X-API-KEY": member["api_key"]},
        allow_redirects=False,
    )
    assert response.status_code == 403


def test_profile_search_returns_results(alice):
    page, _ = alice
    resp = page.request.get(f"{BASE_URL}/profile/search?q=bob")
    assert resp.status == 200
    data = resp.json()
    assert any("bob" in u["username"] for u in data["results"])


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


def test_profile_stats(alice):
    page, user = alice
    page.goto(f"{BASE_URL}/profile/{user['username']}", wait_until="domcontentloaded")
    assert page.is_visible("text=Posts")
    assert page.is_visible("text=Level")
    assert page.is_visible("text=Stars")


def test_profile_level_bar(alice):
    page, user = alice
    page.goto(f"{BASE_URL}/profile/{user['username']}", wait_until="domcontentloaded")
    assert page.is_visible("text=Progress to next level")
    level_bar = page.locator(".profile-level-bar .bar")
    assert level_bar.is_visible()


def test_profile_badges(alice):
    page, user = alice
    page.goto(f"{BASE_URL}/profile/{user['username']}", wait_until="domcontentloaded")
    badges = page.locator(".profile-badge")
    assert badges.count() >= 1


def test_profile_tabs(alice):
    page, user = alice
    page.goto(f"{BASE_URL}/profile/{user['username']}", wait_until="domcontentloaded")
    tabs = ["Posts", "Projects", "Activity"]
    for tab in tabs:
        assert page.is_visible(f"a:has-text('{tab}')")


def test_profile_posts_tab(alice):
    page, user = alice
    page.goto(
        f"{BASE_URL}/profile/{user['username']}?tab=posts",
        wait_until="domcontentloaded",
    )
    assert page.locator(".profile-tab.active:has-text('Posts')").is_visible()
    assert (
        page.locator(".profile-posts .post-card").count() > 0
        or page.locator(".profile-posts .empty-state:has-text('No posts yet.')").count()
        > 0
    )


def test_profile_projects_tab_content(alice):
    page, user = alice
    page.goto(
        f"{BASE_URL}/profile/{user['username']}?tab=projects",
        wait_until="domcontentloaded",
    )
    assert page.locator(".profile-tab.active:has-text('Projects')").is_visible()
    assert (
        page.locator(".profile-posts .project-card").count() > 0
        or page.locator(
            ".profile-posts .empty-state:has-text('No projects yet.')"
        ).count()
        > 0
    )


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


def test_profile_back_link(alice):
    page, user = alice
    page.goto(f"{BASE_URL}/profile/{user['username']}", wait_until="domcontentloaded")
    back = page.locator("a:has-text('Back')").first
    assert back.is_visible()
    back.click()
    page.wait_for_url(f"{BASE_URL}/feed", wait_until="domcontentloaded")


def test_profile_level_display(alice):
    page, user = alice
    page.goto(f"{BASE_URL}/profile/{user['username']}", wait_until="domcontentloaded")
    assert page.locator(".profile-level-bar").is_visible()


def test_profile_avatar(alice):
    page, user = alice
    page.goto(f"{BASE_URL}/profile/{user['username']}", wait_until="domcontentloaded")
    avatar = page.locator(".avatar-lg")
    assert avatar.is_visible()


def test_profile_edit_bio(alice):
    page, user = alice
    page.goto(f"{BASE_URL}/profile/{user['username']}", wait_until="domcontentloaded")
    page.evaluate("""
        document.querySelector('textarea[name="bio"]').value = 'Test bio for testing';
        document.querySelector('form[action="/profile/update"]').submit();
    """)
    page.wait_for_url(
        f"{BASE_URL}/profile/{user['username']}", wait_until="domcontentloaded"
    )
    page.locator("text=Test bio for testing").first.wait_for(state="visible")


def test_profile_edit_location(alice):
    page, user = alice
    page.goto(f"{BASE_URL}/profile/{user['username']}", wait_until="domcontentloaded")
    page.evaluate("""
        document.querySelector('input[name="location"]').value = 'Amsterdam';
        document.querySelector('form[action="/profile/update"]').submit();
    """)
    page.wait_for_url(
        f"{BASE_URL}/profile/{user['username']}", wait_until="domcontentloaded"
    )
    page.locator("text=Amsterdam").first.wait_for(state="visible")


def test_profile_edit_all_fields(alice):
    page, user = alice
    page.goto(f"{BASE_URL}/profile/{user['username']}", wait_until="domcontentloaded")
    page.evaluate("""
        document.querySelector('textarea[name="bio"]').value = 'Full stack dev';
        document.querySelector('input[name="location"]').value = 'London, UK';
        document.querySelector('input[name="git_link"]').value = 'https://git.example.com/alice';
        document.querySelector('input[name="website"]').value = 'https://alice.example.com';
        document.querySelector('form[action="/profile/update"]').submit();
    """)
    page.wait_for_url(
        f"{BASE_URL}/profile/{user['username']}", wait_until="domcontentloaded"
    )
    page.locator("text=Full stack dev").first.wait_for(state="visible")
    assert page.is_visible("text=London, UK")
    assert page.is_visible("text=https://git.example.com/alice")
    assert page.is_visible("text=https://alice.example.com")


def test_profile_message_button_self(alice):
    page, user = alice
    page.goto(f"{BASE_URL}/profile/{user['username']}", wait_until="domcontentloaded")
    message_btn = page.locator("a:has-text('Send Message')")
    assert not message_btn.is_visible()


def test_profile_activity_tab(alice):
    page, user = alice
    page.goto(
        f"{BASE_URL}/profile/{user['username']}?tab=activity",
        wait_until="domcontentloaded",
    )
    assert page.locator(".profile-tab.active:has-text('Activity')").is_visible()
    assert (
        page.locator(".profile-posts .activity-card").count() > 0
        or page.locator(
            ".profile-posts .empty-state:has-text('No activity yet.')"
        ).count()
        > 0
    )


def test_profile_followers_json(alice):
    page, user = alice
    resp = page.request.get(f"{BASE_URL}/profile/{user['username']}/followers")
    assert resp.status == 200
    data = resp.json()
    assert data["username"] == user["username"]
    assert data["mode"] == "followers"
    assert "followers" in data


def test_profile_following_json(alice):
    page, user = alice
    resp = page.request.get(f"{BASE_URL}/profile/{user['username']}/following")
    assert resp.status == 200
    data = resp.json()
    assert data["username"] == user["username"]
    assert data["mode"] == "following"
    assert "following" in data


def test_profile_customization_global_toggle(alice):
    page, user = alice
    page.goto(f"{BASE_URL}/profile/{user['username']}", wait_until="domcontentloaded")
    form = page.locator(f"form[action='/profile/{user['username']}/customization/global']")
    if form.count() > 0:
        button = form.locator("button[type='submit']")
        expect(button).to_have_text("Disable")
        button.click()
        page.locator(".dialog-overlay.visible .dialog-confirm").click()
        expect(button).to_have_text("Enable")
        button.click()
        page.locator(".dialog-overlay.visible .dialog-confirm").click()
        expect(button).to_have_text("Disable")


def test_profile_customization_pagetype_toggle(alice):
    page, user = alice
    page.goto(f"{BASE_URL}/profile/{user['username']}", wait_until="domcontentloaded")
    form = page.locator(f"form[action='/profile/{user['username']}/customization/pagetype']")
    if form.count() > 0:
        button = form.locator("button[type='submit']")
        expect(button).to_have_text("Disable")
        button.click()
        page.locator(".dialog-overlay.visible .dialog-confirm").click()
        expect(button).to_have_text("Enable")
        button.click()
        page.locator(".dialog-overlay.visible .dialog-confirm").click()
        expect(button).to_have_text("Disable")


def test_profile_page_has_structured_data(alice):
    page, user = alice
    page.goto(f"{BASE_URL}/profile/{user['username']}", wait_until="domcontentloaded")
    scripts = page.locator('script[type="application/ld+json"]')
    count = scripts.count()
    assert count >= 1
    text = scripts.first.text_content()
    assert "ProfilePage" in text or "WebSite" in text
