#!/usr/bin/env python3
"""
Integration test: full application on a real server.

Starts the full_application example server, then exercises:
- Health, echo, help, get_commands_list
- Dynamic commands: load (from temp file), execute loaded command, unload, reload
- WebSocket: connect, subscribe, receive, unsubscribe
- Queued command (long_running_task) via execute_command_unified (WS wait)
- Custom commands: calculator, embed_queue, embed_job_status

Run:
  pytest tests/test_full_application_real_server.py -v -s
  python tests/test_full_application_real_server.py

Author: Vasiliy Zdanovskiy
email: vasilyvz@gmail.com
"""

from __future__ import annotations

import asyncio
import subprocess
import sys
import tempfile
import time
from pathlib import Path
from typing import Any, List, Optional, Tuple

PROJECT_ROOT = Path(__file__).resolve().parent.parent
if str(PROJECT_ROOT) not in sys.path:
    sys.path.insert(0, str(PROJECT_ROOT))

# Port for the real server (avoid 8080/15000)
FULL_APP_PORT = 18080
HEALTH_TIMEOUT = 30.0


def kill_port(port: int) -> None:
    """Kill any process listening on the given port."""
    try:
        subprocess.run(
            ["fuser", "-k", "-9", f"{port}/tcp"],
            capture_output=True,
            timeout=3,
        )
    except (FileNotFoundError, subprocess.TimeoutExpired, Exception):
        try:
            out = subprocess.run(
                ["lsof", "-ti", f":{port}"],
                capture_output=True,
                text=True,
                timeout=2,
            )
            if out.returncode == 0 and out.stdout.strip():
                for pid in out.stdout.strip().split():
                    subprocess.run(
                        ["kill", "-9", pid],
                        capture_output=True,
                        timeout=2,
                    )
        except Exception:
            pass
    time.sleep(0.5)


async def wait_health(port: int, timeout: float = HEALTH_TIMEOUT) -> bool:
    """Wait until GET http://localhost:port/health returns 200."""
    import httpx

    url = f"http://localhost:{port}/health"
    deadline = time.monotonic() + timeout
    while time.monotonic() < deadline:
        try:
            async with httpx.AsyncClient() as client:
                r = await client.get(url, timeout=2.0)
                if r.status_code == 200:
                    return True
        except Exception:
            pass
        await asyncio.sleep(0.5)
    return False


def make_dynamic_command_file(tmpdir: Path) -> Path:
    """Create a temporary dynamic command file (test_dynamic)."""
    path = tmpdir / "test_dynamic_command.py"
    path.write_text(
        '''
"""
Test dynamic command for real-server integration test.
"""
from mcp_proxy_adapter.commands.base import Command
from mcp_proxy_adapter.commands.result import CommandResult


class TestDynamicCommand(Command):
    name = "test_dynamic"
    descr = "Test dynamic command"

    async def execute(self, value: int = 0, **kwargs) -> CommandResult:
        return CommandResult(
            success=True,
            data={"value": value * 2, "message": "Dynamic command executed"},
        )

    @classmethod
    def get_schema(cls):
        return {
            "type": "object",
            "properties": {"value": {"type": "integer", "default": 0}},
        }
'''
    )
    return path


