"""Pure widget tests - no SDK needed."""

import pytest
from textual.app import App, ComposeResult
from textual.widgets import Static

from claudechic.widgets import (
    ChatInput,
    ChatMessage,
    ThinkingIndicator,
    SelectionPrompt,
    QuestionPrompt,
    AgentSection,
    PlanSection,
    TodoPanel,
    ProcessPanel,
    BackgroundProcess,
    ModelPrompt,
    StatusFooter,
    ContextBar,
)
from claudechic.widgets.content.todo import TodoItem
from claudechic.widgets.layout.processes import ProcessItem
from claudechic.enums import AgentStatus


class WidgetTestApp(App):
    """Minimal app for testing individual widgets."""

    def __init__(self, widget_factory):
        super().__init__()
        self._widget_factory = widget_factory

    def compose(self) -> ComposeResult:
        yield self._widget_factory()


@pytest.mark.asyncio
async def test_chat_input_submit():
    """Enter posts Submitted message."""
    submitted_text = None

    class TestApp(App):
        def compose(self):
            yield ChatInput(id="input")

        def on_chat_input_submitted(self, event):
            nonlocal submitted_text
            submitted_text = event.text

    app = TestApp()
    async with app.run_test() as pilot:
        input_widget = app.query_one(ChatInput)
        input_widget.text = "hello world"
        await pilot.press("enter")
        assert submitted_text == "hello world"


@pytest.mark.asyncio
async def test_chat_input_history():
    """Up/down navigates history."""

    class HistoryTestApp(App):
        def compose(self):
            yield ChatInput(id="input")

        def on_chat_input_submitted(self, event):
            # Clear input like ChatApp does
            self.query_one(ChatInput).clear()

    app = HistoryTestApp()
    async with app.run_test() as pilot:
        input_widget = app.query_one(ChatInput)

        # Send a few messages to build history
        input_widget.text = "first"
        await pilot.press("enter")
        input_widget.text = "second"
        await pilot.press("enter")
        input_widget.text = "third"
        await pilot.press("enter")

        # Now navigate history
        assert input_widget.text == ""
        await pilot.press("up")
        assert input_widget.text == "third"
        await pilot.press("up")
        assert input_widget.text == "second"
        await pilot.press("up")
        assert input_widget.text == "first"
        await pilot.press("down")
        assert input_widget.text == "second"


@pytest.mark.asyncio
async def test_chat_input_image_detection():
    """Detects image paths."""
    app = WidgetTestApp(lambda: ChatInput(id="input"))
    async with app.run_test():
        input_widget = app.query_one(ChatInput)

        # Test various image path formats
        assert input_widget._is_image_path("/tmp/test.png") == []  # File doesn't exist
        assert input_widget._is_image_path("not an image") == []
        assert input_widget._is_image_path("file:///nonexistent.jpg") == []


@pytest.mark.asyncio
async def test_selection_prompt_arrow_navigation():
    """Up/down cycles through options."""
    options = [("a", "Option A"), ("b", "Option B"), ("c", "Option C")]

    app = WidgetTestApp(lambda: SelectionPrompt("Choose:", options))
    async with app.run_test() as pilot:
        prompt = app.query_one(SelectionPrompt)

        assert prompt.selected_idx == 0
        await pilot.press("down")
        assert prompt.selected_idx == 1
        await pilot.press("down")
        assert prompt.selected_idx == 2
        await pilot.press("down")
        assert prompt.selected_idx == 0  # Wraps around
        await pilot.press("up")
        assert prompt.selected_idx == 2


@pytest.mark.asyncio
async def test_selection_prompt_number_keys():
    """Number keys select options directly."""
    options = [("a", "Option A"), ("b", "Option B"), ("c", "Option C")]
    result = None

    class TestApp(App):
        def compose(self):
            yield SelectionPrompt("Choose:", options)

    app = TestApp()
    async with app.run_test() as pilot:
        prompt = app.query_one(SelectionPrompt)

        # Press "2" to select second option
        await pilot.press("2")
        result = await prompt.wait()

    assert result == "b"


@pytest.mark.asyncio
async def test_selection_prompt_escape_cancels():
    """Escape resolves with empty string."""
    options = [("a", "Option A"), ("b", "Option B")]

    app = WidgetTestApp(lambda: SelectionPrompt("Choose:", options))
    async with app.run_test() as pilot:
        prompt = app.query_one(SelectionPrompt)

        await pilot.press("escape")
        result = await prompt.wait()
        assert result == ""


