"""
Tests for django_cronjob_utils notifications.
"""
from datetime import date
from unittest.mock import patch, MagicMock
from django.test import TestCase, override_settings
from django.core import mail
from django_cronjob_utils.notifications import (
    NotificationBackend,
    EmailBackend,
    SlackBackend,
    TelegramBackend,
    NotificationManager
)
from django_cronjob_utils.models import CronExecution


class NotificationBackendTests(TestCase):
    """Tests for NotificationBackend base class."""
    
    def test_notify_failure_not_implemented(self):
        """Test base backend raises NotImplementedError."""
        backend = NotificationBackend()
        
        execution = CronExecution.objects.create(
            task_code='A001',
            task_name='Test Task',
            execution_date=date(2024, 1, 15)
        )
        
        with self.assertRaises(NotImplementedError):
            backend.notify_failure(execution, "Error message")
    
    def test_notify_success_default(self):
        """Test base backend notify_success does nothing by default."""
        backend = NotificationBackend()
        
        execution = CronExecution.objects.create(
            task_code='A001',
            task_name='Test Task',
            execution_date=date(2024, 1, 15)
        )
        
        # Should not raise
        backend.notify_success(execution, "Success message")


class EmailBackendTests(TestCase):
    """Tests for EmailBackend."""
    
    def setUp(self):
        """Set up test fixtures."""
        self.execution = CronExecution.objects.create(
            task_code='A001',
            task_name='Test Task',
            execution_date=date(2024, 1, 15),
            completed=True,
            success=False,
            error_code='ERROR_001'
        )
    
    @override_settings(
        CRONJOB_UTILS={
            'NOTIFICATIONS': {
                'on_failure': ['email'],
                'email': {
                    'recipients': ['test@example.com'],
                    'from_email': 'cronjobs@example.com',
                },
            },
        }
    )
    def test_email_backend_notify_failure(self):
        """Test email backend sends failure notification."""
        backend = EmailBackend({
            'recipients': ['test@example.com'],
            'from_email': 'cronjobs@example.com',
        })
        
        backend.notify_failure(self.execution, "Test error message")
        
        # Check email was sent
        self.assertEqual(len(mail.outbox), 1)
        email = mail.outbox[0]
        self.assertEqual(email.subject, 'Cronjob Failed: Test Task')
        self.assertEqual(email.from_email, 'cronjobs@example.com')
        self.assertEqual(email.to, ['test@example.com'])
        self.assertIn('Test Task', email.body)
        self.assertIn('A001', email.body)
        self.assertIn('Test error message', email.body)
    
    def test_email_backend_no_recipients(self):
        """Test email backend with no recipients."""
        backend = EmailBackend({
            'recipients': [],
            'from_email': 'cronjobs@example.com',
        })
        
        # Should not raise, just log warning
        backend.notify_failure(self.execution, "Test error")
        self.assertEqual(len(mail.outbox), 0)
    
    def test_email_backend_custom_subject_template(self):
        """Test email backend with custom subject template."""
        backend = EmailBackend({
            'recipients': ['test@example.com'],
            'from_email': 'cronjobs@example.com',
            'subject_template': '[CRITICAL] {task_name} ({task_code}) Failed',
        })
        
        backend.notify_failure(self.execution, "Test error")
        
        self.assertEqual(len(mail.outbox), 1)
        email = mail.outbox[0]
        self.assertEqual(email.subject, '[CRITICAL] Test Task (A001) Failed')
    
    def test_email_backend_multiple_recipients(self):
        """Test email backend with multiple recipients."""
        backend = EmailBackend({
            'recipients': ['test1@example.com', 'test2@example.com'],
            'from_email': 'cronjobs@example.com',
        })
        
        backend.notify_failure(self.execution, "Test error")
        
        self.assertEqual(len(mail.outbox), 1)
        email = mail.outbox[0]
        self.assertEqual(len(email.to), 2)
        self.assertIn('test1@example.com', email.to)
        self.assertIn('test2@example.com', email.to)
    
    @patch('django_cronjob_utils.notifications.send_mail')
    def test_email_backend_send_failure(self, mock_send_mail):
        """Test email backend handles send failure gracefully."""
        mock_send_mail.side_effect = Exception("SMTP error")
        
        backend = EmailBackend({
            'recipients': ['test@example.com'],
            'from_email': 'cronjobs@example.com',
        })
        
        # Should not raise, just log error
        backend.notify_failure(self.execution, "Test error")


