"""Integration tests for event producer → notification creation flow."""

import uuid
from collections.abc import Generator
from unittest.mock import patch

import pytest
import pytest_asyncio
from sqlalchemy import select
from sqlalchemy.ext.asyncio import AsyncSession

from src.models.project.git_repository import GitRepository
from src.models.system.notification import Notification
from src.models.project.project import Project
from src.models.tool.tool_instance import ToolInstance
from src.models.tool.tool_type import ToolType
from src.models.user.user import User
from src.services.instance.event_bus import InstanceEventBus, InstanceEventPayload
from src.services.instance.health_monitor import HealthSnapshot


@pytest.fixture
def event_bus() -> Generator[InstanceEventBus, None, None]:
    """Provide a fresh EventBus instance."""
    bus = InstanceEventBus()
    bus._reset_for_testing()
    yield bus
    bus._reset_for_testing()


@pytest_asyncio.fixture
async def test_instance(db_session: AsyncSession) -> ToolInstance:
    """Create a complete tool instance with all required relations."""
    user = User(
        id=uuid.uuid4(),
        email="owner@headquarter.local",
        name="Owner",
        authentik_id=f"authentik-{uuid.uuid4()}",
        avatar_url=None,
    )
    db_session.add(user)
    await db_session.commit()

    project = Project(
        id=uuid.uuid4(),
        name="test-project",
        description="Test",
        owner_id=user.id,
    )
    repo = GitRepository(
        id=uuid.uuid4(),
        name="test-repo",
        path="/tmp/test-repo",
        project_id=project.id,
        owner_id=user.id,
        remote_url="https://github.com/test/repo.git",
    )
    tool_type = ToolType(
        id=uuid.uuid4(),
        name="test-tool",
        display_name="Test Tool",
        category="other",
        interface_type="web",
        requires_port=True,
        default_port=8080,
        definition_type="legacy",
        compose_template="version: '3.8'\nservices:\n  app:\n    image: alpine\n    command: sleep 3600\n",
    )
    db_session.add_all([project, repo, tool_type])
    await db_session.commit()

    instance = ToolInstance(
        id=uuid.uuid4(),
        name="test-instance",
        display_name="Test Instance",
        tool_type_id=tool_type.id,
        repository_id=repo.id,
        project_id=project.id,
        owner_id=user.id,
        status="running",
        container_id="container123",
        compose_path="/tmp/test-compose.yml",
        port=8080,
    )
    db_session.add(instance)
    await db_session.commit()
    return instance


@pytest.mark.asyncio
@pytest.mark.integration
async def test_lifecycle_started_intermediate_skips_notification(
    db_session: AsyncSession,
    event_bus: InstanceEventBus,
    test_instance: ToolInstance,
) -> None:
    """Intermediate 'starting' state does NOT create a notification."""
    received: list[InstanceEventPayload] = []

    def subscriber(payload: InstanceEventPayload) -> None:
        received.append(payload)

    event_bus.subscribe("instance.started", subscriber)

    from src.services.instance.lifecycle_hooks import publish_lifecycle_event

    await publish_lifecycle_event(
        event_bus=event_bus,
        session=db_session,
        instance=test_instance,
        event_type="instance.started",
        status="starting",
        message="Container starting...",
    )

    # Event still published
    assert len(received) == 1

    # No notification created for intermediate state
    result = await db_session.execute(
        select(Notification).where(Notification.user_id == test_instance.owner_id)
    )
    notifications = list(result.scalars().all())
    assert len(notifications) == 0


@pytest.mark.asyncio
@pytest.mark.integration
async def test_lifecycle_running_creates_notification(
    db_session: AsyncSession,
    event_bus: InstanceEventBus,
    test_instance: ToolInstance,
) -> None:
    """Successful terminal state (running) creates a notification."""
    from src.services.instance.lifecycle_hooks import publish_lifecycle_event

    await publish_lifecycle_event(
        event_bus=event_bus,
        session=db_session,
        instance=test_instance,
        event_type="instance.health_changed",
        status="running",
        message="Container running",
    )

    result = await db_session.execute(
        select(Notification).where(Notification.user_id == test_instance.owner_id)
    )
    notifications = list(result.scalars().all())
    assert len(notifications) == 1
    n = notifications[0]
    assert n.category == "instance"
    assert n.severity == "success"
    assert n.title == "Container ready"
    assert n.source_type == "tool_instances"
    assert n.source_id == test_instance.id


@pytest.mark.asyncio
@pytest.mark.integration
async def test_health_monitor_error_creates_notification(
    db_session: AsyncSession,
    event_bus: InstanceEventBus,
    test_instance: ToolInstance,
) -> None:
    """Simulating a container exit creates an error notification."""
    from src.services.instance.health_monitor import HealthMonitor

    monitor = HealthMonitor(event_bus)
    # Seed a different healthy prior state so the exit is treated as a change.
    monitor._last_known_state[test_instance.id] = HealthSnapshot(
        container_status="running",
        container_healthy=None,
        tunnel_healthy=True,
        exit_code=None,
    )

    received: list[InstanceEventPayload] = []

    def subscriber(payload: InstanceEventPayload) -> None:
        received.append(payload)

    event_bus.subscribe("instance.error", subscriber)

    with patch(
        "src.services.instance.health_monitor.get_container_status",
        return_value={"status": "exited", "exit_code": 137, "health": None},
    ):
        await monitor._check_instance(db_session, test_instance)

    # Event published
    assert len(received) == 1

    # Notification created
    result = await db_session.execute(
        select(Notification).where(Notification.user_id == test_instance.owner_id)
    )
    notifications = list(result.scalars().all())
    assert len(notifications) == 1
    n = notifications[0]
    assert n.category == "instance"
    assert n.severity == "error"
    assert n.source_type == "tool_instances"
    assert n.source_id == test_instance.id


