import json
import os
import tempfile
from unittest.mock import patch

import pytest
from fastmcp import Client
from fastmcp.exceptions import ToolError

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


@pytest.fixture
def tmp():
    with tempfile.TemporaryDirectory() as d:
        yield d


def tmp_path(tmp, name):
    return os.path.join(tmp, name).replace("\\", "/")


def write_file(path, content, encoding="utf-8"):
    with open(path, "w", encoding=encoding) as f:
        f.write(content)


def read_file(path, encoding="utf-8"):
    with open(path, encoding=encoding) as f:
        return f.read()


def make_client(tmp_dir, config=None, *, use_cli_args=False):
    env = {**os.environ}
    args = ["run", "python", "-m", "my_mcp"]
    if use_cli_args:
        # Pass allowed dir as CLI arg instead of env var
        args.append(tmp_dir.replace("\\", "/"))
    else:
        env["ALLOWED_DIRS"] = tmp_dir.replace("\\", "/")
    if config:
        config_path = os.path.join(tmp_dir, "test_config.json")
        with open(config_path, "w") as f:
            json.dump(config, f)
        env["SERVER_CONFIG"] = config_path
    return Client(
        {
            "mcpServers": {
                "wrapper": {
                    "command": "uv",
                    "args": args,
                    "env": env,
                }
            }
        }
    )


# ── _parse_allowed_dirs ─────────────────────────────────────────────────


def test_parse_allowed_dirs_windows_drive_letter_not_split():
    """On Windows, C:\\path must not be split at the drive colon."""
    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():
    """On non-Windows, ':' is a valid path list separator."""
    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():
    """Extra delimiters or whitespace must not produce empty entries."""
    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):
    """If npx is not on PATH, create_client should raise immediately."""
    monkeypatch.setattr("shutil.which", lambda _: None)
    with pytest.raises(RuntimeError, match="npx is not installed"):
        fs.create_client(["/tmp"])


TEST_CONFIG = {
    "umlaut_config": {
        "string_mapping": {"Ä": "CAE_UMLT"},
        "comment_mapping": {"Ä": "Ae"},
    },
    "encoding_config": {
        "default": "utf-8",
        "mapping": {"*.cpp": "latin-1", "*.h": "latin-1"},
    },
}


# ── Tool discovery ──────────────────────────────────────────────────────


@pytest.mark.asyncio
async def test_lists_both_tools(tmp):
    async with make_client(tmp, TEST_CONFIG) as client:
        tools = [t.name for t in await client.list_tools()]
        assert "edit_file" in tools
        assert "read_text_file" in tools


# ── read_text_file ──────────────────────────────────────────────────────


@pytest.mark.asyncio
async def test_read_plain_text(tmp):
    path = tmp_path(tmp, "hello.txt")
    write_file(path, "hello world")

    async with make_client(tmp, TEST_CONFIG) as client:
        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(tmp):
    path = tmp_path(tmp, "test.txt")
    write_file(path, "Ärger mit Übung\n")

    async with make_client(tmp, TEST_CONFIG) as client:
        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(tmp):
    path = tmp_path(tmp, "test.cpp")
    write_file(path, 'const char* s = "Ärger";\n', encoding="latin-1")

    async with make_client(tmp, TEST_CONFIG) as client:
        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(tmp):
    path = tmp_path(tmp, "test.txt")
    write_file(path, "Ärger\n", encoding="utf-8")

    async with make_client(tmp, TEST_CONFIG) as client:
        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(tmp):
    path = tmp_path(tmp, "nonexistent.txt")

    async with make_client(tmp, TEST_CONFIG) as client:
        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(tmp):
    async with make_client(tmp, TEST_CONFIG) as client:
        with pytest.raises(ToolError, match="not within allowed"):
            await client.call_tool("read_text_file", {"path": "/etc/passwd"})


# ── edit_file ───────────────────────────────────────────────────────────


@pytest.mark.asyncio
async def test_edit_basic(tmp):
    path = tmp_path(tmp, "test.txt")
    write_file(path, "hello world")

    async with make_client(tmp, TEST_CONFIG) as client:
        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(tmp):
    path = tmp_path(tmp, "test.cpp")
    write_file(path, 'const char* s = "hello Ärger";\n', encoding="latin-1")

    async with make_client(tmp, TEST_CONFIG) as client:
        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(tmp):
    path = tmp_path(tmp, "test.cpp")
    write_file(path, "int x = 1;\n", encoding="latin-1")

    async with make_client(tmp, TEST_CONFIG) as client:
        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(tmp):
    path = tmp_path(tmp, "test.cpp")
    original = 'const char* s = "hello";\n'
    write_file(path, original, encoding="latin-1")

    async with make_client(tmp, TEST_CONFIG) as client:
        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(tmp):
    path = tmp_path(tmp, "test.txt")
    write_file(path, "aaa bbb ccc")

    async with make_client(tmp, TEST_CONFIG) as client:
        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(tmp):
    path = tmp_path(tmp, "nonexistent.txt")

    async with make_client(tmp, TEST_CONFIG) as client:
        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(tmp):
    async with make_client(tmp, TEST_CONFIG) as client:
        with pytest.raises(ToolError, match="not within 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(tmp):
    path = tmp_path(tmp, "test.txt")
    write_file(path, "hello")

    async with make_client(tmp, TEST_CONFIG) as client:
        with pytest.raises(ToolError):
            await client.call_tool(
                "edit_file",
                {
                    "path": path,
                    "edits": [{"newText": "new"}],
                },
            )


# ── end-to-end: edit then read ─────────────────────────────────────────


@pytest.mark.asyncio
async def test_edit_then_read(tmp):
    path = tmp_path(tmp, "test.txt")
    write_file(path, "hello world")

    async with make_client(tmp, TEST_CONFIG) as client:
        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)


