# retoor <retoor@molodetz.nl>

import requests
from tests.conftest import BASE_URL
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_missing_profile_returns_404(app_server):
    r = requests.get(f"{BASE_URL}/profile/no-such-user-xyz", allow_redirects=False)
    assert r.status_code == 404


def _seed_comment_on_post(commenter_uid):
    from datetime import datetime, timezone
    from uuid import uuid4
    from devplacepy.database import get_table

    post_slug, post_uid = _seed_post_seo()
    comment_uid = str(uuid4())
    get_table("comments").insert(
        {
            "deleted_at": None,
            "deleted_by": None,
            "uid": comment_uid,
            "target_type": "post",
            "target_uid": post_uid,
            "post_uid": post_uid,
            "user_uid": commenter_uid,
            "content": "Activity tab link comment body",
            "parent_uid": None,
            "created_at": datetime.now(timezone.utc).isoformat(),
        }
    )
    return post_slug, post_uid, comment_uid


def test_activity_comment_exposes_parent_post_url(app_server):
    from devplacepy.database import get_table, refresh_snapshot

    commenter = _seed_owner()
    username = get_table("users").find_one(uid=commenter)["username"]
    post_slug, _post_uid, comment_uid = _seed_comment_on_post(commenter)
    refresh_snapshot()

    r = requests.get(
        f"{BASE_URL}/profile/{username}?tab=activity",
        headers={"Accept": "application/json"},
    )
    assert r.status_code == 200
    activities = r.json()["activities"]
    comment_acts = [a for a in activities if a.get("type") == "comment"]
    assert comment_acts, "expected a comment activity"
    act = comment_acts[0]
    assert act["url"] == f"/posts/{post_slug}#comment-{comment_uid}"
    assert act["target_type"] == "post"


def test_activity_comment_url_matches_notification_target(app_server):
    from devplacepy.database import get_table, refresh_snapshot, resolve_object_url

    commenter = _seed_owner()
    username = get_table("users").find_one(uid=commenter)["username"]
    _post_slug, post_uid, comment_uid = _seed_comment_on_post(commenter)
    refresh_snapshot()

    expected = f"{resolve_object_url('post', post_uid)}#comment-{comment_uid}"

    r = requests.get(
        f"{BASE_URL}/profile/{username}?tab=activity",
        headers={"Accept": "application/json"},
    )
    act = [a for a in r.json()["activities"] if a.get("type") == "comment"][0]
    assert act["url"] == expected


def test_activity_post_exposes_url(app_server):
    from devplacepy.database import get_table, refresh_snapshot

    owner = _seed_owner()
    username = get_table("users").find_one(uid=owner)["username"]
    post_slug, post_uid = _seed_post_seo()
    get_table("posts").update({"uid": post_uid, "user_uid": owner}, ["uid"])
    refresh_snapshot()

    r = requests.get(
        f"{BASE_URL}/profile/{username}?tab=activity",
        headers={"Accept": "application/json"},
    )
    assert r.status_code == 200
    post_acts = [a for a in r.json()["activities"] if a.get("type") == "post"]
    assert post_acts, "expected a post activity"
    assert post_acts[0]["url"] == f"/posts/{post_slug}"


def test_activity_comment_card_renders_overlay_link(app_server):
    from devplacepy.database import get_table, refresh_snapshot

    commenter = _seed_owner()
    username = get_table("users").find_one(uid=commenter)["username"]
    post_slug, _post_uid, comment_uid = _seed_comment_on_post(commenter)
    refresh_snapshot()

    r = requests.get(f"{BASE_URL}/profile/{username}?tab=activity")
    assert r.status_code == 200
    assert "activity-card card-link-host" in r.text
    assert f'class="card-link" href="/posts/{post_slug}#comment-{comment_uid}"' in r.text


def test_profile_json_exposes_online_presence(app_server):
    import time

    name = f"pres{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,
    )
    r = session.get(
        f"{BASE_URL}/profile/{name}", headers={"Accept": "application/json"}
    )
    assert r.status_code == 200
    body = r.json()
    assert body["profile_online"] is True
    assert body["profile_user"]["last_seen"]


