"""
Unit tests for Grazuri Haven Investment Limited loan product calculations.

Tests cover the two canonical Grazuri products:
  - Biashara Loan  (product_type='biashara', accountType='B', product_id=2 in Grazuri DB)
  - Log Book Loan  (product_type='logbook',  accountType='P', product_id=3 in Grazuri DB)

Validates: Requirements 3.2, 3.3, 3.5, 9.1, 9.2, 9.3, 9.5
Feature: grazuri-migration, Phase 7: Loan Product Implementation
"""

from decimal import Decimal
import uuid
from django.test import TestCase
from django.utils import timezone
from datetime import timedelta

from loans.models import LoanProduct, LoanApplication, Loan, Repayment


# ---------------------------------------------------------------------------
# Helpers
# ---------------------------------------------------------------------------

def _make_user():
    """Create a minimal test user without triggering signal overhead."""
    from users.models import CustomUser
    uid = uuid.uuid4().hex[:8]
    return CustomUser.objects.create_user(
        username=f"grazuri_test_{uid}",
        email=f"grazuri_{uid}@test.local",
        phone_number=f"+2547{uid[:8]}",
        first_name="Test",
        last_name="Grazuri",
    )


def _make_biashara_product(**overrides):
    """Return an unsaved Biashara Loan LoanProduct with sensible defaults."""
    defaults = dict(
        name="Biashara Loan",
        product_type="biashara",
        grazuri_account_type="B",
        gl_code="11002",
        description="Grazuri Biashara business loan",
        min_amount=Decimal("1000.00"),
        max_amount=Decimal("50000.00"),
        interest_rate=Decimal("15.00"),
        processing_fee=Decimal("2.00"),
        late_payment_penalty=Decimal("5.00"),
        min_duration=30,
        max_duration=365,
        duration_months=1,
        available_repayment_methods=["monthly"],
        available_durations=[],
    )
    defaults.update(overrides)
    return LoanProduct.objects.create(**defaults)


def _make_logbook_product(**overrides):
    """Return an unsaved Log Book Loan LoanProduct with sensible defaults."""
    defaults = dict(
        name="Log Book Loan",
        product_type="logbook",
        grazuri_account_type="P",
        gl_code="11002",
        description="Grazuri Log Book vehicle-secured loan",
        min_amount=Decimal("5000.00"),
        max_amount=Decimal("100000.00"),
        interest_rate=Decimal("12.00"),
        processing_fee=Decimal("3.00"),
        late_payment_penalty=Decimal("5.00"),
        min_duration=30,
        max_duration=365,
        duration_months=3,
        available_repayment_methods=["monthly"],
        available_durations=[],
        requires_collateral=True,
    )
    defaults.update(overrides)
    return LoanProduct.objects.create(**defaults)


def _make_loan(user, product, principal, duration_days=30):
    """Create a complete loan (application + loan) for test purposes."""
    months = max(Decimal("1"), Decimal(str(duration_days)) / Decimal("30"))
    interest = product.calculate_interest(principal, months)
    fee = product.calculate_processing_fee(principal, months)
    total = Decimal(str(principal)) + interest + fee

    app = LoanApplication.objects.create(
        application_number=f"APP-{uuid.uuid4().hex[:8]}",
        borrower=user,
        loan_product=product,
        requested_amount=principal,
        requested_duration=duration_days,
        purpose="Unit test loan",
        status="approved",
        interest_amount=interest,
        processing_fee_amount=fee,
        total_amount=total,
    )
    loan = Loan.objects.create(
        loan_number=f"LOAN-{uuid.uuid4().hex[:8]}",
        application=app,
        borrower=user,
        principal_amount=principal,
        interest_amount=interest,
        processing_fee=fee,
        total_amount=total,
        disbursement_date=timezone.now(),
        due_date=timezone.now() + timedelta(days=duration_days),
        duration_days=duration_days,
        status="active",
    )
    return loan


# ---------------------------------------------------------------------------
# 9.1  Product model instances
# ---------------------------------------------------------------------------

