# 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 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
import sqlite3
from devplacepy.config import DATABASE_URL
from devplacepy.database import get_table
from devplacepy.utils import generate_uid
from devplacepy.constants import REACTION_EMOJI
_counter_reactions = [0]
AJAX_reactions = {"X-Requested-With": "fetch"}
ROCKET = REACTION_EMOJI[2]
HEART = REACTION_EMOJI[1]
UNICORN = "\U0001F984"
SKIN_TONE = "\U0001F44D\U0001F3FF"
ZWJ_SEQUENCE = "\U0001F9D1\u200d\U0001F4BB"
FLAG = "\U0001F1F3\U0001F1F1"
def _session_reactions():
    _counter_reactions[0] += 1
    name = f"rxn{int(time.time() * 1000)}{_counter_reactions[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 _uid_reactions(username):
    return get_table("users").find_one(username=username)["uid"]
def _live_query(sql, params):
    conn = sqlite3.connect(DATABASE_URL.replace("sqlite:///", "", 1))
    try:
        return conn.execute(sql, params).fetchone()
    finally:
        conn.close()
def _live_reaction_count(target_type, target_uid):
    return _live_query(
        "SELECT COUNT(*) FROM reactions "
        "WHERE target_type=? AND target_uid=? AND deleted_at IS NULL",
        (target_type, target_uid),
    )[0]
def _live_post_uid(slug):
    row = _live_query("SELECT uid FROM posts WHERE slug=?", (slug,))
    return row[0] if row else None
def _make_post_reactions(owner_uid):
    uid = generate_uid()
    get_table("posts").insert(
        {
            "deleted_at": None,
            "deleted_by": None,
            "uid": uid,
            "user_uid": owner_uid,
            "slug": f"{uid[:8]}-reaction-post",
            "title": None,
            "content": "reaction target content",
            "topic": "random",
            "project_uid": None,
            "image": None,
            "stars": 0,
            "created_at": datetime.now(timezone.utc).isoformat(),
        }
    )
    return uid
def _make_comment(post_uid, owner_uid):
    uid = generate_uid()
    get_table("comments").insert(
        {
            "deleted_at": None,
            "deleted_by": None,
            "uid": uid,
            "target_type": "post",
            "target_uid": post_uid,
            "post_uid": post_uid,
            "user_uid": owner_uid,
            "content": "reaction target comment",
            "parent_uid": None,
            "created_at": datetime.now(timezone.utc).isoformat(),
        }
    )
    return uid


def test_reaction_recorded(seeded_db):
    s, _ = _member()
    post = _new_post(s)
    s.post(f"{BASE_URL}/reactions/post/{post['uid']}", data={"emoji": "\U0001F44D"}, allow_redirects=False)
    admin = _admin(seeded_db)
    event = _find(admin, "reaction.add", lambda e: e.get("target_uid") == post["uid"])
    assert event is not None
    assert (event.get("metadata") or {}).get("emoji") == "\U0001F44D"


def test_add_reaction_returns_counts(app_server):
    s, name = _session_reactions()
    post_uid = _make_post_reactions(_uid_reactions(name))
    r = s.post(
        f"{BASE_URL}/reactions/post/{post_uid}", data={"emoji": ROCKET}, headers=AJAX_reactions
    )
    payload = r.json()
    assert payload["counts"][ROCKET] == 1
    assert ROCKET in payload["mine"]
    assert get_table("reactions").count(target_type="post", target_uid=post_uid) == 1


def test_same_emoji_toggles_off(app_server):
    s, name = _session_reactions()
    post_uid = _make_post_reactions(_uid_reactions(name))
    s.post(
        f"{BASE_URL}/reactions/post/{post_uid}", data={"emoji": ROCKET}, headers=AJAX_reactions
    )
    r = s.post(
        f"{BASE_URL}/reactions/post/{post_uid}", data={"emoji": ROCKET}, headers=AJAX_reactions
    )
    payload = r.json()
    assert payload["counts"].get(ROCKET, 0) == 0
    assert payload["mine"] == []
    assert (
        get_table("reactions").count(
            target_type="post", target_uid=post_uid, deleted_at=None
        )
        == 0
    )


def test_two_distinct_emoji_both_counted(app_server):
    s, name = _session_reactions()
    post_uid = _make_post_reactions(_uid_reactions(name))
    s.post(
        f"{BASE_URL}/reactions/post/{post_uid}", data={"emoji": ROCKET}, headers=AJAX_reactions
    )
    r = s.post(
        f"{BASE_URL}/reactions/post/{post_uid}", data={"emoji": HEART}, headers=AJAX_reactions
    )
    payload = r.json()
    assert payload["counts"][ROCKET] == 1
    assert payload["counts"][HEART] == 1
    assert set(payload["mine"]) == {ROCKET, HEART}