def test_profile_json_offline_when_last_seen_stale(app_server):
    from datetime import datetime, timezone, timedelta
    from devplacepy.config import PRESENCE_TIMEOUT_SECONDS
    from devplacepy.database import get_table, refresh_snapshot

    owner = _seed_owner()
    username = get_table("users").find_one(uid=owner)["username"]
    stale = (
        datetime.now(timezone.utc)
        - timedelta(seconds=PRESENCE_TIMEOUT_SECONDS + 120)
    ).isoformat()
    get_table("users").update({"uid": owner, "last_seen": stale}, ["uid"])
    refresh_snapshot()

    r = requests.get(
        f"{BASE_URL}/profile/{username}", headers={"Accept": "application/json"}
    )
    assert r.status_code == 200
    body = r.json()
    assert body["profile_online"] is False
    assert body["profile_user"]["last_seen"] == stale


def test_profile_hero_avatar_has_presence_dot(app_server):
    import time
    from devplacepy.database import get_table, refresh_snapshot

    name = f"phero{int(time.time() * 1000)}"
    s = requests.Session()
    s.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,
    )
    refresh_snapshot()
    uid = get_table("users").find_one(username=name)["uid"]
    html = s.get(f"{BASE_URL}/profile/{name}").text
    assert "profile-avatar-wrap" in html
    assert "presence-dot" in html
    assert f'data-presence-uid="{uid}"' in html


def test_followers_list_avatar_has_presence_dot(app_server):
    import time
    from devplacepy.database import get_table, refresh_snapshot

    stamp = int(time.time() * 1000)
    target = f"folt{stamp}"
    follower = f"folf{stamp}"
    st = requests.Session()
    sf = requests.Session()
    for sess, name in ((st, target), (sf, follower)):
        sess.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,
        )
    sf.post(f"{BASE_URL}/follow/{target}", allow_redirects=True)
    refresh_snapshot()
    follower_uid = get_table("users").find_one(username=follower)["uid"]
    html = st.get(f"{BASE_URL}/profile/{target}?tab=followers").text
    assert "follow-user" in html
    assert f'data-presence-uid="{follower_uid}"' in html
    assert "data-presence-last-seen" in html


def test_viewing_profile_marks_notification_read(app_server):
    import time
    from datetime import datetime, timezone
    from devplacepy.database import get_table, refresh_snapshot
    from devplacepy.utils import generate_uid

    name = f"nmark{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,
    )
    refresh_snapshot()
    user = get_table("users").find_one(username=name)
    notif_uid = generate_uid()
    get_table("notifications").insert(
        {
            "uid": notif_uid,
            "user_uid": user["uid"],
            "type": "follow",
            "message": "someone followed you",
            "related_uid": generate_uid(),
            "target_url": f"/profile/{name}",
            "read": False,
            "created_at": datetime.now(timezone.utc).isoformat(),
        }
    )
    refresh_snapshot()

    r = session.get(f"{BASE_URL}/profile/{name}", allow_redirects=True)
    assert r.status_code == 200

    refresh_snapshot()
    assert bool(get_table("notifications").find_one(uid=notif_uid)["read"]) is True


def test_own_profile_json_exposes_xp_fields(app_server, seeded_db):
    """GET /profile (own) with Accept: application/json includes xp_next_level and xp_progress_pct."""
    import requests

    session = requests.Session()
    session.post(
        f"{BASE_URL}/auth/login",
        data={
            "email": seeded_db["alice"]["email"],
            "password": seeded_db["alice"]["password"],
        },
        allow_redirects=True,
    )
    r = session.get(f"{BASE_URL}/profile", headers={"Accept": "application/json"})
    assert r.status_code == 200
    data = r.json()
    pu = data["profile_user"]
    assert "xp_next_level" in pu, "xp_next_level missing from own profile JSON"
    assert "xp_progress_pct" in pu, "xp_progress_pct missing from own profile JSON"
    assert isinstance(pu["xp_next_level"], int)
    assert isinstance(pu["xp_progress_pct"], int)


