#!/usr/bin/env python3
"""
Full end-to-end test: py-ipfs-lite Peer → Kubo (TCP + TLS + Yamux).

Tests:
  1. Kubo daemon starts
  2. py-ipfs-lite Peer starts
  3. Direct dial (host.connect) — not bootstrap — to Kubo
  4. Active connection confirmed
  5. /ipfs/id/1.0.0 stream opened (proves TLS + Yamux end-to-end)
  6. bootstrap() path also tested as a secondary check

Run:  uv run python test_kubo_tls_yamux.py
"""

import logging
import os
import subprocess
import sys
import tempfile
import time

import trio

logging.basicConfig(
    level=logging.DEBUG,
    format="%(asctime)s [%(levelname)s] [%(name)s] %(message)s",
    stream=sys.stderr,
)
logging.getLogger("multiaddr").setLevel(logging.WARNING)
logging.getLogger("libp2p.stream_muxer.yamux").setLevel(logging.INFO)
logging.getLogger("urllib3").setLevel(logging.WARNING)

from libp2p.peer.id import ID
from libp2p.peer.peerinfo import info_from_p2p_addr
from multiaddr import Multiaddr

from py_ipfs_lite.config import Config
from py_ipfs_lite.peer import Peer


# ---------------------------------------------------------------------------
# Kubo helpers
# ---------------------------------------------------------------------------

def start_kubo_daemon(ipfs_path: str):
    env = {**os.environ, "IPFS_PATH": ipfs_path}
    subprocess.run(["ipfs", "init", "--profile=test"], env=env, check=True, capture_output=True)
    subprocess.run(["ipfs", "config", "--json", "Addresses.Swarm", '["/ip4/127.0.0.1/tcp/0"]'],
                   env=env, check=True, capture_output=True)
    subprocess.run(["ipfs", "bootstrap", "rm", "--all"], env=env, check=True, capture_output=True)
    subprocess.run(["ipfs", "config", "Addresses.API", "/ip4/127.0.0.1/tcp/0"], env=env, check=True, capture_output=True)
    subprocess.run(["ipfs", "config", "Addresses.Gateway", "/ip4/127.0.0.1/tcp/0"], env=env, check=True, capture_output=True)

    log_path = os.path.join(ipfs_path, "daemon.log")
    log_file = open(log_path, "w")
    proc = subprocess.Popen(["ipfs", "daemon"], env=env, stdout=log_file, stderr=log_file)

    deadline = time.time() + 30
    while time.time() < deadline:
        time.sleep(0.5)
        if proc.poll() is not None:
            raise RuntimeError("Kubo exited:\n" + open(log_path).read()[-2000:])
        try:
            if "Daemon is ready" in open(log_path).read():
                break
        except FileNotFoundError:
            pass
    else:
        proc.terminate()
        raise RuntimeError("Kubo did not start in 30 s")

    peer_id_str = subprocess.check_output(["ipfs", "id", "-f=<id>"], env=env).decode().strip()
    addrs = subprocess.check_output(["ipfs", "id", "-f=<addrs>"], env=env).decode().strip().splitlines()
    full_addr = next((a.strip() for a in addrs if "127.0.0.1" in a and "/tcp/" in a), None)
    if not full_addr:
        proc.terminate()
        raise RuntimeError(f"No TCP loopback addr found. addrs: {addrs}")

    return proc, ID.from_base58(peer_id_str), full_addr


# ---------------------------------------------------------------------------
# Test
# ---------------------------------------------------------------------------

