from unittest.mock import patch

import pytest
from fastmcp.exceptions import ToolError

import my_mcp.filesystem_server as fs
from my_mcp.server import _parse_allowed_dirs


def test_parse_allowed_dirs_windows_drive_letter_not_split():
    with patch("my_mcp.server.platform.system", return_value="Windows"):
        result = _parse_allowed_dirs(r"C:\Users\foo,D:\work")
    assert result == [r"C:\Users\foo", r"D:\work"]


def test_parse_allowed_dirs_unix_colon_splits():
    with patch("my_mcp.server.platform.system", return_value="Linux"):
        result = _parse_allowed_dirs("/home/foo:/tmp")
    assert result == ["/home/foo", "/tmp"]


def test_parse_allowed_dirs_empty_segments_ignored():
    with patch("my_mcp.server.platform.system", return_value="Linux"):
        result = _parse_allowed_dirs(" /a , /b ; /c ")
    assert result == ["/a", "/b", "/c"]


def test_npx_check_raises_if_missing(monkeypatch):
    monkeypatch.setattr("shutil.which", lambda _: None)
    with pytest.raises(RuntimeError, match="npx is not installed"):
        fs.create_client(["/tmp"])


@pytest.mark.asyncio
async def test_lists_both_tools(client):
    print("\n[test] Calling list_tools...")
    tools = [t.name for t in await client.list_tools()]
    print(f"\n[test] Got tools: {tools}")
    assert "edit_file" in tools
    assert "read_text_file" in tools


@pytest.mark.asyncio
async def test_read_plain_text(client, tmp_path, write_file):
    path = tmp_path("test_read_plain.txt")
    write_file(path, "hello world")
    result = await client.call_tool("read_text_file", {"path": path})
    assert not result.is_error
    assert "hello world" in str(result)


@pytest.mark.asyncio
async def test_read_preserves_umlauts(client, tmp_path, write_file):
    path = tmp_path("test_umlauts.txt")
    write_file(path, "Ärger mit Übung\n")
    content = str(await client.call_tool("read_text_file", {"path": path}))
    assert "Ärger" in content
    assert "Übung" in content


@pytest.mark.asyncio
async def test_read_cpp_uses_latin1_from_config(client, tmp_path, write_file):
    path = tmp_path("test_latin1.cpp")
    write_file(path, 'const char* s = "Ärger";\n', encoding="latin-1")
    result = await client.call_tool("read_text_file", {"path": path})
    assert not result.is_error
    assert "rger" in str(result)


@pytest.mark.asyncio
async def test_read_txt_uses_utf8_from_config(client, tmp_path, write_file):
    path = tmp_path("test_utf8.txt")
    write_file(path, "Ärger\n", encoding="utf-8")
    result = await client.call_tool("read_text_file", {"path": path})
    assert not result.is_error
    assert "Ärger" in str(result)


@pytest.mark.asyncio
async def test_read_nonexistent_file_returns_error(client, tmp_path):
    path = tmp_path("nonexistent.txt")
    with pytest.raises(ToolError):
        await client.call_tool("read_text_file", {"path": path})


@pytest.mark.asyncio
async def test_read_path_outside_allowed_dir_returns_error(client):
    with pytest.raises(ToolError, match="does not match any allowed"):
        await client.call_tool("read_text_file", {"path": "/etc/passwd"})


@pytest.mark.asyncio
async def test_edit_basic(client, tmp_path, write_file, read_file):
    path = tmp_path("test_edit_basic.txt")
    write_file(path, "hello world")
    result = await client.call_tool(
        "edit_file",
        {
            "path": path,
            "edits": [{"oldText": "hello", "newText": "goodbye"}],
            "dryRun": False,
        },
    )
    assert not result.is_error
    assert read_file(path) == "goodbye world"


@pytest.mark.asyncio
async def test_edit_applies_umlaut_fixing(client, tmp_path, write_file, read_file):
    path = tmp_path("test_edit_umlaut.cpp")
    write_file(path, 'const char* s = "hello Ärger";\n', encoding="latin-1")
    result = await client.call_tool(
        "edit_file",
        {
            "path": path,
            "edits": [{"oldText": "hello", "newText": "goodbye"}],
            "dryRun": False,
        },
    )
    assert not result.is_error
    on_disk = read_file(path, encoding="latin-1")
    assert "goodbye" in on_disk
    assert "CAE_UMLT" in on_disk


@pytest.mark.asyncio
async def test_edit_cpp_preserves_latin1_encoding(
    client, tmp_path, write_file, read_file
):
    path = tmp_path("test_edit_latin1.cpp")
    write_file(path, "int x = 1;\n", encoding="latin-1")
    result = await client.call_tool(
        "edit_file",
        {"path": path, "edits": [{"oldText": "1", "newText": "2"}], "dryRun": False},
    )
    assert not result.is_error
    assert read_file(path, encoding="latin-1") == "int x = 2;\n"


@pytest.mark.asyncio
async def test_edit_dry_run_does_not_modify_file(
    client, tmp_path, write_file, read_file
):
    path = tmp_path("test_dry_run.cpp")
    original = 'const char* s = "hello";\n'
    write_file(path, original, encoding="latin-1")
    result = await client.call_tool(
        "edit_file",
        {
            "path": path,
            "edits": [{"oldText": "hello", "newText": "goodbye"}],
            "dryRun": True,
        },
    )
    assert not result.is_error
    assert read_file(path, encoding="latin-1") == original


