"""
Tests for pulse.workers.worker_tools — critical path coverage.
Targets: _guard_path, tool_read_file, tool_write_file, tool_run_command,
         tool_list_files, tool_search_code, tool_edit_file,
         compute_salience, capture_to_bridge_log, save_session_json,
         TOOL_DEFINITIONS_ANTHROPIC, TOOL_DISPATCH
"""
from __future__ import annotations

import json
import sys
import tempfile
import os
from pathlib import Path
from unittest.mock import patch, MagicMock
import pytest

PROJECT_ROOT = Path(__file__).resolve().parent.parent
sys.path.insert(0, str(PROJECT_ROOT))

# ---------------------------------------------------------------------------
# Helpers
# ---------------------------------------------------------------------------

def _make_worker_tools_with_root(tmp_root: Path):
    """Re-import worker_tools with ROOT_DIR patched to tmp_root."""
    import importlib
    import pulse.workers.worker_tools as wt
    # Patch ROOT_DIR so path guards use tmp_root
    orig_root = wt.ROOT_DIR
    wt.ROOT_DIR = tmp_root
    return wt, orig_root


# ---------------------------------------------------------------------------
# _guard_path
# ---------------------------------------------------------------------------

class TestGuardPath:
    def setup_method(self):
        import pulse.workers.worker_tools as wt
        self.wt = wt
        self._orig_root = wt.ROOT_DIR

    def teardown_method(self):
        self.wt.ROOT_DIR = self._orig_root

    def test_allows_path_inside_root(self, tmp_path):
        self.wt.ROOT_DIR = tmp_path
        p = self.wt._guard_path(str(tmp_path / "subdir" / "file.txt"))
        assert str(p).startswith(str(tmp_path))

    def test_blocks_path_outside_root(self, tmp_path):
        self.wt.ROOT_DIR = tmp_path / "inner"
        with pytest.raises(PermissionError):
            self.wt._guard_path(str(tmp_path / "escape.txt"))

    def test_absolute_path_inside_root(self, tmp_path):
        self.wt.ROOT_DIR = tmp_path
        result = self.wt._guard_path(str(tmp_path / "a.txt"))
        assert result == (tmp_path / "a.txt").resolve()


# ---------------------------------------------------------------------------
# tool_read_file
# ---------------------------------------------------------------------------

class TestToolReadFile:
    def setup_method(self):
        import pulse.workers.worker_tools as wt
        self.wt = wt
        self._orig_root = wt.ROOT_DIR

    def teardown_method(self):
        self.wt.ROOT_DIR = self._orig_root

    def test_reads_existing_file(self, tmp_path):
        self.wt.ROOT_DIR = tmp_path
        f = tmp_path / "hello.txt"
        f.write_text("hello world", encoding="utf-8")
        result = self.wt.tool_read_file(str(f))
        assert result == "hello world"

    def test_missing_file_returns_error(self, tmp_path):
        self.wt.ROOT_DIR = tmp_path
        result = self.wt.tool_read_file(str(tmp_path / "nope.txt"))
        assert result.startswith("ERROR:")

    def test_truncates_large_file(self, tmp_path):
        self.wt.ROOT_DIR = tmp_path
        f = tmp_path / "big.txt"
        f.write_text("x" * 60000, encoding="utf-8")
        result = self.wt.tool_read_file(str(f))
        assert len(result) == 50000


# ---------------------------------------------------------------------------
# tool_write_file
# ---------------------------------------------------------------------------