@pytest.mark.asyncio
async def test_selection_prompt_text_option():
    """Text option allows freeform input."""
    options = [("a", "Option A"), ("b", "Option B")]
    text_option = ("custom", "Type something...")

    class TestApp(App):
        def compose(self):
            yield SelectionPrompt("Choose:", options, text_option)

    app = TestApp()
    async with app.run_test() as pilot:
        prompt = app.query_one(SelectionPrompt)

        # Navigate to text option (3rd option, index 2)
        await pilot.press("3")
        # Type some text
        await pilot.press("h", "e", "l", "l", "o")
        await pilot.press("enter")
        result = await prompt.wait()

    assert result == "custom:hello"


@pytest.mark.asyncio
async def test_model_prompt_selection():
    """ModelPrompt allows model selection with arrow keys and numbers."""
    # Mock SDK model list
    models = [
        {"value": "sonnet", "displayName": "Sonnet", "description": "Sonnet 4 · Fast"},
        {"value": "opus", "displayName": "Opus", "description": "Opus 4.5 · Powerful"},
        {"value": "haiku", "displayName": "Haiku", "description": "Haiku 3.5 · Quick"},
    ]

    class TestApp(App):
        def compose(self):
            yield ModelPrompt(models, current_value="sonnet")

    app = TestApp()
    async with app.run_test() as pilot:
        prompt = app.query_one(ModelPrompt)

        # Should start on current model (sonnet = index 0)
        assert prompt.selected_idx == 0

        # Navigate down to opus
        await pilot.press("down")
        assert prompt.selected_idx == 1

        # Select with number key (3 = haiku)
        await pilot.press("3")
        result = await prompt.wait()

    assert result == "haiku"


@pytest.mark.asyncio
async def test_model_prompt_escape():
    """ModelPrompt returns None on escape."""
    models = [
        {"value": "sonnet", "displayName": "Sonnet", "description": "Sonnet 4 · Fast"},
        {"value": "opus", "displayName": "Opus", "description": "Opus 4.5 · Powerful"},
    ]
    app = WidgetTestApp(lambda: ModelPrompt(models, current_value="opus"))
    async with app.run_test() as pilot:
        prompt = app.query_one(ModelPrompt)

        # Should start on opus (index 1)
        assert prompt.selected_idx == 1

        await pilot.press("escape")
        result = await prompt.wait()

    assert result is None


@pytest.mark.asyncio
async def test_question_prompt_multi_question():
    """Handles multiple questions."""
    questions = [
        {"question": "Q1?", "options": [{"label": "Yes"}, {"label": "No"}]},
        {"question": "Q2?", "options": [{"label": "Red"}, {"label": "Blue"}]},
    ]

    app = WidgetTestApp(lambda: QuestionPrompt(questions))
    async with app.run_test() as pilot:
        prompt = app.query_one(QuestionPrompt)
        prompt.focus()

        # Answer first question
        assert prompt.current_q == 0
        await pilot.press("1")  # Select "Yes"

        # Should advance to second question
        assert prompt.current_q == 1
        assert prompt.answers == {"Q1?": "Yes"}

        await pilot.press("2")  # Select "Blue"

        # Prompt should have resolved after second answer
        assert prompt.answers == {"Q1?": "Yes", "Q2?": "Blue"}


@pytest.mark.asyncio
async def test_agent_section_add_remove():
    """Can add and remove agents."""
    app = WidgetTestApp(lambda: AgentSection(id="agents"))
    async with app.run_test():
        section = app.query_one(AgentSection)

        # Add agents
        section.add_agent("id1", "Agent 1")
        section.add_agent("id2", "Agent 2")

        assert "id1" in section._agents
        assert "id2" in section._agents
        assert len(section._agents) == 2

        # Remove one
        section.remove_agent("id1")
        assert "id1" not in section._agents
        assert len(section._agents) == 1


@pytest.mark.asyncio
async def test_agent_section_active_selection():
    """set_active updates visual state."""
    app = WidgetTestApp(lambda: AgentSection(id="agents"))
    async with app.run_test():
        section = app.query_one(AgentSection)

        section.add_agent("id1", "Agent 1")
        section.add_agent("id2", "Agent 2")

        section.set_active("id1")
        assert section._agents["id1"].has_class("active")
        assert not section._agents["id2"].has_class("active")

        section.set_active("id2")
        assert not section._agents["id1"].has_class("active")
        assert section._agents["id2"].has_class("active")


@pytest.mark.asyncio
async def test_agent_section_status_updates():
    """update_status changes indicator."""
    app = WidgetTestApp(lambda: AgentSection(id="agents"))
    async with app.run_test():
        section = app.query_one(AgentSection)

        section.add_agent("id1", "Agent 1", status=AgentStatus.IDLE)
        assert section._agents["id1"].status == AgentStatus.IDLE

        section.update_status("id1", AgentStatus.BUSY)
        assert section._agents["id1"].status == AgentStatus.BUSY

        section.update_status("id1", AgentStatus.NEEDS_INPUT)
        assert section._agents["id1"].status == AgentStatus.NEEDS_INPUT


