"""
Tests for NotificationService with Tenacity retry logic.
"""

import pytest
from unittest.mock import patch, MagicMock

from local_deep_research.notifications.service import (
    NotificationService,
    MAX_RETRY_ATTEMPTS,
    INITIAL_RETRY_DELAY,
    RETRY_BACKOFF_MULTIPLIER,
)
from local_deep_research.notifications.templates import EventType
from local_deep_research.notifications.exceptions import SendError


class TestNotificationServiceInit:
    """Tests for NotificationService initialization."""

    def test_init_creates_apprise_instance(self):
        """Test initialization creates Apprise instance."""
        service = NotificationService(outbound_allowed=True)
        assert service.apprise is not None


class TestSendOutboundGate:
    """Tests for the operator-level master switch enforced inside send().

    Defense-in-depth: the gate is also checked in NotificationManager and
    NotificationService.test_service. Re-checking inside send() means a
    direct caller (now or in the future) cannot bypass it. See SECURITY.md
    'Notification Webhook SSRF'.
    """

    @patch("local_deep_research.notifications.service.apprise.Apprise")
    def test_send_returns_false_when_outbound_disallowed(
        self, mock_apprise_class
    ):
        """send() must bail out before constructing/calling Apprise."""
        mock_apprise_instance = MagicMock()
        mock_apprise_class.return_value = mock_apprise_instance

        service = NotificationService(outbound_allowed=False)

        result = service.send(
            title="t",
            body="b",
            service_urls="discord://webhook/token",
        )

        assert result is False
        # Apprise should never be touched once the gate refuses.
        mock_apprise_instance.notify.assert_not_called()
        mock_apprise_instance.add.assert_not_called()

    @patch("local_deep_research.notifications.service.apprise.Apprise")
    def test_send_proceeds_when_outbound_allowed(self, mock_apprise_class):
        """Sanity check: gate open => normal send path runs."""
        mock_apprise_instance = MagicMock()
        mock_apprise_instance.notify.return_value = True
        mock_apprise_class.return_value = mock_apprise_instance

        service = NotificationService(outbound_allowed=True)

        result = service.send(
            title="t",
            body="b",
            service_urls="discord://webhook/token",
        )

        assert result is True
        assert mock_apprise_instance.notify.call_count == 1


class TestSendWithTenacity:
    """Tests for send method with Tenacity retry logic."""

    @patch("local_deep_research.notifications.service.apprise.Apprise")
    def test_send_success_first_attempt(self, mock_apprise_class):
        """Test successful notification on first attempt."""
        mock_apprise_instance = MagicMock()
        mock_apprise_instance.notify.return_value = True
        mock_apprise_class.return_value = mock_apprise_instance

        service = NotificationService(outbound_allowed=True)

        result = service.send(
            title="Test Title",
            body="Test Body",
            service_urls="discord://webhook/token",
        )

        assert result is True
        assert mock_apprise_instance.notify.call_count == 1

    @patch("local_deep_research.notifications.service.apprise.Apprise")
    def test_send_retries_on_failure_with_tenacity(self, mock_apprise_class):
        """Test Tenacity handles exponential backoff retry automatically."""
        mock_apprise_instance = MagicMock()
        mock_apprise_instance.add.return_value = True
        # Fail twice, succeed on third attempt
        mock_apprise_instance.notify.side_effect = [False, False, True]
        mock_apprise_class.return_value = mock_apprise_instance

        service = NotificationService(outbound_allowed=True)

        result = service.send(
            title="Test",
            body="Body",
            service_urls="discord://webhook/token",
        )

        assert result is True
        # Tenacity should have retried 3 times total (1 initial + 2 retries)
        assert mock_apprise_instance.notify.call_count == 3

        # Verify the Tenacity decorator was applied
        assert hasattr(service._send_with_retry, "__wrapped__")

    @patch("local_deep_research.notifications.service.apprise.Apprise")
    def test_send_raises_after_max_retries_with_tenacity(
        self, mock_apprise_class
    ):
        """Test Tenacity raises exception after max retry attempts."""
        mock_apprise_instance = MagicMock()
        mock_apprise_instance.add.return_value = True
        # Always fail
        mock_apprise_instance.notify.return_value = False
        mock_apprise_class.return_value = mock_apprise_instance

        service = NotificationService(outbound_allowed=True)

        with pytest.raises(SendError, match="Failed to send notification"):
            service.send(
                title="Test",
                body="Body",
                service_urls="discord://webhook/token",
            )

        # Tenacity should have tried 3 times (MAX_RETRY_ATTEMPTS)
        assert mock_apprise_instance.notify.call_count == MAX_RETRY_ATTEMPTS

    @patch("local_deep_research.notifications.service.apprise.Apprise")
    def test_send_handles_exception_with_retry_with_tenacity(
        self, mock_apprise_class
    ):
        """Test Tenacity handles exception retries automatically."""
        mock_apprise_instance = MagicMock()
        mock_apprise_instance.add.return_value = True
        # Raise exception twice, succeed on third attempt
        mock_apprise_instance.notify.side_effect = [
            Exception("Network error"),
            Exception("Network error"),
            True,
        ]
        mock_apprise_class.return_value = mock_apprise_instance

        service = NotificationService(outbound_allowed=True)

        result = service.send(
            title="Test",
            body="Body",
            service_urls="discord://webhook/token",
        )

        assert result is True
        # Tenacity should have retried on exceptions and succeeded
        assert mock_apprise_instance.notify.call_count == 3

    @patch("local_deep_research.notifications.service.apprise.Apprise")
    def test_send_with_no_service_urls(self, mock_apprise_class):
        """Test send returns False when configured instance empty."""
        service = NotificationService(outbound_allowed=True)
        # Don't provide service_urls, use configured instance

        result = service.send(
            title="Test",
            body="Body",
            # No service_urls parameter
        )

        assert result is False


