"""
Comprehensive tests for WalletService covering all documented functionality.

Tests cover:
- Standard cases
- Edge cases
- Error cases
- Outlier cases
"""
from decimal import Decimal
from datetime import datetime, timedelta
from django.test import TestCase, TransactionTestCase
from django.db import transaction as db_transaction

from wallet_utils import WalletService, WalletRepository
from wallet_utils.exceptions import (
    InsufficientBalanceError,
    InvalidPointTypeError,
    InvalidParamsError,
    WalletOperationError,
)
from wallet_utils.transaction_types import (
    WALLET_DEPOSIT,
    WALLET_WITHDRAW,
    ROI_DISTRIBUTION,
    get_transaction_type,
    get_transaction_type_name,
)
from tests.models import TestUser, TestWallet, TestWalletCustomField


class WalletServiceAddPointTests(TestCase):
    """Tests for add_point() method."""
    
    def setUp(self):
        """Set up test fixtures."""
        self.user = TestUser.objects.create(username="testuser", email="test@example.com")
        self.point_types = {
            "credit_balance": 2,
            "reward_points": 0,
            "crypto_balance": 8,
        }
        self.repo = WalletRepository(
            user_model=TestUser,
            point_types=self.point_types,
        )
        self.service = WalletService(self.repo)
    
    def test_add_point_standard(self):
        """Standard case: Add points to user wallet."""
        transaction_id = self.service.add_point(
            user_id=self.user.id,
            point_type="credit_balance",
            amount=Decimal("100.00"),
            remarks="Test deposit",
        )
        
        self.assertIsInstance(transaction_id, int)
        self.assertGreater(transaction_id, 0)
        
        # Check balance updated
        self.user.refresh_from_db()
        self.assertEqual(self.user.credit_balance, Decimal("100.00"))
    
    def test_add_point_with_transaction_type(self):
        """Add points with transaction type constant."""
        transaction_id = self.service.add_point(
            user_id=self.user.id,
            point_type="credit_balance",
            amount=Decimal("50.00"),
            remarks="Deposit",
            trans_type=WALLET_DEPOSIT,
        )
        
        self.assertIsInstance(transaction_id, int)
        
        # Verify transaction record
        transaction = self.service.get_transaction_by_id(transaction_id)
        self.assertIsNotNone(transaction)
        self.assertEqual(transaction.trans_type, WALLET_DEPOSIT)
        self.assertEqual(transaction.type, "c")
    
    def test_add_point_with_iid(self):
        """Add points with initiator ID (user-initiated)."""
        initiator_id = 999
        transaction_id = self.service.add_point(
            user_id=self.user.id,
            point_type="credit_balance",
            amount=Decimal("25.00"),
            remarks="User deposit",
            iid=initiator_id,
        )
        
        transaction = self.service.get_transaction_by_id(transaction_id)
        self.assertEqual(transaction.iid, initiator_id)
    
    def test_add_point_system_initiated(self):
        """Add points with system initiator (default iid=-100)."""
        transaction_id = self.service.add_point(
            user_id=self.user.id,
            point_type="credit_balance",
            amount=Decimal("10.00"),
            remarks="System deposit",
        )
        
        transaction = self.service.get_transaction_by_id(transaction_id)
        self.assertEqual(transaction.iid, -100)
    
    def test_add_point_with_extra_data(self):
        """Add points with extra data in params."""
        transaction_id = self.service.add_point(
            user_id=self.user.id,
            point_type="credit_balance",
            amount=Decimal("100.00"),
            remarks="Deposit with extra data",
            params={
                "data": {
                    "payment_id": 12345,
                    "source": "bank_transfer",
                }
            },
        )
        
        transaction = self.service.get_transaction_by_id(transaction_id)
        self.assertEqual(transaction.extra_data["payment_id"], 12345)
        self.assertEqual(transaction.extra_data["source"], "bank_transfer")
    
    def test_add_point_multiple_point_types(self):
        """Add points to different point types."""
        # Add to credit_balance
        self.service.add_point(
            user_id=self.user.id,
            point_type="credit_balance",
            amount=Decimal("100.00"),
            remarks="Credit deposit",
        )
        
        # Add to reward_points
        self.service.add_point(
            user_id=self.user.id,
            point_type="reward_points",
            amount=Decimal("50"),
            remarks="Reward points",
        )
        
        # Add to crypto_balance
        self.service.add_point(
            user_id=self.user.id,
            point_type="crypto_balance",
            amount=Decimal("0.00000001"),
            remarks="Crypto deposit",
        )
        
        self.user.refresh_from_db()
        self.assertEqual(self.user.credit_balance, Decimal("100.00"))
        self.assertEqual(self.user.reward_points, Decimal("50"))
        self.assertEqual(self.user.crypto_balance, Decimal("0.00000001"))
    
    def test_add_point_decimal_precision(self):
        """Test decimal precision handling."""
        # Test 2 decimal places (credit_balance)
        self.service.add_point(
            user_id=self.user.id,
            point_type="credit_balance",
            amount=Decimal("100.999"),  # Should round to 101.00
            remarks="Test rounding",
        )
        self.user.refresh_from_db()
        self.assertEqual(self.user.credit_balance, Decimal("101.00"))
        
        # Test 0 decimal places (reward_points)
        self.service.add_point(
            user_id=self.user.id,
            point_type="reward_points",
            amount=Decimal("50.7"),  # Should round to 51
            remarks="Test rounding",
        )
        self.user.refresh_from_db()
        self.assertEqual(self.user.reward_points, Decimal("51"))
        
        # Test 8 decimal places (crypto_balance)
        self.service.add_point(
            user_id=self.user.id,
            point_type="crypto_balance",
            amount=Decimal("0.123456789"),  # Should round to 0.12345679
            remarks="Test rounding",
        )
        self.user.refresh_from_db()
        self.assertEqual(self.user.crypto_balance, Decimal("0.12345679"))
    
    def test_add_point_accumulation(self):
        """Test multiple additions accumulate correctly."""
        amounts = [Decimal("10.00"), Decimal("20.00"), Decimal("30.00")]
        
        for amount in amounts:
            self.service.add_point(
                user_id=self.user.id,
                point_type="credit_balance",
                amount=amount,
                remarks=f"Deposit {amount}",
            )
        
        self.user.refresh_from_db()
        self.assertEqual(self.user.credit_balance, Decimal("60.00"))
    
    def test_add_point_zero_amount(self):
        """Edge case: Add zero amount."""
        transaction_id = self.service.add_point(
            user_id=self.user.id,
            point_type="credit_balance",
            amount=Decimal("0.00"),
            remarks="Zero deposit",
        )
        
        self.assertIsInstance(transaction_id, int)
        self.user.refresh_from_db()
        self.assertEqual(self.user.credit_balance, Decimal("0.00"))
    
    def test_add_point_very_small_amount(self):
        """Edge case: Add very small amount."""
        transaction_id = self.service.add_point(
            user_id=self.user.id,
            point_type="crypto_balance",
            amount=Decimal("0.00000001"),
            remarks="Very small deposit",
        )
        
        self.assertIsInstance(transaction_id, int)
        self.user.refresh_from_db()
        self.assertEqual(self.user.crypto_balance, Decimal("0.00000001"))
    
    def test_add_point_very_large_amount(self):
        """Edge case: Add very large amount."""
        # Use a value that fits safely within max_digits=20, decimal_places=2
        # 15 digits total (13 before decimal + 2 after) to avoid any rounding issues
        # Using F() expressions in repository should prevent rounding, but using
        # a conservative value ensures test stability
        large_amount = Decimal("9999999999999.99")
        transaction_id = self.service.add_point(
            user_id=self.user.id,
            point_type="credit_balance",
            amount=large_amount,
            remarks="Large deposit",
        )
        
        self.assertIsInstance(transaction_id, int)
        self.user.refresh_from_db()
        self.assertEqual(self.user.credit_balance, large_amount)
    
    def test_add_point_invalid_point_type(self):
        """Error case: Invalid point type."""
        with self.assertRaises(InvalidPointTypeError) as cm:
            self.service.add_point(
                user_id=self.user.id,
                point_type="invalid_type",
                amount=Decimal("100.00"),
                remarks="Test",
            )
        
        self.assertEqual(cm.exception.point_type, "invalid_type")
    
    def test_add_point_invalid_params_not_dict(self):
        """Error case: params is not a dict."""
        with self.assertRaises(InvalidParamsError) as cm:
            self.service.add_point(
                user_id=self.user.id,
                point_type="credit_balance",
                amount=Decimal("100.00"),
                remarks="Test",
                params="not a dict",
            )
        
        self.assertEqual(cm.exception.method, "add_point")
    
    def test_add_point_invalid_params_data_not_dict(self):
        """Error case: params['data'] is not a dict."""
        with self.assertRaises(InvalidParamsError) as cm:
            self.service.add_point(
                user_id=self.user.id,
                point_type="credit_balance",
                amount=Decimal("100.00"),
                remarks="Test",
                params={"data": "not a dict"},
            )
        
        self.assertEqual(cm.exception.method, "add_point")
    
    def test_add_point_reserved_field_in_extra_data(self):
        """Error case: Reserved field in extra_data."""
        reserved_fields = ["id", "wtype", "iid", "uid", "type", "amount", "balance", "trans_type", "descr", "cdate"]
        
        for field in reserved_fields:
            with self.assertRaises(InvalidParamsError) as cm:
                self.service.add_point(
                    user_id=self.user.id,
                    point_type="credit_balance",
                    amount=Decimal("100.00"),
                    remarks="Test",
                    params={"data": {field: "value"}},
                )
            self.assertEqual(cm.exception.method, "add_point")
    
    def test_add_point_nonexistent_user(self):
        """Edge case: Add points to non-existent user."""
        # This should raise an error when trying to update balance
        with self.assertRaises(WalletOperationError):
            self.service.add_point(
                user_id=99999,
                point_type="credit_balance",
                amount=Decimal("100.00"),
                remarks="Test",
            )


