# retoor <retoor@molodetz.nl>

import base64
import time
import requests
from tests.conftest import BASE_URL
from devplacepy.database import get_table
_counter_api_auth = [0]
def _signup_api_auth(password="secret123"):
    _counter_api_auth[0] += 1
    name = f"apiauth{int(time.time() * 1000)}{_counter_api_auth[0]}"
    email = f"{name}@t.dev"
    session = requests.Session()
    session.post(
        f"{BASE_URL}/auth/signup",
        data={
            "username": name,
            "email": email,
            "password": password,
            "confirm_password": password,
            "birth_date": "1990-01-01",
            "accept_terms": "1",
        },
        allow_redirects=True,
    )
    return session, name, email, password
def _user_api_auth(name):
    return get_table("users").find_one(username=name)
def _key_api_auth(name):
    return _user_api_auth(name)["api_key"]
from datetime import datetime, timezone
import pytest
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
_counter_follow_api = [0]
def _session_follow_api():
    _counter_follow_api[0] += 1
    name = f"flw{int(time.time() * 1000)}{_counter_follow_api[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_follow_api(username):
    return get_table("users").find_one(username=username)["uid"]


def test_x_api_key_authenticates_action(app_server):
    _, follower, _, _ = _signup_api_auth()
    _, target, _, _ = _signup_api_auth()
    r = requests.post(
        f"{BASE_URL}/follow/{target}",
        headers={"X-API-KEY": _key_api_auth(follower)},
        allow_redirects=False,
    )
    assert r.status_code in (302, 200)
    assert (
        get_table("follows").count(
            follower_uid=_user_api_auth(follower)["uid"], following_uid=_user_api_auth(target)["uid"]
        )
        == 1
    )


def test_bearer_token_authenticates_action(app_server):
    _, follower, _, _ = _signup_api_auth()
    _, target, _, _ = _signup_api_auth()
    r = requests.post(
        f"{BASE_URL}/follow/{target}",
        headers={"Authorization": f"Bearer {_key_api_auth(follower)}"},
        allow_redirects=False,
    )
    assert r.status_code in (302, 200)
    assert (
        get_table("follows").count(
            follower_uid=_user_api_auth(follower)["uid"], following_uid=_user_api_auth(target)["uid"]
        )
        == 1
    )


def test_basic_auth_with_username(app_server):
    _, follower, _, password = _signup_api_auth()
    _, target, _, _ = _signup_api_auth()
    r = requests.post(
        f"{BASE_URL}/follow/{target}", auth=(follower, password), allow_redirects=False
    )
    assert r.status_code in (302, 200)
    assert (
        get_table("follows").count(
            follower_uid=_user_api_auth(follower)["uid"], following_uid=_user_api_auth(target)["uid"]
        )
        == 1
    )


def test_basic_auth_with_email(app_server):
    _, follower, email, password = _signup_api_auth()
    _, target, _, _ = _signup_api_auth()
    r = requests.post(
        f"{BASE_URL}/follow/{target}", auth=(email, password), allow_redirects=False
    )
    assert r.status_code in (302, 200)
    assert (
        get_table("follows").count(
            follower_uid=_user_api_auth(follower)["uid"], following_uid=_user_api_auth(target)["uid"]
        )
        == 1
    )


def test_invalid_api_key_returns_401(app_server):
    _, target, _, _ = _signup_api_auth()
    r = requests.post(
        f"{BASE_URL}/follow/{target}",
        headers={"X-API-KEY": "not-a-real-key"},
        allow_redirects=False,
    )
    assert r.status_code == 401


def test_invalid_basic_password_returns_401(app_server):
    _, follower, _, _ = _signup_api_auth()
    _, target, _, _ = _signup_api_auth()
    token = base64.b64encode(f"{follower}:wrongpass".encode()).decode()
    r = requests.post(
        f"{BASE_URL}/follow/{target}",
        headers={"Authorization": f"Basic {token}"},
        allow_redirects=False,
    )
    assert r.status_code == 401


def test_no_credentials_redirects(app_server):
    _, target, _, _ = _signup_api_auth()
    r = requests.post(f"{BASE_URL}/follow/{target}", allow_redirects=False)
    assert r.status_code == 303


def test_follow_recorded(seeded_db):
    s, _ = _member()
    s.post(f"{BASE_URL}/follow/bob_test", headers=JSON_audit_log, allow_redirects=False)
    admin = _admin(seeded_db)
    event = _find(
        admin, "follow.follow", lambda e: e.get("target_label") == "bob_test"
    )
    assert event is not None


def test_free_text_search(seeded_db):
    s, name = _member()
    s.post(f"{BASE_URL}/follow/bob_test", headers=JSON_audit_log, allow_redirects=False)
    admin = _admin(seeded_db)
    data = _audit(admin, q=name)
    assert data["pagination"]["total"] >= 1
    assert all(name in (e["summary"] or "") for e in data["entries"])


def test_follow_emits_notification_create(seeded_db):
    s, name = _member()
    s.post(f"{BASE_URL}/follow/bob_test", headers=JSON_audit_log, allow_redirects=False)
    admin = _admin(seeded_db)
    event = _find(
        admin, "notification.create", lambda e: name in (e.get("summary") or "")
    )
    assert event is not None
    assert event["actor_kind"] == "system"


def test_follow_creates_notification_and_awards_xp(app_server):
    s_a, a_name = _session_follow_api()
    _, b_name = _session_follow_api()
    a_uid, b_uid = _uid_follow_api(a_name), _uid_follow_api(b_name)
    before = get_table("users").find_one(uid=b_uid).get("xp", 0) or 0

    s_a.post(f"{BASE_URL}/follow/{b_name}", allow_redirects=False)

    assert get_table("follows").count(follower_uid=a_uid, following_uid=b_uid) == 1
    assert get_table("notifications").count(user_uid=b_uid, type="follow") == 1
    after = get_table("users").find_one(uid=b_uid).get("xp", 0) or 0
    assert after - before == 5


def test_duplicate_follow_is_idempotent(app_server):
    s_a, a_name = _session_follow_api()
    _, b_name = _session_follow_api()
    a_uid, b_uid = _uid_follow_api(a_name), _uid_follow_api(b_name)

    s_a.post(f"{BASE_URL}/follow/{b_name}", allow_redirects=False)
    s_a.post(f"{BASE_URL}/follow/{b_name}", allow_redirects=False)

    assert get_table("follows").count(follower_uid=a_uid, following_uid=b_uid) == 1
    assert get_table("notifications").count(user_uid=b_uid, type="follow") == 1


def test_self_follow_rejected(app_server):
    s_a, a_name = _session_follow_api()
    a_uid = _uid_follow_api(a_name)
    s_a.post(f"{BASE_URL}/follow/{a_name}", allow_redirects=False)
    assert get_table("follows").count(follower_uid=a_uid, following_uid=a_uid) == 0


def test_follow_nonexistent_user_redirects(app_server):
    s_a, _ = _session_follow_api()
    r = s_a.post(f"{BASE_URL}/follow/no_such_user_zzz", allow_redirects=False)
    assert r.status_code == 302
