# retoor <retoor@molodetz.nl>

from datetime import datetime, timezone
from devplacepy.database import (
    get_table,
    get_users_by_uids,
    get_vote_counts,
    get_user_votes,
    get_comment_counts_by_post_uids,
    resolve_by_slug,
    get_user_stars,
    get_user_rank,
    build_pagination,
)
from devplacepy.utils import generate_uid
def _now_db_helpers():
    return datetime.now(timezone.utc).isoformat()
def _user_db_helpers():
    uid = generate_uid()
    get_table("users").insert(
        {
            "uid": uid,
            "username": f"dbh_{uid[:8]}",
            "terms_version": "1",
            "email": f"{uid[:8]}@t.dev",
            "created_at": _now_db_helpers(),
        }
    )
    return uid
def _post(owner, **extra):
    uid = generate_uid()
    get_table("posts").insert(
        {
            "uid": uid,
            "user_uid": owner,
            "slug": f"{uid[:8]}-post",
            "title": None,
            "content": "db helper post body",
            "topic": "random",
            "project_uid": None,
            "image": None,
            "stars": 0,
            "created_at": _now_db_helpers(),
            "deleted_at": None,
            "deleted_by": None,
            **extra,
        }
    )
    return uid
def _comment(owner, target_uid):
    uid = generate_uid()
    get_table("comments").insert(
        {
            "uid": uid,
            "user_uid": owner,
            "target_uid": target_uid,
            "target_type": "post",
            "content": "db helper comment",
            "created_at": _now_db_helpers(),
            "deleted_at": None,
            "deleted_by": None,
        }
    )
    return uid
def _vote(target_type, target_uid, value):
    get_table("votes").insert(
        {
            "uid": generate_uid(),
            "user_uid": generate_uid(),
            "target_uid": target_uid,
            "target_type": target_type,
            "value": value,
            "created_at": _now_db_helpers(),
            "deleted_at": None,
            "deleted_by": None,
        }
    )
from uuid import uuid4
from tests.conftest import BASE_URL
from devplacepy.database import get_table


def test_get_users_by_uids_dedupes(local_db):
    a, b = generate_uid(), generate_uid()
    users = get_table("users")
    users.insert({"uid": a, "username": f"u_{a[:6]}", "created_at": _now_db_helpers()})
    users.insert({"uid": b, "username": f"u_{b[:6]}", "created_at": _now_db_helpers()})
    result = get_users_by_uids([a, b, a])
    assert set(result.keys()) == {a, b}


def test_get_vote_counts_groups_up_and_down(local_db):
    target = generate_uid()
    votes = get_table("votes")
    for value in (1, 1, -1):
        votes.insert(
            {
                "uid": generate_uid(),
                "user_uid": generate_uid(),
                "target_uid": target,
                "target_type": "post",
                "value": value,
                "created_at": _now_db_helpers(),
                "deleted_at": None,
                "deleted_by": None,
            }
        )
    ups, downs = get_vote_counts([target])
    assert ups[target] == 2
    assert downs[target] == 1


def test_get_user_votes_returns_user_value(local_db):
    target = generate_uid()
    user = generate_uid()
    get_table("votes").insert(
        {
            "uid": generate_uid(),
            "user_uid": user,
            "target_uid": target,
            "target_type": "post",
            "value": 1,
            "created_at": _now_db_helpers(),
            "deleted_at": None,
            "deleted_by": None,
        }
    )
    assert get_user_votes(user, [target]) == {target: 1}


def test_get_comment_counts_by_post_uids(local_db):
    post = generate_uid()
    comments = get_table("comments")
    for _ in range(2):
        comments.insert(
            {
                "uid": generate_uid(),
                "user_uid": generate_uid(),
                "target_type": "post",
                "target_uid": post,
                "content": "hi",
                "created_at": _now_db_helpers(),
                "deleted_at": None,
                "deleted_by": None,
            }
        )
    assert get_comment_counts_by_post_uids([post]) == {post: 2}


def test_resolve_by_slug_finds_by_slug_or_uid(local_db):
    owner = _user_db_helpers()
    uid = _post(owner)
    posts = get_table("posts")
    slug = posts.find_one(uid=uid)["slug"]
    assert resolve_by_slug(posts, slug)["uid"] == uid
    assert resolve_by_slug(posts, uid)["uid"] == uid
    assert resolve_by_slug(posts, "missing-slug-xyz") is None


