from django.core.exceptions import ValidationError
from django.test import TestCase
from decimal import Decimal
from unittest.mock import patch, MagicMock
import logging

from basic_info.models import Product, WareHouse, ProductUnitEnum, ProductCategory, Merchant, MerchantTypeEnum, WareHouseModeEnum
from . import models, services


class StockServicesTestCase(TestCase):
    """库存服务层测试"""

    @classmethod
    def setUpTestData(cls):
        """设置测试数据（类级别，所有测试方法共享）"""
        # 产品单位（使用枚举值）
        cls.unit_meter = ProductUnitEnum.METER
        cls.unit_kg = ProductUnitEnum.KG
        
        # 创建测试商户
        cls.merchant = Merchant.objects.create(
            name="测试商户",
            type=MerchantTypeEnum.STORE,
            area="测试地区",
            mobile="13800138000",
            contact="测试联系人"
        )
        
        # 创建产品类别
        cls.fabric_category = ProductCategory.objects.create(
            merchant=cls.merchant,
            name="布料类",
            description="各种布料产品",
            product_prefix="FAB"
        )
        
        # 创建仓库
        cls.warehouse_main = WareHouse.objects.create(
            merchant=cls.merchant,
            name="主仓库",
            location="主仓库地址"
        )
        cls.warehouse_backup = WareHouse.objects.create(
            merchant=cls.merchant,
            name="备用仓库",
            location="备用仓库地址"
        )
        
        # 创建产品
        cls.product_fabric_a = Product.objects.create(
            merchant=cls.merchant,
            category=cls.fabric_category,
            name="布料A",
            human_id="FABRIC-A-001",
            color="红色",
            spec="规格A",
            single_price_in=Decimal('100.00'),
            single_price_out=Decimal('120.00'),
            unit=cls.unit_meter
        )
        cls.product_fabric_b = Product.objects.create(
            merchant=cls.merchant,
            category=cls.fabric_category,
            name="布料B", 
            human_id="FABRIC-B-001",
            color="蓝色",
            spec="规格B",
            single_price_in=Decimal('150.00'),
            single_price_out=Decimal('180.00'),
            unit=cls.unit_kg
        )

    def setUp(self):
        """每个测试方法执行前的设置"""
        self.purchase_source_id = 1001
        
        # 创建库存变动记录（入库）
        self.stock_change_in = models.StockChangeRecord.objects.create(
            merchant=self.merchant,
            type=models.StockChangeTypeEnum.ADD,
            source_type=models.StockChangeSourceEnum.PURCHASE,
            source_id=self.purchase_source_id,
            warehouse=self.warehouse_main,
            is_finished=False
        )
        
        # 创建库存变动记录（出库）
        self.stock_change_out = models.StockChangeRecord.objects.create(
            merchant=self.merchant,
            type=models.StockChangeTypeEnum.REMOVE,
            source_type=models.StockChangeSourceEnum.SALES,
            source_id=1,
            warehouse=self.warehouse_main,
            is_finished=False
        )
        
        # 创建库存变动明细
        self.stock_detail_in = models.StockChangeDetail.objects.create(
            merchant=self.merchant,
            stock_change_record=self.stock_change_in,
            product=self.product_fabric_a,
            quantity=Decimal('100'),
            unit=self.unit_meter
        )
        
        self.stock_detail_out = models.StockChangeDetail.objects.create(
            merchant=self.merchant,
            stock_change_record=self.stock_change_out,
            product=self.product_fabric_a,
            quantity=Decimal('50'),
            unit=self.unit_meter
        )


class StockChangeRecordValidationTestCase(StockServicesTestCase):
    """验证 StockChangeRecord.clean 的来源类型校验"""

    def test_add_record_accepts_combine_source(self):
        record = models.StockChangeRecord(
            merchant=self.merchant,
            warehouse=self.warehouse_main,
            type=models.StockChangeTypeEnum.ADD,
            source_type=models.StockChangeSourceEnum.COMBINE,
        )
        try:
            record.full_clean()
        except ValidationError as exc:
            self.fail(f'合并应视为入库来源，但触发校验错误: {exc}')

    def test_remove_record_accepts_explode_source(self):
        record = models.StockChangeRecord(
            merchant=self.merchant,
            warehouse=self.warehouse_main,
            type=models.StockChangeTypeEnum.REMOVE,
            source_type=models.StockChangeSourceEnum.EXPLODE,
        )
        try:
            record.full_clean()
        except ValidationError as exc:
            self.fail(f'拆卷应视为出库来源，但触发校验错误: {exc}')

    def test_add_record_rejects_outgoing_source(self):
        record = models.StockChangeRecord(
            merchant=self.merchant,
            warehouse=self.warehouse_main,
            type=models.StockChangeTypeEnum.ADD,
            source_type=models.StockChangeSourceEnum.SALES,
        )
        with self.assertRaises(ValidationError):
            record.full_clean()

    def test_remove_record_rejects_incoming_source(self):
        record = models.StockChangeRecord(
            merchant=self.merchant,
            warehouse=self.warehouse_main,
            type=models.StockChangeTypeEnum.REMOVE,
            source_type=models.StockChangeSourceEnum.PURCHASE,
        )
        with self.assertRaises(ValidationError):
            record.full_clean()