class WalletServiceDeductPointTests(TestCase):
    """Tests for deduct_point() method."""
    
    def setUp(self):
        """Set up test fixtures."""
        self.user = TestUser.objects.create(username="testuser", email="test@example.com")
        self.point_types = {
            "credit_balance": 2,
            "reward_points": 0,
            "crypto_balance": 8,
        }
        self.repo = WalletRepository(
            user_model=TestUser,
            point_types=self.point_types,
        )
        self.service = WalletService(self.repo)
        
        # Pre-fund the wallet
        self.service.add_point(
            user_id=self.user.id,
            point_type="credit_balance",
            amount=Decimal("1000.00"),
            remarks="Initial deposit",
        )
    
    def test_deduct_point_standard(self):
        """Standard case: Deduct points from wallet."""
        transaction_id = self.service.deduct_point(
            user_id=self.user.id,
            point_type="credit_balance",
            amount=Decimal("100.00"),
            remarks="Withdrawal",
        )
        
        self.assertIsInstance(transaction_id, int)
        self.assertGreater(transaction_id, 0)
        
        # Check balance updated
        self.user.refresh_from_db()
        self.assertEqual(self.user.credit_balance, Decimal("900.00"))
    
    def test_deduct_point_with_transaction_type(self):
        """Deduct points with transaction type constant."""
        transaction_id = self.service.deduct_point(
            user_id=self.user.id,
            point_type="credit_balance",
            amount=Decimal("50.00"),
            remarks="Withdrawal",
            trans_type=WALLET_WITHDRAW,
        )
        
        transaction = self.service.get_transaction_by_id(transaction_id)
        self.assertEqual(transaction.trans_type, WALLET_WITHDRAW)
        self.assertEqual(transaction.type, "d")
    
    def test_deduct_point_insufficient_balance(self):
        """Error case: Insufficient balance."""
        with self.assertRaises(InsufficientBalanceError) as cm:
            self.service.deduct_point(
                user_id=self.user.id,
                point_type="credit_balance",
                amount=Decimal("2000.00"),
                remarks="Large withdrawal",
            )
        
        self.assertIsNotNone(cm.exception.available)
        self.assertIsNotNone(cm.exception.requested)
        self.assertEqual(cm.exception.requested, 2000.00)
    
    def test_deduct_point_exact_balance(self):
        """Edge case: Deduct exact balance."""
        self.user.refresh_from_db()
        current_balance = self.user.credit_balance
        
        transaction_id = self.service.deduct_point(
            user_id=self.user.id,
            point_type="credit_balance",
            amount=current_balance,
            remarks="Full withdrawal",
        )
        
        self.assertIsInstance(transaction_id, int)
        self.user.refresh_from_db()
        self.assertEqual(self.user.credit_balance, Decimal("0.00"))
    
    def test_deduct_point_allow_negative(self):
        """Edge case: Allow negative balance."""
        transaction_id = self.service.deduct_point(
            user_id=self.user.id,
            point_type="credit_balance",
            amount=Decimal("2000.00"),
            remarks="Overdraft",
            allow_negative=True,
        )
        
        self.assertIsInstance(transaction_id, int)
        self.user.refresh_from_db()
        self.assertEqual(self.user.credit_balance, Decimal("-1000.00"))
    
    def test_deduct_point_decimal_precision(self):
        """Test decimal precision handling."""
        # Test rounding
        self.service.deduct_point(
            user_id=self.user.id,
            point_type="credit_balance",
            amount=Decimal("100.999"),  # Should round to 101.00
            remarks="Test rounding",
        )
        self.user.refresh_from_db()
        self.assertEqual(self.user.credit_balance, Decimal("899.00"))
    
    def test_deduct_point_zero_amount(self):
        """Edge case: Deduct zero amount."""
        # Refresh user to get current balance from database
        self.user.refresh_from_db()
        initial_balance = self.user.credit_balance
        
        transaction_id = self.service.deduct_point(
            user_id=self.user.id,
            point_type="credit_balance",
            amount=Decimal("0.00"),
            remarks="Zero withdrawal",
        )
        
        self.assertIsInstance(transaction_id, int)
        self.user.refresh_from_db()
        self.assertEqual(self.user.credit_balance, initial_balance)
    
    def test_deduct_point_multiple_deductions(self):
        """Test multiple deductions."""
        amounts = [Decimal("100.00"), Decimal("200.00"), Decimal("300.00")]
        
        for amount in amounts:
            self.service.deduct_point(
                user_id=self.user.id,
                point_type="credit_balance",
                amount=amount,
                remarks=f"Withdrawal {amount}",
            )
        
        self.user.refresh_from_db()
        self.assertEqual(self.user.credit_balance, Decimal("400.00"))
    
    def test_deduct_point_race_condition_prevention(self):
        """Test atomic deduction prevents race conditions."""
        # This test verifies that the SQL WHERE clause prevents race conditions
        # by ensuring balance is checked atomically
        
        # Try to deduct more than available
        with self.assertRaises(InsufficientBalanceError):
            self.service.deduct_point(
                user_id=self.user.id,
                point_type="credit_balance",
                amount=Decimal("2000.00"),
                remarks="Race condition test",
            )
        
        # Balance should remain unchanged
        self.user.refresh_from_db()
        self.assertEqual(self.user.credit_balance, Decimal("1000.00"))


