from __future__ import annotations

import unittest
from datetime import datetime
from decimal import Decimal
from types import SimpleNamespace

from fastapi import HTTPException
from sqlalchemy import BigInteger, create_engine, 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.operations import (  # noqa: E402
    InventoryTxn,
    Process,
    ProcessRoute,
    ProcessRouteOperation,
    StockLot,
    WorkCenter,
    WorkOrder,
    WorkOrderMaterial,
    WorkOrderMaterialIssue,
    WorkOrderOperation,
)
from app.models.org import Department  # noqa: E402
from app.schemas.operations import (  # noqa: E402
    CompletionReceiptCreate,
    CompletionReceiptItemCreate,
    WarehouseInboundCreate,
)


class Context:
    user = SimpleNamespace(id=1)


class ProductionWorkOrderLedgerTest(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.raw_item = Item(id=1, item_code="RM-001", item_name="冷轧钢板", item_type="RAW_MATERIAL", unit_weight_kg=Decimal("1"), status="ACTIVE")
        self.product = Item(id=2, item_code="FG-001", item_name="钢制碗", item_type="FINISHED_GOOD", unit_weight_kg=Decimal("0.50"), 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.scrap_wh = Warehouse(id=3, warehouse_code="WH-SCRAP", warehouse_name="废料库", warehouse_type="SCRAP", status="ACTIVE")
        self.db.add_all([self.raw_item, self.product, self.raw_wh, self.finished_wh, self.scrap_wh])
        self.db.flush()

        self.bom = Bom(id=1, bom_code="BOM-001", product_item_id=2, version_no="V1.0", is_default=1, status="ACTIVE")
        self.bom_item = BomItem(
            id=1,
            bom_id=1,
            seq_no=1,
            material_item_id=1,
            usage_type="WEIGHT",
            net_weight_per_piece_kg=Decimal("0.50"),
            gross_weight_per_piece_kg=Decimal("0.60"),
            loss_rate=Decimal("0.1667"),
        )
        self.department = Department(id=1, dept_code="PROD", dept_name="生产部", dept_type="PRODUCTION", status="ACTIVE")
        self.process1 = Process(id=1, process_code="P1", process_name="开平", process_type="INTERNAL", status="ACTIVE")
        self.process2 = Process(id=2, process_code="P2", 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-001", route_name="钢制碗工艺", product_item_id=2, version_no="V1.0", is_default=1, status="ACTIVE")
        self.route_op1 = 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.route_op2 = ProcessRouteOperation(id=2, route_id=1, seq_no=2, process_id=2, operation_name="2序", work_center_id=1, std_cycle_seconds=Decimal("10"), status="ACTIVE")
        self.db.add_all([self.bom, self.bom_item, self.department, self.process1, self.process2, self.work_center, self.route, self.route_op1, self.route_op2])

        self.source_lot = StockLot(
            id=1,
            lot_no="JH_冷轧钢板00001_0001",
            lot_role="RAW_MATERIAL",
            item_id=1,
            warehouse_id=1,
            source_doc_type="PURCHASE_RECEIPT",
            source_doc_id=10,
            inbound_qty=Decimal("0"),
            inbound_weight_kg=Decimal("500"),
            remaining_qty=Decimal("0"),
            remaining_weight_kg=Decimal("300"),
            locked_qty=Decimal("0"),
            locked_weight_kg=Decimal("0"),
            unit_cost=Decimal("4"),
            quality_status="PASS",
            status="AVAILABLE",
        )
        self.balance = StockBalance(
            id=1,
            item_id=1,
            warehouse_id=1,
            qty_on_hand=Decimal("0"),
            weight_on_hand_kg=Decimal("300"),
            qty_available=Decimal("0"),
            weight_available_kg=Decimal("300"),
            qty_allocated=Decimal("0"),
            weight_allocated_kg=Decimal("0"),
            avg_unit_cost=Decimal("4"),
        )
        self.work_order = WorkOrder(
            id=1,
            work_order_no="WO-001",
            work_order_type="NORMAL",
            product_item_id=2,
            route_id=1,
            bom_id=1,
            planned_qty=Decimal("333"),
            released_qty=Decimal("333"),
            finished_qty=Decimal("0"),
            scrap_qty=Decimal("0"),
            status="RELEASED",
        )
        self.material_row = WorkOrderMaterial(
            id=1,
            work_order_id=1,
            bom_item_id=1,
            material_item_id=1,
            required_qty=Decimal("0"),
            required_weight_kg=Decimal("200"),
            issued_qty=Decimal("0"),
            issued_weight_kg=Decimal("200"),
            returned_qty=Decimal("0"),
            returned_weight_kg=Decimal("0"),
            status="ISSUED",
        )
        self.issue = WorkOrderMaterialIssue(
            id=1,
            work_order_material_id=1,
            work_order_id=1,
            material_item_id=1,
            source_lot_id=1,
            material_sub_batch_no="JH_冷轧钢板00001_0001",
            issued_qty=Decimal("0"),
            issued_weight_kg=Decimal("200"),
            unit_cost=Decimal("4"),
            amount=Decimal("800"),
            issue_time=datetime.now(),
        )
        self.op1 = WorkOrderOperation(
            id=1,
            work_order_id=1,
            route_operation_id=1,
            seq_no=1,
            operation_name="1序",
            work_center_id=1,
            planned_qty=Decimal("333"),
            report_qty=Decimal("210"),
            good_qty=Decimal("206"),
            scrap_qty=Decimal("4"),
            status="RUNNING",
        )
        self.op2 = WorkOrderOperation(
            id=2,
            work_order_id=1,
            route_operation_id=2,
            seq_no=2,
            operation_name="2序",
            work_center_id=1,
            planned_qty=Decimal("333"),
            report_qty=Decimal("200"),
            good_qty=Decimal("198"),
            scrap_qty=Decimal("2"),
            status="RUNNING",
        )
        self.db.add_all([self.source_lot, self.balance, self.work_order, self.material_row, self.issue, self.op1, self.op2])
        self.db.commit()

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

    def test_limit_snapshot_uses_max_report_qty_for_used_weight_and_sum_scrap_qty_for_scrap_weight(self) -> None:
        from app.services.production_work_order_ledger import get_work_order_limit_snapshot

        snapshot = get_work_order_limit_snapshot(self.db, 1)

        self.assertEqual(float(snapshot.issued_weight_kg), 200)
        self.assertEqual(float(snapshot.used_material_weight_kg), 126)
        self.assertEqual(float(snapshot.returned_material_weight_kg), 0)
        self.assertEqual(float(snapshot.max_surplus_inbound_weight_kg), 74)
        self.assertEqual(float(snapshot.final_operation_good_qty), 198)
        self.assertEqual(float(snapshot.finished_received_qty), 0)
        self.assertEqual(float(snapshot.max_finished_inbound_qty), 198)
        self.assertEqual(float(snapshot.total_operation_scrap_qty), 6)
        self.assertEqual(float(snapshot.max_scrap_inbound_weight_kg), 24.0)
        self.assertEqual(float(snapshot.tolerance_finished_inbound_qty), 227.7)
        self.assertEqual(float(snapshot.tolerance_scrap_inbound_weight_kg), 27.6)
        self.assertEqual(float(snapshot.tolerance_surplus_inbound_weight_kg), 85.1)

    def test_scrap_limit_ignores_work_order_surplus_inbound_transactions(self) -> None:
        from app.services.production_work_order_ledger import get_work_order_limit_snapshot

        self.db.add(
            InventoryTxn(
                id=1,
                txn_no="TXN-SURPLUS-001",
                txn_type="PRODUCTION_SURPLUS_IN",
                item_id=1,
                warehouse_id=1,
                lot_id=1,
                qty_change=Decimal("0"),
                weight_change_kg=Decimal("20"),
                unit_cost=Decimal("4"),
                amount=Decimal("80"),
                source_doc_type="WORK_ORDER",
                source_doc_id=1,
                biz_time=datetime.now(),
                operator_user_id=1,
                remark="生产余料入库",
            )
        )
        self.db.commit()

        snapshot = get_work_order_limit_snapshot(self.db, 1)

        self.assertEqual(float(snapshot.max_scrap_inbound_weight_kg), 24.0)

    def test_scrap_tolerance_remaining_uses_cumulative_upper_limit(self) -> None:
        from app.services.production_work_order_ledger import get_work_order_limit_snapshot

        self.db.add(
            InventoryTxn(
                id=1,
                txn_no="TXN-SCRAP-001",
                txn_type="PRODUCTION_SCRAP_IN",
                item_id=1,
                warehouse_id=3,
                lot_id=1,
                qty_change=Decimal("0"),
                weight_change_kg=Decimal("10"),
                unit_cost=Decimal("4"),
                amount=Decimal("40"),
                source_doc_type="WORK_ORDER",
                source_doc_id=1,
                biz_time=datetime.now(),
                operator_user_id=1,
                remark="生产废料入库",
            )
        )
        self.db.commit()

        snapshot = get_work_order_limit_snapshot(self.db, 1)

        self.assertEqual(float(snapshot.max_scrap_inbound_weight_kg), 14.0)
        self.assertEqual(float(snapshot.tolerance_scrap_inbound_weight_kg), 17.6)

    def test_surplus_tolerance_remaining_uses_cumulative_upper_limit(self) -> None:
        from app.services.production_work_order_ledger import get_work_order_limit_snapshot

        self.material_row.returned_weight_kg = Decimal("40")
        self.db.add(self.material_row)
        self.db.commit()

        snapshot = get_work_order_limit_snapshot(self.db, 1)

        self.assertEqual(float(snapshot.max_surplus_inbound_weight_kg), 34.0)
        self.assertEqual(float(snapshot.tolerance_surplus_inbound_weight_kg), 45.1)

    def test_weight_profile_skips_zero_gross_weight_bom_rows(self) -> None:
        from app.services.production_work_order_ledger import get_work_order_limit_snapshot

        self.db.add(
            BomItem(
                id=2,
                bom_id=1,
                seq_no=0,
                material_item_id=1,
                usage_type="WEIGHT",
                net_weight_per_piece_kg=Decimal("0"),
                gross_weight_per_piece_kg=Decimal("0"),
                loss_rate=Decimal("0"),
            )
        )
        self.db.commit()

        snapshot = get_work_order_limit_snapshot(self.db, 1)

        self.assertEqual(float(snapshot.gross_weight_per_piece_kg), 0.6)
        self.assertEqual(float(snapshot.used_material_weight_kg), 126)

    def test_ledger_rows_include_material_operation_and_limit_details(self) -> None:
        from app.services.production_work_order_ledger import get_work_order_ledger_rows

        self.work_order.remark = "生产废料已结单"
        self.db.add(self.work_order)
        self.db.commit()

        rows = get_work_order_ledger_rows(self.db, limit=20)

        self.assertEqual(len(rows), 1)
        row = rows[0]
        self.assertEqual(row.work_order_no, "WO-001")
        self.assertEqual(row.product_name, "钢制碗")
        self.assertEqual(row.remark, "生产废料已结单")
        self.assertEqual(row.material_summary, "冷轧钢板")
        self.assertEqual(row.source_lot_summary, "JH_冷轧钢板00001_0001")
        self.assertEqual(float(row.planned_qty), 333)
        self.assertEqual(float(row.issued_weight_kg), 200)
        self.assertEqual(float(row.used_material_weight_kg), 126)
        self.assertEqual(float(row.returned_material_weight_kg), 0)
        self.assertEqual(float(row.finished_received_qty), 0)
        self.assertEqual(len(row.operations), 2)
        self.assertEqual(row.operations[0].operation_name, "1序")
        self.assertEqual(float(row.operations[0].good_qty), 206)
        self.assertEqual(float(row.operations[0].scrap_qty), 4)
        self.assertEqual(float(row.limits.max_scrap_inbound_weight_kg), 24.0)

    def test_ledger_material_rows_show_returned_weight_by_source_lot(self) -> None:
        from app.services.production_work_order_ledger import get_work_order_ledger_rows

        source_lot_2 = StockLot(
            id=2,
            lot_no="JH_冷轧钢板00001_0002",
            lot_role="RAW_MATERIAL",
            item_id=1,
            warehouse_id=1,
            source_doc_type="PURCHASE_RECEIPT",
            source_doc_id=11,
            inbound_qty=Decimal("0"),
            inbound_weight_kg=Decimal("500"),
            remaining_qty=Decimal("0"),
            remaining_weight_kg=Decimal("300"),
            locked_qty=Decimal("0"),
            locked_weight_kg=Decimal("0"),
            unit_cost=Decimal("4"),
            quality_status="PASS",
            status="AVAILABLE",
        )
        self.issue.issued_weight_kg = Decimal("100")
        self.issue.amount = Decimal("400")
        issue_2 = WorkOrderMaterialIssue(
            id=2,
            work_order_material_id=1,
            work_order_id=1,
            material_item_id=1,
            source_lot_id=2,
            material_sub_batch_no="JH_冷轧钢板00001_0002",
            issued_qty=Decimal("0"),
            issued_weight_kg=Decimal("100"),
            unit_cost=Decimal("4"),
            amount=Decimal("400"),
            issue_time=datetime.now(),
        )
        self.material_row.returned_weight_kg = Decimal("138.2")
        self.db.add_all(
            [
                source_lot_2,
                issue_2,
                self.issue,
                self.material_row,
                InventoryTxn(
                    id=1,
                    txn_no="TXN-SURPLUS-001",
                    txn_type="PRODUCTION_SURPLUS_IN",
                    item_id=1,
                    warehouse_id=1,
                    lot_id=1,
                    qty_change=Decimal("0"),
                    weight_change_kg=Decimal("38.2"),
                    unit_cost=Decimal("4"),
                    amount=Decimal("152.8"),
                    source_doc_type="WORK_ORDER",
                    source_doc_id=1,
                    source_line_id=1,
                    biz_time=datetime.now(),
                    operator_user_id=1,
                    remark="生产余料入库",
                ),
                InventoryTxn(
                    id=2,
                    txn_no="TXN-SURPLUS-002",
                    txn_type="PRODUCTION_SURPLUS_IN",
                    item_id=1,
                    warehouse_id=1,
                    lot_id=2,
                    qty_change=Decimal("0"),
                    weight_change_kg=Decimal("100"),
                    unit_cost=Decimal("4"),
                    amount=Decimal("400"),
                    source_doc_type="WORK_ORDER",
                    source_doc_id=1,
                    source_line_id=2,
                    biz_time=datetime.now(),
                    operator_user_id=1,
                    remark="生产余料入库",
                ),
            ]
        )
        self.db.commit()

        rows = get_work_order_ledger_rows(self.db, limit=20)

        material_rows = sorted(rows[0].materials, key=lambda row: row.source_lot_no)
        self.assertEqual([row.source_lot_no for row in material_rows], ["JH_冷轧钢板00001_0001", "JH_冷轧钢板00001_0002"])
        self.assertEqual([float(row.returned_weight_kg) for row in material_rows], [38.2, 100.0])
        self.assertEqual(float(rows[0].returned_material_weight_kg), 138.2)

    def test_finished_inbound_accepts_15_percent_tolerance_and_rejects_above_tolerance(self) -> None:
        from app.api.routes.production import create_completion_receipt

        with self.assertRaises(HTTPException) as cm:
            create_completion_receipt(
                CompletionReceiptCreate(
                    work_order_id=1,
                    warehouse_id=2,
                    receiver_employee_id=None,
                    items=[CompletionReceiptItemCreate(product_item_id=2, receipt_qty=227.701, receipt_weight_kg=0)],
                ),
                context=Context(),
                db=self.db,
            )
        self.assertIn("超过允许入库上限", cm.exception.detail)
        self.assertEqual(cm.exception.status_code, 400)

        accepted = create_completion_receipt(
            CompletionReceiptCreate(
                work_order_id=1,
                warehouse_id=2,
                receiver_employee_id=None,
                items=[CompletionReceiptItemCreate(product_item_id=2, receipt_qty=227.7, receipt_weight_kg=0)],
            ),
            context=Context(),
            db=self.db,
        )

        self.assertEqual(float(accepted.total_receipt_qty), 227.7)

    def test_finished_inbound_tolerance_is_validated_against_total_receipt_qty(self) -> None:
        from app.api.routes.production import create_completion_receipt

        accepted = create_completion_receipt(
            CompletionReceiptCreate(
                work_order_id=1,
                warehouse_id=2,
                receiver_employee_id=None,
                items=[
                    CompletionReceiptItemCreate(product_item_id=2, receipt_qty=100, receipt_weight_kg=0),
                    CompletionReceiptItemCreate(product_item_id=2, receipt_qty=127.7, receipt_weight_kg=0),
                ],
            ),
            context=Context(),
            db=self.db,
        )

        self.assertEqual(float(accepted.total_receipt_qty), 227.7)
        self.assertEqual(accepted.line_count, 2)

    def test_finished_settle_keeps_scrap_and_surplus_entries_available_until_each_is_settled(self) -> None:
        from app.api.routes.inventory import create_warehouse_inbound
        from app.api.routes.production import create_completion_receipt

        create_completion_receipt(
            CompletionReceiptCreate(
                work_order_id=1,
                warehouse_id=2,
                receiver_employee_id=None,
                items=[CompletionReceiptItemCreate(product_item_id=2, receipt_qty=198, receipt_weight_kg=0)],
                settle_work_order=True,
            ),
            context=Context(),
            db=self.db,
        )

        self.db.refresh(self.work_order)
        self.assertEqual(self.work_order.status, "SETTLED")

        scrap_row = create_warehouse_inbound(
            WarehouseInboundCreate(
                biz_type="PRODUCTION_SCRAP_IN",
                item_id=1,
                warehouse_id=3,
                inbound_weight_kg=1,
                source_doc_type="WORK_ORDER",
                source_doc_id=1,
                remark="生产废料补录",
            ),
            context=Context(),
            db=self.db,
        )
        self.assertEqual(scrap_row.source_doc_id, 1)

        surplus_row = create_warehouse_inbound(
            WarehouseInboundCreate(
                biz_type="PRODUCTION_SURPLUS",
                item_id=1,
                warehouse_id=1,
                source_lot_id=1,
                inbound_weight_kg=1,
                source_doc_type="WORK_ORDER",
                source_doc_id=1,
                remark="生产余料补录",
            ),
            context=Context(),
            db=self.db,
        )
        self.assertEqual(surplus_row.lot_id, 1)

        create_warehouse_inbound(
            WarehouseInboundCreate(
                biz_type="PRODUCTION_SCRAP_IN",
                item_id=1,
                warehouse_id=3,
                inbound_weight_kg=23,
                source_doc_type="WORK_ORDER",
                source_doc_id=1,
                remark="生产废料结单",
                settle_work_order=True,
            ),
            context=Context(),
            db=self.db,
        )

        self.db.refresh(self.work_order)
        self.assertEqual(self.work_order.status, "SETTLED")
        self.assertIn("生产废料已结单", self.work_order.remark)

        with self.assertRaises(HTTPException) as scrap_exc:
            create_warehouse_inbound(
                WarehouseInboundCreate(
                    biz_type="PRODUCTION_SCRAP_IN",
                    item_id=1,
                    warehouse_id=3,
                    inbound_weight_kg=0.1,
                    source_doc_type="WORK_ORDER",
                    source_doc_id=1,
                    remark="生产废料再次补录",
                ),
                context=Context(),
                db=self.db,
            )
        self.assertIn("生产废料入库选择的生产工单已完成本项结单", scrap_exc.exception.detail)

        create_warehouse_inbound(
            WarehouseInboundCreate(
                biz_type="PRODUCTION_SURPLUS",
                item_id=1,
                warehouse_id=1,
                source_lot_id=1,
                inbound_weight_kg=73,
                source_doc_type="WORK_ORDER",
                source_doc_id=1,
                remark="生产余料结单",
                settle_work_order=True,
            ),
            context=Context(),
            db=self.db,
        )

        self.db.refresh(self.work_order)
        self.assertEqual(self.work_order.status, "SETTLED")
        self.assertIn("生产废料已结单", self.work_order.remark)
        self.assertIn("生产余料已结单", self.work_order.remark)

        with self.assertRaises(HTTPException) as surplus_exc:
            create_warehouse_inbound(
                WarehouseInboundCreate(
                    biz_type="PRODUCTION_SURPLUS",
                    item_id=1,
                    warehouse_id=1,
                    source_lot_id=1,
                    inbound_weight_kg=0.1,
                    source_doc_type="WORK_ORDER",
                    source_doc_id=1,
                    remark="生产余料再次补录",
                ),
                context=Context(),
                db=self.db,
            )
        self.assertIn("生产余料入库选择的生产工单已完成本项结单", surplus_exc.exception.detail)

    def test_production_scrap_inbound_requires_work_order_and_respects_tolerance(self) -> None:
        from app.api.routes.inventory import create_warehouse_inbound

        row = create_warehouse_inbound(
            WarehouseInboundCreate(
                biz_type="PRODUCTION_SCRAP_IN",
                item_id=1,
                warehouse_id=3,
                inbound_weight_kg=27.6,
                source_doc_type="WORK_ORDER",
                source_doc_id=1,
                source_material_sub_batch_no="JH_冷轧钢板00001_0001",
                source_material_summary="WO-001生产废料",
                remark="生产废料入库",
            ),
            context=Context(),
            db=self.db,
        )

        lot = self.db.get(StockLot, row.lot_id)
        txn = self.db.scalar(select(InventoryTxn).where(InventoryTxn.lot_id == row.lot_id))
        self.assertEqual(lot.source_doc_type, "WORK_ORDER")
        self.assertEqual(lot.source_doc_id, 1)
        self.assertEqual(txn.source_doc_type, "WORK_ORDER")
        self.assertEqual(txn.source_doc_id, 1)

        with self.assertRaises(HTTPException) as cm:
            create_warehouse_inbound(
                WarehouseInboundCreate(
                    biz_type="PRODUCTION_SCRAP_IN",
                    item_id=1,
                    warehouse_id=3,
                    inbound_weight_kg=0.001,
                    source_doc_type="WORK_ORDER",
                    source_doc_id=1,
                    remark="再次超额入库",
                ),
                context=Context(),
                db=self.db,
            )
        self.assertIn("超过允许入库上限", cm.exception.detail)
        self.assertEqual(cm.exception.status_code, 400)

    def test_production_scrap_non_settle_uses_cumulative_tolerance_upper_limit(self) -> None:
        from app.api.routes.inventory import create_warehouse_inbound

        self.db.add(
            InventoryTxn(
                id=1,
                txn_no="TXN-SCRAP-001",
                txn_type="PRODUCTION_SCRAP_IN",
                item_id=1,
                warehouse_id=3,
                lot_id=1,
                qty_change=Decimal("0"),
                weight_change_kg=Decimal("10"),
                unit_cost=Decimal("4"),
                amount=Decimal("40"),
                source_doc_type="WORK_ORDER",
                source_doc_id=1,
                biz_time=datetime.now(),
                operator_user_id=1,
                remark="生产废料入库",
            )
        )
        self.db.commit()

        row = create_warehouse_inbound(
            WarehouseInboundCreate(
                biz_type="PRODUCTION_SCRAP_IN",
                item_id=1,
                warehouse_id=3,
                inbound_weight_kg=17.6,
                source_doc_type="WORK_ORDER",
                source_doc_id=1,
                remark="生产废料继续入库",
            ),
            context=Context(),
            db=self.db,
        )

        self.assertEqual(row.source_doc_type, "WORK_ORDER")
        self.assertEqual(row.source_doc_id, 1)

    def test_production_scrap_non_settle_rejects_when_cumulative_exceeds_tolerance_upper_limit(self) -> None:
        from app.api.routes.inventory import create_warehouse_inbound

        self.db.add(
            InventoryTxn(
                id=1,
                txn_no="TXN-SCRAP-001",
                txn_type="PRODUCTION_SCRAP_IN",
                item_id=1,
                warehouse_id=3,
                lot_id=1,
                qty_change=Decimal("0"),
                weight_change_kg=Decimal("10"),
                unit_cost=Decimal("4"),
                amount=Decimal("40"),
                source_doc_type="WORK_ORDER",
                source_doc_id=1,
                biz_time=datetime.now(),
                operator_user_id=1,
                remark="生产废料入库",
            )
        )
        self.db.commit()

        with self.assertRaises(HTTPException) as cm:
            create_warehouse_inbound(
                WarehouseInboundCreate(
                    biz_type="PRODUCTION_SCRAP_IN",
                    item_id=1,
                    warehouse_id=3,
                    inbound_weight_kg=17.601,
                    source_doc_type="WORK_ORDER",
                    source_doc_id=1,
                    remark="生产废料继续入库",
                ),
                context=Context(),
                db=self.db,
            )

        self.assertEqual(cm.exception.status_code, 400)
        self.assertIn("累计入库废料重量超过允许入库上限", cm.exception.detail)

    def test_production_scrap_settle_marks_scrap_done_but_keeps_work_order_open(self) -> None:
        from app.api.routes.inventory import create_warehouse_inbound

        create_warehouse_inbound(
            WarehouseInboundCreate(
                biz_type="PRODUCTION_SCRAP_IN",
                item_id=1,
                warehouse_id=3,
                inbound_weight_kg=27.6,
                source_doc_type="WORK_ORDER",
                source_doc_id=1,
                remark="生产废料入库",
                settle_work_order=True,
            ),
            context=Context(),
            db=self.db,
        )

        self.db.refresh(self.work_order)
        self.assertNotEqual(self.work_order.status, "SETTLED")
        self.assertIn("生产废料已结单", self.work_order.remark)

    def test_production_scrap_settle_does_not_block_later_surplus_settle(self) -> None:
        from app.api.routes.inventory import create_warehouse_inbound

        create_warehouse_inbound(
            WarehouseInboundCreate(
                biz_type="PRODUCTION_SCRAP_IN",
                item_id=1,
                warehouse_id=3,
                inbound_weight_kg=27.6,
                source_doc_type="WORK_ORDER",
                source_doc_id=1,
                remark="生产废料入库",
                settle_work_order=True,
            ),
            context=Context(),
            db=self.db,
        )

        self.db.refresh(self.work_order)
        self.assertNotEqual(self.work_order.status, "SETTLED")

        create_warehouse_inbound(
            WarehouseInboundCreate(
                biz_type="PRODUCTION_SURPLUS",
                item_id=1,
                warehouse_id=1,
                source_lot_id=1,
                inbound_weight_kg=85.1,
                source_doc_type="WORK_ORDER",
                source_doc_id=1,
                remark="生产余料归还",
                settle_work_order=True,
            ),
            context=Context(),
            db=self.db,
        )

        self.db.refresh(self.work_order)
        self.assertEqual(self.work_order.status, "SETTLED")

    def test_production_scrap_settle_requires_remark_when_total_is_outside_tolerance(self) -> None:
        from app.api.routes.inventory import create_warehouse_inbound

        with self.assertRaises(HTTPException) as cm:
            create_warehouse_inbound(
                WarehouseInboundCreate(
                    biz_type="PRODUCTION_SCRAP_IN",
                    item_id=1,
                    warehouse_id=3,
                    inbound_weight_kg=20.399,
                    source_doc_type="WORK_ORDER",
                    source_doc_id=1,
                    remark="生产废料入库",
                    settle_work_order=True,
                ),
                context=Context(),
                db=self.db,
            )

        self.assertEqual(cm.exception.status_code, 400)
        self.assertIn("填写结单说明", cm.exception.detail)

    def test_production_surplus_returns_to_original_lot_and_updates_work_order_returned_weight(self) -> None:
        from app.api.routes.inventory import create_warehouse_inbound

        row = create_warehouse_inbound(
            WarehouseInboundCreate(
                biz_type="PRODUCTION_SURPLUS",
                item_id=1,
                warehouse_id=1,
                source_lot_id=1,
                inbound_weight_kg=85.1,
                source_doc_type="WORK_ORDER",
                source_doc_id=1,
                remark="生产余料归还",
            ),
            context=Context(),
            db=self.db,
        )

        self.db.refresh(self.source_lot)
        self.db.refresh(self.material_row)
        txn = self.db.scalar(select(InventoryTxn).where(InventoryTxn.txn_type == "PRODUCTION_SURPLUS_IN"))
        self.assertEqual(row.lot_id, 1)
        self.assertEqual(float(self.source_lot.remaining_weight_kg), 385.1)
        self.assertEqual(float(self.material_row.returned_weight_kg), 85.1)
        self.assertEqual(txn.source_doc_type, "WORK_ORDER")
        self.assertEqual(txn.source_doc_id, 1)
        self.assertEqual(txn.source_line_id, self.source_lot.id)

    def test_production_surplus_non_settle_uses_cumulative_tolerance_upper_limit(self) -> None:
        from app.api.routes.inventory import create_warehouse_inbound

        self.material_row.returned_weight_kg = Decimal("40")
        self.db.add(self.material_row)
        self.db.commit()

        create_warehouse_inbound(
            WarehouseInboundCreate(
                biz_type="PRODUCTION_SURPLUS",
                item_id=1,
                warehouse_id=1,
                source_lot_id=1,
                inbound_weight_kg=45.1,
                source_doc_type="WORK_ORDER",
                source_doc_id=1,
                remark="生产余料继续归还",
            ),
            context=Context(),
            db=self.db,
        )

        self.db.refresh(self.material_row)
        self.assertEqual(float(self.material_row.returned_weight_kg), 85.1)

    def test_production_surplus_non_settle_rejects_when_cumulative_return_exceeds_tolerance_upper_limit(self) -> None:
        from app.api.routes.inventory import create_warehouse_inbound

        self.material_row.returned_weight_kg = Decimal("40")
        self.db.add(self.material_row)
        self.db.commit()

        with self.assertRaises(HTTPException) as cm:
            create_warehouse_inbound(
                WarehouseInboundCreate(
                    biz_type="PRODUCTION_SURPLUS",
                    item_id=1,
                    warehouse_id=1,
                    source_lot_id=1,
                    inbound_weight_kg=45.101,
                    source_doc_type="WORK_ORDER",
                    source_doc_id=1,
                    remark="生产余料继续归还",
                ),
                context=Context(),
                db=self.db,
            )

        self.assertEqual(cm.exception.status_code, 400)
        self.assertIn("累计归还重量超过允许入库上限", cm.exception.detail)

    def test_production_surplus_settle_marks_surplus_done_but_keeps_work_order_open(self) -> None:
        from app.api.routes.inventory import create_warehouse_inbound

        create_warehouse_inbound(
            WarehouseInboundCreate(
                biz_type="PRODUCTION_SURPLUS",
                item_id=1,
                warehouse_id=1,
                source_lot_id=1,
                inbound_weight_kg=85.1,
                source_doc_type="WORK_ORDER",
                source_doc_id=1,
                remark="生产余料归还",
                settle_work_order=True,
            ),
            context=Context(),
            db=self.db,
        )

        self.db.refresh(self.work_order)
        self.db.refresh(self.material_row)
        self.assertNotEqual(self.work_order.status, "SETTLED")
        self.assertIn("生产余料已结单", self.work_order.remark)
        self.assertEqual(float(self.material_row.returned_weight_kg), 85.1)

    def test_production_surplus_settle_does_not_block_later_scrap_settle(self) -> None:
        from app.api.routes.inventory import create_warehouse_inbound

        create_warehouse_inbound(
            WarehouseInboundCreate(
                biz_type="PRODUCTION_SURPLUS",
                item_id=1,
                warehouse_id=1,
                source_lot_id=1,
                inbound_weight_kg=85.1,
                source_doc_type="WORK_ORDER",
                source_doc_id=1,
                remark="生产余料归还",
                settle_work_order=True,
            ),
            context=Context(),
            db=self.db,
        )

        self.db.refresh(self.work_order)
        self.assertNotEqual(self.work_order.status, "SETTLED")

        create_warehouse_inbound(
            WarehouseInboundCreate(
                biz_type="PRODUCTION_SCRAP_IN",
                item_id=1,
                warehouse_id=3,
                inbound_weight_kg=27.6,
                source_doc_type="WORK_ORDER",
                source_doc_id=1,
                remark="生产废料入库",
                settle_work_order=True,
            ),
            context=Context(),
            db=self.db,
        )

        self.db.refresh(self.work_order)
        self.assertEqual(self.work_order.status, "SETTLED")

    def test_production_surplus_settle_requires_remark_when_return_is_outside_tolerance(self) -> None:
        from app.api.routes.inventory import create_warehouse_inbound

        with self.assertRaises(HTTPException) as cm:
            create_warehouse_inbound(
                WarehouseInboundCreate(
                    biz_type="PRODUCTION_SURPLUS",
                    item_id=1,
                    warehouse_id=1,
                    source_lot_id=1,
                    inbound_weight_kg=85.101,
                    source_doc_type="WORK_ORDER",
                    source_doc_id=1,
                    remark="生产余料归还",
                    settle_work_order=True,
                ),
                context=Context(),
                db=self.db,
            )

        self.assertEqual(cm.exception.status_code, 400)
        self.assertIn("填写结单说明", cm.exception.detail)

    def test_production_surplus_settle_accepts_outside_tolerance_with_remark(self) -> None:
        from app.api.routes.inventory import create_warehouse_inbound

        create_warehouse_inbound(
            WarehouseInboundCreate(
                biz_type="PRODUCTION_SURPLUS",
                item_id=1,
                warehouse_id=1,
                source_lot_id=1,
                inbound_weight_kg=85.101,
                source_doc_type="WORK_ORDER",
                source_doc_id=1,
                remark="生产余料归还",
                settle_work_order=True,
                settle_remark="实际称重偏高，现场已复核",
            ),
            context=Context(),
            db=self.db,
        )

        self.db.refresh(self.work_order)
        self.db.refresh(self.material_row)
        self.assertNotEqual(self.work_order.status, "SETTLED")
        self.assertIn("生产余料已结单", self.work_order.remark)
        self.assertEqual(float(self.material_row.returned_weight_kg), 85.101)
        self.assertIn("实际称重偏高", self.work_order.remark)

    def test_settled_work_order_rejects_production_surplus_inbound(self) -> None:
        from app.api.routes.inventory import create_warehouse_inbound

        self.work_order.status = "SETTLED"
        self.db.add(self.work_order)
        self.db.commit()

        with self.assertRaises(HTTPException) as cm:
            create_warehouse_inbound(
                WarehouseInboundCreate(
                    biz_type="PRODUCTION_SURPLUS",
                    item_id=1,
                    warehouse_id=1,
                    source_lot_id=1,
                    inbound_weight_kg=1,
                    source_doc_type="WORK_ORDER",
                    source_doc_id=1,
                    remark="生产余料归还",
                ),
                context=Context(),
                db=self.db,
            )

        self.assertEqual(cm.exception.status_code, 400)
        self.assertIn("已结单", cm.exception.detail)

    def test_settled_work_order_rejects_production_scrap_inbound(self) -> None:
        from app.api.routes.inventory import create_warehouse_inbound

        self.work_order.status = "SETTLED"
        self.db.add(self.work_order)
        self.db.commit()

        with self.assertRaises(HTTPException) as cm:
            create_warehouse_inbound(
                WarehouseInboundCreate(
                    biz_type="PRODUCTION_SCRAP_IN",
                    item_id=1,
                    warehouse_id=3,
                    inbound_weight_kg=1,
                    source_doc_type="WORK_ORDER",
                    source_doc_id=1,
                    remark="生产废料入库",
                ),
                context=Context(),
                db=self.db,
            )

        self.assertEqual(cm.exception.status_code, 400)
        self.assertIn("已结单", cm.exception.detail)

    def test_settled_work_order_rejects_finished_inbound(self) -> None:
        from app.api.routes.production import create_completion_receipt

        self.work_order.status = "SETTLED"
        self.db.add(self.work_order)
        self.db.commit()

        with self.assertRaises(HTTPException) as cm:
            create_completion_receipt(
                CompletionReceiptCreate(
                    work_order_id=1,
                    warehouse_id=2,
                    receiver_employee_id=None,
                    items=[CompletionReceiptItemCreate(product_item_id=2, receipt_qty=1, receipt_weight_kg=0)],
                ),
                context=Context(),
                db=self.db,
            )

        self.assertEqual(cm.exception.status_code, 400)
        self.assertIn("已结单", cm.exception.detail)
