from __future__ import annotations

import unittest
from decimal import Decimal

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.api.routes import master_data  # noqa: E402
from app.models.base import Base  # noqa: E402
from app.models.master_data import Bom, BomItem, Item, Material, Product  # noqa: E402
from app.models.miniapp import MiniAppAttendancePoint, MiniAppProduct  # noqa: E402
from app.models.operations import Process, ProcessRoute, ProcessRouteOperation, WorkCenter  # noqa: E402
from app.models.org import Department  # noqa: E402
from app.schemas.database import MiniAppProductUpsert, ProductUpdate  # noqa: E402


class MiniAppProductMaterialSyncTest(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._seed_required_master_data()

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

    def _seed_required_master_data(self) -> None:
        self.db.add_all(
            [
                Department(id=1, dept_code="D-TEST", dept_name="测试车间", dept_type="WORKSHOP", status="ACTIVE"),
                Process(id=1, process_code="PROC-1", process_name="1", process_type="STAMPING", status="ACTIVE"),
                WorkCenter(
                    id=1,
                    center_code="WC-1",
                    center_name="测试工作中心",
                    dept_id=1,
                    center_type="PRESS",
                    status="ACTIVE",
                ),
                MiniAppAttendancePoint(name="测试考勤点", is_active=True),
            ]
        )
        self.db.commit()

    def _add_miniapp_product(self, *, material_code: str | None, material_name: str | None) -> MiniAppProduct:
        row = MiniAppProduct(
            attendance_point_name="测试考勤点",
            project_no="PRJ-001",
            product_name="测试支架",
            device_no="",
            process_name="1",
            profile_no="PF-001",
            material_code=material_code,
            material_name=material_name,
            supplier="小程序供应商",
            product_net_weight_kg=Decimal("1.2000"),
            product_gross_weight_kg=Decimal("1.5000"),
            scrap_loss_rate=Decimal("0.020000"),
            waste_price_yuan_per_kg=Decimal("3.5000"),
            stamping_method="冲压",
            operator_count=Decimal("1"),
            standard_beat=Decimal("12.000"),
            standard_workload=Decimal("2400.00"),
        )
        self.db.add(row)
        self.db.commit()
        return row

    def _add_material(self) -> Item:
        item = Item(
            item_code="MAT-ERP-001",
            item_name="ERP原材料",
            item_type="RAW_MATERIAL",
            specification="1.0*100",
            material_grade="SPCC",
            unit_weight_kg=Decimal("1.000000"),
            safety_stock_weight_kg=Decimal("0"),
            scrap_sale_price=Decimal("2.5000"),
            status="ACTIVE",
        )
        self.db.add(item)
        self.db.flush()
        self.db.add(
            Material(
                item_id=item.id,
                default_supplier_id=None,
                purchase_calc_mode="BY_WEIGHT",
                min_purchase_qty=Decimal("0"),
                purchase_multiple_qty=Decimal("0"),
                purchase_multiple_weight_kg=Decimal("0"),
                lead_time_days=0,
                quality_rule=None,
            )
        )
        self.db.commit()
        return item

    def test_miniapp_material_not_in_erp_does_not_block_product_spec_sync(self) -> None:
        self._add_miniapp_product(material_code="WX-MAT-001", material_name="小程序自由物料")

        result = master_data._sync_miniapp_product_specs_to_master_data(self.db)
        self.db.commit()

        self.assertEqual(result["created"], 1)
        self.assertEqual(result["matched"], 1)
        self.assertEqual(self.db.scalar(select(func.count(Item.id)).where(Item.item_type == "RAW_MATERIAL")), 0)
        self.assertEqual(self.db.scalar(select(func.count(Bom.id))), 0)
        self.assertEqual(self.db.scalar(select(func.count(ProcessRoute.id))), 1)

        miniapp_row = self.db.get(MiniAppProduct, ("测试考勤点", "PRJ-001", "测试支架", "", "1"))
        self.assertEqual(miniapp_row.material_code, "WX-MAT-001")
        self.assertEqual(miniapp_row.material_name, "小程序自由物料")

    def test_miniapp_material_matching_erp_material_creates_default_bom(self) -> None:
        material = self._add_material()
        self._add_miniapp_product(material_code=material.item_code, material_name=material.item_name)

        result = master_data._sync_miniapp_product_specs_to_master_data(self.db)
        self.db.commit()

        self.assertEqual(result["bom_updated"], 1)
        bom_item = self.db.scalar(select(BomItem).limit(1))
        self.assertIsNotNone(bom_item)
        self.assertEqual(bom_item.material_item_id, material.id)

    def test_upsert_miniapp_product_preserves_erp_selected_material_code_and_name(self) -> None:
        payload = MiniAppProductUpsert(
            attendance_point_name="测试考勤点",
            project_no="ERP-PRJ-001",
            product_name="ERP新建支架",
            profile_no="PF-ERP",
            material_code="MAT-ERP-SELECTED",
            material_name="ERP选择原材料",
            supplier=None,
            product_net_weight_kg=1.2,
            product_gross_weight_kg=1.5,
            allowed_scrap_rate=0.02,
            waste_price_yuan_per_kg=3.5,
            device_no="",
            process_name="1",
            stamping_method="冲压",
            operator_count=1,
            standard_beat=12,
            standard_workload=2400,
        )

        row = master_data._upsert_miniapp_product(self.db, payload)
        self.db.commit()

        self.assertEqual(row.material_code, "MAT-ERP-SELECTED")
        self.assertEqual(row.material_name, "ERP选择原材料")

    def test_partial_miniapp_route_sync_does_not_deactivate_erp_authored_operations(self) -> None:
        product_item = Item(
            item_code="产品需规00001",
            item_name="测试支架",
            item_type="FINISHED_GOOD",
            specification=None,
            material_grade=None,
            unit_weight_kg=Decimal("1.000000"),
            safety_stock_weight_kg=Decimal("0"),
            scrap_sale_price=Decimal("0"),
            status="ACTIVE",
        )
        self.db.add(product_item)
        self.db.flush()
        route = ProcessRoute(
            route_code="ROUTE-001",
            route_name="测试路线",
            product_item_id=product_item.id,
            version_no="V1.0",
            is_default=1,
            status="ACTIVE",
            effective_date=None,
            expire_date=None,
            remark=None,
        )
        self.db.add(route)
        self.db.flush()
        operations = []
        for seq_no, status in [(1, "ACTIVE"), (2, "ACTIVE"), (3, "INACTIVE")]:
            operation = ProcessRouteOperation(
                route_id=route.id,
                seq_no=seq_no,
                process_id=1,
                work_center_id=1,
                operation_name=str(seq_no),
                std_setup_minutes=Decimal("0"),
                std_cycle_seconds=Decimal("10"),
                std_labor_cost=Decimal("0"),
                std_machine_cost=Decimal("0"),
                std_outsource_cost=Decimal("0"),
                allowed_scrap_rate=Decimal("0"),
                report_required=1,
                is_key_operation=0,
                status=status,
                remark=None,
            )
            self.db.add(operation)
            operations.append(operation)
        self.db.commit()

        changed = master_data._sync_miniapp_route_operations(
            self.db,
            route,
            operations,
            [
                {
                    "seq_no": 1,
                    "process_id": 1,
                    "work_center_id": 1,
                    "operation_name": "1",
                    "std_cycle_seconds": Decimal("10"),
                    "allowed_scrap_rate": Decimal("0"),
                }
            ],
        )
        self.db.flush()

        self.assertTrue(changed)
        self.assertEqual(operations[0].status, "ACTIVE")
        self.assertEqual(operations[1].status, "ACTIVE")
        self.assertEqual(operations[2].status, "ACTIVE")

    def test_miniapp_owned_missing_operation_is_deactivated(self) -> None:
        product_item = Item(
            item_code="产品需规00002",
            item_name="测试支架2",
            item_type="FINISHED_GOOD",
            specification=None,
            material_grade=None,
            unit_weight_kg=Decimal("1.000000"),
            safety_stock_weight_kg=Decimal("0"),
            scrap_sale_price=Decimal("0"),
            status="ACTIVE",
        )
        self.db.add(product_item)
        self.db.flush()
        route = ProcessRoute(
            route_code="ROUTE-002",
            route_name="测试路线2",
            product_item_id=product_item.id,
            version_no="V1.0",
            is_default=1,
            status="ACTIVE",
            effective_date=None,
            expire_date=None,
            remark=master_data.MINIAPP_SYNC_REMARK,
        )
        self.db.add(route)
        self.db.flush()
        operation = ProcessRouteOperation(
            route_id=route.id,
            seq_no=2,
            process_id=1,
            work_center_id=1,
            operation_name="2",
            std_setup_minutes=Decimal("0"),
            std_cycle_seconds=Decimal("10"),
            std_labor_cost=Decimal("0"),
            std_machine_cost=Decimal("0"),
            std_outsource_cost=Decimal("0"),
            allowed_scrap_rate=Decimal("0"),
            report_required=1,
            is_key_operation=0,
            status="ACTIVE",
            remark=master_data.MINIAPP_SYNC_REMARK,
        )
        self.db.add(operation)
        self.db.commit()

        master_data._sync_miniapp_route_operations(self.db, route, [operation], [])
        self.db.flush()

        self.assertEqual(operation.status, "INACTIVE")

    def test_stale_product_with_only_miniapp_owned_operations_is_deactivated(self) -> None:
        product_item = Item(
            item_code="产品需规00003",
            item_name="保温杯",
            item_type="FINISHED_GOOD",
            specification=None,
            material_grade=None,
            unit_weight_kg=Decimal("1.000000"),
            safety_stock_weight_kg=Decimal("0"),
            scrap_sale_price=Decimal("0"),
            status="ACTIVE",
        )
        self.db.add(product_item)
        self.db.flush()
        product = Product(
            item_id=product_item.id,
            drawing_no=None,
            product_family=None,
            version_no="V1.0",
            standard_output_qty=Decimal("0"),
            allowed_scrap_rate=Decimal("0"),
            quality_standard=None,
        )
        self.db.add(product)
        route = ProcessRoute(
            route_code="ROUTE-003",
            route_name="保温杯默认工艺路线",
            product_item_id=product_item.id,
            version_no="V1.0",
            is_default=1,
            status="ACTIVE",
            effective_date=None,
            expire_date=None,
            remark=None,
        )
        self.db.add(route)
        self.db.flush()
        operation = ProcessRouteOperation(
            route_id=route.id,
            seq_no=1,
            process_id=1,
            work_center_id=1,
            operation_name="1",
            std_setup_minutes=Decimal("0"),
            std_cycle_seconds=Decimal("10"),
            std_labor_cost=Decimal("0"),
            std_machine_cost=Decimal("0"),
            std_outsource_cost=Decimal("0"),
            allowed_scrap_rate=Decimal("0"),
            report_required=1,
            is_key_operation=0,
            status="ACTIVE",
            remark=master_data.MINIAPP_SYNC_REMARK,
        )
        self.db.add(operation)
        self.db.commit()

        result = master_data._deactivate_stale_miniapp_product_specs_from_master_data(self.db, [])
        self.db.flush()

        self.assertEqual(result["deactivated"], 1)
        self.assertEqual(product_item.status, "INACTIVE")
        self.assertEqual(route.status, "INACTIVE")
        self.assertEqual(operation.status, "INACTIVE")

    def test_process_route_read_includes_miniapp_stamping_method(self) -> None:
        product_item = Item(
            item_code="产品需规00001",
            item_name="测试支架",
            item_type="FINISHED_GOOD",
            unit_weight_kg=Decimal("1.000000"),
            safety_stock_weight_kg=Decimal("0"),
            scrap_sale_price=Decimal("0"),
            status="ACTIVE",
        )
        self.db.add(product_item)
        self.db.flush()
        product = Product(
            item_id=product_item.id,
            drawing_no="PRJ-001",
            product_family="PRJ-001",
            version_no="V1.0",
            standard_output_qty=Decimal("0"),
            allowed_scrap_rate=Decimal("0"),
            quality_standard=None,
        )
        route = ProcessRoute(
            route_code="ROUTE-004",
            route_name="测试支架默认工艺路线",
            product_item_id=product_item.id,
            version_no="V1.0",
            is_default=1,
            status="ACTIVE",
            effective_date=None,
            expire_date=None,
            remark=master_data.MINIAPP_SYNC_REMARK,
        )
        self.db.add_all([product, route])
        self.db.flush()
        operation = ProcessRouteOperation(
            route_id=route.id,
            seq_no=1,
            process_id=1,
            work_center_id=1,
            operation_name="1",
            std_setup_minutes=Decimal("0"),
            std_cycle_seconds=Decimal("10"),
            std_labor_cost=Decimal("0"),
            std_machine_cost=Decimal("0"),
            std_outsource_cost=Decimal("0"),
            allowed_scrap_rate=Decimal("0"),
            report_required=1,
            is_key_operation=0,
            status="ACTIVE",
            remark=master_data.MINIAPP_SYNC_REMARK,
        )
        miniapp_product = MiniAppProduct(
            attendance_point_name="测试考勤点",
            project_no="PRJ-001",
            product_name="测试支架",
            device_no="",
            process_name="1",
            profile_no="PF-001",
            material_code="原材料00001",
            material_name="钢板",
            supplier="测试供应商",
            product_net_weight_kg=Decimal("1.0000"),
            product_gross_weight_kg=Decimal("1.2000"),
            scrap_loss_rate=Decimal("0.020000"),
            waste_price_yuan_per_kg=Decimal("3.5000"),
            stamping_method="切边冲孔",
            operator_count=Decimal("1"),
            standard_beat=Decimal("10.000"),
            standard_workload=Decimal("2400.00"),
        )
        self.db.add_all([operation, miniapp_product])
        self.db.commit()

        rows = master_data._list_process_route_reads(self.db, limit=10)
        target = next(row for row in rows if row.route_id == route.id)

        self.assertEqual(target.operations[0].operation_name, "1")
        self.assertEqual(target.operations[0].stamping_method, "切边冲孔")

    def test_product_update_allows_existing_bom_and_route_without_downstream_usage(self) -> None:
        product_item = Item(
            item_code="产品需规00004",
            item_name="钢制碗",
            item_type="FINISHED_GOOD",
            specification=None,
            material_grade=None,
            unit_weight_kg=Decimal("0.500000"),
            safety_stock_weight_kg=Decimal("0"),
            scrap_sale_price=Decimal("0"),
            status="ACTIVE",
        )
        self.db.add(product_item)
        self.db.flush()
        product = Product(
            item_id=product_item.id,
            drawing_no=None,
            product_family=None,
            version_no="V1.0",
            standard_output_qty=Decimal("0"),
            allowed_scrap_rate=Decimal("0.03"),
            quality_standard=None,
        )
        self.db.add(product)
        self.db.add(
            Bom(
                bom_code="BOM-00004",
                product_item_id=product_item.id,
                version_no="V1.0",
                is_default=1,
                status="ACTIVE",
                effective_date=None,
                expire_date=None,
                remark=master_data.MINIAPP_SYNC_REMARK,
            )
        )
        self.db.add(
            ProcessRoute(
                route_code="ROUTE-004",
                route_name="钢制碗默认工艺路线",
                product_item_id=product_item.id,
                version_no="V1.0",
                is_default=1,
                status="ACTIVE",
                effective_date=None,
                expire_date=None,
                remark=master_data.MINIAPP_SYNC_REMARK,
            )
        )
        self.db.commit()

        updated = master_data.update_product(
            product_item.id,
            ProductUpdate(
                item_name="钢制碗",
                specification=None,
                drawing_no=None,
                product_family=None,
                version_no="V1.0",
                unit_weight_kg=0.5,
                allowed_scrap_rate=0.03,
                standard_output_qty=0,
                quality_standard=None,
                safety_stock_weight_kg=0,
                status="ACTIVE",
            ),
            db=self.db,
        )

        self.assertEqual(updated.item_id, product_item.id)


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