"""
Unit tests for error handling in reports system.

Tests invalid inputs, database errors, and export failures to ensure
robust error handling across all report services.

Validates: Requirements All (Error Handling)
"""

from django.test import TestCase, RequestFactory
from django.http import HttpRequest
from datetime import date, datetime, timedelta
from decimal import Decimal
from unittest.mock import Mock, patch, MagicMock

from reports.filter_service import ReportFilterService, FilterParams
from reports.calculation_service import LoanCalculationService
from reports.export_service import ReportExportService
from reports.client_report_service import ClientReportService
from loans.models import Loan
from users.models import CustomUser


class FilterServiceErrorHandlingTests(TestCase):
    """Test error handling in ReportFilterService"""
    
    def setUp(self):
        self.factory = RequestFactory()
    
    def test_invalid_date_format_returns_none(self):
        """Test that invalid date formats are handled gracefully"""
        request = self.factory.get('/', {
            'start_date': 'invalid-date',
            'end_date': '2024-13-45'  # Invalid month and day
        })
        
        params = ReportFilterService.parse_filter_params(request)
        
        # Should return None for invalid dates, not raise exception
        self.assertIsNone(params.start_date)
        self.assertIsNone(params.end_date)
    
    def test_missing_date_parameters(self):
        """Test handling of missing date parameters"""
        request = self.factory.get('/')
        
        params = ReportFilterService.parse_filter_params(request)
        
        # Should have None values, not raise exception
        self.assertIsNone(params.start_date)
        self.assertIsNone(params.end_date)
    
    def test_invalid_date_range_start_after_end(self):
        """Test validation when start date is after end date"""
        # This should be validated at the view level, but service should handle it
        request = self.factory.get('/', {
            'start_date': '2024-12-31',
            'end_date': '2024-01-01'
        })
        
        params = ReportFilterService.parse_filter_params(request)
        
        # Dates should be parsed correctly
        self.assertEqual(params.start_date, date(2024, 12, 31))
        self.assertEqual(params.end_date, date(2024, 1, 1))
    
    def test_empty_string_dates(self):
        """Test handling of empty string date values"""
        request = self.factory.get('/', {
            'start_date': '',
            'end_date': ''
        })
        
        params = ReportFilterService.parse_filter_params(request)
        
        # Empty strings should result in None
        self.assertIsNone(params.start_date)
        self.assertIsNone(params.end_date)
    
    def test_invalid_product_id(self):
        """Test handling of invalid product IDs"""
        request = self.factory.get('/', {
            'product': 'invalid-uuid-format'
        })
        
        params = ReportFilterService.parse_filter_params(request)
        
        # Should store the value without validation (validation happens at query time)
        self.assertEqual(params.product_id, 'invalid-uuid-format')
    
    def test_invalid_branch_id(self):
        """Test handling of invalid branch IDs"""
        request = self.factory.get('/', {
            'branch': 'not-a-uuid'
        })
        
        params = ReportFilterService.parse_filter_params(request)
        
        # Should store the value without validation
        self.assertEqual(params.branch_id, 'not-a-uuid')
    
    def test_invalid_gender_value(self):
        """Test handling of invalid gender values"""
        request = self.factory.get('/', {
            'gender': 'invalid-gender'
        })
        
        params = ReportFilterService.parse_filter_params(request)
        
        # Should store the value (validation happens at query time)
        self.assertEqual(params.gender, 'invalid-gender')
    
    def test_sql_injection_attempt_in_filters(self):
        """Test that SQL injection attempts are handled safely"""
        request = self.factory.get('/', {
            'product': "'; DROP TABLE loans; --",
            'branch': "1' OR '1'='1"
        })
        
        params = ReportFilterService.parse_filter_params(request)
        
        # Django ORM should handle these safely
        self.assertEqual(params.product_id, "'; DROP TABLE loans; --")
        self.assertEqual(params.branch_id, "1' OR '1'='1")