class TestGrazuriProductInstances(TestCase):
    """Task 9.1 – Verify model fields for Grazuri products."""

    def test_biashara_product_type_and_account_type(self):
        p = _make_biashara_product()
        self.assertEqual(p.product_type, "biashara")
        self.assertEqual(p.grazuri_account_type, "B")
        self.assertEqual(p.gl_code, "11002")

    def test_logbook_product_type_and_account_type(self):
        p = _make_logbook_product()
        self.assertEqual(p.product_type, "logbook")
        self.assertEqual(p.grazuri_account_type, "P")
        self.assertEqual(p.gl_code, "11002")

    def test_biashara_amount_limits(self):
        p = _make_biashara_product()
        self.assertEqual(p.min_amount, Decimal("1000.00"))
        self.assertEqual(p.max_amount, Decimal("50000.00"))

    def test_logbook_amount_limits(self):
        p = _make_logbook_product()
        self.assertEqual(p.min_amount, Decimal("5000.00"))
        self.assertEqual(p.max_amount, Decimal("100000.00"))

    def test_is_grazuri_product_flags(self):
        b = _make_biashara_product()
        l = _make_logbook_product()
        self.assertTrue(b.is_grazuri_product())
        self.assertTrue(l.is_grazuri_product())
        self.assertTrue(b.is_biashara_product())
        self.assertTrue(l.is_logbook_product())

    def test_logbook_requires_collateral(self):
        p = _make_logbook_product()
        self.assertTrue(p.requires_collateral)

    def test_biashara_no_collateral_required(self):
        p = _make_biashara_product()
        self.assertFalse(p.requires_collateral)

    def test_to_dict_includes_grazuri_fields(self):
        p = _make_biashara_product()
        d = p.to_dict()
        self.assertIn("gl_code", d)
        self.assertIn("grazuri_account_type", d)
        self.assertIn("is_grazuri_product", d)
        self.assertEqual(d["gl_code"], "11002")
        self.assertTrue(d["is_grazuri_product"])


# ---------------------------------------------------------------------------
# 9.2  Interest calculation accuracy
# ---------------------------------------------------------------------------

class TestGrazuriInterestCalculation(TestCase):
    """Task 9.2 – Verify flat (simple) monthly interest calculations."""

    # ---- Biashara Loan (15% / month) ----

    def test_biashara_interest_1_month(self):
        """KES 10,000 for 1 month @ 15% = KES 1,500.00"""
        p = _make_biashara_product()
        result = p.calculate_interest(Decimal("10000.00"), 1)
        self.assertEqual(result, Decimal("1500.00"))

    def test_biashara_interest_3_months(self):
        """KES 10,000 for 3 months @ 15% = KES 4,500.00"""
        p = _make_biashara_product()
        result = p.calculate_interest(Decimal("10000.00"), 3)
        self.assertEqual(result, Decimal("4500.00"))

    def test_biashara_interest_6_months(self):
        """KES 20,000 for 6 months @ 15% = KES 18,000.00"""
        p = _make_biashara_product()
        result = p.calculate_interest(Decimal("20000.00"), 6)
        self.assertEqual(result, Decimal("18000.00"))

    def test_biashara_interest_from_days_30(self):
        """30 days → 1 month → same as 1-month calculation."""
        p = _make_biashara_product()
        self.assertEqual(
            p.calculate_interest_from_days(Decimal("10000.00"), 30),
            p.calculate_interest(Decimal("10000.00"), 1),
        )

    def test_biashara_interest_from_days_60(self):
        """60 days → 2 months."""
        p = _make_biashara_product()
        result = p.calculate_interest_from_days(Decimal("10000.00"), 60)
        self.assertEqual(result, Decimal("3000.00"))

    def test_biashara_interest_from_days_less_than_30_enforces_minimum(self):
        """Less than 30 days should be treated as 1 month minimum."""
        p = _make_biashara_product()
        result_15 = p.calculate_interest_from_days(Decimal("10000.00"), 15)
        result_1m = p.calculate_interest(Decimal("10000.00"), 1)
        self.assertEqual(result_15, result_1m)

    # ---- Log Book Loan (12% / month) ----

    def test_logbook_interest_1_month(self):
        """KES 50,000 for 1 month @ 12% = KES 6,000.00"""
        p = _make_logbook_product()
        result = p.calculate_interest(Decimal("50000.00"), 1)
        self.assertEqual(result, Decimal("6000.00"))

    def test_logbook_interest_3_months(self):
        """KES 50,000 for 3 months @ 12% = KES 18,000.00"""
        p = _make_logbook_product()
        result = p.calculate_interest(Decimal("50000.00"), 3)
        self.assertEqual(result, Decimal("18000.00"))

    def test_logbook_interest_from_days_90(self):
        """90 days → 3 months."""
        p = _make_logbook_product()
        result = p.calculate_interest_from_days(Decimal("50000.00"), 90)
        self.assertEqual(result, Decimal("18000.00"))

    def test_interest_is_simple_not_compound(self):
        """Grazuri uses flat/simple interest: year interest = 12 × monthly."""
        p = _make_biashara_product()
        monthly_interest = p.calculate_interest(Decimal("10000.00"), 1)
        annual_interest = p.calculate_interest(Decimal("10000.00"), 12)
        # Simple: annual = 12 × monthly  (not compound which would be higher)
        self.assertEqual(annual_interest, monthly_interest * 12)

    def test_interest_proportional_to_principal(self):
        """Doubling principal should double interest."""
        p = _make_biashara_product()
        i1 = p.calculate_interest(Decimal("10000.00"), 2)
        i2 = p.calculate_interest(Decimal("20000.00"), 2)
        self.assertEqual(i2, i1 * 2)

    def test_interest_proportional_to_months(self):
        """Doubling months should double interest."""
        p = _make_logbook_product()
        i1 = p.calculate_interest(Decimal("30000.00"), 2)
        i2 = p.calculate_interest(Decimal("30000.00"), 4)
        self.assertEqual(i2, i1 * 2)