class WalletServiceTransactionHistoryTests(TestCase):
    """Tests for transaction history retrieval methods."""
    
    def setUp(self):
        """Set up test fixtures."""
        self.user1 = TestUser.objects.create(username="user1", email="user1@example.com")
        self.user2 = TestUser.objects.create(username="user2", email="user2@example.com")
        
        self.point_types = {
            "credit_balance": 2,
            "reward_points": 0,
        }
        self.repo = WalletRepository(
            user_model=TestUser,
            point_types=self.point_types,
        )
        self.service = WalletService(self.repo)
        
        # Create various transactions
        self.transaction_ids = []
        
        # User1 transactions
        for i in range(5):
            tid = self.service.add_point(
                user_id=self.user1.id,
                point_type="credit_balance",
                amount=Decimal(f"{10 * (i + 1)}.00"),
                remarks=f"Deposit {i+1}",
                trans_type=WALLET_DEPOSIT,
            )
            self.transaction_ids.append(tid)
        
        # User2 transactions
        for i in range(3):
            tid = self.service.add_point(
                user_id=self.user2.id,
                point_type="credit_balance",
                amount=Decimal(f"{20 * (i + 1)}.00"),
                remarks=f"Deposit {i+1}",
                trans_type=WALLET_DEPOSIT,
            )
            self.transaction_ids.append(tid)
        
        # Reward points transactions
        for i in range(2):
            tid = self.service.add_point(
                user_id=self.user1.id,
                point_type="reward_points",
                amount=Decimal("100"),
                remarks=f"Reward {i+1}",
                trans_type=ROI_DISTRIBUTION,
            )
            self.transaction_ids.append(tid)
    
    def test_get_transaction_history_by_user_id(self):
        """Get transactions filtered by user_id."""
        transactions = self.service.get_transaction_history(user_id=self.user1.id)
        
        self.assertEqual(len(transactions), 7)  # 5 credit_balance + 2 reward_points
        for txn in transactions:
            self.assertEqual(txn.uid, self.user1.id)
    
    def test_get_transaction_history_by_point_type(self):
        """Get transactions filtered by point_type."""
        transactions = self.service.get_transaction_history(
            user_id=self.user1.id,
            point_type="credit_balance",
        )
        
        self.assertEqual(len(transactions), 5)
        for txn in transactions:
            self.assertEqual(txn.wtype, "credit_balance")
    
    def test_get_transaction_history_by_trans_type(self):
        """Get transactions filtered by trans_type."""
        transactions = self.service.get_transaction_history(
            trans_type=WALLET_DEPOSIT,
        )
        
        self.assertEqual(len(transactions), 8)  # 5 + 3 from both users
        for txn in transactions:
            self.assertEqual(txn.trans_type, WALLET_DEPOSIT)
    
    def test_get_transaction_history_by_transaction_type_credit(self):
        """Get transactions filtered by transaction_type='c'."""
        transactions = self.service.get_transaction_history(
            transaction_type="c",
        )
        
        self.assertEqual(len(transactions), 10)  # All are credits
        for txn in transactions:
            self.assertEqual(txn.type, "c")
    
    def test_get_transaction_history_by_transaction_type_debit(self):
        """Get transactions filtered by transaction_type='d'."""
        # Create a debit transaction
        self.service.deduct_point(
            user_id=self.user1.id,
            point_type="credit_balance",
            amount=Decimal("10.00"),
            remarks="Withdrawal",
        )
        
        transactions = self.service.get_transaction_history(
            transaction_type="d",
        )
        
        self.assertEqual(len(transactions), 1)
        self.assertEqual(transactions[0].type, "d")
    
    def test_get_transaction_history_by_iid(self):
        """Get transactions filtered by initiator ID."""
        # Create transaction with specific iid
        initiator_id = 999
        self.service.add_point(
            user_id=self.user1.id,
            point_type="credit_balance",
            amount=Decimal("50.00"),
            remarks="Test",
            iid=initiator_id,
        )
        
        transactions = self.service.get_transaction_history(iid=initiator_id)
        self.assertEqual(len(transactions), 1)
        self.assertEqual(transactions[0].iid, initiator_id)
    
    def test_get_transaction_history_date_range(self):
        """Get transactions filtered by date range."""
        from datetime import datetime, timedelta
        
        # Wait a moment to ensure different timestamps
        import time
        time.sleep(0.1)
        
        # Create a new transaction
        self.service.add_point(
            user_id=self.user1.id,
            point_type="credit_balance",
            amount=Decimal("100.00"),
            remarks="Recent deposit",
        )
        
        # Get transactions from last minute
        end_date = datetime.now()
        start_date = end_date - timedelta(minutes=1)
        
        transactions = self.service.get_transaction_history(
            start_date=start_date,
            end_date=end_date,
        )
        
        # Should include the recent transaction
        self.assertGreaterEqual(len(transactions), 1)
    
    def test_get_transaction_history_date_range_string(self):
        """Get transactions filtered by date range (ISO string format)."""
        from datetime import datetime, timedelta
        
        end_date = datetime.now()
        start_date = end_date - timedelta(days=1)
        
        transactions = self.service.get_transaction_history(
            start_date=start_date.isoformat(),
            end_date=end_date.isoformat(),
        )
        
        # Should include all transactions
        self.assertGreaterEqual(len(transactions), 10)
    
    def test_get_transaction_history_pagination_limit(self):
        """Get transactions with limit."""
        transactions = self.service.get_transaction_history(
            user_id=self.user1.id,
            limit=3,
        )
        
        self.assertEqual(len(transactions), 3)
    
    def test_get_transaction_history_pagination_offset(self):
        """Get transactions with offset."""
        all_transactions = self.service.get_transaction_history(
            user_id=self.user1.id,
        )
        
        offset_transactions = self.service.get_transaction_history(
            user_id=self.user1.id,
            offset=2,
        )
        
        self.assertEqual(len(offset_transactions), len(all_transactions) - 2)
        # Check that offset transactions are different
        self.assertNotEqual(all_transactions[0].id, offset_transactions[0].id)
    
    def test_get_transaction_history_ordering(self):
        """Test that transactions are ordered by creation date (newest first)."""
        transactions = self.service.get_transaction_history(
            user_id=self.user1.id,
        )
        
        # Check ordering (newest first)
        for i in range(len(transactions) - 1):
            # Parse dates
            date1 = datetime.fromisoformat(transactions[i].cdate)
            date2 = datetime.fromisoformat(transactions[i + 1].cdate)
            self.assertGreaterEqual(date1, date2)
    
    def test_get_transaction_history_combined_filters(self):
        """Get transactions with multiple filters."""
        transactions = self.service.get_transaction_history(
            user_id=self.user1.id,
            point_type="credit_balance",
            transaction_type="c",
            trans_type=WALLET_DEPOSIT,
        )
        
        self.assertEqual(len(transactions), 5)
        for txn in transactions:
            self.assertEqual(txn.uid, self.user1.id)
            self.assertEqual(txn.wtype, "credit_balance")
            self.assertEqual(txn.type, "c")
            self.assertEqual(txn.trans_type, WALLET_DEPOSIT)
    
    def test_get_transaction_by_id(self):
        """Get a single transaction by ID."""
        transaction_id = self.transaction_ids[0]
        transaction = self.service.get_transaction_by_id(transaction_id)
        
        self.assertIsNotNone(transaction)
        self.assertEqual(transaction.id, transaction_id)
        self.assertEqual(transaction.uid, self.user1.id)
    
    def test_get_transaction_by_id_not_found(self):
        """Get transaction by non-existent ID."""
        transaction = self.service.get_transaction_by_id(99999)
        self.assertIsNone(transaction)
    
    def test_count_transactions(self):
        """Count transactions matching filters."""
        count = self.service.count_transactions(user_id=self.user1.id)
        self.assertEqual(count, 7)
        
        count = self.service.count_transactions(
            user_id=self.user1.id,
            point_type="credit_balance",
        )
        self.assertEqual(count, 5)
        
        count = self.service.count_transactions(trans_type=WALLET_DEPOSIT)
        self.assertEqual(count, 8)
    
    def test_get_transaction_history_invalid_point_type(self):
        """Error case: Invalid point type in filter."""
        with self.assertRaises(InvalidPointTypeError):
            self.service.get_transaction_history(
                point_type="invalid_type",
            )
    
    def test_get_transaction_history_invalid_transaction_type(self):
        """Error case: Invalid transaction_type value."""
        with self.assertRaises(InvalidParamsError):
            self.service.get_transaction_history(
                transaction_type="x",  # Invalid, must be 'c' or 'd'
            )


