"""Tests for session_manager module."""

import platform
from unittest.mock import AsyncMock, MagicMock, patch

import pytest

from webterm.config import App
from webterm.docker_watcher import AUTO_COMMAND_SENTINEL
from webterm.session_manager import SessionManager
from webterm.types import RouteKey, SessionID


class TestSessionManager:
    """Tests for SessionManager class."""

    @pytest.fixture
    def mock_poller(self):
        """Create a mock poller."""
        return MagicMock()

    @pytest.fixture
    def mock_path(self, tmp_path):
        """Create a mock path."""
        return tmp_path

    @pytest.fixture
    def sample_apps(self):
        """Create sample apps."""
        return [
            App(name="Test Terminal", slug="terminal", path="./", command="bash", terminal=True),
            App(name="Test App", slug="app", path="./", command="python app.py", terminal=False),
        ]

    def test_init(self, mock_poller, mock_path, sample_apps):
        """Test SessionManager initialization."""
        manager = SessionManager(mock_poller, mock_path, sample_apps)

        assert manager.poller == mock_poller
        assert manager.path == mock_path
        assert len(manager.apps) == 2
        assert "terminal" in manager.apps_by_slug
        assert "app" in manager.apps_by_slug
        assert len(manager.sessions) == 0
        assert len(manager.routes) == 0

    def test_get_default_app(self, mock_poller, mock_path, sample_apps):
        """Test getting the default app."""
        manager = SessionManager(mock_poller, mock_path, sample_apps)
        assert manager.get_default_app() == sample_apps[0]

    def test_get_default_app_empty(self, mock_poller, mock_path):
        """Test getting the default app when no apps are configured."""
        manager = SessionManager(mock_poller, mock_path, [])
        assert manager.get_default_app() is None

    def test_add_app(self, mock_poller, mock_path):
        """Test adding an app."""
        manager = SessionManager(mock_poller, mock_path, [])

        manager.add_app("New App", "python new.py", "newapp", terminal=False)

        assert len(manager.apps) == 1
        assert "newapp" in manager.apps_by_slug
        assert manager.apps_by_slug["newapp"].name == "New App"

    def test_add_app_auto_slug(self, mock_poller, mock_path):
        """Test adding an app with auto-generated slug."""
        manager = SessionManager(mock_poller, mock_path, [])

        manager.add_app("Auto App", "python auto.py", "", terminal=False)

        assert len(manager.apps) == 1
        # Slug should be auto-generated
        assert len(manager.apps[0].slug) > 0

    def test_get_session_not_found(self, mock_poller, mock_path, sample_apps):
        """Test getting a non-existent session."""
        manager = SessionManager(mock_poller, mock_path, sample_apps)

        result = manager.get_session(SessionID("nonexistent"))
        assert result is None

    def test_get_session_by_route_key_not_found(self, mock_poller, mock_path, sample_apps):
        """Test getting session by non-existent route key."""
        manager = SessionManager(mock_poller, mock_path, sample_apps)

        result = manager.get_session_by_route_key(RouteKey("nonexistent"))
        assert result is None

    def test_on_session_end(self, mock_poller, mock_path, sample_apps):
        """Test session end cleanup."""
        manager = SessionManager(mock_poller, mock_path, sample_apps)

        # Manually add a session
        session_id = SessionID("test-session")
        route_key = RouteKey("test-route")
        mock_session = MagicMock()
        manager.sessions[session_id] = mock_session
        manager.routes[route_key] = session_id

        # End session
        manager.on_session_end(session_id)

        assert session_id not in manager.sessions
        assert route_key not in manager.routes

    def test_on_session_end_idempotent(self, mock_poller, mock_path, sample_apps):
        """Test session end cleanup is idempotent."""
        manager = SessionManager(mock_poller, mock_path, sample_apps)

        session_id = SessionID("test-session")
        route_key = RouteKey("test-route")
        manager.sessions[session_id] = MagicMock()
        manager.routes[route_key] = session_id

        manager.on_session_end(session_id)
        manager.on_session_end(session_id)

        assert session_id not in manager.sessions
        assert route_key not in manager.routes

    def test_on_session_end_nonexistent(self, mock_poller, mock_path, sample_apps):
        """Test session end for non-existent session."""
        manager = SessionManager(mock_poller, mock_path, sample_apps)

        # Should not raise
        manager.on_session_end(SessionID("nonexistent"))

    @pytest.mark.asyncio
    async def test_close_all_empty(self, mock_poller, mock_path, sample_apps):
        """Test closing all sessions when empty."""
        manager = SessionManager(mock_poller, mock_path, sample_apps)

        # Should not raise
        await manager.close_all()

    @pytest.mark.asyncio
    async def test_close_all_with_sessions(self, mock_poller, mock_path, sample_apps):
        """Test closing all sessions."""
        manager = SessionManager(mock_poller, mock_path, sample_apps)

        # Add mock sessions
        mock_session = MagicMock()
        mock_session.close = AsyncMock()
        mock_session.wait = AsyncMock()
        manager.sessions[SessionID("s1")] = mock_session

        await manager.close_all(timeout=1.0)

        mock_session.close.assert_called_once()

    @pytest.mark.asyncio
    async def test_close_session(self, mock_poller, mock_path, sample_apps):
        """Test closing a specific session removes it from tracking."""
        manager = SessionManager(mock_poller, mock_path, sample_apps)

        mock_session = MagicMock()
        mock_session.close = AsyncMock()
        session_id = SessionID("test-session")
        route_key = RouteKey("test-route")
        manager.sessions[session_id] = mock_session
        manager.routes[route_key] = session_id

        await manager.close_session(session_id)

        mock_session.close.assert_called_once()
        assert session_id not in manager.sessions
        assert route_key not in manager.routes

    @pytest.mark.asyncio
    async def test_close_session_nonexistent(self, mock_poller, mock_path, sample_apps):
        """Test closing a non-existent session."""
        manager = SessionManager(mock_poller, mock_path, sample_apps)

        # Should not raise
        await manager.close_session(SessionID("nonexistent"))

    @pytest.mark.asyncio
    async def test_new_session_no_app(self, mock_poller, mock_path):
        """Test creating session with no matching app."""
        manager = SessionManager(mock_poller, mock_path, [])

        result = await manager.new_session(
            "nonexistent",
            SessionID("test"),
            RouteKey("route"),
        )

        assert result is None

    @pytest.mark.asyncio
    @pytest.mark.skipif(platform.system() == "Windows", reason="Terminal not supported on Windows")
    async def test_new_terminal_session(self, mock_poller, mock_path):
        """Test creating a new terminal session."""
        from webterm.terminal_session import TerminalSession

        app = App(name="Terminal", slug="term", path="./", command="echo test", terminal=True)
        manager = SessionManager(mock_poller, mock_path, [app])

        with patch.object(TerminalSession, "open", new_callable=AsyncMock):
            result = await manager.new_session(
                "term",
                SessionID("test-session"),
                RouteKey("test-route"),
            )

            assert result is not None
            assert isinstance(result, TerminalSession)
            assert SessionID("test-session") in manager.sessions
            assert RouteKey("test-route") in manager.routes

    @pytest.mark.asyncio
    @pytest.mark.skipif(platform.system() == "Windows", reason="Terminal not supported on Windows")
    async def test_new_docker_exec_session(self, mock_poller, mock_path):
        from webterm.docker_exec_session import DockerExecSession

        app = App(
            name="my-container",
            slug="my-container",
            path="./",
            command=AUTO_COMMAND_SENTINEL,
            terminal=True,
        )
        manager = SessionManager(mock_poller, mock_path, [app])

        with patch.object(DockerExecSession, "open", new_callable=AsyncMock):
            result = await manager.new_session(
                "my-container",
                SessionID("test-session"),
                RouteKey("test-route"),
            )

            assert result is not None
            assert isinstance(result, DockerExecSession)
            assert result.exec_spec.user is None

    async def test_new_docker_exec_session_with_user(self, mock_poller, mock_path, monkeypatch):
        from webterm.docker_exec_session import DockerExecSession

        monkeypatch.setenv("WEBTERM_DOCKER_USERNAME", "testuser")

        app = App(
            name="my-container",
            slug="my-container",
            path="./",
            command=AUTO_COMMAND_SENTINEL,
            terminal=True,
        )
        manager = SessionManager(mock_poller, mock_path, [app])

        with patch.object(DockerExecSession, "open", new_callable=AsyncMock):
            result = await manager.new_session(
                "my-container",
                SessionID("test-session"),
                RouteKey("test-route"),
            )

            assert result is not None
            assert isinstance(result, DockerExecSession)
            assert result.exec_spec.user == "testuser"

    @pytest.mark.asyncio
    @pytest.mark.skipif(platform.system() == "Windows", reason="Terminal not supported on Windows")
    async def test_new_docker_exec_session_container_placeholder(
        self, mock_poller, mock_path, monkeypatch
    ):
        """Test that {container} placeholder in auto command is replaced with container name."""
        from webterm.docker_exec_session import DockerExecSession

        monkeypatch.setenv("WEBTERM_DOCKER_AUTO_COMMAND", "tmux new-session -ADs {container}")

        app = App(
            name="my-webapp",
            slug="my-webapp",
            path="./",
            command=AUTO_COMMAND_SENTINEL,
            terminal=True,
        )
        manager = SessionManager(mock_poller, mock_path, [app])

        with patch.object(DockerExecSession, "open", new_callable=AsyncMock):
            result = await manager.new_session(
                "my-webapp",
                SessionID("test-session"),
                RouteKey("test-route"),
            )

            assert result is not None
            assert isinstance(result, DockerExecSession)
            # Verify the container name was substituted into the command
            assert result.exec_spec.command == ["tmux", "new-session", "-ADs", "my-webapp"]


