from __future__ import annotations

import unittest
from datetime import datetime
from decimal import Decimal
from types import SimpleNamespace
from unittest.mock import patch

from fastapi import HTTPException
from sqlalchemy import BigInteger, create_engine, func, select
from sqlalchemy.ext.compiler import compiles
from sqlalchemy.orm import Session, sessionmaker


@compiles(BigInteger, "sqlite")
def _compile_bigint_for_sqlite(type_, compiler, **kw) -> str:
    _ = type_, compiler, kw
    return "INTEGER"


import app.models.master_data  # noqa: E402,F401
import app.models.miniapp  # noqa: E402,F401
import app.models.operations  # noqa: E402,F401
import app.models.org  # noqa: E402,F401
import app.models.planning  # noqa: E402,F401
import app.models.sales  # noqa: E402,F401
from app.models.base import Base  # noqa: E402
from app.models.master_data import Bom, BomItem, Item, StockBalance, Warehouse  # noqa: E402
from app.models.miniapp import MiniAppPersonnel, MiniAppProductionReport, MiniAppProductionReportItem  # noqa: E402
from app.models.operations import CompletionReceiptItem, InventoryTxn, Process, ProcessRoute, ProcessRouteOperation, ProductionBatchLedger, ProductionBatchLedgerTxn, StockLot, WorkCenter, WorkOrder, WorkOrderMaterial, WorkOrderMaterialIssue, WorkOrderOperation  # noqa: E402
from app.models.org import Department  # noqa: E402
from app.api.routes import production as production_routes  # noqa: E402
from app.api.routes.production import _find_work_order_for_miniapp_item, create_completion_receipt, create_work_order  # noqa: E402
from app.schemas.operations import CompletionReceiptCreate, CompletionReceiptItemCreate, SelectedStockLotIssueCreate, WorkOrderCreate  # noqa: E402
from app.services.operations import issue_selected_stock_lots  # noqa: E402
from app.services.production_batch_ledger import lock_production_batch_ledger  # noqa: E402