class WalletServiceSeparateWalletModelTests(TestCase):
    """Tests for using separate wallet balance model."""
    
    def setUp(self):
        """Set up test fixtures."""
        self.user = TestUser.objects.create(username="testuser", email="test@example.com")
        self.wallet = TestWallet.objects.create(
            user_id=self.user.id,
            credit_balance=Decimal("500.00"),
        )
        
        self.point_types = {
            "credit_balance": 2,
            "reward_points": 0,
        }
        self.repo = WalletRepository(
            user_model=TestUser,
            wallet_balance_model=TestWallet,
            point_types=self.point_types,
        )
        self.service = WalletService(self.repo)
    
    def test_add_point_separate_wallet_model(self):
        """Add points using separate wallet model."""
        transaction_id = self.service.add_point(
            user_id=self.user.id,
            point_type="credit_balance",
            amount=Decimal("100.00"),
            remarks="Deposit",
        )
        
        self.assertIsInstance(transaction_id, int)
        
        # Check balance updated in wallet model, not user model
        self.wallet.refresh_from_db()
        self.user.refresh_from_db()
        self.assertEqual(self.wallet.credit_balance, Decimal("600.00"))
        # User model balance should remain unchanged
        self.assertEqual(self.user.credit_balance, Decimal("0.00"))
    
    def test_deduct_point_separate_wallet_model(self):
        """Deduct points using separate wallet model."""
        transaction_id = self.service.deduct_point(
            user_id=self.user.id,
            point_type="credit_balance",
            amount=Decimal("100.00"),
            remarks="Withdrawal",
        )
        
        self.assertIsInstance(transaction_id, int)
        
        self.wallet.refresh_from_db()
        self.assertEqual(self.wallet.credit_balance, Decimal("400.00"))
    
    def test_get_user_balance_separate_wallet_model(self):
        """Get balance from separate wallet model."""
        balance = self.service.repository.get_user_balance(
            self.user.id,
            "credit_balance",
        )
        
        self.assertEqual(balance, Decimal("500.00"))


