import os
import time
from pathlib import Path

import pytest

from cotext_delta.tools import (
    ToolError,
    apply_patch,
    list_dir,
    grep_files,
    web_search,
    read_file_slice,
    run_shell_command,
    update_plan,
)


def test_apply_patch_adds_file(tmp_path):
    patch = """*** Begin Patch
*** Add File: new.txt
+hello
+world
*** End Patch"""
    changed = apply_patch(patch, base_dir=tmp_path)
    new_file = tmp_path / "new.txt"
    assert new_file in changed
    assert new_file.read_text(encoding="utf-8").strip().splitlines() == ["hello", "world"]


def test_apply_patch_updates_file(tmp_path):
    target = tmp_path / "file.txt"
    target.write_text("a\nb\nc\n", encoding="utf-8")
    patch = """*** Begin Patch
*** Update File: file.txt
@@
 a
-b
+bee
 c
*** End Patch"""
    apply_patch(patch, base_dir=tmp_path)
    assert target.read_text(encoding="utf-8").splitlines() == ["a", "bee", "c"]


def test_apply_patch_rejects_escape(tmp_path):
    patch = """*** Begin Patch
*** Add File: ../escape.txt
+oops
*** End Patch"""
    with pytest.raises(ToolError):
        apply_patch(patch, base_dir=tmp_path)


def test_read_file_slice_limits(tmp_path):
    path = tmp_path / "file.txt"
    path.write_text("line1\nline2\nline3\n", encoding="utf-8")
    output = read_file_slice(path, offset=2, limit=1)
    assert output == "2: line2"


def test_read_file_slice_indent_mode(tmp_path):
    path = tmp_path / "file.txt"
    path.write_text("  line1\n  line2\n", encoding="utf-8")
    output = read_file_slice(path, offset=1, limit=2, indent=True)
    assert output == "  line1\n  line2\n"


def test_list_dir_depth_and_offset(tmp_path):
    (tmp_path / "a").mkdir()
    (tmp_path / "a" / "inner").mkdir()
    (tmp_path / "a" / "inner" / "file.txt").write_text("x", encoding="utf-8")
    listing = list_dir(tmp_path, offset=1, limit=5, depth=2)
    assert "Absolute path" in listing
    assert "[D] a" in listing
    # inner depth=2 should include inner/file.txt entry
    assert "inner/file.txt" in listing


def test_list_dir_depth_cap(tmp_path, monkeypatch):
    monkeypatch.setenv("LIST_DIR_MAX_DEPTH", "1")
    (tmp_path / "a").mkdir()
    with pytest.raises(ToolError):
        list_dir(tmp_path, offset=1, limit=5, depth=3)


def test_run_shell_command_requires_absolute(tmp_path):
    with pytest.raises(ToolError):
        run_shell_command("echo hi", Path("relative"))


def test_run_shell_command_executes(tmp_path):
    result = run_shell_command("echo hi", tmp_path, timeout=5)
    assert result.exit_code == 0
    assert result.stdout.strip() == "hi"


def test_grep_files_matches(tmp_path):
    p = tmp_path / "a.txt"
    p.write_text("hello\nworld\nhello\n", encoding="utf-8")
    out = grep_files("hello", workdir=tmp_path, limit=5, timeout=5)
    assert "a.txt:1:hello" in out
    assert "a.txt:3:hello" in out


def test_grep_files_limit_and_errors(tmp_path):
    (tmp_path / "b.txt").write_text("x\n", encoding="utf-8")
    with pytest.raises(ToolError):
        grep_files("", workdir=tmp_path)
    with pytest.raises(ToolError):
        grep_files("x", workdir=tmp_path, limit=0)


def test_web_search_requires_query_and_key(monkeypatch):
    monkeypatch.delenv("SEARCH_API_KEY", raising=False)
    with pytest.raises(ToolError):
        web_search("", provider="mock")
    with pytest.raises(ToolError):
        web_search("hello", provider="tavily")


def test_exec_command_single_session(monkeypatch, tmp_path):
    from cotext_delta import tools

    tools._EXEC_SESSIONS.clear()
    output = tools.exec_command("printf 'hello'", session="s1", workdir=tmp_path, timeout=5)
    assert "hello" in output
    assert "s1" not in tools._EXEC_SESSIONS


def test_exec_command_allows_single_active(monkeypatch, tmp_path):
    from cotext_delta import tools

    tools._EXEC_SESSIONS.clear()
    tools.exec_command("sleep 2", session="first", workdir=tmp_path, timeout=5)
    with pytest.raises(ToolError):
        tools.exec_command("echo second", session="second", workdir=tmp_path, timeout=5)
    tools.close_exec_session("first")


def test_write_stdin_timeout(monkeypatch, tmp_path):
    from cotext_delta import tools

    tools._EXEC_SESSIONS.clear()
    original_idle = tools._SESSION_IDLE_TIMEOUT
    try:
        tools._SESSION_IDLE_TIMEOUT = 0.1
        tools.exec_command("cat", session="cat1", workdir=tmp_path, timeout=1)
        time.sleep(1.1)
        with pytest.raises(ToolError):
            tools.write_stdin(session="cat1", input="ping\n")
    finally:
        tools._SESSION_IDLE_TIMEOUT = original_idle
        tools.close_exec_session("cat1")


def test_exec_session_id_length(tmp_path):
    from cotext_delta import tools

    tools._EXEC_SESSIONS.clear()
    long_session = "x" * 65
    with pytest.raises(ToolError):
        tools.exec_command("echo hi", session=long_session, workdir=tmp_path, timeout=5)


def test_web_search_mock(monkeypatch):
    monkeypatch.setenv("SEARCH_PROVIDER", "mock")
    out = web_search("hello", limit=2, timeout=5)
    assert "mock web_search results" in out


def test_update_plan_valid_json():
    payload = '{"plan":[{"step":"a","status":"pending"},{"step":"b","status":"completed"}]}'
    out = update_plan(payload)
    assert "plan" in out


def test_update_plan_invalid_status():
    payload = '{"plan":[{"step":"a","status":"oops"}]}'
    with pytest.raises(ToolError):
        update_plan(payload)