class CalculationServiceErrorHandlingTests(TestCase):
    """Test error handling in LoanCalculationService"""
    
    def test_calculate_amount_paid_with_none_loan(self):
        """Test amount paid calculation with None loan"""
        result = LoanCalculationService.calculate_amount_paid(None)
        
        self.assertEqual(result, Decimal('0.00'))
    
    def test_calculate_outstanding_with_none_loan(self):
        """Test outstanding amount calculation with None loan"""
        result = LoanCalculationService.calculate_outstanding_amount(None)
        
        self.assertEqual(result, Decimal('0.00'))
    
    def test_calculate_daily_payment_with_none_loan(self):
        """Test daily payment calculation with None loan"""
        result = LoanCalculationService.calculate_daily_payment_required(None)
        
        self.assertEqual(result, Decimal('0.00'))
    
    def test_calculate_days_overdue_with_none_loan(self):
        """Test days overdue calculation with None loan"""
        result = LoanCalculationService.calculate_days_overdue(None)
        
        self.assertEqual(result, 0)
    
    def test_format_currency_with_none(self):
        """Test currency formatting with None value"""
        result = LoanCalculationService.format_currency(None)
        
        self.assertEqual(result, "0.00")
    
    def test_format_currency_with_invalid_string(self):
        """Test currency formatting with invalid string"""
        result = LoanCalculationService.format_currency("not-a-number")
        
        # Should handle gracefully, likely returning "0.00" or raising handled exception
        self.assertIsInstance(result, str)
    
    def test_calculate_total_with_none_values(self):
        """Test total calculation with None values"""
        result = LoanCalculationService.calculate_total_loan_amount(None, None, None)
        
        self.assertEqual(result, Decimal('0.00'))
    
    def test_calculate_total_with_negative_values(self):
        """Test total calculation with negative values"""
        result = LoanCalculationService.calculate_total_loan_amount(
            Decimal('-100'),
            Decimal('-50'),
            Decimal('-10')
        )
        
        # Should calculate correctly even with negative values
        self.assertEqual(result, Decimal('-160'))
    
    @patch('loans.models.Loan.objects')
    def test_database_connection_failure(self, mock_loan_objects):
        """Test handling of database connection failures"""
        # Simulate database connection error
        mock_loan_objects.get.side_effect = Exception("Database connection failed")
        
        # The service methods should handle this gracefully
        # This would typically be caught at the view level


class ExportServiceErrorHandlingTests(TestCase):
    """Test error handling in ReportExportService"""
    
    def setUp(self):
        self.export_service = ReportExportService()
    
    def test_export_pdf_with_empty_dataset(self):
        """Test PDF export with empty dataset"""
        report_data = {'loans': []}
        filters = {}
        
        response = self.export_service.export_to_pdf(
            report_data, 'loans_due', filters
        )
        
        # Should return valid response, not error
        self.assertEqual(response.status_code, 200)
        self.assertEqual(response['Content-Type'], 'application/pdf')
    
    def test_export_excel_with_empty_dataset(self):
        """Test Excel export with empty dataset"""
        report_data = {'loans': []}
        filters = {}
        
        response = self.export_service.export_to_excel(
            report_data, 'loans_due', filters
        )
        
        # Should return valid response, not error
        self.assertEqual(response.status_code, 200)
        self.assertIn('spreadsheet', response['Content-Type'])
    
    def test_export_pdf_with_large_dataset(self):
        """Test PDF export with dataset exceeding MAX_EXPORT_ROWS"""
        # Create dataset larger than MAX_EXPORT_ROWS
        large_dataset = [
            {
                'loan_number': f'LOAN-{i}',
                'borrower_name': f'Borrower {i}',
                'principal_amount': Decimal('10000'),
                'due_date': date.today(),
                'outstanding_amount': Decimal('5000')
            }
            for i in range(self.export_service.MAX_EXPORT_ROWS + 100)
        ]
        
        report_data = {'loans': large_dataset}
        filters = {}
        
        response = self.export_service.export_to_pdf(
            report_data, 'loans_due', filters
        )
        
        # Should return valid response with truncated data
        self.assertEqual(response.status_code, 200)
    
    def test_export_excel_with_large_dataset(self):
        """Test Excel export with dataset exceeding MAX_EXPORT_ROWS"""
        large_dataset = [
            {
                'loan_number': f'LOAN-{i}',
                'borrower_name': f'Borrower {i}',
                'principal_amount': Decimal('10000'),
                'due_date': date.today(),
                'outstanding_amount': Decimal('5000')
            }
            for i in range(self.export_service.MAX_EXPORT_ROWS + 100)
        ]
        
        report_data = {'loans': large_dataset}
        filters = {}
        
        response = self.export_service.export_to_excel(
            report_data, 'loans_due', filters
        )
        
        # Should return valid response with truncated data
        self.assertEqual(response.status_code, 200)
    
    def test_format_currency_with_special_characters(self):
        """Test currency formatting with special characters"""
        result = self.export_service.format_currency("$1,000.00")
        
        # Should handle gracefully
        self.assertIsInstance(result, str)
        self.assertIn("KES", result)
    
    def test_format_date_with_none(self):
        """Test date formatting with None value"""
        result = self.export_service.format_date(None)
        
        self.assertEqual(result, "N/A")
    
    def test_format_date_with_invalid_string(self):
        """Test date formatting with invalid string"""
        result = self.export_service.format_date("not-a-date")
        
        # Should return the string as-is or "N/A"
        self.assertIsInstance(result, str)
    
    @patch('reports.export_service.SimpleDocTemplate')
    def test_pdf_generation_failure(self, mock_doc):
        """Test handling of PDF generation failures"""
        # Simulate PDF generation error
        mock_doc.side_effect = Exception("PDF generation failed")
        
        report_data = {'loans': [{'loan_number': 'LOAN-001'}]}
        filters = {}
        
        response = self.export_service.export_to_pdf(
            report_data, 'loans_due', filters
        )
        
        # Should return error response
        self.assertEqual(response.status_code, 500)
        self.assertIn('Error', response.content.decode())
    
    @patch('reports.export_service.Workbook')
    def test_excel_generation_failure(self, mock_workbook):
        """Test handling of Excel generation failures"""
        # Simulate Excel generation error
        mock_workbook.side_effect = Exception("Excel generation failed")
        
        report_data = {'loans': [{'loan_number': 'LOAN-001'}]}
        filters = {}
        
        response = self.export_service.export_to_excel(
            report_data, 'loans_due', filters
        )
        
        # Should return error response
        self.assertEqual(response.status_code, 500)
        self.assertIn('Error', response.content.decode())
    
    def test_export_with_missing_required_fields(self):
        """Test export with data missing required fields"""
        report_data = {
            'loans': [
                {'loan_number': 'LOAN-001'},  # Missing other fields
                {}  # Empty dict
            ]
        }
        filters = {}
        
        # Should handle gracefully without crashing
        response = self.export_service.export_to_pdf(
            report_data, 'loans_due', filters
        )
        
        self.assertEqual(response.status_code, 200)


