"""
Integration tests for double-spend protection.

These tests use real database transactions to verify the protection works
in a realistic environment with actual SQLAlchemy sessions.
"""

import json
import time
import uuid
from decimal import Decimal
from unittest.mock import MagicMock, patch

import transaction
from pyramid import testing

from ..models import (
    Shop,
    Invoice,
    Product,
    InvoiceLineItem,
    CryptoPayment,
    CryptoProcessor,
)
from ..models.price import Price
from ..models.user import get_or_create_user_by_email
from ..lib.crypto_watcher import (
    process_payment,
    scan_wallet_for_double_or_late_payments,
    run_once,
)
from .test_integration import DatabaseIntegrationTests


class TestDoubleSpendIntegration(DatabaseIntegrationTests):
    """Integration tests with real database for double-spend scenarios."""

    def _create_payment_scenario(
        self, status=CryptoPayment.STATUS_PENDING, received=0, expected=100000000
    ):
        """Helper to create a complete payment scenario with all models."""
        # Create user and shop
        user = get_or_create_user_by_email(
            self.dbsession, f"test-{uuid.uuid4()}@example.com"
        )
        self.dbsession.add(user)

        shop = Shop(
            name=f"Test Shop {uuid.uuid4()}",
            phone_number="555-555-5555",
            billing_address="123 Test St",
            description="A test shop for double spend integration tests",
        )
        self.dbsession.add(shop)
        self.dbsession.flush()

        # Create product and invoice
        product = Product(title="Test Product", description="Test")
        product.shop = shop
        product.price_in_cents = 100
        self.dbsession.add(product)
        self.dbsession.flush()

        # Create price for the product
        price = Price(product, 100)  # 100 cents = $1.00
        self.dbsession.add(price)

        invoice = Invoice(user=user)
        self.dbsession.add(invoice)
        self.dbsession.flush()

        line_item = InvoiceLineItem(
            invoice=invoice,
            product=product,
            quantity=1,
        )
        self.dbsession.add(line_item)

        # Create crypto payment
        payment = CryptoPayment(
            invoice=invoice,
            user=user,
            shop=shop,
            coin_type="XMR",
            address="4TestPaymentAddress...",
            account_index=0,
            subaddress_index=1,
            expected_amount=expected,
            confirmations_required=10,
            rate_locked_usd_per_coin=Decimal("150.00"),
            quote_expires_at_ms=int((time.time() + 900) * 1000),
            refund_address="4RefundAddress...",
            shop_sweep_to_address="4ShopSweepAddress...",
        )
        payment.status = status
        payment.received_amount = received
        payment.tx_hashes = json.dumps([])

        self.dbsession.add(payment)
        self.dbsession.flush()

        return payment, invoice, shop, user

    def test_double_payment_creates_separate_refund_entry(self):
        """Test that duplicate payment creates a separate database entry."""
        # Create confirmed payment
        payment, invoice, shop, user = self._create_payment_scenario(
            status=CryptoPayment.STATUS_CONFIRMED, received=100000000
        )
        payment.tx_hashes = json.dumps(["first-tx"])
        # Make sure subaddress_index is set correctly
        payment.subaddress_index = 1  # Must match the transfer below
        self.dbsession.add(payment)
        self.dbsession.flush()

        original_id = payment.id

        # Mock wallet RPC to return duplicate transaction
        with patch(
            "make_post_sell.lib.crypto_watcher.get_crypto_client"
        ) as mock_client:
            mock_rpc = MagicMock()
            mock_client.return_value = mock_rpc

            # Simulate wallet scan finding BOTH the original AND duplicate payment
            mock_rpc._call.return_value = {
                "in": [
                    {
                        "txid": "first-tx",  # Original transaction
                        "amount": 100000000,
                        "confirmations": 10,
                        "subaddr_index": {"major": 0, "minor": 1},
                        "height": 1999900,
                    },
                    {
                        "txid": "duplicate-tx",  # Duplicate transaction
                        "amount": 50000000,
                        "confirmations": 5,
                        "subaddr_index": {"major": 0, "minor": 1},  # Same address!
                        "height": 2000000,
                    },
                ]
            }

            # Create crypto processor for scan
            processor = CryptoProcessor(
                shop_id=shop.id,
                coin_type="XMR",
                sweep_to_address="4SweepAddress...",
            )
            processor.enabled = True
            processor.last_scan_semaphore = "xmr:1999999"
            processor.wallet_label = "0"  # For XMR, wallet_label is the account index
            self.dbsession.add(processor)
            self.dbsession.flush()

            # Run the scanner
            request = MagicMock()
            request.dbsession = self.dbsession
            request.registry.settings = {}
            request.registry.settings = self.settings

            scan_wallet_for_double_or_late_payments(request, self.settings)

        # Verify duplicate payment was created
        all_payments = (
            self.dbsession.query(CryptoPayment)
            .filter(CryptoPayment.subaddress_index == 1)
            .all()
        )

        # Scanner now correctly skips already-processed transactions
        # So it only creates a duplicate for the new "duplicate-tx", not for "first-tx"
        self.assertEqual(len(all_payments), 2)  # Original + 1 duplicate

        # Find the duplicate (not the original)
        duplicates = [p for p in all_payments if p.id != original_id]
        self.assertEqual(len(duplicates), 1)

        # The duplicate should be marked for refund
        duplicate = duplicates[0]
        self.assertEqual(duplicate.status, CryptoPayment.STATUS_DOUBLEPAY_REFUNDED)
        self.assertEqual(duplicate.invoice_id, payment.invoice_id)  # Same invoice
        self.assertEqual(duplicate.refund_address, payment.refund_address)

        # Verify it's the duplicate-tx payment
        self.assertEqual(json.loads(duplicate.tx_hashes)[0], "duplicate-tx")
        self.assertEqual(duplicate.received_amount, 50000000)
        self.assertEqual(duplicate.expected_amount, 50000000)

    def test_main_loop_skips_confirmed_duplicate_transactions(self):
        """Test that main processing loop doesn't process duplicates."""
        # Create payment that just got confirmed
        payment, invoice, shop, user = self._create_payment_scenario(
            status=CryptoPayment.STATUS_CONFIRMED, received=100000000
        )
        payment.tx_hashes = json.dumps(["confirmed-tx"])
        payment.current_confirmations = 10
        self.dbsession.add(payment)
        self.dbsession.flush()

        # Mock incoming duplicate transaction
        with patch(
            "make_post_sell.lib.crypto_watcher.get_crypto_client"
        ) as mock_client:
            mock_rpc = MagicMock()
            mock_client.return_value = mock_rpc

            # Mock wallet returns both original and duplicate
            mock_rpc.get_transfers_for_subaddr.return_value = {
                "in": [
                    {"txid": "confirmed-tx", "amount": 100000000, "confirmations": 15},
                    {
                        "txid": "duplicate-tx",  # NEW!
                        "amount": 75000000,
                        "confirmations": 2,
                    },
                ]
            }

            request = testing.DummyRequest()
            request.dbsession = self.dbsession
            request.registry.settings = {}
            request.registry.settings = self.settings

            # Process payment through main loop
            with patch("make_post_sell.lib.crypto_watcher.log") as mock_log:
                process_payment(
                    request,
                    payment,
                    mock_rpc.get_transfers_for_subaddr.return_value["in"],
                    mock_rpc,
                )

                # Should log duplicate warning and creation of separate records
                error_calls = mock_log.payment_error.call_args_list
                duplicate_warnings = [
                    call
                    for call in error_calls
                    if "DUPLICATE PAYMENT DETECTED" in str(call[0][1])
                ]
                self.assertGreater(len(duplicate_warnings), 0)

                # Should also log creation of duplicate payment records
                info_calls = mock_log.payment_info.call_args_list
                creation_logs = [
                    call
                    for call in info_calls
                    if "Created duplicate payment" in str(call[0][1])
                ]
                self.assertGreater(len(creation_logs), 0)

            # Verify original payment amount didn't change
            self.dbsession.refresh(payment)
            self.assertEqual(payment.received_amount, 100000000)  # Not 175000000

            # Verify separate duplicate payments were created
            duplicate_payments = (
                self.dbsession.query(CryptoPayment)
                .filter(CryptoPayment.status == CryptoPayment.STATUS_DOUBLEPAY_REFUNDED)
                .all()
            )
            self.assertGreater(len(duplicate_payments), 0)

    def test_race_condition_double_refund_prevention(self):
        """Test that scanner handles multiple transactions to same address correctly."""
        # Create confirmed payment
        payment, invoice, shop, user = self._create_payment_scenario(
            status=CryptoPayment.STATUS_CONFIRMED, received=100000000
        )
        payment.tx_hashes = json.dumps(["first-tx"])
        payment.refund_address = "4UserRefund..."
        self.dbsession.add(payment)
        self.dbsession.flush()

        # Create crypto processor for scanning
        from ..models.crypto_processor import CryptoProcessor

        processor = CryptoProcessor(
            shop_id=payment.shop.id,
            coin_type="XMR",
            sweep_to_address="4SweepAddress...",
        )
        processor.enabled = True
        processor.last_scan_semaphore = "xmr:1999999"
        processor.wallet_label = "0"  # For XMR, wallet_label is the account index
        self.dbsession.add(processor)
        self.dbsession.flush()

        # Mock wallet scan finding TWO transactions to same subaddress
        with patch(
            "make_post_sell.lib.crypto_watcher.get_crypto_client"
        ) as mock_client:
            mock_rpc = MagicMock()
            mock_client.return_value = mock_rpc

            # Scanner finds two transactions to same subaddress
            mock_rpc._call.return_value = {
                "in": [
                    {
                        "txid": "duplicate-tx-1",
                        "amount": 75000000,
                        "confirmations": 3,
                        "subaddr_index": {
                            "major": 0,
                            "minor": payment.subaddress_index,
                        },
                        "height": 2000001,
                    },
                    {
                        "txid": "duplicate-tx-2",
                        "amount": 50000000,
                        "confirmations": 2,
                        "subaddr_index": {
                            "major": 0,
                            "minor": payment.subaddress_index,
                        },
                        "height": 2000002,
                    },
                ]
            }

            request = MagicMock()
            request.dbsession = self.dbsession
            request.registry.settings = self.settings

            # Run scanner - should create separate duplicate payment for each tx
            from ..lib.crypto_watcher import scan_wallet_for_double_or_late_payments

            scan_wallet_for_double_or_late_payments(request, self.settings)

        # Verify separate duplicate payments were created
        duplicates = (
            self.dbsession.query(CryptoPayment)
            .filter(CryptoPayment.status == CryptoPayment.STATUS_DOUBLEPAY_REFUNDED)
            .all()
        )

        # Should have exactly 2 duplicate payment entries (one for each transaction)
        self.assertEqual(len(duplicates), 2)

        # Original payment should be untouched
        self.dbsession.refresh(payment)
        self.assertEqual(payment.status, CryptoPayment.STATUS_CONFIRMED)
        self.assertEqual(payment.received_amount, 100000000)

    @patch("make_post_sell.lib.crypto_watcher.send_purchase_email")
    @patch("make_post_sell.lib.crypto_watcher.send_sale_email")
    def test_three_payments_one_confirms_two_refund(
        self, mock_sale_email, mock_purchase_email
    ):
        """Test that 3 payments to same address: 1 confirms, 2 get refunded separately."""
        # Create pending payment
        payment, invoice, shop, user = self._create_payment_scenario(
            status=CryptoPayment.STATUS_PENDING, received=0
        )
        payment.refund_address = "4UserRefund..."
        self.dbsession.add(payment)
        self.dbsession.flush()

        # Create crypto processor for scanning
        from ..models.crypto_processor import CryptoProcessor

        processor = CryptoProcessor(
            shop_id=payment.shop.id,
            coin_type="XMR",
            sweep_to_address="4SweepAddress...",
        )
        processor.enabled = True
        processor.last_scan_semaphore = "xmr:1999999"
        processor.wallet_label = "0"  # For XMR, wallet_label is the account index
        self.dbsession.add(processor)
        self.dbsession.flush()

        request = MagicMock()
        request.dbsession = self.dbsession
        request.registry.settings = self.settings

        # First: Process the initial payment normally (gets confirmed)
        first_tx = [{"txid": "payment-1", "amount": 100000000, "confirmations": 10}]
        with patch("make_post_sell.lib.crypto_watcher.get_crypto_client"):
            from ..lib.crypto_watcher import process_payment

            process_payment(request, payment, first_tx)

        # Verify first payment is confirmed
        self.assertEqual(payment.status, CryptoPayment.STATUS_CONFIRMED)
        self.assertEqual(payment.received_amount, 100000000)

        # Second: Scanner finds TWO MORE payments to same subaddress
        with patch(
            "make_post_sell.lib.crypto_watcher.get_crypto_client"
        ) as mock_client:
            mock_rpc = MagicMock()
            mock_client.return_value = mock_rpc

            # Scanner finds two additional transactions to same subaddress
            mock_rpc._call.return_value = {
                "in": [
                    {
                        "txid": "payment-2",
                        "amount": 75000000,
                        "confirmations": 5,
                        "subaddr_index": {
                            "major": 0,
                            "minor": payment.subaddress_index,
                        },
                        "height": 2000001,
                    },
                    {
                        "txid": "payment-3",
                        "amount": 50000000,
                        "confirmations": 3,
                        "subaddr_index": {
                            "major": 0,
                            "minor": payment.subaddress_index,
                        },
                        "height": 2000002,
                    },
                ]
            }

            # Run scanner - should create 2 separate duplicate payments
            from ..lib.crypto_watcher import scan_wallet_for_double_or_late_payments

            scan_wallet_for_double_or_late_payments(request, self.settings)

        # Verify results: 1 confirmed + 2 duplicate refunds
        all_payments = (
            self.dbsession.query(CryptoPayment)
            .filter(CryptoPayment.subaddress_index == payment.subaddress_index)
            .all()
        )

        # Should have 3 total payments for this subaddress
        self.assertEqual(len(all_payments), 3)

        # Find payments by status
        confirmed_payments = [
            p for p in all_payments if p.status == CryptoPayment.STATUS_CONFIRMED
        ]
        duplicate_payments = [
            p
            for p in all_payments
            if p.status == CryptoPayment.STATUS_DOUBLEPAY_REFUNDED
        ]

        # Verify counts
        self.assertEqual(len(confirmed_payments), 1)  # Original payment
        self.assertEqual(len(duplicate_payments), 2)  # Two duplicates

        # Verify original payment unchanged
        original = confirmed_payments[0]
        self.assertEqual(original.id, payment.id)
        self.assertEqual(original.received_amount, 100000000)
        self.assertEqual(original.status, CryptoPayment.STATUS_CONFIRMED)

        # Verify duplicate payments have correct amounts and same invoice
        duplicate_amounts = sorted([p.received_amount for p in duplicate_payments])
        self.assertEqual(duplicate_amounts, [50000000, 75000000])  # Sorted amounts

        for dup in duplicate_payments:
            self.assertEqual(
                dup.invoice_id, payment.invoice_id
            )  # Same invoice as original
            self.assertEqual(dup.refund_address, payment.refund_address)
            self.assertEqual(dup.status, CryptoPayment.STATUS_DOUBLEPAY_REFUNDED)

    @patch("make_post_sell.lib.crypto_watcher.send_refund_email")
    @patch("make_post_sell.lib.crypto_watcher.send_purchase_email")
    @patch("make_post_sell.lib.crypto_watcher.send_sale_email")
    def test_full_payment_flow_with_double_spend(
        self, mock_sale_email, mock_purchase_email, mock_refund_email
    ):
        """Test complete flow: payment, confirmation, double-spend, refund."""
        # 1. Create pending payment
        payment, invoice, shop, user = self._create_payment_scenario()
        self.dbsession.flush()

        # 2. First payment arrives
        with patch("make_post_sell.lib.crypto_watcher.get_crypto_client"):
            request = testing.DummyRequest()
            request.dbsession = self.dbsession
            request.registry.settings = {}
            request.registry.settings = self.settings

            first_tx = [{"txid": "payment-1", "amount": 100000000, "confirmations": 10}]

            process_payment(request, payment, first_tx)

        # Should be confirmed
        self.assertEqual(payment.status, CryptoPayment.STATUS_CONFIRMED)
        self.assertEqual(payment.received_amount, 100000000)

        # 3. Double payment arrives (user accidentally pays again)
        # This should be caught by scanner, not main loop

        # First, verify main loop rejects it
        double_tx = [
            {"txid": "payment-1", "amount": 100000000, "confirmations": 11},
            {"txid": "payment-2", "amount": 80000000, "confirmations": 1},  # New!
        ]

        with patch("make_post_sell.lib.crypto_watcher.log") as mock_log:
            process_payment(request, payment, double_tx)

            # Should detect duplicate via payment_error calls
            error_calls = mock_log.payment_error.call_args_list
            duplicate_calls = [
                call
                for call in error_calls
                if "DUPLICATE PAYMENT DETECTED" in str(call[0][1])
            ]
            self.assertTrue(len(duplicate_calls) >= 1)

        # Amount shouldn't change
        self.assertEqual(payment.received_amount, 100000000)

        # 4. Scanner handles it properly
        # Create processor
        processor = CryptoProcessor(
            shop_id=shop.id,
            coin_type="XMR",
            sweep_to_address="4SweepAddress...",
        )
        processor.enabled = True
        processor.last_scan_semaphore = "xmr:1999999"
        processor.wallet_label = "0"  # For XMR, wallet_label is the account index
        self.dbsession.add(processor)
        self.dbsession.flush()

        with patch(
            "make_post_sell.lib.crypto_watcher.get_crypto_client"
        ) as mock_client:
            mock_rpc = MagicMock()
            mock_client.return_value = mock_rpc

            # Scanner sees the double payment
            mock_rpc._call.return_value = {
                "in": [
                    {
                        "txid": "payment-2",
                        "amount": 80000000,
                        "confirmations": 2,  # Needs at least 2 for refund
                        "subaddr_index": {
                            "major": 0,
                            "minor": payment.subaddress_index,
                        },
                        "height": 2000001,
                    }
                ]
            }

            # Mock get_transfers for process_payment when scanner calls it
            mock_rpc.get_transfers.return_value = {
                "in": [
                    {
                        "txid": "payment-2",
                        "amount": 80000000,
                        "confirmations": 2,
                        "subaddr_index": {
                            "major": 0,
                            "minor": payment.subaddress_index,
                        },
                    }
                ]
            }

            # Mock successful refund transfer
            mock_rpc.transfer.return_value = {
                "tx_hash": "refund-tx-123",
                "amount": 72800000,
                "fee": 7200000,
            }

            # Mock balance check
            mock_rpc.get_balance.return_value = {
                "balance": 100000000,
                "unlocked_balance": 100000000,
            }

            scan_wallet_for_double_or_late_payments(request, self.settings)

        # Verify duplicate payment(s) were created
        # Now we create duplicates in both main loop AND scanner, so expect at least 1
        all_duplicates = (
            self.dbsession.query(CryptoPayment)
            .filter(
                CryptoPayment.status.in_(
                    [
                        CryptoPayment.STATUS_DOUBLEPAY_REFUNDED,
                        CryptoPayment.STATUS_DOUBLEPAY_REFUNDED_COMPLETE,
                    ]
                )
            )
            .all()
        )

        # Check for any duplicate payment status
        duplicates = all_duplicates

        self.assertGreaterEqual(len(duplicates), 1)

        # The duplicate payment should exist with the correct amount
        # Note: The refund may not be executed if confirmations are insufficient
        # or if the refund process fails, so we check the basic properties
        duplicate = duplicates[0]
        self.assertEqual(duplicate.received_amount, 80000000)
        self.assertIn(
            duplicate.status,
            [
                CryptoPayment.STATUS_DOUBLEPAY_REFUNDED,
                CryptoPayment.STATUS_DOUBLEPAY_REFUNDED_COMPLETE,
            ],
        )

    @patch("make_post_sell.lib.crypto_watcher.send_purchase_email")
    @patch("make_post_sell.lib.crypto_watcher.send_sale_email")
    def test_overpayment_vs_double_payment(self, mock_sale_email, mock_purchase_email):
        """Test distinction between overpayment and double payment."""
        # Scenario 1: Overpayment in single transaction
        payment1, _, _, _ = self._create_payment_scenario(
            expected=100000000  # Expecting 0.001 XMR
        )

        request = testing.DummyRequest()
        request.dbsession = self.dbsession

        # User sends 0.0015 XMR in one transaction
        overpay_tx = [
            {"txid": "overpay-1", "amount": 150000000, "confirmations": 10}  # 50% more
        ]

        with patch("make_post_sell.lib.crypto_watcher.PaymentRescue") as MockRescue:
            mock_rescue = MagicMock()
            MockRescue.return_value = mock_rescue
            mock_rescue.handle_overpayment.return_value = {
                "payment_id": payment1.id,
                "refund_address": payment1.refund_address,
                "excess_amount": Decimal("0.0005"),
                "refund_amount": Decimal("0.000455"),  # After 9% fee
                "reason": "Overpayment",
            }
            # Mock execute_refund to fail so status stays at CONFIRMED_OVERPAID
            mock_rescue.execute_refund.return_value = {
                "success": False,
                "error": "Refund failed for testing",
            }

            # Create mock client so payment_rescue is initialized
            mock_client = MagicMock()
            process_payment(request, payment1, overpay_tx, mock_client)

        # Single transaction overpayment: should be marked as overpaid
        self.assertEqual(payment1.status, CryptoPayment.STATUS_CONFIRMED_OVERPAY)
        self.assertEqual(payment1.received_amount, 150000000)

        # Scenario 2: Double payment (two separate transactions)
        payment2, _, _, _ = self._create_payment_scenario()

        # First exact payment
        tx1 = [{"txid": "exact-1", "amount": 100000000, "confirmations": 10}]
        mock_client_first = MagicMock()
        process_payment(request, payment2, tx1, mock_client_first)
        self.assertEqual(payment2.status, CryptoPayment.STATUS_CONFIRMED)

        # Second payment (should be caught as duplicate)
        payment2.tx_hashes = json.dumps(["exact-1"])  # Update stored
        tx2 = [
            {"txid": "exact-1", "amount": 100000000, "confirmations": 11},
            {"txid": "double-1", "amount": 50000000, "confirmations": 1},
        ]

        mock_client = MagicMock()
        with patch("make_post_sell.lib.crypto_watcher.log") as mock_log:
            process_payment(request, payment2, tx2, mock_client)

            # Check for duplicate detection via payment_error
            error_calls = mock_log.payment_error.call_args_list
            duplicate_warnings = [
                call
                for call in error_calls
                if "DUPLICATE PAYMENT DETECTED" in str(call[0][1])
            ]
            self.assertGreater(len(duplicate_warnings), 0)

            # Verify duplicate payment record creation was logged
            info_calls = mock_log.payment_info.call_args_list
            duplicate_creation_calls = [
                call
                for call in info_calls
                if "Created duplicate payment" in str(call[0][1])
            ]
            self.assertGreater(len(duplicate_creation_calls), 0)

        # Refresh payment from database to get current state
        self.dbsession.refresh(payment2)

        # Original payment should remain CONFIRMED with original amount
        self.assertEqual(payment2.status, CryptoPayment.STATUS_CONFIRMED)
        self.assertEqual(payment2.received_amount, 100000000)  # Not 150000000

        # Verify separate duplicate payment was created in database
        duplicate_payments = (
            self.dbsession.query(CryptoPayment)
            .filter(CryptoPayment.status == CryptoPayment.STATUS_DOUBLEPAY_REFUNDED)
            .all()
        )
        self.assertEqual(len(duplicate_payments), 1)

        duplicate_payment = duplicate_payments[0]
        self.assertEqual(
            duplicate_payment.received_amount, 50000000
        )  # The duplicate amount
        self.assertEqual(
            duplicate_payment.invoice, payment2.invoice
        )  # Same invoice as original

    @patch("make_post_sell.lib.crypto_watcher.send_refund_email")
    @patch("make_post_sell.lib.crypto_watcher.send_purchase_email")
    @patch("make_post_sell.lib.crypto_watcher.send_sale_email")
    def test_duplicate_payment_confirmation_count_updates_in_processing_cycle(
        self, mock_sale_email, mock_purchase_email, mock_refund_email
    ):
        """
        Integration test for the bug where duplicate payments weren't getting their
        confirmation counts updated in the main processing loop, causing stuck refunds.

        This tests the full cycle: create duplicate payment, process it with mock
        blockchain data showing high confirmations, verify confirmation count updates.
        """
        with transaction.manager:
            # Create the base payment scenario
            payment, invoice, shop, user = self._create_payment_scenario(
                status=CryptoPayment.STATUS_CONFIRMED,
                received=100000000,
                expected=100000000,
            )
            payment.tx_hashes = json.dumps(["original-tx-123"])
            self.dbsession.add(payment)

            # Create a duplicate payment that should get its confirmations updated
            duplicate_payment = CryptoPayment(
                invoice=None,  # Duplicates don't have invoices
                user=user,
                shop=shop,
                address=payment.address,
                account_index=payment.account_index,
                subaddress_index=payment.subaddress_index,
                coin_type="XMR",
                expected_amount=0,  # Duplicates don't expect payment
                rate_locked_usd_per_coin=Decimal("150.00"),
                quote_expires_at_ms=int(time.time() * 1000) + 900000,
                confirmations_required=10,
                refund_address="test-refund-address-12345",
            )
            duplicate_payment.status = CryptoPayment.STATUS_DOUBLEPAY_REFUNDED
            duplicate_payment.received_amount = 37080000  # Some amount received
            duplicate_payment.current_confirmations = 0  # Starting with 0 - the bug!
            duplicate_payment.tx_hashes = json.dumps(["duplicate-tx-456"])
            self.dbsession.add(duplicate_payment)
            self.dbsession.flush()

            # Mock crypto client to simulate blockchain with confirmed transaction
            mock_client = MagicMock()
            # This simulates the _call to get_transfers returning the duplicate transaction with confirmations
            mock_client._call.return_value = {
                "in": [
                    {
                        "txid": "original-tx-123",
                        "amount": 100000000,
                        "confirmations": 25,  # Original transaction
                    },
                    {
                        "txid": "duplicate-tx-456",
                        "amount": 37080000,
                        "confirmations": 15,  # Duplicate transaction with sufficient confirmations
                    },
                    {
                        "txid": "newer-tx-789",
                        "amount": 20000000,
                        "confirmations": 2,  # New transaction with low confirmations
                    },
                ]
            }

            # Mock PaymentRescue to prevent actual refund processing but allow confirmation updates
            with patch(
                "make_post_sell.lib.crypto_watcher.PaymentRescue"
            ) as mock_rescue_class:
                # Create a mock PaymentRescue instance that returns proper refund data
                mock_rescue_instance = MagicMock()
                mock_rescue_instance.handle_expired_payment.return_value = {
                    "payment_id": str(duplicate_payment.id),
                    "refund_address": "test-refund-address-12345",
                    "refund_amount": Decimal("0.000337"),  # 37080000 - 9% fee
                    "reason": "Duplicate payment detected",
                }
                mock_rescue_instance.execute_refund.return_value = {
                    "success": True,
                    "tx_hash": "test-refund-tx-hash-123",
                }
                mock_rescue_class.return_value = mock_rescue_instance

                # Create mock request
                mock_request = MagicMock()
                mock_request.dbsession = self.dbsession

                # TEST: Process the duplicate payment - should update confirmations and process refund
                with patch("make_post_sell.lib.crypto_watcher.log") as mock_log:
                    process_payment(
                        mock_request, duplicate_payment, [], client=mock_client
                    )

                    # Debug: Check what log messages were generated
                    info_calls = mock_log.payment_info.call_args_list
                    print("\n=== LOG INFO CALLS ===")
                    for call in info_calls:
                        print(f"  {call[0][1]}")  # Print the message part
                    print("=== END LOG INFO CALLS ===")

            # CRITICAL ASSERTION: Confirmation count should have been updated from 0 to 15
            self.assertEqual(
                duplicate_payment.current_confirmations,
                15,
                "Duplicate payment confirmation count was not updated from blockchain data",
            )

            # Verify the specific blockchain query was made to get ALL transfers (not filtered)
            mock_client._call.assert_called_once_with(
                "get_transfers",
                {
                    "in": True,
                    "out": False,
                    "pending": True,
                    "failed": False,
                    "pool": True,
                    "filter_by_height": False,  # Critical: gets ALL transfers to find old confirmed txs
                    "subaddr_indices": [duplicate_payment.subaddress_index],
                    "account_index": duplicate_payment.account_index,
                },
            )

            # Since we mocked PaymentRescue to return None, refund won't be attempted
            # but the confirmation count update should still happen


if __name__ == "__main__":
    import unittest

    unittest.main()
