"""Integration tests for Cleave TUI.

Tests cover:
- App launch and shutdown
- Backend connection with Claude SDK
- Message send/receive with streaming
- Tool use display
- Session persistence
- Cancellation tokens
- History navigation
- CALF file loading and execution
"""

import tempfile
from pathlib import Path
from unittest.mock import AsyncMock, Mock, patch

import pytest

# Skip all tests if textual not available
pytest.importorskip("textual")
pytest.importorskip("claude_agent_sdk")

from textual.pilot import Pilot

from cleave.tui.app import CleaveTUI
from cleave.tui.backends.base import (
    BackendConfig,
    BackendState,
    Message,
    MessageType,
)
from cleave.tui.backends.claude import ClaudeBackend
from cleave.tui.services.cancellation import CancellationToken
from cleave.tui.services.session_store import SessionStore
from cleave.tui.widgets.chat_panel import ChatPanel


class MockBackend:
    """Mock backend for testing without Claude API calls."""

    def __init__(self):
        self._state = BackendState.DISCONNECTED
        self._error = None

    @property
    def name(self) -> str:
        return "Mock"

    @property
    def state(self) -> BackendState:
        return self._state

    @property
    def error(self) -> str | None:
        return self._error

    async def connect(self, config: BackendConfig) -> None:
        self._state = BackendState.CONNECTED

    async def disconnect(self) -> None:
        self._state = BackendState.DISCONNECTED

    async def query(self, prompt: str):
        """Yield mock streaming messages."""
        yield Message(role="assistant", content="", message_type=MessageType.STREAM_START)
        yield Message(role="assistant", content="Mock ", message_type=MessageType.STREAM_DELTA)
        yield Message(role="assistant", content="response", message_type=MessageType.STREAM_DELTA)
        yield Message(
            role="assistant", content="Mock response", message_type=MessageType.STREAM_END
        )

    @classmethod
    def check_auth(cls) -> tuple[bool, str]:
        return True, "Mock authenticated"

    @classmethod
    def available_models(cls, config: BackendConfig | None = None) -> list[str]:
        return ["mock-model-1"]

    @classmethod
    def default_config(cls) -> BackendConfig:
        return BackendConfig(model="mock-model-1")


@pytest.mark.asyncio
class TestTUILaunchShutdown:
    """Tests for app launch and shutdown."""

    async def test_app_launches_successfully(self):
        """TUI should launch and mount without errors."""
        with tempfile.TemporaryDirectory() as tmpdir:
            app = CleaveTUI(working_directory=Path(tmpdir))
            async with app.run_test() as pilot:
                assert app.is_running
                # Main screen should be pushed
                assert len(app.screen_stack) > 0

    async def test_app_shows_welcome_message(self):
        """TUI should display welcome message on launch."""
        with tempfile.TemporaryDirectory() as tmpdir:
            app = CleaveTUI(working_directory=Path(tmpdir))
            async with app.run_test() as pilot:
                await pilot.pause()
                chat = app.screen.query_one("#chat-widget", ChatPanel)
                assert len(chat._log_entries) > 0

    async def test_app_checks_auth_on_launch(self):
        """TUI should check Claude authentication on launch."""
        with tempfile.TemporaryDirectory() as tmpdir:
            app = CleaveTUI(working_directory=Path(tmpdir))
            async with app.run_test() as pilot:
                await pilot.pause()
                chat = app.screen.query_one("#chat-widget", ChatPanel)
                all_text = " ".join(content for _, content in chat._log_entries).lower()
                assert "authenticate" in all_text or "auth" in all_text or "claude" in all_text

    async def test_app_shuts_down_cleanly(self):
        """TUI should shut down without errors."""
        with tempfile.TemporaryDirectory() as tmpdir:
            app = CleaveTUI(working_directory=Path(tmpdir))
            async with app.run_test() as pilot:
                await pilot.press("q")
                # App should exit
                assert not app.is_running


@pytest.mark.asyncio
class TestBackendConnection:
    """Tests for backend connection management."""

    async def test_claude_backend_auth_check(self):
        """Should check for Claude authentication credentials."""
        is_auth, msg = ClaudeBackend.check_auth()
        assert isinstance(is_auth, bool)
        assert isinstance(msg, str)
        assert len(msg) > 0

    async def test_backend_connection_lifecycle(self):
        """Should connect and disconnect cleanly."""
        backend = MockBackend()
        assert backend.state == BackendState.DISCONNECTED

        config = BackendConfig()
        await backend.connect(config)
        assert backend.state == BackendState.CONNECTED

        await backend.disconnect()
        assert backend.state == BackendState.DISCONNECTED

    async def test_backend_available_models(self):
        """Should return list of available models."""
        models = ClaudeBackend.available_models()
        assert isinstance(models, list)
        assert len(models) > 0
        # Should include standard models
        assert any("sonnet" in m.lower() for m in models)

    async def test_mock_backend_query_streaming(self):
        """Mock backend should produce streaming messages."""
        backend = MockBackend()
        await backend.connect(BackendConfig())

        messages = []
        async for msg in backend.query("test prompt"):
            messages.append(msg)

        # Should have stream start, deltas, and end
        assert len(messages) >= 3
        assert messages[0].message_type == MessageType.STREAM_START
        assert any(m.message_type == MessageType.STREAM_DELTA for m in messages)
        assert messages[-1].message_type == MessageType.STREAM_END


