import asyncio
import logging
import os
import subprocess
from pathlib import Path

import pytest

from gjalla_precommit.tools import executor as executor_module
from gjalla_precommit.tools import file_tools, git_tools, search_tools


# =============================================================================
# Tests for read_file_chunked tool
# =============================================================================


@pytest.mark.asyncio
async def test_read_file_chunked_basic(temp_repo: Path):
    """Test reading a chunk of lines from a file."""
    test_file = temp_repo / "test.txt"
    lines = [f"Line {i}" for i in range(1, 101)]
    test_file.write_text("\n".join(lines), encoding="utf-8")

    result = await file_tools.read_file_chunked(
        path=str(test_file),
        repo_root=temp_repo,
        start_line=10,
        max_lines=5,
    )

    assert result.success
    assert "Line 10" in result.data
    assert "Line 14" in result.data
    assert "Lines 10-14 of 100" in result.data


@pytest.mark.asyncio
async def test_read_file_chunked_first_chunk(temp_repo: Path):
    """Test reading from the beginning of a file."""
    test_file = temp_repo / "test.txt"
    lines = [f"Line {i}" for i in range(1, 51)]
    test_file.write_text("\n".join(lines), encoding="utf-8")

    result = await file_tools.read_file_chunked(
        path=str(test_file),
        repo_root=temp_repo,
        start_line=1,
        max_lines=10,
    )

    assert result.success
    assert "Line 1" in result.data
    assert "Lines 1-10 of 50" in result.data
    # Should have "Next chunk" hint but no "Previous chunk"
    assert "Next chunk" in result.data
    assert "Previous chunk" not in result.data


@pytest.mark.asyncio
async def test_read_file_chunked_last_chunk(temp_repo: Path):
    """Test reading the last chunk of a file."""
    test_file = temp_repo / "test.txt"
    lines = [f"Line {i}" for i in range(1, 51)]
    test_file.write_text("\n".join(lines), encoding="utf-8")

    result = await file_tools.read_file_chunked(
        path=str(test_file),
        repo_root=temp_repo,
        start_line=45,
        max_lines=10,
    )

    assert result.success
    assert "Line 50" in result.data
    # Should have "Previous chunk" hint but no "Next chunk"
    assert "Previous chunk" in result.data
    assert "Next chunk" not in result.data


@pytest.mark.asyncio
async def test_read_file_chunked_includes_line_numbers(temp_repo: Path):
    """Test that chunked read includes line numbers in output."""
    test_file = temp_repo / "test.txt"
    lines = ["Hello", "World", "Test"]
    test_file.write_text("\n".join(lines), encoding="utf-8")

    result = await file_tools.read_file_chunked(
        path=str(test_file),
        repo_root=temp_repo,
        start_line=1,
        max_lines=10,
    )

    assert result.success
    # Line numbers should be formatted with |
    assert "1|" in result.data or "     1|" in result.data


@pytest.mark.asyncio
async def test_read_file_chunked_start_beyond_file(temp_repo: Path):
    """Test error when start_line is beyond file length."""
    test_file = temp_repo / "test.txt"
    test_file.write_text("Only one line", encoding="utf-8")

    result = await file_tools.read_file_chunked(
        path=str(test_file),
        repo_root=temp_repo,
        start_line=100,
        max_lines=10,
    )

    assert not result.success
    assert "beyond file length" in result.error.lower()


@pytest.mark.asyncio
async def test_read_file_chunked_path_security(temp_repo: Path):
    """Test that chunked read respects path security."""
    outside = temp_repo.parent / "outside.txt"
    outside.write_text("secret", encoding="utf-8")

    result = await file_tools.read_file_chunked(
        path="../outside.txt",
        repo_root=temp_repo,
        start_line=1,
        max_lines=10,
    )

    assert not result.success
    assert "outside" in result.error.lower() or "path" in result.error.lower()


