# #region Test.NotificationService [C:3] [TYPE Module] [SEMANTICS test,notification,service,dispatch]
# @BRIEF Tests for services/notifications/service.py — NotificationService.
# @RELATION BINDS_TO -> [service]

from pathlib import Path
import sys

sys.path.insert(0, str(Path(__file__).parent.parent.parent / "src"))

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


# #region test_notification_service [C:2] [TYPE Function]
# @BRIEF Test NotificationService initialization and dispatch.
class TestNotificationServiceInit:
    @pytest.fixture
    def mock_db(self):
        return MagicMock()

    @pytest.fixture
    def mock_config_manager(self):
        cm = MagicMock()
        cm.get_payload.return_value = {}
        return cm

    @pytest.fixture
    def service(self, mock_db, mock_config_manager):
        from src.services.notifications.service import NotificationService
        return NotificationService(mock_db, mock_config_manager)

    def test_init(self, service, mock_db, mock_config_manager):
        assert service.db is mock_db
        assert service.config_manager is mock_config_manager
        assert service._providers == {}
        assert service._initialized is False


# #region test_initialize_providers [C:2] [TYPE Function]
# @BRIEF Test _initialize_providers creates providers from config.
class TestInitializeProviders:
    @pytest.fixture
    def mock_db(self):
        return MagicMock()

    @pytest.fixture
    def service(self, mock_db):
        from src.services.notifications.service import NotificationService
        cm = MagicMock()
        cm.get_payload.return_value = {
            "notifications": {
                "smtp": {"host": "smtp.example.com"},
                "telegram": {"bot_token": "123:ABC"},
                "slack": {"webhook_url": "https://hooks.slack.com/xxx"},
            }
        }
        return NotificationService(mock_db, cm)

    def test_initializes_providers(self, service):
        service._initialize_providers()
        assert "SMTP" in service._providers
        assert "TELEGRAM" in service._providers
        assert "SLACK" in service._providers
        assert service._initialized is True

    def test_initializes_only_once(self, service):
        service._initialize_providers()
        providers_count = len(service._providers)
        service._initialize_providers()
        assert len(service._providers) == providers_count  # no duplicates

    def test_no_notification_config(self, mock_db):
        from src.services.notifications.service import NotificationService
        cm = MagicMock()
        cm.get_payload.return_value = {}
        service = NotificationService(mock_db, cm)
        service._initialize_providers()
        assert service._providers == {}
        assert service._initialized is True
# #endregion test_initialize_providers


# #region test_should_notify [C:2] [TYPE Function]
# @BRIEF Test _should_notify decision logic.
class TestShouldNotify:
    @pytest.fixture
    def service(self, mock_db):
        from src.services.notifications.service import NotificationService
        cm = MagicMock()
        cm.get_payload.return_value = {}
        return NotificationService(mock_db, cm)

    def test_returns_true_for_fail_default(self, service):
        record = MagicMock()
        record.status = "FAIL"
        assert service._should_notify(record, None) is True

    def test_returns_false_for_warn_default(self, service):
        record = MagicMock()
        record.status = "WARN"
        assert service._should_notify(record, None) is False

    def test_returns_false_for_success_default(self, service):
        record = MagicMock()
        record.status = "SUCCESS"
        assert service._should_notify(record, None) is False

    def test_policy_always_returns_true(self, service):
        record = MagicMock()
        record.status = "SUCCESS"
        policy = MagicMock()
        policy.alert_condition = "ALWAYS"
        assert service._should_notify(record, policy) is True

    def test_policy_warn_and_fail(self, service):
        record_warn = MagicMock(status="WARN")
        record_fail = MagicMock(status="FAIL")
        record_ok = MagicMock(status="SUCCESS")
        policy = MagicMock()
        policy.alert_condition = "WARN_AND_FAIL"
        assert service._should_notify(record_warn, policy) is True
        assert service._should_notify(record_fail, policy) is True
        assert service._should_notify(record_ok, policy) is False
# #endregion test_should_notify


# #region test_resolve_targets [C:2] [TYPE Function]
# @BRIEF Test _resolve_targets resolves notification targets.
class TestResolveTargets:
    @pytest.fixture
    def service(self, mock_db):
        from src.services.notifications.service import NotificationService
        cm = MagicMock()
        cm.get_payload.return_value = {}
        return NotificationService(mock_db, cm)

    def test_resolves_owner_email(self, service):
        record = MagicMock()
        policy = MagicMock()
        policy.notify_owners = True
        policy.custom_channels = None

        owner_pref = MagicMock()
        owner_pref.notify_on_fail = True
        owner_pref.telegram_id = None
        owner_pref.email_address = "owner@example.com"
        owner_pref.user = MagicMock(email="user@example.com")

        with patch.object(service, "_find_dashboard_owners", return_value=[owner_pref]):
            targets = service._resolve_targets(record, policy)
            assert ("SMTP", "owner@example.com") in targets

    def test_resolves_owner_telegram(self, service):
        record = MagicMock()
        policy = MagicMock()
        policy.notify_owners = True
        policy.custom_channels = None

        owner_pref = MagicMock()
        owner_pref.notify_on_fail = True
        owner_pref.telegram_id = "@admin"
        owner_pref.email_address = None
        owner_pref.user = MagicMock(email="user@example.com")

        with patch.object(service, "_find_dashboard_owners", return_value=[owner_pref]):
            targets = service._resolve_targets(record, policy)
            assert ("TELEGRAM", "@admin") in targets

    def test_skips_owner_with_notify_off(self, service):
        record = MagicMock()
        policy = MagicMock()
        policy.notify_owners = True

        owner_pref = MagicMock()
        owner_pref.notify_on_fail = False

        with patch.object(service, "_find_dashboard_owners", return_value=[owner_pref]):
            targets = service._resolve_targets(record, policy)
            assert len(targets) == 0

    def test_custom_channels_added(self, service):
        record = MagicMock()
        policy = MagicMock()
        policy.notify_owners = False
        policy.custom_channels = [
            {"type": "SLACK", "target": "#alerts"},
            {"type": "TELEGRAM", "target": "@bot_channel"},
        ]

        targets = service._resolve_targets(record, policy)
        assert ("SLACK", "#alerts") in targets
        assert ("TELEGRAM", "@bot_channel") in targets

    def test_skips_owner_without_policy(self, service):
        """When policy is None, owner notification is enabled by default."""
        record = MagicMock()
        policy = None

        owner_pref = MagicMock()
        owner_pref.notify_on_fail = True
        owner_pref.telegram_id = None
        owner_pref.email_address = None
        owner_pref.user = MagicMock(email="user@example.com")

        with patch.object(service, "_find_dashboard_owners", return_value=[owner_pref]):
            targets = service._resolve_targets(record, None)
            # No valid target since both telegrams_id and email are None but user.email exists
            assert ("SMTP", "user@example.com") in targets