class WalletServiceCustomFieldNameTests(TestCase):
    """Tests for custom field name mapping."""
    
    def setUp(self):
        """Set up test fixtures."""
        self.user = TestUser.objects.create(username="testuser", email="test@example.com")
        
        self.point_types = {
            "reward_points": 0,
        }
        self.repo = WalletRepository(
            user_model=TestUser,
            point_types=self.point_types,
            point_type_field_map={
                "reward_points": "points",  # Map to custom field name
            },
        )
        self.service = WalletService(self.repo)
    
    def test_add_point_custom_field_name(self):
        """Add points using custom field name."""
        transaction_id = self.service.add_point(
            user_id=self.user.id,
            point_type="reward_points",
            amount=Decimal("100"),
            remarks="Reward",
        )
        
        self.assertIsInstance(transaction_id, int)
        
        # Check balance updated in custom field
        self.user.refresh_from_db()
        self.assertEqual(self.user.points, Decimal("100"))
        self.assertEqual(self.user.reward_points, Decimal("0"))  # Original field unchanged


class WalletServiceCustomWalletUserFieldTests(TestCase):
    """Tests for custom wallet user_id field name."""
    
    def setUp(self):
        """Set up test fixtures."""
        self.user = TestUser.objects.create(username="testuser", email="test@example.com")
        self.wallet = TestWalletCustomField.objects.create(
            owner_id=self.user.id,
            credit_balance=Decimal("500.00"),
        )
        
        self.point_types = {
            "credit_balance": 2,
        }
        self.repo = WalletRepository(
            user_model=TestUser,
            wallet_balance_model=TestWalletCustomField,
            point_types=self.point_types,
            wallet_user_id_field="owner_id",  # Custom field name
        )
        self.service = WalletService(self.repo)
    
    def test_add_point_custom_user_id_field(self):
        """Add points using custom user_id field name."""
        transaction_id = self.service.add_point(
            user_id=self.user.id,
            point_type="credit_balance",
            amount=Decimal("100.00"),
            remarks="Deposit",
        )
        
        self.assertIsInstance(transaction_id, int)
        
        self.wallet.refresh_from_db()
        self.assertEqual(self.wallet.credit_balance, Decimal("600.00"))


