"""End-to-end tests for ``IngestDaemonSQL`` + ``RelayBus``: real WS, real
signatures, real DuckDB.

These tests reuse the mock relay from ``test_relay_bus`` (a local
``websockets.serve`` on 127.0.0.1) and craft genuine Nostr events with
``coincurve`` BIP-340 signatures so they pass ``_verify_event`` end-to-end.
The DuckDB target is a temp file per test.

Purpose: verify the full ingest path now goes through ``RelayBus`` (and
specifically that the ``Fatal Python error: Aborted`` antipattern is gone —
recv + ping serialized inside one asyncio loop).
"""

from __future__ import annotations

import hashlib
import json
import os
import tempfile
import threading
import time
from typing import Callable

import pytest
from coincurve import PrivateKey

from ingest_sql import IngestDaemonSQL
from relay_bus import RelayBus
from mock_relay import MockRelay


# ---- event helpers ---------------------------------------------------------


def _make_event(
    sk: PrivateKey, kind: int, content: str, *, created_at: int = 1,
    tags: list | None = None,
) -> dict:
    """Build a real Nostr event signed with ``sk``."""
    tags = tags or []
    pubkey = sk.public_key_xonly.format().hex()
    canonical = json.dumps(
        [0, pubkey, created_at, kind, tags, content],
        separators=(",", ":"),
        ensure_ascii=False,
    ).encode("utf-8")
    eid = hashlib.sha256(canonical).hexdigest()
    sig = sk.sign_schnorr(bytes.fromhex(eid)).hex()
    return {
        "id": eid,
        "pubkey": pubkey,
        "created_at": created_at,
        "kind": kind,
        "tags": tags,
        "content": content,
        "sig": sig,
    }


# ---- fixtures --------------------------------------------------------------


@pytest.fixture
def temp_daemon():
    """Fresh ``IngestDaemonSQL`` backed by a temp DuckDB + a running bus."""
    bus = RelayBus()
    with tempfile.TemporaryDirectory() as td:
        daemon = IngestDaemonSQL(os.path.join(td, "t.duckdb"), bus=bus)
        daemon.start()
        try:
            yield daemon
        finally:
            daemon.stop()


# ---- tests -----------------------------------------------------------------


def test_req_and_ingest_writes_events_to_duckdb(temp_daemon):
    sk = PrivateKey()
    events = [
        _make_event(sk, 1, "hello world", created_at=100),
        _make_event(sk, 1, "second event", created_at=101),
    ]

    async def handler(relay, ws, msg):
        if msg[0] == "REQ":
            for e in events:
                await ws.send(json.dumps(["EVENT", msg[1], e]))
            await ws.send(json.dumps(["EOSE", msg[1]]))

    relay = MockRelay(handler)
    relay.start()
    try:
        # The daemon only queries relays whose `relays.active = true`, so seed
        # the row before kicking off `req_and_ingest`.
        with temp_daemon._sql_lock:
            temp_daemon._conn.execute(
                "INSERT INTO relays (url, active) VALUES (?, true)", [relay.url],
            )
        result = temp_daemon.req_and_ingest({"kinds": [1], "limit": 10})
    finally:
        relay.stop()

    assert result["unique_events"] == 2
    with temp_daemon._sql_lock:
        ids = [r[0] for r in temp_daemon._conn.execute(
            "SELECT id FROM events ORDER BY created_at",
        ).fetchall()]
    assert ids == [events[0]["id"], events[1]["id"]]


def test_req_and_ingest_skips_fake_events(temp_daemon):
    """Events with a forged signature must NOT land in `events` and must
    increment the `fake_events` audit table. Confirms `_verify_event` still
    gates ingest after the WS refactor."""
    sk = PrivateKey()
    good = _make_event(sk, 1, "ok", created_at=200)
    forged = dict(good)
    forged["id"] = "0" * 64  # id no longer matches the canonical sha256

    async def handler(relay, ws, msg):
        if msg[0] == "REQ":
            await ws.send(json.dumps(["EVENT", msg[1], forged]))
            await ws.send(json.dumps(["EVENT", msg[1], good]))
            await ws.send(json.dumps(["EOSE", msg[1]]))

    relay = MockRelay(handler)
    relay.start()
    try:
        with temp_daemon._sql_lock:
            temp_daemon._conn.execute(
                "INSERT INTO relays (url, active) VALUES (?, true)", [relay.url],
            )
        temp_daemon.req_and_ingest({"kinds": [1]})
    finally:
        relay.stop()

    with temp_daemon._sql_lock:
        n_events = temp_daemon._conn.execute(
            "SELECT count(*) FROM events",
        ).fetchone()[0]
        n_fake = temp_daemon._conn.execute(
            "SELECT count(*) FROM fake_events",
        ).fetchone()[0]
    assert n_events == 1
    assert n_fake == 1


def test_live_subscription_pushes_events_into_storage(temp_daemon):
    """Open a live sub on the mock relay; push a kind-1 event; assert it
    lands in DuckDB. Exercises the same path that previously crashed under
    the sync-thread WS model."""
    sk = PrivateKey()
    evt = _make_event(sk, 1, "live", created_at=500)
    pushed = threading.Event()

    async def handler(relay, ws, msg):
        if msg[0] == "REQ":
            # The bus issues two REQs for a primary sub (authors + #p), but
            # the test event only matches the authors filter. Push it
            # whenever a REQ that contains an `authors` key comes in.
            flt = msg[2] if len(msg) >= 3 else {}
            if "authors" in flt:
                await ws.send(json.dumps(["EVENT", msg[1], evt]))
            await ws.send(json.dumps(["EOSE", msg[1]]))
            pushed.set()

    relay = MockRelay(handler)
    relay.start()
    try:
        pubkey = sk.public_key_xonly.format().hex()
        # Add the relay to the registry so the live-sub layer treats it as
        # a known target (some downstream consumers look it up by url).
        with temp_daemon._sql_lock:
            temp_daemon._conn.execute(
                "INSERT INTO relays (url, active) VALUES (?, true)", [relay.url],
            )
        temp_daemon.live_subs.add(pubkey, relay.url, limit=10, role="primary")
        # Wait for the relay to have processed the REQ before we look.
        assert pushed.wait(timeout=3.0)
        # Live ingest is async; give the bus loop a moment to deliver the
        # event into DuckDB.
        deadline = time.monotonic() + 3.0
        while time.monotonic() < deadline:
            with temp_daemon._sql_lock:
                got = temp_daemon._conn.execute(
                    "SELECT id FROM events WHERE pubkey = ?", [pubkey],
                ).fetchall()
            if got:
                break
            time.sleep(0.05)
        assert got and got[0][0] == evt["id"]
    finally:
        temp_daemon.live_subs.remove_pubkey(pubkey)
        relay.stop()
