# retoor <retoor@molodetz.nl>

import time
from datetime import datetime, timezone
import pytest
import requests
from tests.conftest import BASE_URL
from devplacepy.database import db, get_table, refresh_snapshot, set_setting
from devplacepy.utils import generate_uid, make_combined_slug
JSON_audit_log = {"Accept": "application/json"}
_counter_audit_log = [0]
@pytest.fixture(scope="module", autouse=True)
def _audit_test_settings(app_server):
    # On a fresh DB init_db skips seeding the operational/upload settings (its
    # `tables` snapshot predates site_settings creation), so those rows are
    # absent and the admin settings form would INSERT them as "" - which both
    # closes registration and makes consumers that do int("") crash. Seed sane
    # values here so the form's empty submissions are skipped (existing key), and
    # lift the per-IP rate limit since this file fires many mutating requests.
    for key, value in {
        "rate_limit_per_minute": "1000000",
        "rate_limit_window_seconds": "60",
        "registration_open": "1",
        "maintenance_mode": "0",
        "max_upload_size_mb": "10",
        "allowed_file_types": "",
        "max_attachments_per_resource": "10",
        "session_max_age_days": "7",
        "session_remember_days": "30",
        "news_service_interval": "3600",
        "news_grade_threshold": "7",
    }.items():
        set_setting(key, value)
    yield
def _db_user(name):
    # the user is created by the server subprocess; refresh the test-process
    # SQLite snapshot before reading it back across the process boundary.
    refresh_snapshot()
    return get_table("users").find_one(username=name)
def _unique(prefix="au"):
    _counter_audit_log[0] += 1
    return f"{prefix}{int(time.time() * 1000)}{_counter_audit_log[0]}"
def _member():
    name = _unique("aumem")
    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,
    )
    return s, name
def _member_key():
    _, name = _member()
    return _db_user(name)["api_key"]
def _admin(seeded_db):
    # authenticate via the seeded admin's API key (header auth) rather than a
    # login POST - GET reads are exempt from the rate limiter, so reusing this
    # across the file's many tests never counts against the per-IP write budget.
    key = _db_user("alice_test")["api_key"]
    s = requests.Session()
    s.headers.update({"X-API-KEY": key})
    return s
def _audit(admin, **params):
    r = admin.get(f"{BASE_URL}/admin/audit-log", headers=JSON_audit_log, params=params)
    assert r.status_code == 200, r.text[:300]
    return r.json()
def _find(admin, event_key, predicate):
    data = _audit(admin, event_key=event_key)
    for entry in data["entries"]:
        if predicate(entry):
            return entry
    return None
def _new_post(session, body="audited post body here"):
    return session.post(
        f"{BASE_URL}/posts/create",
        headers=JSON_audit_log,
        data={"title": _unique("aup"), "content": body, "topic": "devlog"},
    ).json()["data"]
def _new_project(session):
    return session.post(
        f"{BASE_URL}/projects/create",
        headers=JSON_audit_log,
        data={
            "title": _unique("aupr"),
            "description": "audited project description text",
            "project_type": "software",
            "status": "In Development",
            "platforms": "",
        },
    ).json()["data"]
def _seed_news_audit_log():
    uid = generate_uid()
    title = _unique("aunews")
    get_table("news").insert(
        {
            "deleted_at": None,
            "deleted_by": None,
            "uid": uid,
            "slug": make_combined_slug(title, uid),
            "title": title,
            "external_id": uid,
            "status": "draft",
            "featured": 0,
            "show_on_landing": 0,
            "grade": 5,
            "source_name": "AuditTest",
            "synced_at": datetime.now(timezone.utc).isoformat(),
            "description": "audited news article",
        }
    )
    refresh_snapshot()
    return uid
from devplacepy.database import get_table
AJAX_comments = {"X-Requested-With": "fetch"}
_counter_comments = [0]
def _session_comments():
    _counter_comments[0] += 1
    name = f"cmt{int(time.time() * 1000)}{_counter_comments[0]}"
    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,
    )
    return s, name
