#!/usr/bin/env python3
"""GodotIQ Bridge E2E Verification — Sprint 4a + 4b + 4c

Run with: python3 test_bridge_e2e.py

Requirements:
- Godot editor open with PFT project
- GodotIQ addon enabled (WebSocket on port 6007)
- Game NOT running (script will start/stop it)

Tests all 11 bridge tools in order:
  Sprint 4a: ping, editor_context, run, stop, screenshot, perf_snapshot
  Sprint 4b: scene_tree, node_ops (editor-side, no game needed)
  Sprint 4c: state_inspect, exec, input (game-side, need game running)
"""

import asyncio
import json
import sys
import time

try:
    import websockets
except ImportError:
    print("ERROR: pip install websockets")
    sys.exit(1)


URI = "ws://127.0.0.1:6007"
PASS = 0
FAIL = 0
SKIP = 0


async def send_recv(ws, req_id, method, params=None, timeout=10):
    """Send request and wait for response, skipping push events."""
    msg = {"id": req_id, "method": method, "params": params or {}}
    await ws.send(json.dumps(msg))
    deadline = time.time() + timeout
    while time.time() < deadline:
        try:
            raw = await asyncio.wait_for(ws.recv(), timeout=max(0.1, deadline - time.time()))
            data = json.loads(raw)
            # Skip push events (game_started, game_stopped, etc.)
            if "event" in data:
                print(f"    [event: {data['event']}]")
                continue
            # Match response by id
            if data.get("id") == req_id:
                return data
        except asyncio.TimeoutError:
            break
    return {"id": req_id, "status": "error", "error": {"code": "TIMEOUT", "message": f"No response in {timeout}s"}}


def check(name, response, *conditions):
    """Check response and print result."""
    global PASS, FAIL
    status = response.get("status", "???")
    result = response.get("result", {})
    error = response.get("error", {})

    ok = status == "ok"
    details = []

    if not ok:
        details.append(f"status={status} error={error.get('code','?')}: {error.get('message','?')}")

    for desc, condition in conditions:
        if not condition:
            ok = False
            details.append(f"FAILED: {desc}")

    if ok:
        PASS += 1
        print(f"  ✅ {name}")
    else:
        FAIL += 1
        print(f"  ❌ {name}")
        for d in details:
            print(f"     {d}")

    return ok, result


