"""
Unit Tests for Disbursed Loans Report (Task 2.4)

Feature: comprehensive-reports-and-fixes
Tests specific examples, edge cases, and error conditions for the disbursed loans report.

**Validates: Requirements 8.1**
"""
import os
import sys
import django
from decimal import Decimal
from datetime import datetime, timedelta, date

# Setup Django
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'branch_system.settings')
django.setup()

from django.test import TestCase, RequestFactory
from django.contrib.auth import get_user_model
from django.utils import timezone

from loans.models import Loan, LoanApplication, LoanProduct
from users.models import Branch
from reports.views import disbursed_loans_report

User = get_user_model()


class DisbursedLoansReportUnitTests(TestCase):
    """
    Unit tests for disbursed loans report filtering logic.
    
    **Validates: Requirements 8.1**
    """
    
    @classmethod
    def setUpClass(cls):
        super().setUpClass()
        
        cls.factory = RequestFactory()
        
        # Create test user with staff permissions
        cls.user = User.objects.create_user(
            username='test_staff',
            email='staff@test.com',
            password='testpass123',
            role='admin',
            is_staff=True,
            is_superuser=True
        )
        
        # Create test branch
        cls.branch = Branch.objects.create(
            name='Test Branch',
            code='TB001',
            is_active=True
        )
        
        # Create test loan product
        cls.loan_product = LoanProduct.objects.create(
            name='Test Product',
            product_type='boost',
            description='Test loan product',
            min_amount=Decimal('1000.00'),
            max_amount=Decimal('100000.00'),
            interest_rate=Decimal('10.00'),
            processing_fee=Decimal('5.00'),
            min_duration=7,
            max_duration=90,
            available_repayment_methods=['monthly'],
            is_active=True
        )
        
        # Create test borrower
        cls.borrower = User.objects.create_user(
            username='test_borrower',
            email='borrower@test.com',
            password='testpass123',
            role='borrower',
            first_name='Test',
            last_name='Borrower',
            phone_number='+254700000000',
            branch=cls.branch
        )
    
    def tearDown(self):
        """Clean up test data after each test"""
        Loan.objects.filter(borrower=self.borrower).delete()
        LoanApplication.objects.filter(borrower=self.borrower).delete()
    
    def create_test_loan(self, status, disbursement_date, principal_amount=Decimal('10000.00')):
        """Helper method to create a test loan"""
        application = LoanApplication.objects.create(
            borrower=self.borrower,
            loan_product=self.loan_product,
            requested_amount=principal_amount,
            requested_duration=30,
            purpose='Test loan',
            repayment_method='monthly',
            status='approved'
        )
        
        interest_amount = principal_amount * Decimal('0.10')
        processing_fee = principal_amount * Decimal('0.05')
        total_amount = principal_amount + interest_amount + processing_fee
        
        loan = Loan.objects.create(
            application=application,
            borrower=self.borrower,
            principal_amount=principal_amount,
            interest_amount=interest_amount,
            processing_fee=processing_fee,
            total_amount=total_amount,
            disbursement_date=disbursement_date,
            due_date=disbursement_date + timedelta(days=30),
            duration_days=30,
            status=status,
            is_deleted=False
        )
        
        return loan
    
    def call_view(self, **params):
        """Helper to call the view with parameters"""
        from django.test import Client
        client = Client()
        client.force_login(self.user)
        response = client.get('/reports/disbursed-loans/', params)
        return response
    
    def test_today_filter_returns_only_todays_disbursements(self):
        """Test that 'today' filter returns only loans disbursed today"""
        today = timezone.now().date()
        yesterday = today - timedelta(days=1)
        tomorrow = today + timedelta(days=1)
        
        # Create loans with different disbursement dates
        loan_today = self.create_test_loan('active', datetime.combine(today, datetime.min.time()))
        loan_yesterday = self.create_test_loan('active', datetime.combine(yesterday, datetime.min.time()))
        loan_tomorrow = self.create_test_loan('active', datetime.combine(tomorrow, datetime.min.time()))
        
        # Request report with 'today' filter
        response = self.call_view(period='today')
        
        self.assertEqual(response.status_code, 200)
        report_data = response.context['report_data']
        
        # Should only include today's loan
        self.assertEqual(len(report_data), 1)
        self.assertEqual(report_data[0]['loan_number'], loan_today.loan_number)
        
        # Verify summary
        summary = response.context['summary']
        self.assertEqual(summary['total_loans'], 1)
    
    def test_this_week_filter_returns_current_weeks_disbursements(self):
        """Test that 'this_week' filter returns loans disbursed this week"""
        today = timezone.now().date()
        
        # Calculate week boundaries (Monday to Sunday)
        week_start = today - timedelta(days=today.weekday())
        week_end = week_start + timedelta(days=6)
        
        # Create loans within and outside this week
        loan_this_week_start = self.create_test_loan('active', datetime.combine(week_start, datetime.min.time()))
        loan_this_week_mid = self.create_test_loan('paid', datetime.combine(today, datetime.min.time()))
        loan_this_week_end = self.create_test_loan('defaulted', datetime.combine(week_end, datetime.min.time()))
        loan_last_week = self.create_test_loan('active', datetime.combine(week_start - timedelta(days=7), datetime.min.time()))
        loan_next_week = self.create_test_loan('active', datetime.combine(week_end + timedelta(days=1), datetime.min.time()))
        
        # Request report with 'this_week' filter
        response = self.call_view(period='this_week')
        
        self.assertEqual(response.status_code, 200)
        report_data = response.context['report_data']
        
        # Should include 3 loans from this week
        self.assertEqual(len(report_data), 3)
        
        loan_numbers = [loan['loan_number'] for loan in report_data]
        self.assertIn(loan_this_week_start.loan_number, loan_numbers)
        self.assertIn(loan_this_week_mid.loan_number, loan_numbers)
        self.assertIn(loan_this_week_end.loan_number, loan_numbers)
        self.assertNotIn(loan_last_week.loan_number, loan_numbers)
        self.assertNotIn(loan_next_week.loan_number, loan_numbers)
    
    def test_total_filter_returns_all_disbursements(self):
        """Test that 'total' filter returns all disbursed loans regardless of date"""
        today = timezone.now().date()
        
        # Create loans with various dates
        loan1 = self.create_test_loan('active', datetime.combine(today, datetime.min.time()))
        loan2 = self.create_test_loan('paid', datetime.combine(today - timedelta(days=30), datetime.min.time()))
        loan3 = self.create_test_loan('defaulted', datetime.combine(today - timedelta(days=90), datetime.min.time()))
        loan4 = self.create_test_loan('written_off', datetime.combine(today - timedelta(days=180), datetime.min.time()))
        
        # Request report with 'total' filter (default)
        response = self.call_view(period='total')
        
        self.assertEqual(response.status_code, 200)
        report_data = response.context['report_data']
        
        # Should include all 4 loans
        self.assertEqual(len(report_data), 4)
        
        loan_numbers = [loan['loan_number'] for loan in report_data]
        self.assertIn(loan1.loan_number, loan_numbers)
        self.assertIn(loan2.loan_number, loan_numbers)
        self.assertIn(loan3.loan_number, loan_numbers)
        self.assertIn(loan4.loan_number, loan_numbers)
    
    def test_status_filter_combinations(self):
        """Test filtering by different status values"""
        today = timezone.now().date()
        
        # Create loans with different statuses
        loan_active = self.create_test_loan('active', datetime.combine(today, datetime.min.time()))
        loan_paid = self.create_test_loan('paid', datetime.combine(today, datetime.min.time()))
        loan_defaulted = self.create_test_loan('defaulted', datetime.combine(today, datetime.min.time()))
        loan_rolled_over = self.create_test_loan('rolled_over', datetime.combine(today, datetime.min.time()))
        loan_written_off = self.create_test_loan('written_off', datetime.combine(today, datetime.min.time()))
        loan_pending = self.create_test_loan('pending', datetime.combine(today, datetime.min.time()))
        
        # Test filtering by 'active' status
        response = self.call_view(status='active')
        self.assertEqual(response.status_code, 200)
        report_data = response.context['report_data']
        self.assertEqual(len(report_data), 1)
        self.assertEqual(report_data[0]['loan_number'], loan_active.loan_number)
        
        # Test filtering by 'paid' status
        response = self.call_view(status='paid')
        report_data = response.context['report_data']
        self.assertEqual(len(report_data), 1)
        self.assertEqual(report_data[0]['loan_number'], loan_paid.loan_number)
        
        # Test filtering by 'defaulted' status
        response = self.call_view(status='defaulted')
        report_data = response.context['report_data']
        self.assertEqual(len(report_data), 1)
        self.assertEqual(report_data[0]['loan_number'], loan_defaulted.loan_number)
        
        # Test no status filter (should include all disbursed statuses, not pending)
        response = self.call_view()
        report_data = response.context['report_data']
        self.assertEqual(len(report_data), 5)  # All except pending
        
        loan_numbers = [loan['loan_number'] for loan in report_data]
        self.assertNotIn(loan_pending.loan_number, loan_numbers)
    
    def test_amount_range_filtering(self):
        """Test filtering by minimum and maximum amount"""
        today = timezone.now().date()
        
        # Create loans with different amounts
        loan_small = self.create_test_loan('active', datetime.combine(today, datetime.min.time()), Decimal('5000.00'))
        loan_medium = self.create_test_loan('active', datetime.combine(today, datetime.min.time()), Decimal('15000.00'))
        loan_large = self.create_test_loan('active', datetime.combine(today, datetime.min.time()), Decimal('50000.00'))
        
        # Test minimum amount filter
        response = self.call_view(amount_min='10000')
        report_data = response.context['report_data']
        self.assertEqual(len(report_data), 2)
        loan_numbers = [loan['loan_number'] for loan in report_data]
        self.assertNotIn(loan_small.loan_number, loan_numbers)
        self.assertIn(loan_medium.loan_number, loan_numbers)
        self.assertIn(loan_large.loan_number, loan_numbers)
        
        # Test maximum amount filter
        response = self.call_view(amount_max='20000')
        report_data = response.context['report_data']
        self.assertEqual(len(report_data), 2)
        loan_numbers = [loan['loan_number'] for loan in report_data]
        self.assertIn(loan_small.loan_number, loan_numbers)
        self.assertIn(loan_medium.loan_number, loan_numbers)
        self.assertNotIn(loan_large.loan_number, loan_numbers)
        
        # Test both min and max filters
        response = self.call_view(amount_min='10000', amount_max='30000')
        report_data = response.context['report_data']
        self.assertEqual(len(report_data), 1)
        self.assertEqual(report_data[0]['loan_number'], loan_medium.loan_number)
    
    def test_sorting_by_each_column(self):
        """Test sorting by different columns in ascending and descending order"""
        today = timezone.now().date()
        
        # Create loans with different values
        loan1 = self.create_test_loan('active', datetime.combine(today - timedelta(days=2), datetime.min.time()), Decimal('5000.00'))
        loan2 = self.create_test_loan('paid', datetime.combine(today - timedelta(days=1), datetime.min.time()), Decimal('15000.00'))
        loan3 = self.create_test_loan('defaulted', datetime.combine(today, datetime.min.time()), Decimal('25000.00'))
        
        # Test sorting by disbursement_date (ascending)
        response = self.call_view(sort_by='disbursement_date', sort_order='asc')
        report_data = response.context['report_data']
        self.assertEqual(report_data[0]['loan_number'], loan1.loan_number)
        self.assertEqual(report_data[2]['loan_number'], loan3.loan_number)
        
        # Test sorting by disbursement_date (descending)
        response = self.call_view(sort_by='disbursement_date', sort_order='desc')
        report_data = response.context['report_data']
        self.assertEqual(report_data[0]['loan_number'], loan3.loan_number)
        self.assertEqual(report_data[2]['loan_number'], loan1.loan_number)
        
        # Test sorting by principal_amount (ascending)
        response = self.call_view(sort_by='principal_amount', sort_order='asc')
        report_data = response.context['report_data']
        self.assertEqual(report_data[0]['loan_number'], loan1.loan_number)
        self.assertEqual(report_data[1]['loan_number'], loan2.loan_number)
        self.assertEqual(report_data[2]['loan_number'], loan3.loan_number)
        
        # Test sorting by principal_amount (descending)
        response = self.call_view(sort_by='principal_amount', sort_order='desc')
        report_data = response.context['report_data']
        self.assertEqual(report_data[0]['loan_number'], loan3.loan_number)
        self.assertEqual(report_data[1]['loan_number'], loan2.loan_number)
        self.assertEqual(report_data[2]['loan_number'], loan1.loan_number)
        
        # Test sorting by borrower_name
        response = self.call_view(sort_by='borrower_name', sort_order='asc')
        self.assertEqual(response.status_code, 200)
        
        # Test sorting by loan_number
        response = self.call_view(sort_by='loan_number', sort_order='asc')
        self.assertEqual(response.status_code, 200)
    
    def test_summary_calculations(self):
        """Test that summary totals are calculated correctly"""
        today = timezone.now().date()
        
        # Create loans with known amounts
        loan1 = self.create_test_loan('active', datetime.combine(today, datetime.min.time()), Decimal('10000.00'))
        loan2 = self.create_test_loan('paid', datetime.combine(today, datetime.min.time()), Decimal('20000.00'))
        
        response = self.call_view()
        summary = response.context['summary']
        
        # Verify summary calculations
        self.assertEqual(summary['total_loans'], 2)
        self.assertEqual(summary['total_principal'], Decimal('30000.00'))
        
        # Interest: 10% of principal
        expected_interest = Decimal('10000.00') * Decimal('0.10') + Decimal('20000.00') * Decimal('0.10')
        self.assertEqual(summary['total_interest'], expected_interest)
        
        # Processing fee: 5% of principal
        expected_fee = Decimal('10000.00') * Decimal('0.05') + Decimal('20000.00') * Decimal('0.05')
        self.assertEqual(summary['total_processing_fee'], expected_fee)
        
        # Total amount: principal + interest + fee
        expected_total = Decimal('30000.00') + expected_interest + expected_fee
        self.assertEqual(summary['total_amount'], expected_total)
    
    def test_edge_case_empty_results(self):
        """Test that report handles empty results gracefully"""
        # Request report with filter that matches no loans
        response = self.call_view(period='today', amount_min='1000000')
        
        self.assertEqual(response.status_code, 200)
        report_data = response.context['report_data']
        summary = response.context['summary']
        
        # Should return empty results
        self.assertEqual(len(report_data), 0)
        self.assertEqual(summary['total_loans'], 0)
        self.assertEqual(summary['total_principal'], Decimal('0.00'))
    
    def test_edge_case_invalid_amount_values(self):
        """Test that invalid amount values are handled gracefully"""
        today = timezone.now().date()
        loan = self.create_test_loan('active', datetime.combine(today, datetime.min.time()))
        
        # Test with invalid amount_min
        response = self.call_view(amount_min='invalid')
        self.assertEqual(response.status_code, 200)
        # Should ignore invalid value and return all loans
        report_data = response.context['report_data']
        self.assertEqual(len(report_data), 1)
        
        # Test with invalid amount_max
        response = self.call_view(amount_max='invalid')
        self.assertEqual(response.status_code, 200)
        report_data = response.context['report_data']
        self.assertEqual(len(report_data), 1)


if __name__ == '__main__':
    import unittest
    
    # Run the tests
    suite = unittest.TestLoader().loadTestsFromTestCase(DisbursedLoansReportUnitTests)
    runner = unittest.TextTestRunner(verbosity=2)
    result = runner.run(suite)
    
    # Exit with appropriate code
    sys.exit(0 if result.wasSuccessful() else 1)