def test_other_profile_json_exposes_xp_fields(app_server):
    """GET /profile/{username} with Accept: application/json includes xp_next_level and xp_progress_pct."""
    from devplacepy.database import get_table, refresh_snapshot

    owner = _seed_owner()
    username = get_table("users").find_one(uid=owner)["username"]
    refresh_snapshot()

    r = requests.get(
        f"{BASE_URL}/profile/{username}", headers={"Accept": "application/json"}
    )
    assert r.status_code == 200
    data = r.json()
    pu = data["profile_user"]
    assert "xp_next_level" in pu, "xp_next_level missing from other profile JSON"
    assert "xp_progress_pct" in pu, "xp_progress_pct missing from other profile JSON"
    assert isinstance(pu["xp_next_level"], int)
    assert isinstance(pu["xp_progress_pct"], int)


def test_profile_json_xp_fields_zero_xp(app_server):
    """User with 0 XP returns xp_next_level=100 (level 1) and xp_progress_pct=0."""
    from devplacepy.database import get_table, refresh_snapshot
    from devplacepy.utils.rewards import LEVEL_XP

    owner = _seed_owner()
    username = get_table("users").find_one(uid=owner)["username"]
    refresh_snapshot()

    r = requests.get(
        f"{BASE_URL}/profile/{username}", headers={"Accept": "application/json"}
    )
    assert r.status_code == 200
    pu = r.json()["profile_user"]
    assert pu["xp"] == 0 or pu["xp"] is None, f"expected 0 xp, got {pu['xp']}"
    assert pu["xp_next_level"] == LEVEL_XP, (
        f"expected xp_next_level={LEVEL_XP} for level 1, got {pu['xp_next_level']}"
    )
    assert pu["xp_progress_pct"] == 0, (
        f"expected xp_progress_pct=0 for 0 XP, got {pu['xp_progress_pct']}"
    )


def test_profile_json_xp_fields_boundary_xp(app_server):
    """User with exactly 100 XP (level 2) returns xp_next_level=200 and xp_progress_pct=0."""
    from devplacepy.database import get_table, refresh_snapshot
    from devplacepy.utils.rewards import award_xp

    owner = _seed_owner()
    username = get_table("users").find_one(uid=owner)["username"]
    award_xp(owner, 100)
    refresh_snapshot()

    r = requests.get(
        f"{BASE_URL}/profile/{username}", headers={"Accept": "application/json"}
    )
    assert r.status_code == 200
    pu = r.json()["profile_user"]
    assert pu["xp"] == 100, f"expected 100 xp, got {pu['xp']}"
    assert pu["level"] == 2, f"expected level 2, got {pu['level']}"
    assert pu["xp_next_level"] == 200, (
        f"expected xp_next_level=200 for level 2, got {pu['xp_next_level']}"
    )
    assert pu["xp_progress_pct"] == 0, (
        f"expected xp_progress_pct=0 at boundary, got {pu['xp_progress_pct']}"
    )


def _signup_badge_user():
    import time

    name = f"bdgi{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 test_profile_badges_json_description_matches_catalog(app_server):
    from devplacepy.database import get_table, refresh_snapshot
    from devplacepy.utils import BADGE_CATALOG, award_badge

    session, name = _signup_badge_user()
    refresh_snapshot()
    user = get_table("users").find_one(username=name)
    assert user, f"user {name} not found after signup"
    award_badge(user["uid"], "First Post")
    award_badge(user["uid"], "Member")
    refresh_snapshot()

    r = session.get(
        f"{BASE_URL}/profile/{name}", headers={"Accept": "application/json"}
    )
    assert r.status_code == 200
    body = r.json()
    assert "badges" in body, "badges key missing from profile JSON"
    assert isinstance(body["badges"], list), "badges is not a list"
    assert body["badges"], "expected at least one badge"
    for badge in body["badges"]:
        assert "description" in badge, f"badge missing description key: {badge}"
        assert badge["description"] is not None, f"badge description is null: {badge}"
        assert isinstance(badge["description"], str), (
            f"badge description is not a string: {badge}"
        )
        assert badge["description"] == BADGE_CATALOG[badge["name"]]["description"], (
            f"badge description mismatch: {badge['name']}"
        )