async def run_test() -> dict:
    results: dict = {}

    with tempfile.TemporaryDirectory(prefix="kubo_test_") as ipfs_path:
        print("\n" + "=" * 60)
        print("  TEST: py-ipfs-lite → Kubo (TCP + TLS + Yamux)")
        print("=" * 60)

        # ── 1. Kubo ──────────────────────────────────────────────────────
        print("\n[1/6] Starting local Kubo daemon …")
        try:
            kubo_proc, kubo_id, kubo_addr = start_kubo_daemon(ipfs_path)
            print(f"      ✅ Kubo started  peer={kubo_id}  addr={kubo_addr}")
            results["kubo_started"] = True
        except Exception as e:
            print(f"      ❌ Failed: {e}")
            results["kubo_started"] = False
            return results

        try:
            # ── 2. py-ipfs-lite Peer ─────────────────────────────────────
            print("\n[2/6] Starting py-ipfs-lite Peer …")
            config = Config(offline=False, reprovide_interval_seconds=-1)
            peer = Peer(config, listen_addrs=["/ip4/127.0.0.1/tcp/0"])
            try:
                await peer.start()
                print(f"      ✅ Peer started  id={peer.host.id()}")
                results["peer_started"] = True
            except Exception as e:
                print(f"      ❌ Failed: {e}")
                results["peer_started"] = False
                return results

            # ── 3. Direct dial (host.connect) ────────────────────────────
            print(f"\n[3/6] Direct dial to Kubo via host.connect() …")
            try:
                raw_host = peer.host._host
                peer_info = info_from_p2p_addr(Multiaddr(kubo_addr))
                with trio.fail_after(10):
                    await raw_host.connect(peer_info)
                print(f"      ✅ host.connect() succeeded!")
                results["direct_dial"] = True
            except trio.TooSlowError:
                print("      ❌ Timed out after 10 s")
                results["direct_dial"] = False
            except Exception as e:
                print(f"      ❌ {type(e).__name__}: {e}")
                results["direct_dial"] = False

            # ── 4. Active connection check ────────────────────────────────
            print("\n[4/6] Checking active connection in swarm …")
            await trio.sleep(1)
            network = peer.host._host.get_network()
            conns = network.connections.get(kubo_id, [])
            active = [c for c in conns if not c.is_closed]

            if active:
                conn = active[0]
                muxed_conn = getattr(conn, "muxed_conn", None)
                mux_name = type(muxed_conn).__name__ if muxed_conn else "unknown"
                sec_proto = (
                    getattr(conn, "security_protocol", None)
                    or getattr(getattr(conn, "secured_conn", None), "protocol_id", None)
                    or "unknown"
                )
                print(f"      ✅ {len(active)} active conn(s)")
                print(f"      Security : {sec_proto}")
                print(f"      Muxer    : {mux_name}")
                results["connection_active"] = True
                results["muxer"] = mux_name
                results["security"] = str(sec_proto)
            else:
                print("      ❌ No active connections to Kubo")
                results["connection_active"] = False

            # ── 5. Open /ipfs/id/1.0.0 stream ────────────────────────────
            print("\n[5/6] Opening /ipfs/id/1.0.0 stream (TLS+Yamux proof) …")
            try:
                raw_host = peer.host._host
                with trio.fail_after(10):
                    stream = await raw_host.new_stream(kubo_id, ["/ipfs/id/1.0.0"])
                    await stream.close()
                print("      ✅ Stream opened → TCP + TLS + Yamux end-to-end WORKS!")
                results["identify_stream"] = True
            except trio.TooSlowError:
                print("      ❌ Timed out after 10 s")
                results["identify_stream"] = False
            except Exception as e:
                print(f"      ❌ {type(e).__name__}: {e}")
                results["identify_stream"] = False

            # ── 6. bootstrap() path (secondary) ──────────────────────────
            print("\n[6/6] Also testing peer.bootstrap() path …")
            # Close the direct connection first so bootstrap has to re-dial
            try:
                await network.close_peer(kubo_id)
            except Exception:
                pass
            await trio.sleep(0.5)

            try:
                with trio.fail_after(15):
                    await peer.bootstrap([kubo_addr])
                print("      ✅ peer.bootstrap() returned without exception")
                results["bootstrap_path"] = True
            except trio.TooSlowError:
                print("      ⚠️  peer.bootstrap() timed out (DHT warm-up stalls it)")
                results["bootstrap_path"] = "timeout"
            except Exception as e:
                print(f"      ❌ {type(e).__name__}: {e}")
                results["bootstrap_path"] = False

        finally:
            try:
                await peer.close()
            except Exception:
                pass
            try:
                kubo_proc.terminate()
                kubo_proc.wait(timeout=5)
            except Exception:
                pass

    return results


def print_summary(results: dict) -> int:
    print("\n" + "=" * 60)
    print("  SUMMARY")
    print("=" * 60)

    checks = [
        ("kubo_started",      "Kubo daemon started"),
        ("peer_started",      "py-ipfs-lite Peer started"),
        ("direct_dial",       "Direct dial (host.connect) to Kubo"),
        ("connection_active", "Active connection in swarm"),
        ("identify_stream",   "Identify stream (TLS+Yamux end-to-end)"),
        ("bootstrap_path",    "peer.bootstrap() path"),
    ]

    failed = 0
    for key, label in checks:
        val = results.get(key)
        if val is True:
            icon = "✅"
        elif val == "timeout":
            icon = "⚠️ "
        elif val is False:
            icon = "❌"
            failed += 1
        else:
            icon = "–  (skipped)"
        print(f"  {icon}  {label}")

    extras = {k: v for k, v in results.items() if k not in [c[0] for c in checks]}
    if extras:
        print()
        for k, v in extras.items():
            print(f"  {k}: {v}")

    print()
    if failed == 0:
        print("  🎉 All critical checks PASSED!")
    else:
        print(f"  ⚠️  {failed} critical check(s) FAILED")
    print("=" * 60)
    return failed


if __name__ == "__main__":
    results = trio.run(run_test)
    sys.exit(print_summary(results))
