# 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
from devplacepy.database import get_table
JSON_content_negotiation = {"Accept": "application/json"}
_counter_content_negotiation = [0]
def _session_content_negotiation(password="secret123"):
    _counter_content_negotiation[0] += 1
    name = f"cn{int(time.time() * 1000)}{_counter_content_negotiation[0]}"
    s = requests.Session()
    s.post(
        f"{BASE_URL}/auth/signup",
        data={
            "username": name,
            "email": f"{name}@t.dev",
            "password": password,
            "confirm_password": password,
            "birth_date": "1990-01-01",
            "accept_terms": "1",
        },
        allow_redirects=True,
    )
    return s, name
PUBLIC_PAGES = ["/feed", "/projects", "/gists", "/news", "/leaderboard", "/issues"]
_counter_polls = [0]
AJAX_polls = {"X-Requested-With": "fetch"}
def _session_polls():
    _counter_polls[0] += 1
    name = f"pol{int(time.time() * 1000)}{_counter_polls[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_polls(session, title, question, options):
    fields = [
        ("content", "Poll host post content for tests."),
        ("title", title),
        ("topic", "question"),
        ("poll_question", question),
    ]
    fields.extend(("poll_options", option) for option in options)
    r = session.post(f"{BASE_URL}/posts/create", data=fields, allow_redirects=False)
    slug = r.headers["location"].split("/posts/")[-1]
    return get_table("posts").find_one(slug=slug)["uid"]
def _poll_for(post_uid):
    poll = get_table("polls").find_one(post_uid=post_uid)
    options = list(
        get_table("poll_options").find(poll_uid=poll["uid"], order_by=["position"])
    )
    return poll, options
def _create_plain_post_polls(session, title):
    r = session.post(
        f"{BASE_URL}/posts/create",
        data={
            "content": "Plain post awaiting a poll on edit.",
            "title": title,
            "topic": "question",
        },
        allow_redirects=False,
    )
    slug = r.headers["location"].split("/posts/")[-1]
    return slug, get_table("posts").find_one(slug=slug)["uid"]
import sqlite3
from devplacepy.config import DATABASE_URL
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]
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 _seed_news_seo():
    from datetime import datetime, timezone
    from devplacepy.database import get_table
    from devplacepy.utils import generate_uid, make_combined_slug

    uid = generate_uid()
    title = f"SEO Test News Article {uid.split('-')[-1]}"
    slug = make_combined_slug(title, uid)
    get_table("news").insert(
        {
            "deleted_at": None,
            "deleted_by": None,
            "uid": uid,
            "slug": slug,
            "title": title,
            "description": "A seeded news article for SEO tests.",
            "content": "Body content for the seeded article.",
            "url": "https://example.com/article",
            "source_name": "ExampleSource",
            "status": "published",
            "synced_at": datetime.now(timezone.utc).isoformat(),
            "show_on_landing": 0,
            "grade": 8,
        }
    )
    return slug, uid
def _seed_owner():
    from datetime import datetime, timezone
    from uuid import uuid4
    from devplacepy.database import get_table

    uid = str(uuid4())
    get_table("users").insert(
        {
            "uid": uid,
            "username": f"seo_{uid[:8]}",
            "terms_version": "1",
            "email": f"{uid[:8]}@seo.test",
            "password_hash": "x",
            "role": "Member",
            "is_active": True,
            "created_at": datetime.now(timezone.utc).isoformat(),
        }
    )
    return uid
def _seed_post_seo(image=None):
    from datetime import datetime, timezone
    from uuid import uuid4
    from devplacepy.database import get_table
    from devplacepy.utils import make_combined_slug

    owner = _seed_owner()
    uid = str(uuid4())
    slug = make_combined_slug("SEO Detail Post", uid)
    get_table("posts").insert(
        {
            "deleted_at": None,
            "deleted_by": None,
            "uid": uid,
            "user_uid": owner,
            "slug": slug,
            "title": "SEO Detail Post",
            "content": "Body text for the SEO detail post.",
            "topic": "general",
            "project_uid": None,
            "image": image,
            "stars": 0,
            "created_at": datetime.now(timezone.utc).isoformat(),
        }
    )
    return slug, uid
