# 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 html
import json
import re
from devplacepy.database import get_table
from devplacepy.docs_api import API_GROUPS
from devplacepy.utils import clear_user_cache
def _configs_by_id(text):
    configs = re.findall(r"data-config='(.*?)'", text, re.S)
    return {
        json.loads(html.unescape(cfg))["id"]: json.loads(html.unescape(cfg))
        for cfg in configs
    }
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"]]


def test_audit_docs_page_admin_only(seeded_db):
    # guest cannot see the admin-only audit-log docs page
    assert requests.get(f"{BASE_URL}/docs/audit-log.html").status_code == 404
    admin = _db_user("alice_test")
    r = requests.get(
        f"{BASE_URL}/docs/audit-log.html", headers={"X-API-KEY": admin["api_key"]}
    )
    assert r.status_code == 200
    assert "Audit Log" in r.text


def test_docs_root_redirects_to_index(app_server):
    r = requests.get(f"{BASE_URL}/docs", allow_redirects=False)
    assert r.status_code == 302
    assert r.headers["location"].endswith("/docs/index.html")


def test_docs_index_loads(app_server):
    r = requests.get(f"{BASE_URL}/docs/index.html")
    assert r.status_code == 200
    assert "Documentation" in r.text
    assert "/docs/authentication.html" in r.text


def test_docs_authentication_page_loads(app_server):
    r = requests.get(f"{BASE_URL}/docs/authentication.html")
    assert r.status_code == 200
    for needle in ("X-API-KEY", "Bearer", "Basic"):
        assert needle in r.text


def test_docs_unknown_page_404(app_server):
    r = requests.get(f"{BASE_URL}/docs/nope.html")
    assert r.status_code == 404


def test_docs_index_links_every_public_group(app_server):
    r = requests.get(f"{BASE_URL}/docs/index.html")
    assert r.status_code == 200
    for group in API_GROUPS:
        if group.get("admin"):
            assert f"/docs/{group['slug']}.html" not in r.text
        else:
            assert f"/docs/{group['slug']}.html" in r.text


def test_docs_operator_pages_require_admin(seeded_db):
    users = get_table("users")
    bob = users.find_one(username="bob_test")
    assert requests.get(f"{BASE_URL}/docs/services.html").status_code == 404
    assert requests.get(f"{BASE_URL}/docs/admin.html").status_code == 404
    users.update({"uid": bob["uid"], "role": "Admin"}, ["uid"])
    clear_user_cache(bob["uid"])
    try:
        headers = {"X-API-KEY": bob["api_key"]}
        services = requests.get(f"{BASE_URL}/docs/services.html", headers=headers)
        assert services.status_code == 200
        for name in ("news", "bots", "openai"):
            assert name in services.text
        configs = re.findall(r"data-config='(.*?)'", services.text, re.S)
        assert configs
        for cfg in configs:
            json.loads(html.unescape(cfg))
        assert (
            requests.get(f"{BASE_URL}/docs/admin.html", headers=headers).status_code
            == 200
        )
        admin_index = requests.get(f"{BASE_URL}/docs/index.html", headers=headers)
        assert "/docs/services.html" in admin_index.text
    finally:
        users.update({"uid": bob["uid"], "role": "Member"}, ["uid"])
        clear_user_cache(bob["uid"])


def test_docs_endpoint_config_is_valid_json(app_server):
    r = requests.get(f"{BASE_URL}/docs/social-actions.html")
    assert r.status_code == 200
    configs = re.findall(r"data-config='(.*?)'", r.text, re.S)
    assert configs
    ids = {json.loads(html.unescape(cfg))["id"] for cfg in configs}
    assert "votes-cast" in ids


def test_docs_lookups_group_renamed_and_works(app_server):
    # the former "search" API group lives at /docs/lookups.html now
    r = requests.get(f"{BASE_URL}/docs/lookups.html")
    assert r.status_code == 200
    assert "/profile/search" in r.text
    assert "data-api-tester" in r.text
    assert "/docs/lookups.html" in requests.get(f"{BASE_URL}/docs/index.html").text


def test_docs_search_page_loads(app_server):
    r = requests.get(f"{BASE_URL}/docs/search.html")
    assert r.status_code == 200
    assert 'name="q"' in r.text
    assert "Type a query" in r.text


def test_docs_search_ranks_relevant_results(seeded_db):
    admin = get_table("users").find_one(username="alice_test")
    vision = requests.get(
        f"{BASE_URL}/docs/search.html",
        params={"q": "vision image"},
        headers={"X-API-KEY": admin["api_key"]},
    )
    assert vision.status_code == 200
    assert "/docs/gateway.html" in vision.text
    assert "<mark>" in vision.text
    auth = requests.get(f"{BASE_URL}/docs/search.html", params={"q": "api key"})
    assert "/docs/authentication.html" in auth.text