def test_get_user_stars_counts_all_votable_contributions(local_db):
    user = _user_db_helpers()
    post_uid = _post(user)
    for _ in range(7):
        _vote("post", post_uid, 1)
    comment_uid = _comment(user, post_uid)
    _vote("comment", comment_uid, 1)
    _vote("comment", comment_uid, 1)
    _vote("comment", comment_uid, -1)
    assert get_user_stars(user) == 8


def test_get_user_rank_unknown_user_is_none(local_db):
    assert get_user_rank(generate_uid()) is None


def test_build_pagination_metadata():
    first = build_pagination(1, 100, 25)
    assert first["total_pages"] == 4
    assert first["has_prev"] is False
    assert first["has_next"] is True
    middle = build_pagination(2, 100, 25)
    assert middle["has_prev"] is True
    assert middle["has_next"] is True
    clamped = build_pagination(99, 10, 25)
    assert clamped["page"] == 1
    assert clamped["total_pages"] == 1


def test_news_usage_accumulates_and_computes_averages(local_db):
    from devplacepy.database import add_news_usage, get_news_usage

    get_table("news_usage").delete()
    empty = get_news_usage()
    assert empty["calls"] == 0
    assert empty["avg_tokens"] == 0.0

    totals = {
        "calls": 2,
        "prompt_tokens": 2000,
        "completion_tokens": 400,
        "total_tokens": 2400,
        "cost_usd": 0.0005,
        "upstream_latency_ms": 1600.0,
        "total_latency_ms": 1800.0,
    }
    add_news_usage(totals)
    add_news_usage(totals)

    usage = get_news_usage()
    assert usage["calls"] == 4
    assert usage["total_tokens"] == 4800
    assert abs(usage["cost_usd"] - 0.001) < 1e-9
    assert usage["avg_tokens"] == 1200.0
    assert usage["avg_cost_usd"] == 0.00025
    assert usage["avg_tokens_per_second"] == 250.0


def test_add_news_usage_ignores_zero_call_batches(local_db):
    from devplacepy.database import add_news_usage, get_news_usage

    get_table("news_usage").delete()
    add_news_usage({"calls": 0, "total_tokens": 999, "cost_usd": 1.0})
    assert get_news_usage()["calls"] == 0


def test_issue_usage_accumulates_and_computes_averages(local_db):
    from devplacepy.database import add_issue_usage, get_issue_usage

    get_table("issue_usage").delete()
    empty = get_issue_usage()
    assert empty["calls"] == 0
    assert empty["avg_tokens"] == 0.0

    totals = {
        "calls": 2,
        "prompt_tokens": 2000,
        "completion_tokens": 400,
        "total_tokens": 2400,
        "cost_usd": 0.0005,
        "upstream_latency_ms": 1600.0,
        "total_latency_ms": 1800.0,
    }
    add_issue_usage(totals)
    add_issue_usage(totals)

    usage = get_issue_usage()
    assert usage["calls"] == 4
    assert usage["total_tokens"] == 4800
    assert abs(usage["cost_usd"] - 0.001) < 1e-9
    assert usage["avg_tokens"] == 1200.0
    assert usage["avg_cost_usd"] == 0.00025
    assert usage["avg_tokens_per_second"] == 250.0


def test_add_issue_usage_ignores_zero_call_batches(local_db):
    from devplacepy.database import add_issue_usage, get_issue_usage

    get_table("issue_usage").delete()
    add_issue_usage({"calls": 0, "total_tokens": 999, "cost_usd": 1.0})
    assert get_issue_usage()["calls"] == 0


def test_seo_usage_accumulates_and_computes_averages(local_db):
    from devplacepy.database import add_seo_usage, get_seo_usage

    get_table("seo_usage").delete()
    empty = get_seo_usage()
    assert empty["calls"] == 0
    assert empty["avg_tokens"] == 0.0
    assert empty["avg_cost_usd"] == 0.0

    totals = {
        "calls": 2,
        "prompt_tokens": 2000,
        "completion_tokens": 400,
        "total_tokens": 2400,
        "cost_usd": 0.0005,
        "upstream_latency_ms": 1600.0,
        "total_latency_ms": 1800.0,
    }
    add_seo_usage(totals)
    add_seo_usage(totals)

    usage = get_seo_usage()
    assert usage["calls"] == 4
    assert usage["total_tokens"] == 4800
    assert abs(usage["cost_usd"] - 0.001) < 1e-9
    assert usage["avg_tokens"] == 1200.0
    assert usage["avg_cost_usd"] == 0.00025
    assert usage["avg_tokens_per_second"] == 250.0