async def main():
    global PASS, FAIL, SKIP

    print(f"\n{'='*60}")
    print("GodotIQ Bridge E2E Verification")
    print(f"{'='*60}\n")

    try:
        ws = await asyncio.wait_for(websockets.connect(URI), timeout=3)
    except Exception as e:
        print(f"❌ Cannot connect to {URI}: {e}")
        print("   Is Godot running with the GodotIQ addon enabled?")
        sys.exit(1)

    print("Connected to addon\n")

    # ═══════════════════════════════════════
    print("── Sprint 4a: Foundation ──\n")
    # ═══════════════════════════════════════

    # 1. Ping
    r = await send_recv(ws, "t01", "ping")
    check("ping", r,
          ("has addon_version", "addon_version" in r.get("result", {})),
          ("has editor_version", "editor_version" in r.get("result", {})))

    # 2. Editor context
    r = await send_recv(ws, "t02", "editor_context")
    res = r.get("result", {})
    check("editor_context", r,
          ("has open_scenes", "open_scenes" in res),
          ("has selected_nodes", "selected_nodes" in res),
          ("has project_path", "project_path" in res))

    # 3. Run game (no scene param = play main scene)
    r = await send_recv(ws, "t03", "run", {}, timeout=10)
    check("run (play)", r)
    print("    Waiting 4s for game to start...")
    await asyncio.sleep(4)

    # 4. Screenshot
    r = await send_recv(ws, "t04", "screenshot", {"scale": 0.25, "format": "webp"}, timeout=15)
    res = r.get("result", {})
    img_len = len(res.get("image", ""))
    check("screenshot", r,
          ("has image data", img_len > 100),
          ("has dimensions", res.get("width", 0) > 0))
    if img_len > 0:
        print(f"    {res.get('format','?')} {res.get('width','?')}x{res.get('height','?')}, base64={img_len} chars")

    # 5. Perf snapshot
    r = await send_recv(ws, "t05", "perf_snapshot", timeout=10)
    res = r.get("result", {})
    check("perf_snapshot", r,
          ("has fps", "fps" in res),
          ("fps > 0", res.get("fps", 0) > 0),
          ("has draw_calls", "draw_calls" in res),
          ("has total_nodes", "total_nodes" in res))
    if res.get("fps"):
        print(f"    fps={res['fps']} draws={res.get('draw_calls')} tris={res.get('triangles')} nodes={res.get('total_nodes')}")

    # ═══════════════════════════════════════
    print("\n── Sprint 4b: Editor Operations ──\n")
    # ═══════════════════════════════════════

    # 6. Stop game first (scene_tree and node_ops are editor-side)
    r = await send_recv(ws, "t06a", "stop", timeout=5)
    check("stop (for editor tests)", r)
    await asyncio.sleep(1)

    # 7. Scene tree
    r = await send_recv(ws, "t07", "scene_tree", {"depth": 2}, timeout=10)
    res = r.get("result", {})
    check("scene_tree", r,
          ("has tree", "tree" in res),
          ("has total_nodes", res.get("total_nodes", 0) > 0),
          ("returned_nodes > 0", res.get("returned_nodes", 0) > 0))
    if res.get("total_nodes"):
        print(f"    total={res['total_nodes']} returned={res['returned_nodes']} root={res.get('root','?')}")

    # 8. Node ops — set a property, then read it back
    r = await send_recv(ws, "t08", "node_ops", {
        "operations": [
            {"op": "set_property", "node": "Sun", "property": "light_energy", "value": 1.5}
        ]
    }, timeout=10)
    res = r.get("result", {})
    results = res.get("results", [{}])
    check("node_ops (set_property)", r,
          ("first op ok", results[0].get("status") == "ok" if results else False),
          ("undo_available", res.get("undo_available") is True))

    # ═══════════════════════════════════════
    print("\n── Sprint 4c: Runtime Interaction ──\n")
    # ═══════════════════════════════════════

    # 9. Start game again for runtime tools
    r = await send_recv(ws, "t09", "run", {}, timeout=10)
    check("run (for runtime tests)", r)
    print("    Waiting 4s for game to start...")
    await asyncio.sleep(4)

    # 10. State inspect
    r = await send_recv(ws, "t10", "state_inspect", {
        "queries": [
            {"autoload": "GameManager", "properties": ["current_day"]},
        ]
    }, timeout=10)
    res = r.get("result", {})
    results_list = res.get("results", [])
    first = results_list[0] if results_list else {}
    check("state_inspect", r,
          ("has results", len(results_list) > 0),
          ("node found", first.get("found") is True))
    if first.get("found"):
        print(f"    {first.get('node','?')}.{list(first.get('properties',{}).keys())} = {list(first.get('properties',{}).values())}")

    # 11. Exec
    r = await send_recv(ws, "t11", "exec", {
        "code": "func run():\n\treturn Engine.get_version_info()"
    }, timeout=10)
    res = r.get("result", {})
    check("exec", r,
          ("status OK", res.get("status") == "OK"),
          ("has result", len(res.get("result", "")) > 0))
    if res.get("result"):
        print(f"    result={res['result'][:150]}")

    # 12. Input simulation
    r = await send_recv(ws, "t12", "input", {
        "commands": [
            {"actions": ["ui_accept"], "hold_ms": 100}
        ],
        "track_side_effects": True,
    }, timeout=15)
    res = r.get("result", {})
    check("input (action)", r,
          ("success", res.get("success") is True),
          ("commands_executed > 0", res.get("commands_executed", 0) > 0))
    if res.get("side_effects"):
        print(f"    side_effects: {res['side_effects'][:200]}")

    # 13. Input with UI tap (may fail if no visible button — that's OK)
    r = await send_recv(ws, "t13", "input", {
        "commands": [{"tap": "SpeedButton"}],
    }, timeout=10)
    res = r.get("result", {})
    tap_ok = res.get("success", False)
    if tap_ok:
        PASS += 1
        print(f"  ✅ input (UI tap) — target found")
    else:
        SKIP += 1
        tap_results = res.get("results", [{}])
        err = tap_results[0].get("error", "unknown") if tap_results else "no result"
        print(f"  ⚠️  input (UI tap) — SKIPPED: {err}")

    # 14. Exec with Engine access (RefCounted sandbox can't use get_node)
    r = await send_recv(ws, "t14", "exec", {
        "code": "func run():\n\treturn Engine.get_frames_per_second()"
    }, timeout=10)
    res = r.get("result", {})
    check("exec (engine access)", r,
          ("status OK", res.get("status") == "OK"),
          ("got fps", len(res.get("result", "")) > 0 and res.get("result") != "null"))
    if res.get("result"):
        print(f"    FPS: {res['result']}")

    # 15. Final stop
    r = await send_recv(ws, "t15", "stop", timeout=5)
    check("stop (final)", r)

    await ws.close()

    # ═══════════════════════════════════════
    print(f"\n{'='*60}")
    print(f"RESULTS: {PASS} passed, {FAIL} failed, {SKIP} skipped")
    print(f"{'='*60}\n")

    if FAIL > 0:
        sys.exit(1)


if __name__ == "__main__":
    asyncio.run(main())