def _seed_gist():
    from datetime import datetime, timezone
    from uuid import uuid4
    from devplacepy.database import get_table
    from devplacepy.utils import make_combined_slug

    owner = _seed_owner()
    uid = str(uuid4())
    slug = make_combined_slug("SEO Detail Gist", uid)
    get_table("gists").insert(
        {
            "deleted_at": None,
            "deleted_by": None,
            "uid": uid,
            "user_uid": owner,
            "slug": slug,
            "title": "SEO Detail Gist",
            "description": "Gist description for SEO tests.",
            "source_code": "print('seo')",
            "language": "python",
            "stars": 0,
            "created_at": datetime.now(timezone.utc).isoformat(),
        }
    )
    return slug, uid
def _seed_project():
    from datetime import datetime, timezone
    from uuid import uuid4
    from devplacepy.database import get_table
    from devplacepy.utils import make_combined_slug

    owner = _seed_owner()
    uid = str(uuid4())
    slug = make_combined_slug("SEO Detail Project", uid)
    get_table("projects").insert(
        {
            "deleted_at": None,
            "deleted_by": None,
            "uid": uid,
            "user_uid": owner,
            "slug": slug,
            "title": "SEO Detail Project",
            "description": "Project description for SEO tests.",
            "project_type": "software",
            "platforms": "Linux",
            "status": "Released",
            "stars": 0,
            "created_at": datetime.now(timezone.utc).isoformat(),
        }
    )
    return slug, uid
def _seed_news_image(news_uid):
    from devplacepy.database import get_table

    get_table("news_images").insert(
        {
            "deleted_at": None,
            "deleted_by": None,
            "news_uid": news_uid,
            "url": "https://example.com/seo-news-image.jpg",
        }
    )
def _seed_feed_posts(count):
    from datetime import datetime, timezone, timedelta
    from uuid import uuid4
    from devplacepy.database import get_table
    from devplacepy.utils import make_combined_slug

    owner = _seed_owner()
    topic = f"seopag{owner[:8]}"
    base = datetime(2026, 1, 1, tzinfo=timezone.utc)
    posts = get_table("posts")
    for i in range(count):
        uid = str(uuid4())
        posts.insert(
            {
                "deleted_at": None,
                "deleted_by": None,
                "uid": uid,
                "user_uid": owner,
                "slug": make_combined_slug(f"seo pag {i}", uid),
                "title": None,
                "content": f"seo pag post {i}",
                "topic": topic,
                "project_uid": None,
                "image": None,
                "stars": 0,
                "created_at": (base - timedelta(seconds=i)).isoformat(),
            }
        )
    return topic
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 _session_validation():
    s = requests.Session()
    name = f"val_{int(time.time() * 1000)}"
    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


def test_post_create_and_delete_recorded(seeded_db):
    s, _ = _member()
    create = s.post(
        f"{BASE_URL}/posts/create",
        headers=JSON_audit_log,
        data={"title": _unique("aupost"), "content": "audit body", "topic": "devlog"},
    ).json()
    uid = create["data"]["uid"]
    slug = create["data"]["slug"]
    admin = _admin(seeded_db)
    assert _find(admin, "post.create", lambda e: e.get("target_uid") == uid) is not None
    s.post(f"{BASE_URL}/posts/delete/{slug}", headers=JSON_audit_log, allow_redirects=False)
    assert _find(admin, "post.delete", lambda e: e.get("target_uid") == uid) is not None


def test_vote_recorded(seeded_db):
    s, _ = _member()
    create = s.post(
        f"{BASE_URL}/posts/create",
        headers=JSON_audit_log,
        data={"title": _unique("auvote"), "content": "vote target", "topic": "devlog"},
    ).json()
    uid = create["data"]["uid"]
    s.post(f"{BASE_URL}/votes/post/{uid}", data={"value": "1"}, allow_redirects=False)
    admin = _admin(seeded_db)
    event = _find(admin, "vote.post.up", lambda e: e.get("target_uid") == uid)
    assert event is not None
    assert event["new_value"] == "1"