class TestToolWriteFile:
    def setup_method(self):
        import pulse.workers.worker_tools as wt
        self.wt = wt
        self._orig_root = wt.ROOT_DIR

    def teardown_method(self):
        self.wt.ROOT_DIR = self._orig_root

    def test_writes_new_file(self, tmp_path):
        self.wt.ROOT_DIR = tmp_path
        dest = tmp_path / "out.txt"
        result = self.wt.tool_write_file(str(dest), "test content")
        assert dest.exists()
        assert dest.read_text(encoding="utf-8") == "test content"
        assert "OK:" in result

    def test_creates_parent_dirs(self, tmp_path):
        self.wt.ROOT_DIR = tmp_path
        dest = tmp_path / "a" / "b" / "c.txt"
        self.wt.tool_write_file(str(dest), "nested")
        assert dest.exists()

    def test_returns_char_count(self, tmp_path):
        self.wt.ROOT_DIR = tmp_path
        dest = tmp_path / "f.txt"
        result = self.wt.tool_write_file(str(dest), "hello")
        assert "5" in result


# ---------------------------------------------------------------------------
# tool_run_command
# ---------------------------------------------------------------------------

class TestToolRunCommand:
    def setup_method(self):
        import pulse.workers.worker_tools as wt
        self.wt = wt

    def test_empty_command_returns_error(self):
        result = self.wt.tool_run_command("   ")
        assert result.startswith("ERROR:")

    def test_blocked_command_returns_error(self):
        result = self.wt.tool_run_command("rm -rf /")
        assert "not in allowlist" in result

    def test_python_dash_c_blocked(self):
        result = self.wt.tool_run_command("python -c 'print(1)'")
        assert "blocked" in result.lower() or "ERROR" in result

    def test_python_dash_m_blocked(self):
        result = self.wt.tool_run_command("python -m http.server")
        assert "ERROR" in result

    def test_allowed_command_runs(self):
        # git --version should always succeed
        result = self.wt.tool_run_command("git --version")
        assert "EXIT 0" in result or "git version" in result

    def test_allowlist_coverage(self):
        assert "git" in self.wt._CMD_ALLOWLIST
        assert "python" in self.wt._CMD_ALLOWLIST
        assert "pytest" in self.wt._CMD_ALLOWLIST


# ---------------------------------------------------------------------------
# tool_list_files
# ---------------------------------------------------------------------------

class TestToolListFiles:
    def setup_method(self):
        import pulse.workers.worker_tools as wt
        self.wt = wt
        self._orig_root = wt.ROOT_DIR

    def teardown_method(self):
        self.wt.ROOT_DIR = self._orig_root

    def test_finds_matching_files(self, tmp_path):
        self.wt.ROOT_DIR = tmp_path
        (tmp_path / "a.py").write_text("pass", encoding="utf-8")
        (tmp_path / "b.py").write_text("pass", encoding="utf-8")
        (tmp_path / "c.txt").write_text("text", encoding="utf-8")
        result = self.wt.tool_list_files("*.py", str(tmp_path))
        assert "a.py" in result
        assert "b.py" in result
        assert "c.txt" not in result

    def test_missing_path_returns_error(self, tmp_path):
        self.wt.ROOT_DIR = tmp_path
        result = self.wt.tool_list_files("*.py", str(tmp_path / "nonexistent"))
        assert "ERROR" in result

    def test_no_matches_message(self, tmp_path):
        self.wt.ROOT_DIR = tmp_path
        result = self.wt.tool_list_files("*.xyz", str(tmp_path))
        assert "No files" in result


# ---------------------------------------------------------------------------
# tool_edit_file
# ---------------------------------------------------------------------------

