"""
Property-Based Tests for Report Export Functionality

This module contains property-based tests for the report export service,
validating universal correctness properties across all inputs.

Feature: comprehensive-reports-and-fixes
"""

from hypothesis import given, strategies as st, settings, assume
from hypothesis.extra.django import TestCase
from decimal import Decimal
from datetime import date, datetime, timedelta
from django.test import RequestFactory
from django.contrib.auth import get_user_model
from django.http import HttpResponse
from io import BytesIO
import openpyxl
from PyPDF2 import PdfReader

from reports.export_service import ReportExportService
from loans.models import Loan, LoanProduct, LoanApplication
from users.models import Branch

User = get_user_model()


class ExportPropertyTests(TestCase):
    """
    Property-based tests for export functionality.
    
    These tests verify universal properties that should hold for all valid inputs.
    """
    
    def setUp(self):
        """Set up test data"""
        self.export_service = ReportExportService()
        self.factory = RequestFactory()
        
        # Create or get test branch
        self.branch, _ = Branch.objects.get_or_create(
            code='TB001',
            defaults={
                'name': 'Test Branch',
                'is_main_branch': False
            }
        )
        
        # Create test user
        self.user, _ = User.objects.get_or_create(
            username='testuser_export',
            defaults={
                'email': 'test@example.com',
                'first_name': 'Test',
                'last_name': 'User',
                'phone_number': '0700000000',
                'branch': self.branch
            }
        )
        
        # Create test loan product
        self.loan_product, _ = LoanProduct.objects.get_or_create(
            name='Test Product',
            defaults={
                'product_type': 'boost',
                'description': 'Test product for testing',
                '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,
                'duration_months': 1,
                'available_repayment_methods': ['daily', 'weekly', 'monthly']
            }
        )
    
    @given(
        num_loans=st.integers(min_value=1, max_value=50),
        principal=st.decimals(min_value=1000, max_value=100000, places=2),
    )
    @settings(max_examples=50, deadline=None)
    def test_property_4_export_completeness(self, num_loans, principal):
        """
        Feature: comprehensive-reports-and-fixes, Property 4: Export Completeness
        
        For any filtered report data set, when exported to PDF or Excel, 
        the export should contain all records from the filtered set without 
        truncation or omission.
        
        Validates: Requirements 1.9, 1.10, 1.12, 7.10, 7.11
        """
        # Create test loans
        loans_data = []
        for i in range(num_loans):
            loan_data = {
                'loan_number': f'LN{i:04d}',
                'borrower_name': f'Borrower {i}',
                'phone_number': f'07000000{i:02d}',
                'disbursement_date': date.today() - timedelta(days=i),
                'principal_amount': principal,
                'interest_amount': principal * Decimal('0.10'),
                'processing_fee': principal * Decimal('0.05'),
                'total_amount': principal * Decimal('1.15'),
                'amount_paid': Decimal('0.00'),
                'outstanding_amount': principal * Decimal('1.15'),
            }
            loans_data.append(loan_data)
        
        report_data = {'loans': loans_data}
        filters = {'period': 'total'}
        
        # Test Excel export completeness
        excel_response = self.export_service.export_to_excel(
            report_data, 'disbursed_loans', filters
        )
        
        # Verify response is valid
        self.assertIsInstance(excel_response, HttpResponse)
        self.assertEqual(
            excel_response['Content-Type'],
            'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'
        )
        
        # Parse Excel content
        excel_content = BytesIO(excel_response.content)
        workbook = openpyxl.load_workbook(excel_content)
        sheet = workbook.active
        
        # Count data rows (excluding headers and metadata)
        data_rows = 0
        for row in sheet.iter_rows(min_row=1):
            # Look for loan numbers in the row
            for cell in row:
                if cell.value and isinstance(cell.value, str) and cell.value.startswith('LN'):
                    data_rows += 1
                    break
        
        # Property: All loans should be in the export
        self.assertEqual(
            data_rows, num_loans,
            f"Excel export should contain all {num_loans} loans, found {data_rows}"
        )
        
        # Test PDF export completeness
        pdf_response = self.export_service.export_to_pdf(
            report_data, 'disbursed_loans', filters
        )
        
        # Verify response is valid
        self.assertIsInstance(pdf_response, HttpResponse)
        self.assertEqual(pdf_response['Content-Type'], 'application/pdf')
        
        # Parse PDF content
        pdf_content = BytesIO(pdf_response.content)
        pdf_reader = PdfReader(pdf_content)
        
        # Extract text from all pages
        pdf_text = ''
        for page in pdf_reader.pages:
            pdf_text += page.extract_text()
        
        # Property: All loan numbers should appear in PDF
        for loan in loans_data:
            self.assertIn(
                loan['loan_number'], pdf_text,
                f"Loan {loan['loan_number']} should appear in PDF export"
            )
    
    @given(
        currency_amount=st.decimals(min_value=0, max_value=1000000, places=2),
    )
    @settings(max_examples=50, deadline=None)
    def test_property_22_pdf_formatting_standards(self, currency_amount):
        """
        Feature: comprehensive-reports-and-fixes, Property 22: PDF Formatting Standards
        
        For any PDF export, the document should include headers, footers, page numbers,
        consistent styling, properly formatted currency (thousand separators, 2 decimal places),
        and consistently formatted dates.
        
        Validates: Requirements 7.1, 7.2, 7.4, 7.5, 7.6, 7.7, 7.8
        """
        # Create test loan data with the given amount
        loan_data = {
            'loan_number': 'LN0001',
            'borrower_name': 'Test Borrower',
            'phone_number': '0700000000',
            'disbursement_date': date.today(),
            'principal_amount': currency_amount,
            'interest_amount': currency_amount * Decimal('0.10'),
            'processing_fee': currency_amount * Decimal('0.05'),
            'total_amount': currency_amount * Decimal('1.15'),
            'amount_paid': Decimal('0.00'),
            'outstanding_amount': currency_amount * Decimal('1.15'),
        }
        
        report_data = {'loans': [loan_data]}
        filters = {'period': 'today', 'start_date': date.today(), 'end_date': date.today()}
        
        # Generate PDF
        pdf_response = self.export_service.export_to_pdf(
            report_data, 'disbursed_loans', filters
        )
        
        # Verify response is valid
        self.assertIsInstance(pdf_response, HttpResponse)
        self.assertEqual(pdf_response['Content-Type'], 'application/pdf')
        
        # Parse PDF content
        pdf_content = BytesIO(pdf_response.content)
        pdf_reader = PdfReader(pdf_content)
        
        # Extract text from all pages
        pdf_text = ''
        for page in pdf_reader.pages:
            pdf_text += page.extract_text()
        
        # Property: Currency should be formatted with "KES" prefix and 2 decimal places
        formatted_currency = self.export_service.format_currency(currency_amount)
        self.assertIn('KES', formatted_currency)
        self.assertRegex(formatted_currency, r'KES\s+[\d,]+\.\d{2}')
        
        # Property: Date should be formatted consistently
        formatted_date = self.export_service.format_date(date.today())
        self.assertRegex(formatted_date, r'\d{4}-\d{2}-\d{2}')
        
        # Property: Report title should be present
        self.assertIn('Disbursed Loans Report', pdf_text)
        
        # Property: Generation date should be present
        self.assertIn('Generated', pdf_text)
        
        # Property: Filter metadata should be present
        self.assertIn('Applied Filters', pdf_text)
    
    @given(
        num_loans=st.integers(min_value=1, max_value=30),
    )
    @settings(max_examples=50, deadline=None)
    def test_property_23_excel_completeness(self, num_loans):
        """
        Feature: comprehensive-reports-and-fixes, Property 23: Excel Completeness
        
        For any report exported to Excel, the spreadsheet should include all visible
        columns from the web view with the same filters and sorting applied.
        
        Validates: Requirements 7.3, 7.10, 7.11
        """
        # Create test loans
        loans_data = []
        expected_columns = [
            'loan_number', 'borrower_name', 'phone_number', 'disbursement_date',
            'principal_amount', 'interest_amount', 'processing_fee', 'total_amount',
            'amount_paid', 'outstanding_amount'
        ]
        
        for i in range(num_loans):
            loan_data = {
                'loan_number': f'LN{i:04d}',
                'borrower_name': f'Borrower {i}',
                'phone_number': f'07000000{i:02d}',
                'disbursement_date': date.today() - timedelta(days=i),
                'principal_amount': Decimal('10000.00'),
                'interest_amount': Decimal('1000.00'),
                'processing_fee': Decimal('500.00'),
                'total_amount': Decimal('11500.00'),
                'amount_paid': Decimal('0.00'),
                'outstanding_amount': Decimal('11500.00'),
            }
            loans_data.append(loan_data)
        
        report_data = {'loans': loans_data}
        filters = {'period': 'total'}
        
        # Generate Excel export
        excel_response = self.export_service.export_to_excel(
            report_data, 'disbursed_loans', filters
        )
        
        # Parse Excel content
        excel_content = BytesIO(excel_response.content)
        workbook = openpyxl.load_workbook(excel_content)
        sheet = workbook.active
        
        # Find header row (look for "Loan Number" or similar)
        header_row_idx = None
        for idx, row in enumerate(sheet.iter_rows(min_row=1, max_row=20), start=1):
            for cell in row:
                if cell.value and 'Loan Number' in str(cell.value):
                    header_row_idx = idx
                    break
            if header_row_idx:
                break
        
        # Property: Header row should exist
        self.assertIsNotNone(header_row_idx, "Excel should have a header row")
        
        # Property: All data rows should be present
        data_row_count = 0
        if header_row_idx:
            for row in sheet.iter_rows(min_row=header_row_idx + 1):
                # Check if row has loan number
                if row[0].value and str(row[0].value).startswith('LN'):
                    data_row_count += 1
        
        self.assertEqual(
            data_row_count, num_loans,
            f"Excel should contain all {num_loans} data rows"
        )
    
    @given(
        report_type=st.sampled_from(['disbursed_loans', 'loans_due', 'processing_fees']),
        num_loans=st.integers(min_value=1, max_value=20),
    )
    @settings(max_examples=50, deadline=None)
    def test_property_24_report_standards_consistency(self, report_type, num_loans):
        """
        Feature: comprehensive-reports-and-fixes, Property 24: Report Standards Consistency
        
        For any page with sorting, filtering, or report generation functionality,
        the same report generation standards (formatting, completeness, metadata)
        should be applied.
        
        Validates: Requirements 7.12
        """
        # Create test loans
        loans_data = []
        for i in range(num_loans):
            loan_data = {
                'loan_number': f'LN{i:04d}',
                'borrower_name': f'Borrower {i}',
                'phone_number': f'07000000{i:02d}',
                'disbursement_date': date.today() - timedelta(days=i),
                'principal_amount': Decimal('10000.00'),
                'interest_amount': Decimal('1000.00'),
                'processing_fee': Decimal('500.00'),
                'total_amount': Decimal('11500.00'),
                'amount_paid': Decimal('0.00'),
                'outstanding_amount': Decimal('11500.00'),
                'due_date': date.today() + timedelta(days=30),
            }
            loans_data.append(loan_data)
        
        report_data = {'loans': loans_data}
        filters = {'period': 'total'}
        
        # Generate PDF for this report type
        pdf_response = self.export_service.export_to_pdf(
            report_data, report_type, filters
        )
        
        # Property: All report types should return valid PDF responses
        self.assertIsInstance(pdf_response, HttpResponse)
        self.assertEqual(pdf_response['Content-Type'], 'application/pdf')
        
        # Parse PDF
        pdf_content = BytesIO(pdf_response.content)
        pdf_reader = PdfReader(pdf_content)
        pdf_text = ''
        for page in pdf_reader.pages:
            pdf_text += page.extract_text()
        
        # Property: All reports should have consistent metadata
        self.assertIn('Generated', pdf_text, "All reports should show generation date")
        self.assertIn('Report', pdf_text, "All reports should have 'Report' in title")
        
        # Generate Excel for this report type
        excel_response = self.export_service.export_to_excel(
            report_data, report_type, filters
        )
        
        # Property: All report types should return valid Excel responses
        self.assertIsInstance(excel_response, HttpResponse)
        self.assertEqual(
            excel_response['Content-Type'],
            'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'
        )
        
        # Property: All Excel exports should have proper filename
        self.assertIn('attachment', excel_response['Content-Disposition'])
        self.assertIn('.xlsx', excel_response['Content-Disposition'])
    
    @given(
        num_loans=st.integers(min_value=1, max_value=20),
    )
    @settings(max_examples=50, deadline=None)
    def test_property_35_excel_export_round_trip(self, num_loans):
        """
        Feature: comprehensive-reports-and-fixes, Property 35: Excel Export Round-Trip
        
        For any valid report data object, serializing to Excel then parsing the Excel
        should produce equivalent data with the same values.
        
        Validates: Requirements 10.6
        """
        # Create test loans with specific values
        loans_data = []
        for i in range(num_loans):
            loan_data = {
                'loan_number': f'LN{i:04d}',
                'borrower_name': f'Borrower {i}',
                'phone_number': f'07000000{i:02d}',
                'disbursement_date': date.today() - timedelta(days=i),
                'principal_amount': Decimal('10000.00') + Decimal(i),
                'interest_amount': Decimal('1000.00'),
                'processing_fee': Decimal('500.00'),
                'total_amount': Decimal('11500.00') + Decimal(i),
                'amount_paid': Decimal('0.00'),
                'outstanding_amount': Decimal('11500.00') + Decimal(i),
            }
            loans_data.append(loan_data)
        
        report_data = {'loans': loans_data}
        filters = {'period': 'total'}
        
        # Generate Excel export
        excel_response = self.export_service.export_to_excel(
            report_data, 'disbursed_loans', filters
        )
        
        # Parse Excel content
        excel_content = BytesIO(excel_response.content)
        workbook = openpyxl.load_workbook(excel_content)
        sheet = workbook.active
        
        # Find header row and extract data
        header_row_idx = None
        for idx, row in enumerate(sheet.iter_rows(min_row=1, max_row=20), start=1):
            for cell in row:
                if cell.value and 'Loan Number' in str(cell.value):
                    header_row_idx = idx
                    break
            if header_row_idx:
                break
        
        # Extract loan numbers from Excel
        excel_loan_numbers = []
        if header_row_idx:
            for row in sheet.iter_rows(min_row=header_row_idx + 1):
                loan_number = row[0].value
                if loan_number and str(loan_number).startswith('LN'):
                    excel_loan_numbers.append(str(loan_number))
        
        # Property: All original loan numbers should be in Excel
        original_loan_numbers = [loan['loan_number'] for loan in loans_data]
        for loan_number in original_loan_numbers:
            self.assertIn(
                loan_number, excel_loan_numbers,
                f"Loan number {loan_number} should be in Excel export"
            )
        
        # Property: Excel should have same number of loans as original
        self.assertEqual(
            len(excel_loan_numbers), num_loans,
            "Excel should contain same number of loans as original data"
        )


