"""Tests for docker_watcher module."""

from __future__ import annotations

from unittest.mock import AsyncMock, MagicMock

import pytest

from webterm.docker_watcher import (
    AUTO_COMMAND_SENTINEL,
    LABEL_NAME,
    THEME_LABEL,
    DockerWatcher,
    _has_webterm_label,
)


@pytest.fixture
def session_manager():
    manager = MagicMock()
    manager.apps_by_slug = {}
    manager.apps = []
    manager.get_session_by_route_key.return_value = None
    return manager


@pytest.fixture
def docker_watcher(session_manager):
    return DockerWatcher(session_manager)


class TestDockerWatcher:
    """Tests for DockerWatcher class."""

    def test_container_to_slug(self, docker_watcher):
        """Test slug generation from container names."""
        # Test basic name
        container = {"Names": ["/my-container"]}
        assert docker_watcher._container_to_slug(container) == "my-container"

        # Test with underscores
        container = {"Names": ["/my_container_name"]}
        assert docker_watcher._container_to_slug(container) == "my-container-name"

        # Test with dots
        container = {"Names": ["/service.name"]}
        assert docker_watcher._container_to_slug(container) == "service-name"

        # Test fallback to ID
        container = {"Id": "abc123def456"}
        assert docker_watcher._container_to_slug(container) == "abc123def456"

    def test_get_container_name(self, docker_watcher):
        """Test extracting container name."""
        container = {"Names": ["/my-container"]}
        assert docker_watcher._get_container_name(container) == "my-container"

        container = {"Names": []}
        container["Id"] = "abc123def456789"
        assert docker_watcher._get_container_name(container) == "abc123def456"

    def test_get_container_command_auto(self, docker_watcher):
        """Test command generation when label is 'auto'."""
        container = {"Names": ["/my-container"], "Labels": {LABEL_NAME: "auto"}}
        assert docker_watcher._get_container_command(container) == AUTO_COMMAND_SENTINEL

    def test_get_container_command_custom(self, docker_watcher):
        """Test command when label has custom value."""
        container = {
            "Names": ["/my-container"],
            "Labels": {LABEL_NAME: "docker logs -f my-container"},
        }
        assert docker_watcher._get_container_command(container) == "docker logs -f my-container"

    def test_get_container_theme(self, docker_watcher):
        container = {"Labels": {THEME_LABEL: "nord"}}
        assert docker_watcher._get_container_theme(container) == "nord"

    def test_get_container_theme_blank(self, docker_watcher):
        container = {"Labels": {THEME_LABEL: "  "}}
        assert docker_watcher._get_container_theme(container) is None

    @pytest.mark.asyncio
    async def test_add_container(self, session_manager):
        """Test adding a container."""
        on_added = MagicMock()
        watcher = DockerWatcher(session_manager, on_container_added=on_added)

        container = {
            "Id": "abc123",
            "Names": ["/test-container"],
            "Labels": {LABEL_NAME: "auto", THEME_LABEL: "monokai"},
        }

        await watcher._add_container(container)

        # Should add to session manager
        session_manager.add_app.assert_called_once()
        call_args = session_manager.add_app.call_args
        assert call_args[0][0] == "test-container"  # name
        assert call_args[0][1] == AUTO_COMMAND_SENTINEL  # command
        assert call_args[0][2] == "test-container"  # slug
        assert call_args[1]["terminal"] is True
        assert call_args[1]["theme"] == "monokai"

        # Should call callback
        on_added.assert_called_once_with("test-container", "test-container", call_args[0][1])

        # Should track container
        assert "test-container" in watcher._managed_containers

    @pytest.mark.asyncio
    async def test_add_container_already_managed(self, session_manager):
        """Test adding a container that's already managed."""
        watcher = DockerWatcher(session_manager)
        watcher._managed_containers["test-container"] = "abc123"

        container = {"Id": "abc123", "Names": ["/test-container"], "Labels": {LABEL_NAME: "auto"}}

        await watcher._add_container(container)

        # Should not add again
        session_manager.add_app.assert_not_called()

    @pytest.mark.asyncio
    async def test_remove_container(self, session_manager):
        """Test removing a container."""
        session_manager.apps_by_slug = {"test-container": MagicMock()}
        session_manager.apps = [session_manager.apps_by_slug["test-container"]]
        session_manager.get_session_by_route_key.return_value = None

        on_removed = MagicMock()
        watcher = DockerWatcher(session_manager, on_container_removed=on_removed)
        watcher._managed_containers["test-container"] = "abc123"

        await watcher._remove_container("abc123")

        # Should remove from tracking
        assert "test-container" not in watcher._managed_containers

        # Should call callback
        on_removed.assert_called_once_with("test-container")

    @pytest.mark.asyncio
    async def test_remove_container_with_active_session(self, session_manager):
        """Test removing a container that has an active session cleans up session."""
        mock_session = MagicMock()
        mock_app = MagicMock()
        session_manager.apps_by_slug = {"test-container": mock_app}
        session_manager.apps = [mock_app]
        session_manager.get_session_by_route_key.return_value = mock_session
        session_manager.routes = MagicMock()
        session_manager.routes.get.return_value = "session-123"
        session_manager.close_session = AsyncMock()

        on_removed = MagicMock()
        watcher = DockerWatcher(session_manager, on_container_removed=on_removed)
        watcher._managed_containers["test-container"] = "abc123"

        await watcher._remove_container("abc123")

        # Session should be closed
        session_manager.close_session.assert_called_once_with("session-123")

        # App should be removed after session cleanup
        assert "test-container" not in session_manager.apps_by_slug
        assert mock_app not in session_manager.apps

        # Container should be untracked
        assert "test-container" not in watcher._managed_containers
        on_removed.assert_called_once_with("test-container")
        """Test removing a container that's not managed."""
        on_removed = MagicMock()
        watcher = DockerWatcher(session_manager, on_container_removed=on_removed)

        await watcher._remove_container("unknown123")

        # Should not call callback
        on_removed.assert_not_called()

    @pytest.mark.asyncio
    async def test_start_stop(self, session_manager):
        """Test starting and stopping the watcher."""
        watcher = DockerWatcher(session_manager, socket_path="/nonexistent.sock")

        # Mock the methods that would fail without Docker
        watcher._get_labeled_containers = AsyncMock(return_value=[])
        watcher._watch_events = AsyncMock()

        await watcher.start()
        assert watcher._running is True

        await watcher.stop()
        assert watcher._running is False