@pytest.mark.asyncio
@pytest.mark.integration
async def test_notification_failure_does_not_block_event_pipeline(
    db_session: AsyncSession,
    event_bus: InstanceEventBus,
    test_instance: ToolInstance,
) -> None:
    """If NotificationService raises, the event is still published and no exception escapes."""
    received: list[InstanceEventPayload] = []

    def subscriber(payload: InstanceEventPayload) -> None:
        received.append(payload)

    event_bus.subscribe("instance.started", subscriber)

    from src.services.instance.lifecycle_hooks import publish_lifecycle_event

    with patch(
        "src.services.instance.lifecycle_hooks.notification_service.create_notification",
        side_effect=RuntimeError("DB is down"),
    ):
        # Should not raise
        await publish_lifecycle_event(
            event_bus=event_bus,
            session=db_session,
            instance=test_instance,
            event_type="instance.started",
            status="starting",
            message="Container started",
        )

    assert len(received) == 1
    assert received[0]["event"] == "instance.started"

    # No notification should have been created
    result = await db_session.execute(
        select(Notification).where(Notification.user_id == test_instance.owner_id)
    )
    assert result.scalar_one_or_none() is None


@pytest.mark.asyncio
@pytest.mark.integration
async def test_notification_ownership_matches_instance_owner(
    db_session: AsyncSession,
    event_bus: InstanceEventBus,
) -> None:
    """Notification user_id matches the instance owner, not any caller."""
    # Create a caller user (simulates the user making an API request)
    caller = User(
        id=uuid.uuid4(),
        email="caller@headquarter.local",
        name="Caller",
        authentik_id=f"authentik-{uuid.uuid4()}",
        avatar_url=None,
    )
    db_session.add(caller)
    await db_session.commit()

    # Create the actual owner
    owner = User(
        id=uuid.uuid4(),
        email="owner@headquarter.local",
        name="Owner",
        authentik_id=f"authentik-{uuid.uuid4()}",
        avatar_url=None,
    )
    db_session.add(owner)
    await db_session.commit()

    project = Project(
        id=uuid.uuid4(),
        name="test-project",
        description="Test",
        owner_id=owner.id,
    )
    repo = GitRepository(
        id=uuid.uuid4(),
        name="test-repo",
        path="/tmp/test-repo",
        project_id=project.id,
        owner_id=owner.id,
        remote_url="https://github.com/test/repo.git",
    )
    tool_type = ToolType(
        id=uuid.uuid4(),
        name="test-tool",
        display_name="Test Tool",
        category="other",
        interface_type="web",
        requires_port=True,
        default_port=8080,
        definition_type="legacy",
        compose_template="version: '3.8'\nservices:\n  app:\n    image: alpine\n    command: sleep 3600\n",
    )
    db_session.add_all([project, repo, tool_type])
    await db_session.commit()

    instance = ToolInstance(
        id=uuid.uuid4(),
        name="test-instance",
        display_name="Test Instance",
        tool_type_id=tool_type.id,
        repository_id=repo.id,
        project_id=project.id,
        owner_id=owner.id,
        status="running",
        compose_path="/tmp/test-compose.yml",
        port=8080,
    )
    db_session.add(instance)
    await db_session.commit()

    from src.services.instance.lifecycle_hooks import publish_lifecycle_event

    await publish_lifecycle_event(
        event_bus=event_bus,
        session=db_session,
        instance=instance,
        event_type="instance.health_changed",
        status="running",
        message="Container running",
    )

    result = await db_session.execute(
        select(Notification).where(Notification.source_id == instance.id)
    )
    n = result.scalar_one()
    assert n.user_id == owner.id
    assert n.user_id != caller.id


@pytest.mark.asyncio
@pytest.mark.integration
async def test_lifecycle_error_creates_error_notification(
    db_session: AsyncSession,
    event_bus: InstanceEventBus,
    test_instance: ToolInstance,
) -> None:
    """An instance.error lifecycle event creates a severity=error notification."""
    from src.services.instance.lifecycle_hooks import publish_lifecycle_event

    await publish_lifecycle_event(
        event_bus=event_bus,
        session=db_session,
        instance=test_instance,
        event_type="instance.error",
        status="error",
        message="Container failed",
    )

    result = await db_session.execute(
        select(Notification).where(Notification.user_id == test_instance.owner_id)
    )
    n = result.scalar_one()
    assert n.severity == "error"
    assert n.title == "Container error"


@pytest.mark.asyncio
@pytest.mark.integration
async def test_health_monitor_unhealthy_creates_warning_notification(
    db_session: AsyncSession,
    event_bus: InstanceEventBus,
    test_instance: ToolInstance,
) -> None:
    """Health monitor marking instance unhealthy creates severity=warning notification."""
    from src.services.instance.health_monitor import HealthMonitor

    monitor = HealthMonitor(event_bus)
    monitor._last_known_state[test_instance.id] = HealthSnapshot(
        container_status="running",
        container_healthy=None,
        tunnel_healthy=True,
        exit_code=None,
    )
    test_instance.public_url = "https://example.trycloudflare.com"

    with (
        patch(
            "src.services.instance.health_monitor.get_container_status",
            return_value={"status": "running", "exit_code": None, "health": "healthy"},
        ),
        patch(
            "src.services.instance.health_monitor.check_tunnel_health",
            return_value={"healthy": False, "tunnel_status": "error_response"},
        ),
    ):
        await monitor._check_instance(db_session, test_instance)

    result = await db_session.execute(
        select(Notification).where(Notification.user_id == test_instance.owner_id)
    )
    n = result.scalar_one()
    assert n.category == "health"
    assert n.severity == "warning"
    assert n.title == "Container unhealthy"