async def run_full_application_tests(
    port: int = FULL_APP_PORT,
) -> Tuple[int, int, List[str]]:
    """
    Start full_application server, run all tests, stop server.
    Returns (passed_count, failed_count, list of error messages).
    """
    configs_dir = (
        PROJECT_ROOT
        / "mcp_proxy_adapter"
        / "examples"
        / "full_application"
        / "configs"
    )
    config_path = configs_dir / "http_basic.json"
    server_script = (
        PROJECT_ROOT
        / "mcp_proxy_adapter"
        / "examples"
        / "full_application"
        / "main.py"
    )
    if not server_script.exists() or not config_path.exists():
        return 0, 1, [f"Missing server_script or config: {server_script}, {config_path}"]

    kill_port(port)
    proc: Optional[subprocess.Popen[Any]] = None
    errors: List[str] = []
    passed = 0
    failed = 0

    try:
        proc = subprocess.Popen(
            [
                sys.executable,
                str(server_script),
                "--config",
                str(config_path),
                "--port",
                str(port),
            ],
            stdout=subprocess.PIPE,
            stderr=subprocess.STDOUT,
            cwd=str(PROJECT_ROOT),
            text=True,
        )
        if not await wait_health(port):
            out, _ = proc.communicate(timeout=2)
            return 0, 1, [f"Server did not become ready: {(out or '')[:400]}"]

        from mcp_proxy_adapter.client.jsonrpc_client import JsonRpcClient
        from mcp_proxy_adapter.client.jsonrpc_client import open_bidirectional_ws_channel

        client = JsonRpcClient(
            protocol="http",
            host="localhost",
            port=port,
            check_hostname=False,
        )
        base_url = f"http://localhost:{port}"

        # --- Health ---
        try:
            import httpx

            async with httpx.AsyncClient() as http_client:
                r = await http_client.get(f"{base_url}/health", timeout=5.0)
            if r.status_code == 200:
                passed += 1
            else:
                failed += 1
                errors.append(f"health: status {r.status_code}")
        except Exception as e:
            failed += 1
            errors.append(f"health: {e}")

        # --- Echo ---
        try:
            result = await client.execute_command(
                "echo", {"message": "RealServerTest"}
            )
            if result.get("success") and "data" in result:
                passed += 1
            else:
                failed += 1
                errors.append(f"echo: {result}")
        except Exception as e:
            failed += 1
            errors.append(f"echo: {e}")

        # --- Help (no args) ---
        try:
            result = await client.help()
            if result and isinstance(result.get("data"), dict):
                passed += 1
            else:
                failed += 1
                errors.append(f"help: {result}")
        except Exception as e:
            failed += 1
            errors.append(f"help: {e}")

        # --- get_commands_list (GET /commands) ---
        try:
            result = await client.get_commands_list()
            if isinstance(result, dict) and "commands" in result:
                passed += 1
            else:
                failed += 1
                errors.append(f"get_commands_list: {result}")
        except Exception as e:
            failed += 1
            errors.append(f"get_commands_list: {e}")

        # --- Dynamic: load (optional if registry has no loader) ---
        load_ok = False
        with tempfile.TemporaryDirectory(prefix="full_app_test_") as tmpdir:
            dyn_path = make_dynamic_command_file(Path(tmpdir))
            try:
                load_result = await client.execute_command(
                    "load", {"source": str(dyn_path)}
                )
                data = load_result.get("data") or load_result.get("result") or {}
                err = load_result.get("error")
                if load_result.get("success") or data.get("success"):
                    loaded = data.get("loaded_commands", [])
                    if "test_dynamic" in loaded or data.get("commands_loaded", 0) >= 1:
                        passed += 1
                        load_ok = True
                    else:
                        failed += 1
                        errors.append(f"load: no test_dynamic in {data}")
                elif err and "load_command_from_source" in str(err):
                    # Registry has no loader wired - skip dynamic tests
                    pass
                else:
                    failed += 1
                    errors.append(f"load: {load_result}")

                if load_ok:
                    # --- Execute loaded command ---
                    try:
                        exec_result = await client.execute_command(
                            "test_dynamic", {"value": 5}
                        )
                        d = exec_result.get("data") or exec_result.get("result") or {}
                        if exec_result.get("success") and d.get("value") == 10:
                            passed += 1
                        else:
                            failed += 1
                            errors.append(f"test_dynamic execute: {exec_result}")
                    except Exception as e:
                        failed += 1
                        errors.append(f"test_dynamic execute: {e}")

                    # --- Unload (optional if registry has no unload_command) ---
                    try:
                        unload_result = await client.execute_command(
                            "unload", {"command_name": "test_dynamic"}
                        )
                        u = unload_result.get("data") or unload_result.get("result") or {}
                        err = unload_result.get("error")
                        if unload_result.get("success") or u.get("success"):
                            passed += 1
                        elif err and "unload_command" in str(err):
                            pass  # skip
                        else:
                            failed += 1
                            errors.append(f"unload: {unload_result}")
                    except Exception as e:
                        failed += 1
                        errors.append(f"unload: {e}")

            except Exception as e:
                failed += 1
                errors.append(f"load block: {e}")

        # --- Reload ---
        try:
            reload_result = await client.execute_command("reload", {})
            r = reload_result.get("data") or reload_result.get("result") or {}
            if reload_result.get("success") or r.get("success") is not False:
                passed += 1
            else:
                failed += 1
                errors.append(f"reload: {reload_result}")
        except Exception as e:
            failed += 1
            errors.append(f"reload: {e}")

        # --- WebSocket: connect, subscribe, receive, unsubscribe ---
        try:
            channel = open_bidirectional_ws_channel(client, receive_timeout=10.0)
            async with channel:
                await channel.send_json(
                    {"action": "subscribe", "job_id": "real-server-test"}
                )
                try:
                    await asyncio.wait_for(
                        channel.receive_iter().__anext__(), 2.0
                    )
                except asyncio.TimeoutError:
                    pass
                await channel.send_json(
                    {"action": "unsubscribe", "job_id": "real-server-test"}
                )
            passed += 1
        except Exception as e:
            failed += 1
            errors.append(f"ws: {e}")

        # --- Queued command via execute_command_unified (WS wait) ---
        # Use long_running_task (use_queue=True) so job completion is pushed via WS
        try:
            result = await client.execute_command_unified(
                "long_running_task",
                {"duration": 1, "steps": 2},
                auto_poll=True,
                timeout=25.0,
            )
            if result.get("mode") == "queued" and result.get("status"):
                passed += 1
            elif result.get("mode") == "immediate":
                passed += 1
            else:
                failed += 1
                errors.append(f"long_running_task: {result}")
        except Exception as e:
            failed += 1
            errors.append(f"long_running_task: {e}")

        # --- Calculator (custom) ---
        try:
            calc_result = await client.execute_command(
                "calculator",
                {"operation": "add", "a": 2, "b": 3},
            )
            d = calc_result.get("data") or calc_result.get("result") or {}
            res_val = d.get("result") if isinstance(d, dict) else None
            if calc_result.get("success") and res_val == 5:
                passed += 1
            else:
                failed += 1
                errors.append(f"calculator: {calc_result!r}")
        except Exception as e:
            failed += 1
            errors.append(f"calculator: {e!r}")

        # --- embed_queue + embed_job_status ---
        try:
            eq_result = await client.execute_command(
                "embed_queue",
                {"command": "echo", "params": {"message": "embed-test"}},
            )
            eq_data = eq_result.get("data") or eq_result.get("result") or {}
            if isinstance(eq_data, dict):
                job_id = eq_data.get("job_id")
            else:
                job_id = None
            if not job_id and eq_result.get("success"):
                job_id = (eq_result.get("data") or {}).get("job_id")
            if job_id:
                await asyncio.sleep(1.0)  # allow job to complete
                status_result = await client.execute_command(
                    "embed_job_status", {"job_id": job_id}
                )
                s = status_result.get("data") or status_result.get("result") or {}
                if status_result.get("success") or (
                    isinstance(s, dict) and s.get("status")
                ):
                    passed += 1
                else:
                    failed += 1
                    errors.append(f"embed_job_status: {status_result!r}")
            else:
                failed += 1
                errors.append(f"embed_queue: no job_id in {eq_result!r}")
        except Exception as e:
            failed += 1
            errors.append(f"embed_queue/embed_job_status: {e!r}")

    finally:
        if proc is not None:
            proc.terminate()
            try:
                proc.wait(timeout=5)
            except subprocess.TimeoutExpired:
                proc.kill()
                proc.wait()
        kill_port(port)
        await asyncio.sleep(1)

    return passed, failed, errors


def test_full_application_real_server() -> None:
    """Pytest entry: run full application integration test on real server."""
    passed, failed, errors = asyncio.run(run_full_application_tests(FULL_APP_PORT))
    print(f"\n📊 Full application real server: {passed} passed, {failed} failed")
    for err in errors:
        print(f"   ❌ {err}")
    assert failed == 0, f"Failed: {errors}"


if __name__ == "__main__":
    passed, failed, errors = asyncio.run(run_full_application_tests(FULL_APP_PORT))
    print(f"\n📊 Full application real server: {passed} passed, {failed} failed")
    for err in errors:
        print(f"   ❌ {err}")
    sys.exit(0 if failed == 0 else 1)