class TestDockerWatcherIntegration:
    """Integration-style tests for Docker watcher."""

    @pytest.mark.asyncio
    async def test_handle_start_event(self, session_manager):
        """Test handling a container start event."""
        watcher = DockerWatcher(session_manager)

        # Mock the docker request to return container info
        async def mock_request(method, path):
            if "/containers/" in path and "/json" in path:
                return (
                    200,
                    '{"Name": "/test-service", "Config": {"Labels": {"webterm-command": "auto"}}}',
                )
            return 404, ""

        watcher._docker_request = mock_request

        event = {
            "Action": "start",
            "Actor": {"ID": "container123", "Attributes": {LABEL_NAME: "auto"}},
        }

        await watcher._handle_event(event)

        # Should add container
        session_manager.add_app.assert_called_once()

    @pytest.mark.asyncio
    async def test_handle_die_event(self, session_manager):
        """Test handling a container die event."""
        session_manager.apps_by_slug = {}
        session_manager.apps = []
        session_manager.get_session_by_route_key.return_value = None

        watcher = DockerWatcher(session_manager)
        watcher._managed_containers["test-service"] = "container123"

        event = {
            "Action": "die",
            "Actor": {"ID": "container123", "Attributes": {LABEL_NAME: "auto"}},
        }

        await watcher._handle_event(event)

        # Should remove container
        assert "test-service" not in watcher._managed_containers

    @pytest.mark.asyncio
    async def test_handle_event_without_label(self, session_manager):
        """Test that events without our label are ignored."""
        watcher = DockerWatcher(session_manager)
        watcher._docker_request = AsyncMock(return_value=(404, ""))

        event = {
            "Action": "start",
            "Actor": {
                "ID": "container123",
                "Attributes": {},  # No label
            },
        }

        await watcher._handle_event(event)

        # Should not add container
        session_manager.add_app.assert_not_called()

    @pytest.mark.asyncio
    async def test_handle_start_event_label_added_after_start(self, session_manager):
        """Container that gains label after start is picked up."""
        watcher = DockerWatcher(session_manager)

        async def mock_request(method, path):
            if "/containers/" in path and "/json" in path:
                return (
                    200,
                    '{"Name": "/test-service", "Config": {"Labels": {"webterm-command": "auto"}}}',
                )
            return 404, ""

        watcher._docker_request = mock_request

        event = {
            "Action": "start",
            "Actor": {
                "ID": "container123",
                "Attributes": {},  # Labels not present on event
            },
        }

        await watcher._handle_event(event)

        session_manager.add_app.assert_called_once()

    @pytest.mark.asyncio
    async def test_handle_start_event_null_labels(self, session_manager):
        """Container with null labels in Docker inspect response is handled gracefully."""
        watcher = DockerWatcher(session_manager)

        async def mock_request(method, path):
            if "/containers/" in path and "/json" in path:
                # Docker returns null for Labels when container has none
                return (
                    200,
                    '{"Name": "/no-labels-container", "Config": {"Labels": null}}',
                )
            return 404, ""

        watcher._docker_request = mock_request

        event = {
            "Action": "start",
            "Actor": {
                "ID": "container456",
                "Attributes": {},
            },
        }

        # Should not raise an exception
        await watcher._handle_event(event)

        # Should not add container (no webterm labels)
        session_manager.add_app.assert_not_called()


