"""Unit tests for ReadFileTool — G5 (R22, R23) + characterization baseline.

Per plan U1 Execution note: characterization-first — assert that
`symbol=None` returns the full file content (matches pre-existing benchmark
`_FakeTool` shape) before adding symbol-extraction behavior.
"""

from __future__ import annotations

import textwrap

import pytest

from agentkit.tools.file_read import ReadFileTool


# ---------------------------------------------------------------------------
# Schema
# ---------------------------------------------------------------------------


class TestReadFileToolSchema:
    def test_name_is_read_file(self):
        tool = ReadFileTool()
        assert tool.name == "read_file"

    def test_required_path(self):
        tool = ReadFileTool()
        assert "path" in tool.input_schema["required"]
        assert "path" in tool.input_schema["properties"]

    def test_optional_symbol_and_lines(self):
        tool = ReadFileTool()
        props = tool.input_schema["properties"]
        assert "symbol" in props
        assert "start_line" in props
        assert "end_line" in props
        # None of the optional fields should be in `required`.
        required = set(tool.input_schema["required"])
        assert required == {"path"}

    def test_additional_properties_false(self):
        # LLM tool-call schemas should reject unknown args (Wave 1 U3 pattern).
        tool = ReadFileTool()
        assert tool.input_schema.get("additionalProperties") is False

    def test_tags_contain_io_and_read(self):
        tool = ReadFileTool()
        assert "io" in tool.tags
        assert "read" in tool.tags


# ---------------------------------------------------------------------------
# Characterization — symbol=None returns full file
# ---------------------------------------------------------------------------


@pytest.fixture
def sample_py_file(tmp_path):
    path = tmp_path / "sample.py"
    path.write_text(
        textwrap.dedent('''
            """Sample module."""

            def my_func():
                return 42


            class MyClass:
                attr = 1

                def method_a(self):
                    return self.attr
            ''').lstrip(),
        encoding="utf-8",
    )
    return path


@pytest.fixture
def sample_ts_file(tmp_path):
    path = tmp_path / "sample.ts"
    path.write_text(
        textwrap.dedent('''
            export function renderComponent(): JSX.Element {
              return <div/>;
            }

            export class BaseService {
              abstract run(): void;
            }
            ''').lstrip(),
        encoding="utf-8",
    )
    return path


class TestCharacterizationFullFile:
    """symbol=None returns the whole file (matches _FakeTool baseline)."""

    @pytest.mark.asyncio
    async def test_full_file_returned_when_symbol_none(self, sample_py_file):
        tool = ReadFileTool()
        result = await tool.execute(path=str(sample_py_file))

        assert result["is_error"] is False
        assert result["path"] == str(sample_py_file)
        assert result["start_line"] == 1
        assert result["end_line"] == result["total_lines"]
        assert "def my_func" in result["content"]
        assert "class MyClass" in result["content"]
        assert result["content"].startswith('"""Sample module."""')

    @pytest.mark.asyncio
    async def test_full_file_includes_all_lines(self, sample_py_file):
        tool = ReadFileTool()
        result = await tool.execute(path=str(sample_py_file))
        assert result["total_lines"] >= 8
        assert result["content"].count("\n") >= result["total_lines"] - 1


# ---------------------------------------------------------------------------
# Symbol slicing — happy paths
# ---------------------------------------------------------------------------


class TestSymbolSlicing:
    @pytest.mark.asyncio
    async def test_python_function(self, sample_py_file):
        tool = ReadFileTool()
        result = await tool.execute(path=str(sample_py_file), symbol="my_func")

        assert result["is_error"] is False
        assert result["symbol"] == "my_func"
        assert result["symbol_kind"] == "function"
        assert "def my_func" in result["content"]
        assert "return 42" in result["content"]
        # Should NOT include the class below.
        assert "class MyClass" not in result["content"]

    @pytest.mark.asyncio
    async def test_python_class_includes_method(self, sample_py_file):
        tool = ReadFileTool()
        result = await tool.execute(path=str(sample_py_file), symbol="MyClass")

        assert result["is_error"] is False
        assert result["symbol"] == "MyClass"
        assert result["symbol_kind"] == "class"
        assert "class MyClass" in result["content"]
        assert "def method_a" in result["content"]  # method included

    @pytest.mark.asyncio
    async def test_python_method_directly(self, sample_py_file):
        tool = ReadFileTool()
        result = await tool.execute(path=str(sample_py_file), symbol="method_a")

        assert result["is_error"] is False
        assert result["symbol"] == "method_a"
        assert result["symbol_kind"] == "method"
        assert "def method_a" in result["content"]

    @pytest.mark.asyncio
    async def test_typescript_function(self, sample_ts_file):
        tool = ReadFileTool()
        result = await tool.execute(path=str(sample_ts_file), symbol="renderComponent")

        assert result["is_error"] is False
        assert result["symbol"] == "renderComponent"
        assert "renderComponent" in result["content"]
        # Should not include the class below.
        assert "BaseService" not in result["content"]

    @pytest.mark.asyncio
    async def test_typescript_class(self, sample_ts_file):
        tool = ReadFileTool()
        result = await tool.execute(path=str(sample_ts_file), symbol="BaseService")

        assert result["is_error"] is False
        assert result["symbol"] == "BaseService"
        assert result["symbol_kind"] == "class"
        assert "BaseService" in result["content"]


# ---------------------------------------------------------------------------
# Symbol slicing — edge cases
# ---------------------------------------------------------------------------