class SelectedStockLotProductionIssueTest(unittest.TestCase):
    def setUp(self) -> None:
        engine = create_engine("sqlite+pysqlite:///:memory:", future=True)
        raw_connection = engine.raw_connection()
        raw_connection.create_function("greatest", -1, lambda *values: max(value for value in values if value is not None))
        raw_connection.close()
        Base.metadata.create_all(engine)
        self.SessionLocal = sessionmaker(bind=engine, autoflush=False, autocommit=False, future=True)
        self.db: Session = self.SessionLocal()
        self.material = Item(id=1, item_code="RM-SEL-001", item_name="选择批次原料", item_type="RAW_MATERIAL", unit_weight_kg=Decimal("1"), status="ACTIVE")
        self.product = Item(id=2, item_code="FG-SEL-001", item_name="选择批次成品", item_type="FINISHED_GOOD", unit_weight_kg=Decimal("2"), status="ACTIVE")
        self.raw_wh = Warehouse(id=1, warehouse_code="WH-RAW", warehouse_name="原材料库", warehouse_type="RAW", status="ACTIVE")
        self.finished_wh = Warehouse(id=2, warehouse_code="WH-FG", warehouse_name="成品库", warehouse_type="FINISHED", status="ACTIVE")
        self.db.add_all([self.material, self.product, self.raw_wh, self.finished_wh])
        self.db.flush()
        self.bom = Bom(id=1, bom_code="BOM-SEL-001", product_item_id=self.product.id, version_no="V1.0", is_default=1, status="ACTIVE")
        self.bom_item = BomItem(id=1, bom_id=1, seq_no=1, material_item_id=self.material.id, usage_type="WEIGHT", gross_weight_per_piece_kg=Decimal("1.5"), loss_rate=Decimal("0"))
        self.department = Department(id=1, dept_code="D-PROD", dept_name="生产部", dept_type="PRODUCTION", status="ACTIVE")
        self.process = Process(id=1, process_code="P-001", process_name="冲压", process_type="INTERNAL", status="ACTIVE")
        self.work_center = WorkCenter(id=1, center_code="WC-001", center_name="冲压", dept_id=1, center_type="INTERNAL", capacity_hours_per_day=Decimal("8"), status="ACTIVE")
        self.route = ProcessRoute(id=1, route_code="RT-SEL-001", route_name="选择批次工艺", product_item_id=self.product.id, version_no="V1.0", is_default=1, status="ACTIVE")
        self.route_op = ProcessRouteOperation(id=1, route_id=1, seq_no=1, process_id=1, operation_name="1序", work_center_id=1, std_cycle_seconds=Decimal("10"), status="ACTIVE")
        self.db.add_all([self.bom, self.bom_item, self.department, self.process, self.work_center, self.route, self.route_op])
        self.lot1 = StockLot(
            id=1,
            lot_no="YL0001",
            lot_role="INBOUND_RAW",
            item_id=self.material.id,
            warehouse_id=self.raw_wh.id,
            source_doc_type="PURCHASE_RECEIPT",
            source_doc_id=1,
            inbound_qty=Decimal("0"),
            inbound_weight_kg=Decimal("100"),
            remaining_qty=Decimal("0"),
            remaining_weight_kg=Decimal("100"),
            locked_qty=Decimal("0"),
            locked_weight_kg=Decimal("0"),
            unit_cost=Decimal("4"),
            quality_status="PASS",
            status="AVAILABLE",
        )
        self.lot2 = StockLot(
            id=2,
            lot_no="YL0002",
            lot_role="INBOUND_RAW",
            item_id=self.material.id,
            warehouse_id=self.raw_wh.id,
            source_doc_type="PURCHASE_RECEIPT",
            source_doc_id=2,
            inbound_qty=Decimal("0"),
            inbound_weight_kg=Decimal("50"),
            remaining_qty=Decimal("0"),
            remaining_weight_kg=Decimal("50"),
            locked_qty=Decimal("0"),
            locked_weight_kg=Decimal("0"),
            unit_cost=Decimal("5"),
            quality_status="PASS",
            status="AVAILABLE",
        )
        self.balance = StockBalance(
            id=1,
            item_id=self.material.id,
            warehouse_id=self.raw_wh.id,
            location_id=None,
            qty_on_hand=Decimal("0"),
            weight_on_hand_kg=Decimal("150"),
            qty_available=Decimal("0"),
            weight_available_kg=Decimal("150"),
            qty_allocated=Decimal("0"),
            weight_allocated_kg=Decimal("0"),
            avg_unit_cost=Decimal("4.3333"),
        )
        self.db.add_all([self.lot1, self.lot2, self.balance])
        self.db.commit()

    def tearDown(self) -> None:
        self.db.close()

    def _add_issued_work_order(
        self,
        *,
        work_order_id: int,
        work_order_no: str,
        product: Item,
        issue_id: int,
        material_row_id: int,
    ) -> WorkOrder:
        work_order = WorkOrder(
            id=work_order_id,
            work_order_no=work_order_no,
            work_order_type="NORMAL",
            product_item_id=product.id,
            route_id=self.route.id,
            bom_id=self.bom.id,
            planned_qty=Decimal("10"),
            released_qty=Decimal("10"),
            finished_qty=Decimal("0"),
            scrap_qty=Decimal("0"),
            status="RELEASED",
        )
        material_row = WorkOrderMaterial(
            id=material_row_id,
            work_order_id=work_order_id,
            bom_item_id=self.bom_item.id,
            material_item_id=self.material.id,
            required_qty=Decimal("0"),
            required_weight_kg=Decimal("10"),
            issued_qty=Decimal("0"),
            issued_weight_kg=Decimal("10"),
            returned_qty=Decimal("0"),
            returned_weight_kg=Decimal("0"),
            status="ISSUED",
        )
        issue = WorkOrderMaterialIssue(
            id=issue_id,
            work_order_material_id=material_row_id,
            work_order_id=work_order_id,
            material_item_id=self.material.id,
            source_lot_id=self.lot1.id,
            material_sub_batch_no=self.lot1.lot_no,
            issued_qty=Decimal("0"),
            issued_weight_kg=Decimal("10"),
            unit_cost=Decimal("4"),
            amount=Decimal("40"),
            issue_time=datetime.now(),
        )
        self.db.add_all([work_order, material_row, issue])
        return work_order

    def _add_work_order_operation(
        self,
        *,
        operation_id: int,
        work_order_id: int,
        operation_name: str = "1序",
    ) -> WorkOrderOperation:
        operation = WorkOrderOperation(
            id=operation_id,
            work_order_id=work_order_id,
            route_operation_id=self.route_op.id,
            seq_no=1,
            operation_name=operation_name,
            work_center_id=self.work_center.id,
            planned_qty=Decimal("10"),
            report_qty=Decimal("0"),
            good_qty=Decimal("0"),
            scrap_qty=Decimal("0"),
            rework_qty=Decimal("0"),
            std_labor_cost=Decimal("0"),
            std_machine_cost=Decimal("0"),
            std_outsource_cost=Decimal("0"),
            allowed_scrap_rate=Decimal("0.05"),
            is_abnormal=0,
            status="READY",
        )
        self.db.add(operation)
        return operation

    def _add_miniapp_report_item(
        self,
        *,
        item_id: int,
        raw_material_batch_no: str,
        process_name: str,
        good_qty: Decimal = Decimal("1"),
    ) -> MiniAppProductionReportItem:
        timestamp = datetime(2026, 6, 2, 8, 0, 0)
        self.db.add(
            MiniAppPersonnel(
                phone="13800000000",
                name="小程序员工",
                created_at=timestamp,
                updated_at=timestamp,
            )
        )
        self.db.add(
            MiniAppProductionReport(
                id=9000 + item_id,
                attendance_point_name="宁波嘉恒智能科技有限公司",
                session_id=8000 + item_id,
                employee_phone="13800000000",
                report_date=timestamp.date(),
                start_at=timestamp,
                end_at=timestamp.replace(hour=9),
                duration_minutes=Decimal("60"),
                break_minutes=Decimal("0"),
                effective_minutes=Decimal("60"),
                total_good_qty=good_qty,
                total_output_qty=good_qty,
                actual_beat=Decimal("0"),
                standard_beat=Decimal("0"),
                expected_workload=Decimal("0"),
                pace_rate=Decimal("0"),
                workload_rate=Decimal("0"),
                status="approved",
                submitted_at=timestamp,
                updated_at=timestamp,
            )
        )
        item = MiniAppProductionReportItem(
            id=item_id,
            report_id=9000 + item_id,
            attendance_point_name="宁波嘉恒智能科技有限公司",
            device_no="",
            project_no=self.product.item_code,
            product_name=self.product.item_name,
            material_code=self.material.item_code,
            material_name=self.material.item_name,
            raw_material_batch_no=raw_material_batch_no,
            process_name=process_name,
            stamping_method=None,
            standard_beat=Decimal("10"),
            standard_workload=Decimal("0"),
            good_qty=good_qty,
            defect_qty=Decimal("0"),
            scrap_qty=Decimal("0"),
            allocated_minutes=Decimal("60"),
            started_at=timestamp,
            created_at=timestamp,
        )
        self.db.add(item)
        return item

    def test_issue_selected_stock_lots_uses_lot_no_as_trace_key(self) -> None:
        issues = issue_selected_stock_lots(
            self.db,
            work_order_id=1,
            work_order_material_id=1,
            material_item_id=self.material.id,
            selected_lots=[
                {"source_lot_id": self.lot1.id, "issued_weight_kg": Decimal("80")},
                {"source_lot_id": self.lot2.id, "issued_weight_kg": Decimal("30")},
            ],
            operator_user_id=1,
        )

        self.assertEqual([issue.material_sub_batch_no for issue in issues], ["YL0001", "YL0002"])
        self.assertEqual(float(self.db.get(StockLot, self.lot1.id).remaining_weight_kg), 20)
        self.assertEqual(float(self.db.get(StockLot, self.lot2.id).remaining_weight_kg), 20)
        persisted = self.db.scalars(select(WorkOrderMaterialIssue).order_by(WorkOrderMaterialIssue.id)).all()
        self.assertEqual(len(persisted), 2)
        self.assertEqual(persisted[0].source_lot_id, self.lot1.id)
        self.assertEqual(persisted[1].source_lot_id, self.lot2.id)
        self.assertEqual(float(persisted[0].amount), 320)
        self.assertEqual(float(persisted[1].amount), 150)
        txns = self.db.scalars(select(InventoryTxn).order_by(InventoryTxn.id)).all()
        self.assertEqual([float(txn.amount) for txn in txns], [320, 150])

    def test_create_work_order_rejects_legacy_auto_issue_without_selected_lots(self) -> None:
        with self.assertRaises(HTTPException) as exc:
            create_work_order(
                WorkOrderCreate(
                    product_item_id=self.product.id,
                    planned_qty=10,
                    issue_weight_kg=Decimal("10"),
                    auto_issue_materials=True,
                    selected_stock_lots=[],
                ),
                context=SimpleNamespace(user=SimpleNamespace(id=1)),
                db=self.db,
            )

        self.assertEqual(exc.exception.status_code, 400)
        self.assertIn("必须选择库存批次号", exc.exception.detail)

    def test_create_work_order_updates_production_ledger_without_creating_normal_work_order(self) -> None:
        result = create_work_order(
            WorkOrderCreate(
                product_item_id=self.product.id,
                selected_stock_lots=[
                    SelectedStockLotIssueCreate(source_lot_id=self.lot1.id, material_item_id=self.material.id, issued_weight_kg=Decimal("10")),
                ],
            ),
            context=SimpleNamespace(user=SimpleNamespace(id=1)),
            db=self.db,
        )

        self.assertEqual(self.db.scalar(select(func.count(WorkOrder.id))), 0)
        ledger = self.db.scalar(select(ProductionBatchLedger).where(ProductionBatchLedger.material_lot_id == self.lot1.id))
        self.assertIsNotNone(ledger)
        self.assertEqual(result.work_order_id, ledger.id)
        self.assertEqual(result.work_order_no, self.lot1.lot_no)
        self.assertEqual(result.status, "在生产")
        self.assertEqual(float(ledger.total_issued_weight_kg), 10)
        self.assertEqual(float(ledger.outside_weight_kg), 10)
        self.assertEqual(float(self.db.get(StockLot, self.lot1.id).remaining_weight_kg), 90)
        txns = self.db.scalars(select(ProductionBatchLedgerTxn).order_by(ProductionBatchLedgerTxn.id)).all()
        self.assertEqual([row.txn_type for row in txns], ["生产出库"])
        inventory_txn = self.db.scalar(select(InventoryTxn).where(InventoryTxn.lot_id == self.lot1.id))
        self.assertIsNotNone(inventory_txn)
        self.assertEqual(inventory_txn.source_doc_type, "生产台账")
        self.assertEqual(inventory_txn.source_doc_id, ledger.id)

    def test_create_work_order_compat_no_uses_material_lot_no(self) -> None:
        class FrozenDateTime(datetime):
            @classmethod
            def now(cls, tz=None):  # noqa: ANN001
                _ = tz
                return cls(2026, 6, 7, 14, 30, 15, 143000)

        with patch.object(production_routes, "datetime", FrozenDateTime):
            result = create_work_order(
                WorkOrderCreate(
                    product_item_id=self.product.id,
                    selected_stock_lots=[
                        SelectedStockLotIssueCreate(source_lot_id=self.lot1.id, material_item_id=self.material.id, issued_weight_kg=Decimal("10")),
                    ],
                ),
                context=SimpleNamespace(user=SimpleNamespace(id=1)),
                db=self.db,
            )

        self.assertEqual(result.work_order_no, self.lot1.lot_no)
        self.assertEqual(self.db.scalar(select(func.count(WorkOrder.id))), 0)

    def test_create_work_order_accumulates_same_lot_and_product_into_production_ledger(self) -> None:
        class FrozenDateTime(datetime):
            @classmethod
            def now(cls, tz=None):  # noqa: ANN001
                _ = tz
                return cls(2026, 6, 7, 14, 30, 15, 143000)

        with patch.object(production_routes, "datetime", FrozenDateTime):
            first = create_work_order(
                WorkOrderCreate(
                    product_item_id=self.product.id,
                    selected_stock_lots=[
                        SelectedStockLotIssueCreate(source_lot_id=self.lot1.id, material_item_id=self.material.id, issued_weight_kg=Decimal("10")),
                    ],
                ),
                context=SimpleNamespace(user=SimpleNamespace(id=1)),
                db=self.db,
            )
            second = create_work_order(
                WorkOrderCreate(
                    product_item_id=self.product.id,
                    selected_stock_lots=[
                        SelectedStockLotIssueCreate(source_lot_id=self.lot1.id, material_item_id=self.material.id, issued_weight_kg=Decimal("8")),
                    ],
                ),
                context=SimpleNamespace(user=SimpleNamespace(id=1)),
                db=self.db,
            )

        self.assertEqual(first.work_order_id, second.work_order_id)
        self.assertEqual(first.work_order_no, self.lot1.lot_no)
        self.assertEqual(second.work_order_no, first.work_order_no)
        self.assertEqual(self.db.scalar(select(func.count(WorkOrder.id))), 0)
        ledger = self.db.get(ProductionBatchLedger, first.work_order_id)
        self.assertIsNotNone(ledger)
        self.assertEqual(float(ledger.total_issued_weight_kg), 18)
        self.assertEqual(float(ledger.outside_weight_kg), 18)
        issues = self.db.scalars(select(ProductionBatchLedgerTxn).where(ProductionBatchLedgerTxn.production_ledger_id == first.work_order_id).order_by(ProductionBatchLedgerTxn.id)).all()
        self.assertEqual([float(issue.weight_delta_kg) for issue in issues if issue.txn_type == "生产出库"], [10, 8])

    def test_create_work_order_reopens_locked_production_ledger_for_same_lot_and_product(self) -> None:
        class FrozenDateTime(datetime):
            @classmethod
            def now(cls, tz=None):  # noqa: ANN001
                _ = tz
                return cls(2026, 6, 7, 14, 30, 15, 143000)

        with patch.object(production_routes, "datetime", FrozenDateTime):
            result = create_work_order(
                WorkOrderCreate(
                    product_item_id=self.product.id,
                    selected_stock_lots=[
                        SelectedStockLotIssueCreate(source_lot_id=self.lot1.id, material_item_id=self.material.id, issued_weight_kg=Decimal("10")),
                    ],
                ),
                context=SimpleNamespace(user=SimpleNamespace(id=1)),
                db=self.db,
            )
            locked = lock_production_batch_ledger(
                self.db,
                production_ledger_id=result.work_order_id,
                operator_user_id=1,
                biz_time=FrozenDateTime(2026, 6, 7, 18, 0),
                remark="测试锁单",
            )
            self.assertEqual(locked.status, "锁单")
            reopened = create_work_order(
                WorkOrderCreate(
                    product_item_id=self.product.id,
                    selected_stock_lots=[
                        SelectedStockLotIssueCreate(source_lot_id=self.lot1.id, material_item_id=self.material.id, issued_weight_kg=Decimal("8")),
                    ],
                ),
                context=SimpleNamespace(user=SimpleNamespace(id=1)),
                db=self.db,
            )

        self.assertEqual(reopened.work_order_id, result.work_order_id)
        ledger = self.db.get(ProductionBatchLedger, result.work_order_id)
        self.assertEqual(ledger.status, "在生产")
        self.assertEqual(float(ledger.total_issued_weight_kg), 18)
        self.assertEqual(float(ledger.outside_weight_kg), 8)

    def test_create_work_order_rejects_multiple_selected_stock_lots(self) -> None:
        with self.assertRaises(HTTPException) as exc:
            create_work_order(
                WorkOrderCreate(
                    product_item_id=self.product.id,
                    selected_stock_lots=[
                        SelectedStockLotIssueCreate(source_lot_id=self.lot1.id, material_item_id=self.material.id, issued_weight_kg=Decimal("10")),
                        SelectedStockLotIssueCreate(source_lot_id=self.lot2.id, material_item_id=self.material.id, issued_weight_kg=Decimal("8")),
                    ],
                ),
                context=SimpleNamespace(user=SimpleNamespace(id=1)),
                db=self.db,
            )

        self.assertEqual(exc.exception.status_code, 400)
        self.assertIn("一次生产出库只能选择一个材料库存批次号", exc.exception.detail)

    def test_create_work_order_issues_only_selected_material_and_leaves_other_bom_rows_unissued(self) -> None:
        material2 = Item(id=5, item_code="RM-SEL-002", item_name="第二种原料", item_type="RAW_MATERIAL", unit_weight_kg=Decimal("1"), status="ACTIVE")
        bom_item2 = BomItem(id=2, bom_id=self.bom.id, seq_no=2, material_item_id=material2.id, usage_type="WEIGHT", gross_weight_per_piece_kg=Decimal("0.5"), loss_rate=Decimal("0"))
        self.db.add_all([material2, bom_item2])
        self.db.commit()

        result = create_work_order(
            WorkOrderCreate(
                product_item_id=self.product.id,
                selected_stock_lots=[
                    SelectedStockLotIssueCreate(source_lot_id=self.lot1.id, material_item_id=self.material.id, issued_weight_kg=Decimal("15")),
                ],
            ),
            context=SimpleNamespace(user=SimpleNamespace(id=1)),
            db=self.db,
        )

        self.assertEqual(self.db.scalar(select(func.count(WorkOrderMaterial.id))), 0)
        self.assertEqual(self.db.scalar(select(func.count(WorkOrderMaterialIssue.id))), 0)
        ledger = self.db.get(ProductionBatchLedger, result.work_order_id)
        self.assertIsNotNone(ledger)
        self.assertEqual(ledger.material_item_id, self.material.id)
        self.assertEqual(ledger.material_lot_no, self.lot1.lot_no)
        self.assertEqual(float(ledger.total_issued_weight_kg), 15)

    def test_create_work_order_issues_only_selected_non_first_bom_material(self) -> None:
        material2 = Item(id=5, item_code="RM-SEL-002", item_name="第二种原料", item_type="RAW_MATERIAL", unit_weight_kg=Decimal("1"), status="ACTIVE")
        bom_item2 = BomItem(id=2, bom_id=self.bom.id, seq_no=2, material_item_id=material2.id, usage_type="WEIGHT", gross_weight_per_piece_kg=Decimal("0.5"), loss_rate=Decimal("0"))
        lot3 = StockLot(
            id=3,
            lot_no="YL0003",
            lot_role="INBOUND_RAW",
            item_id=material2.id,
            warehouse_id=self.raw_wh.id,
            source_doc_type="PURCHASE_RECEIPT",
            source_doc_id=3,
            inbound_qty=Decimal("0"),
            inbound_weight_kg=Decimal("60"),
            remaining_qty=Decimal("0"),
            remaining_weight_kg=Decimal("60"),
            locked_qty=Decimal("0"),
            locked_weight_kg=Decimal("0"),
            unit_cost=Decimal("8"),
            quality_status="PASS",
            status="AVAILABLE",
        )
        balance2 = StockBalance(
            id=2,
            item_id=material2.id,
            warehouse_id=self.raw_wh.id,
            location_id=None,
            qty_on_hand=Decimal("0"),
            weight_on_hand_kg=Decimal("60"),
            qty_available=Decimal("0"),
            weight_available_kg=Decimal("60"),
            qty_allocated=Decimal("0"),
            weight_allocated_kg=Decimal("0"),
            avg_unit_cost=Decimal("8"),
        )
        self.db.add_all([material2, bom_item2, lot3, balance2])
        self.db.commit()

        result = create_work_order(
            WorkOrderCreate(
                product_item_id=self.product.id,
                selected_stock_lots=[
                    SelectedStockLotIssueCreate(source_lot_id=lot3.id, material_item_id=material2.id, issued_weight_kg=Decimal("12")),
                ],
            ),
            context=SimpleNamespace(user=SimpleNamespace(id=1)),
            db=self.db,
        )

        self.assertEqual(self.db.scalar(select(func.count(WorkOrderMaterial.id))), 0)
        self.assertEqual(self.db.scalar(select(func.count(WorkOrderMaterialIssue.id))), 0)
        ledger = self.db.get(ProductionBatchLedger, result.work_order_id)
        self.assertIsNotNone(ledger)
        self.assertEqual(ledger.material_item_id, material2.id)
        self.assertEqual(ledger.material_lot_no, lot3.lot_no)
        self.assertEqual(float(ledger.total_issued_weight_kg), 12)

    def test_merge_non_first_bom_material_recalculates_reference_qty_and_requirements(self) -> None:
        material2 = Item(id=5, item_code="RM-SEL-002", item_name="第二种原料", item_type="RAW_MATERIAL", unit_weight_kg=Decimal("1"), status="ACTIVE")
        bom_item2 = BomItem(id=2, bom_id=self.bom.id, seq_no=2, material_item_id=material2.id, usage_type="WEIGHT", gross_weight_per_piece_kg=Decimal("0.5"), loss_rate=Decimal("0"))
        lot3 = StockLot(
            id=3,
            lot_no="YL0003",
            lot_role="INBOUND_RAW",
            item_id=material2.id,
            warehouse_id=self.raw_wh.id,
            source_doc_type="PURCHASE_RECEIPT",
            source_doc_id=3,
            inbound_qty=Decimal("0"),
            inbound_weight_kg=Decimal("60"),
            remaining_qty=Decimal("0"),
            remaining_weight_kg=Decimal("60"),
            locked_qty=Decimal("0"),
            locked_weight_kg=Decimal("0"),
            unit_cost=Decimal("8"),
            quality_status="PASS",
            status="AVAILABLE",
        )
        balance2 = StockBalance(
            id=2,
            item_id=material2.id,
            warehouse_id=self.raw_wh.id,
            location_id=None,
            qty_on_hand=Decimal("0"),
            weight_on_hand_kg=Decimal("60"),
            qty_available=Decimal("0"),
            weight_available_kg=Decimal("60"),
            qty_allocated=Decimal("0"),
            weight_allocated_kg=Decimal("0"),
            avg_unit_cost=Decimal("8"),
        )
        self.db.add_all([material2, bom_item2, lot3, balance2])
        self.db.commit()

        class FrozenDateTime(datetime):
            @classmethod
            def now(cls, tz=None):  # noqa: ANN001
                _ = tz
                return cls(2026, 6, 7, 14, 30, 15, 143000)

        with patch.object(production_routes, "datetime", FrozenDateTime):
            first = create_work_order(
                WorkOrderCreate(
                    product_item_id=self.product.id,
                    selected_stock_lots=[
                        SelectedStockLotIssueCreate(source_lot_id=lot3.id, material_item_id=material2.id, issued_weight_kg=Decimal("12")),
                    ],
                ),
                context=SimpleNamespace(user=SimpleNamespace(id=1)),
                db=self.db,
            )
            second = create_work_order(
                WorkOrderCreate(
                    product_item_id=self.product.id,
                    selected_stock_lots=[
                        SelectedStockLotIssueCreate(source_lot_id=lot3.id, material_item_id=material2.id, issued_weight_kg=Decimal("6")),
                    ],
                ),
                context=SimpleNamespace(user=SimpleNamespace(id=1)),
                db=self.db,
            )

        self.assertEqual(second.work_order_id, first.work_order_id)
        self.assertEqual(self.db.scalar(select(func.count(WorkOrder.id))), 0)
        ledger = self.db.get(ProductionBatchLedger, first.work_order_id)
        self.assertIsNotNone(ledger)
        self.assertEqual(ledger.material_item_id, material2.id)
        self.assertEqual(float(ledger.total_issued_weight_kg), 18)
        self.assertEqual(float(ledger.outside_weight_kg), 18)

    def test_completion_receipt_allows_partial_reported_finished_qty(self) -> None:
        work_order = self._add_issued_work_order(
            work_order_id=1,
            work_order_no="历史工单-001",
            product=self.product,
            issue_id=10,
            material_row_id=10,
        )
        self.db.flush()
        material_row = self.db.get(WorkOrderMaterial, 10)
        material_row.issued_weight_kg = Decimal("90")
        material_row.required_weight_kg = Decimal("90")
        issue = self.db.get(WorkOrderMaterialIssue, 10)
        issue.issued_weight_kg = Decimal("90")
        issue.amount = Decimal("360")
        self.db.add(
            InventoryTxn(
                id=100,
                txn_no="TXN-LEGACY-001",
                txn_type="MATERIAL_ISSUE",
                item_id=self.material.id,
                warehouse_id=self.raw_wh.id,
                lot_id=self.lot1.id,
                qty_change=Decimal("0"),
                weight_change_kg=Decimal("-90"),
                unit_cost=Decimal("4"),
                amount=Decimal("360"),
                source_doc_type="WORK_ORDER",
                source_doc_id=work_order.id,
                biz_time=datetime(2026, 6, 8, 9, 0),
                operator_user_id=1,
            )
        )
        operation = self._add_work_order_operation(operation_id=1001, work_order_id=work_order.id)
        operation.report_qty = Decimal("40")
        operation.good_qty = Decimal("38")
        operation.scrap_qty = Decimal("2")
        operation.std_labor_cost = Decimal("2")
        operation.std_machine_cost = Decimal("3")
        operation.status = "IN_PROGRESS"
        self.db.add(operation)
        self.db.commit()

        receipt = create_completion_receipt(
            CompletionReceiptCreate(
                work_order_id=work_order.id,
                warehouse_id=self.finished_wh.id,
                items=[
                    CompletionReceiptItemCreate(
                        product_item_id=self.product.id,
                        receipt_qty=38,
                    )
                ],
            ),
            context=SimpleNamespace(user=SimpleNamespace(id=1)),
            db=self.db,
        )

        self.assertEqual(receipt.work_order_id, work_order.id)
        finished_lot = self.db.scalar(select(StockLot).where(StockLot.warehouse_id == self.finished_wh.id))
        self.assertIsNotNone(finished_lot)
        self.assertEqual(float(finished_lot.remaining_qty), 38)
        self.assertEqual(float(finished_lot.unit_cost), 6)
        receipt_item = self.db.scalar(select(CompletionReceiptItem).where(CompletionReceiptItem.completion_receipt_id == receipt.completion_receipt_id))
        self.assertIsNotNone(receipt_item)
        self.assertEqual(float(receipt_item.unit_cost), 6)
        finished_txn = self.db.scalar(select(InventoryTxn).where(InventoryTxn.txn_type == "FG_IN"))
        self.assertIsNotNone(finished_txn)
        self.assertEqual(float(finished_txn.unit_cost), 6)
        self.assertEqual(float(finished_txn.amount), 228)

    def test_issue_selected_stock_lots_rejects_depleted_or_overdrawn_lots(self) -> None:
        self.lot2.remaining_weight_kg = Decimal("0")
        self.lot2.status = "DEPLETED"
        self.db.commit()

        with self.assertRaises(HTTPException) as exc:
            issue_selected_stock_lots(
                self.db,
                work_order_id=1,
                work_order_material_id=1,
                material_item_id=self.material.id,
                selected_lots=[{"source_lot_id": self.lot2.id, "issued_weight_kg": Decimal("1")}],
                operator_user_id=1,
            )

        self.assertEqual(exc.exception.status_code, 400)
        self.assertIn("不可用", exc.exception.detail)

    def test_issue_selected_stock_lots_rejects_duplicate_lot_selection(self) -> None:
        with self.assertRaises(HTTPException) as exc:
            issue_selected_stock_lots(
                self.db,
                work_order_id=1,
                work_order_material_id=1,
                material_item_id=self.material.id,
                selected_lots=[
                    {"source_lot_id": self.lot1.id, "issued_weight_kg": Decimal("10")},
                    {"source_lot_id": self.lot1.id, "issued_weight_kg": Decimal("5")},
                ],
                operator_user_id=1,
            )

        self.assertEqual(exc.exception.status_code, 400)
        self.assertIn("库存批次重复选择", exc.exception.detail)

        self.lot1.remaining_weight_kg = Decimal("20")
        self.lot1.status = "LOCKED"
        self.db.commit()

        with self.assertRaises(HTTPException) as exc:
            issue_selected_stock_lots(
                self.db,
                work_order_id=1,
                work_order_material_id=1,
                material_item_id=self.material.id,
                selected_lots=[{"source_lot_id": self.lot1.id, "issued_weight_kg": Decimal("1")}],
                operator_user_id=1,
            )

        self.assertEqual(exc.exception.status_code, 400)
        self.assertIn("不可用", exc.exception.detail)

    def test_miniapp_material_lot_no_resolves_unsettled_work_order(self) -> None:
        lot_no = "YL0001"
        work_order = self._add_issued_work_order(
            work_order_id=1,
            work_order_no=f"20260607-{lot_no}-{self.product.item_name}",
            product=self.product,
            issue_id=10,
            material_row_id=10,
        )
        self.db.commit()

        matched = _find_work_order_for_miniapp_item(
            self.db,
            {
                "raw_material_batch_no": lot_no,
                "product_name": self.product.item_name,
                "project_no": self.product.item_code,
            },
        )

        self.assertEqual(matched.id, work_order.id)

    def test_miniapp_material_lot_no_ignores_settled_work_order(self) -> None:
        lot_no = "YL0001"
        self._add_issued_work_order(
            work_order_id=1,
            work_order_no=lot_no,
            product=self.product,
            issue_id=10,
            material_row_id=10,
        ).status = "SETTLED"
        self.db.commit()

        matched = _find_work_order_for_miniapp_item(
            self.db,
            {
                "raw_material_batch_no": lot_no,
                "product_name": self.product.item_name,
                "project_no": self.product.item_code,
            },
        )

        self.assertIsNone(matched)

    def test_miniapp_material_lot_no_ignores_canceled_work_order(self) -> None:
        lot_no = "YL0001"
        self._add_issued_work_order(
            work_order_id=1,
            work_order_no=f"20260607-{lot_no}-{self.product.item_name}",
            product=self.product,
            issue_id=10,
            material_row_id=10,
        ).status = "CANCELED"
        self.db.commit()

        matched = _find_work_order_for_miniapp_item(
            self.db,
            {
                "raw_material_batch_no": lot_no,
                "product_name": self.product.item_name,
                "project_no": self.product.item_code,
            },
        )

        self.assertIsNone(matched)

    def test_miniapp_lot_resolver_does_not_fallback_to_work_order_no_when_lot_is_ambiguous(self) -> None:
        other_product = Item(
            id=3,
            item_code="FG-SEL-002",
            item_name="另一个选择批次成品",
            item_type="FINISHED_GOOD",
            unit_weight_kg=Decimal("2"),
            status="ACTIVE",
        )
        direct_collision_product = Item(
            id=4,
            item_code="FG-SEL-003",
            item_name="碰撞工单成品",
            item_type="FINISHED_GOOD",
            unit_weight_kg=Decimal("2"),
            status="ACTIVE",
        )
        self.db.add_all([other_product, direct_collision_product])
        self.db.flush()
        self._add_issued_work_order(
            work_order_id=1,
            work_order_no="WO-LOT-AMB-001",
            product=self.product,
            issue_id=10,
            material_row_id=10,
        )
        other_work_order = self._add_issued_work_order(
            work_order_id=2,
            work_order_no="WO-LOT-AMB-002",
            product=other_product,
            issue_id=11,
            material_row_id=11,
        )
        self.db.add(
            WorkOrder(
                id=3,
                work_order_no=self.lot1.lot_no,
                work_order_type="NORMAL",
                product_item_id=direct_collision_product.id,
                route_id=self.route.id,
                bom_id=self.bom.id,
                planned_qty=Decimal("10"),
                released_qty=Decimal("10"),
                finished_qty=Decimal("0"),
                scrap_qty=Decimal("0"),
                status="RELEASED",
            )
        )
        self.db.commit()

        ambiguous_row = {
            "raw_material_batch_no": self.lot1.lot_no,
            "product_name": "不存在的成品",
            "project_no": "FG-NOT-MATCHED",
        }
        self.assertIsNone(_find_work_order_for_miniapp_item(self.db, ambiguous_row))

        cache = {"work_order": {}}
        matched_by_name = _find_work_order_for_miniapp_item(
            self.db,
            {
                "raw_material_batch_no": self.lot1.lot_no,
                "product_name": other_product.item_name,
                "project_no": "",
            },
            cache,
        )
        matched_by_code = _find_work_order_for_miniapp_item(
            self.db,
            {
                "raw_material_batch_no": self.lot1.lot_no,
                "product_name": "",
                "project_no": other_product.item_code,
            },
            cache,
        )
        self.assertEqual(matched_by_name.id, other_work_order.id)
        self.assertEqual(matched_by_code.id, other_work_order.id)

    def test_miniapp_jh_short_code_resolves_yearly_work_order_and_distinguishes_same_lot(self) -> None:
        first_work_order = self._add_issued_work_order(
            work_order_id=1,
            work_order_no=f"20260001-{self.product.item_name}",
            product=self.product,
            issue_id=10,
            material_row_id=10,
        )
        second_work_order = self._add_issued_work_order(
            work_order_id=2,
            work_order_no=f"20260002-{self.product.item_name}",
            product=self.product,
            issue_id=11,
            material_row_id=11,
        )
        self.db.commit()

        class FrozenDateTime(datetime):
            @classmethod
            def now(cls, tz=None):  # noqa: ANN001
                _ = tz
                return cls(2026, 6, 2, 14, 30, 15)

        with patch.object(production_routes, "datetime", FrozenDateTime):
            matched_first = _find_work_order_for_miniapp_item(
                self.db,
                {
                    "raw_material_batch_no": "JH0001",
                    "product_name": self.product.item_name,
                    "project_no": self.product.item_code,
                },
            )
            matched_second = _find_work_order_for_miniapp_item(
                self.db,
                {
                    "raw_material_batch_no": "JH0002",
                    "product_name": self.product.item_name,
                    "project_no": self.product.item_code,
                },
            )

        self.assertEqual(matched_first.id, first_work_order.id)
        self.assertEqual(matched_second.id, second_work_order.id)

    def test_miniapp_jh_short_code_does_not_resolve_settled_work_order(self) -> None:
        self._add_issued_work_order(
            work_order_id=1,
            work_order_no=f"20260001-{self.product.item_name}",
            product=self.product,
            issue_id=10,
            material_row_id=10,
        ).status = "SETTLED"
        self.db.commit()

        class FrozenDateTime(datetime):
            @classmethod
            def now(cls, tz=None):  # noqa: ANN001
                _ = tz
                return cls(2026, 6, 2, 14, 30, 15)

        with patch.object(production_routes, "datetime", FrozenDateTime):
            matched = _find_work_order_for_miniapp_item(
                self.db,
                {
                    "raw_material_batch_no": "JH0001",
                    "product_name": self.product.item_name,
                    "project_no": self.product.item_code,
                },
            )

        self.assertIsNone(matched)

    def test_miniapp_jh_short_code_does_not_resolve_canceled_work_order(self) -> None:
        self._add_issued_work_order(
            work_order_id=1,
            work_order_no=f"20260001-{self.product.item_name}",
            product=self.product,
            issue_id=10,
            material_row_id=10,
        ).status = "CANCELLED"
        self.db.commit()

        class FrozenDateTime(datetime):
            @classmethod
            def now(cls, tz=None):  # noqa: ANN001
                _ = tz
                return cls(2026, 6, 2, 14, 30, 15)

        with patch.object(production_routes, "datetime", FrozenDateTime):
            matched = _find_work_order_for_miniapp_item(
                self.db,
                {
                    "raw_material_batch_no": "JH0001",
                    "product_name": self.product.item_name,
                    "project_no": self.product.item_code,
                },
            )

        self.assertIsNone(matched)

    def test_miniapp_jh_short_code_sync_requires_exact_process_match(self) -> None:
        work_order = self._add_issued_work_order(
            work_order_id=1,
            work_order_no=f"20260001-{self.product.item_name}",
            product=self.product,
            issue_id=10,
            material_row_id=10,
        )
        self._add_work_order_operation(operation_id=101, work_order_id=work_order.id, operation_name="1序")
        item = self._add_miniapp_report_item(item_id=7001, raw_material_batch_no="JH0001", process_name="不存在的工序")
        self.db.commit()

        class FrozenDateTime(datetime):
            @classmethod
            def now(cls, tz=None):  # noqa: ANN001
                _ = tz
                return cls(2026, 6, 2, 14, 30, 15)

        cache = production_routes._new_miniapp_resolve_cache()
        with patch.object(production_routes, "datetime", FrozenDateTime):
            matched_work_order = _find_work_order_for_miniapp_item(
                self.db,
                {
                    "raw_material_batch_no": "JH0001",
                    "product_name": self.product.item_name,
                    "project_no": self.product.item_code,
                },
                cache,
            )
            synced_report = production_routes._sync_miniapp_item_to_erp(
                self.db,
                item.id,
                row=None,
                cache=cache,
                required=False,
            )

        self.assertEqual(matched_work_order.id, work_order.id)
        self.assertIsNone(synced_report)