@pytest.mark.asyncio
async def test_read_file_chunked_via_executor(temp_repo: Path):
    """Test read_file_chunked through the tool executor."""
    test_file = temp_repo / "test.txt"
    lines = [f"Line {i}" for i in range(1, 51)]
    test_file.write_text("\n".join(lines), encoding="utf-8")

    executor = executor_module.ToolExecutor(repo_root=temp_repo)
    result = await executor.execute_legacy_async(
        "read_file_chunked",
        {"path": str(test_file), "start_line": 20, "max_lines": 5}
    )

    assert "Line 20" in result
    assert "Lines 20-24" in result


def _run_git(repo_path: Path, *args: str) -> str:
    result = subprocess.run(
        ["git", *args],
        cwd=repo_path,
        check=True,
        capture_output=True,
        text=True,
    )
    return result.stdout.strip()


def _build_executor(repo_root: Path) -> executor_module.ToolExecutor:
    executor = executor_module.ToolExecutor(repo_root=repo_root)
    executor.register("read_file", file_tools.read_file)
    executor.register("list_files", file_tools.list_files)
    executor.register("grep", search_tools.grep)
    executor.register("git_status", git_tools.git_status)
    executor.register("git_log", git_tools.git_log)
    executor.register("git_diff", getattr(git_tools, "get_diff"))
    return executor


@pytest.mark.asyncio
async def test_tool_path_traversal_rejected(temp_repo: Path):
    outside = temp_repo.parent / "outside.txt"
    outside.write_text("secret", encoding="utf-8")
    executor = _build_executor(temp_repo)
    result = await executor.execute("read_file", path="../outside.txt")
    assert not result.success
    assert "path" in result.error.lower() or "outside" in result.error.lower()


@pytest.mark.asyncio
async def test_tool_absolute_path_rejected(temp_repo: Path, tmp_path: Path):
    outside = tmp_path / "outside.txt"
    outside.write_text("secret", encoding="utf-8")
    executor = _build_executor(temp_repo)
    result = await executor.execute("read_file", path=str(outside))
    assert not result.success
    assert "absolute" in result.error.lower()


@pytest.mark.asyncio
async def test_tool_symlink_escape_rejected(temp_repo: Path, tmp_path: Path):
    if os.name == "nt" or not hasattr(os, "symlink"):
        pytest.skip("symlink escape test not supported on this platform")
    outside = tmp_path / "outside.txt"
    outside.write_text("secret", encoding="utf-8")
    link_path = temp_repo / "link.txt"
    os.symlink(outside, link_path)
    executor = _build_executor(temp_repo)
    result = await executor.execute("read_file", path=str(link_path))
    assert not result.success
    assert "symlink" in result.error.lower()


@pytest.mark.asyncio
async def test_tool_hidden_file_warning(temp_repo: Path, caplog):
    caplog.set_level(logging.WARNING)
    hidden = temp_repo / ".env"
    hidden.write_text("TOKEN=secret", encoding="utf-8")
    executor = _build_executor(temp_repo)
    result = await executor.execute("read_file", path=str(hidden))
    assert result.success
    assert "hidden" in caplog.text.lower()


@pytest.mark.asyncio
async def test_tool_file_size_limit(temp_repo: Path):
    large = temp_repo / "large.txt"
    large.write_text("a" * 120_000, encoding="utf-8")
    executor = _build_executor(temp_repo)
    result = await executor.execute("read_file", path=str(large))
    assert result.success
    assert len(result.data) <= 100_000
    assert result.error is None or "truncat" in result.error.lower()


@pytest.mark.asyncio
async def test_tool_binary_file_detected(temp_repo: Path):
    binary = temp_repo / "image.bin"
    binary.write_bytes(b"\x00\xff\x00\xff")
    executor = _build_executor(temp_repo)
    result = await executor.execute("read_file", path=str(binary))
    assert not result.success
    assert "binary" in result.error.lower()


@pytest.mark.asyncio
async def test_tool_encoding_fallback(temp_repo: Path):
    latin1 = temp_repo / "latin1.txt"
    latin1.write_bytes(b"caf\xe9")
    executor = _build_executor(temp_repo)
    result = await executor.execute("read_file", path=str(latin1))
    assert result.success
    assert "caf" in result.data