class SlackBackendTests(TestCase):
    """Tests for SlackBackend."""
    
    def setUp(self):
        """Set up test fixtures."""
        self.execution = CronExecution.objects.create(
            task_code='A001',
            task_name='Test Task',
            execution_date=date(2024, 1, 15),
            completed=True,
            success=False,
            error_code='ERROR_001'
        )
    
    def test_slack_backend_init(self):
        """Test SlackBackend initialization."""
        backend = SlackBackend({
            'webhook_url': 'https://hooks.slack.com/test',
            'channel': '#cronjobs',
            'username': 'Cronbot'
        })
        
        self.assertEqual(backend.webhook_url, 'https://hooks.slack.com/test')
        self.assertEqual(backend.channel, '#cronjobs')
        self.assertEqual(backend.username, 'Cronbot')
    
    def test_slack_backend_init_no_webhook(self):
        """Test SlackBackend initialization without webhook raises error."""
        with self.assertRaises(ValueError) as cm:
            SlackBackend({
                'channel': '#cronjobs',
            })
        
        self.assertIn("webhook_url", str(cm.exception))
    
    def test_slack_backend_defaults(self):
        """Test SlackBackend default values."""
        backend = SlackBackend({
            'webhook_url': 'https://hooks.slack.com/test',
        })
        
        self.assertEqual(backend.channel, '#cronjobs')
        self.assertEqual(backend.username, 'Cronbot')
    
    @patch('django_cronjob_utils.notifications.requests')
    def test_slack_backend_notify_failure(self, mock_requests):
        """Test SlackBackend sends failure notification."""
        mock_response = MagicMock()
        mock_response.raise_for_status = MagicMock()
        mock_requests.post.return_value = mock_response
        
        backend = SlackBackend({
            'webhook_url': 'https://hooks.slack.com/test',
        })
        
        backend.notify_failure(self.execution, "Test error message")
        
        # Check request was made
        mock_requests.post.assert_called_once()
        call_args = mock_requests.post.call_args
        
        self.assertEqual(call_args[0][0], 'https://hooks.slack.com/test')
        payload = call_args[1]['json']
        self.assertEqual(payload['channel'], '#cronjobs')
        self.assertEqual(payload['username'], 'Cronbot')
        self.assertIn('Test Task', payload['text'])
        self.assertEqual(len(payload['attachments']), 1)
        self.assertEqual(payload['attachments'][0]['color'], 'danger')
    
    @patch('django_cronjob_utils.notifications.requests')
    def test_slack_backend_request_failure(self, mock_requests):
        """Test SlackBackend handles request failure gracefully."""
        mock_requests.post.side_effect = Exception("Network error")
        
        backend = SlackBackend({
            'webhook_url': 'https://hooks.slack.com/test',
        })
        
        # Should not raise, just log error
        backend.notify_failure(self.execution, "Test error")
    
    def test_slack_backend_no_requests_library(self):
        """Test SlackBackend when requests library is not available."""
        with patch('django_cronjob_utils.notifications.requests', None):
            backend = SlackBackend({
                'webhook_url': 'https://hooks.slack.com/test',
            })
            
            # Should not raise, just log error
            backend.notify_failure(self.execution, "Test error")