@pytest.mark.asyncio
async def test_plan_section():
    """PlanSection set_plan shows/hides plan item."""
    from pathlib import Path

    app = WidgetTestApp(lambda: PlanSection(id="plan"))
    async with app.run_test():
        section = app.query_one(PlanSection)

        # Initially no plan item
        assert section._plan_item is None

        # Set plan creates item
        plan_path = Path("/tmp/test-plan.md")
        section.set_plan(plan_path)
        assert section._plan_item is not None
        assert section._plan_item.plan_path == plan_path

        # Clear plan hides section
        section.set_plan(None)
        assert section.has_class("hidden")


@pytest.mark.asyncio
async def test_context_bar_rendering():
    """ContextBar shows correct fill and color."""
    app = WidgetTestApp(lambda: ContextBar(id="ctx"))
    async with app.run_test():
        bar = app.query_one(ContextBar)

        # Low usage - should be dim
        bar.tokens = 10000
        bar.max_tokens = 200000
        rendered = bar.render()
        assert hasattr(rendered, "plain")
        assert "5%" in rendered.plain  # type: ignore[union-attr]

        # High usage - should be red
        bar.tokens = 180000
        rendered = bar.render()
        assert hasattr(rendered, "plain")
        assert "90%" in rendered.plain  # type: ignore[union-attr]


@pytest.mark.asyncio
async def test_todo_panel_updates():
    """TodoPanel displays and updates todos."""
    app = WidgetTestApp(lambda: TodoPanel(id="panel"))
    async with app.run_test():
        panel = app.query_one(TodoPanel)

        todos = [
            {
                "content": "Task 1",
                "status": "completed",
                "activeForm": "Completing task 1",
            },
            {
                "content": "Task 2",
                "status": "in_progress",
                "activeForm": "Working on task 2",
            },
            {"content": "Task 3", "status": "pending", "activeForm": "Starting task 3"},
        ]

        panel.update_todos(todos)

        items = list(panel.query(TodoItem))
        assert len(items) == 3
        assert items[0].has_class("completed")
        assert items[1].has_class("in_progress")
        assert items[2].has_class("pending")


@pytest.mark.asyncio
async def test_status_footer_auto_edit():
    """Footer shows auto-edit state."""
    app = WidgetTestApp(lambda: StatusFooter())
    async with app.run_test():
        footer = app.query_one(StatusFooter)

        footer.auto_edit = False
        label = footer.query_one("#auto-edit-label", Static)
        rendered = label.render()
        assert hasattr(rendered, "plain")
        assert "off" in rendered.plain.lower()  # type: ignore[union-attr]

        footer.auto_edit = True
        rendered = label.render()
        assert hasattr(rendered, "plain")
        assert "on" in rendered.plain.lower()  # type: ignore[union-attr]


@pytest.mark.asyncio
async def test_chat_message_append():
    """ChatMessage accumulates content."""
    app = WidgetTestApp(lambda: ChatMessage("Hello"))
    async with app.run_test():
        msg = app.query_one(ChatMessage)

        assert msg.get_raw_content() == "Hello"

        msg.append_content(" world")
        assert msg.get_raw_content() == "Hello world"

        msg.append_content("!")
        assert msg.get_raw_content() == "Hello world!"


@pytest.mark.asyncio
async def test_thinking_indicator_animates():
    """ThinkingIndicator cycles through frames."""
    app = WidgetTestApp(ThinkingIndicator)
    async with app.run_test() as pilot:
        indicator = app.query_one(ThinkingIndicator)

        initial_frame = indicator._frame
        # Wait for animation (now at 4Hz = 250ms interval)
        await pilot.pause(0.5)
        # Frame should have changed
        assert indicator._frame != initial_frame or indicator._frame == 0  # May wrap


