"""Tests for the text editor."""

import pytest

from mudlib.editor import Editor


@pytest.mark.asyncio
async def test_create_editor_with_empty_buffer():
    """Editor starts with empty buffer."""
    editor = Editor()
    assert editor.buffer == []
    assert editor.cursor == 0
    assert editor.dirty is False


@pytest.mark.asyncio
async def test_create_editor_with_initial_content():
    """Editor can be initialized with content."""
    editor = Editor(initial_content="line 1\nline 2\nline 3")
    assert editor.buffer == ["line 1", "line 2", "line 3"]
    assert editor.dirty is False


@pytest.mark.asyncio
async def test_append_line():
    """Typing text that isn't a command appends to buffer."""
    editor = Editor()
    response = await editor.handle_input("hello world")
    assert response.output == ""
    assert response.done is False
    assert editor.buffer == ["hello world"]
    assert editor.dirty is True


@pytest.mark.asyncio
async def test_append_multiple_lines():
    """Multiple lines can be appended."""
    editor = Editor()
    await editor.handle_input("line 1")
    await editor.handle_input("line 2")
    await editor.handle_input("line 3")
    assert editor.buffer == ["line 1", "line 2", "line 3"]
    assert editor.dirty is True


@pytest.mark.asyncio
async def test_view_buffer():
    """':' shows the entire buffer with line numbers."""
    editor = Editor(initial_content="line 1\nline 2\nline 3")
    response = await editor.handle_input(":")
    assert "1  line 1" in response.output
    assert "2  line 2" in response.output
    assert "3  line 3" in response.output


@pytest.mark.asyncio
async def test_view_empty_buffer():
    """':' on empty buffer shows appropriate message."""
    editor = Editor()
    response = await editor.handle_input(":")
    assert "empty" in response.output.lower() or response.output == ""


@pytest.mark.asyncio
async def test_view_specific_line():
    """':5' shows line 5 with context."""
    editor = Editor(initial_content="\n".join([f"line {i}" for i in range(1, 11)]))
    response = await editor.handle_input(":5")
    # Should show lines 2-8 (3 lines of context on each side)
    assert "5  line 5" in response.output
    assert "2  line 2" in response.output
    assert "8  line 8" in response.output


@pytest.mark.asyncio
async def test_view_specific_line_at_start():
    """Viewing line near start shows available context."""
    editor = Editor(initial_content="line 1\nline 2\nline 3")
    response = await editor.handle_input(":1")
    assert "1  line 1" in response.output


@pytest.mark.asyncio
async def test_insert_at_line():
    """':i 5 text' inserts 'text' at line 5."""
    editor = Editor(initial_content="\n".join([f"line {i}" for i in range(1, 6)]))
    await editor.handle_input(":i 3 inserted text")
    assert editor.buffer[2] == "inserted text"
    assert editor.buffer[3] == "line 3"
    assert len(editor.buffer) == 6
    assert editor.dirty is True


@pytest.mark.asyncio
async def test_insert_at_end():
    """':i' can insert at end of buffer."""
    editor = Editor(initial_content="line 1\nline 2")
    await editor.handle_input(":i 3 line 3")
    assert editor.buffer[2] == "line 3"


@pytest.mark.asyncio
async def test_delete_single_line():
    """':d 5' deletes line 5."""
    editor = Editor(initial_content="\n".join([f"line {i}" for i in range(1, 6)]))
    await editor.handle_input(":d 3")
    assert editor.buffer == ["line 1", "line 2", "line 4", "line 5"]
    assert editor.dirty is True


@pytest.mark.asyncio
async def test_delete_range():
    """':d 5 8' deletes lines 5 through 8."""
    editor = Editor(initial_content="\n".join([f"line {i}" for i in range(1, 11)]))
    await editor.handle_input(":d 3 6")
    expected = ["line 1", "line 2", "line 7", "line 8", "line 9", "line 10"]
    assert editor.buffer == expected
    assert editor.dirty is True


@pytest.mark.asyncio
async def test_delete_invalid_line():
    """Deleting invalid line shows error."""
    editor = Editor(initial_content="line 1\nline 2")
    response = await editor.handle_input(":d 10")
    assert "error" in response.output.lower() or "invalid" in response.output.lower()
    assert editor.buffer == ["line 1", "line 2"]


@pytest.mark.asyncio
async def test_replace_line():
    """':r 5 text' replaces line 5 with 'text'."""
    editor = Editor(initial_content="\n".join([f"line {i}" for i in range(1, 6)]))
    await editor.handle_input(":r 3 replaced text")
    assert editor.buffer[2] == "replaced text"
    assert len(editor.buffer) == 5
    assert editor.dirty is True


