"""Unit tests for invoice deletion on terminal crypto payment states."""

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

from ..models.crypto_payment import CryptoPayment
from ..models.invoice import Invoice
from ..lib.crypto_watcher import delete_invoice_for_terminal_state


class TestInvoiceDeletion(unittest.TestCase):
    """Unit tests for invoice deletion logic."""

    def setUp(self):
        """Set up test fixtures."""
        self.mock_dbsession = Mock()
        self.mock_invoice = Mock()
        self.mock_invoice.id = "test-invoice-123"
        self.mock_crypto_payment = Mock()
        self.mock_crypto_payment.id = "test-payment-456"
        self.mock_crypto_payment.invoice = self.mock_invoice
        self.mock_crypto_payment.invoice_id = "test-invoice-123"

    @patch("make_post_sell.lib.crypto_watcher.delete_invoice_by_id")
    def test_delete_invoice_for_expired_status(self, mock_delete_invoice):
        """Test that expired payments have their invoices deleted."""
        mock_delete_invoice.return_value = {
            "success": True,
            "message": "Deleted successfully",
        }
        self.mock_crypto_payment.status = CryptoPayment.STATUS_EXPIRED
        self.mock_crypto_payment.should_keep_invoice.return_value = False

        delete_invoice_for_terminal_state(self.mock_dbsession, self.mock_crypto_payment)

        # Verify invoice reference was cleared
        self.assertIsNone(self.mock_crypto_payment.invoice_id)
        self.assertIsNone(self.mock_crypto_payment.invoice)

        # Verify payment was added to session
        self.mock_dbsession.add.assert_called_once_with(self.mock_crypto_payment)

        # Verify invoice deletion was called
        mock_delete_invoice.assert_called_once_with(
            self.mock_dbsession, "test-invoice-123"
        )

    @patch("make_post_sell.lib.crypto_watcher.delete_invoice_by_id")
    def test_delete_invoice_for_cancelled_status(self, mock_delete_invoice):
        """Test that cancelled payments have their invoices deleted."""
        mock_delete_invoice.return_value = {
            "success": True,
            "message": "Deleted successfully",
        }
        self.mock_crypto_payment.status = CryptoPayment.STATUS_CANCELLED
        self.mock_crypto_payment.should_keep_invoice.return_value = False

        delete_invoice_for_terminal_state(self.mock_dbsession, self.mock_crypto_payment)

        # Verify invoice reference was cleared
        self.assertIsNone(self.mock_crypto_payment.invoice_id)
        self.assertIsNone(self.mock_crypto_payment.invoice)
        mock_delete_invoice.assert_called_once_with(
            self.mock_dbsession, "test-invoice-123"
        )

    @patch("make_post_sell.lib.crypto_watcher.delete_invoice_by_id")
    def test_delete_invoice_for_no_refund_status(self, mock_delete_invoice):
        """Test that no-refund payments have their invoices deleted."""
        mock_delete_invoice.return_value = {
            "success": True,
            "message": "Deleted successfully",
        }
        self.mock_crypto_payment.status = CryptoPayment.STATUS_LATEPAY_NOT_REFUNDED
        self.mock_crypto_payment.should_keep_invoice.return_value = False

        delete_invoice_for_terminal_state(self.mock_dbsession, self.mock_crypto_payment)

        mock_delete_invoice.assert_called_once_with(
            self.mock_dbsession, "test-invoice-123"
        )

    @patch("make_post_sell.lib.crypto_watcher.delete_invoice_by_id")
    def test_delete_invoice_for_complete_refund_statuses(self, mock_delete_invoice):
        """Test that complete refund statuses have their invoices deleted."""
        mock_delete_invoice.return_value = {
            "success": True,
            "message": "Deleted successfully",
        }

        complete_refund_statuses = [
            CryptoPayment.STATUS_LATEPAY_REFUNDED_COMPLETE,
            CryptoPayment.STATUS_UNDERPAID_REFUNDED_COMPLETE,
            CryptoPayment.STATUS_OUT_OF_STOCK_REFUNDED_COMPLETE,
        ]

        for status in complete_refund_statuses:
            with self.subTest(status=status):
                # Reset mocks
                self.mock_dbsession.reset_mock()
                mock_delete_invoice.reset_mock()
                self.mock_crypto_payment.invoice = self.mock_invoice
                self.mock_crypto_payment.invoice_id = "test-invoice-123"
                self.mock_crypto_payment.status = status
                self.mock_crypto_payment.should_keep_invoice.return_value = False

                delete_invoice_for_terminal_state(
                    self.mock_dbsession, self.mock_crypto_payment
                )

                mock_delete_invoice.assert_called_once_with(
                    self.mock_dbsession, "test-invoice-123"
                )

    @patch("make_post_sell.lib.crypto_watcher.delete_invoice_by_id")
    def test_keep_invoice_for_confirmed_status(self, mock_delete_invoice):
        """Test that confirmed payments keep their invoices."""
        self.mock_crypto_payment.status = CryptoPayment.STATUS_CONFIRMED

        delete_invoice_for_terminal_state(self.mock_dbsession, self.mock_crypto_payment)

        # Verify invoice deletion was NOT called
        mock_delete_invoice.assert_not_called()

        # Verify invoice reference was NOT cleared
        self.assertEqual(self.mock_crypto_payment.invoice, self.mock_invoice)
        self.assertEqual(self.mock_crypto_payment.invoice_id, "test-invoice-123")

    @patch("make_post_sell.lib.crypto_watcher.delete_invoice_by_id")
    def test_keep_invoice_for_confirmed_overpaid_status(self, mock_delete_invoice):
        """Test that confirmed-overpaid payments keep their invoices."""
        self.mock_crypto_payment.status = CryptoPayment.STATUS_CONFIRMED_OVERPAY

        delete_invoice_for_terminal_state(self.mock_dbsession, self.mock_crypto_payment)

        mock_delete_invoice.assert_not_called()

    @patch("make_post_sell.lib.crypto_watcher.delete_invoice_by_id")
    def test_keep_invoice_for_confirmed_overpaid_refunded_status(
        self, mock_delete_invoice
    ):
        """Test that confirmed-overpaid-refunded payments keep their invoices."""
        self.mock_crypto_payment.status = (
            CryptoPayment.STATUS_CONFIRMED_OVERPAY_REFUNDED
        )

        delete_invoice_for_terminal_state(self.mock_dbsession, self.mock_crypto_payment)

        mock_delete_invoice.assert_not_called()

    @patch("make_post_sell.lib.crypto_watcher.delete_invoice_by_id")
    def test_skip_when_no_invoice(self, mock_delete_invoice):
        """Test that function returns early when payment has no invoice."""
        self.mock_crypto_payment.invoice = None
        self.mock_crypto_payment.status = CryptoPayment.STATUS_EXPIRED

        delete_invoice_for_terminal_state(self.mock_dbsession, self.mock_crypto_payment)

        # Verify invoice deletion was NOT called
        mock_delete_invoice.assert_not_called()
        self.mock_dbsession.add.assert_not_called()

    @patch("make_post_sell.lib.crypto_watcher.delete_invoice_by_id")
    @patch("make_post_sell.lib.crypto_watcher.log")
    def test_handle_invoice_deletion_failure(self, mock_logger, mock_delete_invoice):
        """Test that invoice deletion failures are handled gracefully."""
        mock_delete_invoice.side_effect = Exception("Database error")
        self.mock_crypto_payment.status = CryptoPayment.STATUS_EXPIRED
        self.mock_crypto_payment.should_keep_invoice.return_value = False

        # Should not raise exception
        delete_invoice_for_terminal_state(self.mock_dbsession, self.mock_crypto_payment)

        # Verify error was logged using centralized error_with_context method
        mock_logger.error_with_context.assert_called_once()

        # Verify invoice reference was NOT cleared since deletion failed
        self.assertEqual(self.mock_crypto_payment.invoice_id, "test-invoice-123")
        self.assertEqual(self.mock_crypto_payment.invoice, self.mock_invoice)

    @patch("make_post_sell.lib.crypto_watcher.delete_invoice_by_id")
    @patch("make_post_sell.lib.crypto_watcher.log")
    def test_handle_invoice_deletion_returns_false(self, mock_log, mock_delete_invoice):
        """Test that function handles when delete_invoice_by_id returns failure."""
        mock_delete_invoice.return_value = {
            "success": False,
            "message": "Deletion failed",
        }
        self.mock_crypto_payment.status = CryptoPayment.STATUS_EXPIRED
        self.mock_crypto_payment.should_keep_invoice.return_value = False

        delete_invoice_for_terminal_state(self.mock_dbsession, self.mock_crypto_payment)

        # Verify error was logged
        mock_log.error_with_context.assert_called_once()

        # Verify invoice reference was NOT cleared since deletion failed
        self.assertEqual(self.mock_crypto_payment.invoice_id, "test-invoice-123")
        self.assertEqual(self.mock_crypto_payment.invoice, self.mock_invoice)

    def test_all_terminal_states_except_confirmed_delete_invoice(self):
        """Test that all terminal states except confirmed ones trigger deletion."""
        confirmed_states = {
            CryptoPayment.STATUS_CONFIRMED,
            CryptoPayment.STATUS_CONFIRMED_OVERPAY,
            CryptoPayment.STATUS_CONFIRMED_OVERPAY_REFUNDED,
            CryptoPayment.STATUS_CONFIRMED_OVERPAY_REFUNDED_COMPLETE,
            CryptoPayment.STATUS_CONFIRMED_COMPLETE,  # Also a confirmed state that keeps invoice
        }

        terminal_states_that_should_delete = (
            set(CryptoPayment.TERMINAL_STATUSES) - confirmed_states
        )

        # Verify we have the expected terminal states that should delete invoices
        # Based on the current TERMINAL_STATUSES definition
        expected_delete_states = {
            CryptoPayment.STATUS_EXPIRED,
            CryptoPayment.STATUS_LATEPAY_REFUNDED_COMPLETE,
            CryptoPayment.STATUS_UNDERPAID_REFUNDED_COMPLETE,
            CryptoPayment.STATUS_CANCELLED,
            CryptoPayment.STATUS_OUT_OF_STOCK_REFUNDED_COMPLETE,
            CryptoPayment.STATUS_DOUBLEPAY_REFUNDED_COMPLETE,
            # Not-refunded states
            CryptoPayment.STATUS_LATEPAY_NOT_REFUNDED,
            CryptoPayment.STATUS_UNDERPAID_NOT_REFUNDED,
            CryptoPayment.STATUS_CONFIRMED_OVERPAY_NOT_REFUNDED,
            CryptoPayment.STATUS_OUT_OF_STOCK_NOT_REFUNDED,
            CryptoPayment.STATUS_DOUBLEPAY_NOT_REFUNDED,
        }

        self.assertEqual(terminal_states_that_should_delete, expected_delete_states)

        with patch(
            "make_post_sell.lib.crypto_watcher.delete_invoice_by_id"
        ) as mock_delete:
            mock_delete.return_value = {
                "success": True,
                "message": "Deleted successfully",
            }

            for status in terminal_states_that_should_delete:
                with self.subTest(status=status):
                    # Reset payment state
                    self.mock_crypto_payment.invoice = self.mock_invoice
                    self.mock_crypto_payment.invoice_id = "test-invoice-123"
                    self.mock_crypto_payment.status = status
                    # Mock should_keep_invoice to return False for failed payments
                    self.mock_crypto_payment.should_keep_invoice.return_value = False
                    self.mock_dbsession.reset_mock()
                    mock_delete.reset_mock()

                    delete_invoice_for_terminal_state(
                        self.mock_dbsession, self.mock_crypto_payment
                    )

                    # Should delete invoice for all these states
                    mock_delete.assert_called_once_with(
                        self.mock_dbsession, "test-invoice-123"
                    )