# #endregion test_resolve_targets


# #region test_build_body [C:2] [TYPE Function]
# @BRIEF Test _build_body formats correctly.
class TestBuildBody:
    def test_formats_body(self, mock_db):
        from src.services.notifications.service import NotificationService
        cm = MagicMock()
        cm.get_payload.return_value = {}
        service = NotificationService(mock_db, cm)
        record = MagicMock()
        record.dashboard_id = "dash-1"
        record.environment_id = "env-1"
        record.status = "FAIL"
        record.summary = "Dashboard has issues"
        record.issues = ["issue1", "issue2"]

        body = service._build_body(record)
        assert "dash-1" in body
        assert "env-1" in body
        assert "FAIL" in body
        assert "2" in body  # len(record.issues)
# #endregion test_build_body


# #region test_dispatch_report [C:2] [TYPE Function]
# @BRIEF Test dispatch_report full flow.
class TestDispatchReport:
    @pytest.fixture
    def service(self, mock_db):
        from src.services.notifications.service import NotificationService
        cm = MagicMock()
        cm.get_payload.return_value = {
            "notifications": {
                "smtp": {"host": "smtp.example.com"},
                "telegram": {"bot_token": "123:ABC"},
            }
        }
        return NotificationService(mock_db, cm)

    @pytest.mark.asyncio
    async def test_skips_notification_when_not_needed(self, service):
        record = MagicMock()
        record.status = "SUCCESS"
        record.id = "rec-1"
        policy = None

        with patch.object(service, "_should_notify", return_value=False):
            await service.dispatch_report(record, policy)
            # No providers should be called

    @pytest.mark.asyncio
    async def test_dispatches_via_background_tasks(self, service):
        record = MagicMock()
        record.status = "FAIL"
        record.id = "rec-1"
        policy = MagicMock()
        policy.alert_condition = "FAIL_ONLY"

        background_tasks = MagicMock()

        with patch.object(service, "_resolve_targets", return_value=[("SMTP", "admin@x.com")]):
            await service.dispatch_report(record, policy, background_tasks)
            background_tasks.add_task.assert_called_once()

    @pytest.mark.asyncio
    async def test_dispatches_via_gather(self):
        from src.services.notifications.service import NotificationService
        db = MagicMock()
        cm = MagicMock()
        cm.get_payload.return_value = {}
        service = NotificationService(db, cm)
        service._initialized = True  # Skip provider init

        record = MagicMock()
        record.status = "FAIL"
        record.id = "rec-1"
        policy = MagicMock()
        policy.alert_condition = "FAIL_ONLY"

        provider_mock = AsyncMock()
        provider_mock.send = AsyncMock(return_value=True)
        service._providers["SMTP"] = provider_mock

        with patch.object(service, "_resolve_targets", return_value=[("SMTP", "admin@x.com")]):
            await service.dispatch_report(record, policy, background_tasks=None)
            provider_mock.send.assert_called_once()

    @pytest.mark.asyncio
    async def test_skips_unconfigured_channel(self, service):
        record = MagicMock()
        record.status = "FAIL"
        record.id = "rec-1"
        policy = MagicMock()
        policy.alert_condition = "FAIL_ONLY"

        with patch.object(service, "_resolve_targets", return_value=[("UNKNOWN", "nowhere")]):
            await service.dispatch_report(record, policy)
            # No exception raised, unknown channel skipped
# #endregion test_dispatch_report


# #region test_find_dashboard_owners [C:2] [TYPE Function]
# @BRIEF Test _find_dashboard_owners DB query.
class TestFindDashboardOwners:
    def test_queries_db(self, mock_db):
        from src.services.notifications.service import NotificationService
        cm = MagicMock()
        cm.get_payload.return_value = {}
        service = NotificationService(mock_db, cm)
        from src.models.profile import UserDashboardPreference
        mock_db.query.return_value.filter.return_value.all.return_value = ["owner1", "owner2"]
        owners = service._find_dashboard_owners(MagicMock())
        assert len(owners) == 2
        mock_db.query.assert_called_once_with(UserDashboardPreference)
# #endregion test_find_dashboard_owners


# @pytest.fixture helpers
@pytest.fixture
def mock_db():
    return MagicMock()
# #endregion Test.NotificationService
