"""Tests for TUI interactivity features.

Tests cover:
- Tool use display widget
- Input history navigation
- Cancellation tokens
"""

import asyncio
import tempfile
from pathlib import Path

import pytest

# Test core modules (no textual dependency)
from cleave.tui.services.cancellation import (
    CancellationError,
    CancellationScope,
    CancellationToken,
)
from cleave.tui.services.history import InputHistory


class TestInputHistory:
    """Tests for input history navigation."""

    def test_add_entry(self):
        """Should add entries to history."""
        history = InputHistory()
        history.add("first")
        history.add("second")
        assert len(history) == 2

    def test_add_ignores_empty(self):
        """Should ignore empty entries."""
        history = InputHistory()
        history.add("")
        history.add("   ")
        assert len(history) == 0

    def test_add_ignores_duplicates(self):
        """Should not add consecutive duplicates."""
        history = InputHistory()
        history.add("command")
        history.add("command")
        assert len(history) == 1

    def test_previous_navigation(self):
        """Should navigate backwards through history."""
        history = InputHistory()
        history.add("first")
        history.add("second")
        history.add("third")

        assert history.previous() == "third"
        assert history.previous() == "second"
        assert history.previous() == "first"
        # At oldest, should stay on oldest
        assert history.previous() == "first"

    def test_next_navigation(self):
        """Should navigate forwards through history."""
        history = InputHistory()
        history.add("first")
        history.add("second")

        # Navigate back
        history.previous()
        history.previous()

        # Navigate forward
        assert history.next() == "second"
        assert history.next() == ""  # Back to current input

    def test_saves_current_input(self):
        """Should save current input when starting navigation."""
        history = InputHistory()
        history.add("old")

        # Navigate with current input
        history.previous("current typing")
        assert history.next() == "current typing"

    def test_reset_position(self):
        """Should reset navigation position."""
        history = InputHistory()
        history.add("first")
        history.previous()
        history.reset_position()
        assert history.previous() == "first"  # Starts from beginning again

    def test_search_prefix(self):
        """Should search for entries by prefix."""
        history = InputHistory()
        history.add("git status")
        history.add("git commit")
        history.add("make build")
        history.add("git push")

        results = list(history.search("git"))
        assert len(results) == 3
        assert results[0] == "git push"  # Most recent first

    def test_search_contains(self):
        """Should search for entries containing substring."""
        history = InputHistory()
        history.add("git status")
        history.add("npm test")
        history.add("git commit -m 'test'")

        results = list(history.search_contains("test"))
        assert len(results) == 2

    def test_persistence(self):
        """Should persist history to file."""
        with tempfile.TemporaryDirectory() as tmpdir:
            history_file = Path(tmpdir) / "history"

            # Create and populate history
            history1 = InputHistory(history_file=history_file)
            history1.add("command1")
            history1.add("command2")

            # Create new instance - should load
            history2 = InputHistory(history_file=history_file)
            assert len(history2) == 2
            assert history2.previous() == "command2"

    def test_max_size(self):
        """Should respect max_size limit."""
        history = InputHistory(max_size=3)
        history.add("one")
        history.add("two")
        history.add("three")
        history.add("four")

        assert len(history) == 3
        # Oldest should be evicted
        entries = list(history)
        assert "one" not in entries
        assert "four" in entries


