"""Integration tests for invoice deletion in crypto payment processing."""

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

from pyramid.paster import get_appsettings
import transaction

from ..models import get_tm_session
from ..models.meta import Base
from ..models.shop import Shop
from ..models.user import User, get_or_create_user_by_email
from ..models.product import Product
from ..models.invoice import Invoice
from ..models.crypto_payment import CryptoPayment
from ..models.price import Price
from ..lib.crypto_watcher import process_payment, delete_invoice_for_terminal_state
from ..models.crypto_processor import CryptoProcessor


class DatabaseIntegrationTests(unittest.TestCase):
    """Base class for integration tests that need real database."""

    def setUp(self):
        from make_post_sell import main

        self.settings = get_appsettings("test.ini")
        self.app = main({}, **self.settings)

        self.session_factory = self.app.registry["dbsession_factory"]
        self.engine = self.session_factory.kw["bind"]
        Base.metadata.create_all(bind=self.engine)

        self.dbsession = get_tm_session(self.session_factory, transaction.manager)

    def tearDown(self):
        transaction.abort()
        Base.metadata.drop_all(bind=self.engine)


class InvoiceDeletionIntegrationTests(DatabaseIntegrationTests):
    """Integration tests for invoice deletion on crypto payment state transitions."""

    def create_crypto_payment_with_invoice(self, status="pending"):
        """Helper to create a crypto payment with an associated invoice."""
        # Create user
        user = get_or_create_user_by_email(self.dbsession, "test@example.com")
        self.dbsession.add(user)

        # Create shop with unique name to avoid constraint violations
        import time

        unique_suffix = str(int(time.time() * 1000000))[
            -6:
        ]  # Last 6 digits of microseconds
        shop = Shop(
            name=f"Test Shop {unique_suffix}",
            phone_number="555-1234",
            billing_address="123 Test St",
            description="Test shop for integration tests",
        )
        self.dbsession.add(shop)

        # Create product
        product = Product(title="Test Product", description="A test product")
        product.shop_id = shop.id
        product.price_in_cents = 1000  # $10.00
        product.is_physical = False
        self.dbsession.add(product)

        # Create price
        price = Price(product, 1000)  # $10.00 in cents
        self.dbsession.add(price)

        self.dbsession.flush()

        # Create invoice
        invoice = Invoice(user)
        invoice.user_id = user.id  # Explicitly set user_id
        invoice.shop = shop
        invoice.shop_id = shop.id
        invoice.new_line_item(product=product, quantity=1)
        self.dbsession.add(invoice)
        self.dbsession.flush()

        # Create crypto payment
        payment = CryptoPayment(
            invoice=invoice,
            user=user,
            shop=shop,
            address="test_address",
            account_index=0,
            subaddress_index=1,
            coin_type="XMR",
            expected_amount=1000000000000,  # 1 XMR in piconero
            rate_locked_usd_per_coin=Decimal("150.0"),
            quote_expires_at_ms=int((time.time() + 900) * 1000),
            confirmations_required=10,
        )
        payment.status = status
        self.dbsession.add(payment)
        self.dbsession.flush()

        # Refresh the invoice to ensure backref relationship is established
        self.dbsession.refresh(invoice)

        return payment, invoice

    def test_expired_payment_deletes_invoice(self):
        """Test that expired payments have their invoices deleted."""
        payment, invoice = self.create_crypto_payment_with_invoice(
            CryptoPayment.STATUS_EXPIRED
        )

        invoice_id = invoice.id
        payment_id = payment.id

        # Verify invoice exists before deletion
        self.assertIsNotNone(
            self.dbsession.query(Invoice).filter_by(id=invoice_id).first()
        )

        # Call the deletion function
        delete_invoice_for_terminal_state(self.dbsession, payment)
        self.dbsession.flush()

        # Verify invoice was deleted
        self.assertIsNone(
            self.dbsession.query(Invoice).filter_by(id=invoice_id).first()
        )

        # Verify payment still exists but invoice reference is cleared
        payment_from_db = (
            self.dbsession.query(CryptoPayment).filter_by(id=payment_id).first()
        )
        self.assertIsNotNone(payment_from_db)
        self.assertIsNone(payment_from_db.invoice_id)
        self.assertIsNone(payment_from_db.invoice)

    def test_cancelled_payment_deletes_invoice(self):
        """Test that cancelled payments have their invoices deleted."""
        payment, invoice = self.create_crypto_payment_with_invoice(
            CryptoPayment.STATUS_CANCELLED
        )

        invoice_id = invoice.id

        delete_invoice_for_terminal_state(self.dbsession, payment)
        self.dbsession.flush()

        # Verify invoice was deleted
        self.assertIsNone(
            self.dbsession.query(Invoice).filter_by(id=invoice_id).first()
        )

    def test_no_refund_payment_deletes_invoice(self):
        """Test that no-refund payments have their invoices deleted."""
        payment, invoice = self.create_crypto_payment_with_invoice(
            CryptoPayment.STATUS_LATEPAY_NOT_REFUNDED
        )

        invoice_id = invoice.id

        delete_invoice_for_terminal_state(self.dbsession, payment)
        self.dbsession.flush()

        # Verify invoice was deleted
        self.assertIsNone(
            self.dbsession.query(Invoice).filter_by(id=invoice_id).first()
        )

    def test_complete_refund_states_delete_invoice(self):
        """Test that all complete refund states delete invoices."""
        complete_refund_states = [
            CryptoPayment.STATUS_LATEPAY_REFUNDED_COMPLETE,
            CryptoPayment.STATUS_UNDERPAID_REFUNDED_COMPLETE,
            CryptoPayment.STATUS_OUT_OF_STOCK_REFUNDED_COMPLETE,
        ]

        for status in complete_refund_states:
            with self.subTest(status=status):
                payment, invoice = self.create_crypto_payment_with_invoice(status)

                invoice_id = invoice.id

                delete_invoice_for_terminal_state(self.dbsession, payment)
                self.dbsession.flush()

                # Verify invoice was deleted
                self.assertIsNone(
                    self.dbsession.query(Invoice).filter_by(id=invoice_id).first()
                )

    def test_confirmed_payment_keeps_invoice(self):
        """Test that confirmed payments keep their invoices."""
        payment, invoice = self.create_crypto_payment_with_invoice(
            CryptoPayment.STATUS_CONFIRMED
        )

        invoice_id = invoice.id
        payment_id = payment.id

        delete_invoice_for_terminal_state(self.dbsession, payment)
        self.dbsession.flush()

        # Verify invoice still exists
        self.assertIsNotNone(
            self.dbsession.query(Invoice).filter_by(id=invoice_id).first()
        )

        # Verify payment still has invoice reference
        payment_from_db = (
            self.dbsession.query(CryptoPayment).filter_by(id=payment_id).first()
        )
        self.assertIsNotNone(payment_from_db.invoice)
        self.assertEqual(str(payment_from_db.invoice_id), str(invoice_id))

    def test_confirmed_overpaid_payment_keeps_invoice(self):
        """Test that confirmed-overpaid payments keep their invoices."""
        payment, invoice = self.create_crypto_payment_with_invoice(
            CryptoPayment.STATUS_CONFIRMED_OVERPAY
        )

        invoice_id = invoice.id

        delete_invoice_for_terminal_state(self.dbsession, payment)
        self.dbsession.flush()

        # Verify invoice still exists
        self.assertIsNotNone(
            self.dbsession.query(Invoice).filter_by(id=invoice_id).first()
        )

    def test_confirmed_overpaid_refunded_payment_keeps_invoice(self):
        """Test that confirmed-overpaid-refunded payments keep their invoices."""
        payment, invoice = self.create_crypto_payment_with_invoice(
            CryptoPayment.STATUS_CONFIRMED_OVERPAY_REFUNDED
        )

        invoice_id = invoice.id

        delete_invoice_for_terminal_state(self.dbsession, payment)
        self.dbsession.flush()

        # Verify invoice still exists
        self.assertIsNotNone(
            self.dbsession.query(Invoice).filter_by(id=invoice_id).first()
        )

    def test_payment_without_invoice_handled_gracefully(self):
        """Test that payments without invoices are handled gracefully."""
        # Create user and shop
        user = get_or_create_user_by_email(self.dbsession, "test@example.com")
        import time

        unique_suffix = str(int(time.time() * 1000000))[
            -6:
        ]  # Last 6 digits of microseconds
        shop = Shop(
            name=f"Test Shop {unique_suffix}",
            phone_number="555-1234",
            billing_address="123 Test St",
            description="Test shop",
        )
        self.dbsession.add(user)
        self.dbsession.add(shop)
        self.dbsession.flush()

        # Create payment without invoice
        payment = CryptoPayment(
            invoice=None,
            user=user,
            shop=shop,
            address="test_address",
            account_index=0,
            subaddress_index=1,
            coin_type="XMR",
            expected_amount=1000000000000,
            rate_locked_usd_per_coin=Decimal("150.0"),
            quote_expires_at_ms=int((time.time() + 900) * 1000),
            confirmations_required=10,
        )
        payment.status = CryptoPayment.STATUS_EXPIRED
        self.dbsession.add(payment)
        self.dbsession.flush()

        # Should not raise any exceptions
        delete_invoice_for_terminal_state(self.dbsession, payment)
        self.dbsession.flush()

    @patch("make_post_sell.lib.crypto_watcher.delete_invoice_by_id")
    def test_invoice_deletion_failure_handled_gracefully(self, mock_delete):
        """Test that invoice deletion failures don't crash the watcher."""
        mock_delete.side_effect = Exception("Database constraint error")

        payment, invoice = self.create_crypto_payment_with_invoice("expired")

        # Should not raise any exceptions despite deletion failure
        delete_invoice_for_terminal_state(self.dbsession, payment)

        # Payment should still have invoice reference since deletion failed
        self.assertIsNotNone(payment.invoice_id)
        self.assertIsNotNone(payment.invoice)

    def test_state_transition_triggers_invoice_deletion(self):
        """Test that changing payment state to terminal triggers invoice deletion."""
        payment, invoice = self.create_crypto_payment_with_invoice(
            CryptoPayment.STATUS_PENDING
        )

        invoice_id = invoice.id

        # Simulate state transition in crypto watcher
        payment.status = CryptoPayment.STATUS_EXPIRED
        delete_invoice_for_terminal_state(self.dbsession, payment)
        self.dbsession.flush()

        # Verify invoice was deleted
        self.assertIsNone(
            self.dbsession.query(Invoice).filter_by(id=invoice_id).first()
        )

    def test_multiple_payments_same_invoice_edge_case(self):
        """Test edge case where multiple payments might reference same invoice."""
        # Create user, shop, product like in helper method
        user = get_or_create_user_by_email(self.dbsession, "test@example.com")
        import time

        unique_suffix = str(int(time.time() * 1000000))[
            -6:
        ]  # Last 6 digits of microseconds
        shop = Shop(
            name=f"Test Shop {unique_suffix}",
            phone_number="555-1234",
            billing_address="123 Test St",
            description="Test shop for integration tests",
        )
        product = Product(title="Test Product", description="A test product")
        product.shop_id = shop.id
        product.price_in_cents = 1000
        product.is_physical = False

        self.dbsession.add(user)
        self.dbsession.add(shop)
        self.dbsession.add(product)

        price = Price(product, 1000)
        self.dbsession.add(price)
        self.dbsession.flush()

        # Create invoice
        invoice = Invoice(user)
        invoice.user_id = user.id
        invoice.shop = shop
        invoice.shop_id = shop.id
        invoice.new_line_item(product=product, quantity=1)
        self.dbsession.add(invoice)
        self.dbsession.flush()

        # Create two payments for same invoice (edge case)
        payment1 = CryptoPayment(
            invoice=invoice,
            user=user,
            shop=shop,
            address="test_address_1",
            account_index=0,
            subaddress_index=1,
            coin_type="XMR",
            expected_amount=1000000000000,
            rate_locked_usd_per_coin=Decimal("150.0"),
            quote_expires_at_ms=int((time.time() + 900) * 1000),
            confirmations_required=10,
        )
        payment1.status = CryptoPayment.STATUS_CANCELLED

        payment2 = CryptoPayment(
            invoice=invoice,
            user=user,
            shop=shop,
            address="test_address_2",
            account_index=0,
            subaddress_index=2,
            coin_type="XMR",
            expected_amount=1000000000000,
            rate_locked_usd_per_coin=Decimal("150.0"),
            quote_expires_at_ms=int((time.time() + 900) * 1000),
            confirmations_required=10,
        )
        payment2.status = CryptoPayment.STATUS_PENDING

        self.dbsession.add(payment1)
        self.dbsession.add(payment2)
        self.dbsession.flush()

        invoice_id = invoice.id

        # First payment deletion should work
        delete_invoice_for_terminal_state(self.dbsession, payment1)
        self.dbsession.flush()

        # Invoice should be deleted
        self.assertIsNone(
            self.dbsession.query(Invoice).filter_by(id=invoice_id).first()
        )

        # Second payment should handle gracefully (invoice already deleted)
        delete_invoice_for_terminal_state(self.dbsession, payment2)
        self.dbsession.flush()

        # Should not crash even though invoice is already gone
        self.assertIsNone(payment2.invoice_id)