class TestSymbolSlicingEdgeCases:
    @pytest.mark.asyncio
    async def test_symbol_not_found_lists_available(self, sample_py_file):
        tool = ReadFileTool()
        result = await tool.execute(path=str(sample_py_file), symbol="nonexistent")

        assert result["is_error"] is False  # soft error, not hard
        assert result["content"] == ""
        assert result["symbol"] == "nonexistent"
        available = result["available_symbols"]
        assert "my_func" in available
        assert "MyClass" in available
        assert "method_a" in available
        assert "nonexistent" not in result["content"]

    @pytest.mark.asyncio
    async def test_unsupported_extension_returns_full_with_note(self, tmp_path):
        path = tmp_path / "notes.md"
        path.write_text("# Hello\nworld\n", encoding="utf-8")
        tool = ReadFileTool()
        result = await tool.execute(path=str(path), symbol="anything")

        assert result["is_error"] is False
        assert result["content"] == "# Hello\nworld\n"
        assert "symbol extraction not supported" in result["note"]
        assert ".md" in result["note"]

    @pytest.mark.asyncio
    async def test_empty_file(self, tmp_path):
        path = tmp_path / "empty.py"
        path.write_text("", encoding="utf-8")
        tool = ReadFileTool()
        result = await tool.execute(path=str(path))

        assert result["is_error"] is False
        assert result["content"] == ""
        assert result["total_lines"] == 0

    @pytest.mark.asyncio
    async def test_file_with_no_symbols(self, tmp_path):
        path = tmp_path / "data.py"
        path.write_text("# just a comment\nPI = 3.14\n", encoding="utf-8")
        tool = ReadFileTool()
        result = await tool.execute(path=str(path), symbol="PI")

        # PI is not a def/class — extractor finds no symbols; soft error lists available.
        assert result["is_error"] is False
        assert result["content"] == ""
        assert result["available_symbols"] == []


# ---------------------------------------------------------------------------
# Error paths
# ---------------------------------------------------------------------------


class TestReadFileToolErrors:
    @pytest.mark.asyncio
    async def test_path_required(self):
        tool = ReadFileTool()
        result = await tool.execute()
        assert result["is_error"] is True
        assert "path" in result["error"].lower()

    @pytest.mark.asyncio
    async def test_path_empty_string(self):
        tool = ReadFileTool()
        result = await tool.execute(path="")
        assert result["is_error"] is True

    @pytest.mark.asyncio
    async def test_file_not_found(self, tmp_path):
        tool = ReadFileTool()
        result = await tool.execute(path=str(tmp_path / "missing.py"))
        assert result["is_error"] is True
        assert "not found" in result["error"].lower()

    @pytest.mark.asyncio
    async def test_path_is_directory(self, tmp_path):
        tool = ReadFileTool()
        result = await tool.execute(path=str(tmp_path))
        assert result["is_error"] is True
        assert "directory" in result["error"].lower()


# ---------------------------------------------------------------------------
# Manual line slicing
# ---------------------------------------------------------------------------


class TestManualLineSlicing:
    @pytest.mark.asyncio
    async def test_start_and_end_line(self, sample_py_file):
        tool = ReadFileTool()
        result = await tool.execute(
            path=str(sample_py_file),
            start_line=3,
            end_line=5,
        )
        assert result["is_error"] is False
        assert result["start_line"] == 3
        assert result["end_line"] == 5
        # Lines 3-5 of the sample file:
        # line 3: "def my_func():"
        # line 4: "    return 42"
        # line 5: "" (blank)
        assert "def my_func" in result["content"]
        assert "return 42" in result["content"]

    @pytest.mark.asyncio
    async def test_start_line_only_extends_to_eof(self, sample_py_file):
        tool = ReadFileTool()
        result = await tool.execute(path=str(sample_py_file), start_line=8)
        assert result["is_error"] is False
        assert result["start_line"] == 8
        assert result["end_line"] == result["total_lines"]

    @pytest.mark.asyncio
    async def test_end_line_only_starts_at_one(self, sample_py_file):
        tool = ReadFileTool()
        result = await tool.execute(path=str(sample_py_file), end_line=2)
        assert result["is_error"] is False
        assert result["start_line"] == 1
        assert result["end_line"] == 2

    @pytest.mark.asyncio
    async def test_invalid_start_line_zero(self, sample_py_file):
        tool = ReadFileTool()
        result = await tool.execute(path=str(sample_py_file), start_line=0)
        assert result["is_error"] is True
        assert "start_line" in result["error"].lower()

    @pytest.mark.asyncio
    async def test_end_before_start(self, sample_py_file):
        tool = ReadFileTool()
        result = await tool.execute(
            path=str(sample_py_file),
            start_line=5,
            end_line=3,
        )
        assert result["is_error"] is True
        assert "end_line" in result["error"].lower()

    @pytest.mark.asyncio
    async def test_manual_lines_override_symbol(self, sample_py_file):
        # Per plan U1 Approach: "start_line/end_line overrides symbol".
        tool = ReadFileTool()
        result = await tool.execute(
            path=str(sample_py_file),
            symbol="my_func",
            start_line=1,
            end_line=1,
        )
        assert result["is_error"] is False
        # Manual slicing won — symbol field absent.
        assert "symbol" not in result or result.get("symbol") is None
        assert result["start_line"] == 1
        assert result["end_line"] == 1


# ---------------------------------------------------------------------------
# Integration — tool registry discovery
# ---------------------------------------------------------------------------


class TestToolRegistryDiscovery:
    def test_instantiable_without_args(self):
        # Default constructor — matches the convention used by ToolRegistry
        # to instantiate tools by class.
        tool = ReadFileTool()
        assert tool.name == "read_file"

    def test_to_dict_serializable(self):
        tool = ReadFileTool()
        d = tool.to_dict()
        assert d["name"] == "read_file"
        assert "input_schema" in d
        assert "output_schema" in d
        assert d["tags"] == ["io", "file", "read"]