def test_profile_badge_description_exact_string(app_server):
    from devplacepy.database import get_table, refresh_snapshot
    from devplacepy.utils import award_badge

    session, name = _signup_badge_user()
    refresh_snapshot()
    user = get_table("users").find_one(username=name)
    assert user, f"user {name} not found after signup"
    award_badge(user["uid"], "Cheerleader")
    refresh_snapshot()

    r = session.get(
        f"{BASE_URL}/profile/{name}", headers={"Accept": "application/json"}
    )
    assert r.status_code == 200
    badges = r.json()["badges"]
    cheerleader = [b for b in badges if b["name"] == "Cheerleader"]
    assert cheerleader, f"Cheerleader badge missing from profile JSON: {badges}"
    assert cheerleader[0]["description"] == "Reacted 50 times", (
        f"expected 'Reacted 50 times', got {cheerleader[0]['description']!r}"
    )


PRIVACY_FIELDS = (
    "age_band",
    "terms_version",
    "terms_accepted_at",
    "suspended_until",
    "suspension_reason",
)


def _privacy_member(prefix="priv"):
    import time

    name = f"{prefix}{int(time.time() * 1000000)}"
    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 _admin_session():
    session = requests.Session()
    session.post(
        f"{BASE_URL}/auth/login",
        data={"email": "alice@test.devplace", "password": "secret123"},
        allow_redirects=True,
    )
    return session


def test_the_owner_reads_their_own_privacy_state(app_server):
    session, name = _privacy_member()
    body = session.get(
        f"{BASE_URL}/profile/{name}?tab=privacy",
        headers={"Accept": "application/json"},
    ).json()
    assert body["age_band"] == "adult"
    assert body["terms_version"]
    assert body["terms_accepted_at"]
    assert body["consents"]


def test_privacy_state_is_withheld_from_a_stranger(app_server):
    _, name = _privacy_member()
    stranger, _ = _privacy_member("prvs")
    body = stranger.get(
        f"{BASE_URL}/profile/{name}?tab=privacy",
        headers={"Accept": "application/json"},
    ).json()
    for field in PRIVACY_FIELDS:
        assert body[field] in ("", None), f"{field} leaked to a stranger: {body[field]!r}"
    assert body["mature_opt_in"] is False
    assert body["consents"] == []


def test_privacy_state_is_withheld_from_a_guest(app_server):
    _, name = _privacy_member()
    body = requests.get(
        f"{BASE_URL}/profile/{name}?tab=privacy",
        headers={"Accept": "application/json"},
    ).json()
    for field in PRIVACY_FIELDS:
        assert body[field] in ("", None), f"{field} leaked to a guest: {body[field]!r}"
    assert body["consents"] == []


def test_a_strangers_profile_html_never_renders_the_privacy_panel(app_server):
    _, name = _privacy_member()
    stranger, _ = _privacy_member("prvh")
    html = stranger.get(f"{BASE_URL}/profile/{name}?tab=privacy").text
    assert "privacy-panel" not in html
    assert f"/profile/{name}/consent" not in html
    assert f"/profile/{name}/delete" not in html


def test_an_admin_reads_the_state_but_gets_no_controls(app_server, seeded_db):
    _, name = _privacy_member()
    admin = _admin_session()
    body = admin.get(
        f"{BASE_URL}/profile/{name}?tab=privacy",
        headers={"Accept": "application/json"},
    ).json()
    assert body["age_band"] == "adult"
    assert body["consents"]

    html = admin.get(f"{BASE_URL}/profile/{name}?tab=privacy").text
    assert "privacy-panel" in html
    assert "Only the account holder can change this." in html
    assert "Only the account holder can change this preference." in html
    assert f'action="/profile/{name}/consent"' not in html
    assert f'action="/profile/{name}/mature-content"' not in html
    assert f"/profile/{name}/delete" not in html


def test_the_owner_gets_the_privacy_controls(app_server):
    session, name = _privacy_member()
    html = session.get(f"{BASE_URL}/profile/{name}?tab=privacy").text
    assert f'action="/profile/{name}/consent"' in html
    assert f'action="/profile/{name}/mature-content"' in html
    assert f"/profile/{name}/delete" in html
    assert "Only the account holder can change this." not in html



