# retoor <retoor@molodetz.nl>

from datetime import datetime, timedelta, timezone
import pytest
from devplacepy.database import get_table
from devplacepy.services.audit import store, query
from devplacepy.services.audit import record as audit
from devplacepy.services.audit.categories import category_for
from devplacepy.services.audit.record import _sanitize_summary, _actor_from, _coerce_scalar
def _now_audit():
    return datetime.now(timezone.utc).isoformat()
@pytest.fixture(autouse=True)
def _tables():
    store.ensure_tables()
    yield
def _cli_user(role="Member"):
    from devplacepy.utils import generate_uid

    username = f"auditcli_{generate_uid()[:8]}"
    get_table("users").insert(
        {
            "uid": generate_uid(),
            "username": username,
            "terms_version": "1",
            "email": f"{username}@t.dev",
            "api_key": generate_uid(),
            "role": role,
            "is_active": True,
            "created_at": _now_audit(),
        }
    )
    return username
def _latest(event_key, **match):
    rows = sorted(
        get_table(store.AUDIT_TABLE).find(event_key=event_key),
        key=lambda r: r.get("created_at", ""),
        reverse=True,
    )
    for row in rows:
        if all(row.get(k) == v for k, v in match.items()):
            return row
    return None
_counter_audit = [0]
def audit_uid_marker():
    _counter_audit[0] += 1
    return f"{int(datetime.now(timezone.utc).timestamp() * 1000)}_{_counter_audit[0]}"


def test_category_for_known_prefixes():
    assert category_for("auth.login.success") == "auth"
    assert category_for("vote.post.up") == "engagement"
    assert category_for("reaction.add") == "engagement"
    assert category_for("file.write.create") == "project_files"
    assert category_for("dir.create") == "project_files"
    assert category_for("job.zip.complete") == "project_files"
    assert category_for("admin.setting.update") == "admin"
    assert category_for("container.instance.start") == "container"
    assert category_for("devii.turn") == "devii"
    assert category_for("cli.role.set") == "cli"
    assert category_for("reward.xp.grant") == "reward"
    assert category_for("security.authz.denied") == "security"
    assert category_for("proxy.access") == "ingress"
    assert category_for("ai.gateway.call") == "ai"


def test_category_for_unknown_falls_back_to_first_segment():
    assert category_for("totallyunknown.event") == "totallyunknown"
    assert category_for("") == "other"


def test_sanitize_summary_strips_html_and_truncates():
    assert _sanitize_summary("<b>hi</b> <script>x()</script> there") == "hi x() there"
    assert _sanitize_summary(None) is None
    assert _sanitize_summary("") is None
    long = "a" * 500
    out = _sanitize_summary(long)
    assert len(out) <= 140
    assert out.endswith("...")


def test_actor_from_resolves_role():
    assert _actor_from(None)["actor_kind"] == "guest"
    assert _actor_from(None)["actor_role"] == "guest"
    member = _actor_from({"uid": "u1", "username": "m", "role": "Member"})
    assert member["actor_kind"] == "user" and member["actor_role"] == "member"
    admin = _actor_from({"uid": "u2", "username": "a", "role": "Admin"})
    assert admin["actor_role"] == "admin"


def test_coerce_scalar():
    assert _coerce_scalar(None) is None
    assert _coerce_scalar(True) == "1"
    assert _coerce_scalar(False) == "0"
    assert _coerce_scalar(5) == "5"
    assert _coerce_scalar("x") == "x"


def test_record_system_inserts_row_with_fields():
    key = f"test.unit.basic.{audit_uid_marker()}"
    uid = audit.record_system(
        key,
        actor_kind="user",
        actor_uid="actor-1",
        actor_username="alice",
        actor_role="admin",
        target_type="post",
        target_uid="post-1",
        target_label="Hello",
        old_value=0,
        new_value=1,
        summary="<i>did</i> a thing",
        metadata={"k": "v", "n": 3},
        result="success",
    )
    assert uid
    row = store.get_event(uid)
    assert row["event_key"] == key
    assert row["category"] == "test"
    assert row["actor_kind"] == "user"
    assert row["actor_uid"] == "actor-1"
    assert row["actor_role"] == "admin"
    assert row["target_type"] == "post"
    assert row["old_value"] == "0" and row["new_value"] == "1"
    assert row["summary"] == "did a thing"
    assert row["result"] == "success"
    import json

    assert json.loads(row["metadata"]) == {"k": "v", "n": 3}