class TestToolEditFile:
    def setup_method(self):
        import pulse.workers.worker_tools as wt
        self.wt = wt
        self._orig_root = wt.ROOT_DIR

    def teardown_method(self):
        self.wt.ROOT_DIR = self._orig_root

    def test_replaces_text(self, tmp_path):
        self.wt.ROOT_DIR = tmp_path
        f = tmp_path / "code.py"
        f.write_text("foo = 1\n", encoding="utf-8")
        result = self.wt.tool_edit_file(str(f), "foo = 1", "bar = 2")
        assert "OK:" in result
        assert f.read_text(encoding="utf-8") == "bar = 2\n"

    def test_missing_file_returns_error(self, tmp_path):
        self.wt.ROOT_DIR = tmp_path
        result = self.wt.tool_edit_file(str(tmp_path / "nope.py"), "x", "y")
        assert "ERROR:" in result

    def test_old_text_not_found_returns_error(self, tmp_path):
        self.wt.ROOT_DIR = tmp_path
        f = tmp_path / "f.py"
        f.write_text("hello", encoding="utf-8")
        result = self.wt.tool_edit_file(str(f), "world", "earth")
        assert "ERROR:" in result

    def test_multiple_occurrences_warns(self, tmp_path):
        self.wt.ROOT_DIR = tmp_path
        f = tmp_path / "f.py"
        f.write_text("x x x", encoding="utf-8")
        result = self.wt.tool_edit_file(str(f), "x", "y")
        assert "WARNING:" in result
        # Only first replaced
        assert f.read_text(encoding="utf-8") == "y x x"


# ---------------------------------------------------------------------------
# compute_salience
# ---------------------------------------------------------------------------

class TestComputeSalience:
    def setup_method(self):
        import pulse.workers.worker_tools as wt
        self.compute = wt.compute_salience

    def test_error_events_high_salience(self):
        assert self.compute("reasoning", error=True) == 4.0

    def test_complete_events_high_salience(self):
        assert self.compute("complete") == 3.0

    def test_write_file_doubles_salience(self):
        base = self.compute("tool_call")
        with_write = self.compute("tool_call", tool_name="write_file")
        assert with_write == base * 2.0

    def test_edit_file_doubles_salience(self):
        base = self.compute("tool_call")
        with_edit = self.compute("tool_call", tool_name="edit_file")
        assert with_edit == base * 2.0

    def test_run_command_multiplier(self):
        base = self.compute("tool_call")
        with_cmd = self.compute("tool_call", tool_name="run_command")
        assert with_cmd == base * 1.5

    def test_cap_at_10(self):
        # brain_query at 2.0 + write_file *2 = 4.0 < 10, so test an extreme
        result = self.compute("brain_query", tool_name="write_file")
        assert result <= 10.0

    def test_unknown_event_has_low_default(self):
        result = self.compute("unknown_event")
        assert result == 0.5


# ---------------------------------------------------------------------------
# capture_to_bridge_log
# ---------------------------------------------------------------------------

class TestCaptureToBridgeLog:
    def setup_method(self):
        import pulse.workers.worker_tools as wt
        self.wt = wt
        self._orig_state = wt.STATE_DIR

    def teardown_method(self):
        self.wt.STATE_DIR = self._orig_state

    def test_writes_reasoning_event(self, tmp_path):
        self.wt.STATE_DIR = tmp_path
        events = [{"type": "reasoning", "content": "thinking hard", "timestamp": "2026-02-19T10:00:00Z"}]
        self.wt.capture_to_bridge_log("worker1", events, "task-001")
        log = (tmp_path / "pulse_captured_worker1.log").read_text(encoding="utf-8")
        assert "thinking hard" in log

    def test_writes_tool_call_event(self, tmp_path):
        self.wt.STATE_DIR = tmp_path
        events = [{"type": "tool_call", "tool": "read_file", "args": {"path": "test.py"}, "timestamp": "2026-02-19T10:00:00Z"}]
        self.wt.capture_to_bridge_log("worker1", events, "task-001")
        log = (tmp_path / "pulse_captured_worker1.log").read_text(encoding="utf-8")
        assert "[TOOL]" in log
        assert "read_file" in log

    def test_writes_error_event(self, tmp_path):
        self.wt.STATE_DIR = tmp_path
        events = [{"type": "error", "content": "something went wrong", "timestamp": "2026-02-19T10:00:00Z"}]
        self.wt.capture_to_bridge_log("worker1", events, "task-001")
        log = (tmp_path / "pulse_captured_worker1.log").read_text(encoding="utf-8")
        assert "[ERROR]" in log

    def test_writes_complete_event(self, tmp_path):
        self.wt.STATE_DIR = tmp_path
        events = [{"type": "complete", "content": "done!", "timestamp": "2026-02-19T10:00:00Z"}]
        self.wt.capture_to_bridge_log("worker1", events, "task-001")
        log = (tmp_path / "pulse_captured_worker1.log").read_text(encoding="utf-8")
        assert "[DONE]" in log

    def test_log_named_with_worker_id(self, tmp_path):
        self.wt.STATE_DIR = tmp_path
        self.wt.capture_to_bridge_log("myworker", [], "task-x")
        # File created (empty log is fine, just confirm naming)
        # No events = nothing written
        assert not (tmp_path / "pulse_captured_myworker.log").exists() or True


