from __future__ import annotations

import unittest
from datetime import datetime
from decimal import Decimal

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,
    Stocktake,
    StocktakeWarehouse,
    WorkCenter,
    WorkOrder,
    WorkOrderMaterial,
    WorkOrderMaterialIssue,
    WorkOrderOperation,
)
from app.models.org import Department  # noqa: E402
from app.schemas.operations import ProductionWorkOrderInboundCreate  # noqa: E402
from app.services.production_work_order_inbound import (  # noqa: E402
    create_production_work_order_inbound,
    preview_production_work_order_inbound,
)


class ProductionWorkOrderInboundTest(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_preview_calculates_defaults_from_finished_qty(self) -> None:
        preview = preview_production_work_order_inbound(self.db, work_order_id=1, finished_qty=Decimal("198"))

        self.assertEqual(preview.work_order_no, "WO-001")
        self.assertEqual(preview.source_lot_no, "JH_冷轧钢板00001_0001")
        self.assertEqual(preview.finished_qty, 198)
        self.assertEqual(preview.issued_weight_kg, 200)
        self.assertEqual(preview.returned_weight_kg, 0)
        self.assertEqual(preview.final_operation_good_qty, 198)
        self.assertEqual(preview.reported_scrap_qty, 6)
        self.assertEqual(preview.theoretical_surplus_weight_kg, 81.2)
        self.assertEqual(preview.theoretical_scrap_weight_kg, 24)

    def test_preview_defaults_finished_qty_from_issued_weight_reference(self) -> None:
        preview = preview_production_work_order_inbound(self.db, work_order_id=1)

        self.assertEqual(preview.reference_finished_qty, 333)
        self.assertEqual(preview.remaining_reference_finished_qty, 333)
        self.assertEqual(preview.finished_qty, 333)
        self.assertEqual(preview.theoretical_surplus_weight_kg, 0.2)

    def test_submit_requires_deviation_remark_when_finished_qty_exceeds_reference_by_15_percent(self) -> None:
        payload = ProductionWorkOrderInboundCreate(
            work_order_id=1,
            finished_qty=500,
            surplus_weight_kg=0,
            scrap_weight_kg=24,
        )

        with self.assertRaises(HTTPException) as raised:
            create_production_work_order_inbound(self.db, payload=payload, operator_user_id=1)

        self.assertEqual(raised.exception.status_code, 400)
        self.assertEqual(raised.exception.detail, "成品入库数量超过本工单参考生产数量上限15%，请填写偏差说明")

    def test_submit_requires_deviation_remark_when_surplus_or_scrap_differs_more_than_15_percent(self) -> None:
        payload = ProductionWorkOrderInboundCreate(
            work_order_id=1,
            finished_qty=198,
            surplus_weight_kg=60,
            scrap_weight_kg=24,
        )

        with self.assertRaises(HTTPException) as raised:
            create_production_work_order_inbound(self.db, payload=payload, operator_user_id=1)

        self.assertEqual(raised.exception.status_code, 400)
        self.assertEqual(raised.exception.detail, "余料或废料入库重量与理论值偏差超过上下15%，请填写偏差说明")

    def test_submit_posts_finished_surplus_and_scrap_in_one_transaction(self) -> None:
        payload = ProductionWorkOrderInboundCreate(
            work_order_id=1,
            finished_qty=198,
            surplus_weight_kg=81.2,
            scrap_weight_kg=24,
        )

        result = create_production_work_order_inbound(self.db, payload=payload, operator_user_id=7)

        self.assertEqual(result.finished_qty, 198)
        self.assertEqual(result.surplus_weight_kg, 81.2)
        self.assertEqual(result.scrap_weight_kg, 24)
        txns = self.db.scalars(select(InventoryTxn).order_by(InventoryTxn.id)).all()
        self.assertEqual([txn.txn_type for txn in txns], ["FG_IN", "PRODUCTION_SURPLUS_IN", "PRODUCTION_SCRAP_IN"])
        self.assertEqual(txns[0].operator_user_id, 7)
        self.assertEqual(txns[0].source_doc_type, "FG_RECEIPT")
        self.assertEqual(txns[1].source_doc_type, "WORK_ORDER")
        self.assertEqual(txns[1].lot_id, 1)
        self.assertEqual(float(txns[1].weight_change_kg), 81.2)
        self.assertEqual(txns[2].source_doc_type, "WORK_ORDER")
        self.assertEqual(txns[2].source_doc_id, 1)

        source_lot = self.db.get(StockLot, 1)
        work_order = self.db.get(WorkOrder, 1)
        material_row = self.db.get(WorkOrderMaterial, 1)
        finished_lot = self.db.get(StockLot, result.finished_lot_id)
        scrap_lot = self.db.get(StockLot, result.scrap_lot_id)
        self.assertEqual(float(source_lot.remaining_weight_kg), 381.2)
        self.assertEqual(float(work_order.finished_qty), 198)
        self.assertEqual(float(material_row.returned_weight_kg), 81.2)
        self.assertEqual(finished_lot.warehouse_id, 2)
        self.assertEqual(finished_lot.source_material_lot_id, 1)
        self.assertEqual(float(finished_lot.unit_cost), 2.4)
        self.assertEqual(scrap_lot.warehouse_id, 3)
        self.assertEqual(scrap_lot.source_material_lot_id, 1)
        self.assertEqual(scrap_lot.quality_status, "SCRAP")

    def test_submit_settles_only_when_requested_and_confirmed(self) -> None:
        payload = ProductionWorkOrderInboundCreate(
            work_order_id=1,
            finished_qty=198,
            surplus_weight_kg=81.2,
            scrap_weight_kg=24,
            settle_work_order=True,
        )

        with self.assertRaises(HTTPException) as raised:
            create_production_work_order_inbound(self.db, payload=payload, operator_user_id=1)
        self.assertEqual(raised.exception.status_code, 400)
        self.assertEqual(raised.exception.detail, "结单前必须确认本次入库明细及结单影响")
        self.assertEqual(self.db.get(WorkOrder, 1).status, "RELEASED")

        payload.settle_confirmed = True
        result = create_production_work_order_inbound(self.db, payload=payload, operator_user_id=1)

        work_order = self.db.get(WorkOrder, 1)
        self.assertTrue(result.settled)
        self.assertEqual(work_order.status, "SETTLED")
        self.assertIsNotNone(work_order.actual_end_time)
        self.assertIn("生产工单入库已结单", work_order.remark)

    def test_submit_without_settle_flags_does_not_settle(self) -> None:
        payload = ProductionWorkOrderInboundCreate(
            work_order_id=1,
            finished_qty=198,
            surplus_weight_kg=81.2,
            scrap_weight_kg=24,
        )

        result = create_production_work_order_inbound(self.db, payload=payload, operator_user_id=1)

        work_order = self.db.get(WorkOrder, 1)
        self.assertFalse(result.settled)
        self.assertEqual(work_order.status, "RELEASED")
        self.assertIsNone(work_order.actual_end_time)

    def test_preview_rejects_work_order_with_more_than_one_source_lot(self) -> None:
        other_lot = StockLot(
            id=2,
            lot_no="JH_冷轧钢板00002_0001",
            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("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",
        )
        other_issue = 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_冷轧钢板00002_0001",
            issued_qty=Decimal("0"),
            issued_weight_kg=Decimal("10"),
            unit_cost=Decimal("4"),
            amount=Decimal("40"),
            issue_time=datetime.now(),
        )
        self.db.add_all([other_lot, other_issue])
        self.db.commit()

        with self.assertRaises(HTTPException) as raised:
            preview_production_work_order_inbound(self.db, work_order_id=1, finished_qty=Decimal("198"))

        self.assertEqual(raised.exception.status_code, 400)
        self.assertEqual(raised.exception.detail, "该工单存在多个材料来源库存批次，不能进行生产工单入库")

    def test_submit_rejects_locked_involved_warehouse_before_posting_inventory(self) -> None:
        stocktake = Stocktake(
            stocktake_no="PK-20260608-001",
            status="LOCKED",
            started_by_user_id=1,
            started_at=datetime.now(),
            remark="成品库盘库",
        )
        self.db.add(stocktake)
        self.db.flush()
        self.db.add(
            StocktakeWarehouse(
                stocktake_id=stocktake.id,
                warehouse_id=self.finished_wh.id,
                warehouse_type="FINISHED",
                warehouse_name=self.finished_wh.warehouse_name,
                status="LOCKED",
            )
        )
        self.db.commit()
        payload = ProductionWorkOrderInboundCreate(
            work_order_id=1,
            finished_qty=198,
            surplus_weight_kg=81.2,
            scrap_weight_kg=24,
        )

        with self.assertRaises(HTTPException) as raised:
            create_production_work_order_inbound(self.db, payload=payload, operator_user_id=1)

        self.assertEqual(raised.exception.status_code, 423)
        self.assertIn("成品库正在盘库", raised.exception.detail)
        txns = self.db.scalars(select(InventoryTxn)).all()
        self.assertEqual(txns, [])
        self.assertEqual(float(self.db.get(WorkOrder, 1).finished_qty), 0)


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