def test_devii_origin_header_attribution(seeded_db):
    s, _ = _member()
    create = s.post(
        f"{BASE_URL}/posts/create",
        headers={**JSON_audit_log, "X-Devii-Agent": "1"},
        data={"title": _unique("audevii"), "content": "audited via the agent", "topic": "devlog"},
    ).json()
    uid = create["data"]["uid"]
    admin = _admin(seeded_db)
    event = _find(admin, "post.create", lambda e: e.get("target_uid") == uid)
    assert event is not None
    assert event["origin"] == "devii"
    assert event["via_agent"] == 1


def test_poll_create_recorded(seeded_db):
    s, _ = _member()
    question = _unique("auq") + "?"
    s.post(
        f"{BASE_URL}/posts/create",
        headers=JSON_audit_log,
        data={
            "title": _unique("aupoll"),
            "content": "post with an audited poll",
            "topic": "devlog",
            "poll_question": question,
            "poll_options": ["Alpha", "Beta"],
        },
    )
    admin = _admin(seeded_db)
    event = _find(
        admin, "poll.create", lambda e: (e.get("metadata") or {}).get("question") == question
    )
    assert event is not None


def test_feed_json_shape_hides_sensitive_author_fields(app_server):
    s, _ = _session_content_negotiation()
    s.post(
        f"{BASE_URL}/posts/create",
        data={"content": "negotiation post body", "title": "CN", "topic": "devlog"},
    )
    data = requests.get(f"{BASE_URL}/feed", headers=JSON_content_negotiation).json()
    assert isinstance(data["posts"], list)
    author = data["posts"][0]["author"]
    assert (
        "email" not in author
        and "api_key" not in author
        and "password_hash" not in author
    )


def test_action_returns_envelope_for_json(app_server):
    s, _ = _session_content_negotiation()
    r = s.post(
        f"{BASE_URL}/posts/create",
        headers=JSON_content_negotiation,
        data={"content": "json action body", "title": "JA", "topic": "devlog"},
        allow_redirects=False,
    )
    assert r.status_code == 200
    body = r.json()
    assert body["ok"] is True
    assert body["redirect"].startswith("/posts/")
    assert body["data"]["slug"]
    assert get_table("posts").find_one(uid=body["data"]["uid"]) is not None


def test_action_redirects_for_browser(app_server):
    s, _ = _session_content_negotiation()
    r = s.post(
        f"{BASE_URL}/posts/create",
        data={"content": "browser action body", "title": "BA", "topic": "devlog"},
        allow_redirects=False,
    )
    assert r.status_code == 302
    assert r.headers["location"].startswith("/posts/")


def test_json_validation_returns_422(app_server):
    s, _ = _session_content_negotiation()
    r = s.post(f"{BASE_URL}/posts/create", headers=JSON_content_negotiation, json={})
    assert r.status_code == 422
    body = r.json()
    assert body["error"] == "validation"
    assert "fields" in body


def test_poll_from_comma_separated_string(app_server):
    s, _ = _session_polls()
    title = f"comma-poll-{int(time.time() * 1000)}"
    r = s.post(
        f"{BASE_URL}/posts/create",
        data={
            "content": "Poll built from a comma separated string.",
            "title": title,
            "topic": "question",
            "poll_question": "Tabs or spaces?",
            "poll_options": "Tabs, Spaces, Both",
        },
        allow_redirects=False,
    )
    slug = r.headers["location"].split("/posts/")[-1]
    post_uid = get_table("posts").find_one(slug=slug)["uid"]
    poll, options = _poll_for(post_uid)
    assert poll is not None
    assert [option["label"] for option in options] == ["Tabs", "Spaces", "Both"]


def test_poll_from_newline_separated_string(app_server):
    s, _ = _session_polls()
    title = f"newline-poll-{int(time.time() * 1000)}"
    r = s.post(
        f"{BASE_URL}/posts/create",
        data={
            "content": "Poll built from a newline separated string.",
            "title": title,
            "topic": "question",
            "poll_question": "Pick a language",
            "poll_options": "Python\nRust\nGo",
        },
        allow_redirects=False,
    )
    slug = r.headers["location"].split("/posts/")[-1]
    post_uid = get_table("posts").find_one(slug=slug)["uid"]
    poll, options = _poll_for(post_uid)
    assert poll is not None
    assert [option["label"] for option in options] == ["Python", "Rust", "Go"]