def test_record_system_auto_appends_actor_link():
    uid = audit.record_system(
        f"test.unit.actorlink.{audit_uid_marker()}",
        actor_kind="user",
        actor_uid="actor-2",
        actor_username="bob",
    )
    links = store.get_links(uid)
    actor_links = [link for link in links if link["relation"] == "actor"]
    assert len(actor_links) == 1
    assert actor_links[0]["object_uid"] == "actor-2"


def test_record_system_explicit_links_preserved():
    uid = audit.record_system(
        f"test.unit.links.{audit_uid_marker()}",
        actor_kind="user",
        actor_uid="actor-3",
        links=[audit.target("project", "proj-1", "P"), audit.author("author-1")],
    )
    links = store.get_links(uid)
    relations = sorted(link["relation"] for link in links)
    assert relations == ["actor", "author", "target"]


def test_record_never_raises(monkeypatch):
    def boom(_row):
        raise RuntimeError("db exploded")

    monkeypatch.setattr(store, "insert_event", boom)
    # persistence is deferred to the background task queue (inline under tests); a
    # failing write is swallowed there and must never propagate into the caller
    audit.record_system("test.unit.raise", actor_kind="system")
    audit.record(None, "test.unit.raise2", user=None)


def test_record_with_none_request_uses_explicit_user():
    uid = audit.record(
        None,
        f"test.unit.norequest.{audit_uid_marker()}",
        user={"uid": "u9", "username": "carol", "role": "Member"},
        summary="no request object",
    )
    row = store.get_event(uid)
    assert row["actor_uid"] == "u9"
    assert row["actor_role"] == "member"
    assert row["origin"] == "web"
    assert row["request_method"] is None


def test_query_list_events_filters():
    marker = f"qfilter{audit_uid_marker()}"
    key = f"test.qf.{marker}"
    for i in range(3):
        audit.record_system(
            key,
            actor_kind="user",
            actor_uid=f"u{i}",
            actor_role="member",
            result="success" if i < 2 else "failure",
            summary=f"event {marker} number {i}",
        )
    rows, pagination = query.list_events({"event_key": key}, 1)
    assert pagination["total"] == 3
    rows_fail, pag_fail = query.list_events({"event_key": key, "result": "failure"}, 1)
    assert pag_fail["total"] == 1
    rows_q, pag_q = query.list_events({"q": f"{marker} number 1"}, 1)
    assert pag_q["total"] >= 1
    assert all(marker in (r["summary"] or "") for r in rows_q)


def test_query_list_events_pagination():
    key = f"test.qpage.{audit_uid_marker()}"
    for i in range(30):
        audit.record_system(key, actor_kind="system", summary=f"page row {i}")
    rows1, pag1 = query.list_events({"event_key": key}, 1)
    assert pag1["total"] == 30
    assert pag1["total_pages"] == 2
    assert len(rows1) == 25
    rows2, pag2 = query.list_events({"event_key": key}, 2)
    assert len(rows2) == 5
    # newest first ordering: page 1 first row is newer than page 2 last row
    assert rows1[0]["created_at"] >= rows2[-1]["created_at"]


def test_query_date_range():
    key = f"test.qdate.{audit_uid_marker()}"
    audit.record_system(key, actor_kind="system", summary="dated")
    rows, pag = query.list_events(
        {"event_key": key, "date_from": "1999-01-01", "date_to": "2099-12-31"}, 1
    )
    assert pag["total"] == 1
    rows_none, pag_none = query.list_events(
        {"event_key": key, "date_to": "2000-01-01"}, 1
    )
    assert pag_none["total"] == 0


def test_filter_options_returns_distinct():
    key = f"test.opts.{audit_uid_marker()}"
    audit.record_system(key, actor_kind="cli", actor_role="system", origin="cli")
    query._options_cache.clear()
    options = query.filter_options()
    assert key in options["event_key"]
    assert "cli" in options["origin"]
    assert "category" in options and "result" in options


def test_get_event_with_links():
    uid = audit.record_system(
        f"test.unit.detail.{audit_uid_marker()}",
        actor_kind="user",
        actor_uid="u-detail",
        links=[audit.target("gist", "g1", "G")],
    )
    data = query.get_event_with_links(uid)
    assert data is not None
    assert data["event"]["uid"] == uid
    relations = {link["relation"] for link in data["links"]}
    assert "target" in relations and "actor" in relations
    assert query.get_event_with_links("nonexistent-uid") is None


