#!/usr/bin/env python3
"""Real end-to-end test for PeerDrop.

Spins up two real daemons and exercises the full stack over IPC:
1. Real file transfer with content-hash verification
2. Correct file_size reporting
3. Live progress updates
4. Chat messaging between nodes
5. Transfer failure -> FAILED (not COMPLETED)
6. Cancel of an in-flight transfer
7. CLI send command (async client) end-to-end
8. REST API endpoints

Run from the repo root:
    python3 test_e2e_real.py
"""

import hashlib
import json
import os
import signal
import subprocess
import sys
import time
import urllib.request

TEST_DIR = "/tmp/peerdrop_e2e"
SOCK_A = os.path.join(TEST_DIR, "node_a.sock")
SOCK_B = os.path.join(TEST_DIR, "node_b.sock")
DOWNLOAD_DIR = os.path.join(TEST_DIR, "downloads")
PORT_A = 4131
PORT_B = 4132
REST_PORT = 8090

os.makedirs(TEST_DIR, exist_ok=True)
os.makedirs(DOWNLOAD_DIR, exist_ok=True)

PASS = 0
FAIL = 0


def check(name: str, cond: bool, detail: str = "") -> None:
    global PASS, FAIL
    if cond:
        PASS += 1
        print(f"  PASS: {name}", flush=True)
    else:
        FAIL += 1
        print(f"  FAIL: {name}  {detail}", flush=True)


def sha256_of(path: str) -> str:
    h = hashlib.sha256()
    with open(path, "rb") as f:
        while True:
            data = f.read(8 * 1024 * 1024)
            if not data:
                break
            h.update(data)
    return h.hexdigest()


def make_file(path: str, size_mb: int) -> None:
    if os.path.exists(path) and os.path.getsize(path) == size_mb * 1024 * 1024:
        return
    seed = os.urandom(1024 * 1024)
    with open(path, "wb") as f:
        written = 0
        target = size_mb * 1024 * 1024
        while written < target:
            chunk = seed[: min(len(seed), target - written)]
            f.write(chunk)
            written += len(chunk)


def kill_daemon(sock_path: str) -> None:
    pid_file = sock_path.replace(".sock", ".pid")
    try:
        if os.path.exists(pid_file):
            pid = int(open(pid_file).read().strip())
            try:
                os.kill(pid, signal.SIGTERM)
                time.sleep(1)
            except ProcessLookupError:
                pass
    except Exception:
        pass
    for f in (sock_path, pid_file):
        if os.path.exists(f):
            try:
                os.unlink(f)
            except OSError:
                pass


def wait_for_socket(path: str, timeout: int = 45) -> bool:
    for _ in range(timeout):
        time.sleep(1)
        if os.path.exists(path):
            return True
    return False


def start_daemon(sock_path: str, port: int, download_dir: str | None = None, rest_port: int | None = None) -> subprocess.Popen:
    cmd = [sys.executable, "-m", "peerdrop.interfaces.cli.app",
           "--sock", sock_path, "daemon", "start", "--port", str(port)]
    if download_dir:
        cmd += ["--download-dir", download_dir]
    if rest_port:
        cmd += ["--rest-port", str(rest_port)]
    log = open(os.path.join(TEST_DIR, os.path.basename(sock_path) + ".log"), "w")
    return subprocess.Popen(cmd, stdout=log, stderr=subprocess.STDOUT)


