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

from ..models.crypto_payment import CryptoPayment
from ..models.meta import now_timestamp


class TestCryptoPaymentTransitions(unittest.TestCase):
    """Unit tests for CryptoPayment state transition validation.

    IMPORTANT: Do NOT modify production code to make tests work.
    Do NOT use __dict__ access in production code.
    We need to test the ACTUAL CryptoPayment object behavior.
    Do NOT create fake/mock/testable objects - test the REAL CryptoPayment class.
    """

    def setUp(self):
        """Set up a basic CryptoPayment for testing."""
        # Following the pattern from test_double_spend_protection.py
        self.config = MagicMock()
        self.request = MagicMock()
        self.request.dbsession = MagicMock()

    def _create_test_payment(self, **kwargs):
        """Helper to create a test payment with defaults."""
        # Use the same pattern as test_double_spend_protection.py
        defaults = {
            "invoice": MagicMock(id="test-invoice"),
            "user": MagicMock(id="test-user"),
            "shop": MagicMock(id="test-shop"),
            "address": "test_xmr_address",
            "account_index": 0,
            "subaddress_index": 1,
            "coin_type": "XMR",
            "expected_amount": 1000000000000,  # 1 XMR
            "rate_locked_usd_per_coin": Decimal("100.0"),
            "quote_expires_at_ms": now_timestamp() + 900000,
            "confirmations_required": 10,
        }
        defaults.update(kwargs)

        payment = CryptoPayment(**defaults)
        # Override properties that were set in __init__ to avoid SQLAlchemy issues
        for key, value in kwargs.items():
            if hasattr(payment, key):
                setattr(payment, key, value)
        return payment

    def test_valid_transitions_from_pending(self):
        """Test valid transitions from STATUS_PENDING."""
        payment = self._create_test_payment()
        payment.status = CryptoPayment.STATUS_PENDING

        # Valid transitions
        self.assertTrue(payment.is_valid_transition(CryptoPayment.STATUS_RECEIVED))
        self.assertTrue(payment.is_valid_transition(CryptoPayment.STATUS_EXPIRED))
        self.assertTrue(payment.is_valid_transition(CryptoPayment.STATUS_CANCELLED))

        # Invalid transitions
        self.assertFalse(payment.is_valid_transition(CryptoPayment.STATUS_CONFIRMED))
        self.assertFalse(
            payment.is_valid_transition(CryptoPayment.STATUS_DOUBLEPAY_REFUNDED)
        )

    def test_valid_transitions_from_received(self):
        """Test valid transitions from STATUS_RECEIVED."""
        payment = self._create_test_payment()
        payment.status = CryptoPayment.STATUS_RECEIVED

        # Valid transitions
        self.assertTrue(payment.is_valid_transition(CryptoPayment.STATUS_CONFIRMED))
        self.assertTrue(
            payment.is_valid_transition(CryptoPayment.STATUS_CONFIRMED_OVERPAY)
        )
        # Cancelled is no longer valid from received (only from pending)
        self.assertFalse(payment.is_valid_transition(CryptoPayment.STATUS_CANCELLED))
        self.assertTrue(
            payment.is_valid_transition(CryptoPayment.STATUS_UNDERPAID_REFUNDED)
        )
        self.assertTrue(
            payment.is_valid_transition(CryptoPayment.STATUS_DOUBLEPAY_REFUNDED)
        )
        self.assertTrue(
            payment.is_valid_transition(CryptoPayment.STATUS_OUT_OF_STOCK_REFUNDED)
        )

        # Invalid transitions
        self.assertFalse(payment.is_valid_transition(CryptoPayment.STATUS_PENDING))
        self.assertFalse(payment.is_valid_transition(CryptoPayment.STATUS_EXPIRED))
        self.assertFalse(
            payment.is_valid_transition(CryptoPayment.STATUS_LATEPAY_REFUNDED_COMPLETE)
        )

    def test_valid_transitions_from_confirmed(self):
        """Test valid transitions from STATUS_CONFIRMED (mostly terminal)."""
        payment = self._create_test_payment()
        payment.status = CryptoPayment.STATUS_CONFIRMED

        # Confirmed is now fully terminal - no valid transitions
        self.assertFalse(payment.is_valid_transition(CryptoPayment.STATUS_CANCELLED))

        # Invalid transitions - confirmed is now fully terminal
        self.assertFalse(payment.is_valid_transition(CryptoPayment.STATUS_RECEIVED))
        self.assertFalse(payment.is_valid_transition(CryptoPayment.STATUS_EXPIRED))
        self.assertFalse(
            payment.is_valid_transition(CryptoPayment.STATUS_CONFIRMED_OVERPAY)
        )

    def test_valid_transitions_from_confirmed_overpaid(self):
        """Test valid transitions from STATUS_CONFIRMED_OVERPAY."""
        payment = self._create_test_payment()
        payment.status = CryptoPayment.STATUS_CONFIRMED_OVERPAY

        # Valid transitions
        self.assertTrue(
            payment.is_valid_transition(CryptoPayment.STATUS_CONFIRMED_OVERPAY_REFUNDED)
        )
        # Cancelled is no longer valid from confirmed_overpaid (only from pending)
        self.assertFalse(payment.is_valid_transition(CryptoPayment.STATUS_CANCELLED))

        # Invalid transitions
        self.assertFalse(payment.is_valid_transition(CryptoPayment.STATUS_CONFIRMED))
        self.assertFalse(payment.is_valid_transition(CryptoPayment.STATUS_RECEIVED))

    def test_priority_consistency_across_all_statuses(self):
        """Test that priority assignments make sense for all statuses."""
        payment = self._create_test_payment()

        # Test that we can get priority for every status
        for status in CryptoPayment.VALID_TRANSITIONS.keys():
            payment.status = status
            priority = payment.get_processing_priority()

            with self.subTest(status=status, priority=priority):
                # Priority should be 0-4
                self.assertIn(
                    priority,
                    [0, 1, 2, 3, 4],
                    f"Status {status} has invalid priority {priority}",
                )

                # Verify priority matches expected business logic
                if status in [
                    CryptoPayment.STATUS_DOUBLEPAY_REFUNDED,
                    CryptoPayment.STATUS_LATEPAY_REFUNDED,
                    CryptoPayment.STATUS_UNDERPAID_REFUNDED,
                    CryptoPayment.STATUS_OUT_OF_STOCK_REFUNDED,
                ]:
                    self.assertEqual(
                        priority, 0, f"Refund status {status} should be Priority 0"
                    )

                elif status == CryptoPayment.STATUS_RECEIVED:
                    self.assertEqual(
                        priority, 1, f"Received status should be Priority 1"
                    )

                elif status in [
                    CryptoPayment.STATUS_CONFIRMED,
                    CryptoPayment.STATUS_CONFIRMED_OVERPAY,
                ]:
                    self.assertEqual(
                        priority, 3, f"Auto-sweep status {status} should be Priority 3"
                    )

                elif status in [
                    CryptoPayment.STATUS_LATEPAY_REFUNDED_COMPLETE,
                    CryptoPayment.STATUS_UNDERPAID_REFUNDED_COMPLETE,
                    CryptoPayment.STATUS_CONFIRMED_OVERPAY_REFUNDED_COMPLETE,
                    CryptoPayment.STATUS_DOUBLEPAY_REFUNDED_COMPLETE,
                ]:
                    self.assertEqual(
                        priority,
                        4,
                        f"Restocking fee status {status} should be Priority 4",
                    )

    def test_new_not_refunded_states(self):
        """Test the new *-not-refunded states for when no refund wallet is configured."""
        payment = self._create_test_payment()

        # Test confirmed_overpaid_refunded → confirmed_overpaid_not_refunded
        payment.status = CryptoPayment.STATUS_CONFIRMED_OVERPAY_REFUNDED
        self.assertTrue(
            payment.is_valid_transition(
                CryptoPayment.STATUS_CONFIRMED_OVERPAY_NOT_REFUNDED
            )
        )

        # Test underpaid_refunded → underpaid_not_refunded
        payment.status = CryptoPayment.STATUS_UNDERPAID_REFUNDED
        self.assertTrue(
            payment.is_valid_transition(CryptoPayment.STATUS_UNDERPAID_NOT_REFUNDED)
        )

        # Test out_of_stock_refunded → out_of_stock_not_refunded
        payment.status = CryptoPayment.STATUS_OUT_OF_STOCK_REFUNDED
        self.assertTrue(
            payment.is_valid_transition(CryptoPayment.STATUS_OUT_OF_STOCK_NOT_REFUNDED)
        )

        # Test doublepay_refund → doublepay_not_refund
        payment.status = CryptoPayment.STATUS_DOUBLEPAY_REFUNDED
        self.assertTrue(
            payment.is_valid_transition(CryptoPayment.STATUS_DOUBLEPAY_NOT_REFUNDED)
        )

        # Test that all *_not_refunded states are terminal
        terminal_not_refunded_states = [
            CryptoPayment.STATUS_CONFIRMED_OVERPAY_NOT_REFUNDED,
            CryptoPayment.STATUS_UNDERPAID_NOT_REFUNDED,
            CryptoPayment.STATUS_OUT_OF_STOCK_NOT_REFUNDED,
            CryptoPayment.STATUS_DOUBLEPAY_NOT_REFUNDED,
            CryptoPayment.STATUS_LATEPAY_NOT_REFUNDED,
        ]

        for state in terminal_not_refunded_states:
            payment.status = state
            self.assertEqual(
                payment.get_valid_next_statuses(),
                [],
                f"{state} should be terminal with no outgoing transitions",
            )

    def test_validate_and_set_status_success(self):
        """Test successful status validation and setting."""
        payment = self._create_test_payment()

        with patch("logging.getLogger") as mock_logger:
            mock_log = Mock()
            mock_logger.return_value = mock_log

            payment.status = CryptoPayment.STATUS_PENDING

            # Valid transition should succeed
            result = payment.validate_and_set_status(
                CryptoPayment.STATUS_RECEIVED, "payment received"
            )

            self.assertTrue(result)
            self.assertEqual(payment.status, CryptoPayment.STATUS_RECEIVED)
            mock_log.info.assert_called_once()
            mock_log.error.assert_not_called()

    def test_validate_and_set_status_failure(self):
        """Test failed status validation."""
        payment = self._create_test_payment()

        with patch("logging.getLogger") as mock_logger:
            mock_log = Mock()
            mock_logger.return_value = mock_log

            payment.status = CryptoPayment.STATUS_PENDING

            # Invalid transition should fail
            result = payment.validate_and_set_status(
                CryptoPayment.STATUS_CONFIRMED, "invalid transition"
            )

            self.assertFalse(result)
            self.assertEqual(
                payment.status, CryptoPayment.STATUS_PENDING
            )  # Should not change
            mock_log.error.assert_called_once()
            mock_log.info.assert_not_called()

    def test_get_valid_next_statuses(self):
        """Test getting valid next statuses for different states."""
        payment = self._create_test_payment()

        # Test pending state
        payment.status = CryptoPayment.STATUS_PENDING
        expected = [
            CryptoPayment.STATUS_RECEIVED,
            CryptoPayment.STATUS_EXPIRED,
            CryptoPayment.STATUS_CANCELLED,
        ]
        self.assertEqual(set(payment.get_valid_next_statuses()), set(expected))

        # Test received state
        payment.status = CryptoPayment.STATUS_RECEIVED
        expected = [
            CryptoPayment.STATUS_CONFIRMED,
            CryptoPayment.STATUS_CONFIRMED_OVERPAY,
            # CryptoPayment.STATUS_CANCELLED,  # Only from pending now
            CryptoPayment.STATUS_UNDERPAID_REFUNDED,
            CryptoPayment.STATUS_DOUBLEPAY_REFUNDED,
            CryptoPayment.STATUS_OUT_OF_STOCK_REFUNDED,
        ]
        self.assertEqual(set(payment.get_valid_next_statuses()), set(expected))

    def test_state_machine_reachability_all_paths(self):
        """Test that every terminal state is reachable from PENDING through valid paths."""
        from collections import deque

        # Build graph of all valid transitions
        transitions = CryptoPayment.VALID_TRANSITIONS

        # Get all terminal states (states with no outgoing transitions)
        terminal_states = []
        for status, valid_next in transitions.items():
            if not valid_next:
                terminal_states.append(status)

        # BFS to find all reachable states from PENDING
        def find_reachable_states(start_status):
            visited = set()
            queue = deque([start_status])
            paths_to_terminal = {}

            while queue:
                current = queue.popleft()
                if current in visited:
                    continue
                visited.add(current)

                # If this is a terminal state, record it
                if current in terminal_states:
                    paths_to_terminal[current] = True

                # Add all valid next states to queue
                for next_status in transitions.get(current, []):
                    if next_status not in visited:
                        queue.append(next_status)

            return visited, paths_to_terminal

        reachable, terminal_paths = find_reachable_states(CryptoPayment.STATUS_PENDING)

        # Verify all terminal states are reachable from PENDING
        # Note: expired_refunded* and expired_not_refunded states are not reachable from original PENDING
        # payment since they represent NEW payment objects created for late payments
        excluded_from_pending = {
            # These states represent separate payment objects for late payments
            CryptoPayment.STATUS_LATEPAY_REFUNDED_COMPLETE,
            CryptoPayment.STATUS_LATEPAY_NOT_REFUNDED,
        }

        for terminal in terminal_states:
            if terminal in excluded_from_pending:
                # These states represent separate payment objects for late payments
                continue
            self.assertIn(
                terminal,
                terminal_paths,
                f"Terminal state {terminal} is not reachable from PENDING",
            )

        # Verify no orphaned states exist (all states should be reachable from somewhere)
        all_states = set(transitions.keys())

        # For orphan detection, we need to check if every state is reachable from at least one other state
        reachable_from_any = set()
        for start_state in all_states:
            visited = set()
            queue = deque([start_state])
            while queue:
                current = queue.popleft()
                if current in visited:
                    continue
                visited.add(current)
                reachable_from_any.update(visited)
                for next_status in transitions.get(current, []):
                    queue.append(next_status)

        # The only acceptable orphan is PENDING (the start state)
        orphaned = all_states - reachable_from_any
        orphaned.discard(
            CryptoPayment.STATUS_PENDING
        )  # PENDING is allowed to be orphaned
        self.assertEqual(orphaned, set(), f"Orphaned states found: {orphaned}")

    def test_state_machine_cycles_detection(self):
        """Test for unexpected cycles in the state machine."""
        transitions = CryptoPayment.VALID_TRANSITIONS

        def find_cycles(start_status, visited=None, path=None):
            """DFS to find cycles in the state machine."""
            if visited is None:
                visited = set()
            if path is None:
                path = []

            path.append(start_status)
            visited.add(start_status)

            cycles = []
            for next_status in transitions.get(start_status, []):
                if next_status in path:
                    # Found a cycle
                    cycle_start_idx = path.index(next_status)
                    cycle = path[cycle_start_idx:] + [next_status]
                    cycles.append(cycle)
                elif next_status not in visited:
                    cycles.extend(find_cycles(next_status, visited.copy(), path.copy()))

            return cycles

        # Find all cycles starting from each state
        all_cycles = []
        for status in transitions.keys():
            cycles = find_cycles(status)
            all_cycles.extend(cycles)

        # Remove duplicates
        unique_cycles = []
        for cycle in all_cycles:
            # Normalize cycle to start with smallest status
            min_idx = cycle.index(min(cycle))
            normalized = cycle[min_idx:] + cycle[:min_idx]
            if normalized not in unique_cycles:
                unique_cycles.append(normalized)

        # Verify no unexpected cycles exist
        # The only expected cycles would be through CANCELLED back to itself
        for cycle in unique_cycles:
            self.fail(f"Unexpected cycle detected: {' → '.join(cycle)}")

    def test_property_transition_symmetry(self):
        """Property: the state machine should be logically consistent."""
        transitions = CryptoPayment.VALID_TRANSITIONS

        # For each state, verify its transitions make logical sense
        for from_status, valid_targets in transitions.items():
            # Terminal states should have no or limited transitions
            if from_status in [
                CryptoPayment.STATUS_CANCELLED,
                CryptoPayment.STATUS_CONFIRMED_OVERPAY_REFUNDED_COMPLETE,
                CryptoPayment.STATUS_LATEPAY_REFUNDED_COMPLETE,
                CryptoPayment.STATUS_UNDERPAID_REFUNDED_COMPLETE,
                CryptoPayment.STATUS_OUT_OF_STOCK_REFUNDED_COMPLETE,
                CryptoPayment.STATUS_DOUBLEPAY_REFUNDED_COMPLETE,
            ]:
                self.assertEqual(
                    valid_targets,
                    [],
                    f"Terminal state {from_status} should have no transitions",
                )

            # Verify no backward transitions to earlier states (except CANCELLED)
            if from_status != CryptoPayment.STATUS_CANCELLED:
                for target in valid_targets:
                    # Ensure we're not going backwards in the flow
                    if target == CryptoPayment.STATUS_PENDING:
                        self.fail(
                            f"Invalid backward transition: {from_status} → PENDING"
                        )

    def test_property_terminal_states_unchangeable(self):
        """Property: terminal states should never change once reached."""
        payment = self._create_test_payment()

        terminal_states = [
            CryptoPayment.STATUS_CANCELLED,
            CryptoPayment.STATUS_CONFIRMED_OVERPAY_REFUNDED_COMPLETE,
            CryptoPayment.STATUS_LATEPAY_REFUNDED_COMPLETE,
            CryptoPayment.STATUS_UNDERPAID_REFUNDED_COMPLETE,
            CryptoPayment.STATUS_OUT_OF_STOCK_REFUNDED_COMPLETE,
            CryptoPayment.STATUS_DOUBLEPAY_REFUNDED_COMPLETE,
        ]

        for terminal_status in terminal_states:
            payment.status = terminal_status

            # Try to transition to every other status
            for target_status in CryptoPayment.VALID_TRANSITIONS.keys():
                if target_status != terminal_status:
                    with self.subTest(terminal=terminal_status, target=target_status):
                        self.assertFalse(
                            payment.is_valid_transition(target_status),
                            f"Terminal state {terminal_status} should not allow transition to {target_status}",
                        )

    def test_property_idempotent_transitions(self):
        """Property: setting same status multiple times should be idempotent in behavior."""
        payment = self._create_test_payment()

        test_statuses = [
            CryptoPayment.STATUS_PENDING,
            CryptoPayment.STATUS_RECEIVED,
            CryptoPayment.STATUS_CONFIRMED,
            CryptoPayment.STATUS_EXPIRED,
        ]

        for status in test_statuses:
            with self.subTest(status=status):
                payment.status = status
                original_status = payment.status

                # Setting same status should not be explicitly allowed in transitions
                # but it should be safe (not cause errors)
                is_valid = payment.is_valid_transition(status)
                # Most statuses don't allow self-transitions
                self.assertFalse(
                    is_valid,
                    f"Status {status} should not explicitly allow self-transition",
                )

                # But validate_and_set_status should handle it gracefully
                with patch("logging.getLogger"):
                    result = payment.validate_and_set_status(status, "idempotent test")
                    # It should fail validation but not crash
                    self.assertFalse(result)
                    # Status should remain unchanged
                    self.assertEqual(payment.status, original_status)

    def test_priority_edge_cases(self):
        """Test priority system with edge case statuses."""
        payment = self._create_test_payment()

        # Test unknown status (should default to Priority 2)
        payment.status = "unknown-status"
        priority = payment.get_processing_priority()
        self.assertEqual(priority, 2, "Unknown status should default to Priority 2")

        # Test that no two different priority levels conflict in dangerous ways
        priority_groups = {0: [], 1: [], 2: [], 3: [], 4: []}

        for status in CryptoPayment.VALID_TRANSITIONS.keys():
            payment.status = status
            priority = payment.get_processing_priority()
            priority_groups[priority].append(status)

        # Verify dangerous operations (Priority 4) are separate from customer operations (Priority 0-1)
        dangerous_ops = priority_groups[4]  # Restocking fees
        customer_ops = priority_groups[0] + priority_groups[1]  # Refunds + new payments

        # These should be completely separate sets
        self.assertEqual(
            set(dangerous_ops) & set(customer_ops),
            set(),
            "Dangerous operations should not overlap with customer operations",
        )

        # Verify refunds (Priority 0) come before auto-sweeps (Priority 3)
        refunds = priority_groups[0]
        auto_sweeps = priority_groups[3]
        self.assertTrue(len(refunds) > 0, "Should have refund statuses")
        self.assertTrue(len(auto_sweeps) > 0, "Should have auto-sweep statuses")

    def test_all_possible_payment_lifecycles(self):
        """Test every possible path through the payment lifecycle."""
        from collections import deque

        transitions = CryptoPayment.VALID_TRANSITIONS

        def generate_all_paths(start_status, max_depth=10):
            """Generate all possible valid paths from start_status."""
            paths = []
            queue = deque([(start_status, [start_status])])

            while queue:
                current_status, path = queue.popleft()

                if len(path) > max_depth:
                    continue

                # Check if this is a terminal state
                next_statuses = transitions.get(current_status, [])
                if not next_statuses:
                    # Terminal state reached
                    paths.append(path)
                    continue

                # Add all possible next states
                for next_status in next_statuses:
                    if next_status not in path:  # Avoid cycles
                        new_path = path + [next_status]
                        queue.append((next_status, new_path))

            return paths

        # Generate all possible paths from PENDING
        all_paths = generate_all_paths(CryptoPayment.STATUS_PENDING)

        # Verify we have reasonable coverage
        self.assertGreater(
            len(all_paths), 5, "Should have multiple possible payment lifecycles"
        )

        # Test each complete flow
        payment = self._create_test_payment()
        for path in all_paths:
            with self.subTest(path=" → ".join(path)):
                # Reset to start
                payment.status = CryptoPayment.STATUS_PENDING

                # Test that each transition in the path is valid
                for i in range(len(path) - 1):
                    current = path[i]
                    next_status = path[i + 1]

                    payment.status = current
                    self.assertTrue(
                        payment.is_valid_transition(next_status),
                        f"Invalid transition in path: {current} → {next_status}",
                    )

                # Verify the final state is terminal
                final_status = path[-1]
                payment.status = final_status
                terminal_transitions = payment.get_valid_next_statuses()
                self.assertEqual(
                    terminal_transitions,
                    [],
                    f"Final status {final_status} should be terminal",
                )

    def test_all_states_are_covered(self):
        """Test that all defined statuses are covered in VALID_TRANSITIONS."""
        # Get all status constants
        all_statuses = []
        for attr in dir(CryptoPayment):
            if attr.startswith("STATUS_") and not attr.endswith("STATUSES"):
                all_statuses.append(getattr(CryptoPayment, attr))

        # Check that all are in VALID_TRANSITIONS
        for status in all_statuses:
            self.assertIn(
                status,
                CryptoPayment.VALID_TRANSITIONS,
                f"Status {status} missing from VALID_TRANSITIONS",
            )

    def test_semantic_helper_methods(self):
        """Test the semantic helper methods with real CryptoPayment objects."""
        # Test successful payment statuses
        successful_statuses = [
            CryptoPayment.STATUS_CONFIRMED,
            CryptoPayment.STATUS_CONFIRMED_OVERPAY,
            CryptoPayment.STATUS_CONFIRMED_OVERPAY_REFUNDED,
            CryptoPayment.STATUS_CONFIRMED_OVERPAY_REFUNDED_COMPLETE,
        ]

        for status in successful_statuses:
            payment = self._create_test_payment()
            payment.status = status

            with self.subTest(status=status):
                self.assertTrue(
                    payment.is_successful_payment(),
                    f"{status} should be considered successful",
                )
                self.assertFalse(
                    payment.is_failed_payment(),
                    f"{status} should not be considered failed",
                )
                self.assertTrue(
                    payment.should_keep_invoice(), f"{status} should keep invoice"
                )

        # Test failed payment statuses
        failed_statuses = [
            CryptoPayment.STATUS_EXPIRED,
            CryptoPayment.STATUS_CANCELLED,
            CryptoPayment.STATUS_LATEPAY_REFUNDED_COMPLETE,
            CryptoPayment.STATUS_LATEPAY_NOT_REFUNDED,
            CryptoPayment.STATUS_UNDERPAID_REFUNDED_COMPLETE,
            CryptoPayment.STATUS_UNDERPAID_NOT_REFUNDED,
            CryptoPayment.STATUS_OUT_OF_STOCK_REFUNDED_COMPLETE,
            CryptoPayment.STATUS_OUT_OF_STOCK_NOT_REFUNDED,
            CryptoPayment.STATUS_DOUBLEPAY_REFUNDED_COMPLETE,
            CryptoPayment.STATUS_DOUBLEPAY_NOT_REFUNDED,
            CryptoPayment.STATUS_CONFIRMED_OVERPAY_NOT_REFUNDED,
        ]

        for status in failed_statuses:
            payment = self._create_test_payment()
            payment.status = status

            with self.subTest(status=status):
                self.assertFalse(
                    payment.is_successful_payment(),
                    f"{status} should not be considered successful",
                )
                self.assertTrue(
                    payment.is_failed_payment(), f"{status} should be considered failed"
                )
                self.assertFalse(
                    payment.should_keep_invoice(), f"{status} should delete invoice"
                )

        # Test intermediate statuses (neither successful nor failed)
        intermediate_statuses = [
            CryptoPayment.STATUS_PENDING,
            CryptoPayment.STATUS_RECEIVED,
            CryptoPayment.STATUS_LATEPAY_REFUNDED,
            CryptoPayment.STATUS_UNDERPAID_REFUNDED,
            CryptoPayment.STATUS_OUT_OF_STOCK_REFUNDED,
            CryptoPayment.STATUS_DOUBLEPAY_REFUNDED,
        ]

        for status in intermediate_statuses:
            payment = self._create_test_payment()
            payment.status = status

            with self.subTest(status=status):
                self.assertFalse(
                    payment.is_successful_payment(),
                    f"{status} should not be considered successful (intermediate)",
                )
                # These could be in FAILED_PAYMENT_STATUSES or not, depending on business logic
                # We'll test what should_keep_invoice returns based on the actual groups
                expected_keep = status in CryptoPayment.SUCCESSFUL_PAYMENT_STATUSES
                self.assertEqual(
                    payment.should_keep_invoice(),
                    expected_keep,
                    f"{status} invoice handling should match semantic groups",
                )

    def test_semantic_groups_are_mutually_exclusive(self):
        """Test that successful and failed payment groups don't overlap."""
        successful_set = set(CryptoPayment.SUCCESSFUL_PAYMENT_STATUSES)
        failed_set = set(CryptoPayment.FAILED_PAYMENT_STATUSES)

        # These groups should not overlap
        overlap = successful_set & failed_set
        self.assertEqual(
            overlap,
            set(),
            f"Successful and failed payment statuses should not overlap: {overlap}",
        )

    def test_terminal_statuses_logic(self):
        """Test that terminal status logic is consistent with semantic groups."""
        terminal_set = set(CryptoPayment.TERMINAL_STATUSES)
        successful_set = set(CryptoPayment.SUCCESSFUL_PAYMENT_STATUSES)
        failed_set = set(CryptoPayment.FAILED_PAYMENT_STATUSES)

        # All terminal statuses should be either successful or failed
        for status in terminal_set:
            self.assertTrue(
                status in successful_set or status in failed_set,
                f"Terminal status {status} should be in either successful or failed groups",
            )

    def test_state_machine_completeness(self):
        """Test that the state machine covers all expected scenarios."""
        payment = self._create_test_payment()

        # Test normal payment flow
        payment.status = CryptoPayment.STATUS_PENDING
        self.assertTrue(payment.is_valid_transition(CryptoPayment.STATUS_RECEIVED))

        payment.status = CryptoPayment.STATUS_RECEIVED
        self.assertTrue(payment.is_valid_transition(CryptoPayment.STATUS_CONFIRMED))

        # Test overpayment flow
        payment.status = CryptoPayment.STATUS_RECEIVED
        self.assertTrue(
            payment.is_valid_transition(CryptoPayment.STATUS_CONFIRMED_OVERPAY)
        )

        payment.status = CryptoPayment.STATUS_CONFIRMED_OVERPAY
        self.assertTrue(
            payment.is_valid_transition(CryptoPayment.STATUS_CONFIRMED_OVERPAY_REFUNDED)
        )

        # Test expiration flow - expired is now terminal (late payments create new objects)
        payment.status = CryptoPayment.STATUS_PENDING
        self.assertTrue(payment.is_valid_transition(CryptoPayment.STATUS_EXPIRED))

        # Expired is now terminal - no transitions out (late payments create new payment objects)
        payment.status = CryptoPayment.STATUS_EXPIRED
        self.assertEqual(payment.get_valid_next_statuses(), [])

        # Test duplicate payment flow
        payment.status = CryptoPayment.STATUS_RECEIVED
        self.assertTrue(
            payment.is_valid_transition(CryptoPayment.STATUS_DOUBLEPAY_REFUNDED)
        )

        payment.status = CryptoPayment.STATUS_DOUBLEPAY_REFUNDED
        self.assertTrue(
            payment.is_valid_transition(
                CryptoPayment.STATUS_DOUBLEPAY_REFUNDED_COMPLETE
            )
        )


if __name__ == "__main__":
    unittest.main()