@pytest.mark.parametrize(
    ("labels", "expected"),
    [
        ({"webterm-command": "echo hi"}, "echo hi"),
        ({"webterm-command": "auto"}, AUTO_COMMAND_SENTINEL),
        ({"webterm-command": ""}, AUTO_COMMAND_SENTINEL),
        ({"other": "value"}, AUTO_COMMAND_SENTINEL),
    ],
)
def test_get_container_command_variants(docker_watcher, labels, expected):
    container = {"Names": ["/my-container"], "Labels": labels}
    assert docker_watcher._get_container_command(container) == expected


def test_auto_command_env_override(monkeypatch, docker_watcher):
    monkeypatch.setenv("WEBTERM_DOCKER_AUTO_COMMAND", "/bin/sh")
    container = {"Names": ["/my-container"], "Labels": {LABEL_NAME: "auto"}}
    assert docker_watcher._get_container_command(container) == AUTO_COMMAND_SENTINEL


@pytest.mark.asyncio
@pytest.mark.parametrize(
    ("status", "body", "expected"),
    [
        (200, '[{"Id":"abc","Names":["/c1"],"Labels":{"webterm-command":"auto"}}]', 1),
        (200, "[]", 0),
        (500, "error", 0),
    ],
)
async def test_get_labeled_containers_handles_status(
    docker_watcher, status, body, expected, monkeypatch
):
    async def fake_request(method: str, path: str):
        return status, body

    monkeypatch.setattr(docker_watcher, "_docker_request", fake_request)
    result = await docker_watcher._get_labeled_containers()
    assert len(result) == expected


@pytest.mark.asyncio
async def test_watch_events_recovers_from_errors(docker_watcher, monkeypatch):
    docker_watcher._running = True

    async def fail_once(*_args, **_kwargs):
        docker_watcher._running = False
        raise OSError("boom")

    async def fake_sleep(_seconds):
        return None

    monkeypatch.setattr("webterm.docker_watcher.asyncio.open_unix_connection", fail_once)
    monkeypatch.setattr("webterm.docker_watcher.asyncio.sleep", fake_sleep)
    await docker_watcher._watch_events()


class TestHasWebtermLabel:
    """Tests for _has_webterm_label helper."""

    def test_has_command_label(self):
        """Container with webterm-command label is detected."""
        assert _has_webterm_label({LABEL_NAME: "auto"}) is True

    def test_has_theme_label(self):
        """Container with webterm-theme label is detected."""
        assert _has_webterm_label({THEME_LABEL: "dracula"}) is True

    def test_has_both_labels(self):
        """Container with both labels is detected."""
        assert _has_webterm_label({LABEL_NAME: "bash", THEME_LABEL: "dark"}) is True

    def test_no_webterm_labels(self):
        """Container without webterm labels is not detected."""
        assert _has_webterm_label({"other-label": "value"}) is False

    def test_empty_attributes(self):
        """Empty attributes means no labels."""
        assert _has_webterm_label({}) is False


class TestHandleEventWithThemeLabel:
    """Tests for event handling with theme-only labels."""

    @pytest.mark.asyncio
    async def test_handle_start_event_theme_only(self):
        """Container with only theme label is picked up."""
        manager = MagicMock()
        manager.apps_by_slug = {}
        manager.apps = []
        watcher = DockerWatcher(manager)

        async def mock_request(method, path):
            if "/containers/" in path and "/json" in path:
                import json

                return 200, json.dumps(
                    {
                        "Id": "abc123",
                        "Name": "/themed-container",
                        "Config": {"Labels": {THEME_LABEL: "monokai"}},
                    }
                )
            return 404, ""

        watcher._docker_request = mock_request

        event = {
            "Action": "start",
            "Actor": {
                "ID": "abc123",
                "Attributes": {THEME_LABEL: "monokai"},  # Only theme label
            },
        }

        await watcher._handle_event(event)

        # Should add container with auto command
        manager.add_app.assert_called_once()
        call_args = manager.add_app.call_args
        assert call_args[0][1] == AUTO_COMMAND_SENTINEL  # command arg
        assert call_args[1]["theme"] == "monokai"