class WalletServiceTransactionTypesTests(TestCase):
    """Tests for transaction type constants and helpers."""
    
    def setUp(self):
        """Set up test fixtures."""
        self.user = TestUser.objects.create(username="testuser", email="test@example.com")
        self.point_types = {"credit_balance": 2}
        self.repo = WalletRepository(
            user_model=TestUser,
            point_types=self.point_types,
        )
        self.service = WalletService(self.repo)
    
    def test_transaction_type_constants(self):
        """Test that transaction type constants are integers."""
        from wallet_utils.transaction_types import (
            WALLET_DEPOSIT,
            WALLET_WITHDRAW,
            ROI_DISTRIBUTION,
        )
        
        self.assertIsInstance(WALLET_DEPOSIT, int)
        self.assertIsInstance(WALLET_WITHDRAW, int)
        self.assertIsInstance(ROI_DISTRIBUTION, int)
        
        self.assertEqual(WALLET_DEPOSIT, 1000)
        self.assertEqual(WALLET_WITHDRAW, 1003)
        self.assertEqual(ROI_DISTRIBUTION, 3100)
    
    def test_get_transaction_type(self):
        """Test get_transaction_type() helper function."""
        self.assertEqual(get_transaction_type("wallet-deposit"), 1000)
        self.assertEqual(get_transaction_type("wallet-withdraw"), 1003)
        self.assertEqual(get_transaction_type("roi-distribution"), 3100)
        
        with self.assertRaises(ValueError):
            get_transaction_type("invalid-type")
    
    def test_get_transaction_type_name(self):
        """Test get_transaction_type_name() helper function."""
        self.assertEqual(get_transaction_type_name(1000), "wallet-deposit")
        self.assertEqual(get_transaction_type_name(1003), "wallet-withdraw")
        self.assertEqual(get_transaction_type_name(3100), "roi-distribution")
        self.assertIsNone(get_transaction_type_name(99999))
    
    def test_transaction_type_in_transaction_record(self):
        """Test transaction type is stored correctly."""
        transaction_id = self.service.add_point(
            user_id=self.user.id,
            point_type="credit_balance",
            amount=Decimal("100.00"),
            remarks="Test",
            trans_type=WALLET_DEPOSIT,
        )
        
        transaction = self.service.get_transaction_by_id(transaction_id)
        self.assertEqual(transaction.trans_type, WALLET_DEPOSIT)
        
        # Verify name lookup
        name = get_transaction_type_name(transaction.trans_type)
        self.assertEqual(name, "wallet-deposit")


