# -*- coding: utf-8 -*-
"""
КРИТИЧЕСКИЕ ТЕСТЫ: Переходы между статусами заказов и управление резервами

Тестируем 5 самых критичных сценариев:
1. draft → completed → cancelled → completed (танцы туда-сюда)
2. draft → cancelled (освобождение резервов при отмене из черновика)
3. cancelled → pending (резервирование при возврате из отмены)
4. Создание заказа с промежуточным статусом (не positive, не negative)
5. completed → draft (откат Sale и возврат в резервы)

Каждый тест проверяет:
- Корректность статусов резервов
- Корректность Stock (доступно, зарезервировано, свободно)
- Корректность StockBatch (количество не дублируется)
- Отсутствие дублирования Sale
- Корректность SaleBatchAllocation
"""
from django.test import TestCase
from django.db import connection
from django.contrib.auth import get_user_model
from django_tenants.utils import schema_context
from django.core.exceptions import ValidationError
from decimal import Decimal

from tenants.models import Client, Domain
from products.models import Product, ProductCategory
from inventory.models import (
    Warehouse, StockBatch, Stock, Reservation, 
    Sale, SaleBatchAllocation
)
from orders.models import Order, OrderItem, OrderStatus, PaymentMethod
from customers.models import Customer


User = get_user_model()