def test_docs_search_respects_admin_visibility(seeded_db):
    users = get_table("users")
    bob = users.find_one(username="bob_test")
    public = requests.get(
        f"{BASE_URL}/docs/search.html", params={"q": "background services"}
    )
    assert "/docs/services.html" not in public.text
    users.update({"uid": bob["uid"], "role": "Admin"}, ["uid"])
    clear_user_cache(bob["uid"])
    try:
        admin = requests.get(
            f"{BASE_URL}/docs/search.html",
            params={"q": "background services"},
            headers={"X-API-KEY": bob["api_key"]},
        )
        assert "/docs/services.html" in admin.text
    finally:
        users.update({"uid": bob["uid"], "role": "Member"}, ["uid"])
        clear_user_cache(bob["uid"])


def test_docs_pages_inject_runtime_context(app_server):
    r = requests.get(f"{BASE_URL}/docs/conventions.html")
    assert "window.DEVPLACE_DOCS" in r.text
    assert "{{ base }}" not in r.text


def test_docs_endpoint_config_has_negotiation(app_server):
    by_id = _configs_by_id(requests.get(f"{BASE_URL}/docs/content.html").text)
    assert by_id["feed-list"]["negotiation"] == "negotiable"
    assert by_id["feed-list"]["sample_response"] is not None
    assert by_id["feed-list"]["interactive"] is True
    assert by_id["posts-create"]["negotiation"] == "negotiable"


def test_docs_ajax_endpoint_negotiation(app_server):
    by_id = _configs_by_id(requests.get(f"{BASE_URL}/docs/social-actions.html").text)
    assert by_id["votes-cast"]["negotiation"] == "ajax"


def test_docs_lookups_endpoint_is_json_negotiation(app_server):
    by_id = _configs_by_id(requests.get(f"{BASE_URL}/docs/lookups.html").text)
    assert by_id["search-users"]["negotiation"] == "json"


def test_media_gallery_doc_is_public_and_member_safe(app_server):
    r = requests.get(f"{BASE_URL}/docs/media-gallery.html")
    assert r.status_code == 200
    assert "Media gallery" in r.text
    lowered = r.text.lower()
    for leak in ("soft delete", "soft-delete", "trash", "purge", "restore"):
        assert leak not in lowered, leak


def test_media_moderation_doc_is_admin_only(seeded_db):
    # hidden from guests and members
    assert requests.get(f"{BASE_URL}/docs/media-moderation.html").status_code == 404
    bob = get_table("users").find_one(username="bob_test")
    clear_user_cache(bob["uid"])
    # A prior test may have promoted bob and warmed the server's auth cache; the
    # cache-version bump propagates cross-process with up to a one second delay,
    # so poll until the server resolves bob back to a member (404).
    deadline = time.time() + 5
    member = None
    while time.time() < deadline:
        member = requests.get(
            f"{BASE_URL}/docs/media-moderation.html",
            headers={"X-API-KEY": bob["api_key"]},
        )
        if member.status_code == 404:
            break
        time.sleep(0.2)
    assert member.status_code == 404
    # visible to admins, and this is where soft-delete is explained
    admin = get_table("users").find_one(username="alice_test")
    page = requests.get(
        f"{BASE_URL}/docs/media-moderation.html",
        headers={"X-API-KEY": admin["api_key"]},
    )
    assert page.status_code == 200
    assert "soft delete" in page.text.lower()


def test_media_moderation_hidden_from_member_search(seeded_db):
    public = requests.get(f"{BASE_URL}/docs/search.html", params={"q": "soft delete media"})
    assert "/docs/media-moderation.html" not in public.text
    admin = get_table("users").find_one(username="alice_test")
    found = requests.get(
        f"{BASE_URL}/docs/search.html",
        params={"q": "soft delete media"},
        headers={"X-API-KEY": admin["api_key"]},
    )
    assert "/docs/media-moderation.html" in found.text


def test_profiles_api_doc_delete_is_member_safe(app_server):
    text = requests.get(f"{BASE_URL}/docs/profiles.html").text
    assert "/media/" in text  # the member delete endpoint is documented
    lowered = text.lower()
    assert "soft delete" not in lowered
    assert "purge" not in lowered


def test_admin_api_doc_has_moderation(seeded_db):
    admin = get_table("users").find_one(username="alice_test")
    text = requests.get(
        f"{BASE_URL}/docs/admin.html", headers={"X-API-KEY": admin["api_key"]}
    ).text
    assert "/admin/media" in text
    lowered = text.lower()
    assert "restore" in lowered
    assert "purge" in lowered
    assert "soft" in lowered
