# 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 re
from uuid import uuid4
from datetime import datetime, timedelta, timezone
from playwright.sync_api import expect
from tests.conftest import BASE_URL, assert_share_copies
from devplacepy.database import get_table
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
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_gist_create_recorded(seeded_db):
    s, _ = _member()
    gist = s.post(
        f"{BASE_URL}/gists/create",
        headers=JSON_audit_log,
        data={"title": _unique("aug"), "description": "audited gist", "source_code": "print(1)", "language": "python"},
    ).json()["data"]
    admin = _admin(seeded_db)
    assert _find(admin, "gist.create", lambda e: e.get("target_uid") == gist["uid"]) is not None


def test_create_triple_dpbot_gist_saves(app_server):
    session, _ = _register_session("gbig")
    title = f"Triple Dpbot {int(time.time() * 1000)}"
    source = _make_source(3 * DPBOT_SIZE)
    assert len(source) <= SOURCE_LENGTH_LIMIT
    response = session.post(
        f"{BASE_URL}/gists/create",
        data={
            "title": title,
            "description": "large source",
            "language": "python",
            "source_code": source,
        },
        allow_redirects=True,
    )
    assert "/gists/" in response.url
    assert response.url.rstrip("/") != f"{BASE_URL}/gists"
    saved = get_table("gists").find_one(title=title)
    assert saved is not None
    assert saved["source_code"] == source.strip()
    assert len(saved["source_code"]) > 2 * DPBOT_SIZE


def test_create_gist_at_limit_saves(app_server):
    session, _ = _register_session("glim")
    title = f"At Limit {int(time.time() * 1000)}"
    source = _make_source(SOURCE_LENGTH_LIMIT)
    response = session.post(
        f"{BASE_URL}/gists/create",
        data={
            "title": title,
            "description": "",
            "language": "python",
            "source_code": source,
        },
        allow_redirects=True,
    )
    assert "/gists/" in response.url
    assert response.url.rstrip("/") != f"{BASE_URL}/gists"
    assert get_table("gists").find_one(title=title) is not None


def test_oversized_gist_rejected_server_side(app_server):
    session, _ = _register_session("govr")
    title = f"Oversized {int(time.time() * 1000)}"
    source = _make_source(SOURCE_LENGTH_LIMIT + 1)
    session.post(
        f"{BASE_URL}/gists/create",
        data={
            "title": title,
            "description": "",
            "language": "python",
            "source_code": source,
        },
        allow_redirects=True,
    )
    assert get_table("gists").find_one(title=title) is None


def test_gist_links_multiple_attachments(app_server):
    s = _session_uploads()
    u1, u2 = _upload_uploads(s), _upload_uploads(s)
    r = s.post(
        f"{BASE_URL}/gists/create",
        data={
            "title": "Attach Gist",
            "description": "Gist with attachments",
            "source_code": "print('hi')",
            "language": "python",
            "attachment_uids": f"{u1},{u2}",
        },
        allow_redirects=True,
    )
    assert r.status_code == 200, r.text[:300]
    assert "/gists/" in r.url, r.url
    assert u1 in r.text and u2 in r.text, (
        "both attachments must be linked and displayed on the gist"
    )