class TestTenacityConfiguration:
    """Tests for Tenacity retry configuration."""

    def test_retry_constants_are_defined(self):
        """Test backward compatibility constants are still available."""
        assert MAX_RETRY_ATTEMPTS == 3
        assert INITIAL_RETRY_DELAY == 0.5
        assert RETRY_BACKOFF_MULTIPLIER == 2

    @patch("local_deep_research.notifications.service.apprise.Apprise")
    def test_tenacity_retry_configuration(self, mock_apprise_class):
        """Test Tenacity is configured with correct retry parameters."""
        mock_apprise_instance = MagicMock()
        mock_apprise_instance.add.return_value = True
        mock_apprise_instance.notify.return_value = False  # Always fail
        mock_apprise_class.return_value = mock_apprise_instance

        service = NotificationService(outbound_allowed=True)

        # Verify the retry decorator is applied
        assert hasattr(service._send_with_retry, "__wrapped__")

        # Check that the retry configuration matches our constants
        # (We can't easily inspect the decorator config, so we verify behavior)
        with pytest.raises(SendError):
            service.send(
                title="Test",
                body="Body",
                service_urls="discord://webhook/token",
            )

        # Should have tried exactly MAX_RETRY_ATTEMPTS times
        assert mock_apprise_instance.notify.call_count == MAX_RETRY_ATTEMPTS


class TestSendEvent:
    """Tests for send_event method."""

    @patch("local_deep_research.notifications.service.apprise.Apprise")
    def test_send_event_formats_template(self, mock_apprise_class):
        """Test send_event formats message using template."""
        mock_apprise_instance = MagicMock()
        mock_apprise_instance.notify.return_value = True
        mock_apprise_instance.add.return_value = True
        mock_apprise_class.return_value = mock_apprise_instance

        service = NotificationService(outbound_allowed=True)

        # Include all required template variables
        context = {
            "query": "What is quantum computing?",
            "research_id": "123",
            "summary": "Brief summary",
            "url": "http://localhost:5000/research/123",
        }

        result = service.send_event(
            event_type=EventType.RESEARCH_COMPLETED,
            context=context,
            service_urls="discord://webhook/token",
        )

        assert result is True

        # Verify notify was called with formatted message
        call_kwargs = mock_apprise_instance.notify.call_args[1]
        assert "title" in call_kwargs
        assert "body" in call_kwargs
        # Title should contain the query (from template: "Research Completed: {query}")
        assert "quantum computing" in call_kwargs["title"].lower()
        assert "research completed" in call_kwargs["title"].lower()


class TestTestService:
    """Tests for test_service method."""

    @patch("local_deep_research.notifications.service.apprise.Apprise")
    def test_test_service_success(self, mock_apprise_class):
        """Test successful service test."""
        mock_apprise_instance = MagicMock()
        mock_apprise_instance.add.return_value = True
        mock_apprise_instance.notify.return_value = True
        mock_apprise_class.return_value = mock_apprise_instance

        service = NotificationService(outbound_allowed=True)

        result = service.test_service("discord://webhook/token")

        assert result["success"] is True
        assert "message" in result
        # Verify test notification was sent
        mock_apprise_instance.notify.assert_called_once()

    def test_test_service_exception(self):
        """Test service test handles exceptions."""
        service = NotificationService(outbound_allowed=True)

        # Mock the internal apprise.Apprise to raise exception
        with patch(
            "local_deep_research.notifications.service.apprise.Apprise"
        ) as mock_apprise:
            mock_instance = MagicMock()
            mock_instance.add.side_effect = Exception("Network error")
            mock_apprise.return_value = mock_instance

            result = service.test_service("discord://webhook/token")

            assert result["success"] is False
            assert "error" in result
            # Exception should be caught and returned in error field
            assert len(result["error"]) > 0


class TestGetServiceType:
    """Tests for get_service_type method."""

    def test_get_service_type_discord(self):
        """Test detecting Discord service."""
        service = NotificationService(outbound_allowed=True)
        service_type = service.get_service_type("discord://webhook/token")
        assert service_type == "discord"

    def test_get_service_type_unknown(self):
        """Test unknown service type."""
        service = NotificationService(outbound_allowed=True)
        service_type = service.get_service_type("unknown://service")
        assert service_type == "unknown"


class TestIntegration:
    """Integration tests for NotificationService."""

    @patch("local_deep_research.notifications.service.apprise.Apprise")
    def test_complete_notification_flow(self, mock_apprise_class):
        """Test complete notification flow from event to send."""
        mock_apprise_instance = MagicMock()
        mock_apprise_instance.add.return_value = True
        mock_apprise_instance.notify.return_value = True
        mock_apprise_class.return_value = mock_apprise_instance

        service = NotificationService(outbound_allowed=True)

        context = {
            "query": "Test research query",
            "research_id": "test-123",
            "summary": "Test summary",
            "url": "http://localhost:5000/research/test-123",
        }

        result = service.send_event(
            event_type=EventType.RESEARCH_COMPLETED,
            context=context,
            service_urls="discord://webhook/token",
        )

        assert result is True
        mock_apprise_instance.notify.assert_called_once()

        # Verify the formatted message
        call_args = mock_apprise_instance.notify.call_args
        title = call_args[1]["title"]
        body = call_args[1]["body"]

        assert "Test research query" in title
        assert "Test summary" in body
        assert "http://localhost:5000/research/test-123" in body