@pytest.mark.asyncio
class TestMessageSendReceive:
    """Tests for message send/receive with streaming."""

    async def test_chat_panel_displays_streaming_message(self):
        """Chat panel should handle streaming message display."""
        with tempfile.TemporaryDirectory() as tmpdir:
            app = CleaveTUI(working_directory=Path(tmpdir))
            async with app.run_test() as pilot:
                await pilot.pause()
                chat = app.screen.query_one("#chat-widget", ChatPanel)
                rich_log = app.screen.query_one("#chat-widget #chat-log")

                # Start streaming
                chat.start_streaming("assistant")
                chat.add_streaming_token("Hello ")
                chat.add_streaming_token("world")
                chat.finalize_streaming()
                await pilot.pause()

                # Finalized stream should be recorded in log entries
                assert any("Hello" in c and "world" in c for _, c in chat._log_entries)
                # Streaming state should be cleared
                assert not chat._is_streaming

    async def test_loading_indicator_control(self):
        """Chat panel should show/hide loading indicator."""
        with tempfile.TemporaryDirectory() as tmpdir:
            app = CleaveTUI(working_directory=Path(tmpdir))
            async with app.run_test() as pilot:
                await pilot.pause()
                chat = app.screen.query_one("#chat-widget", ChatPanel)

                # Enable loading
                chat.set_loading(True)
                await pilot.pause()

                # Disable loading
                chat.set_loading(False)
                await pilot.pause()

                # Should complete without errors


@pytest.mark.asyncio
class TestToolUseDisplay:
    """Tests for tool use display in TUI."""

    async def test_tool_invocation_display(self):
        """Should display tool invocations in chat."""
        with tempfile.TemporaryDirectory() as tmpdir:
            app = CleaveTUI(working_directory=Path(tmpdir))
            async with app.run_test() as pilot:
                await pilot.pause()
                chat = app.screen.query_one("#chat-widget", ChatPanel)
                rich_log = app.screen.query_one("#chat-widget #chat-log")

                # Add a tool use via the tool display API
                tool_id = chat.add_tool_use("Read", {"file_path": "/test.py"})
                await pilot.pause()

                # Tool display should be visible
                tool_display = app.screen.query_one("#chat-widget #tool-display")
                assert "has-tools" in tool_display.classes


@pytest.mark.asyncio
class TestSessionPersistence:
    """Tests for session persistence."""

    async def test_session_store_creates_conversations(self):
        """Should create and persist conversations."""
        with tempfile.TemporaryDirectory() as tmpdir:
            store = SessionStore(Path(tmpdir))
            conv = store.create("Test Session")

            assert conv.id.startswith("conv-")
            assert conv.title == "Test Session"

    async def test_session_store_saves_and_loads(self):
        """Should save and reload conversations."""
        with tempfile.TemporaryDirectory() as tmpdir:
            store = SessionStore(Path(tmpdir))
            conv = store.create("Test Session")
            conv.add_message("user", "Hello")
            conv.add_message("assistant", "Hi there")

            store.save(conv)

            # Reload
            loaded = store.load(conv.id)
            assert loaded is not None
            assert loaded.title == "Test Session"
            assert len(loaded.messages) == 2


@pytest.mark.asyncio
class TestCancellationToken:
    """Tests for cancellation token integration."""

    async def test_cancellation_token_basic_lifecycle(self):
        """Cancellation token should support cancel/reset."""
        token = CancellationToken()
        assert not token.is_cancelled

        token.cancel()
        assert token.is_cancelled

        token.reset()
        assert not token.is_cancelled

    async def test_cancellation_during_operation(self):
        """Should handle cancellation during async operations."""
        token = CancellationToken()

        async def long_operation():
            for i in range(10):
                if token.is_cancelled:
                    return "cancelled"
                await asyncio.sleep(0.01)
            return "completed"

        # Start operation and cancel midway
        import asyncio

        task = asyncio.create_task(long_operation())
        await asyncio.sleep(0.03)
        token.cancel()
        result = await task

        assert result == "cancelled"


