#!/usr/bin/env python3
"""End-to-end checks for paths bench_reload does not cover."""
import os, re, subprocess, sys, threading, time

ROOT = "/Users/oscar/.worktree/hot-reload/labs/hot-reload"
CLI = "/Users/oscar/.worktree/hot-reload/target/debug/kan-hot-dev"
API_COMMON = os.path.join(ROOT, "api-common/src/lib.rs")
API = os.path.join(ROOT, "api/src/lib.rs")
SHARED = os.path.join(ROOT, "shared/src/lib.rs")

env = dict(os.environ, RUST_LOG="info", KAN_HOT_TIMING="1",
           KAN_HOT_EXAMPLE_INTERVAL_MS="50", KAN_HOT_EXAMPLE_SLOW="1")
p = subprocess.Popen([CLI, "run", "--bin", "server", "--lib", "api"], cwd=ROOT,
                     stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.STDOUT,
                     env=env, text=True, bufsize=1)
lines = []
lock = threading.Lock()

def reader():
    for line in p.stdout:
        with lock:
            lines.append((time.monotonic(), line))
        sys.stdout.write(line)

threading.Thread(target=reader, daemon=True).start()

def snapshot():
    with lock:
        return list(lines)

def wait_for(pred, timeout=60):
    end = time.monotonic() + timeout
    while time.monotonic() < end:
        for ts, line in reversed(snapshot()):
            if pred(line):
                return ts, line
        time.sleep(0.005)
    raise TimeoutError("timeout")

def count(pred):
    return sum(1 for _, line in snapshot() if pred(line))

def wait_for_new(pred, before, timeout=60):
    end = time.monotonic() + timeout
    while time.monotonic() < end:
        if count(pred) > before:
            return True
        time.sleep(0.01)
    raise TimeoutError("timeout waiting for new match")

orig_common = open(API_COMMON).read()
orig_api = open(API).read()
orig_shared = open(SHARED).read()
results = []
try:
    wait_for(lambda l: "gen 1 hello" in l)

    tag = f"nested-{time.time_ns()}"
    open(API_COMMON, "w").write(orig_common.replace("nested-v1", tag))
    wait_for(lambda l, t=tag: f'"nested":"{t}"' in l)
    results.append(("nested hot-crate reload", True))
    time.sleep(2.5)  # let any slow stream finish

    # Reload while a slow stream is in flight.
    finished = count(lambda l: "finished at" in l and "with 10 ticks" in l)
    started = count(lambda l: "slow stream started" in l)
    wait_for_new(lambda l: "slow stream started" in l, started, timeout=20)
    time.sleep(0.45)
    tag2 = f"inflight-{time.time_ns()}"
    open(API_COMMON, "w").write(orig_common.replace("nested-v1", tag2))
    wait_for(lambda l, t=tag2: f'"nested":"{t}"' in l)
    wait_for_new(lambda l: "finished at" in l and "with 10 ticks" in l, finished, timeout=25)
    results.append(("in-flight streaming survives reload", True))

    # Builder panic (duplicate route) must keep the serving generation.
    before_gen = max(int(m.group(1)) for _, l in snapshot()
                     for m in [re.search(r"loaded generation (\d+)", l)] if m)
    broken = orig_api.replace(
        '.query("count"',
        '.query("count", |state: &AppState, _: ()| async move { state.prefix.clone() })\n        .query("count"',
        1)
    open(API, "w").write(broken)
    wait_for(lambda l: "duplicate route" in l, timeout=60)
    time.sleep(1.5)
    after_gen = max(int(m.group(1)) for _, l in snapshot()
                    for m in [re.search(r"loaded generation (\d+)", l)] if m)
    results.append((f"builder panic keeps generation {before_gen} (still {after_gen})",
                    after_gen == before_gen))
    open(API, "w").write(orig_api)

    tag3 = f"afterpanic-{time.time_ns()}"
    open(API_COMMON, "w").write(orig_common.replace("nested-v1", tag3))
    wait_for(lambda l, t=tag3: f'"nested":"{t}"' in l)
    results.append(("reload after a builder panic", True))

    # Runtime crate change rebuilds/restarts the host from generation 1.
    gen1 = count(lambda l: "gen 1 hello" in l)
    open(SHARED, "w").write(orig_shared + "\n// runtime change probe\n")
    wait_for(lambda l: "shared/runtime code changed" in l, timeout=60)
    wait_for_new(lambda l: "gen 1 hello" in l, gen1, timeout=90)
    results.append(("runtime change restarts the host", True))
    open(SHARED, "w").write(orig_shared)
finally:
    open(API_COMMON, "w").write(orig_common)
    open(API, "w").write(orig_api)
    open(SHARED, "w").write(orig_shared)
    time.sleep(0.6)
    try:
        p.stdin.write("q\n"); p.stdin.flush()
    except BrokenPipeError:
        pass
    try:
        p.wait(timeout=10)
    except subprocess.TimeoutExpired:
        p.kill()

print("\n=== RESULTS ===")
for name, ok in results:
    print(("PASS " if ok else "FAIL ") + name)
sys.exit(0 if all(ok for _, ok in results) else 1)