def _create_post_comments(session, title):
    r = session.post(
        f"{BASE_URL}/posts/create",
        data={
            "content": "Post for comment test.",
            "title": title,
            "topic": "devlog",
        },
        allow_redirects=False,
    )
    slug = r.headers["location"].split("/posts/")[-1]
    return get_table("posts").find_one(slug=slug)["uid"]
import io
import re
import uuid
from PIL import Image
def _user_uploads(prefix="up"):
    s = requests.Session()
    name = f"{prefix}_{uuid.uuid4().hex[:10]}"
    s.post(
        f"{BASE_URL}/auth/signup",
        data={
            "username": name,
            "email": f"{name}@test.dev",
            "password": "secret123",
            "confirm_password": "secret123",
            "birth_date": "1990-01-01",
            "accept_terms": "1",
        },
        allow_redirects=True,
    )
    return s, name
def _session_uploads():
    s, _ = _user_uploads()
    return s
def _png_bytes_uploads():
    buf = io.BytesIO()
    Image.new("RGB", (4, 4), (255, 0, 0)).save(buf, "PNG")
    return buf.getvalue()
def _upload_uploads(s, name="a.png"):
    r = s.post(
        f"{BASE_URL}/uploads/upload", files={"file": (name, _png_bytes_uploads(), "image/png")}
    )
    assert r.status_code == 201, r.text
    return r.json()["uid"]


def test_comment_create_and_delete_recorded(seeded_db):
    s, _ = _member()
    post = _new_post(s)
    comment = s.post(
        f"{BASE_URL}/comments/create",
        headers=JSON_audit_log,
        data={"content": "audited comment body", "target_type": "post", "post_uid": post["uid"]},
    ).json()["data"]
    admin = _admin(seeded_db)
    assert _find(admin, "comment.create.post", lambda e: e.get("target_uid") == comment["uid"]) is not None
    s.post(f"{BASE_URL}/comments/delete/{comment['uid']}", headers=JSON_audit_log, allow_redirects=False)
    assert _find(admin, "comment.delete", lambda e: e.get("target_uid") == comment["uid"]) is not None


def test_create_comment_on_post(app_server):
    s, _ = _session_comments()
    title = f"comment-test-{int(time.time() * 1000)}"
    post_uid = _create_post_comments(s, title)
    r = s.post(
        f"{BASE_URL}/comments/create",
        data={
            "content": "Nice post!",
            "target_type": "post",
            "post_uid": post_uid,
            "target_uid": post_uid,
        },
        allow_redirects=False,
    )
    assert r.status_code in (302, 303)
    comment = get_table("comments").find_one(target_uid=post_uid)
    assert comment is not None
    assert comment["content"] == "Nice post!"


def test_create_reply_to_comment(app_server):
    s, _ = _session_comments()
    title = f"reply-test-{int(time.time() * 1000)}"
    post_uid = _create_post_comments(s, title)
    s.post(
        f"{BASE_URL}/comments/create",
        data={
            "content": "Parent comment",
            "target_type": "post",
            "post_uid": post_uid,
            "target_uid": post_uid,
        },
        allow_redirects=False,
    )
    refresh_snapshot()
    parent = get_table("comments").find_one(target_uid=post_uid)
    r = s.post(
        f"{BASE_URL}/comments/create",
        data={
            "content": "Reply to parent",
            "target_type": "post",
            "post_uid": post_uid,
            "target_uid": post_uid,
            "parent_uid": parent["uid"],
        },
        allow_redirects=False,
    )
    assert r.status_code in (302, 303)
    refresh_snapshot()
    replies = list(
        db.query(
            "SELECT content FROM comments WHERE parent_uid = :p AND deleted_at IS NULL",
            p=parent["uid"],
        )
    )
    assert len(replies) == 1
    assert replies[0]["content"] == "Reply to parent"