class FindInventoryTestCase(StockServicesTestCase):
    """测试 find_inventory 函数"""
    
    def test_find_existing_inventory(self):
        """测试查找已存在的库存记录"""
        # 先创建一个库存记录
        inventory = models.Inventory.objects.create(
            merchant=self.merchant,
            product=self.product_fabric_a,
            warehouse=self.warehouse_main,
            quantity=50,
            num_of_rolls=5
        )
        
        # 测试查找
        found_inventory = services.find_inventory(
            product_id=self.product_fabric_a.id,
            warehouse_id=self.warehouse_main.id
        )
        
        self.assertIsNotNone(found_inventory)
        self.assertEqual(found_inventory.id, inventory.id)
        self.assertEqual(found_inventory.quantity, 50)
        self.assertEqual(found_inventory.num_of_rolls, 5)
    
    def test_find_nonexistent_inventory(self):
        """测试查找不存在的库存记录"""
        result = services.find_inventory(
            product_id=self.product_fabric_a.id,
            warehouse_id=self.warehouse_main.id
        )
        
        self.assertIsNone(result)
    def test_find_inventory_wrong_warehouse(self):
        """测试在错误的仓库中查找库存"""
        # 在主仓库创建库存
        models.Inventory.objects.create(
            merchant=self.merchant,
            product=self.product_fabric_a,
            warehouse=self.warehouse_main,
            quantity=50,
            num_of_rolls=5
        )
        
        # 在备用仓库查找
        result = services.find_inventory(
            product_id=self.product_fabric_a.id,
            warehouse_id=self.warehouse_backup.id
        )
        
        self.assertIsNone(result)


class CreateStockSnapshotTestCase(StockServicesTestCase):
    """测试 create_stock_snapshot 函数"""
    def setUp(self):
        super().setUp()
        # 创建库存记录
        self.inventory = models.Inventory.objects.create(
            merchant=self.merchant,
            product=self.product_fabric_a,
            warehouse=self.warehouse_main,
            quantity=150,
            num_of_rolls=15
        )
    
    def test_create_snapshot_for_incoming_stock(self):
        """测试为入库记录创建快照"""
        with patch.object(services.logger, 'info') as mock_logger:
            snapshot = services.create_stock_snapshot(
                self.stock_detail_in, 
                self.inventory
            )
        
        self.assertIsNotNone(snapshot)
        self.assertEqual(snapshot.product_id, self.product_fabric_a.id)
        self.assertEqual(snapshot.warehouse_id, self.warehouse_main.id)
        self.assertEqual(snapshot.delta, 100)  # 入库数量为正
        self.assertEqual(snapshot.quantity_before, 50)  # 150 - 100
        self.assertEqual(snapshot.quantity_after, 150)
        self.assertEqual(snapshot.num_of_rolls, self.inventory.num_of_rolls)
        self.assertEqual(snapshot.stock_change_record_id, self.stock_change_in.id)
        
        # 验证日志调用
        mock_logger.assert_called_once()
    
    def test_create_snapshot_for_outgoing_stock(self):
        """测试为出库记录创建快照"""
        snapshot = services.create_stock_snapshot(
            self.stock_detail_out,
            self.inventory
        )
        
        self.assertIsNotNone(snapshot)
        self.assertEqual(snapshot.delta, -50)  # 出库数量为负
        self.assertEqual(snapshot.quantity_before, 200)  # 150 - (-50)
        self.assertEqual(snapshot.quantity_after, 150)
        self.assertEqual(snapshot.num_of_rolls, self.inventory.num_of_rolls)
    
    def test_create_snapshot_with_invalid_detail(self):
        """测试使用无效明细记录创建快照"""
        # 测试 None 参数
        with self.assertRaises(ValueError) as cm:
            services.create_stock_snapshot(None, self.inventory)
        self.assertIn("无效的库存变动明细记录", str(cm.exception))
        
        # 测试未保存的明细记录
        unsaved_detail = models.StockChangeDetail(
            merchant=self.merchant,
            stock_change_record=self.stock_change_in,
            product=self.product_fabric_a,
            quantity=50
        )
        with self.assertRaises(ValueError):
            services.create_stock_snapshot(unsaved_detail, self.inventory)