class OrderStatusTransitionCriticalTest(TestCase):
    """
    Критические тесты переходов между статусами заказов.
    
    TransactionTestCase используется для работы с тенантами и реальными транзакциями.
    """
    
    @classmethod
    def setUpClass(cls):
        """Создаём тестовый тенант один раз для всех тестов"""
        super().setUpClass()
        
        # Создаём тестовый тенант
        cls.tenant = Client.objects.create(
            schema_name='test_order_status',
            name='Test Order Status Tenant',
            is_active=True
        )
        
        # Создаём домен
        Domain.objects.create(
            domain='test_order_status.localhost',
            tenant=cls.tenant,
            is_primary=True
        )
    
    @classmethod
    def tearDownClass(cls):
        """Удаляем тестовый тенант после всех тестов"""
        # Удаляем схему и тенант (игнорируем все ошибки)
        try:
            with connection.cursor() as cursor:
                cursor.execute(f'DROP SCHEMA IF EXISTS {cls.tenant.schema_name} CASCADE')
        except Exception:
            pass
        
        try:
            cls.tenant.delete()
        except Exception:
            pass
        
        super().tearDownClass()
    
    def setUp(self):
        """Подготовка перед каждым тестом"""
        with schema_context('test_order_status'):
            # Создаём системные сущности
            self._create_system_entities()
            
            # Создаём тестовые данные
            self._create_test_data()
    
    def tearDown(self):
        """Очистка после каждого теста"""
        with schema_context('test_order_status'):
            # Удаляем все тестовые данные
            Sale.objects.all().delete()
            SaleBatchAllocation.objects.all().delete()
            Reservation.objects.all().delete()
            OrderItem.objects.all().delete()
            Order.objects.all().delete()
            Stock.objects.all().delete()
            StockBatch.objects.all().delete()
            Product.objects.all().delete()
    
    def _create_system_entities(self):
        """Создаёт системные сущности (статусы, способы оплаты, склад)"""
        # Очищаем старые статусы (если остались от предыдущего теста)
        OrderStatus.objects.all().delete()
        
        # Создаём статусы заказов (из реальной системы)
        self.status_draft = OrderStatus.objects.create(
            code='draft',
            name='Черновик',
            is_system=True,
            is_positive_end=False,
            is_negative_end=False,
            color='#9E9E9E',
            order=0
        )
        
        self.status_new = OrderStatus.objects.create(
            code='new',
            name='Новый',
            is_system=True,
            is_positive_end=False,
            is_negative_end=False,
            color='#2196F3',
            order=10
        )
        
        self.status_confirmed = OrderStatus.objects.create(
            code='confirmed',
            name='Подтвержден',
            is_system=True,
            is_positive_end=False,
            is_negative_end=False,
            color='#FF9800',
            order=20
        )
        
        self.status_in_assembly = OrderStatus.objects.create(
            code='in_assembly',
            name='В сборке',
            is_system=True,
            is_positive_end=False,
            is_negative_end=False,
            color='#FF9800',
            order=30
        )
        
        self.status_in_delivery = OrderStatus.objects.create(
            code='in_delivery',
            name='В доставке',
            is_system=True,
            is_positive_end=False,
            is_negative_end=False,
            color='#9C27B0',
            order=40
        )
        
        self.status_completed = OrderStatus.objects.create(
            code='completed',
            name='Выполнен',
            is_system=True,
            is_positive_end=True,
            is_negative_end=False,
            color='#4CAF50',
            order=50
        )
        
        self.status_return = OrderStatus.objects.create(
            code='return',
            name='Возврат',
            is_system=True,
            is_positive_end=False,
            is_negative_end=False,
            color='#FF5722',
            order=60
        )
        
        self.status_cancelled = OrderStatus.objects.create(
            code='cancelled',
            name='Отменен',
            is_system=True,
            is_positive_end=False,
            is_negative_end=True,
            color='#F44336',
            order=70
        )
        
        # Создаём склад (БЕЗ поля code - его нет в модели!)
        self.warehouse = Warehouse.objects.create(
            name='Основной склад'
        )
        
        # Создаём системного клиента (используем get_or_create чтобы избежать дублирования)
        self.customer, _ = Customer.objects.get_or_create(
            phone='+375291111111',
            defaults={
                'name': 'Тестовый клиент',
                'is_system_customer': False
            }
        )
    
    def _create_test_data(self):
        """Создаёт тестовые товары и партии"""
        # Категория
        category = ProductCategory.objects.create(
            name='Тестовая категория',
            is_active=True
        )
        
        # Товар (используем правильные поля: price вместо base_price, без category)
        self.product = Product.objects.create(
            name='Тестовый товар',
            sku='TEST-001',
            status='active',
            price=Decimal('10.00')  # Основная цена
        )
        # Добавляем категорию через M2M связь
        self.product.categories.add(category)
        
        # Партия товара (100 шт по 5.00 за шт)
        self.stock_batch = StockBatch.objects.create(
            product=self.product,
            warehouse=self.warehouse,
            quantity=Decimal('100.00'),
            cost_price=Decimal('5.00')
        )
        
        # Создаём Stock вручную (т.к. сигнал не сработает в тестах)
        self.stock, _ = Stock.objects.get_or_create(
            product=self.product,
            warehouse=self.warehouse
        )
        # Пересчитываем остатки из партий
        self.stock.refresh_from_batches()
    
    def _create_order(self, status, quantity=Decimal('10.00')):
        """Вспомогательный метод для создания заказа"""
        order = Order.objects.create(
            customer=self.customer,
            status=status,
            total_amount=quantity * self.product.price,
            amount_paid=Decimal('0.00')
        )
        
        OrderItem.objects.create(
            order=order,
            product=self.product,
            quantity=quantity,
            price=self.product.price  # Используем price вместо base_price
        )
        
        return order
    
    def _assert_stock_state(self, available, reserved, free, msg_prefix=""):
        """Проверяет состояние Stock"""
        self.stock.refresh_from_db()
        
        self.assertEqual(
            self.stock.quantity_available,
            available,
            f"{msg_prefix}Stock.quantity_available: ожидалось {available}, получено {self.stock.quantity_available}"
        )
        self.assertEqual(
            self.stock.quantity_reserved,
            reserved,
            f"{msg_prefix}Stock.quantity_reserved: ожидалось {reserved}, получено {self.stock.quantity_reserved}"
        )
        self.assertEqual(
            self.stock.quantity_free,
            free,
            f"{msg_prefix}Stock.quantity_free: ожидалось {free}, получено {self.stock.quantity_free}"
        )
    
    def _assert_reservation_status(self, order, expected_status, msg_prefix=""):
        """Проверяет статус резерва для заказа"""
        reservation = Reservation.objects.filter(
            order_item__order=order
        ).first()
        
        self.assertIsNotNone(reservation, f"{msg_prefix}Резерв не найден")
        self.assertEqual(
            reservation.status,
            expected_status,
            f"{msg_prefix}Reservation.status: ожидался '{expected_status}', получен '{reservation.status}'"
        )
        
        return reservation
    
    def _assert_sale_exists(self, order, should_exist=True):
        """Проверяет наличие/отсутствие Sale для заказа"""
        sale = Sale.objects.filter(order=order).first()
        
        if should_exist:
            self.assertIsNotNone(sale, "Sale должен существовать")
            return sale
        else:
            self.assertIsNone(sale, "Sale НЕ должен существовать")
            return None
    
    # ==================== ТЕСТ 1: Танцы туда-сюда ====================
    
    def test_01_draft_to_completed_to_cancelled_to_completed(self):
        """
        КРИТИЧЕСКИЙ ТЕСТ #1: Множественные переходы между статусами
        
        Сценарий:
        1. draft (резервы created)
        2. → completed (Sale created, товар списан)
        3. → cancelled (Sale откатывается, резервы released)
        4. → completed СНОВА (Sale создаётся заново, товар списан снова)
        
        Проверяем:
        - Нет дублирования Sale
        - Корректность Stock на каждом шаге
        - Корректность StockBatch (количество не дублируется)
        """
        with schema_context('test_order_status'):
            # ШАГ 1: Создаём заказ в draft
            order = self._create_order(self.status_draft, quantity=Decimal('10.00'))
            
            # Проверки после draft
            self._assert_stock_state(
                available=Decimal('100.00'),
                reserved=Decimal('10.00'),
                free=Decimal('90.00'),
                msg_prefix="[DRAFT] "
            )
            self._assert_reservation_status(order, 'reserved', "[DRAFT] ")
            self._assert_sale_exists(order, should_exist=False)
            
            # ШАГ 2: Переход в completed
            order.status = self.status_completed
            order.save()
            
            # Проверки после completed
            self._assert_stock_state(
                available=Decimal('90.00'),
                reserved=Decimal('0.00'),
                free=Decimal('90.00'),
                msg_prefix="[COMPLETED] "
            )
            self._assert_reservation_status(order, 'converted_to_sale', "[COMPLETED] ")
            sale1 = self._assert_sale_exists(order, should_exist=True)
            self.assertEqual(sale1.quantity, Decimal('10.00'))
            
            # Проверяем StockBatch
            self.stock_batch.refresh_from_db()
            self.assertEqual(
                self.stock_batch.quantity,
                Decimal('90.00'),
                "[COMPLETED] StockBatch должен уменьшиться на 10"
            )
            
            # ШАГ 3: Переход в cancelled
            order.status = self.status_cancelled
            order.save()
            
            # Проверки после cancelled
            self._assert_stock_state(
                available=Decimal('100.00'),
                reserved=Decimal('0.00'),
                free=Decimal('100.00'),
                msg_prefix="[CANCELLED] "
            )
            self._assert_reservation_status(order, 'released', "[CANCELLED] ")
            self._assert_sale_exists(order, should_exist=False)
            
            # Проверяем StockBatch восстановился
            self.stock_batch.refresh_from_db()
            self.assertEqual(
                self.stock_batch.quantity,
                Decimal('100.00'),
                "[CANCELLED] StockBatch должен восстановиться"
            )
            
            # ШАГ 4: Переход обратно в completed
            order.status = self.status_completed
            order.save()
            
            # Проверки после повторного completed
            self._assert_stock_state(
                available=Decimal('90.00'),
                reserved=Decimal('0.00'),
                free=Decimal('90.00'),
                msg_prefix="[COMPLETED AGAIN] "
            )
            self._assert_reservation_status(order, 'converted_to_sale', "[COMPLETED AGAIN] ")
            
            # КРИТИЧЕСКАЯ ПРОВЕРКА: Sale должен быть ОДИН (не дублируется)
            sales_count = Sale.objects.filter(order=order).count()
            self.assertEqual(
                sales_count,
                1,
                f"[COMPLETED AGAIN] Должен быть ОДИН Sale, найдено {sales_count}"
            )
            
            sale2 = Sale.objects.get(order=order)
            self.assertEqual(sale2.quantity, Decimal('10.00'))
            
            # Проверяем StockBatch снова уменьшился
            self.stock_batch.refresh_from_db()
            self.assertEqual(
                self.stock_batch.quantity,
                Decimal('90.00'),
                "[COMPLETED AGAIN] StockBatch должен снова уменьшиться"
            )
    
    # ==================== ТЕСТ 2: Отмена из любого статуса (кроме completed) ====================
    
    def test_02_cancel_from_any_status_releases_reservations(self):
        """
        КРИТИЧЕСКИЙ ТЕСТ #2: Отмена из любого статуса (кроме Выполнен) освобождает резервы
        
        Сценарии:
        1. new → cancelled (Новый → Отменен)
        2. confirmed → cancelled (Подтвержден → Отменен)
        3. in_assembly → cancelled (В сборке → Отменен)
        4. in_delivery → cancelled (В доставке → Отменен)
        
        Проверяем:
        - Резервы переходят в 'released' из ЛЮБОГО промежуточного статуса
        - Stock корректно обновляется
        - Товар НЕ списывается (Sale не создаётся)
        - StockBatch остаётся неизменным
        """
        with schema_context('test_order_status'):
            # СЦЕНАРИЙ 1: new → cancelled
            order1 = self._create_order(self.status_new, quantity=Decimal('10.00'))
            self._assert_stock_state(
                available=Decimal('100.00'),
                reserved=Decimal('10.00'),
                free=Decimal('90.00'),
                msg_prefix="[NEW] "
            )
            
            order1.status = self.status_cancelled
            order1.save()
            
            self._assert_stock_state(
                available=Decimal('100.00'),
                reserved=Decimal('0.00'),
                free=Decimal('100.00'),
                msg_prefix="[NEW→CANCELLED] "
            )
            self._assert_reservation_status(order1, 'released', "[NEW→CANCELLED] ")
            self._assert_sale_exists(order1, should_exist=False)
            
            # СЦЕНАРИЙ 2: confirmed → cancelled
            order2 = self._create_order(self.status_confirmed, quantity=Decimal('15.00'))
            self._assert_stock_state(
                available=Decimal('100.00'),
                reserved=Decimal('15.00'),
                free=Decimal('85.00'),
                msg_prefix="[CONFIRMED] "
            )
            
            order2.status = self.status_cancelled
            order2.save()
            
            self._assert_stock_state(
                available=Decimal('100.00'),
                reserved=Decimal('0.00'),
                free=Decimal('100.00'),
                msg_prefix="[CONFIRMED→CANCELLED] "
            )
            self._assert_reservation_status(order2, 'released', "[CONFIRMED→CANCELLED] ")
            self._assert_sale_exists(order2, should_exist=False)
            
            # СЦЕНАРИЙ 3: in_assembly → cancelled
            order3 = self._create_order(self.status_in_assembly, quantity=Decimal('20.00'))
            self._assert_stock_state(
                available=Decimal('100.00'),
                reserved=Decimal('20.00'),
                free=Decimal('80.00'),
                msg_prefix="[IN_ASSEMBLY] "
            )
            
            order3.status = self.status_cancelled
            order3.save()
            
            self._assert_stock_state(
                available=Decimal('100.00'),
                reserved=Decimal('0.00'),
                free=Decimal('100.00'),
                msg_prefix="[IN_ASSEMBLY→CANCELLED] "
            )
            self._assert_reservation_status(order3, 'released', "[IN_ASSEMBLY→CANCELLED] ")
            self._assert_sale_exists(order3, should_exist=False)
            
            # СЦЕНАРИЙ 4: in_delivery → cancelled
            order4 = self._create_order(self.status_in_delivery, quantity=Decimal('12.00'))
            self._assert_stock_state(
                available=Decimal('100.00'),
                reserved=Decimal('12.00'),
                free=Decimal('88.00'),
                msg_prefix="[IN_DELIVERY] "
            )
            
            order4.status = self.status_cancelled
            order4.save()
            
            self._assert_stock_state(
                available=Decimal('100.00'),
                reserved=Decimal('0.00'),
                free=Decimal('100.00'),
                msg_prefix="[IN_DELIVERY→CANCELLED] "
            )
            self._assert_reservation_status(order4, 'released', "[IN_DELIVERY→CANCELLED] ")
            self._assert_sale_exists(order4, should_exist=False)
            
            # ФИНАЛЬНАЯ ПРОВЕРКА: StockBatch не должен изменяться ни в одном случае
            self.stock_batch.refresh_from_db()
            self.assertEqual(
                self.stock_batch.quantity,
                Decimal('100.00'),
                "[FINAL] StockBatch НЕ должен изменяться при отмене из любого промежуточного статуса"
            )
    
    # ==================== ТЕСТ 3: cancelled → in_assembly ====================
    
    def test_03_cancelled_to_in_assembly_reserves_stock(self):
        """
        КРИТИЧЕСКИЙ ТЕСТ #3: Возврат из отмены резервирует товар
        
        Сценарий:
        1. new (резервы created)
        2. → cancelled (резервы released)
        3. → in_assembly (резервы должны вернуться в 'reserved')
        
        Проверяем:
        - Резервы переходят обратно в 'reserved'
        - Stock корректно резервирует товар
        """
        with schema_context('test_order_status'):
            # ШАГ 1: Создаём заказ и переводим в cancelled
            order = self._create_order(self.status_new, quantity=Decimal('20.00'))
            order.status = self.status_cancelled
            order.save()
            
            # Проверяем cancelled
            self._assert_stock_state(
                available=Decimal('100.00'),
                reserved=Decimal('0.00'),
                free=Decimal('100.00'),
                msg_prefix="[CANCELLED] "
            )
            self._assert_reservation_status(order, 'released', "[CANCELLED] ")
            
            # ШАГ 2: Переход в in_assembly (В сборке)
            order.status = self.status_in_assembly
            order.save()
            
            # КРИТИЧЕСКИЕ ПРОВЕРКИ
            self._assert_stock_state(
                available=Decimal('100.00'),
                reserved=Decimal('20.00'),
                free=Decimal('80.00'),
                msg_prefix="[IN_ASSEMBLY] "
            )
            self._assert_reservation_status(order, 'reserved', "[IN_ASSEMBLY] ")
            self._assert_sale_exists(order, should_exist=False)
    
    # ==================== ТЕСТ 4: Промежуточный статус ====================
    
    def test_04_create_order_with_intermediate_status(self):
        """
        КРИТИЧЕСКИЙ ТЕСТ #4: Создание заказа с промежуточным статусом
        
        Сценарий:
        1. Создаём заказ СРАЗУ со статусом "В доставке"
           (is_positive_end=False, is_negative_end=False)
        
        Проверяем:
        - Резервы создаются корректно
        - Stock резервируется
        - Sale НЕ создаётся
        """
        with schema_context('test_order_status'):
            # Создаём заказ сразу со статусом "В доставке"
            order = self._create_order(self.status_in_delivery, quantity=Decimal('12.00'))
            
            # КРИТИЧЕСКИЕ ПРОВЕРКИ
            self._assert_stock_state(
                available=Decimal('100.00'),
                reserved=Decimal('12.00'),
                free=Decimal('88.00'),
                msg_prefix="[IN_DELIVERY] "
            )
            self._assert_reservation_status(order, 'reserved', "[IN_DELIVERY] ")
            self._assert_sale_exists(order, should_exist=False)
            
            # Проверяем что можем перейти в completed
            order.status = self.status_completed
            order.save()
            
            self._assert_stock_state(
                available=Decimal('88.00'),
                reserved=Decimal('0.00'),
                free=Decimal('88.00'),
                msg_prefix="[IN_DELIVERY→COMPLETED] "
            )
            self._assert_reservation_status(order, 'converted_to_sale', "[IN_DELIVERY→COMPLETED] ")
            self._assert_sale_exists(order, should_exist=True)
    
    # ==================== ТЕСТ 5: completed → draft ====================
    
    def test_05_completed_to_draft_rollback_sale(self):
        """
        КРИТИЧЕСКИЙ ТЕСТ #5: Откат от completed к draft
        
        Сценарий:
        1. draft (резервы created)
        2. → completed (Sale created)
        3. → draft (Sale откатывается, резервы восстанавливаются)
        
        Проверяем:
        - Sale удаляется
        - Резервы возвращаются в 'reserved'
        - Stock корректно восстанавливается
        - StockBatch восстанавливается
        """
        with schema_context('test_order_status'):
            # ШАГ 1: Создаём заказ и переводим в completed
            order = self._create_order(self.status_draft, quantity=Decimal('25.00'))
            order.status = self.status_completed
            order.save()
            
            # Проверяем completed
            self._assert_stock_state(
                available=Decimal('75.00'),
                reserved=Decimal('0.00'),
                free=Decimal('75.00'),
                msg_prefix="[COMPLETED] "
            )
            self._assert_reservation_status(order, 'converted_to_sale', "[COMPLETED] ")
            self._assert_sale_exists(order, should_exist=True)
            
            self.stock_batch.refresh_from_db()
            batch_qty_after_sale = self.stock_batch.quantity
            self.assertEqual(batch_qty_after_sale, Decimal('75.00'))
            
            # ШАГ 2: Откат в draft
            order.status = self.status_draft
            order.save()
            
            # КРИТИЧЕСКИЕ ПРОВЕРКИ
            self._assert_stock_state(
                available=Decimal('100.00'),
                reserved=Decimal('25.00'),
                free=Decimal('75.00'),
                msg_prefix="[DRAFT ROLLBACK] "
            )
            self._assert_reservation_status(order, 'reserved', "[DRAFT ROLLBACK] ")
            self._assert_sale_exists(order, should_exist=False)
            
            # Проверяем восстановление StockBatch
            self.stock_batch.refresh_from_db()
            self.assertEqual(
                self.stock_batch.quantity,
                Decimal('100.00'),
                "[DRAFT ROLLBACK] StockBatch должен восстановиться до исходного значения"
            )
    
    # ==================== ТЕСТ 6: Валидация резервов (is_returned + только released) ====================
    
    def test_06a_released_reservations_items_available_allow_transition(self):
        """
        ТЕСТ #6a: Переход из CANCELLED в COMPLETED разрешён если товары доступны

        Сценарий:
        1. draft → completed (Sale созданы, резервы converted_to_sale)
        2. → cancelled (резервы released, is_returned=True, Sale удалены, товар восстановлен)
        3. Товары НЕ использованы в других заказах (quantity_free достаточно)
        4. → completed ДОЛЖНО РАБОТАТЬ ✅

        Проверяем:
        - Заказ с is_returned=True и released резервами МОЖЕТ вернуться в COMPLETED
        - Резервы корректно переходят: released → reserved → converted_to_sale
        - Sales создаются заново
        - is_returned обновляется на False
        """
        with schema_context('test_order_status'):
            # ШАГ 1: Создаём заказ и переводим в completed
            order = self._create_order(self.status_draft, quantity=Decimal('10.00'))
            order.status = self.status_completed
            order.save()

            # Проверяем состояние после первого completed
            self._assert_sale_exists(order, should_exist=True)
            self._assert_reservation_status(order, 'converted_to_sale')
            order.refresh_from_db()
            self.assertFalse(order.is_returned, "[FIRST COMPLETED] is_returned должен быть False")
            self._assert_stock_state(
                available=Decimal('90.00'),
                reserved=Decimal('0.00'),
                free=Decimal('90.00'),
                msg_prefix="[FIRST COMPLETED] "
            )

            # ШАГ 2: Переводим в cancelled
            order.status = self.status_cancelled
            order.save()

            # Проверяем состояние после cancelled
            self._assert_sale_exists(order, should_exist=False)
            self._assert_reservation_status(order, 'released', "[CANCELLED] ")
            order.refresh_from_db()
            self.assertTrue(order.is_returned, "[CANCELLED] is_returned должен быть True")

            # ВАЖНО: Проверяем что товары ДОСТУПНЫ (quantity_free = 100)
            self._assert_stock_state(
                available=Decimal('100.00'),
                reserved=Decimal('0.00'),
                free=Decimal('100.00'),
                msg_prefix="[CANCELLED - товары доступны] "
            )

            # ШАГ 3: Возвращаем в completed - ДОЛЖНО РАБОТАТЬ
            order.status = self.status_completed
            order.save()  # НЕ должно быть ValidationError

            # КРИТИЧЕСКИЕ ПРОВЕРКИ
            self._assert_sale_exists(order, should_exist=True)
            self._assert_reservation_status(order, 'converted_to_sale', "[RE-COMPLETED] ")
            order.refresh_from_db()
            self.assertFalse(
                order.is_returned,
                "[RE-COMPLETED] is_returned должен быть False после успешной продажи"
            )

            self._assert_stock_state(
                available=Decimal('90.00'),
                reserved=Decimal('0.00'),
                free=Decimal('90.00'),
                msg_prefix="[RE-COMPLETED] "
            )

            # Проверяем количество Sales (должен быть ОДИН, не дублируется)
            sales_count = Sale.objects.filter(order=order).count()
            self.assertEqual(
                sales_count, 1,
                f"[RE-COMPLETED] Должен быть ОДИН Sale, найдено {sales_count}"
            )

    def test_06b_released_reservations_items_used_block_transition(self):
        """
        ТЕСТ #6b: Переход из CANCELLED в COMPLETED блокируется если товары использованы

        Сценарий:
        1. Order A: draft → completed → cancelled (резервы released)
        2. Order B: создан и резервирует те же товары (quantity_free уменьшается)
        3. Order A: попытка вернуться в completed → ДОЛЖЕН БЛОКИРОВАТЬСЯ ❌

        Проверяем:
        - Если товары из released резервов зарезервированы другим заказом
        - ValidationError должен быть поднят
        - Сообщение об ошибке корректное
        """
        with schema_context('test_order_status'):
            # ШАГ 1: Order A - completed → cancelled
            order_a = self._create_order(self.status_draft, quantity=Decimal('50.00'))
            order_a.status = self.status_completed
            order_a.save()
            order_a.status = self.status_cancelled
            order_a.save()

            # Проверяем состояние Order A
            order_a.refresh_from_db()
            self.assertTrue(order_a.is_returned, "[Order A CANCELLED] is_returned должен быть True")
            self._assert_reservation_status(order_a, 'released', "[Order A CANCELLED] ")
            self._assert_stock_state(
                available=Decimal('100.00'),
                reserved=Decimal('0.00'),
                free=Decimal('100.00'),
                msg_prefix="[После отмены Order A] "
            )

            # ШАГ 2: Order B резервирует большую часть товаров
            order_b = self._create_order(self.status_draft, quantity=Decimal('60.00'))

            # Проверяем что Order B зарезервировал товары
            self._assert_stock_state(
                available=Decimal('100.00'),
                reserved=Decimal('60.00'),
                free=Decimal('40.00'),  # Осталось только 40, а Order A нужно 50
                msg_prefix="[После создания Order B] "
            )

            # ШАГ 3: Попытка вернуть Order A в completed - ДОЛЖНА БЛОКИРОВАТЬСЯ
            order_a.refresh_from_db()
            original_status_id = order_a.status_id
            order_a.status = self.status_completed

            # Ожидаем ValidationError при попытке сохранения
            # ВАЖНО: ValidationError в post_save сигнале откатывает транзакцию,
            # но объект в памяти остается измененным. Поэтому сохраняем ID статуса ДО изменения
            with self.assertRaises(ValidationError) as context:
                order_a.save()

            # Проверяем сообщение об ошибке
            error_message = str(context.exception)
            self.assertIn('был отменён', error_message, "Сообщение должно содержать 'был отменён'")
            self.assertIn('товары проданы', error_message, "Сообщение должно содержать 'товары проданы'")
            self.assertIn(str(order_a.order_number), error_message, "Сообщение должно содержать номер заказа")

            # Проверяем что Order A остался в cancelled (обновляем из БД!)
            order_a.refresh_from_db()
            self.assertEqual(order_a.status.code, 'cancelled', "Order A должен остаться в cancelled")
            self.assertTrue(order_a.is_returned, "is_returned Order A должен остаться True")
    
    # ==================== ТЕСТ 7: Ручное удаление Sale ====================
    
    def test_07_manual_sale_deletion_updates_flag(self):
        """
        ТЕСТ #7: Обновление is_returned при ручном удалении Sale
        
        Сценарий:
        1. completed (Sale созданы, is_returned=False)
        2. Ручное удаление Sale
        3. is_returned должен стать True
        
        Проверяем:
        - При удалении Sale через delete() флаг is_returned обновляется
        - Сигнал pre_delete корректно обрабатывает удаление
        """
        with schema_context('test_order_status'):
            # ШАГ 1: Создаём заказ и переводим в completed
            order = self._create_order(self.status_draft, quantity=Decimal('10.00'))
            order.status = self.status_completed
            order.save()
            
            # Проверяем начальное состояние
            sale = self._assert_sale_exists(order, should_exist=True)
            order.refresh_from_db()
            self.assertFalse(order.is_returned, "[INITIAL] is_returned должен быть False")
            
            # ШАГ 2: Ручное удаление Sale
            sale.delete()

            # Проверяем, что флаг обновился
            # Примечание: Order.save() теперь обернут в transaction.atomic(),
            # поэтому принудительный commit() больше не нужен и вызовет ошибку
            order.refresh_from_db()
            self.assertTrue(order.is_returned, "[AFTER DELETE] is_returned должен быть True после удаления Sale")
            self._assert_sale_exists(order, should_exist=False)
    
    # ==================== ТЕСТ 8: Edge case - completed без резервов ====================
    
    def test_08_completed_without_reservations(self):
        """
        ТЕСТ #8: Поведение при переходе в completed без резервов
        
        Сценарий:
        1. Создаём заказ без резервов (или удаляем резервы)
        2. Переход в completed
        3. Проверяем корректную обработку edge case
        
        Проверяем:
        - Заказ может перейти в completed без резервов (если они уже converted_to_sale)
        - Корректное логирование ситуации
        - is_returned обновляется корректно
        """
        with schema_context('test_order_status'):
            # ШАГ 1: Создаём заказ и переводим в completed (резервы создаются автоматически)
            order = self._create_order(self.status_draft, quantity=Decimal('10.00'))
            
            # Проверяем, что резервы созданы
            reservation = Reservation.objects.filter(order_item__order=order).first()
            self.assertIsNotNone(reservation, "[DRAFT] Резерв должен быть создан")
            self.assertEqual(reservation.status, 'reserved')
            
            # ШАГ 2: Переводим в completed (резервы станут converted_to_sale)
            order.status = self.status_completed
            order.save()
            
            # Проверяем, что резервы converted_to_sale
            reservation.refresh_from_db()
            self.assertEqual(reservation.status, 'converted_to_sale')
            self._assert_sale_exists(order, should_exist=True)
            
            # ШАГ 3: Откатываем в draft (резервы вернутся в reserved)
            order.status = self.status_draft
            order.save()
            
            reservation.refresh_from_db()
            self.assertEqual(reservation.status, 'reserved')
            self._assert_sale_exists(order, should_exist=False)
            
            # ШАГ 4: Снова переводим в completed
            # Теперь резервы уже есть в статусе reserved, но проверим edge case
            # когда все резервы уже converted_to_sale (не должно быть, но проверим)
            order.status = self.status_completed
            order.save()
            
            # Проверяем, что Sale созданы снова
            self._assert_sale_exists(order, should_exist=True)
            reservation.refresh_from_db()
            self.assertEqual(reservation.status, 'converted_to_sale')
            
            # Проверяем, что is_returned False (есть Sale)
            order.refresh_from_db()
            self.assertFalse(order.is_returned, "[COMPLETED AGAIN] is_returned должен быть False при наличии Sale")

    # ==================== ТЕСТ 9: Хаотичная смена статусов (стресс-тест) ====================

    def test_09_random_status_changes_stress_test(self):
        """
        ТЕСТ #9: Хаотичная смена статусов заказа в разные стороны

        Сценарий (симулирует реальное поведение пользователя):
        1. draft → completed (продажа)
        2. completed → draft (откат)
        3. draft → cancelled (отмена)
        4. cancelled → in_assembly (возобновление)
        5. in_assembly → completed (продажа снова)
        6. completed → cancelled (отмена выполненного)
        7. cancelled → completed (автоматический промежуточный переход через draft)
        8. completed → draft → cancelled (откат и финальная отмена)

        Проверяем на каждом шаге:
        - Консистентность резервов
        - Консистентность Stock (quantity_available, quantity_reserved, quantity_free)
        - Отсутствие дублирования Sale
        - Корректность восстановления партий
        - Корректность флага is_returned
        """
        with schema_context('test_order_status'):
            # Создаём заказ в draft
            order = self._create_order(self.status_draft, quantity=Decimal('10.00'))

            # ========== ШАГ 1: draft → completed ==========
            order.status = self.status_completed
            order.save()
            order.refresh_from_db()

            # Проверки после первой продажи
            self._assert_stock_state(
                available=Decimal('90.00'),
                reserved=Decimal('0.00'),
                free=Decimal('90.00'),
                msg_prefix="[STEP 1: draft→completed] "
            )
            self._assert_reservation_status(order, 'converted_to_sale', "[STEP 1] ")
            self._assert_sale_exists(order, should_exist=True)
            self.assertFalse(order.is_returned, "[STEP 1] is_returned должен быть False после продажи")

            # Проверяем партии
            self.stock_batch.refresh_from_db()
            self.assertEqual(
                self.stock_batch.quantity,
                Decimal('90.00'),
                "[STEP 1] Партия должна уменьшиться на 10"
            )

            # ========== ШАГ 2: completed → draft ==========
            order.status = self.status_draft
            order.save()
            order.refresh_from_db()

            # Проверки после отката
            self._assert_stock_state(
                available=Decimal('100.00'),
                reserved=Decimal('10.00'),
                free=Decimal('90.00'),
                msg_prefix="[STEP 2: completed→draft] "
            )
            self._assert_reservation_status(order, 'reserved', "[STEP 2] ")
            self._assert_sale_exists(order, should_exist=False)
            self.assertTrue(order.is_returned, "[STEP 2] is_returned должен быть True (был completed, но Sale удалены)")

            # Партии должны восстановиться
            self.stock_batch.refresh_from_db()
            self.assertEqual(
                self.stock_batch.quantity,
                Decimal('100.00'),
                "[STEP 2] Партия должна восстановиться"
            )

            # ========== ШАГ 3: draft → cancelled ==========
            order.status = self.status_cancelled
            order.save()
            order.refresh_from_db()

            # Проверки после отмены
            self._assert_stock_state(
                available=Decimal('100.00'),
                reserved=Decimal('0.00'),
                free=Decimal('100.00'),
                msg_prefix="[STEP 3: draft→cancelled] "
            )
            self._assert_reservation_status(order, 'released', "[STEP 3] ")
            self._assert_sale_exists(order, should_exist=False)
            self.assertTrue(order.is_returned, "[STEP 3] is_returned остается True")

            # ========== ШАГ 4: cancelled → in_assembly ==========
            order.status = self.status_in_assembly
            order.save()
            order.refresh_from_db()

            # Проверки после возобновления
            self._assert_stock_state(
                available=Decimal('100.00'),
                reserved=Decimal('10.00'),
                free=Decimal('90.00'),
                msg_prefix="[STEP 4: cancelled→in_assembly] "
            )
            self._assert_reservation_status(order, 'reserved', "[STEP 4] ")
            self._assert_sale_exists(order, should_exist=False)
            self.assertTrue(order.is_returned, "[STEP 4] is_returned остается True (Sale еще нет)")

            # ========== ШАГ 5: in_assembly → completed ==========
            order.status = self.status_completed
            order.save()
            order.refresh_from_db()

            # Проверки после второй продажи
            self._assert_stock_state(
                available=Decimal('90.00'),
                reserved=Decimal('0.00'),
                free=Decimal('90.00'),
                msg_prefix="[STEP 5: in_assembly→completed] "
            )
            self._assert_reservation_status(order, 'converted_to_sale', "[STEP 5] ")
            self._assert_sale_exists(order, should_exist=True)
            self.assertFalse(order.is_returned, "[STEP 5] is_returned должен стать False (Sale созданы)")

            # Проверяем, что НЕТ дублей Sale
            sales_count = Sale.objects.filter(order=order).count()
            self.assertEqual(sales_count, 1, "[STEP 5] Должна быть ровно 1 Sale (нет дублей)")

            self.stock_batch.refresh_from_db()
            self.assertEqual(
                self.stock_batch.quantity,
                Decimal('90.00'),
                "[STEP 5] Партия должна снова уменьшиться на 10"
            )

            # ========== ШАГ 6: completed → cancelled ==========
            order.status = self.status_cancelled
            order.save()
            order.refresh_from_db()

            # Проверки после отмены выполненного
            self._assert_stock_state(
                available=Decimal('100.00'),
                reserved=Decimal('0.00'),
                free=Decimal('100.00'),
                msg_prefix="[STEP 6: completed→cancelled] "
            )
            self._assert_reservation_status(order, 'released', "[STEP 6] ")
            self._assert_sale_exists(order, should_exist=False)
            self.assertTrue(order.is_returned, "[STEP 6] is_returned должен быть True (был completed, Sale удалены)")

            self.stock_batch.refresh_from_db()
            self.assertEqual(
                self.stock_batch.quantity,
                Decimal('100.00'),
                "[STEP 6] Партия должна восстановиться после отмены"
            )

            # ========== ШАГ 7: cancelled → completed (автоматический промежуточный переход) ==========
            # Должен произойти: cancelled → draft → completed
            order.status = self.status_completed
            order.save()
            order.refresh_from_db()

            # Проверки после автоматического перехода
            self._assert_stock_state(
                available=Decimal('90.00'),
                reserved=Decimal('0.00'),
                free=Decimal('90.00'),
                msg_prefix="[STEP 7: cancelled→completed auto] "
            )
            self._assert_reservation_status(order, 'converted_to_sale', "[STEP 7] ")
            self._assert_sale_exists(order, should_exist=True)
            self.assertFalse(order.is_returned, "[STEP 7] is_returned должен быть False")

            # Проверяем, что НЕТ дублей Sale после автоматического перехода
            sales_count = Sale.objects.filter(order=order).count()
            self.assertEqual(sales_count, 1, "[STEP 7] Должна быть ровно 1 Sale (нет дублей после auto-transition)")

            # ========== ШАГ 8: completed → draft → cancelled ==========
            order.status = self.status_draft
            order.save()
            order.refresh_from_db()

            # Промежуточная проверка в draft
            self._assert_stock_state(
                available=Decimal('100.00'),
                reserved=Decimal('10.00'),
                free=Decimal('90.00'),
                msg_prefix="[STEP 8a: completed→draft] "
            )
            self._assert_reservation_status(order, 'reserved', "[STEP 8a] ")
            self._assert_sale_exists(order, should_exist=False)

            # Финальная отмена
            order.status = self.status_cancelled
            order.save()
            order.refresh_from_db()

            # ========== ФИНАЛЬНЫЕ ПРОВЕРКИ ==========
            self._assert_stock_state(
                available=Decimal('100.00'),
                reserved=Decimal('0.00'),
                free=Decimal('100.00'),
                msg_prefix="[FINAL: cancelled] "
            )
            self._assert_reservation_status(order, 'released', "[FINAL] ")
            self._assert_sale_exists(order, should_exist=False)
            self.assertTrue(order.is_returned, "[FINAL] is_returned должен быть True в финале")

            # Проверяем, что все партии восстановлены до исходного состояния
            self.stock_batch.refresh_from_db()
            self.assertEqual(
                self.stock_batch.quantity,
                Decimal('100.00'),
                "[FINAL] Партия должна полностью восстановиться до начального значения"
            )

            # Проверяем, что НЕТ активных Sale
            sales = Sale.objects.filter(order=order)
            self.assertEqual(sales.count(), 0, "[FINAL] Не должно быть активных Sale после финальной отмены")

            # Проверяем общую консистентность данных
            # Все резервы должны быть в released
            reservations = Reservation.objects.filter(order_item__order=order)
            for res in reservations:
                self.assertEqual(
                    res.status,
                    'released',
                    f"[FINAL] Все резервы должны быть в 'released', но нашли {res.status}"
                )
