"""Live hardware smoke test — requires OpenOCD running on localhost:6666.

Run with: uv run python tests/smoke_hardware.py

This is NOT a pytest test — it hits real hardware and needs a running
OpenOCD instance. It exercises the mcjtag tool stack against a live
CMSIS-DAP probe (target optional).
"""

import asyncio
import sys
from unittest.mock import AsyncMock, MagicMock

# Add src to path for direct execution
sys.path.insert(0, "src")

from openocd.session import Session  # noqa: E402

from mcjtag.mixins.connection import ConnectionMixin  # noqa: E402
from mcjtag.mixins.memory import MemoryMixin  # noqa: E402
from mcjtag.mixins.raw import RawMixin  # noqa: E402
from mcjtag.mixins.registers import RegistersMixin  # noqa: E402
from mcjtag.mixins.target import TargetMixin  # noqa: E402
from mcjtag.state import SessionState  # noqa: E402


def _make_ctx(state: SessionState):
    """Build a fake FastMCP Context carrying real SessionState."""
    ctx = MagicMock()
    ctx.request_context.lifespan_context = state
    ctx.report_progress = AsyncMock()
    return ctx


PASS = "\033[32m✓\033[0m"
FAIL = "\033[31m✗\033[0m"
SKIP = "\033[33m⊘\033[0m"


async def run_test(name: str, coro):
    """Run a single test and print result."""
    try:
        result = await coro
        print(f"  {PASS} {name}")
        return ("pass", name, result)
    except Exception as exc:
        print(f"  {FAIL} {name}: {exc}")
        return ("fail", name, exc)


async def main():
    host = "localhost"
    port = 6666

    print(f"\n{'=' * 60}")
    print("  mcjtag Hardware Smoke Test")
    print(f"  Target: {host}:{port}")
    print(f"{'=' * 60}\n")

    # ── Phase 1: Direct connection via openocd-python ──────────
    print("Phase 1: Direct openocd-python connection")
    try:
        session = await Session.connect(host, port)
        print(f"  {PASS} Session.connect()")
    except Exception as exc:
        print(f"  {FAIL} Session.connect(): {exc}")
        print("\n  Cannot proceed without connection. Is OpenOCD running?")
        return 1

    # ── Phase 2: Raw TCL commands via session ──────────────────
    print("\nPhase 2: Raw TCL commands")

    version = await session.command("version")
    print(f"  {PASS} version: {version.strip()[:60]}")

    adapter_speed = await session.command("adapter speed")
    print(f"  {PASS} adapter speed: {adapter_speed.strip()}")

    transport = await session.command("transport select")
    print(f"  {PASS} transport: {transport.strip()}")

    # ── Phase 3: mcjtag tool layer ─────────────────────────────
    print("\nPhase 3: mcjtag tool layer (via mixins)")
    state = SessionState(session=session)
    results = []

    # Connection mixin — connect when already connected
    mixin_conn = ConnectionMixin()
    r = await run_test(
        "connect (already connected)",
        mixin_conn.connect(_make_ctx(state), host=host, port=port),
    )
    results.append(r)

    # Raw command mixin — safe commands
    mixin_raw = RawMixin()
    r = await run_test(
        "raw_command('version')",
        mixin_raw.raw_command(_make_ctx(state), command="version"),
    )
    results.append(r)
    if r[0] == "pass":
        print(f"      → {r[2].response.strip()[:60]}")

    r = await run_test(
        "raw_command('adapter speed')",
        mixin_raw.raw_command(_make_ctx(state), command="adapter speed"),
    )
    results.append(r)
    if r[0] == "pass":
        print(f"      → {r[2].response.strip()}")

    # Raw command — blocked dangerous command
    r = await run_test(
        "raw_command('shutdown') [should block]",
        mixin_raw.raw_command(_make_ctx(state), command="shutdown"),
    )
    results.append(r)
    if r[0] == "pass" and "Blocked" in r[2].response:
        print(f"      → Correctly blocked: {r[2].response[:50]}...")

    # Raw command — TCL metacommand blocked
    r = await run_test(
        "raw_command('eval ...') [should block]",
        mixin_raw.raw_command(_make_ctx(state), command="eval {version}"),
    )
    results.append(r)
    if r[0] == "pass" and "Blocked" in r[2].response:
        print("      → Correctly blocked")

    # ── Phase 4: Target-dependent tools (expect graceful errors) ──
    print("\nPhase 4: Target-dependent tools (no target connected)")

    # Target state — should error since no target configured
    mixin_target = TargetMixin()
    try:
        result = await mixin_target.target_state(_make_ctx(state))
        print(f"  {PASS} target_state: {result}")
    except Exception as exc:
        err_msg = str(exc)
        if "target" in err_msg.lower() or "error" in err_msg.lower():
            print(f"  {PASS} target_state: graceful error → {err_msg[:60]}")
        else:
            print(f"  {FAIL} target_state: unexpected error → {err_msg[:60]}")

    # Memory read — should error since no target
    mixin_mem = MemoryMixin()
    try:
        result = await mixin_mem.read_memory(_make_ctx(state), address="0x08000000", count=1)
        print(f"  {PASS} read_memory: got {result.values}")
    except Exception as exc:
        err_msg = str(exc)
        print(f"  {PASS} read_memory: graceful error → {err_msg[:60]}")

    # Register read — should error since no target
    mixin_reg = RegistersMixin()
    try:
        result = await mixin_reg.read_registers(_make_ctx(state))
        print(f"  {PASS} read_registers: {result.count} registers")
    except Exception as exc:
        err_msg = str(exc)
        print(f"  {PASS} read_registers: graceful error → {err_msg[:60]}")

    # ── Phase 5: Disconnect ────────────────────────────────────
    print("\nPhase 5: Cleanup")
    mixin_conn = ConnectionMixin()
    r = await run_test(
        "disconnect",
        mixin_conn.disconnect(_make_ctx(state)),
    )
    results.append(r)
    if r[0] == "pass":
        print(f"      → {r[2].message}")

    # ── Summary ────────────────────────────────────────────────
    passed = sum(1 for r in results if r[0] == "pass")
    failed = sum(1 for r in results if r[0] == "fail")
    print(f"\n{'=' * 60}")
    print(f"  Results: {passed} passed, {failed} failed")
    print(f"{'=' * 60}\n")

    return 0 if failed == 0 else 1


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