"""
Unit tests for security features
Tests permission checks, CSRF protection, SQL injection, and XSS prevention
Requirements: Security Requirements 1, 2, 3, 4, 5
"""
import pytest
from django.test import TestCase, Client, RequestFactory
from django.contrib.auth import get_user_model
from django.utils import timezone
from django.urls import reverse
from decimal import Decimal
from datetime import timedelta
from loans.models import Loan, LoanApplication, LoanProduct, PenaltyCharge
from loans.sanitizers import InputSanitizer
from users.models import CustomUser
import json

User = get_user_model()


@pytest.mark.django_db
class TestSecurityUnit(TestCase):
    """
    Unit tests for security features
    """
    
    def setUp(self):
        """Set up test data"""
        self.client = Client()
        self.factory = RequestFactory()
        
        # Create admin user
        self.admin_user = CustomUser.objects.create_user(
            username='admin',
            email='admin@test.com',
            password='testpass123',
            phone_number='+254700000001',
            role='admin'
        )
        
        # Create non-admin user
        self.regular_user = CustomUser.objects.create_user(
            username='regular',
            email='regular@test.com',
            password='testpass123',
            phone_number='+254700000002',
            role='loan_officer'
        )
        
        # Create borrower
        self.borrower = CustomUser.objects.create_user(
            username='borrower',
            email='borrower@test.com',
            password='testpass123',
            phone_number='+254700000003',
            role='borrower'
        )
        
        # Create loan product
        self.loan_product = LoanProduct.objects.create(
            name='Test Product',
            product_type='boost',
            description='Test product',
            min_amount=Decimal('1000.00'),
            max_amount=Decimal('100000.00'),
            interest_rate=Decimal('10.00'),
            processing_fee=Decimal('5.00'),
            duration_months=1,
            min_duration=7,
            max_duration=90
        )
        
        # Create loan application
        self.application = LoanApplication.objects.create(
            borrower=self.borrower,
            loan_product=self.loan_product,
            requested_amount=Decimal('10000.00'),
            requested_duration=30,  # Add required field
            status='approved'
        )
        
        # Create loan
        self.loan = Loan.objects.create(
            loan_number='TEST-001',
            borrower=self.borrower,
            application=self.application,
            principal_amount=Decimal('10000.00'),
            interest_amount=Decimal('1000.00'),
            processing_fee=Decimal('500.00'),
            total_amount=Decimal('11500.00'),
            disbursement_date=timezone.now(),
            due_date=timezone.now() + timedelta(days=30),
            status='active'
        )
    
    def test_non_admin_cannot_access_edit_endpoints(self):
        """
        Test that non-admin users cannot access loan edit endpoints
        Security Requirement: 1, 2
        """
        # Login as regular user
        self.client.login(username='regular', password='testpass123')
        
        # Try to access edit loan page
        response = self.client.get(f'/loans/{self.loan.pk}/edit/')
        
        # Should be redirected or get 403
        self.assertIn(response.status_code, [302, 403])
        
        # Try to POST edit
        response = self.client.post(f'/loans/{self.loan.pk}/edit/', {
            'principal_amount': '15000.00',
            'interest_amount': '1500.00',
            'processing_fee': '750.00',
            'disbursement_date': timezone.now().date().strftime('%Y-%m-%d'),
            'due_date': (timezone.now() + timedelta(days=30)).date().strftime('%Y-%m-%d'),
            'status': 'active'
        })
        
        # Should be redirected or get 403
        self.assertIn(response.status_code, [302, 403])
        
        # Verify loan was not modified
        loan = Loan.objects.get(pk=self.loan.pk)
        self.assertEqual(loan.principal_amount, Decimal('10000.00'))
    
    def test_non_admin_cannot_add_penalties(self):
        """
        Test that non-admin users cannot add penalties
        Security Requirement: 2, 3
        """
        # Login as regular user
        self.client.login(username='regular', password='testpass123')
        
        # Try to add penalty
        response = self.client.post(
            f'/loans/{self.loan.pk}/add-penalty/',
            json.dumps({
                'amount': '500.00',
                'penalty_date': timezone.now().date().strftime('%Y-%m-%d'),
                'reason': 'Test penalty'
            }),
            content_type='application/json'
        )
        
        # Should get 403 Forbidden
        self.assertEqual(response.status_code, 403)
        
        # Verify no penalty was created
        penalties = PenaltyCharge.objects.filter(loan=self.loan)
        self.assertEqual(penalties.count(), 0)
    
    def test_csrf_protection_on_forms(self):
        """
        Test that CSRF protection is enabled on forms
        Security Requirement: 4
        """
        # Login as admin
        self.client.login(username='admin', password='testpass123')
        
        # Try to POST without CSRF token (using enforce_csrf_checks=True)
        from django.test import Client as CSRFClient
        csrf_client = CSRFClient(enforce_csrf_checks=True)
        csrf_client.login(username='admin', password='testpass123')
        
        # Try to edit loan without CSRF token
        response = csrf_client.post(f'/loans/{self.loan.pk}/edit/', {
            'principal_amount': '15000.00',
            'interest_amount': '1500.00',
            'processing_fee': '750.00',
            'disbursement_date': timezone.now().date().strftime('%Y-%m-%d'),
            'due_date': (timezone.now() + timedelta(days=30)).date().strftime('%Y-%m-%d'),
            'status': 'active'
        })
        
        # Should get 403 Forbidden due to missing CSRF token
        self.assertEqual(response.status_code, 403)
    
    def test_sql_injection_attempts_rejected(self):
        """
        Test that SQL injection attempts are rejected
        Security Requirement: 10.9
        """
        # Test various SQL injection patterns
        sql_injection_attempts = [
            "'; DROP TABLE loans; --",
            "1' OR '1'='1",
            "admin'--",
            "1; DELETE FROM loans WHERE 1=1",
            "' UNION SELECT * FROM users--"
        ]
        
        for injection_attempt in sql_injection_attempts:
            # Sanitize the input
            sanitized = InputSanitizer.sanitize_string(injection_attempt)
            
            # Verify dangerous SQL keywords are removed or escaped
            self.assertNotIn('DROP', sanitized.upper())
            self.assertNotIn('DELETE', sanitized.upper())
            self.assertNotIn('UNION', sanitized.upper())
            # OR should be removed if it's part of injection pattern
            if "OR '1'='1'" in injection_attempt:
                self.assertNotIn("OR '1'='1'", sanitized)
    
    def test_xss_attempts_sanitized(self):
        """
        Test that XSS attempts are sanitized
        Security Requirement: 10.9
        """
        # Test various XSS patterns
        xss_attempts = [
            "<script>alert('XSS')</script>",
            "<img src=x onerror=alert('XSS')>",
            "javascript:alert('XSS')",
            "<iframe src='malicious.com'></iframe>",
            "<body onload=alert('XSS')>"
        ]
        
        for xss_attempt in xss_attempts:
            # Sanitize the input
            sanitized = InputSanitizer.sanitize_string(xss_attempt)
            
            # Verify dangerous patterns are removed or escaped
            self.assertNotIn('<script', sanitized.lower())
            self.assertNotIn('javascript:', sanitized.lower())
            self.assertNotIn('onerror=', sanitized.lower())
            self.assertNotIn('onload=', sanitized.lower())
            self.assertNotIn('<iframe', sanitized.lower())
            
            # Should contain escaped HTML entities if any HTML was present
            if '<' in xss_attempt:
                # Either removed or escaped
                self.assertTrue('&lt;' in sanitized or '<' not in sanitized)
    
    def test_admin_can_access_edit_endpoints(self):
        """
        Test that admin users CAN access loan edit endpoints
        Security Requirement: 5
        """
        # Login as admin
        self.client.login(username='admin', password='testpass123')
        
        # Try to access edit loan page
        response = self.client.get(f'/loans/{self.loan.pk}/edit/')
        
        # Should be successful
        self.assertEqual(response.status_code, 200)
    
    def test_admin_can_add_penalties(self):
        """
        Test that admin users CAN add penalties
        Security Requirement: 5
        """
        # Login as admin
        self.client.login(username='admin', password='testpass123')
        
        # Add penalty
        response = self.client.post(
            f'/loans/{self.loan.pk}/add-penalty/',
            json.dumps({
                'amount': '500.00',
                'penalty_date': timezone.now().date().strftime('%Y-%m-%d'),
                'reason': 'Test penalty'
            }),
            content_type='application/json'
        )
        
        # Should be successful
        self.assertEqual(response.status_code, 200)
        
        # Verify penalty was created
        penalties = PenaltyCharge.objects.filter(loan=self.loan)
        self.assertEqual(penalties.count(), 1)
        self.assertEqual(penalties.first().amount, Decimal('500.00'))
    
    def test_unauthenticated_user_redirected(self):
        """
        Test that unauthenticated users are redirected to login
        Security Requirement: 1
        """
        # Try to access edit loan page without logging in
        response = self.client.get(f'/loans/{self.loan.pk}/edit/')
        
        # Should be redirected to login
        self.assertEqual(response.status_code, 302)
        self.assertIn('/login', response.url)
    
    def test_special_characters_handled_correctly(self):
        """
        Test that special characters in names and loan numbers are handled correctly
        Security Requirement: 10.10
        """
        # Test name with special characters
        name_with_special_chars = "O'Brien-Smith Jr."
        sanitized_name = InputSanitizer.sanitize_name(name_with_special_chars)
        
        # Should preserve valid special characters
        self.assertIn("'", sanitized_name)
        self.assertIn("-", sanitized_name)
        self.assertIn(".", sanitized_name)
        
        # Test loan number with special characters
        loan_number = "LOAN-2024-001"
        sanitized_loan_number = InputSanitizer.sanitize_loan_number(loan_number)
        
        # Should preserve alphanumeric and hyphens
        self.assertEqual(sanitized_loan_number, "LOAN-2024-001")
        
        # Test with invalid characters
        invalid_loan_number = "LOAN<script>-001"
        sanitized_invalid = InputSanitizer.sanitize_loan_number(invalid_loan_number)
        
        # Should remove invalid characters
        self.assertNotIn('<', sanitized_invalid)
        self.assertNotIn('>', sanitized_invalid)
        self.assertNotIn('script', sanitized_invalid)
    
    def test_permission_checks_logged(self):
        """
        Test that permission check failures are logged
        Security Requirement: 4
        """
        import logging
        from io import StringIO
        
        # Set up logging capture
        log_stream = StringIO()
        handler = logging.StreamHandler(log_stream)
        logger = logging.getLogger('loans')
        logger.addHandler(handler)
        logger.setLevel(logging.WARNING)
        
        # Login as regular user
        self.client.login(username='regular', password='testpass123')
        
        # Try to access admin endpoint
        response = self.client.get(f'/loans/{self.loan.pk}/edit/')
        
        # Clean up
        logger.removeHandler(handler)
        
        # Verify access was denied
        self.assertIn(response.status_code, [302, 403])