class MakeStockChangeCompletedTestCase(StockServicesTestCase):
    """测试 make_stock_change_completed 函数"""
    
    def test_complete_invalid_record(self):
        """测试完成无效的库存变动记录"""
        result = services.make_stock_change_completed(None)
        self.assertFalse(result)
        
        # 测试未保存的记录
        unsaved_record = models.StockChangeRecord(
            merchant=self.merchant,
            type=models.StockChangeTypeEnum.ADD,
            warehouse=self.warehouse_main
        )
        result = services.make_stock_change_completed(unsaved_record)
        self.assertFalse(result)
    
    def test_complete_already_finished_record(self):
        """测试完成已经完成的记录"""
        self.stock_change_in.is_finished = True
        self.stock_change_in.save()
        
        result = services.make_stock_change_completed(self.stock_change_in)
        self.assertTrue(result)
    
    @patch('stock.services.find_inventory')
    @patch('stock.services.create_stock_snapshot')
    def test_complete_inbound_record_new_inventory(self, mock_snapshot, mock_find):
        """测试完成入库记录（新建库存）"""
        # 模拟没有找到现有库存
        mock_find.return_value = None
        mock_snapshot.return_value = MagicMock()
        
        result = services.make_stock_change_completed(self.stock_change_in)
        
        self.assertTrue(result)
        self.assertTrue(self.stock_change_in.is_finished)
        self.assertIsNotNone(self.stock_change_in.finished_at)
        
        # 验证创建了新的库存记录
        new_inventory = models.Inventory.objects.get(
            product=self.product_fabric_a,
            warehouse=self.warehouse_main
        )
        self.assertEqual(new_inventory.quantity, 100)
        self.assertEqual(new_inventory.num_of_rolls, 1)
        
        # 验证调用了相关函数
        mock_find.assert_called_once_with(
            product_id=self.product_fabric_a.id,
            warehouse_id=self.warehouse_main.id
        )

    def test_complete_inbound_record_existing_inventory(self):
        """测试完成入库记录（更新现有库存）"""
        existing_inventory = models.Inventory.objects.create(
            merchant=self.merchant,
            product=self.product_fabric_a,
            warehouse=self.warehouse_main,
            quantity=50,
            num_of_rolls=5,
        )

        with patch('stock.services.create_stock_snapshot') as mock_snapshot:
            mock_snapshot.return_value = MagicMock()
            result = services.make_stock_change_completed(self.stock_change_in)

        self.assertTrue(result)
        existing_inventory.refresh_from_db()
        self.assertEqual(existing_inventory.quantity, 150)
        self.assertEqual(existing_inventory.num_of_rolls, 6)

        self.stock_change_in.refresh_from_db()
        self.assertTrue(self.stock_change_in.is_finished)