class TestSessionManagerRoutes:
    """Tests for SessionManager route handling."""

    @pytest.fixture
    def manager(self, tmp_path):
        """Create a session manager with mock poller."""
        mock_poller = MagicMock()
        return SessionManager(mock_poller, tmp_path, [])

    def test_route_mapping(self, manager):
        """Test route to session mapping."""
        session_id = SessionID("session1")
        route_key = RouteKey("route1")

        manager.routes[route_key] = session_id

        assert manager.routes.get(route_key) == session_id
        assert manager.routes.get_key(session_id) == route_key

    def test_get_session_by_route(self, manager):
        """Test getting session by route key."""
        session_id = SessionID("session1")
        route_key = RouteKey("route1")
        mock_session = MagicMock()

        manager.sessions[session_id] = mock_session
        manager.routes[route_key] = session_id

        result = manager.get_session_by_route_key(route_key)
        assert result == mock_session

    def test_get_first_running_session_none(self, manager):
        """Test getting first running session when empty."""
        assert manager.get_first_running_session() is None

    def test_get_first_running_session_found(self, manager):
        """Test getting first running session."""
        session_id = SessionID("s1")
        route_key = RouteKey("r1")
        mock_session = MagicMock()
        mock_session.is_running.return_value = True

        manager.sessions[session_id] = mock_session
        manager.routes[route_key] = session_id

        result = manager.get_first_running_session()
        assert result == (route_key, mock_session)