# ---------------------------------------------------------------------------
# 9.3  Processing fee calculation accuracy
# ---------------------------------------------------------------------------

class TestGrazuriProcessingFeeCalculation(TestCase):
    """Task 9.3 – Verify one-time upfront processing fee calculations."""

    def test_biashara_fee_one_time(self):
        """KES 10,000 @ 2% = KES 200.00 regardless of months."""
        p = _make_biashara_product()
        fee_1m = p.calculate_processing_fee(Decimal("10000.00"), months=1)
        fee_3m = p.calculate_processing_fee(Decimal("10000.00"), months=3)
        self.assertEqual(fee_1m, Decimal("200.00"))
        # Biashara is one-time — should NOT vary with months
        self.assertEqual(fee_1m, fee_3m)

    def test_logbook_fee_one_time(self):
        """KES 50,000 @ 3% = KES 1,500.00 regardless of months."""
        p = _make_logbook_product()
        fee_1m = p.calculate_processing_fee(Decimal("50000.00"), months=1)
        fee_6m = p.calculate_processing_fee(Decimal("50000.00"), months=6)
        self.assertEqual(fee_1m, Decimal("1500.00"))
        self.assertEqual(fee_1m, fee_6m)

    def test_fee_proportional_to_principal(self):
        """Doubling principal doubles the fee."""
        p = _make_biashara_product()
        f1 = p.calculate_processing_fee(Decimal("10000.00"))
        f2 = p.calculate_processing_fee(Decimal("20000.00"))
        self.assertEqual(f2, f1 * 2)

    def test_biashara_fee_rounding(self):
        """Fee should always be rounded to 2 decimal places."""
        p = _make_biashara_product()
        fee = p.calculate_processing_fee(Decimal("10001.00"))
        self.assertEqual(fee, fee.quantize(Decimal("0.01")))


# ---------------------------------------------------------------------------
# 9.5  Total loan amount calculations
# ---------------------------------------------------------------------------

class TestGrazuriTotalLoanAmount(TestCase):
    """Task 9.5 – Verify total = principal + interest + fee."""

    def test_biashara_total_30_days(self):
        """KES 10,000 Biashara, 30 days: total = 10000 + 1500 + 200 = 11700."""
        p = _make_biashara_product()
        total = p.calculate_total_loan_amount(Decimal("10000.00"), 30)
        self.assertEqual(total, Decimal("11700.00"))

    def test_biashara_total_60_days(self):
        """KES 10,000 Biashara, 60 days (2 months): 10000 + 3000 + 200 = 13200."""
        p = _make_biashara_product()
        total = p.calculate_total_loan_amount(Decimal("10000.00"), 60)
        self.assertEqual(total, Decimal("13200.00"))

    def test_logbook_total_30_days(self):
        """KES 50,000 Log Book, 30 days: 50000 + 6000 + 1500 = 57500."""
        p = _make_logbook_product()
        total = p.calculate_total_loan_amount(Decimal("50000.00"), 30)
        self.assertEqual(total, Decimal("57500.00"))

    def test_logbook_total_90_days(self):
        """KES 50,000 Log Book, 90 days (3 months): 50000 + 18000 + 1500 = 69500."""
        p = _make_logbook_product()
        total = p.calculate_total_loan_amount(Decimal("50000.00"), 90)
        self.assertEqual(total, Decimal("69500.00"))

    def test_total_equals_principal_plus_components(self):
        """total_loan_amount = principal + interest + fee (always)."""
        p = _make_biashara_product()
        principal = Decimal("25000.00")
        duration_days = 60
        months = max(Decimal("1"), Decimal(str(duration_days)) / Decimal("30"))
        interest = p.calculate_interest(principal, months)
        fee = p.calculate_processing_fee(principal, months)
        expected = (principal + interest + fee).quantize(Decimal("0.01"))
        total = p.calculate_total_loan_amount(principal, duration_days)
        self.assertEqual(total, expected)


# ---------------------------------------------------------------------------
# 9.5  Loan balance after payments
# ---------------------------------------------------------------------------