class StockOffsetServiceTestCase(TestCase):
    """测试库存红冲逻辑"""

    def setUp(self):
        self.merchant = Merchant.objects.create(
            name='红冲商户',
            type=MerchantTypeEnum.STORE,
        )
        self.warehouse = WareHouse.objects.create(
            merchant=self.merchant,
            name='红冲仓',
            mode=WareHouseModeEnum.RESTRICT_IN,
        )
        category = ProductCategory.objects.create(
            merchant=self.merchant,
            name='红冲品类',
            product_prefix='RC',
        )
        self.product = Product.objects.create(
            merchant=self.merchant,
            category=category,
            name='红冲产品',
            human_id='RC-001',
            unit=ProductUnitEnum.METER,
        )
        self.stock_service = services.StockFlowService(merchant=self.merchant, created_by=None)

        self.source_record, _, _ = services.create_stock_change_record_with_details(
            merchant=self.merchant,
            created_by=None,
            type=models.StockChangeTypeEnum.ADD,
            warehouse_id=self.warehouse.id,
            source_type=models.StockChangeSourceEnum.PURCHASE,
            source_id=101,
            products=[{'product': self.product.id, 'quantity': [Decimal('10')]}],
        )
        services.make_stock_change_completed(self.source_record)

    def test_offset_stock_change_creates_reverse_record(self):
        offset_record, details, detail_count = self.stock_service.offset_stock_change(
            source_record_id=self.source_record.id,
            reason='测试红冲',
        )
        self.assertEqual(offset_record.type, models.StockChangeTypeEnum.REMOVE)
        self.assertEqual(offset_record.source_type, models.StockChangeSourceEnum.OFFSET)
        self.assertEqual(detail_count, 1)
        self.assertEqual(len(details), 1)

        inventory = models.Inventory.objects.get(product=self.product, warehouse=self.warehouse)
        self.assertEqual(inventory.quantity, 0)

        self.assertTrue(
            models.StockSnapshot.objects.filter(
                stock_change_record=self.source_record,
                cancelled=True,
                offset_id__isnull=False,
            ).exists()
        )

    def test_offset_cannot_repeat(self):
        self.stock_service.offset_stock_change(
            source_record_id=self.source_record.id,
            reason='第一次红冲',
        )
        with self.assertRaises(ValueError):
            self.stock_service.offset_stock_change(
                source_record_id=self.source_record.id,
                reason='再次红冲',
            )

    def test_offset_requires_finished_record(self):
        record, _, _ = services.create_stock_change_record_with_details(
            merchant=self.merchant,
            created_by=None,
            type=models.StockChangeTypeEnum.ADD,
            warehouse_id=self.warehouse.id,
            source_type=models.StockChangeSourceEnum.PURCHASE,
            source_id=202,
            products=[{'product': self.product.id, 'quantity': [Decimal('8')]}],
        )
        with self.assertRaises(ValueError):
            self.stock_service.offset_stock_change(
                source_record_id=record.id,
                reason='未完成记录红冲',
            )

    def test_offset_checks_merchant(self):
        other_merchant = Merchant.objects.create(
            name='外部商户',
            type=MerchantTypeEnum.STORE,
        )
        other_service = services.StockFlowService(merchant=other_merchant, created_by=None)
        with self.assertRaises(ValueError):
            other_service.offset_stock_change(
                source_record_id=self.source_record.id,
                reason='跨商户红冲',
            )
    