@pytest.mark.asyncio
async def test_search_and_replace_first():
    """':s old new' replaces first occurrence."""
    editor = Editor(initial_content="hello world\nhello again\nhello there")
    await editor.handle_input(":s hello hi")
    assert editor.buffer[0] == "hi world"
    assert editor.buffer[1] == "hello again"
    assert editor.dirty is True


@pytest.mark.asyncio
async def test_search_and_replace_all():
    """':sa old new' replaces all occurrences."""
    editor = Editor(initial_content="hello world\nhello again\nhello there")
    await editor.handle_input(":sa hello hi")
    assert editor.buffer[0] == "hi world"
    assert editor.buffer[1] == "hi again"
    assert editor.buffer[2] == "hi there"
    assert editor.dirty is True


@pytest.mark.asyncio
async def test_search_replace_no_match():
    """Search/replace with no match shows message."""
    editor = Editor(initial_content="hello world")
    response = await editor.handle_input(":s foo bar")
    assert (
        "not found" in response.output.lower() or "no match" in response.output.lower()
    )


@pytest.mark.asyncio
async def test_undo():
    """':u' undoes last buffer change."""
    editor = Editor(initial_content="line 1\nline 2")
    await editor.handle_input("line 3")
    assert len(editor.buffer) == 3
    response = await editor.handle_input(":u")
    assert editor.buffer == ["line 1", "line 2"]
    assert "undo" in response.output.lower()


@pytest.mark.asyncio
async def test_undo_multiple_times():
    """Undo can be called multiple times."""
    editor = Editor(initial_content="line 1")
    await editor.handle_input("line 2")
    await editor.handle_input("line 3")
    await editor.handle_input(":u")
    await editor.handle_input(":u")
    assert editor.buffer == ["line 1"]


@pytest.mark.asyncio
async def test_undo_when_empty():
    """Undo with no history shows message."""
    editor = Editor()
    response = await editor.handle_input(":u")
    assert (
        "nothing to undo" in response.output.lower()
        or "no undo" in response.output.lower()
    )


@pytest.mark.asyncio
async def test_save():
    """':w' calls save callback."""
    saved_content = None

    async def save_callback(content: str):
        nonlocal saved_content
        saved_content = content

    editor = Editor(initial_content="line 1\nline 2", save_callback=save_callback)
    await editor.handle_input("line 3")
    response = await editor.handle_input(":w")
    assert response.saved is True
    assert response.done is False
    assert saved_content == "line 1\nline 2\nline 3"
    assert editor.dirty is False


@pytest.mark.asyncio
async def test_save_without_callback():
    """':w' without callback shows error."""
    editor = Editor(initial_content="line 1")
    response = await editor.handle_input(":w")
    assert (
        "cannot save" in response.output.lower() or "no save" in response.output.lower()
    )


@pytest.mark.asyncio
async def test_save_and_quit():
    """':wq' saves and quits."""
    saved_content = None

    async def save_callback(content: str):
        nonlocal saved_content
        saved_content = content

    editor = Editor(initial_content="line 1", save_callback=save_callback)
    response = await editor.handle_input(":wq")
    assert response.saved is True
    assert response.done is True
    assert saved_content == "line 1"


@pytest.mark.asyncio
async def test_quit_when_dirty_warns():
    """':q' when dirty warns and doesn't quit."""
    editor = Editor(initial_content="line 1")
    await editor.handle_input("line 2")
    response = await editor.handle_input(":q")
    assert response.done is False
    assert "unsaved" in response.output.lower() or "save" in response.output.lower()


@pytest.mark.asyncio
async def test_quit_when_clean():
    """':q' when clean quits successfully."""
    editor = Editor(initial_content="line 1")
    response = await editor.handle_input(":q")
    assert response.done is True


@pytest.mark.asyncio
async def test_force_quit():
    """':q!' quits without saving."""
    editor = Editor(initial_content="line 1")
    await editor.handle_input("line 2")
    response = await editor.handle_input(":q!")
    assert response.done is True
    assert response.saved is False


@pytest.mark.asyncio
async def test_dot_saves_and_quits():
    """'.' on its own line saves and quits (MOO convention)."""
    saved_content = None

    async def save_callback(content: str):
        nonlocal saved_content
        saved_content = content

    editor = Editor(initial_content="line 1", save_callback=save_callback)
    await editor.handle_input("line 2")
    response = await editor.handle_input(".")
    assert response.done is True
    assert response.saved is True
    assert saved_content == "line 1\nline 2"