class WalletServiceEdgeCasesTests(TestCase):
    """Tests for edge cases and outliers."""
    
    def setUp(self):
        """Set up test fixtures."""
        self.user = TestUser.objects.create(username="testuser", email="test@example.com")
        self.point_types = {
            "credit_balance": 2,
            "reward_points": 0,
            "crypto_balance": 8,
        }
        self.repo = WalletRepository(
            user_model=TestUser,
            point_types=self.point_types,
        )
        self.service = WalletService(self.repo)
    
    def test_system_user_id(self):
        """Test using system user ID (-100) for uid."""
        # Create transaction with system user ID
        # Note: uid is the wallet owner, iid is the initiator
        transaction_id = self.service.add_point(
            user_id=-100,  # System user as wallet owner
            point_type="credit_balance",
            amount=Decimal("100.00"),
            remarks="System wallet",
            iid=-100,  # System as initiator
        )
        
        transaction = self.service.get_transaction_by_id(transaction_id)
        self.assertEqual(transaction.uid, -100)
        self.assertEqual(transaction.iid, -100)
    
    def test_very_long_remarks(self):
        """Test with very long remarks."""
        long_remarks = "A" * 10000
        transaction_id = self.service.add_point(
            user_id=self.user.id,
            point_type="credit_balance",
            amount=Decimal("100.00"),
            remarks=long_remarks,
        )
        
        transaction = self.service.get_transaction_by_id(transaction_id)
        self.assertEqual(transaction.descr, long_remarks)
    
    def test_empty_remarks(self):
        """Test with empty remarks."""
        transaction_id = self.service.add_point(
            user_id=self.user.id,
            point_type="credit_balance",
            amount=Decimal("100.00"),
            remarks="",
        )
        
        transaction = self.service.get_transaction_by_id(transaction_id)
        self.assertEqual(transaction.descr, "")
    
    def test_none_transaction_type(self):
        """Test with None transaction type."""
        transaction_id = self.service.add_point(
            user_id=self.user.id,
            point_type="credit_balance",
            amount=Decimal("100.00"),
            remarks="Test",
            trans_type=None,
        )
        
        transaction = self.service.get_transaction_by_id(transaction_id)
        self.assertIsNone(transaction.trans_type)
    
    def test_empty_extra_data(self):
        """Test with empty extra_data."""
        transaction_id = self.service.add_point(
            user_id=self.user.id,
            point_type="credit_balance",
            amount=Decimal("100.00"),
            remarks="Test",
            params={"data": {}},
        )
        
        transaction = self.service.get_transaction_by_id(transaction_id)
        self.assertEqual(transaction.extra_data, {})
    
    def test_no_params(self):
        """Test without params parameter."""
        transaction_id = self.service.add_point(
            user_id=self.user.id,
            point_type="credit_balance",
            amount=Decimal("100.00"),
            remarks="Test",
        )
        
        transaction = self.service.get_transaction_by_id(transaction_id)
        self.assertEqual(transaction.extra_data, {})
    
    def test_multiple_concurrent_operations(self):
        """Test multiple concurrent operations."""
        # Add multiple points
        for i in range(10):
            self.service.add_point(
                user_id=self.user.id,
                point_type="credit_balance",
                amount=Decimal("10.00"),
                remarks=f"Deposit {i}",
            )
        
        self.user.refresh_from_db()
        self.assertEqual(self.user.credit_balance, Decimal("100.00"))
        
        # Deduct multiple points
        for i in range(5):
            self.service.deduct_point(
                user_id=self.user.id,
                point_type="credit_balance",
                amount=Decimal("10.00"),
                remarks=f"Withdrawal {i}",
            )
        
        self.user.refresh_from_db()
        self.assertEqual(self.user.credit_balance, Decimal("50.00"))
    
    def test_balance_accuracy_after_multiple_operations(self):
        """Test balance accuracy after many operations."""
        # Perform many small operations
        for i in range(100):
            self.service.add_point(
                user_id=self.user.id,
                point_type="credit_balance",
                amount=Decimal("0.01"),
                remarks=f"Micro deposit {i}",
            )
        
        self.user.refresh_from_db()
        self.assertEqual(self.user.credit_balance, Decimal("1.00"))
        
        # Deduct in small increments
        for i in range(50):
            self.service.deduct_point(
                user_id=self.user.id,
                point_type="credit_balance",
                amount=Decimal("0.01"),
                remarks=f"Micro withdrawal {i}",
            )
        
        self.user.refresh_from_db()
        self.assertEqual(self.user.credit_balance, Decimal("0.50"))