class CreateStockChangeRecordWithDetailsTestCase(StockServicesTestCase):
    """测试 create_stock_change_record_with_details 服务"""

    def setUp(self):
        super().setUp()
        self.products_payload = [
            {'product': self.product_fabric_a.id, 'quantity': [Decimal('10.00'), Decimal('20.50')]},
            {'product': self.product_fabric_b.id, 'quantity': [Decimal('5.00')]}
        ]

    def test_create_record_success(self):
        """创建库存变动记录并返回所有明细"""
        record, details, created_count = services.create_stock_change_record_with_details(
            merchant=self.merchant,
            created_by=None,
            type=models.StockChangeTypeEnum.ADD,
            warehouse_id=self.warehouse_main.id,
            source_type=models.StockChangeSourceEnum.PURCHASE,
            source_id=self.purchase_source_id,
            products=self.products_payload,
        )

        self.assertEqual(record.details.count(), 3)
        self.assertEqual(len(details), 3)
        self.assertEqual(created_count, 3)
        self.assertEqual(record.merchant, self.merchant)
        self.assertEqual(record.warehouse, self.warehouse_main)
        self.assertCountEqual(
            [Decimal('10.00'), Decimal('20.50'), Decimal('5.00')],
            [detail.quantity for detail in details]
        )

    def test_auto_complete_triggers_completion(self):
        """商户开启自动确认时会调用完成逻辑"""
        self.merchant.auto_complete_stock_change = True
        self.merchant.save()

        with patch('stock.services.make_stock_change_completed') as mock_complete:
            services.create_stock_change_record_with_details(
                merchant=self.merchant,
                created_by=None,
                type=models.StockChangeTypeEnum.ADD,
                warehouse_id=self.warehouse_main.id,
                source_type=models.StockChangeSourceEnum.PURCHASE,
                source_id=self.purchase_source_id,
                products=self.products_payload,
            )

        mock_complete.assert_called_once()

    def test_invalid_product_raises_error(self):
        """无效的产品ID会触发 ValueError"""
        products = [{'product': 99999, 'quantity': [Decimal('1.00')]}]

        with self.assertRaises(ValueError):
            services.create_stock_change_record_with_details(
                merchant=self.merchant,
                created_by=None,
                type=models.StockChangeTypeEnum.ADD,
                warehouse_id=self.warehouse_main.id,
                source_type=models.StockChangeSourceEnum.PURCHASE,
                source_id=self.purchase_source_id,
                products=products,
            )

    def test_strict_mode_requires_restrict_in(self):
        """严谨模式仅允许在严进宽出仓库中使用"""
        self.warehouse_main.mode = WareHouseModeEnum.UNRESTRICTED
        self.warehouse_main.save()

        with self.assertRaises(ValueError) as ctx:
            services.create_stock_change_record_with_details(
                merchant=self.merchant,
                created_by=None,
                type=models.StockChangeTypeEnum.ADD,
                warehouse_id=self.warehouse_main.id,
                source_type=models.StockChangeSourceEnum.PURCHASE,
                source_id=self.purchase_source_id,
                products=[{'product': self.product_fabric_a.id, 'quantity': [Decimal('10.00')]}],
            )

        self.assertIn('宽进宽出', str(ctx.exception))

    def test_restrict_in_out_incoming_behaves_like_strict(self):
        """严进严出模式的入库沿用严谨逻辑"""
        self.warehouse_main.mode = WareHouseModeEnum.RESTRICT_IN_OUT
        self.warehouse_main.save()

        record, details, created_count = services.create_stock_change_record_with_details(
            merchant=self.merchant,
            created_by=None,
            type=models.StockChangeTypeEnum.ADD,
            warehouse_id=self.warehouse_main.id,
            source_type=models.StockChangeSourceEnum.PURCHASE,
            source_id=self.purchase_source_id,
            products=[{'product': self.product_fabric_a.id, 'quantity': [Decimal('5.00'), Decimal('6.00')]}],
        )

        self.assertEqual(created_count, 2)
        self.assertEqual(record.details.count(), 2)

    def test_restrict_in_out_outgoing_consumes_detail(self):
        """严进严出模式的出库需要消耗入库明细"""
        self.warehouse_main.mode = WareHouseModeEnum.RESTRICT_IN_OUT
        self.warehouse_main.save()

        products = [{
            'product': self.product_fabric_a.id,
            'consume_with': [self.stock_detail_in.id],
        }]

        record, details, created_count = services.create_stock_change_record_with_details(
            merchant=self.merchant,
            created_by=None,
            type=models.StockChangeTypeEnum.REMOVE,
            warehouse_id=self.warehouse_main.id,
            source_type=models.StockChangeSourceEnum.SALES,
            source_id=123,
            products=products,
        )

        self.assertEqual(created_count, 1)
        detail = details[0]
        self.stock_detail_in.refresh_from_db()
        self.assertTrue(self.stock_detail_in.is_consumed)
        self.assertEqual(detail.consume_with_id, self.stock_detail_in.id)
        self.assertEqual(detail.quantity, self.stock_detail_in.quantity)

    def test_restrict_in_out_outgoing_requires_consume_ids(self):
        self.warehouse_main.mode = WareHouseModeEnum.RESTRICT_IN_OUT
        self.warehouse_main.save()

        with self.assertRaises(ValueError):
            services.create_stock_change_record_with_details(
                merchant=self.merchant,
                created_by=None,
                type=models.StockChangeTypeEnum.REMOVE,
                warehouse_id=self.warehouse_main.id,
                source_type=models.StockChangeSourceEnum.SALES,
                source_id=123,
                products=[{'product': self.product_fabric_a.id}],
            )

    def test_restrict_in_out_outgoing_rejects_consumed_detail(self):
        self.warehouse_main.mode = WareHouseModeEnum.RESTRICT_IN_OUT
        self.warehouse_main.save()

        products = [{
            'product': self.product_fabric_a.id,
            'consume_with': [self.stock_detail_in.id],
        }]

        services.create_stock_change_record_with_details(
            merchant=self.merchant,
            created_by=None,
            type=models.StockChangeTypeEnum.REMOVE,
            warehouse_id=self.warehouse_main.id,
            source_type=models.StockChangeSourceEnum.SALES,
            source_id=123,
            products=products,
        )

        with self.assertRaises(ValueError):
            services.create_stock_change_record_with_details(
                merchant=self.merchant,
                created_by=None,
                type=models.StockChangeTypeEnum.REMOVE,
                warehouse_id=self.warehouse_main.id,
                source_type=models.StockChangeSourceEnum.SALES,
                source_id=124,
                products=products,
            )

    def test_restrict_in_out_outgoing_rejects_other_warehouse_detail(self):
        self.warehouse_main.mode = WareHouseModeEnum.RESTRICT_IN_OUT
        self.warehouse_main.save()

        other_record = models.StockChangeRecord.objects.create(
            merchant=self.merchant,
            type=models.StockChangeTypeEnum.ADD,
            source_type=models.StockChangeSourceEnum.PURCHASE,
            warehouse=self.warehouse_backup,
        )
        other_detail = models.StockChangeDetail.objects.create(
            merchant=self.merchant,
            stock_change_record=other_record,
            product=self.product_fabric_a,
            quantity=Decimal('8.00'),
            unit=self.unit_meter,
        )

        products = [{
            'product': self.product_fabric_a.id,
            'consume_with': [other_detail.id],
        }]

        with self.assertRaises(ValueError):
            services.create_stock_change_record_with_details(
                merchant=self.merchant,
                created_by=None,
                type=models.StockChangeTypeEnum.REMOVE,
                warehouse_id=self.warehouse_main.id,
                source_type=models.StockChangeSourceEnum.SALES,
                source_id=125,
                products=products,
            )