@pytest.mark.asyncio
async def test_edit_multiple_edits(client, tmp_path, write_file, read_file):
    path = tmp_path("test_multi_edit.txt")
    write_file(path, "aaa bbb ccc")
    result = await client.call_tool(
        "edit_file",
        {
            "path": path,
            "edits": [
                {"oldText": "aaa", "newText": "xxx"},
                {"oldText": "ccc", "newText": "zzz"},
            ],
            "dryRun": False,
        },
    )
    assert not result.is_error
    assert read_file(path) == "xxx bbb zzz"


@pytest.mark.asyncio
async def test_edit_nonexistent_file_returns_error(client, tmp_path):
    path = tmp_path("nonexistent_edit.txt")
    with pytest.raises(ToolError):
        await client.call_tool(
            "edit_file", {"path": path, "edits": [{"oldText": "old", "newText": "new"}]}
        )


@pytest.mark.asyncio
async def test_edit_path_outside_allowed_dir_returns_error(client):
    with pytest.raises(ToolError, match="does not match any allowed"):
        await client.call_tool(
            "edit_file",
            {"path": "/etc/passwd", "edits": [{"oldText": "old", "newText": "new"}]},
        )


@pytest.mark.asyncio
async def test_edit_malformed_payload_missing_oldtext(client, tmp_path, write_file):
    path = tmp_path("test_malformed.txt")
    write_file(path, "hello")
    with pytest.raises(ToolError):
        await client.call_tool(
            "edit_file", {"path": path, "edits": [{"newText": "new"}]}
        )


@pytest.mark.asyncio
async def test_edit_then_read(client, tmp_path, write_file):
    path = tmp_path("test_edit_then_read.txt")
    write_file(path, "hello world")
    await client.call_tool(
        "edit_file",
        {
            "path": path,
            "edits": [{"oldText": "hello", "newText": "goodbye"}],
            "dryRun": False,
        },
    )
    result = await client.call_tool("read_text_file", {"path": path})
    assert not result.is_error
    assert "goodbye world" in str(result)


@pytest.mark.asyncio
async def test_read_head(client, tmp_path, write_file):
    path = tmp_path("test_head.txt")
    write_file(path, "line1\nline2\nline3\nline4\n")
    result = await client.call_tool("read_text_file", {"path": path, "head": 2})
    text = str(result)
    assert "line1" in text
    assert "line2" in text
    assert "line3" not in text


@pytest.mark.asyncio
async def test_read_tail(client, tmp_path, write_file):
    path = tmp_path("test_tail.txt")
    write_file(path, "line1\nline2\nline3\nline4\n")
    result = await client.call_tool("read_text_file", {"path": path, "tail": 2})
    text = str(result)
    assert "line1" not in text
    assert "line3" in text or "line4" in text


@pytest.mark.asyncio
async def test_edit_oldtext_not_found_returns_error(
    client, tmp_path, write_file, read_file
):
    path = tmp_path("test_not_found.txt")
    write_file(path, "hello world")
    with pytest.raises(ToolError):
        await client.call_tool(
            "edit_file",
            {
                "path": path,
                "edits": [{"oldText": "nonexistent text", "newText": "new"}],
            },
        )
    assert read_file(path) == "hello world"


@pytest.mark.asyncio
async def test_edit_empty_edits_list(client, tmp_path, write_file, read_file):
    path = tmp_path("test_empty_edits.txt")
    write_file(path, "unchanged content")
    result = await client.call_tool("edit_file", {"path": path, "edits": []})
    assert not result.is_error
    assert read_file(path) == "unchanged content"


@pytest.mark.asyncio
async def test_full_roundtrip_latin1_umlaut_edit_read(
    client, tmp_path, write_file, read_file
):
    path = tmp_path("test_roundtrip.cpp")
    write_file(
        path, '// Ärger\nconst char* msg = "Ärger ist normal";\n', encoding="latin-1"
    )
    await client.call_tool(
        "edit_file", {"path": path, "edits": [{"oldText": "normal", "newText": "okay"}]}
    )
    result = await client.call_tool("read_text_file", {"path": path})
    text = str(result)
    assert "CAE_UMLT" in text
    assert "okay" in text
    on_disk = read_file(path, encoding="latin-1")
    assert "okay" in on_disk


@pytest.mark.asyncio
async def test_multiple_operations_single_session(client, tmp_path, write_file):
    path1 = tmp_path("test_multi_a.txt")
    path2 = tmp_path("test_multi_b.txt")
    write_file(path1, "file one")
    write_file(path2, "file two")
    r1 = await client.call_tool("read_text_file", {"path": path1})
    r2 = await client.call_tool("read_text_file", {"path": path2})
    assert "file one" in str(r1)
    assert "file two" in str(r2)
    await client.call_tool(
        "edit_file", {"path": path1, "edits": [{"oldText": "one", "newText": "ONE"}]}
    )
    await client.call_tool(
        "edit_file", {"path": path2, "edits": [{"oldText": "two", "newText": "TWO"}]}
    )
    r1 = await client.call_tool("read_text_file", {"path": path1})
    r2 = await client.call_tool("read_text_file", {"path": path2})
    assert "file ONE" in str(r1)
    assert "file TWO" in str(r2)