# ── CLI args ──────────────────────────────────────────────────────────


@pytest.mark.asyncio
async def test_cli_args_allowed_dir(tmp):
    """Allowed dirs passed as CLI args (not env var) should work."""
    path = tmp_path(tmp, "test.txt")
    write_file(path, "hello via cli args")

    async with make_client(tmp, TEST_CONFIG, use_cli_args=True) as client:
        result = await client.call_tool("read_text_file", {"path": path})
        assert not result.is_error
        assert "hello via cli args" in str(result)


@pytest.mark.asyncio
async def test_cli_args_edit(tmp):
    """Edit should work when allowed dirs come from CLI args."""
    path = tmp_path(tmp, "test.txt")
    write_file(path, "original text")

    async with make_client(tmp, TEST_CONFIG, use_cli_args=True) as client:
        result = await client.call_tool(
            "edit_file",
            {
                "path": path,
                "edits": [{"oldText": "original", "newText": "modified"}],
            },
        )
        assert not result.is_error

    assert read_file(path) == "modified text"


# ── head / tail ───────────────────────────────────────────────────────


@pytest.mark.asyncio
async def test_read_head(tmp):
    path = tmp_path(tmp, "test.txt")
    write_file(path, "line1\nline2\nline3\nline4\n")

    async with make_client(tmp, TEST_CONFIG) as client:
        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(tmp):
    path = tmp_path(tmp, "test.txt")
    write_file(path, "line1\nline2\nline3\nline4\n")

    async with make_client(tmp, TEST_CONFIG) as client:
        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


# ── edit edge cases ──────────────────────────────────────────────────


@pytest.mark.asyncio
async def test_edit_oldtext_not_found_returns_error(tmp):
    """Editing with oldText that doesn't exist should error."""
    path = tmp_path(tmp, "test.txt")
    write_file(path, "hello world")

    async with make_client(tmp, TEST_CONFIG) as client:
        with pytest.raises(ToolError):
            await client.call_tool(
                "edit_file",
                {
                    "path": path,
                    "edits": [{"oldText": "nonexistent text", "newText": "new"}],
                },
            )

    # File should be unchanged
    assert read_file(path) == "hello world"


@pytest.mark.asyncio
async def test_edit_empty_edits_list(tmp):
    """Empty edits list should succeed and not modify the file."""
    path = tmp_path(tmp, "test.txt")
    write_file(path, "unchanged content")

    async with make_client(tmp, TEST_CONFIG) as client:
        result = await client.call_tool(
            "edit_file",
            {"path": path, "edits": []},
        )
        assert not result.is_error

    assert read_file(path) == "unchanged content"


# ── full round-trip: latin-1 with umlauts ────────────────────────────


@pytest.mark.asyncio
async def test_full_roundtrip_latin1_umlaut_edit_read(tmp):
    """Write latin-1 C++ file, edit it, read it back — umlauts transformed."""
    path = tmp_path(tmp, "main.cpp")
    write_file(
        path,
        '// Ärger\nconst char* msg = "Ärger ist normal";\n',
        encoding="latin-1",
    )

    async with make_client(tmp, TEST_CONFIG) as client:
        # Edit: change "normal" to "okay"
        await client.call_tool(
            "edit_file",
            {
                "path": path,
                "edits": [{"oldText": "normal", "newText": "okay"}],
            },
        )

        # Read back through the server
        result = await client.call_tool("read_text_file", {"path": path})
        text = str(result)

        # Umlauts should be transformed
        assert "CAE_UMLT" in text  # string mapping for Ä
        assert "okay" in text

    # Verify on-disk encoding is still latin-1
    on_disk = read_file(path, encoding="latin-1")
    assert "okay" in on_disk


# ── multiple sequential operations in one session ────────────────────


@pytest.mark.asyncio
async def test_multiple_operations_single_session(tmp):
    """Multiple read/edit ops in one session reuse the same backend."""
    path1 = tmp_path(tmp, "a.txt")
    path2 = tmp_path(tmp, "b.txt")
    write_file(path1, "file one")
    write_file(path2, "file two")

    async with make_client(tmp, TEST_CONFIG) as client:
        # Read both
        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)

        # Edit both
        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"}]},
        )

        # Read back
        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)