def test_delete_own_comment(app_server):
    s, _ = _session_comments()
    title = f"del-cmt-{int(time.time() * 1000)}"
    post_uid = _create_post_comments(s, title)
    s.post(
        f"{BASE_URL}/comments/create",
        data={
            "content": "Delete me",
            "target_type": "post",
            "post_uid": post_uid,
            "target_uid": post_uid,
        },
        allow_redirects=False,
    )
    comment = get_table("comments").find_one(target_uid=post_uid)
    r = s.post(
        f"{BASE_URL}/comments/delete/{comment['uid']}",
        allow_redirects=False,
    )
    assert r.status_code in (302, 303)
    assert get_table("comments").find_one(uid=comment["uid"], deleted_at=None) is None


def test_delete_comment_requires_owner(app_server):
    s_alice, _ = _session_comments()
    title = f"owner-test-{int(time.time() * 1000)}"
    post_uid = _create_post_comments(s_alice, title)
    s_alice.post(
        f"{BASE_URL}/comments/create",
        data={
            "content": "Alice comment",
            "target_type": "post",
            "post_uid": post_uid,
            "target_uid": post_uid,
        },
        allow_redirects=False,
    )
    comment = get_table("comments").find_one(target_uid=post_uid)
    s_bob, _ = _session_comments()
    r = s_bob.post(
        f"{BASE_URL}/comments/delete/{comment['uid']}",
        allow_redirects=False,
    )
    assert r.status_code in (302, 303)
    assert get_table("comments").find_one(uid=comment["uid"]) is not None


def test_create_comment_requires_login(app_server):
    title = f"anon-cmt-{int(time.time() * 1000)}"
    s, name = _session_comments()
    post_uid = _create_post_comments(s, title)
    anon = requests.Session()
    r = anon.post(
        f"{BASE_URL}/comments/create",
        data={
            "content": "Anon comment",
            "target_type": "post",
            "post_uid": post_uid,
            "target_uid": post_uid,
        },
        allow_redirects=False,
    )
    assert r.status_code == 303
    assert get_table("comments").find_one(target_uid=post_uid) is None


def test_comment_links_multiple_attachments(app_server):
    s = _session_uploads()
    post = s.post(
        f"{BASE_URL}/posts/create",
        data={
            "content": "Host post for comment attachments",
            "title": "host",
            "topic": "random",
        },
        allow_redirects=True,
    )
    target_uid = re.search(r'name="target_uid"\s+value="([^"]+)"', post.text).group(1)

    u1, u2 = _upload_uploads(s), _upload_uploads(s)
    r = s.post(
        f"{BASE_URL}/comments/create",
        data={
            "content": "Comment with attachments",
            "target_uid": target_uid,
            "target_type": "post",
            "attachment_uids": f"{u1},{u2}",
        },
        allow_redirects=True,
    )
    assert r.status_code == 200, r.text[:300]
    assert u1 in r.text and u2 in r.text, (
        "both attachments must be linked and displayed on the comment"
    )


def test_thread_notifies_other_commenters_disregarding_hierarchy(app_server):
    owner, _ = _session_comments()
    title = f"thread-test-{int(time.time() * 1000)}"
    post_uid = _create_post_comments(owner, title)

    first, first_name = _session_comments()
    first.post(
        f"{BASE_URL}/comments/create",
        data={
            "content": "First commenter",
            "target_type": "post",
            "post_uid": post_uid,
            "target_uid": post_uid,
        },
        allow_redirects=False,
    )
    refresh_snapshot()
    first_uid = _db_user(first_name)["uid"]

    second, _ = _session_comments()
    second.post(
        f"{BASE_URL}/comments/create",
        data={
            "content": "Second commenter, unrelated to the first's comment",
            "target_type": "post",
            "post_uid": post_uid,
            "target_uid": post_uid,
        },
        allow_redirects=False,
    )
    refresh_snapshot()
    assert get_table("notifications").count(user_uid=first_uid, type="thread") == 1