def test_add_seo_usage_ignores_zero_call_batches(local_db):
    from devplacepy.database import add_seo_usage, get_seo_usage

    get_table("seo_usage").delete()
    add_seo_usage({"calls": 0, "total_tokens": 999, "cost_usd": 1.0})
    assert get_seo_usage()["calls"] == 0


def test_upsert_seo_metadata_inserts_then_updates_single_row(local_db):
    from devplacepy.database import (
        get_seo_metadata,
        has_fresh_seo_metadata,
        upsert_seo_metadata,
    )

    target_uid = generate_uid()
    table = get_table("seo_metadata")

    upsert_seo_metadata(
        "post", target_uid, "Plain Title", "Plain description", "alpha, beta", "pending", "plain"
    )
    assert has_fresh_seo_metadata("post", target_uid) is False
    assert get_seo_metadata("post", target_uid) is None

    upsert_seo_metadata(
        "post", target_uid, "AI Title", "AI description", "python, sqlite", "ready", "ai"
    )
    ready = get_seo_metadata("post", target_uid)
    assert ready is not None
    assert ready["seo_title"] == "AI Title"
    assert ready["seo_keywords"] == "python, sqlite"
    assert ready["generated_at"]
    assert ready["deleted_at"] is None
    assert has_fresh_seo_metadata("post", target_uid) is True

    rows = list(table.find(target_type="post", target_uid=target_uid))
    assert len(rows) == 1


def test_seo_metadata_batch_returns_only_ready_live_rows(local_db):
    from devplacepy.database import (
        get_seo_metadata_batch,
        upsert_seo_metadata,
    )

    ready_uid = generate_uid()
    pending_uid = generate_uid()
    upsert_seo_metadata("gist", ready_uid, "T", "D", "k1, k2", "ready", "ai")
    upsert_seo_metadata("gist", pending_uid, "T", "D", "k1, k2", "pending", "plain")

    batch = get_seo_metadata_batch("gist", [ready_uid, pending_uid])
    assert ready_uid in batch
    assert pending_uid not in batch
    assert batch[ready_uid]["status"] == "ready"


def test_mark_seo_metadata_stale_drops_ready_status(local_db):
    from devplacepy.database import (
        get_seo_metadata,
        mark_seo_metadata_stale,
        upsert_seo_metadata,
    )

    target_uid = generate_uid()
    upsert_seo_metadata("project", target_uid, "T", "D", "k", "ready", "ai")
    assert get_seo_metadata("project", target_uid) is not None

    mark_seo_metadata_stale("project", target_uid)
    assert get_seo_metadata("project", target_uid) is None


def test_seo_metadata_is_soft_deletable_and_indexed(local_db):
    from devplacepy.database import SOFT_DELETE_TABLES

    assert "seo_metadata" in SOFT_DELETE_TABLES
    columns = get_table("seo_metadata").columns
    assert "deleted_at" in columns
    assert "deleted_by" in columns


def test_interleave_by_author_spreads_consecutive_runs():
    from devplacepy.database import interleave_by_author

    rows = [
        {"uid": 1, "user_uid": "a"},
        {"uid": 2, "user_uid": "a"},
        {"uid": 3, "user_uid": "a"},
        {"uid": 4, "user_uid": "b"},
        {"uid": 5, "user_uid": "b"},
        {"uid": 6, "user_uid": "c"},
    ]
    spread = interleave_by_author(rows)
    authors = [r["user_uid"] for r in spread]
    assert len(spread) == len(rows)
    assert all(authors[i] != authors[i + 1] for i in range(len(authors) - 1))


def test_interleave_by_author_preserves_per_author_order():
    from devplacepy.database import interleave_by_author

    rows = [
        {"uid": 1, "user_uid": "a"},
        {"uid": 2, "user_uid": "a"},
        {"uid": 3, "user_uid": "a"},
        {"uid": 4, "user_uid": "b"},
    ]
    spread = interleave_by_author(rows)
    a_order = [r["uid"] for r in spread if r["user_uid"] == "a"]
    assert a_order == [1, 2, 3]