@pytest.mark.asyncio
async def test_tool_file_not_found(temp_repo: Path):
    executor = _build_executor(temp_repo)
    result = await executor.execute("read_file", path="missing.txt")
    assert not result.success
    assert "not found" in result.error.lower()


@pytest.mark.asyncio
async def test_tool_permission_denied(temp_repo: Path):
    if os.name == "nt":
        pytest.skip("permission test not supported on this platform")
    protected = temp_repo / "protected.txt"
    protected.write_text("secret", encoding="utf-8")
    os.chmod(protected, 0)
    executor = _build_executor(temp_repo)
    try:
        result = await executor.execute("read_file", path=str(protected))
    finally:
        os.chmod(protected, 0o600)
    assert not result.success
    assert "permission" in result.error.lower()


@pytest.mark.asyncio
async def test_tool_grep_max_results(temp_repo: Path):
    target = temp_repo / "sample.txt"
    target.write_text("\n".join(["TODO"] * 200), encoding="utf-8")
    executor = _build_executor(temp_repo)
    result = await executor.execute("grep", path=str(target), pattern="TODO", max_results=50)
    assert result.success
    assert len(result.data) == 50


@pytest.mark.asyncio
async def test_tool_list_files_glob(temp_repo: Path):
    (temp_repo / "src").mkdir(exist_ok=True)
    (temp_repo / "src" / "app.ts").write_text("console.log('hi')", encoding="utf-8")
    (temp_repo / "src" / "app.py").write_text("print('hi')", encoding="utf-8")
    executor = _build_executor(temp_repo)
    result = await executor.execute("list_files", path=str(temp_repo), pattern="**/*.ts")
    assert result.success
    assert all(path.endswith(".ts") for path in result.data)


@pytest.mark.asyncio
async def test_tool_git_not_initialized(tmp_path: Path):
    executor = _build_executor(tmp_path)
    result = await executor.execute("git_status")
    assert not result.success
    assert "not a git repository" in result.error.lower()


@pytest.mark.asyncio
async def test_tool_git_diff_base_head(temp_repo: Path):
    file_path = temp_repo / "src" / "app.py"
    file_path.parent.mkdir(parents=True, exist_ok=True)
    file_path.write_text("print('v1')\n", encoding="utf-8")
    _run_git(temp_repo, "add", str(file_path))
    _run_git(temp_repo, "commit", "-m", "Add app")
    base = _run_git(temp_repo, "rev-parse", "HEAD")

    file_path.write_text("print('v2')\n", encoding="utf-8")
    _run_git(temp_repo, "add", str(file_path))
    _run_git(temp_repo, "commit", "-m", "Update app")
    head = _run_git(temp_repo, "rev-parse", "HEAD")

    executor = _build_executor(temp_repo)
    result = await executor.execute("git_diff", base=base, head=head)
    assert result.success
    assert "app.py" in result.data


def test_tool_git_diff_via_legacy_executor(temp_repo: Path):
    file_path = temp_repo / "src" / "legacy.py"
    file_path.parent.mkdir(parents=True, exist_ok=True)
    file_path.write_text("print('v1')\n", encoding="utf-8")
    _run_git(temp_repo, "add", str(file_path))
    _run_git(temp_repo, "commit", "-m", "Add legacy")
    base = _run_git(temp_repo, "rev-parse", "HEAD")

    file_path.write_text("print('v2')\n", encoding="utf-8")
    _run_git(temp_repo, "add", str(file_path))
    _run_git(temp_repo, "commit", "-m", "Update legacy")
    head = _run_git(temp_repo, "rev-parse", "HEAD")

    executor = executor_module.ToolExecutor(repo_root=temp_repo)
    loop = asyncio.new_event_loop()
    asyncio.set_event_loop(loop)
    try:
        result = executor.execute_legacy("git_diff", {"base": base, "head": head})
    finally:
        loop.close()
        asyncio.set_event_loop(None)

    assert "legacy.py" in result