class CreateStockChangeRecordRelaxedTestCase(StockServicesTestCase):
    """测试宽松模式的库存变动创建"""

    def setUp(self):
        super().setUp()
        self.warehouse_main.mode = WareHouseModeEnum.UNRESTRICTED
        self.warehouse_main.save()

    def test_relaxed_even_split(self):
        products = [
            {
                'product': self.product_fabric_a.id,
                'quantity': {'value': Decimal('100.00'), 'unit_count': Decimal('25.00')},
            }
        ]

        record, details, created_count = services.create_stock_change_record_relaxed(
            merchant=self.merchant,
            created_by=None,
            type=models.StockChangeTypeEnum.ADD,
            warehouse_id=self.warehouse_main.id,
            source_type=models.StockChangeSourceEnum.PURCHASE,
            source_id=self.purchase_source_id,
            products=products,
        )

        self.assertEqual(created_count, 4)
        self.assertEqual(record.details.count(), 4)
        self.assertListEqual(
            [detail.quantity for detail in details],
            [Decimal('25.00')] * 4,
        )

    def test_relaxed_with_remainder(self):
        products = [
            {
                'product': self.product_fabric_a.id,
                'quantity': {'value': Decimal('105.00'), 'unit_count': Decimal('30.00')},
            }
        ]

        _, details, created_count = services.create_stock_change_record_relaxed(
            merchant=self.merchant,
            created_by=None,
            type=models.StockChangeTypeEnum.ADD,
            warehouse_id=self.warehouse_main.id,
            source_type=models.StockChangeSourceEnum.PURCHASE,
            source_id=self.purchase_source_id,
            products=products,
        )

        self.assertEqual(created_count, 4)
        self.assertListEqual(
            [detail.quantity for detail in details],
            [Decimal('30.00'), Decimal('30.00'), Decimal('30.00'), Decimal('15.00')],
        )

    def test_relaxed_invalid_unit_count(self):
        products = [
            {
                'product': self.product_fabric_a.id,
                'quantity': {'value': Decimal('50.00'), 'unit_count': Decimal('0')},
            }
        ]

        with self.assertRaises(ValueError):
            services.create_stock_change_record_relaxed(
                merchant=self.merchant,
                created_by=None,
                type=models.StockChangeTypeEnum.ADD,
                warehouse_id=self.warehouse_main.id,
                source_type=models.StockChangeSourceEnum.PURCHASE,
                source_id=self.purchase_source_id,
                products=products,
            )

    def test_relaxed_auto_complete_triggers_completion(self):
        self.merchant.auto_complete_stock_change = True
        self.merchant.save()

        products = [
            {
                'product': self.product_fabric_a.id,
                'quantity': {'value': Decimal('10.00'), 'unit_count': Decimal('2.00')},
            }
        ]

        with patch('stock.services.make_stock_change_completed') as mock_complete:
            services.create_stock_change_record_relaxed(
                merchant=self.merchant,
                created_by=None,
                type=models.StockChangeTypeEnum.ADD,
                warehouse_id=self.warehouse_main.id,
                source_type=models.StockChangeSourceEnum.PURCHASE,
                source_id=self.purchase_source_id,
                products=products,
            )

        mock_complete.assert_called_once()

    def test_relaxed_mode_requires_unrestricted(self):
        self.warehouse_main.mode = WareHouseModeEnum.RESTRICT_IN
        self.warehouse_main.save()

        products = [
            {
                'product': self.product_fabric_a.id,
                'quantity': {'value': Decimal('10.00'), 'unit_count': Decimal('2.00')},
            }
        ]

        with self.assertRaises(ValueError) as ctx:
            services.create_stock_change_record_relaxed(
                merchant=self.merchant,
                created_by=None,
                type=models.StockChangeTypeEnum.ADD,
                warehouse_id=self.warehouse_main.id,
                source_type=models.StockChangeSourceEnum.PURCHASE,
                source_id=self.purchase_source_id,
                products=products,
            )

        self.assertIn('严进宽出', str(ctx.exception))

    def test_relaxed_mode_not_implemented_for_restrict_in_out(self):
        self.warehouse_main.mode = WareHouseModeEnum.RESTRICT_IN_OUT
        self.warehouse_main.save()

        products = [
            {
                'product': self.product_fabric_a.id,
                'quantity': {'value': Decimal('10.00'), 'unit_count': Decimal('2.00')},
            }
        ]

        with self.assertRaises(ValueError) as ctx:
            services.create_stock_change_record_relaxed(
                merchant=self.merchant,
                created_by=None,
                type=models.StockChangeTypeEnum.ADD,
                warehouse_id=self.warehouse_main.id,
                source_type=models.StockChangeSourceEnum.PURCHASE,
                source_id=self.purchase_source_id,
                products=products,
            )

        self.assertIn('严进严出', str(ctx.exception))
    
    def test_complete_outbound_record(self):
        """测试完成出库记录"""
        # 创建现有库存
        existing_inventory = models.Inventory.objects.create(
            merchant=self.merchant,
            product=self.product_fabric_a,
            warehouse=self.warehouse_main,
            quantity=100,
            num_of_rolls=10
        )
        
        with patch('stock.services.create_stock_snapshot') as mock_snapshot:
            mock_snapshot.return_value = MagicMock()
            
            result = services.make_stock_change_completed(self.stock_change_out)
        
        self.assertTrue(result)
        
        # 检查库存减少
        existing_inventory.refresh_from_db()
        self.assertEqual(existing_inventory.quantity, 50)  # 100 - 50
        self.assertEqual(existing_inventory.num_of_rolls, 9)  # 10 - 1
    
    def test_complete_record_with_multiple_details(self):
        """测试完成包含多个明细的记录"""
        # 添加另一个明细
        models.StockChangeDetail.objects.create(
            merchant=self.merchant,
            stock_change_record=self.stock_change_in,
            product=self.product_fabric_b,
            quantity=Decimal('200'),
            unit=self.unit_kg
        )
        
        with patch('stock.services.create_stock_snapshot') as mock_snapshot:
            mock_snapshot.return_value = MagicMock()
            
            result = services.make_stock_change_completed(self.stock_change_in)
        
        self.assertTrue(result)
        
        # 检查为两个产品都创建了库存记录
        inventory_a = models.Inventory.objects.get(
            product=self.product_fabric_a,
            warehouse=self.warehouse_main
        )
        inventory_b = models.Inventory.objects.get(
            product=self.product_fabric_b,
            warehouse=self.warehouse_main
        )
        
        self.assertEqual(inventory_a.quantity, 100)
        self.assertEqual(inventory_b.quantity, 200)
        
        # 验证快照创建了两次
        self.assertEqual(mock_snapshot.call_count, 2)
    
    @patch.object(services.logger, 'info')
    def test_logging_during_completion(self, mock_logger):
        """测试完成过程中的日志记录"""
        services.make_stock_change_completed(self.stock_change_in)
        
        # 验证日志调用
        self.assertGreater(mock_logger.call_count, 0)
        
        # 检查最后一条日志消息
        last_call_args = mock_logger.call_args_list[-1][0]
        self.assertIn('标记为已完成', last_call_args[0])