class WalletServiceRaceConditionTests(TransactionTestCase):
    """Tests for race condition prevention (requires TransactionTestCase)."""
    
    def setUp(self):
        """Set up test fixtures."""
        self.user = TestUser.objects.create(username="testuser", email="test@example.com")
        self.point_types = {"credit_balance": 2}
        self.repo = WalletRepository(
            user_model=TestUser,
            point_types=self.point_types,
        )
        self.service = WalletService(self.repo)
        
        # Pre-fund
        self.service.add_point(
            user_id=self.user.id,
            point_type="credit_balance",
            amount=Decimal("1000.00"),
            remarks="Initial deposit",
        )
    
    def test_atomic_deduction_prevents_race_condition(self):
        """Test that atomic deduction prevents race conditions."""
        # Simulate concurrent deduction attempts
        # Both try to deduct 600, but only 1000 available
        
        def deduct_600():
            try:
                return self.service.deduct_point(
                    user_id=self.user.id,
                    point_type="credit_balance",
                    amount=Decimal("600.00"),
                    remarks="Concurrent withdrawal",
                )
            except InsufficientBalanceError:
                return None
        
        # In a real race condition scenario, only one should succeed
        # The atomic SQL WHERE clause ensures this
        result1 = deduct_600()
        result2 = deduct_600()
        
        # At least one should succeed, but not both
        success_count = sum(1 for r in [result1, result2] if r is not None)
        self.assertLessEqual(success_count, 1)
        
        # Check final balance
        self.user.refresh_from_db()
        if success_count == 1:
            # One succeeded: balance should be 400
            self.assertEqual(self.user.credit_balance, Decimal("400.00"))
        else:
            # Both failed: balance should remain 1000
            self.assertEqual(self.user.credit_balance, Decimal("1000.00"))