def test_interleave_by_author_single_author_keeps_order():
    from devplacepy.database import interleave_by_author

    rows = [{"uid": i, "user_uid": "a"} for i in range(4)]
    spread = interleave_by_author(rows)
    assert [r["uid"] for r in spread] == [0, 1, 2, 3]


def _relation(actor_uid, target_uid, kind):
    get_table("user_relations").insert(
        {
            "uid": generate_uid(),
            "user_uid": actor_uid,
            "target_uid": target_uid,
            "kind": kind,
            "created_at": _now_db_helpers(),
            "deleted_at": None,
            "deleted_by": None,
        }
    )


def test_get_user_relations_partitions_both_kinds(local_db):
    from devplacepy.database import get_user_relations, invalidate_user_relations

    actor = _user_db_helpers()
    blocked = _user_db_helpers()
    muted = _user_db_helpers()
    _relation(actor, blocked, "block")
    _relation(actor, muted, "mute")
    invalidate_user_relations(actor)

    relations = get_user_relations(actor)
    assert relations["block"] == frozenset({blocked})
    assert relations["mute"] == frozenset({muted})


def test_get_user_relations_anonymous_is_empty(local_db):
    from devplacepy.database import get_user_relations

    relations = get_user_relations(None)
    assert relations["block"] == frozenset()
    assert relations["mute"] == frozenset()


def test_get_silenced_uids_is_union(local_db):
    from devplacepy.database import get_silenced_uids, invalidate_user_relations

    actor = _user_db_helpers()
    blocked = _user_db_helpers()
    muted = _user_db_helpers()
    _relation(actor, blocked, "block")
    _relation(actor, muted, "mute")
    invalidate_user_relations(actor)

    assert get_silenced_uids(actor) == frozenset({blocked, muted})


def test_soft_deleted_relation_is_ignored(local_db):
    from devplacepy.database import get_blocked_uids, invalidate_user_relations

    actor = _user_db_helpers()
    target = _user_db_helpers()
    _relation(actor, target, "block")
    invalidate_user_relations(actor)
    assert get_blocked_uids(actor) == frozenset({target})

    row = get_table("user_relations").find_one(user_uid=actor, target_uid=target)
    get_table("user_relations").update(
        {"id": row["id"], "deleted_at": _now_db_helpers()}, ["id"]
    )
    invalidate_user_relations(actor)
    assert get_blocked_uids(actor) == frozenset()


def test_paginate_excludes_blocked_authors(local_db):
    from devplacepy.database import paginate, invalidate_user_relations

    viewer = _user_db_helpers()
    blocked = _user_db_helpers()
    visible = _user_db_helpers()
    blocked_post = _post(blocked)
    visible_post = _post(visible)
    _relation(viewer, blocked, "block")
    invalidate_user_relations(viewer)

    rows, _ = paginate(get_table("posts"), viewer_uid=viewer)
    uids = {r["uid"] for r in rows}
    assert visible_post in uids
    assert blocked_post not in uids


def test_paginate_without_viewer_keeps_all(local_db):
    from devplacepy.database import paginate, invalidate_user_relations

    viewer = _user_db_helpers()
    blocked = _user_db_helpers()
    blocked_post = _post(blocked)
    _relation(viewer, blocked, "block")
    invalidate_user_relations(viewer)

    rows, _ = paginate(get_table("posts"), viewer_uid=None)
    assert blocked_post in {r["uid"] for r in rows}


def test_load_comments_drops_blocked_author(local_db):
    from devplacepy.database import load_comments, invalidate_user_relations

    viewer = _user_db_helpers()
    host = _user_db_helpers()
    blocked = _user_db_helpers()
    post_uid = _post(host)
    _comment(blocked, post_uid)
    _relation(viewer, blocked, "block")
    invalidate_user_relations(viewer)

    viewer_row = get_table("users").find_one(uid=viewer)
    comments = load_comments("post", post_uid, viewer_row)
    assert comments == []

    others = load_comments("post", post_uid, None)
    assert len(others) == 1


def test_get_uids_by_username_match_returns_matching_uids(local_db):
    from devplacepy.database import get_uids_by_username_match

    target = generate_uid()
    get_table("users").insert(
        {
            "uid": target,
            "username": "searchablehero",
            "terms_version": "1",
            "created_at": _now_db_helpers(),
        }
    )
    assert target in get_uids_by_username_match("searchablehero")
    assert target in get_uids_by_username_match("hablehe")
    assert get_uids_by_username_match("") == []


