"""Unit tests for timmy.tools — coverage gaps.

Tests _make_smart_read_file, _safe_eval edge cases, consult_grok,
_create_stub_toolkit, get_tools_for_agent, and AiderTool edge cases.
"""

from __future__ import annotations

import ast
import math
from unittest.mock import MagicMock, patch

import pytest

from timmy.tools import (
    _create_stub_toolkit,
    _safe_eval,
    consult_grok,
    create_aider_tool,
    get_tools_for_agent,
)

# ── _safe_eval edge cases ─────────────────────────────────────────────────────


class TestSafeEval:
    """Edge cases for the AST-based safe evaluator."""

    def _eval(self, expr: str):
        allowed = {k: getattr(math, k) for k in dir(math) if not k.startswith("_")}
        allowed["math"] = math
        allowed["abs"] = abs
        allowed["round"] = round
        allowed["min"] = min
        allowed["max"] = max
        tree = ast.parse(expr, mode="eval")
        return _safe_eval(tree, allowed)

    def test_unsupported_constant_type(self):
        """String constants should be rejected."""
        with pytest.raises(ValueError, match="Unsupported constant"):
            self._eval("'hello'")

    def test_unsupported_binary_op(self):
        """Bitwise ops are not in the allowlist."""
        with pytest.raises(ValueError, match="Unsupported"):
            self._eval("3 & 5")

    def test_unsupported_unary_op(self):
        """Bitwise inversion is not supported."""
        with pytest.raises(ValueError, match="Unsupported"):
            self._eval("~5")

    def test_unknown_name(self):
        with pytest.raises(ValueError, match="Unknown name"):
            self._eval("foo")

    def test_attribute_on_non_math(self):
        """Attribute access on anything except the math module is blocked."""
        with pytest.raises(ValueError, match="Attribute access not allowed"):
            self._eval("abs.__class__")

    def test_call_non_callable(self):
        """Calling a non-callable (like a number) should fail."""
        with pytest.raises((ValueError, TypeError)):
            self._eval("(42)()")

    def test_unsupported_syntax_subscript(self):
        """Subscript syntax (a[0]) is not supported."""
        with pytest.raises(ValueError, match="Unsupported syntax"):
            self._eval("[1, 2][0]")

    def test_kwargs_in_call(self):
        """math.log with keyword arg should work through the evaluator."""
        result = self._eval("round(3.14159)")
        assert result == 3

    def test_math_attr_valid(self):
        """Accessing a valid math attribute should work."""
        result = self._eval("math.pi")
        assert result == math.pi

    def test_math_attr_invalid(self):
        """Accessing a nonexistent math attribute should fail."""
        with pytest.raises(ValueError, match="Attribute access not allowed"):
            self._eval("math.__builtins__")


# ── _make_smart_read_file ─────────────────────────────────────────────────────