def test_delete_post_cascades_reactions(app_server):
    s, name = _session_reactions()
    title = f"cascade-{int(time.time() * 1000)}"
    r = s.post(
        f"{BASE_URL}/posts/create",
        data={
            "content": "Post created via the server for the reaction cascade test.",
            "title": title,
            "topic": "random",
        },
        allow_redirects=False,
    )
    slug = r.headers["location"].split("/posts/")[-1]
    post_uid = _live_post_uid(slug)
    s.post(
        f"{BASE_URL}/reactions/post/{post_uid}", data={"emoji": ROCKET}, headers=AJAX_reactions
    )
    assert _live_reaction_count("post", post_uid) == 1
    s.post(f"{BASE_URL}/posts/delete/{slug}", allow_redirects=False)
    assert s.get(f"{BASE_URL}/posts/{slug}").status_code == 404
    assert _live_reaction_count("post", post_uid) == 0


def test_post_uid_redirects_to_canonical_slug(app_server):
    slug, uid = _seed_post_seo()
    r = requests.get(f"{BASE_URL}/posts/{uid}", allow_redirects=False)
    assert r.status_code == 301
    assert r.headers["location"].endswith(f"/posts/{slug}"), r.headers.get("location")


def test_post_slug_served_without_redirect(app_server):
    slug, uid = _seed_post_seo()
    r = requests.get(f"{BASE_URL}/posts/{slug}", allow_redirects=False)
    assert r.status_code == 200


def test_post_links_multiple_attachments(app_server):
    s = _session_uploads()
    u1, u2 = _upload_uploads(s), _upload_uploads(s)
    r = s.post(
        f"{BASE_URL}/posts/create",
        data={
            "content": "Post body with two attachments here",
            "title": "attach post",
            "topic": "random",
            "attachment_uids": f"{u1},{u2}",
        },
        allow_redirects=True,
    )
    assert r.status_code == 200, r.text[:300]
    assert "/posts/" in r.url, r.url
    assert u1 in r.text and u2 in r.text, (
        "both attachments must be linked and displayed on the post"
    )


def test_single_attachment_links_to_post(app_server):
    s = _session_uploads()
    u1 = _upload_uploads(s)
    r = s.post(
        f"{BASE_URL}/posts/create",
        data={
            "content": "Post body with one attachment",
            "title": "one",
            "topic": "random",
            "attachment_uids": u1,
        },
        allow_redirects=True,
    )
    assert r.status_code == 200, r.text[:300]
    assert u1 in r.text, "single attachment must be linked and displayed"


def test_oversized_post_content_rejected(app_server):
    s = _session_validation()
    r = s.post(
        f"{BASE_URL}/posts/create",
        data={"content": "x" * 125001, "topic": "random"},
        allow_redirects=False,
    )
    assert r.status_code in (302, 303, 400)
    assert "/posts/" not in (r.headers.get("location") or "")


def test_short_post_content_rejected(app_server):
    s = _session_validation()
    r = s.post(
        f"{BASE_URL}/posts/create",
        data={"content": "short", "topic": "random"},
        allow_redirects=False,
    )
    assert "/posts/" not in (r.headers.get("location") or "")


def _new_project(session):
    return session.post(
        f"{BASE_URL}/projects/create",
        headers=JSON_audit_log,
        data={
            "title": _unique("aupj"),
            "description": "project for linked post test",
            "project_type": "software",
            "status": "In Development",
            "platforms": "",
        },
    ).json()["data"]


def test_post_detail_includes_project_when_linked(app_server):
    s, _ = _member()
    project = _new_project(s)
    project_uid = project["uid"]
    created = s.post(
        f"{BASE_URL}/posts/create",
        headers=JSON_audit_log,
        data={
            "title": _unique("aupjpost"),
            "content": "this post is linked to a project",
            "topic": "devlog",
            "project_uid": project_uid,
        },
    ).json()["data"]
    slug = created["slug"]
    detail = s.get(
        f"{BASE_URL}/posts/{slug}",
        headers=JSON_audit_log,
    ).json()
    assert detail.get("project_link") is not None, "linked project must appear in post detail"
    assert detail["project_link"]["uid"] == project_uid
    assert detail["project_link"]["name"] is not None
    assert "/projects/" in detail["project_link"]["url"]