def main() -> int:
    print("=" * 60)
    print("PeerDrop Real End-to-End Test")
    print("=" * 60)

    sys.path.insert(0, os.getcwd())
    from peerdrop.interfaces.client import PeerDropClient

    # ─── Setup: kill stale daemons, start A and B ─────────────────────
    print("\n[setup] Starting two daemons...")
    kill_daemon(SOCK_A)
    kill_daemon(SOCK_B)
    proc_a = start_daemon(SOCK_A, PORT_A, rest_port=REST_PORT)
    proc_b = start_daemon(SOCK_B, PORT_B, download_dir=DOWNLOAD_DIR)

    if not wait_for_socket(SOCK_A) or not wait_for_socket(SOCK_B):
        print("ERROR: daemons did not start", flush=True)
        return 1
    print("  Both daemons ready.", flush=True)

    client_a = PeerDropClient(SOCK_A)
    client_a.connect()
    client_b = PeerDropClient(SOCK_B)
    client_b.connect()

    id_a = client_a.get_identity()["data"]
    id_b = client_b.get_identity()["data"]
    peer_a = id_a["peer_id"]
    peer_b = id_b["peer_id"]
    print(f"  Node A: {peer_a}", flush=True)
    print(f"  Node B: {peer_b}", flush=True)

    # ─── Test 0: Connect A -> B ───────────────────────────────────────
    print("\n[0] Connecting A -> B...")
    connected = False
    for addr in id_b["addrs"]:
        if "/ip4/" in addr and "/ip6/" not in addr:
            try:
                res = client_a.connect_peer(addr)
                if res.get("ok"):
                    connected = True
                    print(f"  Connected via {addr[:60]}...", flush=True)
                    break
            except Exception as e:
                print(f"  connect attempt failed: {e}", flush=True)
    check("A connects to B", connected)
    time.sleep(1)

    # ─── Test 1: Real file transfer + hash verification ───────────────
    print("\n[1] Real file transfer (500MB, for live progress)...")
    file_big = os.path.join(TEST_DIR, "payload_500mb.bin")
    make_file(file_big, 500)
    orig_hash = sha256_of(file_big)
    orig_size = os.path.getsize(file_big)

    result = client_a.send_file(file_big, peer_b)
    check("send_file returns ok", result.get("ok"), str(result))

    # Poll for completion, recording progress (observe BOTH sender and
    # receiver: the sender's progress is overwritten to 1.0 by DONE almost
    # instantly, while the receiver's is updated every 0.25s by its disk
    # reporter, so it's the observable intermediate signal).
    last_status = None
    max_progress_before_done = 0.0
    completed = False
    deadline = time.time() + 300
    while time.time() < deadline:
        transfers = client_a.list_transfers() + client_b.list_transfers()
        match = [t for t in transfers if t.get("file_name") == "payload_500mb.bin"]
        if match:
            t = match[-1]
            last_status = t.get("status")
            p = t.get("progress", 0.0)
            if last_status != "completed" and p < 1.0:
                max_progress_before_done = max(max_progress_before_done, p)
            if last_status == "completed":
                completed = True
                break
            if last_status in ("failed", "cancelled"):
                break
        time.sleep(0.05)

    check("transfer completes", completed and last_status == "completed", f"last status: {last_status}")
    if match:
        t = match[-1]
        size_ok = t.get("file_size") == orig_size
        check("file_size reported correctly", size_ok,
              f"expected {orig_size}, got {t.get('file_size')}")
        check("progress reached 100%", t.get("progress") == 1.0, str(t.get("progress")))

    recv_file = os.path.join(DOWNLOAD_DIR, "payload_500mb.bin")
    recv_exists = os.path.exists(recv_file)
    check("receiver wrote the file", recv_exists)
    if recv_exists:
        check("received size matches", os.path.getsize(recv_file) == orig_size)
        recv_hash = sha256_of(recv_file)
        check("content hash matches (SHA256)", recv_hash == orig_hash)

    print(f"  (max intermediate progress seen before completion: {max_progress_before_done:.2f})", flush=True)
    check("live progress updates observed", max_progress_before_done > 0.0,
          "no intermediate progress — progress reporting may still be broken")

    # ─── Test 2: Chat messaging A -> B ────────────────────────────────
    # GossipSub needs a few heartbeats (initial delay 2s, interval 5s) to
    # form the mesh, so retry publish+check over a generous window.
    print("\n[2] Messaging (A publishes, B receives)...")
    got = False
    deadline = time.time() + 30
    while time.time() < deadline and not got:
        r = client_a.publish_message("peerdrop", "hello from A, E2E!")
        time.sleep(3)
        msgs_b = client_b.list_messages("peerdrop")
        got = any("hello from A, E2E!" in m.get("data", "") for m in msgs_b)
    check("publish succeeds", r.get("ok"), str(r))
    check("B received A's message", got, str(msgs_b)[:200])

    # ─── Test 3: Failure -> FAILED (not COMPLETED) ────────────────────
    print("\n[3] Transfer to unreachable peer fails with FAILED...")
    from libp2p.crypto.ed25519 import create_new_key_pair
    import hashlib as _hl
    kp = create_new_key_pair(seed=_hl.sha256(b"unreachable-peer").digest())
    from libp2p.peer.id import ID
    bogus_peer = str(ID.from_pubkey(kp.public_key))
    small_file = os.path.join(TEST_DIR, "small_fail.txt")
    with open(small_file, "w") as f:
        f.write("failure test")
    result = client_a.send_file(small_file, bogus_peer)
    check("send to bogus peer returns ok", result.get("ok"), str(result))

    status = None
    deadline = time.time() + 60
    while time.time() < deadline:
        transfers = client_a.list_transfers()
        for t in transfers:
            if t.get("receiver_peer_id") == bogus_peer and t.get("file_name") == "small_fail.txt":
                status = t.get("status")
        if status in ("failed", "completed", "cancelled"):
            break
        time.sleep(0.2)
    check("failed transfer is FAILED (not COMPLETED)", status == "failed", f"status: {status}")

    # ─── Test 4: Cancel of an in-flight transfer ───────────────────────
    print("\n[4] Cancel an in-flight transfer...")
    file_cancel = os.path.join(TEST_DIR, "payload_cancel.bin")
    make_file(file_cancel, 100)

    result = client_a.send_file(file_cancel, peer_b)
    check("send (cancel test) returns ok", result.get("ok"), str(result))

    # Cancel as soon as the transfer is registered (it's still connecting, so
    # the localhost transfer can't race ahead and complete first).
    tid = None
    deadline = time.time() + 30
    while time.time() < deadline:
        for t in client_a.list_transfers():
            if t.get("file_name") == "payload_cancel.bin" and t.get("transfer_id") != "pending":
                tid = t.get("transfer_id")
                break
        if tid:
            break
        time.sleep(0.01)
    print(f"  cancelling transfer {tid}...", flush=True)

    check("in-flight transfer found", tid is not None)
    if tid:
        cancel_result = client_a.cancel_transfer(tid)
        check("cancel_transfer returns ok", cancel_result.get("ok"), str(cancel_result))

        cancelled = False
        deadline = time.time() + 30
        while time.time() < deadline:
            for t in client_a.list_transfers():
                if t.get("transfer_id") == tid:
                    if t.get("status") == "cancelled":
                        cancelled = True
                    break
            if cancelled:
                break
            time.sleep(0.1)
        check("sender transfer status becomes cancelled", cancelled)

        # Wait for the background send task to notice and settle, then make
        # sure the status stays cancelled (never flipped to completed).
        time.sleep(8)
        final_status = None
        for t in client_a.list_transfers():
            if t.get("transfer_id") == tid:
                final_status = t.get("status")
                break
        check("cancelled transfer stays cancelled (not completed)",
              final_status == "cancelled", f"final status: {final_status}")

    # ─── Test 5: CLI send (async client) end-to-end ───────────────────
    print("\n[5] CLI send command (async client path)...")
    cli_file = os.path.join(TEST_DIR, "payload_cli.txt")
    with open(cli_file, "w") as f:
        f.write("cli end-to-end payload " * 10000)
    r = subprocess.run(
        [sys.executable, "-m", "peerdrop.interfaces.cli.app",
         "--sock", SOCK_A, "send", cli_file, "--to", peer_b],
        capture_output=True, text=True, timeout=120,
    )
    out = r.stdout + r.stderr
    check("CLI send exits 0", r.returncode == 0, out[-300:])
    check("CLI reports completion", "Transfer completed" in out, out[-300:])
    cli_recv = os.path.join(DOWNLOAD_DIR, "payload_cli.txt")
    if os.path.exists(cli_recv):
        check("CLI file received with same content",
              open(cli_recv).read() == open(cli_file).read())

    # ─── Test 6: REST API ──────────────────────────────────────────────
    print("\n[6] REST API...")
    try:
        with urllib.request.urlopen(f"http://127.0.0.1:{REST_PORT}/health", timeout=10) as resp:
            body = json.loads(resp.read())
            check("REST /health returns ok", body.get("status") == "ok", str(body))
        with urllib.request.urlopen(f"http://127.0.0.1:{REST_PORT}/api/transfers", timeout=10) as resp:
            body = json.loads(resp.read())
            check("REST /api/transfers lists transfers", isinstance(body.get("transfers"), list))

        # Invalid JSON body -> 400 (not 500)
        req = urllib.request.Request(
            f"http://127.0.0.1:{REST_PORT}/api/settings/download-dir",
            data=b"{not json", method="PUT",
        )
        try:
            urllib.request.urlopen(req, timeout=10)
            check("REST invalid JSON rejected", False, "unexpectedly succeeded")
        except urllib.error.HTTPError as e:
            check("REST invalid JSON returns 400", e.code == 400, f"code {e.code}")
    except Exception as e:
        check("REST API reachable", False, str(e))

    # ─── Summary & cleanup ─────────────────────────────────────────────
    print("\n" + "=" * 60)
    print(f"RESULTS: {PASS} passed, {FAIL} failed")
    print("=" * 60)
    client_a.close()
    client_b.close()
    kill_daemon(SOCK_A)
    kill_daemon(SOCK_B)
    try:
        proc_a.wait(timeout=10)
        proc_b.wait(timeout=10)
    except Exception:
        pass
    return 1 if FAIL else 0


if __name__ == "__main__":
    sys.exit(main())
