from __future__ import annotations

import unittest
from datetime import datetime
from decimal import Decimal

from sqlalchemy import BigInteger, create_engine
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.api.routes.dashboard import get_material_lot_lifecycle, list_dashboard_material_lots  # noqa: E402
from app.models.base import Base  # noqa: E402
from app.models.master_data import Item, Warehouse  # noqa: E402
from app.models.operations import Delivery, DeliveryItem, OperationReport, ProductionBatchLedger, ProductionBatchLedgerTxn, StockLot  # noqa: E402
from app.models.org import Department, Employee, SystemConfig  # noqa: E402
from app.schemas.dashboard import DashboardMaterialLotRead  # noqa: E402
from app.services.material_lifecycle_dag import build_material_lot_lifecycle_graph  # noqa: E402


class DashboardMaterialLotLifecycleTest(unittest.TestCase):
    def setUp(self) -> None:
        engine = create_engine("sqlite+pysqlite:///:memory:", future=True)
        Base.metadata.create_all(engine)
        self.SessionLocal = sessionmaker(bind=engine, autoflush=False, autocommit=False, future=True)
        self.db: Session = self.SessionLocal()
        self.now = datetime(2026, 6, 14, 9, 0)
        self._seed_lot_and_ledger()

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

    def _seed_lot_and_ledger(self) -> None:
        raw_item = Item(
            id=1,
            item_code="RM-001",
            item_name="冷轧钢板",
            item_type="RAW_MATERIAL",
            unit_weight_kg=Decimal("1"),
            status="ACTIVE",
            created_at=self.now,
            updated_at=self.now,
        )
        product = Item(
            id=2,
            item_code="FG-001",
            item_name="钢制碗",
            item_type="FINISHED_GOOD",
            unit_weight_kg=Decimal("0.5"),
            status="ACTIVE",
            created_at=self.now,
            updated_at=self.now,
        )
        warehouse = Warehouse(
            id=1,
            warehouse_code="WH-RAW",
            warehouse_name="原材料库",
            warehouse_type="RAW",
            status="ACTIVE",
            created_at=self.now,
            updated_at=self.now,
        )
        lot = StockLot(
            id=1,
            lot_no="YL0001",
            lot_role="RAW_MATERIAL",
            item_id=1,
            warehouse_id=1,
            source_doc_type="期初入库",
            source_doc_id=1,
            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",
            created_at=self.now,
            updated_at=self.now,
        )
        ledger = ProductionBatchLedger(
            id=1,
            material_lot_id=1,
            material_lot_no="YL0001",
            material_item_id=1,
            product_item_id=2,
            status="在生产",
            miniapp_selectable=1,
            total_issued_weight_kg=Decimal("120"),
            outside_weight_kg=Decimal("42"),
            finished_inbound_qty=Decimal("100"),
            surplus_inbound_weight_kg=Decimal("18"),
            scrap_inbound_weight_kg=Decimal("6"),
            writeoff_weight_kg=Decimal("54"),
            first_issue_time=self.now,
            last_issue_time=self.now,
            created_at=self.now,
            updated_at=self.now,
        )
        dept = Department(
            id=1,
            dept_code="D-001",
            dept_name="生产部",
            dept_type="PRODUCTION",
            status="ACTIVE",
            created_at=self.now,
            updated_at=self.now,
        )
        employee = Employee(
            id=1,
            employee_code="EMP-001",
            employee_name="张三",
            dept_id=1,
            is_operator=1,
            is_workshop_staff=1,
            status="ACTIVE",
            created_at=self.now,
            updated_at=self.now,
        )
        txns = [
            ProductionBatchLedgerTxn(
                id=1,
                production_ledger_id=1,
                txn_type="生产出库",
                qty_delta=Decimal("0"),
                weight_delta_kg=Decimal("120"),
                outside_weight_after_kg=Decimal("120"),
                source_doc_type="库存流水",
                source_doc_id=101,
                biz_time=self.now,
                operator_user_id=1,
                remark="生产出库累计到生产台账",
                created_at=self.now,
                updated_at=self.now,
            ),
            ProductionBatchLedgerTxn(
                id=2,
                production_ledger_id=1,
                txn_type="成品入库",
                qty_delta=Decimal("100"),
                weight_delta_kg=Decimal("-50"),
                outside_weight_after_kg=Decimal("70"),
                source_doc_type="库存流水",
                source_doc_id=102,
                biz_time=self.now,
                operator_user_id=1,
                remark="单据批次号：PB20260614001",
                created_at=self.now,
                updated_at=self.now,
            ),
            ProductionBatchLedgerTxn(
                id=3,
                production_ledger_id=1,
                txn_type="生产余料入库",
                qty_delta=Decimal("0"),
                weight_delta_kg=Decimal("-18"),
                outside_weight_after_kg=Decimal("52"),
                source_doc_type="库存流水",
                source_doc_id=103,
                biz_time=self.now,
                operator_user_id=1,
                remark="单据批次号：PB20260614001",
                created_at=self.now,
                updated_at=self.now,
            ),
            ProductionBatchLedgerTxn(
                id=4,
                production_ledger_id=1,
                txn_type="生产废料入库",
                qty_delta=Decimal("0"),
                weight_delta_kg=Decimal("-6"),
                outside_weight_after_kg=Decimal("46"),
                source_doc_type="库存流水",
                source_doc_id=104,
                biz_time=self.now,
                operator_user_id=1,
                remark="单据批次号：PB20260614001",
                created_at=self.now,
                updated_at=self.now,
            ),
            ProductionBatchLedgerTxn(
                id=5,
                production_ledger_id=1,
                txn_type="结单核销",
                qty_delta=Decimal("0"),
                weight_delta_kg=Decimal("-4"),
                outside_weight_after_kg=Decimal("42"),
                source_doc_type="结单",
                source_doc_id=105,
                biz_time=self.now,
                operator_user_id=1,
                remark="单据批次号：PB20260614001",
                created_at=self.now,
                updated_at=self.now,
            ),
        ]
        report = OperationReport(
            id=1,
            report_no="BG20260614001",
            production_ledger_id=1,
            employee_id=1,
            report_source="MINIAPP",
            start_time=self.now,
            end_time=self.now,
            report_qty=Decimal("100"),
            good_qty=Decimal("96"),
            scrap_qty=Decimal("4"),
            rework_qty=Decimal("0"),
            downtime_minutes=Decimal("0"),
            extra_cost_amount=Decimal("0"),
            is_abnormal=0,
            created_at=self.now,
            updated_at=self.now,
        )
        self.db.add_all([raw_item, product, warehouse, lot, ledger, dept, employee, *txns, report])
        self.db.commit()

    def _lot_read(self) -> DashboardMaterialLotRead:
        return DashboardMaterialLotRead(
            lot_id=1,
            lot_no="YL0001",
            material_item_id=1,
            material_code="RM-001",
            material_name="冷轧钢板",
            warehouse_name="原材料库",
            inbound_weight_kg=500,
            remaining_weight_kg=300,
            unit_cost=4,
            inbound_amount=2000,
            issued_batch_count=0,
            issued_weight_kg=0,
            finished_batch_count=0,
            finished_qty=0,
            delivery_count=0,
            status="AVAILABLE",
            quality_status="PASS",
            created_at=self.now,
        )

    def test_production_ledger_graph_replaces_work_order_main_chain(self) -> None:
        nodes, _ = build_material_lot_lifecycle_graph(self.db, self._lot_read())

        node_types = {node.type for node in nodes}
        self.assertIn("production_ledger", node_types)
        self.assertIn("production_issue_txn", node_types)
        self.assertIn("ledger_outside_balance", node_types)
        self.assertIn("production_ledger_settlement", node_types)
        self.assertIn("finished_inbound", node_types)
        self.assertIn("surplus_inbound", node_types)
        self.assertIn("scrap_inbound", node_types)
        self.assertNotIn("work_order", node_types)

    def test_production_ledger_title_stage_and_outside_balance_metric(self) -> None:
        nodes, _ = build_material_lot_lifecycle_graph(self.db, self._lot_read())

        ledger_node = next(node for node in nodes if node.type == "production_ledger")
        metrics = {item.label: item.value for item in ledger_node.metrics}
        self.assertEqual(ledger_node.title, "YL0001 · 钢制碗")
        self.assertEqual(ledger_node.stage, "生产台账")
        self.assertEqual(metrics["库外余额"], "42 kg")

    def test_material_lot_dashboard_list_excludes_scrap_warehouse_lots(self) -> None:
        scrap_warehouse = Warehouse(
            id=3,
            warehouse_code="WH-SCRAP",
            warehouse_name="废料库",
            warehouse_type="SCRAP",
            status="ACTIVE",
            created_at=self.now,
            updated_at=self.now,
        )
        scrap_lot = StockLot(
            id=30,
            lot_no="SCRAPI-YL0001-001",
            lot_role="PRODUCTION_SCRAP",
            item_id=1,
            warehouse_id=3,
            source_doc_type="PRODUCTION_SCRAP_IN",
            source_doc_id=1,
            inbound_qty=Decimal("0"),
            inbound_weight_kg=Decimal("5"),
            remaining_qty=Decimal("0"),
            remaining_weight_kg=Decimal("5"),
            locked_qty=Decimal("0"),
            locked_weight_kg=Decimal("0"),
            unit_cost=Decimal("4"),
            quality_status="SCRAP",
            status="AVAILABLE",
            created_at=self.now,
            updated_at=self.now,
        )
        self.db.add_all([scrap_warehouse, scrap_lot])
        self.db.commit()

        rows = list_dashboard_material_lots(limit=100, db=self.db)

        lot_nos = {row.lot_no for row in rows}
        self.assertIn("YL0001", lot_nos)
        self.assertNotIn("SCRAPI-YL0001-001", lot_nos)
        self.assertTrue(all(row.warehouse_name == "原材料库" for row in rows))

    def test_operation_report_respects_smart_operation_report_switch(self) -> None:
        nodes, _ = build_material_lot_lifecycle_graph(self.db, self._lot_read())
        self.assertIn("operation_report", {node.type for node in nodes})

        self.db.add(
            SystemConfig(
                config_code="SMART_OPERATION_REPORT_ENABLED",
                config_name="对接智能报工小程序",
                config_value="关闭",
                status="ACTIVE",
                created_at=self.now,
                updated_at=self.now,
            )
        )
        self.db.commit()

        nodes, _ = build_material_lot_lifecycle_graph(self.db, self._lot_read())
        self.assertNotIn("operation_report", {node.type for node in nodes})

    def test_route_returns_new_graph_when_production_ledger_exists(self) -> None:
        response = get_material_lot_lifecycle(1, self.db)

        node_types = {node.type for node in response.nodes}
        self.assertIn("production_ledger", node_types)
        self.assertNotIn("work_order", node_types)

    def test_route_legacy_fallback_does_not_crash_without_production_ledger(self) -> None:
        legacy_lot = StockLot(
            id=2,
            lot_no="YL9999",
            lot_role="RAW_MATERIAL",
            item_id=1,
            warehouse_id=1,
            source_doc_type="期初入库",
            source_doc_id=2,
            inbound_qty=Decimal("0"),
            inbound_weight_kg=Decimal("80"),
            remaining_qty=Decimal("0"),
            remaining_weight_kg=Decimal("80"),
            locked_qty=Decimal("0"),
            locked_weight_kg=Decimal("0"),
            unit_cost=Decimal("4"),
            quality_status="PASS",
            status="AVAILABLE",
            created_at=self.now,
            updated_at=self.now,
        )
        self.db.add(legacy_lot)
        self.db.commit()

        response = get_material_lot_lifecycle(2, self.db)

        self.assertEqual(response.lot.lot_no, "YL9999")
        node_types = {node.type for node in response.nodes}
        self.assertIn("raw_material_lot", node_types)
        self.assertNotIn("production_ledger", node_types)

    def test_delivery_edges_target_finished_inbound_for_exact_finished_lot(self) -> None:
        finished_lot_a = StockLot(
            id=10,
            lot_no="FGL-A",
            lot_role="INVENTORY",
            item_id=2,
            warehouse_id=1,
            source_doc_type="PRODUCTION_LEDGER_IN",
            source_doc_id=1,
            inbound_qty=Decimal("40"),
            inbound_weight_kg=Decimal("20"),
            remaining_qty=Decimal("40"),
            remaining_weight_kg=Decimal("20"),
            locked_qty=Decimal("0"),
            locked_weight_kg=Decimal("0"),
            unit_cost=Decimal("8"),
            quality_status="PASS",
            status="AVAILABLE",
            created_at=self.now,
            updated_at=self.now,
        )
        finished_lot_b = StockLot(
            id=11,
            lot_no="FGL-B",
            lot_role="INVENTORY",
            item_id=2,
            warehouse_id=1,
            source_doc_type="PRODUCTION_LEDGER_IN",
            source_doc_id=1,
            inbound_qty=Decimal("60"),
            inbound_weight_kg=Decimal("30"),
            remaining_qty=Decimal("0"),
            remaining_weight_kg=Decimal("0"),
            locked_qty=Decimal("0"),
            locked_weight_kg=Decimal("0"),
            unit_cost=Decimal("8"),
            quality_status="PASS",
            status="AVAILABLE",
            created_at=self.now,
            updated_at=self.now,
        )
        finished_txn_a = ProductionBatchLedgerTxn(
            id=20,
            production_ledger_id=1,
            txn_type="成品入库",
            qty_delta=Decimal("40"),
            weight_delta_kg=Decimal("-20"),
            outside_weight_after_kg=Decimal("70"),
            source_doc_type="库存流水",
            source_doc_id=201,
            source_line_id=10,
            biz_time=self.now,
            operator_user_id=1,
            remark="单据批次号：PB-FGL-A",
            created_at=self.now,
            updated_at=self.now,
        )
        finished_txn_b = ProductionBatchLedgerTxn(
            id=21,
            production_ledger_id=1,
            txn_type="成品入库",
            qty_delta=Decimal("60"),
            weight_delta_kg=Decimal("-30"),
            outside_weight_after_kg=Decimal("42"),
            source_doc_type="库存流水",
            source_doc_id=202,
            source_line_id=11,
            biz_time=self.now,
            operator_user_id=1,
            remark="单据批次号：PB-FGL-B",
            created_at=self.now,
            updated_at=self.now,
        )
        delivery = Delivery(
            id=1,
            delivery_no="FH20260614001",
            warehouse_id=1,
            delivery_date=self.now,
            status="POSTED",
            created_at=self.now,
            updated_at=self.now,
        )
        delivery_item = DeliveryItem(
            id=1,
            delivery_id=1,
            line_no=1,
            product_item_id=2,
            lot_id=11,
            delivery_qty=Decimal("10"),
            delivery_weight_kg=Decimal("5"),
            unit_price=Decimal("12"),
            line_amount=Decimal("120"),
            status="POSTED",
            created_at=self.now,
            updated_at=self.now,
        )
        self.db.add_all([finished_lot_a, finished_lot_b, finished_txn_a, finished_txn_b, delivery, delivery_item])
        self.db.commit()

        nodes, edges = build_material_lot_lifecycle_graph(self.db, self._lot_read())

        delivery_node = next(node for node in nodes if node.type == "delivery")
        finished_a = next(node for node in nodes if node.type == "finished_inbound" and node.subtitle == "PB-FGL-A")
        finished_b = next(node for node in nodes if node.type == "finished_inbound" and node.subtitle == "PB-FGL-B")
        delivery_source_ids = {edge.source for edge in edges if edge.target == delivery_node.id}
        self.assertIn(finished_b.id, delivery_source_ids)
        self.assertNotIn(finished_a.id, delivery_source_ids)
        self.assertNotIn("ledger-1", delivery_source_ids)

    def test_reopen_txn_uses_reopen_node_type_not_writeoff(self) -> None:
        self.db.add(
            ProductionBatchLedgerTxn(
                id=30,
                production_ledger_id=1,
                txn_type="重新开工",
                qty_delta=Decimal("0"),
                weight_delta_kg=Decimal("0"),
                outside_weight_after_kg=Decimal("42"),
                source_doc_type="生产台账",
                source_doc_id=1,
                biz_time=self.now,
                operator_user_id=1,
                remark="重新开工",
                created_at=self.now,
                updated_at=self.now,
            )
        )
        self.db.commit()

        nodes, _ = build_material_lot_lifecycle_graph(self.db, self._lot_read())

        reopen_node = next(node for node in nodes if node.title == "重新开工")
        self.assertEqual(reopen_node.type, "ledger_reopen")
        self.assertEqual(reopen_node.stage, "重新开工")


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