def test_feed_includes_project_when_linked(app_server):
    s, _ = _member()
    project = _new_project(s)
    project_uid = project["uid"]
    s.post(
        f"{BASE_URL}/posts/create",
        headers=JSON_audit_log,
        data={
            "title": _unique("aupjfeed"),
            "content": "this post appears in feed with a project link",
            "topic": "devlog",
            "project_uid": project_uid,
        },
    )
    feed = s.get(
        f"{BASE_URL}/feed",
        headers=JSON_audit_log,
    ).json()
    found = False
    for item in feed["posts"]:
        if item.get("project_link") is not None and item["project_link"]["uid"] == project_uid:
            found = True
            assert "/projects/" in item["project_link"]["url"]
            break
    assert found, "feed must include project info for linked posts"


def test_create_post_with_war_creates_battle(app_server):
    from devplacepy.services.opinionwar import rules

    s, _ = _member()
    r = s.post(
        f"{BASE_URL}/posts/create",
        headers=JSON_audit_log,
        data={
            "title": _unique("auwar"),
            "content": "this post carries an opinion war between two factions",
            "topic": "question",
            "war_faction_a": "Tabs",
            "war_faction_b": "Spaces",
        },
    )
    post_uid = r.json()["data"]["uid"]
    refresh_snapshot()
    war = get_table("opinion_wars").find_one(post_uid=post_uid)
    assert war is not None
    assert war["faction_a"] == "Tabs"
    assert war["faction_b"] == "Spaces"
    assert war["status"] == "active"
    created = datetime.fromisoformat(war["created_at"])
    ends = datetime.fromisoformat(war["ends_at"])
    assert abs((ends - created).total_seconds() - rules.WAR_DURATION_DAYS * 86400) < 5


def test_create_post_with_one_faction_creates_no_war(app_server):
    s, _ = _member()
    r = s.post(
        f"{BASE_URL}/posts/create",
        headers=JSON_audit_log,
        data={
            "title": _unique("auwarhalf"),
            "content": "one faction alone starts no war on this post",
            "topic": "question",
            "war_faction_a": "Loners",
        },
    )
    post_uid = r.json()["data"]["uid"]
    refresh_snapshot()
    assert get_table("opinion_wars").find_one(post_uid=post_uid) is None


def test_create_post_with_equal_factions_creates_no_war(app_server):
    s, _ = _member()
    r = s.post(
        f"{BASE_URL}/posts/create",
        headers=JSON_audit_log,
        data={
            "title": _unique("auwareq"),
            "content": "two identical faction names start no war here",
            "topic": "question",
            "war_faction_a": "Same",
            "war_faction_b": "same",
        },
    )
    post_uid = r.json()["data"]["uid"]
    refresh_snapshot()
    assert get_table("opinion_wars").find_one(post_uid=post_uid) is None


def test_create_post_with_poll_and_war_attaches_both(app_server):
    s, _ = _member()
    r = s.post(
        f"{BASE_URL}/posts/create",
        headers=JSON_audit_log,
        data=[
            ("title", _unique("auwarpoll")),
            ("content", "this post carries a poll and an opinion war together"),
            ("topic", "question"),
            ("poll_question", "Which side?"),
            ("poll_options", "Tabs"),
            ("poll_options", "Spaces"),
            ("war_faction_a", "Tabs"),
            ("war_faction_b", "Spaces"),
        ],
    )
    post_uid = r.json()["data"]["uid"]
    refresh_snapshot()
    assert get_table("polls").find_one(post_uid=post_uid) is not None
    assert get_table("opinion_wars").find_one(post_uid=post_uid) is not None


def test_feed_serializes_war_on_post(app_server):
    s, _ = _member()
    marker = _unique("auwarfeed")
    r = s.post(
        f"{BASE_URL}/posts/create",
        headers=JSON_audit_log,
        data={
            "title": marker,
            "content": "the feed must carry the serialized war for this post",
            "topic": "question",
            "war_faction_a": "Cats",
            "war_faction_b": "Dogs",
        },
    )
    post_uid = r.json()["data"]["uid"]
    feed = s.get(f"{BASE_URL}/feed", headers=JSON_audit_log).json()
    item = next(i for i in feed["posts"] if i["post"]["uid"] == post_uid)
    assert item["war"] is not None
    assert item["war"]["faction_a"] == "Cats"
    assert item["war"]["pct_a"] == 50
