"""Tests for Phase 81: Remote Execution."""

import json
import pytest
from pathlib import Path

import brynq.remote_execution as re_mod


@pytest.fixture(autouse=True)
def _isolate(tmp_path, monkeypatch):
    """Redirect storage to tmp_path and reset in-memory state."""
    exec_dir = tmp_path / "remote_exec"
    nodes_dir = exec_dir / "nodes"
    nodes_dir.mkdir(parents=True, exist_ok=True)

    monkeypatch.setattr(re_mod, "EXEC_DIR", exec_dir)
    monkeypatch.setattr(re_mod, "NODES_DIR", nodes_dir)
    monkeypatch.setattr(re_mod, "LOG_FILE", exec_dir / "log.jsonl")
    re_mod.reset()


# ── register_node ──────────────────────────────────────────────────────────

class TestRegisterNode:
    def test_register_basic(self):
        node = re_mod.register_node("n1", ["llm", "search"])
        assert node["node_id"] == "n1"
        assert "llm" in node["capabilities"]
        assert node["status"] == "online"

    def test_register_with_endpoint(self):
        node = re_mod.register_node("n2", ["code"], endpoint_url="http://10.0.0.1:8080")
        assert node["endpoint_url"] == "http://10.0.0.1:8080"

    def test_register_with_tools(self):
        tools = {"search": lambda a: "found", "summarize": lambda a: "summary"}
        node = re_mod.register_node("n3", ["llm"], tools=tools)
        assert "search" in node["tools"]
        assert "summarize" in node["tools"]

    def test_register_overwrites(self):
        re_mod.register_node("n1", ["a"])
        re_mod.register_node("n1", ["b", "c"])
        node = re_mod.get_node("n1")
        assert node["capabilities"] == ["b", "c"]

    def test_register_persists(self, tmp_path):
        re_mod.register_node("persist", ["x"])
        f = tmp_path / "remote_exec" / "nodes" / "persist.json"
        assert f.exists()


# ── list_nodes ─────────────────────────────────────────────────────────────

class TestListNodes:
    def test_list_all(self):
        re_mod.register_node("a", ["llm"])
        re_mod.register_node("b", ["code"])
        nodes = re_mod.list_nodes()
        assert len(nodes) == 2

    def test_list_by_capability(self):
        re_mod.register_node("a", ["llm", "search"])
        re_mod.register_node("b", ["code"])
        assert len(re_mod.list_nodes(capability="llm")) == 1
        assert len(re_mod.list_nodes(capability="code")) == 1
        assert len(re_mod.list_nodes(capability="missing")) == 0

    def test_list_empty(self):
        assert re_mod.list_nodes() == []


# ── get_node / remove_node ─────────────────────────────────────────────────

class TestGetAndRemove:
    def test_get_existing(self):
        re_mod.register_node("x", ["a"])
        node = re_mod.get_node("x")
        assert node["node_id"] == "x"

    def test_get_missing(self):
        assert re_mod.get_node("ghost") is None

    def test_remove_existing(self):
        re_mod.register_node("rm", ["a"])
        assert re_mod.remove_node("rm") is True
        assert re_mod.get_node("rm") is None

    def test_remove_missing(self):
        assert re_mod.remove_node("nope") is False


# ── execute_remote ─────────────────────────────────────────────────────────