def test_emoji_outside_palette_accepted(app_server):
    s, name = _session_reactions()
    post_uid = _make_post_reactions(_uid_reactions(name))
    r = s.post(
        f"{BASE_URL}/reactions/post/{post_uid}",
        data={"emoji": UNICORN},
        headers=AJAX_reactions,
    )
    payload = r.json()
    assert payload["counts"][UNICORN] == 1
    assert payload["mine"] == [UNICORN]
    assert _live_reaction_count("post", post_uid) == 1


def test_composed_emoji_accepted(app_server):
    s, name = _session_reactions()
    post_uid = _make_post_reactions(_uid_reactions(name))
    for value in (SKIN_TONE, ZWJ_SEQUENCE, FLAG):
        r = s.post(
            f"{BASE_URL}/reactions/post/{post_uid}",
            data={"emoji": value},
            headers=AJAX_reactions,
        )
        assert r.json()["counts"][value] == 1
    assert _live_reaction_count("post", post_uid) == 3


def test_off_palette_reaction_toggles_off(app_server):
    s, name = _session_reactions()
    post_uid = _make_post_reactions(_uid_reactions(name))
    for _ in range(2):
        r = s.post(
            f"{BASE_URL}/reactions/post/{post_uid}",
            data={"emoji": UNICORN},
            headers=AJAX_reactions,
        )
    payload = r.json()
    assert payload["counts"].get(UNICORN, 0) == 0
    assert payload["mine"] == []
    assert _live_reaction_count("post", post_uid) == 0


def test_non_emoji_rejected(app_server):
    s, name = _session_reactions()
    post_uid = _make_post_reactions(_uid_reactions(name))
    for value in ("notanemoji", "<script>alert(1)</script>", " ", f"{UNICORN} text"):
        r = s.post(
            f"{BASE_URL}/reactions/post/{post_uid}",
            data={"emoji": value},
            headers=AJAX_reactions,
            allow_redirects=False,
        )
        assert r.status_code == 303, value
    assert get_table("reactions").count(target_type="post", target_uid=post_uid) == 0


def test_multiple_emoji_in_one_reaction_rejected(app_server):
    s, name = _session_reactions()
    post_uid = _make_post_reactions(_uid_reactions(name))
    r = s.post(
        f"{BASE_URL}/reactions/post/{post_uid}",
        data={"emoji": UNICORN + ROCKET},
        headers=AJAX_reactions,
        allow_redirects=False,
    )
    assert r.status_code == 303
    assert get_table("reactions").count(target_type="post", target_uid=post_uid) == 0


def test_invalid_target_type_returns_400(app_server):
    s, name = _session_reactions()
    post_uid = _make_post_reactions(_uid_reactions(name))
    r = s.post(
        f"{BASE_URL}/reactions/news/{post_uid}", data={"emoji": ROCKET}, headers=AJAX_reactions
    )
    assert r.status_code == 400


def test_reaction_requires_login(app_server):
    owner_s, owner_name = _session_reactions()
    post_uid = _make_post_reactions(_uid_reactions(owner_name))
    anon = requests.Session()
    r = anon.post(
        f"{BASE_URL}/reactions/post/{post_uid}",
        data={"emoji": ROCKET},
        headers=AJAX_reactions,
        allow_redirects=False,
    )
    assert r.status_code == 303
    assert get_table("reactions").count(target_type="post", target_uid=post_uid) == 0


def test_react_on_comment_target(app_server):
    s, name = _session_reactions()
    owner_uid = _uid_reactions(name)
    post_uid = _make_post_reactions(owner_uid)
    comment_uid = _make_comment(post_uid, owner_uid)
    r = s.post(
        f"{BASE_URL}/reactions/comment/{comment_uid}",
        data={"emoji": HEART},
        headers=AJAX_reactions,
    )
    assert r.json()["counts"][HEART] == 1
    assert (
        get_table("reactions").count(target_type="comment", target_uid=comment_uid) == 1
    )


def test_react_on_gist_target(app_server):
    s, name = _session_reactions()
    gist_uid = generate_uid()
    r = s.post(
        f"{BASE_URL}/reactions/gist/{gist_uid}",
        data={"emoji": HEART},
        headers=AJAX_reactions,
    )
    assert r.json()["counts"][HEART] == 1
    assert get_table("reactions").count(target_type="gist", target_uid=gist_uid) == 1


def test_react_on_project_target(app_server):
    s, name = _session_reactions()
    project_uid = generate_uid()
    r = s.post(
        f"{BASE_URL}/reactions/project/{project_uid}",
        data={"emoji": ROCKET},
        headers=AJAX_reactions,
    )
    assert r.json()["counts"][ROCKET] == 1
    assert (
        get_table("reactions").count(target_type="project", target_uid=project_uid) == 1
    )