@pytest.mark.asyncio
class TestHistoryNavigation:
    """Tests for history navigation in chat input."""

    async def test_input_history_previous_next(self):
        """Should navigate through input history."""
        from cleave.tui.services.history import InputHistory

        history = InputHistory()
        history.add("first command")
        history.add("second command")
        history.add("third command")

        # Navigate backwards
        assert history.previous() == "third command"
        assert history.previous() == "second command"

        # Navigate forwards
        assert history.next() == "third command"

    async def test_input_history_persistence(self):
        """Should persist history to disk."""
        from cleave.tui.services.history import InputHistory

        with tempfile.TemporaryDirectory() as tmpdir:
            history_file = Path(tmpdir) / "history.txt"

            # Create and populate
            hist1 = InputHistory(history_file=history_file)
            hist1.add("command 1")
            hist1.add("command 2")

            # Reload
            hist2 = InputHistory(history_file=history_file)
            assert len(hist2) == 2
            assert hist2.previous() == "command 2"


@pytest.mark.asyncio
class TestCALFFileLoading:
    """Tests for CALF file loading and execution."""

    async def test_calf_manager_list_files(self):
        """Should list .calf files in directory."""
        from cleave.tui.services.calf import CalfManager

        with tempfile.TemporaryDirectory() as tmpdir:
            workdir = Path(tmpdir)
            (workdir / "test1.calf").write_text("Directive 1")
            (workdir / "test2.calf").write_text("Directive 2")

            manager = CalfManager(workdir)
            calves = manager.list_calves()

            assert len(calves) == 2
            assert all(c.suffix == ".calf" for c in calves)

    async def test_calf_manager_read_write(self):
        """Should read and write .calf files."""
        from cleave.tui.services.calf import CalfManager

        with tempfile.TemporaryDirectory() as tmpdir:
            workdir = Path(tmpdir)
            manager = CalfManager(workdir)

            # Write
            test_path = workdir / "test.calf"
            test_content = "Implement feature X with Y constraints"
            manager.write_calf(test_path, test_content)

            # Read
            content = manager.read_calf(test_path)
            assert content == test_content

    async def test_calf_manager_generate_filename(self):
        """Should generate unique filenames from directives."""
        from cleave.tui.services.calf import CalfManager

        with tempfile.TemporaryDirectory() as tmpdir:
            workdir = Path(tmpdir)
            manager = CalfManager(workdir)

            # Generate from hint
            filename = manager.generate_filename("Implement user authentication system")
            assert filename.suffix == ".calf"
            assert "implement" in filename.stem.lower()
            assert "user" in filename.stem.lower()

    async def test_tui_loads_calf_file(self):
        """TUI should load and display .calf file when selected."""
        with tempfile.TemporaryDirectory() as tmpdir:
            workdir = Path(tmpdir)
            calf_file = workdir / "test-directive.calf"
            calf_file.write_text("Add comprehensive error handling")

            app = CleaveTUI(working_directory=workdir)
            async with app.run_test() as pilot:
                await pilot.pause()

                # Simulate file selection
                from cleave.tui.widgets.control_panel import ControlPanel

                control = app.screen.query_one("#control-widget", ControlPanel)
                control.refresh_files()
                await pilot.pause()

                # Should detect the .calf file
                calves = list(workdir.glob("*.calf"))
                assert len(calves) == 1


@pytest.mark.asyncio
class TestNegativeCases:
    """Negative test cases for error handling."""

    async def test_backend_auth_failure(self):
        """Should handle authentication failures gracefully."""
        backend = ClaudeBackend()
        config = BackendConfig()

        # If not authenticated, should raise or handle gracefully
        is_auth, msg = ClaudeBackend.check_auth()
        if not is_auth:
            # Connection should fail with auth error
            with pytest.raises(Exception):  # Could be BackendAuthError
                await backend.connect(config)

    async def test_invalid_calf_file(self):
        """Should handle missing/invalid .calf files."""
        from cleave.tui.services.calf import CalfManager

        with tempfile.TemporaryDirectory() as tmpdir:
            manager = CalfManager(Path(tmpdir))

            # Try to read non-existent file
            with pytest.raises(FileNotFoundError):
                manager.read_calf(Path(tmpdir) / "nonexistent.calf")

    async def test_chat_input_empty_submission(self):
        """Should handle empty chat submissions gracefully."""
        with tempfile.TemporaryDirectory() as tmpdir:
            app = CleaveTUI(working_directory=Path(tmpdir))
            async with app.run_test() as pilot:
                await pilot.pause()

                chat = app.screen.query_one("#chat-widget", ChatPanel)
                initial_count = len(chat._log_entries)

                # Simulate empty submission via ChatPanel's handler
                chat._handle_submit("")
                await pilot.pause()

                # Empty submission should not add new entries
                assert len(chat._log_entries) == initial_count