class TelegramBackendTests(TestCase):
    """Tests for TelegramBackend."""
    
    def setUp(self):
        """Set up test fixtures."""
        self.execution = CronExecution.objects.create(
            task_code='A001',
            task_name='Test Task',
            execution_date=date(2024, 1, 15),
            completed=True,
            success=False,
            error_code='ERROR_001'
        )
    
    def test_telegram_backend_init(self):
        """Test TelegramBackend initialization."""
        backend = TelegramBackend({
            'bot_token': '123456:ABC-DEF',
            'chat_id': '123456789'
        })
        
        self.assertEqual(backend.bot_token, '123456:ABC-DEF')
        self.assertEqual(backend.chat_id, '123456789')
        self.assertIn('123456:ABC-DEF', backend.api_url)
    
    def test_telegram_backend_init_no_token(self):
        """Test TelegramBackend initialization without token raises error."""
        with self.assertRaises(ValueError) as cm:
            TelegramBackend({
                'chat_id': '123456789'
            })
        
        self.assertIn("bot_token", str(cm.exception))
    
    def test_telegram_backend_init_no_chat_id(self):
        """Test TelegramBackend initialization without chat_id raises error."""
        with self.assertRaises(ValueError) as cm:
            TelegramBackend({
                'bot_token': '123456:ABC-DEF'
            })
        
        self.assertIn("chat_id", str(cm.exception))
    
    @patch('django_cronjob_utils.notifications.requests')
    def test_telegram_backend_notify_failure(self, mock_requests):
        """Test TelegramBackend sends failure notification."""
        mock_response = MagicMock()
        mock_response.raise_for_status = MagicMock()
        mock_requests.post.return_value = mock_response
        
        backend = TelegramBackend({
            'bot_token': '123456:ABC-DEF',
            'chat_id': '123456789'
        })
        
        backend.notify_failure(self.execution, "Test error message")
        
        # Check request was made
        mock_requests.post.assert_called_once()
        call_args = mock_requests.post.call_args
        
        self.assertIn('123456:ABC-DEF', call_args[0][0])
        payload = call_args[1]['json']
        self.assertEqual(payload['chat_id'], '123456789')
        self.assertEqual(payload['parse_mode'], 'HTML')
        self.assertIn('Test Task', payload['text'])
        self.assertIn('A001', payload['text'])
        self.assertIn('Test error message', payload['text'])
    
    @patch('django_cronjob_utils.notifications.requests')
    def test_telegram_backend_long_message(self, mock_requests):
        """Test TelegramBackend truncates very long messages."""
        mock_response = MagicMock()
        mock_response.raise_for_status = MagicMock()
        mock_requests.post.return_value = mock_response
        
        backend = TelegramBackend({
            'bot_token': '123456:ABC-DEF',
            'chat_id': '123456789'
        })
        
        long_message = 'A' * 2000  # Longer than 1000 char limit
        backend.notify_failure(self.execution, long_message)
        
        call_args = mock_requests.post.call_args
        payload = call_args[1]['json']
        # Message should be truncated to 1000 chars
        self.assertLessEqual(len(payload['text']), 1200)  # Allow for HTML tags
    
    @patch('django_cronjob_utils.notifications.requests')
    def test_telegram_backend_request_failure(self, mock_requests):
        """Test TelegramBackend handles request failure gracefully."""
        mock_requests.post.side_effect = Exception("Network error")
        
        backend = TelegramBackend({
            'bot_token': '123456:ABC-DEF',
            'chat_id': '123456789'
        })
        
        # Should not raise, just log error
        backend.notify_failure(self.execution, "Test error")
    
    def test_telegram_backend_no_requests_library(self):
        """Test TelegramBackend when requests library is not available."""
        with patch('django_cronjob_utils.notifications.requests', None):
            backend = TelegramBackend({
                'bot_token': '123456:ABC-DEF',
                'chat_id': '123456789'
            })
            
            # Should not raise, just log error
            backend.notify_failure(self.execution, "Test error")