def test_sweep_removes_old_rows_and_links():
    ancient = "1990-01-01T00:00:00+00:00"
    recent_key = f"test.sweep.recent.{audit_uid_marker()}"
    old_uids = []
    for i in range(3):
        uid = store.insert_event(
            {
                "event_key": f"test.sweep.old.{i}",
                "category": "test",
                "actor_kind": "system",
                "created_at": ancient,
                "result": "success",
            }
        )
        store.insert_links(uid, [audit.target("post", f"p{i}")])
        old_uids.append(uid)
    recent_uid = audit.record_system(recent_key, actor_kind="system")
    removed_links, removed_events = store.sweep("2000-01-01T00:00:00+00:00")
    assert removed_events >= 3
    assert removed_links >= 3
    for uid in old_uids:
        assert store.get_event(uid) is None
        assert store.get_links(uid) == []
    # the recent row survives
    assert store.get_event(recent_uid) is not None


def test_sweep_zero_cutoff_is_safe():
    # a cutoff far in the past removes nothing recent
    recent_uid = audit.record_system(
        f"test.sweep.keep.{audit_uid_marker()}", actor_kind="system"
    )
    store.sweep("1970-01-01T00:00:00+00:00")
    assert store.get_event(recent_uid) is not None


def test_retention_service_prunes_old_rows():
    from tests.conftest import run_async
    from devplacepy.database import set_setting
    from devplacepy.services.audit import AuditService

    ancient = "1991-01-01T00:00:00+00:00"
    old_uid = store.insert_event(
        {
            "event_key": f"test.retention.old.{audit_uid_marker()}",
            "category": "test",
            "actor_kind": "system",
            "created_at": ancient,
            "result": "success",
        }
    )
    recent_uid = audit.record_system(
        f"test.retention.recent.{audit_uid_marker()}", actor_kind="system"
    )
    set_setting("audit_log_retention_days", "30")
    run_async(AuditService().run_once())
    assert store.get_event(old_uid) is None
    assert store.get_event(recent_uid) is not None


def test_retention_service_disabled_when_zero():
    from tests.conftest import run_async
    from devplacepy.database import set_setting
    from devplacepy.services.audit import AuditService

    ancient_uid = store.insert_event(
        {
            "event_key": f"test.retention.keep.{audit_uid_marker()}",
            "category": "test",
            "actor_kind": "system",
            "created_at": "1992-01-01T00:00:00+00:00",
            "result": "success",
        }
    )
    set_setting("audit_log_retention_days", "0")
    try:
        run_async(AuditService().run_once())
        assert store.get_event(ancient_uid) is not None
    finally:
        set_setting("audit_log_retention_days", "90")


def test_cli_role_set_recorded(local_db):
    import argparse
    from devplacepy import cli

    username = _cli_user(role="Member")
    uid = get_table("users").find_one(username=username)["uid"]
    cli.cmd_role_set(argparse.Namespace(username=username, role="admin"))
    event = _latest("cli.role.set", target_uid=uid)
    assert event is not None
    assert event["actor_kind"] == "cli"
    assert event["origin"] == "cli"
    import json

    meta = json.loads(event["metadata"])
    assert meta["old"] == "Member" and meta["new"] == "Admin"
    links = store.get_links(event["uid"])
    assert any(link["object_uid"] == uid for link in links)


def test_cli_apikey_reset_recorded(local_db):
    import argparse
    from devplacepy import cli

    username = _cli_user()
    uid = get_table("users").find_one(username=username)["uid"]
    cli.cmd_apikey_reset(argparse.Namespace(username=username))
    event = _latest("cli.apikey.reset", target_uid=uid)
    assert event is not None
    # the new key value is never written into the audit row
    new_key = get_table("users").find_one(uid=uid)["api_key"]
    assert new_key not in (event.get("summary") or "")
    assert new_key not in (event.get("metadata") or "")


def test_cli_apikey_backfill_recorded(local_db):
    import argparse
    from devplacepy import cli

    cli.cmd_apikey_backfill(argparse.Namespace())
    event = _latest("cli.apikey.backfill")
    assert event is not None
    assert event["actor_kind"] == "cli"
    assert (event.get("metadata") or "").find("count") != -1