class TestMakeSmartReadFile:
    """Test the smart_read_file wrapper for directory detection."""

    def test_directory_returns_listing(self, tmp_path):
        """When given a directory, should list its contents."""
        (tmp_path / "alpha.txt").touch()
        (tmp_path / "beta.py").touch()
        (tmp_path / ".hidden").touch()  # should be excluded

        from timmy.tools import _make_smart_read_file

        file_tools = MagicMock()
        file_tools.check_escape.return_value = (True, tmp_path)
        smart_read = _make_smart_read_file(file_tools)

        result = smart_read(file_name=str(tmp_path))
        assert "is a directory" in result
        assert "alpha.txt" in result
        assert "beta.py" in result
        assert ".hidden" not in result

    def test_empty_directory(self, tmp_path):
        """Empty directory should show placeholder."""
        from timmy.tools import _make_smart_read_file

        empty_dir = tmp_path / "empty"
        empty_dir.mkdir()

        file_tools = MagicMock()
        file_tools.check_escape.return_value = (True, empty_dir)
        smart_read = _make_smart_read_file(file_tools)

        result = smart_read(file_name=str(empty_dir))
        assert "empty directory" in result

    def test_no_file_name_uses_path_kwarg(self):
        """When file_name is empty, should fall back to path= kwarg."""
        from timmy.tools import _make_smart_read_file

        file_tools = MagicMock()
        file_tools.check_escape.return_value = (True, MagicMock(is_dir=lambda: False))
        file_tools.read_file.return_value = "file content"
        smart_read = _make_smart_read_file(file_tools)

        smart_read(path="/some/file.txt")
        file_tools.read_file.assert_called_once()

    def test_no_file_name_no_path(self):
        """When neither file_name nor path is given, return error."""
        from timmy.tools import _make_smart_read_file

        file_tools = MagicMock()
        smart_read = _make_smart_read_file(file_tools)

        result = smart_read()
        assert "Error" in result

    def test_file_delegates_to_original(self, tmp_path):
        """Regular files should delegate to original read_file."""
        from timmy.tools import _make_smart_read_file

        f = tmp_path / "hello.txt"
        f.write_text("hello world")

        file_tools = MagicMock()
        file_tools.check_escape.return_value = (True, f)
        file_tools.read_file.return_value = "hello world"
        smart_read = _make_smart_read_file(file_tools)

        result = smart_read(file_name=str(f))
        assert result == "hello world"

    def test_preserves_docstring(self):
        """smart_read_file should copy the original's docstring."""
        from timmy.tools import _make_smart_read_file

        file_tools = MagicMock()
        file_tools.read_file.__doc__ = "Original docstring."
        smart_read = _make_smart_read_file(file_tools)
        assert smart_read.__doc__ == "Original docstring."


# ── consult_grok ──────────────────────────────────────────────────────────────


class TestConsultGrok:
    """Test the Grok consultation tool."""

    @patch("timmy.tools.settings")
    def test_grok_unavailable(self, mock_settings):
        """When Grok is disabled, should return a helpful message."""
        with patch("timmy.backends.grok_available", return_value=False):
            result = consult_grok("What is 2+2?")
        assert "not available" in result.lower()


# ── _create_stub_toolkit ──────────────────────────────────────────────────────


class TestCreateStubToolkit:
    """Test stub toolkit creation for creative agents."""

    def test_stub_has_correct_name(self):
        toolkit = _create_stub_toolkit("pixel")
        if toolkit is None:
            pytest.skip("Agno tools not available")
        assert toolkit.name == "pixel"

    def test_stub_for_different_agent(self):
        toolkit = _create_stub_toolkit("lyra")
        if toolkit is None:
            pytest.skip("Agno tools not available")
        assert toolkit.name == "lyra"


# ── get_tools_for_agent ───────────────────────────────────────────────────────


class TestGetToolsForAgent:
    """Test get_tools_for_agent (not just the alias)."""

    def test_known_agent_returns_toolkit(self):
        result = get_tools_for_agent("echo")
        assert result is not None

    def test_unknown_agent_returns_none(self):
        result = get_tools_for_agent("nonexistent")
        assert result is None

    def test_custom_base_dir(self, tmp_path):
        result = get_tools_for_agent("echo", base_dir=tmp_path)
        assert result is not None


# ── AiderTool edge cases ─────────────────────────────────────────────────────


class TestAiderToolEdgeCases:
    """Additional edge cases for the AiderTool."""

    @patch("subprocess.run")
    def test_aider_success_empty_stdout(self, mock_run, tmp_path):
        """When stdout is empty, should return fallback message."""
        mock_run.return_value = MagicMock(returncode=0, stdout="")
        tool = create_aider_tool(tmp_path)
        result = tool.run_aider("do something")
        assert "successfully" in result.lower()

    @patch("subprocess.run")
    def test_aider_custom_model(self, mock_run, tmp_path):
        """Custom model parameter should be passed to subprocess."""
        mock_run.return_value = MagicMock(returncode=0, stdout="done")
        tool = create_aider_tool(tmp_path)
        tool.run_aider("task", model="deepseek-coder:6.7b")
        args = mock_run.call_args[0][0]
        assert "ollama/deepseek-coder:6.7b" in args

    @patch("subprocess.run")
    def test_aider_os_error(self, mock_run, tmp_path):
        """OSError should be caught gracefully."""
        mock_run.side_effect = OSError("Permission denied")
        tool = create_aider_tool(tmp_path)
        result = tool.run_aider("task")
        assert "error" in result.lower()