class StockServicesIntegrationTestCase(StockServicesTestCase):
    """集成测试：测试服务函数之间的协作"""
    
    def test_complete_workflow(self):
        """测试完整的库存变动工作流"""
        # 1. 确保开始时没有库存
        self.assertEqual(
            models.Inventory.objects.filter(
                product=self.product_fabric_a,
                warehouse=self.warehouse_main
            ).count(), 
            0
        )
        
        # 2. 完成入库操作
        result = services.make_stock_change_completed(self.stock_change_in)
        self.assertTrue(result)
        
        # 3. 验证库存被创建
        inventory = services.find_inventory(
            self.product_fabric_a.id,
            self.warehouse_main.id
        )
        self.assertIsNotNone(inventory)
        self.assertEqual(inventory.quantity, 100)
        
        # 4. 验证快照被创建
        snapshots = models.StockSnapshot.objects.filter(
            stock_change_record=self.stock_change_in
        )
        self.assertEqual(snapshots.count(), 1)
        snapshot = snapshots.first()
        self.assertEqual(snapshot.delta, 100)
        self.assertEqual(snapshot.quantity_before, 0)
        self.assertEqual(snapshot.quantity_after, 100)
        
        # 5. 完成出库操作
        result = services.make_stock_change_completed(self.stock_change_out)
        self.assertTrue(result)
        
        # 6. 验证库存被更新
        inventory.refresh_from_db()
        self.assertEqual(inventory.quantity, 50)  # 100 - 50
        
        # 7. 验证出库快照
        out_snapshots = models.StockSnapshot.objects.filter(
            stock_change_record=self.stock_change_out
        )
        self.assertEqual(out_snapshots.count(), 1)
        out_snapshot = out_snapshots.first()
        self.assertEqual(out_snapshot.delta, -50)
        self.assertEqual(out_snapshot.quantity_before, 100)
        self.assertEqual(out_snapshot.quantity_after, 50)