class NotificationManagerTests(TestCase):
    """Tests for NotificationManager."""
    
    def setUp(self):
        """Set up test fixtures."""
        self.execution = CronExecution.objects.create(
            task_code='A001',
            task_name='Test Task',
            execution_date=date(2024, 1, 15),
            completed=True,
            success=False
        )
    
    @override_settings(
        CRONJOB_UTILS={
            'NOTIFICATIONS': {
                'on_failure': ['email'],
                'email': {
                    'recipients': ['test@example.com'],
                    'from_email': 'cronjobs@example.com',
                },
            },
        }
    )
    def test_notification_manager_loads_email_backend(self):
        """Test NotificationManager loads email backend."""
        manager = NotificationManager()
        
        self.assertEqual(len(manager.backends), 1)
        self.assertIsInstance(manager.backends[0], EmailBackend)
    
    @override_settings(
        CRONJOB_UTILS={
            'NOTIFICATIONS': {
                'on_failure': ['email', 'slack'],
                'email': {
                    'recipients': ['test@example.com'],
                },
                'slack': {
                    'webhook_url': 'https://hooks.slack.com/test',
                },
            },
        }
    )
    @patch('django_cronjob_utils.notifications.requests')
    def test_notification_manager_loads_multiple_backends(self, mock_requests):
        """Test NotificationManager loads multiple backends."""
        mock_response = MagicMock()
        mock_response.raise_for_status = MagicMock()
        mock_requests.post.return_value = mock_response
        
        manager = NotificationManager()
        
        self.assertEqual(len(manager.backends), 2)
        backend_types = [type(b).__name__ for b in manager.backends]
        self.assertIn('EmailBackend', backend_types)
        self.assertIn('SlackBackend', backend_types)
    
    @override_settings(
        CRONJOB_UTILS={
            'NOTIFICATIONS': {
                'on_failure': ['email'],
                'email': {
                    'recipients': ['test@example.com'],
                    'from_email': 'cronjobs@example.com',
                },
            },
        }
    )
    def test_notification_manager_notify_failure(self):
        """Test NotificationManager notifies failure."""
        manager = NotificationManager()
        manager.notify_failure(self.execution, "Test error")
        
        # Check email was sent
        self.assertEqual(len(mail.outbox), 1)
    
    @override_settings(
        CRONJOB_UTILS={
            'NOTIFICATIONS': {
                'on_failure': ['email', 'slack', 'telegram'],
                'email': {
                    'recipients': ['test@example.com'],
                },
                'slack': {
                    'webhook_url': 'https://hooks.slack.com/test',
                },
                'telegram': {
                    'bot_token': '123456:ABC-DEF',
                    'chat_id': '123456789'
                },
            },
        }
    )
    @patch('django_cronjob_utils.notifications.requests')
    def test_notification_manager_all_backends(self, mock_requests):
        """Test NotificationManager with all backends."""
        mock_response = MagicMock()
        mock_response.raise_for_status = MagicMock()
        mock_requests.post.return_value = mock_response
        
        manager = NotificationManager()
        manager.notify_failure(self.execution, "Test error")
        
        # Email should be sent
        self.assertEqual(len(mail.outbox), 1)
        # Slack and Telegram should be called
        self.assertEqual(mock_requests.post.call_count, 2)
    
    @override_settings(
        CRONJOB_UTILS={
            'NOTIFICATIONS': {
                'on_failure': ['email'],
                'email': {
                    'recipients': ['test@example.com'],
                },
            },
        }
    )
    def test_notification_manager_backend_failure_handling(self):
        """Test NotificationManager handles backend failures gracefully."""
        manager = NotificationManager()
        
        # Make one backend fail
        manager.backends[0].notify_failure = MagicMock(side_effect=Exception("Backend error"))
        
        # Should not raise, just log error
        manager.notify_failure(self.execution, "Test error")
    
    @override_settings(
        CRONJOB_UTILS={
            'NOTIFICATIONS': {
                'on_failure': [],
            },
        }
    )
    def test_notification_manager_no_backends(self):
        """Test NotificationManager with no backends configured."""
        manager = NotificationManager()
        
        self.assertEqual(len(manager.backends), 0)
        manager.notify_failure(self.execution, "Test error")  # Should not raise
    
    @override_settings(
        CRONJOB_UTILS={
            'NOTIFICATIONS': {
                'on_failure': ['email'],
                'on_success': ['email'],
                'email': {
                    'recipients': ['test@example.com'],
                },
            },
        }
    )
    def test_notification_manager_notify_success(self):
        """Test NotificationManager notifies success when configured."""
        manager = NotificationManager()
        
        # Mock notify_success on backend
        manager.backends[0].notify_success = MagicMock()
        
        manager.notify_success(self.execution, "Success message")
        
        manager.backends[0].notify_success.assert_called_once_with(
            self.execution,
            "Success message"
        )
    
    @override_settings(
        CRONJOB_UTILS={
            'NOTIFICATIONS': {
                'on_failure': ['email'],
                'on_success': [],
                'email': {
                    'recipients': ['test@example.com'],
                },
            },
        }
    )
    def test_notification_manager_notify_success_not_configured(self):
        """Test NotificationManager skips success notification when not configured."""
        manager = NotificationManager()
        
        # Should not call backends
        manager.notify_success(self.execution, "Success message")
        
        # Email backend doesn't implement notify_success, so nothing happens
        # This is expected behavior