@pytest.mark.asyncio
async def test_history_search_filters():
    """HistorySearch filters history and cycles through matches."""
    from claudechic.widgets.input.history_search import HistorySearch
    from unittest.mock import patch

    class TestApp(App):
        def compose(self):
            yield HistorySearch(id="history")

    # Mock history data (most recent first)
    mock_history = [
        "fix the bug",
        "add new feature",
        "fix another bug",
        "refactor code",
    ]

    with patch(
        "claudechic.widgets.input.history_search.load_global_history",
        return_value=mock_history,
    ):
        app = TestApp()
        async with app.run_test() as pilot:
            hs = app.query_one(HistorySearch)
            hs.show()
            await pilot.pause()

            # Initially shows most recent match
            assert hs._current_match() == "fix the bug"

            # Type to filter
            from textual.widgets import Input

            inp = hs.query_one("#search-input", Input)
            inp.value = "fix"
            hs.on_input_changed(Input.Changed(inp, "fix"))

            # Should filter to matching entries
            assert len(hs._filtered) == 2
            assert hs._current_match() == "fix the bug"

            # Ctrl+R cycles to next match
            hs.action_next_match()
            assert hs._current_match() == "fix another bug"

            # Up goes back
            hs.action_prev_match()
            assert hs._current_match() == "fix the bug"


@pytest.mark.asyncio
async def test_process_panel_updates():
    """ProcessPanel displays and updates background processes."""
    from datetime import datetime

    app = WidgetTestApp(lambda: ProcessPanel(id="panel", classes="hidden"))
    async with app.run_test():
        panel = app.query_one(ProcessPanel)

        # Initially hidden (no processes)
        assert panel.has_class("hidden")

        # Add some processes
        processes = [
            BackgroundProcess(pid=123, command="sleep 100", start_time=datetime.now()),
            BackgroundProcess(
                pid=456, command="npm run dev", start_time=datetime.now()
            ),
        ]
        panel.update_processes(processes)

        # Processes added, but visibility controlled by set_visible()
        assert panel.process_count == 2
        assert panel.has_class("hidden")  # Still hidden until set_visible(True)

        # Make visible
        panel.set_visible(True)
        assert not panel.has_class("hidden")

        items = list(panel.query(ProcessItem))
        assert len(items) == 2

        # Clear processes - set_visible(True) with no processes still hides
        panel.update_processes([])
        panel.set_visible(True)
        assert panel.has_class("hidden")


# --- Diff token snapping tests ---


def test_snap_to_tokens_expands_partial_spans():
    """Word-diff spans that cut through tokens get expanded to token boundaries."""
    from textual.content import Content, Span
    from claudechic.widgets.content.diff import _snap_to_tokens

    # Simulate syntax-highlighted "activeInsertionOrders:" with tokens:
    # [0-21] identifier, [21-22] punctuation
    content = Content(
        "activeInsertionOrders:",
        spans=[Span(0, 21, "blue"), Span(21, 22, "white")],
    )

    # Span that cuts through the identifier (e.g., highlighting just "active")
    raw_spans = [(0, 6)]
    snapped = _snap_to_tokens(raw_spans, content)

    # Should expand to cover the whole identifier token
    assert snapped == [(0, 21)]


def test_snap_to_tokens_preserves_aligned_spans():
    """Spans already aligned with token boundaries stay unchanged."""
    from textual.content import Content, Span
    from claudechic.widgets.content.diff import _snap_to_tokens

    content = Content(
        "foo bar",
        spans=[Span(0, 3, "red"), Span(3, 4, "white"), Span(4, 7, "blue")],
    )

    # Span exactly matching first token
    raw_spans = [(0, 3)]
    snapped = _snap_to_tokens(raw_spans, content)
    assert snapped == [(0, 3)]


def test_snap_to_tokens_empty_spans():
    """Empty span list returns empty."""
    from textual.content import Content
    from claudechic.widgets.content.diff import _snap_to_tokens

    content = Content("hello")
    assert _snap_to_tokens([], content) == []


def test_word_diff_with_go_syntax():
    """Integration test: word diff + snapping with real Go syntax highlighting."""
    from claudechic.widgets.content.diff import (
        _word_diff_spans,
        _snap_to_tokens,
        _highlight_lines,
    )

    old_line = 'activeOrders: getValue("active",'
    new_line = 'dirtyOrders: getValue("dirty",'

    # Get raw word-diff spans
    old_spans, new_spans = _word_diff_spans(old_line, new_line)

    # Raw spans should identify the changed words
    assert len(old_spans) == 2  # "activeOrders" and "active"
    assert len(new_spans) == 2  # "dirtyOrders" and "dirty"

    # Get syntax-highlighted content
    old_highlighted = _highlight_lines(old_line, "go")
    new_highlighted = _highlight_lines(new_line, "go")

    assert old_highlighted and new_highlighted

    # Snap to token boundaries
    snapped_old = _snap_to_tokens(old_spans, old_highlighted[0])

    # Snapped spans should cover complete tokens
    # The string "active" should expand to include quotes -> "active"
    for start, end in snapped_old:
        text = old_line[start:end]
        # Should not have partial words (no cuts mid-identifier)
        assert not text[0].isalnum() or start == 0 or not old_line[start - 1].isalnum()