@pytest.mark.asyncio
async def test_dirty_flag_tracking():
    """Dirty flag is properly tracked."""
    editor = Editor(initial_content="line 1")
    assert editor.dirty is False

    await editor.handle_input("line 2")
    assert editor.dirty is True

    # Mock save
    saved_content = None

    async def save_callback(content: str):
        nonlocal saved_content
        saved_content = content

    editor.save_callback = save_callback
    await editor.handle_input(":w")
    assert editor.dirty is False


@pytest.mark.asyncio
async def test_help():
    """':h' shows help."""
    editor = Editor()
    response = await editor.handle_input(":h")
    assert ":w" in response.output
    assert ":q" in response.output
    assert ":i" in response.output
    assert ":d" in response.output


@pytest.mark.asyncio
async def test_undo_stack_size_limit():
    """Undo stack is capped at 50 entries."""
    editor = Editor()
    # Add 60 lines
    for i in range(60):
        await editor.handle_input(f"line {i}")
    # Undo stack should be capped
    assert len(editor.undo_stack) <= 50


@pytest.mark.asyncio
async def test_line_numbers_padded():
    """Line numbers in view are left-padded."""
    editor = Editor(initial_content="\n".join([f"line {i}" for i in range(1, 12)]))
    response = await editor.handle_input(":")
    # Line 1 should have padding, line 11 should not have extra padding
    lines = response.output.split("\n")
    # Find lines with numbers
    assert any(" 1  line 1" in line for line in lines)
    assert any("11  line 11" in line for line in lines)


# Syntax highlighting tests


@pytest.mark.asyncio
async def test_view_buffer_with_python_includes_ansi():
    """Viewing buffer with content_type=python includes ANSI codes."""
    editor = Editor(
        initial_content="def foo():\n    return 42",
        content_type="python",
        color_depth="16",
    )
    response = await editor.handle_input(":")
    # Should contain ANSI escape codes from syntax highlighting
    assert "\033[" in response.output
    # Should contain the code
    assert "def" in response.output
    assert "foo" in response.output


@pytest.mark.asyncio
async def test_view_buffer_with_text_no_ansi():
    """Viewing buffer with content_type=text does NOT include ANSI codes."""
    editor = Editor(
        initial_content="plain text line 1\nplain text line 2", content_type="text"
    )
    response = await editor.handle_input(":")
    # Should NOT contain ANSI escape codes (plain line numbers)
    assert "\033[" not in response.output
    # Should contain plain line numbers
    assert "1  plain text line 1" in response.output
    assert "2  plain text line 2" in response.output


@pytest.mark.asyncio
async def test_view_specific_line_with_python_includes_ansi():
    """Viewing a specific line with python content includes ANSI codes."""
    content = "\n".join([f"def func{i}():\n    return {i}" for i in range(10)])
    editor = Editor(
        initial_content=content,
        content_type="python",
        color_depth="16",
    )
    response = await editor.handle_input(":5")
    # Should contain ANSI escape codes from syntax highlighting
    assert "\033[" in response.output
    # Should contain code from the context window
    assert "def" in response.output


@pytest.mark.asyncio
async def test_color_depth_passed_to_highlight():
    """Color depth is passed through to highlight function."""
    editor = Editor(
        initial_content="x = 1\ny = 2",
        content_type="python",
        color_depth="256",
    )
    response = await editor.handle_input(":")
    # Should contain ANSI codes (256-color formatter)
    assert "\033[" in response.output
    # Should contain the code
    assert "x" in response.output
    assert "y" in response.output


@pytest.mark.asyncio
async def test_empty_buffer_with_python_content_type():
    """Empty buffer with content_type=python still works."""
    editor = Editor(content_type="python", color_depth="16")
    response = await editor.handle_input(":")
    # Should show empty buffer message
    assert "empty" in response.output.lower()


@pytest.mark.asyncio
async def test_undo_past_save_marks_dirty():
    """Undoing after save should mark buffer dirty again."""
    saved = []

    async def cb(content: str):
        saved.append(content)

    editor = Editor(initial_content="line 1", save_callback=cb)
    await editor.handle_input("line 2")  # dirty
    await editor.handle_input(":w")  # clean
    assert not editor.dirty
    await editor.handle_input(":u")  # undo the append - should be dirty
    assert editor.dirty