class TestExecuteRemote:
    def test_execute_success(self):
        tools = {"echo": lambda a: a.get("msg", "default")}
        re_mod.register_node("n1", ["util"], tools=tools)
        result = re_mod.execute_remote("n1", "echo", {"msg": "hello"})
        assert result["ok"] is True
        assert result["result"] == "hello"
        assert "exec_id" in result

    def test_execute_no_args(self):
        tools = {"ping": lambda a: "pong"}
        re_mod.register_node("n1", ["util"], tools=tools)
        result = re_mod.execute_remote("n1", "ping")
        assert result["ok"] is True
        assert result["result"] == "pong"

    def test_execute_error_isolated(self):
        tools = {"bad": lambda a: 1 / 0}
        re_mod.register_node("n1", ["util"], tools=tools)
        result = re_mod.execute_remote("n1", "bad")
        assert result["ok"] is False
        assert "division" in result["error"]

    def test_execute_node_not_found(self):
        result = re_mod.execute_remote("ghost", "tool")
        assert result["ok"] is False
        assert "not found" in result["error"]

    def test_execute_tool_not_on_node(self):
        re_mod.register_node("n1", ["util"])
        result = re_mod.execute_remote("n1", "missing_tool")
        assert result["ok"] is False
        assert "not available" in result["error"]

    def test_execute_offline_node(self, tmp_path):
        re_mod.register_node("n1", ["util"])
        # Manually set node offline
        f = tmp_path / "remote_exec" / "nodes" / "n1.json"
        data = json.loads(f.read_text("utf-8"))
        data["status"] = "offline"
        f.write_text(json.dumps(data))
        result = re_mod.execute_remote("n1", "tool")
        assert result["ok"] is False
        assert "offline" in result["error"]

    def test_execute_logs_to_file(self, tmp_path):
        tools = {"t": lambda a: "ok"}
        re_mod.register_node("n1", ["x"], tools=tools)
        re_mod.execute_remote("n1", "t")
        log_file = tmp_path / "remote_exec" / "log.jsonl"
        assert log_file.exists()
        entries = log_file.read_text("utf-8").strip().splitlines()
        assert len(entries) >= 1


# ── find_node_for_tool ─────────────────────────────────────────────────────

class TestFindNode:
    def test_find_existing(self):
        tools = {"search": lambda a: []}
        re_mod.register_node("n1", ["util"], tools=tools)
        found = re_mod.find_node_for_tool("search")
        assert found is not None
        assert found["node_id"] == "n1"

    def test_find_missing(self):
        re_mod.register_node("n1", ["util"])
        assert re_mod.find_node_for_tool("nonexistent") is None

    def test_find_prefers_online(self, tmp_path):
        tools_a = {"search": lambda a: "a"}
        tools_b = {"search": lambda a: "b"}
        re_mod.register_node("a", ["util"], tools=tools_a)
        re_mod.register_node("b", ["util"], tools=tools_b)
        # Set first node offline
        f = tmp_path / "remote_exec" / "nodes" / "a.json"
        data = json.loads(f.read_text("utf-8"))
        data["status"] = "offline"
        f.write_text(json.dumps(data))
        found = re_mod.find_node_for_tool("search")
        assert found["node_id"] == "b"


# ── get_execution_log ──────────────────────────────────────────────────────

class TestExecutionLog:
    def test_log_entries(self):
        tools = {"t": lambda a: "ok"}
        re_mod.register_node("n1", ["x"], tools=tools)
        re_mod.execute_remote("n1", "t")
        re_mod.execute_remote("n1", "t")
        log = re_mod.get_execution_log()
        assert len(log) == 2

    def test_log_filter_by_node(self):
        tools = {"t": lambda a: "ok"}
        re_mod.register_node("n1", ["x"], tools=tools)
        re_mod.register_node("n2", ["x"], tools=tools)
        re_mod.execute_remote("n1", "t")
        re_mod.execute_remote("n2", "t")
        assert len(re_mod.get_execution_log(node_id="n1")) == 1

    def test_log_limit(self):
        tools = {"t": lambda a: "ok"}
        re_mod.register_node("n1", ["x"], tools=tools)
        for _ in range(10):
            re_mod.execute_remote("n1", "t")
        assert len(re_mod.get_execution_log(limit=3)) == 3

    def test_log_empty(self):
        assert re_mod.get_execution_log() == []


# ── ping_node ──────────────────────────────────────────────────────────────

class TestPingNode:
    def test_ping_existing(self):
        re_mod.register_node("n1", ["x"])
        result = re_mod.ping_node("n1")
        assert result["ok"] is True
        assert result["status"] == "online"

    def test_ping_missing(self):
        result = re_mod.ping_node("ghost")
        assert result["ok"] is False

    def test_ping_updates_timestamp(self):
        re_mod.register_node("n1", ["x"])
        before = re_mod.get_node("n1")["last_ping"]
        re_mod.ping_node("n1")
        after = re_mod.get_node("n1")["last_ping"]
        assert after >= before