def test_text_search_clause_matches_author_username(local_db):
    from devplacepy.database import text_search_clause

    author = generate_uid()
    get_table("users").insert(
        {
            "uid": author,
            "username": "authorquery",
            "terms_version": "1",
            "created_at": _now_db_helpers(),
        }
    )
    post_uid = _post(author, title="unrelated title", content="unrelated body")
    posts = get_table("posts")

    clause = text_search_clause(
        posts, "authorquery", ("title", "content"), author_field="user_uid"
    )
    assert clause is not None
    rows = list(posts.find(clause, deleted_at=None))
    assert post_uid in {r["uid"] for r in rows}


def test_text_search_clause_without_author_field_ignores_username(local_db):
    from devplacepy.database import text_search_clause

    author = generate_uid()
    get_table("users").insert(
        {
            "uid": author,
            "username": "lonelyauthor",
            "terms_version": "1",
            "created_at": _now_db_helpers(),
        }
    )
    _post(author, title="nothing here", content="nothing here either")
    posts = get_table("posts")

    clause = text_search_clause(posts, "lonelyauthor", ("title", "content"))
    rows = list(posts.find(clause, deleted_at=None)) if clause is not None else []
    assert all(r["user_uid"] != author for r in rows)


def test_text_search_clause_still_matches_text_fields(local_db):
    from devplacepy.database import text_search_clause

    author = _user_db_helpers()
    post_uid = _post(author, title="distinctivekeyword", content="body text")
    posts = get_table("posts")

    clause = text_search_clause(
        posts, "distinctivekeyword", ("title", "content"), author_field="user_uid"
    )
    assert clause is not None
    rows = list(posts.find(clause, deleted_at=None))
    assert post_uid in {r["uid"] for r in rows}


def _notification(user_uid, target_url, read=False):
    uid = generate_uid()
    get_table("notifications").insert(
        {
            "uid": uid,
            "user_uid": user_uid,
            "type": "comment",
            "message": "someone replied",
            "related_uid": generate_uid(),
            "target_url": target_url,
            "read": read,
            "created_at": _now_db_helpers(),
        }
    )
    return uid


def _is_read(uid):
    return bool(get_table("notifications").find_one(uid=uid)["read"])


def test_mark_notifications_marks_exact_url(local_db):
    from devplacepy.database import mark_notifications_read_by_target

    user = _user_db_helpers()
    uid = _notification(user, "/posts/abc-post")
    assert mark_notifications_read_by_target(user, "/posts/abc-post") == 1
    assert _is_read(uid) is True


def test_mark_notifications_matches_anchor_prefix(local_db):
    from devplacepy.database import mark_notifications_read_by_target

    user = _user_db_helpers()
    uid = _notification(user, "/posts/abc-post#comment-5")
    assert mark_notifications_read_by_target(user, "/posts/abc-post") == 1
    assert _is_read(uid) is True


def test_mark_notifications_leaves_other_targets(local_db):
    from devplacepy.database import mark_notifications_read_by_target

    user = _user_db_helpers()
    keep = _notification(user, "/posts/other-post")
    assert mark_notifications_read_by_target(user, "/posts/abc-post") == 0
    assert _is_read(keep) is False


def test_mark_notifications_only_unread_counted(local_db):
    from devplacepy.database import mark_notifications_read_by_target

    user = _user_db_helpers()
    _notification(user, "/posts/abc-post", read=True)
    assert mark_notifications_read_by_target(user, "/posts/abc-post") == 0


def test_mark_notifications_empty_args(local_db):
    from devplacepy.database import mark_notifications_read_by_target

    user = _user_db_helpers()
    assert mark_notifications_read_by_target("", "/posts/abc") == 0
    assert mark_notifications_read_by_target(user, "") == 0


def test_mark_notifications_scoped_per_user(local_db):
    from devplacepy.database import mark_notifications_read_by_target

    owner = _user_db_helpers()
    other = _user_db_helpers()
    other_uid = _notification(other, "/posts/abc-post")
    assert mark_notifications_read_by_target(owner, "/posts/abc-post") == 0
    assert _is_read(other_uid) is False