class TestCancellationToken:
    """Tests for cancellation token."""

    def test_initial_state(self):
        """Should start not cancelled."""
        token = CancellationToken()
        assert not token.is_cancelled

    def test_cancel(self):
        """Should set cancelled state."""
        token = CancellationToken()
        token.cancel()
        assert token.is_cancelled

    def test_cancel_idempotent(self):
        """Cancelling multiple times should be safe."""
        token = CancellationToken()
        token.cancel()
        token.cancel()
        assert token.is_cancelled

    def test_callback_on_cancel(self):
        """Should invoke callbacks on cancel."""
        token = CancellationToken()
        called = []
        token.on_cancel(lambda: called.append(True))
        token.cancel()
        assert len(called) == 1

    def test_callback_immediate_if_cancelled(self):
        """Should invoke callback immediately if already cancelled."""
        token = CancellationToken()
        token.cancel()
        called = []
        token.on_cancel(lambda: called.append(True))
        assert len(called) == 1

    def test_reset(self):
        """Should reset to initial state."""
        token = CancellationToken()
        token.cancel()
        token.reset()
        assert not token.is_cancelled

    def test_raise_if_cancelled(self):
        """Should raise CancellationError if cancelled."""
        token = CancellationToken()
        token.cancel()
        with pytest.raises(CancellationError):
            token.raise_if_cancelled()

    def test_raise_if_not_cancelled(self):
        """Should not raise if not cancelled."""
        token = CancellationToken()
        token.raise_if_cancelled()  # Should not raise


class TestCancellationScope:
    """Tests for cancellation scope context manager."""

    def test_token_access(self):
        """Should provide access to token."""
        scope = CancellationScope()
        assert not scope.is_cancelled
        scope.cancel()
        assert scope.is_cancelled

    def test_auto_cancel_on_exit(self):
        """Should auto-cancel on context exit."""
        scope = CancellationScope()
        with scope:
            pass
        assert scope.is_cancelled


class TestCancellationTokenAsync:
    """Async tests for cancellation token."""

    def test_wait_returns_on_cancel(self):
        """Wait should return when cancelled."""

        async def run_test():
            token = CancellationToken()

            async def cancel_later():
                await asyncio.sleep(0.01)
                token.cancel()

            asyncio.create_task(cancel_later())
            result = await token.wait(timeout=1.0)
            return result

        result = asyncio.run(run_test())
        assert result is True

    def test_wait_timeout(self):
        """Wait should return False on timeout."""

        async def run_test():
            token = CancellationToken()
            result = await token.wait(timeout=0.01)
            return result

        result = asyncio.run(run_test())
        assert result is False


# Tests requiring textual
pytest.importorskip("textual")

from cleave.tui.widgets.tool_use import ToolInvocation, ToolUseBlock, ToolUseDisplay


class TestToolInvocation:
    """Tests for ToolInvocation dataclass."""

    def test_default_state(self):
        """Should have pending status by default."""
        inv = ToolInvocation(name="Read")
        assert inv.status == "pending"
        assert not inv.is_complete

    def test_is_complete(self):
        """Should report completion correctly."""
        inv = ToolInvocation(name="Read")
        inv.status = "success"
        assert inv.is_complete

        inv.status = "error"
        assert inv.is_complete

        inv.status = "running"
        assert not inv.is_complete

    def test_status_icon(self):
        """Should return appropriate icons."""
        inv = ToolInvocation(name="Read")
        assert inv.status_icon == "⏳"

        inv.status = "running"
        assert inv.status_icon == "⚙️"

        inv.status = "success"
        assert inv.status_icon == "✓"

        inv.status = "error"
        assert inv.status_icon == "✗"


class TestToolUseBlock:
    """Tests for ToolUseBlock widget."""

    def test_widget_exists(self):
        """ToolUseBlock class should be importable."""
        assert ToolUseBlock is not None

    def test_has_status_methods(self):
        """Should have status update methods."""
        assert hasattr(ToolUseBlock, "set_status")
        assert hasattr(ToolUseBlock, "set_output")
        assert hasattr(ToolUseBlock, "set_error")
        assert hasattr(ToolUseBlock, "set_duration")


class TestToolUseDisplay:
    """Tests for ToolUseDisplay container."""

    def test_widget_exists(self):
        """ToolUseDisplay class should be importable."""
        assert ToolUseDisplay is not None

    def test_has_management_methods(self):
        """Should have tool management methods."""
        assert hasattr(ToolUseDisplay, "add_tool")
        assert hasattr(ToolUseDisplay, "update_tool")
        assert hasattr(ToolUseDisplay, "get_tool")
        assert hasattr(ToolUseDisplay, "clear")