def test_thread_does_not_notify_self_when_commenting_again(app_server):
    owner, _ = _session_comments()
    title = f"thread-self-{int(time.time() * 1000)}"
    post_uid = _create_post_comments(owner, title)

    commenter, commenter_name = _session_comments()
    commenter.post(
        f"{BASE_URL}/comments/create",
        data={
            "content": "First comment from this user",
            "target_type": "post",
            "post_uid": post_uid,
            "target_uid": post_uid,
        },
        allow_redirects=False,
    )
    refresh_snapshot()
    commenter_uid = _db_user(commenter_name)["uid"]

    commenter.post(
        f"{BASE_URL}/comments/create",
        data={
            "content": "Same user comments again on the same post",
            "target_type": "post",
            "post_uid": post_uid,
            "target_uid": post_uid,
        },
        allow_redirects=False,
    )
    refresh_snapshot()
    assert get_table("notifications").count(user_uid=commenter_uid, type="thread") == 0


def test_thread_notification_excludes_actor_and_already_notified_participants(app_server):
    owner, owner_name = _session_comments()
    title = f"thread-excl-{int(time.time() * 1000)}"
    post_uid = _create_post_comments(owner, title)
    refresh_snapshot()
    owner_uid = _db_user(owner_name)["uid"]

    owner.post(
        f"{BASE_URL}/comments/create",
        data={
            "content": "Owner's own top-level comment",
            "target_type": "post",
            "post_uid": post_uid,
            "target_uid": post_uid,
        },
        allow_redirects=False,
    )
    refresh_snapshot()
    assert get_table("notifications").count(user_uid=owner_uid, type="thread") == 0
    parent = get_table("comments").find_one(target_uid=post_uid, user_uid=owner_uid)

    replier, replier_name = _session_comments()
    replier.post(
        f"{BASE_URL}/comments/create",
        data={
            "content": "Reply to the post owner's comment",
            "target_type": "post",
            "post_uid": post_uid,
            "target_uid": post_uid,
            "parent_uid": parent["uid"],
        },
        allow_redirects=False,
    )
    refresh_snapshot()
    assert get_table("notifications").count(user_uid=owner_uid, type="reply") == 1
    assert get_table("notifications").count(user_uid=owner_uid, type="thread") == 0

    third, _ = _session_comments()
    third.post(
        f"{BASE_URL}/comments/create",
        data={
            "content": "Another top-level comment",
            "target_type": "post",
            "post_uid": post_uid,
            "target_uid": post_uid,
        },
        allow_redirects=False,
    )
    refresh_snapshot()
    assert get_table("notifications").count(user_uid=owner_uid, type="comment") == 1
    assert get_table("notifications").count(user_uid=owner_uid, type="thread") == 0
    replier_uid = _db_user(replier_name)["uid"]
    assert get_table("notifications").count(user_uid=replier_uid, type="thread") == 1


def test_post_page_embeds_comment_as_nested_schema(app_server):
    s, _ = _session_comments()
    title = f"schema-comment-{int(time.time() * 1000)}"
    post_uid = _create_post_comments(s, title)
    slug = get_table("posts").find_one(uid=post_uid)["slug"]
    unique_comment_text = f"a schema-visible comment {int(time.time() * 1000)}"
    s.post(
        f"{BASE_URL}/comments/create",
        data={
            "content": unique_comment_text,
            "target_type": "post",
            "post_uid": post_uid,
            "target_uid": post_uid,
        },
        allow_redirects=False,
    )
    r = requests.get(f"{BASE_URL}/posts/{slug}", allow_redirects=False)
    assert r.status_code == 200
    assert '"@type": "Comment"' in r.text
    assert unique_comment_text in r.text