class TestGrazuriLoanBalance(TestCase):
    """Task 9.5 – Verify outstanding_amount after payments."""

    def setUp(self):
        self.user = _make_user()
        self.biashara = _make_biashara_product()
        self.logbook = _make_logbook_product()

    def test_outstanding_equals_total_when_no_payments(self):
        loan = _make_loan(self.user, self.biashara, Decimal("10000.00"), 30)
        self.assertEqual(loan.outstanding_amount, loan.total_amount)

    def test_outstanding_decreases_with_partial_payment(self):
        loan = _make_loan(self.user, self.biashara, Decimal("10000.00"), 30)
        payment = Decimal("5000.00")
        Repayment.objects.create(
            loan=loan,
            amount=payment,
            payment_method="mpesa",
            receipt_number=f"RCP-{uuid.uuid4().hex[:8]}",
            payment_date=timezone.now(),
        )
        self.assertEqual(loan.outstanding_amount, loan.total_amount - payment)

    def test_outstanding_zero_when_fully_paid(self):
        loan = _make_loan(self.user, self.logbook, Decimal("50000.00"), 30)
        Repayment.objects.create(
            loan=loan,
            amount=loan.total_amount,
            payment_method="mpesa",
            receipt_number=f"RCP-{uuid.uuid4().hex[:8]}",
            payment_date=timezone.now(),
        )
        self.assertEqual(loan.outstanding_amount, Decimal("0.00"))

    def test_amount_paid_equals_sum_of_repayments(self):
        loan = _make_loan(self.user, self.logbook, Decimal("50000.00"), 90)
        p1 = Decimal("20000.00")
        p2 = Decimal("15000.00")
        for amt in (p1, p2):
            Repayment.objects.create(
                loan=loan,
                amount=amt,
                payment_method="mpesa",
                receipt_number=f"RCP-{uuid.uuid4().hex[:8]}",
                payment_date=timezone.now(),
            )
        self.assertEqual(loan.amount_paid, p1 + p2)

    def test_outstanding_never_negative_on_overpayment(self):
        """Even if repayments exceed total, outstanding should not go below 0."""
        loan = _make_loan(self.user, self.biashara, Decimal("10000.00"), 30)
        Repayment.objects.create(
            loan=loan,
            amount=loan.total_amount + Decimal("1000.00"),
            payment_method="mpesa",
            receipt_number=f"RCP-{uuid.uuid4().hex[:8]}",
            payment_date=timezone.now(),
        )
        self.assertGreaterEqual(loan.outstanding_amount, Decimal("0.00"))


# ---------------------------------------------------------------------------
# 9.4  Loan approval workflow — Grazuri products
# ---------------------------------------------------------------------------

class TestGrazuriLoanApproval(TestCase):
    """Task 9.4 – Verify loan creation/approval computes correct amounts."""

    def setUp(self):
        self.user = _make_user()
        self.biashara = _make_biashara_product()
        self.logbook = _make_logbook_product()

    def test_biashara_loan_amounts_set_on_approval(self):
        loan = _make_loan(self.user, self.biashara, Decimal("10000.00"), 30)
        self.assertEqual(loan.principal_amount, Decimal("10000.00"))
        self.assertEqual(loan.interest_amount, Decimal("1500.00"))
        self.assertEqual(loan.processing_fee, Decimal("200.00"))
        self.assertEqual(loan.total_amount, Decimal("11700.00"))

    def test_logbook_loan_amounts_set_on_approval(self):
        loan = _make_loan(self.user, self.logbook, Decimal("50000.00"), 30)
        self.assertEqual(loan.principal_amount, Decimal("50000.00"))
        self.assertEqual(loan.interest_amount, Decimal("6000.00"))
        self.assertEqual(loan.processing_fee, Decimal("1500.00"))
        self.assertEqual(loan.total_amount, Decimal("57500.00"))

    def test_biashara_amount_validation_below_minimum(self):
        with self.assertRaises(ValueError):
            self.biashara.validate_amount(Decimal("500.00"))

    def test_biashara_amount_validation_above_maximum(self):
        with self.assertRaises(ValueError):
            self.biashara.validate_amount(Decimal("60000.00"))

    def test_logbook_amount_validation_within_range(self):
        # Should not raise
        self.assertTrue(self.logbook.validate_amount(Decimal("50000.00")))

    def test_grazuri_products_allow_flexible_duration(self):
        """Biashara/Logbook allow any duration within min–max (not fixed list)."""
        p = _make_biashara_product()
        # Any day in range should pass validation
        self.assertTrue(p.validate_duration(45))
        self.assertTrue(p.validate_duration(180))
        self.assertTrue(p.validate_duration(365))

    def test_duration_below_minimum_raises(self):
        p = _make_biashara_product()
        with self.assertRaises(ValueError):
            p.validate_duration(10)  # min is 30

    def test_duration_above_maximum_raises(self):
        p = _make_biashara_product()
        with self.assertRaises(ValueError):
            p.validate_duration(400)  # max is 365