class StockFlowServiceTestCase(TestCase):
    """StockFlowService 统一出入库服务测试"""

    def setUp(self):
        self.merchant = Merchant.objects.create(
            name='统一服务商户',
            type=MerchantTypeEnum.FACTORY,
        )
        self.category = ProductCategory.objects.create(
            merchant=self.merchant,
            name='面料',
            product_prefix='FAB',
        )
        self.product = Product.objects.create(
            merchant=self.merchant,
            category=self.category,
            name='测试面料',
            human_id='FAB-0001',
            unit=ProductUnitEnum.METER,
        )
        self.strict_warehouse = WareHouse.objects.create(
            merchant=self.merchant,
            name='严谨仓',
            mode=WareHouseModeEnum.RESTRICT_IN,
        )
        self.relaxed_warehouse = WareHouse.objects.create(
            merchant=self.merchant,
            name='宽松仓',
            mode=WareHouseModeEnum.UNRESTRICTED,
        )
        self.restrict_in_out = WareHouse.objects.create(
            merchant=self.merchant,
            name='严进严出仓',
            mode=WareHouseModeEnum.RESTRICT_IN_OUT,
        )
        self.service = services.StockFlowService(merchant=self.merchant, created_by=None)

    def test_stock_in_strict_mode_uses_quantities(self):
        record, details, count = self.service.stock_in(
            warehouse_id=self.strict_warehouse.id,
            source_type=models.StockChangeSourceEnum.PURCHASE,
            source_id=10,
            items=[{'product_id': self.product.id, 'quantities': ['5', '3']}],
        )
        self.assertEqual(count, 2)
        self.assertEqual(record.warehouse_id, self.strict_warehouse.id)
        self.assertEqual(len(details), 2)

    def test_stock_in_relaxed_mode_splits_value(self):
        _, details, count = self.service.stock_in(
            warehouse_id=self.relaxed_warehouse.id,
            source_type=models.StockChangeSourceEnum.PURCHASE,
            source_id=11,
            items=[{'product_id': self.product.id, 'value': '9', 'num_of_rolls': 4}],
        )
        self.assertEqual(count, 4)
        self.assertEqual(len(details), 4)

    def test_stock_out_restrict_mode_consumes_details(self):
        _, inbound_details, _ = self.service.stock_in(
            warehouse_id=self.restrict_in_out.id,
            source_type=models.StockChangeSourceEnum.PURCHASE,
            source_id=12,
            items=[{'product_id': self.product.id, 'quantities': ['6']}],
        )
        detail_id = inbound_details[0].id
        _, outbound_details, _ = self.service.stock_out(
            warehouse_id=self.restrict_in_out.id,
            source_type=models.StockChangeSourceEnum.SALES,
            source_id=13,
            items=[{'product_id': self.product.id, 'consume_detail_ids': [detail_id]}],
        )
        inbound_details[0].refresh_from_db()
        self.assertTrue(inbound_details[0].is_consumed)
        self.assertEqual(outbound_details[0].consume_with_id, detail_id)


# 禁用测试期间的日志输出
logging.disable(logging.CRITICAL)