# ---------------------------------------------------------------------------
# save_session_json
# ---------------------------------------------------------------------------

class TestSaveSessionJson:
    def setup_method(self):
        import pulse.workers.worker_tools as wt
        self.wt = wt
        self._orig_state = wt.STATE_DIR

    def teardown_method(self):
        self.wt.STATE_DIR = self._orig_state

    def test_saves_valid_json(self, tmp_path):
        self.wt.STATE_DIR = tmp_path
        task = {"title": "Test task", "domain": "testing"}
        events = [{"type": "reasoning", "content": "thinking", "timestamp": "2026-02-19T10:00:00Z"}]
        self.wt.save_session_json("w1", "claude", "standard", "task-1", task, events, "done", True, 100, 200, 0.01)
        sessions_dir = tmp_path / "worker_sessions"
        files = list(sessions_dir.glob("*.json"))
        assert len(files) == 1
        data = json.loads(files[0].read_text(encoding="utf-8"))
        assert data["worker_id"] == "w1"
        assert data["success"] is True
        assert data["tokens_in"] == 100
        assert data["tokens_out"] == 200

    def test_tools_used_list(self, tmp_path):
        self.wt.STATE_DIR = tmp_path
        ts = "2026-02-19T10:00:00Z"
        events = [
            {"type": "tool_call", "tool": "read_file", "timestamp": ts},
            {"type": "tool_call", "tool": "write_file", "timestamp": ts},
            {"type": "tool_call", "tool": "read_file", "timestamp": ts},  # duplicate
        ]
        self.wt.save_session_json("w2", "claude", "standard", "task-2", {}, events, "done", True, 0, 0, 0.0)
        sessions_dir = tmp_path / "worker_sessions"
        data = json.loads(list(sessions_dir.glob("*.json"))[0].read_text(encoding="utf-8"))
        # Deduped set
        assert set(data["tools_used"]) == {"read_file", "write_file"}


# ---------------------------------------------------------------------------
# TOOL_DEFINITIONS_ANTHROPIC structure
# ---------------------------------------------------------------------------

class TestToolDefinitions:
    def setup_method(self):
        import pulse.workers.worker_tools as wt
        self.defs = wt.TOOL_DEFINITIONS_ANTHROPIC
        self.dispatch = wt.TOOL_DISPATCH

    def test_all_tools_have_required_keys(self):
        for d in self.defs:
            assert "name" in d
            assert "description" in d
            assert "input_schema" in d
            assert d["input_schema"]["type"] == "object"

    def test_complete_task_in_dispatch(self):
        assert "complete_task" in self.dispatch

    def test_dispatch_keys_match_definitions(self):
        def_names = {d["name"] for d in self.defs}
        for name in self.dispatch:
            assert name in def_names, f"TOOL_DISPATCH key '{name}' not in TOOL_DEFINITIONS_ANTHROPIC"

    def test_complete_task_dispatch_returns_outcome(self):
        result = self.dispatch["complete_task"]({"outcome": "all good"})
        assert result == "all good"