class ExportFormattingPropertyTests(TestCase):
    """Property tests for export formatting functions"""
    
    def setUp(self):
        """Set up test service"""
        self.export_service = ReportExportService()
    
    @given(
        amount=st.decimals(min_value=0, max_value=10000000, places=2),
    )
    @settings(max_examples=50, deadline=None)
    def test_currency_formatting_always_has_two_decimals(self, amount):
        """
        Currency formatting should always produce exactly 2 decimal places.
        
        Validates: Requirements 7.6, 9.5
        """
        formatted = self.export_service.format_currency(amount)
        
        # Property: Should have "KES" prefix
        self.assertIn('KES', formatted)
        
        # Property: Should have exactly 2 decimal places
        self.assertRegex(formatted, r'KES\s+[\d,]+\.\d{2}$')
    
    @given(
        test_date=st.dates(min_value=date(2020, 1, 1), max_value=date(2030, 12, 31)),
    )
    @settings(max_examples=50, deadline=None)
    def test_date_formatting_consistency(self, test_date):
        """
        Date formatting should be consistent across all dates.
        
        Validates: Requirements 7.7
        """
        formatted = self.export_service.format_date(test_date)
        
        # Property: Should match YYYY-MM-DD format
        self.assertRegex(formatted, r'\d{4}-\d{2}-\d{2}')
        
        # Property: Should be parseable back to a date
        parsed_date = datetime.strptime(formatted, '%Y-%m-%d').date()
        self.assertEqual(parsed_date, test_date)