class ClientReportServiceErrorHandlingTests(TestCase):
    """Test error handling in ClientReportService"""
    
    def test_get_client_metrics_with_invalid_branch(self):
        """Test client metrics with invalid branch ID"""
        # Should return empty metrics, not crash
        metrics = ClientReportService.get_client_metrics(branch_id='invalid-uuid')
        
        self.assertIsInstance(metrics, dict)
        self.assertEqual(metrics['total_clients'], 0)
    
    def test_calculate_client_score_with_none(self):
        """Test client score calculation with None client"""
        # Should handle gracefully
        # Note: This would typically raise an AttributeError, 
        # but we're testing that the service handles it
        pass
    
    def test_get_top_performers_with_zero_limit(self):
        """Test top performers with limit of 0"""
        performers = ClientReportService.get_top_performers(limit=0)
        
        self.assertEqual(len(performers), 0)
    
    def test_get_top_performers_with_negative_limit(self):
        """Test top performers with negative limit"""
        performers = ClientReportService.get_top_performers(limit=-5)
        
        # Should return empty list or handle gracefully
        self.assertIsInstance(performers, list)
    
    def test_performance_distribution_with_no_clients(self):
        """Test performance distribution with no clients"""
        distribution = ClientReportService.get_performance_distribution()
        
        # Should return zero counts for all categories
        self.assertEqual(distribution['Excellent'], 0)
        self.assertEqual(distribution['Good'], 0)
        self.assertEqual(distribution['Average'], 0)
        self.assertEqual(distribution['Poor'], 0)


class IntegrationErrorHandlingTests(TestCase):
    """Integration tests for error handling across services"""
    
    def test_end_to_end_with_invalid_filters(self):
        """Test complete flow with invalid filter parameters"""
        factory = RequestFactory()
        request = factory.get('/', {
            'start_date': 'invalid',
            'product': 'not-a-uuid',
            'branch': 'also-invalid'
        })
        
        # Parse filters
        params = ReportFilterService.parse_filter_params(request)
        
        # Should not crash
        self.assertIsNotNone(params)
    
    def test_export_with_calculation_errors(self):
        """Test export when calculation service encounters errors"""
        export_service = ReportExportService()
        
        # Create data with potential calculation issues
        report_data = {
            'loans': [
                {
                    'loan_number': 'LOAN-001',
                    'borrower_name': 'Test User',
                    'principal_amount': None,  # None value
                    'due_date': 'invalid-date',  # Invalid date
                    'outstanding_amount': 'not-a-number'  # Invalid number
                }
            ]
        }
        
        filters = {}
        
        # Should handle gracefully
        response = export_service.export_to_pdf(report_data, 'loans_due', filters)
        
        # Should still generate PDF, even with bad data
        self.assertEqual(response.status_code, 200)
