"""Tests for point transfer feature."""

from decimal import Decimal
from unittest.mock import Mock, patch

from django.test import TestCase

from wallet_utils import WalletService
from wallet_utils.exceptions import (
    InsufficientBalanceError,
    InvalidParamsError,
    UserNotFoundError,
)
from wallet_utils.signals import transfer_completed
from wallet_utils.transaction_types import WALLET_TRANSFER

from .models import TestUser


class TransferPointsTests(TestCase):
    """Test cases for the transfer_points method."""

    def setUp(self):
        """Set up test data."""
        from wallet_utils import WalletRepository

        # Create test users
        self.user1 = TestUser.objects.create(username="user1", email="user1@test.com")
        self.user2 = TestUser.objects.create(username="user2", email="user2@test.com")

        # Set up repository and service
        point_types = {
            "cash_balance": 2,
            "credit_balance": 2,
            "reward_points": 0,
        }
        self.repo = WalletRepository(user_model=TestUser, point_types=point_types)
        self.service = WalletService(self.repo)

        # Give user1 some initial balance
        self.service.add_point(
            user_id=self.user1.id,
            point_type="cash_balance",
            amount=Decimal("1000.00"),
            remarks="Initial balance",
        )

    def test_transfer_same_point_type(self):
        """Test transferring points of the same type between users."""
        # Arrange
        transfer_amount = Decimal("100.00")

        # Act
        result = self.service.transfer_points(
            from_user_id=self.user1.id,
            to_user_id=self.user2.id,
            from_point_type="cash_balance",
            to_point_type="cash_balance",
            amount=transfer_amount,
            remarks="Transfer test",
        )

        # Assert
        self.assertIn("transfer_id", result)
        self.assertIn("from_transaction_id", result)
        self.assertIn("to_transaction_id", result)
        self.assertEqual(result["from_user_id"], self.user1.id)
        self.assertEqual(result["to_user_id"], self.user2.id)
        self.assertEqual(result["amount"], transfer_amount)

        # Check balances
        user1_balance = self.repo.get_user_balance(self.user1.id, "cash_balance")
        user2_balance = self.repo.get_user_balance(self.user2.id, "cash_balance")
        self.assertEqual(user1_balance, Decimal("900.00"))
        self.assertEqual(user2_balance, Decimal("100.00"))

    def test_transfer_cross_type(self):
        """Test transferring points across different point types."""
        # Arrange
        transfer_amount = Decimal("50.00")

        # Act
        result = self.service.transfer_points(
            from_user_id=self.user1.id,
            to_user_id=self.user2.id,
            from_point_type="cash_balance",
            to_point_type="credit_balance",
            amount=transfer_amount,
            remarks="Cash to credit transfer",
        )

        # Assert
        self.assertEqual(result["from_point_type"], "cash_balance")
        self.assertEqual(result["to_point_type"], "credit_balance")

        # Check balances
        user1_cash = self.repo.get_user_balance(self.user1.id, "cash_balance")
        user2_credit = self.repo.get_user_balance(self.user2.id, "credit_balance")
        self.assertEqual(user1_cash, Decimal("950.00"))
        self.assertEqual(user2_credit, Decimal("50.00"))

    def test_transfer_self(self):
        """Test self-transfer (same user, different point types)."""
        # Arrange
        transfer_amount = Decimal("200.00")

        # Act
        result = self.service.transfer_points(
            from_user_id=self.user1.id,
            to_user_id=self.user1.id,
            from_point_type="cash_balance",
            to_point_type="credit_balance",
            amount=transfer_amount,
            remarks="Convert cash to credit",
        )

        # Assert
        self.assertEqual(result["from_user_id"], result["to_user_id"])

        # Check balances
        cash = self.repo.get_user_balance(self.user1.id, "cash_balance")
        credit = self.repo.get_user_balance(self.user1.id, "credit_balance")
        self.assertEqual(cash, Decimal("800.00"))
        self.assertEqual(credit, Decimal("200.00"))

    def test_transfer_insufficient_balance(self):
        """Test transfer with insufficient balance."""
        # Arrange
        transfer_amount = Decimal("2000.00")  # More than user1 has

        # Act & Assert
        with self.assertRaises(InsufficientBalanceError) as ctx:
            self.service.transfer_points(
                from_user_id=self.user1.id,
                to_user_id=self.user2.id,
                from_point_type="cash_balance",
                to_point_type="cash_balance",
                amount=transfer_amount,
                remarks="Over budget",
            )

        # Verify error details
        self.assertIsNotNone(ctx.exception.available)
        self.assertIsNotNone(ctx.exception.requested)

    def test_transfer_nonexistent_recipient(self):
        """Test transfer to a user that doesn't exist."""
        # Arrange
        nonexistent_user_id = 99999

        # Act & Assert
        with self.assertRaises(UserNotFoundError) as ctx:
            self.service.transfer_points(
                from_user_id=self.user1.id,
                to_user_id=nonexistent_user_id,
                from_point_type="cash_balance",
                to_point_type="cash_balance",
                amount=Decimal("100.00"),
                remarks="To ghost",
            )

        # Verify error details
        self.assertEqual(ctx.exception.user_id, nonexistent_user_id)

    def test_transfer_negative_amount(self):
        """Test transfer with negative amount."""
        # Act & Assert
        with self.assertRaises(InvalidParamsError) as ctx:
            self.service.transfer_points(
                from_user_id=self.user1.id,
                to_user_id=self.user2.id,
                from_point_type="cash_balance",
                to_point_type="cash_balance",
                amount=Decimal("-50.00"),
                remarks="Negative test",
            )

        self.assertEqual(ctx.exception.method, "transfer_points")

    def test_transfer_zero_amount(self):
        """Test transfer with zero amount."""
        # Act & Assert
        with self.assertRaises(InvalidParamsError) as ctx:
            self.service.transfer_points(
                from_user_id=self.user1.id,
                to_user_id=self.user2.id,
                from_point_type="cash_balance",
                to_point_type="cash_balance",
                amount=Decimal("0.00"),
                remarks="Zero test",
            )

        self.assertEqual(ctx.exception.method, "transfer_points")

    def test_transfer_with_metadata(self):
        """Test transfer with custom metadata."""
        # Arrange
        metadata = {"order_id": "ORD-123", "campaign": "summer_promo"}

        # Act
        result = self.service.transfer_points(
            from_user_id=self.user1.id,
            to_user_id=self.user2.id,
            from_point_type="cash_balance",
            to_point_type="cash_balance",
            amount=Decimal("75.00"),
            remarks="Promo transfer",
            metadata=metadata,
        )

        # Assert - fetch transaction records and check metadata
        from_record = self.service.get_transaction_by_id(result["from_transaction_id"])
        to_record = self.service.get_transaction_by_id(result["to_transaction_id"])

        self.assertIn("order_id", from_record.extra_data)
        self.assertEqual(from_record.extra_data["order_id"], "ORD-123")
        self.assertIn("transfer_to", from_record.extra_data)
        self.assertEqual(from_record.extra_data["transfer_to"], self.user2.id)

        self.assertIn("campaign", to_record.extra_data)
        self.assertEqual(to_record.extra_data["campaign"], "summer_promo")
        self.assertIn("transfer_from", to_record.extra_data)
        self.assertEqual(to_record.extra_data["transfer_from"], self.user1.id)

    def test_transfer_atomicity(self):
        """Test that transfer is atomic (both succeed or both fail)."""
        # This test simulates a failure during the add operation
        # to ensure the deduct is rolled back

        # Arrange
        original_add_point = self.service.add_point

        def failing_add_point(*args, **kwargs):
            raise Exception("Simulated failure")

        # Act & Assert
        with patch.object(self.service, "add_point", side_effect=failing_add_point):
            with self.assertRaises(Exception):
                self.service.transfer_points(
                    from_user_id=self.user1.id,
                    to_user_id=self.user2.id,
                    from_point_type="cash_balance",
                    to_point_type="cash_balance",
                    amount=Decimal("100.00"),
                    remarks="Should fail",
                )

        # Verify user1's balance is unchanged (rollback worked)
        balance = self.repo.get_user_balance(self.user1.id, "cash_balance")
        self.assertEqual(balance, Decimal("1000.00"))

    def test_transfer_signal_sent(self):
        """Test that transfer_completed signal is sent after successful transfer."""
        # Arrange
        signal_received = []

        def signal_handler(sender, **kwargs):
            signal_received.append(kwargs)

        transfer_completed.connect(signal_handler)

        try:
            # Act
            result = self.service.transfer_points(
                from_user_id=self.user1.id,
                to_user_id=self.user2.id,
                from_point_type="cash_balance",
                to_point_type="cash_balance",
                amount=Decimal("100.00"),
                remarks="Signal test",
            )

            # Assert
            self.assertEqual(len(signal_received), 1)
            signal_data = signal_received[0]
            self.assertEqual(signal_data["transfer_id"], result["transfer_id"])
            self.assertEqual(signal_data["from_user_id"], self.user1.id)
            self.assertEqual(signal_data["to_user_id"], self.user2.id)
            self.assertEqual(signal_data["amount"], Decimal("100.00"))
            self.assertIsNotNone(signal_data["from_record"])
            self.assertIsNotNone(signal_data["to_record"])
        finally:
            transfer_completed.disconnect(signal_handler)

    def test_transfer_custom_trans_type(self):
        """Test transfer with custom transaction type."""
        # Arrange
        from wallet_utils.transaction_types import SYSTEM_REWARD

        # Act
        result = self.service.transfer_points(
            from_user_id=self.user1.id,
            to_user_id=self.user2.id,
            from_point_type="cash_balance",
            to_point_type="cash_balance",
            amount=Decimal("50.00"),
            remarks="Custom type test",
            trans_type=SYSTEM_REWARD,
        )

        # Assert
        from_record = self.service.get_transaction_by_id(result["from_transaction_id"])
        to_record = self.service.get_transaction_by_id(result["to_transaction_id"])
        self.assertEqual(from_record.trans_type, SYSTEM_REWARD)
        self.assertEqual(to_record.trans_type, SYSTEM_REWARD)

    def test_transfer_with_custom_iid(self):
        """Test transfer with custom initiator ID."""
        # Arrange
        admin_user_id = 99

        # Act
        result = self.service.transfer_points(
            from_user_id=self.user1.id,
            to_user_id=self.user2.id,
            from_point_type="cash_balance",
            to_point_type="cash_balance",
            amount=Decimal("25.00"),
            remarks="Admin transfer",
            iid=admin_user_id,
        )

        # Assert
        from_record = self.service.get_transaction_by_id(result["from_transaction_id"])
        to_record = self.service.get_transaction_by_id(result["to_transaction_id"])
        self.assertEqual(from_record.iid, admin_user_id)
        self.assertEqual(to_record.iid, admin_user_id)

    def test_transfer_return_values(self):
        """Test that transfer returns all expected values."""
        # Act
        result = self.service.transfer_points(
            from_user_id=self.user1.id,
            to_user_id=self.user2.id,
            from_point_type="cash_balance",
            to_point_type="cash_balance",
            amount=Decimal("100.00"),
            remarks="Complete test",
        )

        # Assert all expected keys are present
        expected_keys = {
            "success",  # Added with hook system integration
            "transfer_id",
            "from_transaction_id",
            "to_transaction_id",
            "from_user_id",
            "to_user_id",
            "from_point_type",
            "to_point_type",
            "amount",
            "from_balance",
            "to_balance",
        }
        self.assertEqual(set(result.keys()), expected_keys)

        # Verify transfer_id format
        self.assertIn(":", result["transfer_id"])
        expected_id = f"{result['from_transaction_id']}:{result['to_transaction_id']}"
        self.assertEqual(result["transfer_id"], expected_id)

    def test_transfer_decimal_precision(self):
        """Test that transfer respects decimal precision for point types."""
        # Arrange - reward_points has 0 decimal places
        self.service.add_point(
            user_id=self.user1.id,
            point_type="reward_points",
            amount=Decimal("100"),
            remarks="Initial points",
        )

        # Act
        result = self.service.transfer_points(
            from_user_id=self.user1.id,
            to_user_id=self.user2.id,
            from_point_type="reward_points",
            to_point_type="reward_points",
            amount=Decimal("25.999"),  # Should round to 26
            remarks="Rounding test",
        )

        # Assert
        user1_points = self.repo.get_user_balance(self.user1.id, "reward_points")
        user2_points = self.repo.get_user_balance(self.user2.id, "reward_points")
        # 100 - 26 = 74
        self.assertEqual(user1_points, Decimal("74"))
        self.assertEqual(user2_points, Decimal("26"))
