from __future__ import annotations

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

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.document_archive  # noqa: E402,F401
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.document_archive import DocumentArchive  # noqa: E402
from app.models.master_data import Item, Product, StockBalance, Unit, Warehouse  # noqa: E402
from app.models.operations import InventoryTxn, StockLot, WarehouseLocation  # noqa: E402
from app.models.org import Department, Employee, User  # noqa: E402
from app.schemas.operations import (  # noqa: E402
    SpecialWarehouseAdjustmentCreate,
    SpecialWarehouseAdjustmentLineCreate,
)
from app.api.routes.inventory import create_special_warehouse_adjustment  # noqa: E402
from app.services.document_archives import ARCHIVE_STATUS_READY, DOCUMENT_TYPE_WAREHOUSE_OPERATION  # noqa: E402
from app.services.operations import get_inventory_txn_ledger_query  # noqa: E402
from app.services.special_inventory_adjustment import create_special_inventory_adjustment  # noqa: E402


class SpecialWarehouseAdjustmentTest(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_master_data()

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

    def _seed_master_data(self) -> None:
        now = datetime(2026, 6, 6, 9, 0, tzinfo=UTC)
        self.department = Department(
            id=1,
            dept_code="D-SPECIAL",
            dept_name="仓库部",
            dept_type="WAREHOUSE",
            status="ACTIVE",
            created_at=now,
            updated_at=now,
        )
        self.employee = Employee(
            id=1,
            employee_code="EMP-SPECIAL-001",
            employee_name="特殊调整员",
            dept_id=1,
            status="ACTIVE",
            created_at=now,
            updated_at=now,
        )
        self.user = User(
            id=1,
            username="special_tester",
            password_hash="x",
            employee_id=1,
            dept_id=1,
            nickname="特殊调整员",
            is_super_admin=1,
            status="ACTIVE",
            created_at=now,
            updated_at=now,
        )
        self.unit = Unit(id=1, unit_code="PCS", unit_name="个", precision_digits=0, created_at=now, updated_at=now)
        self.raw_item = Item(
            id=1,
            item_code="原材料00001",
            item_name="304不锈钢卷",
            item_type="RAW_MATERIAL",
            unit_weight_kg=Decimal("0"),
            status="ACTIVE",
            created_at=now,
            updated_at=now,
        )
        self.aux_item = Item(
            id=2,
            item_code="辅料00001",
            item_name="包装薄膜",
            item_type="RAW_MATERIAL",
            unit_weight_kg=Decimal("0"),
            stock_unit_id=1,
            status="ACTIVE",
            created_at=now,
            updated_at=now,
        )
        self.finished_item = Item(
            id=3,
            item_code="产品需规00001",
            item_name="冲压支架",
            item_type="FINISHED_GOOD",
            unit_weight_kg=Decimal("2.5"),
            stock_unit_id=1,
            status="ACTIVE",
            created_at=now,
            updated_at=now,
        )
        self.finished_product = Product(
            id=1,
            item_id=3,
            drawing_no="FG-DWG-001",
            product_family="支架",
            version_no="V1",
            created_at=now,
            updated_at=now,
        )
        self.raw_warehouse = Warehouse(
            id=1,
            warehouse_code="WH-RAW",
            warehouse_name="原材料库",
            warehouse_type="RAW",
            status="ACTIVE",
            created_at=now,
            updated_at=now,
        )
        self.aux_warehouse = Warehouse(
            id=2,
            warehouse_code="WH-AUX",
            warehouse_name="辅料库",
            warehouse_type="AUX",
            status="ACTIVE",
            created_at=now,
            updated_at=now,
        )
        self.finished_warehouse = Warehouse(
            id=3,
            warehouse_code="WH-FG",
            warehouse_name="成品库",
            warehouse_type="FINISHED",
            status="ACTIVE",
            created_at=now,
            updated_at=now,
        )
        self.raw_location = WarehouseLocation(
            id=1,
            warehouse_id=1,
            location_code="RAW-A",
            location_name="原材料默认库位",
            is_default=1,
            is_locked=0,
            status="ACTIVE",
            created_at=now,
            updated_at=now,
        )
        self.aux_location = WarehouseLocation(
            id=2,
            warehouse_id=2,
            location_code="AUX-A",
            location_name="辅料默认库位",
            is_default=1,
            is_locked=0,
            status="ACTIVE",
            created_at=now,
            updated_at=now,
        )
        self.finished_location = WarehouseLocation(
            id=3,
            warehouse_id=3,
            location_code="FG-A",
            location_name="成品默认库位",
            is_default=1,
            is_locked=0,
            status="ACTIVE",
            created_at=now,
            updated_at=now,
        )
        self.raw_lot = StockLot(
            id=1,
            lot_no="RAW-LOT-001",
            lot_role="INBOUND_RAW",
            item_id=1,
            warehouse_id=1,
            location_id=1,
            source_doc_type="OPENING_STOCK",
            source_doc_id=1,
            inbound_qty=Decimal("0"),
            inbound_weight_kg=Decimal("100"),
            remaining_qty=Decimal("0"),
            remaining_weight_kg=Decimal("80"),
            locked_qty=Decimal("0"),
            locked_weight_kg=Decimal("0"),
            unit_cost=Decimal("10"),
            quality_status="PASS",
            status="AVAILABLE",
            created_at=now,
            updated_at=now,
        )
        self.aux_lot = StockLot(
            id=2,
            lot_no="AUX-LOT-001",
            lot_role="INVENTORY",
            item_id=2,
            warehouse_id=2,
            location_id=2,
            source_doc_type="OPENING_STOCK",
            source_doc_id=2,
            inbound_qty=Decimal("10"),
            inbound_weight_kg=Decimal("0"),
            remaining_qty=Decimal("6"),
            remaining_weight_kg=Decimal("0"),
            locked_qty=Decimal("0"),
            locked_weight_kg=Decimal("0"),
            unit_cost=Decimal("2.5"),
            quality_status="PASS",
            status="AVAILABLE",
            created_at=now,
            updated_at=now,
        )
        self.finished_lot = StockLot(
            id=3,
            lot_no="FG-LOT-001",
            lot_role="INBOUND_FINISHED",
            item_id=3,
            warehouse_id=3,
            location_id=3,
            source_doc_type="FG_RECEIPT",
            source_doc_id=3,
            inbound_qty=Decimal("20"),
            inbound_weight_kg=Decimal("50"),
            remaining_qty=Decimal("12"),
            remaining_weight_kg=Decimal("30"),
            locked_qty=Decimal("0"),
            locked_weight_kg=Decimal("0"),
            unit_cost=Decimal("18"),
            quality_status="PASS",
            status="AVAILABLE",
            created_at=now,
            updated_at=now,
        )
        self.raw_balance = StockBalance(
            id=1,
            item_id=1,
            warehouse_id=1,
            location_id=1,
            qty_on_hand=Decimal("0"),
            weight_on_hand_kg=Decimal("80"),
            qty_available=Decimal("0"),
            weight_available_kg=Decimal("80"),
            avg_unit_cost=Decimal("10"),
            created_at=now,
            updated_at=now,
        )
        self.aux_balance = StockBalance(
            id=2,
            item_id=2,
            warehouse_id=2,
            location_id=2,
            qty_on_hand=Decimal("6"),
            weight_on_hand_kg=Decimal("0"),
            qty_available=Decimal("6"),
            weight_available_kg=Decimal("0"),
            avg_unit_cost=Decimal("2.5"),
            created_at=now,
            updated_at=now,
        )
        self.finished_balance = StockBalance(
            id=3,
            item_id=3,
            warehouse_id=3,
            location_id=3,
            qty_on_hand=Decimal("12"),
            weight_on_hand_kg=Decimal("30"),
            qty_available=Decimal("12"),
            weight_available_kg=Decimal("30"),
            avg_unit_cost=Decimal("18"),
            created_at=now,
            updated_at=now,
        )
        self.db.add_all(
            [
                self.department,
                self.employee,
                self.user,
                self.unit,
                self.raw_item,
                self.aux_item,
                self.finished_item,
                self.finished_product,
                self.raw_warehouse,
                self.aux_warehouse,
                self.finished_warehouse,
                self.raw_location,
                self.aux_location,
                self.finished_location,
                self.raw_lot,
                self.aux_lot,
                self.finished_lot,
                self.raw_balance,
                self.aux_balance,
                self.finished_balance,
            ]
        )
        self.db.commit()

    def _adjustment_payload(
        self,
        *,
        direction: str,
        warehouse_id: int,
        item_id: int,
        lot_id: int | None = None,
        location_id: int | None = None,
        adjust_qty: Decimal = Decimal("0"),
        adjust_weight_kg: Decimal = Decimal("0"),
        reason: str | None = None,
        unit_cost: Decimal | None = None,
        line_reason: str | None = None,
    ) -> SpecialWarehouseAdjustmentCreate:
        return SpecialWarehouseAdjustmentCreate(
            warehouse_id=warehouse_id,
            direction=direction,
            reason=reason,
            lines=[
                SpecialWarehouseAdjustmentLineCreate(
                    item_id=item_id,
                    lot_id=lot_id,
                    location_id=location_id,
                    adjust_qty=adjust_qty,
                    adjust_weight_kg=adjust_weight_kg,
                    unit_cost=unit_cost,
                    line_reason=line_reason,
                )
            ],
        )

    def _only_txn(self, txn_type: str) -> InventoryTxn:
        return self.db.scalar(select(InventoryTxn).where(InventoryTxn.txn_type == txn_type))

    def _auth_context(self):
        return SimpleNamespace(user=SimpleNamespace(id=self.user.id))

    def _seed_second_raw_lot(self) -> StockLot:
        now = datetime(2026, 6, 6, 9, 0, tzinfo=UTC)
        second_lot = StockLot(
            id=99,
            lot_no="RAW-LOT-002",
            lot_role="INBOUND_RAW",
            item_id=self.raw_item.id,
            warehouse_id=self.raw_warehouse.id,
            location_id=self.raw_location.id,
            source_doc_type="OPENING_STOCK",
            source_doc_id=99,
            inbound_qty=Decimal("0"),
            inbound_weight_kg=Decimal("20"),
            remaining_qty=Decimal("0"),
            remaining_weight_kg=Decimal("20"),
            locked_qty=Decimal("0"),
            locked_weight_kg=Decimal("0"),
            unit_cost=Decimal("10"),
            quality_status="PASS",
            status="AVAILABLE",
            created_at=now,
            updated_at=now,
        )
        self.raw_balance.weight_on_hand_kg += Decimal("20")
        self.raw_balance.weight_available_kg += Decimal("20")
        self.db.add_all([second_lot, self.raw_balance])
        self.db.commit()
        return second_lot

    def test_special_out_requires_remark_and_keeps_inventory_unchanged(self) -> None:
        before_lot_weight = self.raw_lot.remaining_weight_kg
        before_balance_weight = self.raw_balance.weight_on_hand_kg

        with self.assertRaises(ValueError) as exc:
            create_special_inventory_adjustment(
                self.db,
                self._adjustment_payload(
                    direction="OUT",
                    warehouse_id=self.raw_warehouse.id,
                    item_id=self.raw_item.id,
                    lot_id=self.raw_lot.id,
                    adjust_weight_kg=Decimal("5"),
                    reason="",
                ),
                user_id=9,
            )

        self.assertIn("特殊出库必须填写说明", str(exc.exception))
        self.db.refresh(self.raw_lot)
        self.db.refresh(self.raw_balance)
        self.assertEqual(self.raw_lot.remaining_weight_kg, before_lot_weight)
        self.assertEqual(self.raw_balance.weight_on_hand_kg, before_balance_weight)
        self.assertIsNone(self._only_txn("SPECIAL_OUT"))

    def test_raw_special_out_deducts_lot_balance_by_weight_and_writes_ledger(self) -> None:
        create_special_inventory_adjustment(
            self.db,
            self._adjustment_payload(
                direction="OUT",
                warehouse_id=self.raw_warehouse.id,
                item_id=self.raw_item.id,
                lot_id=self.raw_lot.id,
                adjust_weight_kg=Decimal("12.5"),
                reason="样品寄出",
            ),
            user_id=9,
        )

        self.db.refresh(self.raw_lot)
        self.db.refresh(self.raw_balance)
        txn = self._only_txn("SPECIAL_OUT")
        self.assertEqual(self.raw_lot.remaining_qty, Decimal("0.000000"))
        self.assertEqual(self.raw_lot.remaining_weight_kg, Decimal("67.500000"))
        self.assertEqual(self.raw_balance.qty_on_hand, Decimal("0.000000"))
        self.assertEqual(self.raw_balance.weight_on_hand_kg, Decimal("67.500000"))
        self.assertEqual(self.raw_balance.weight_available_kg, Decimal("67.500000"))
        self.assertIsNotNone(txn)
        self.assertEqual(txn.lot_id, self.raw_lot.id)
        self.assertEqual(txn.qty_change, Decimal("0.000000"))
        self.assertEqual(txn.weight_change_kg, Decimal("-12.500000"))
        self.assertIn("特殊出库说明：样品寄出", txn.remark)
        self.assertIn("调整前重量：80", txn.remark)
        self.assertIn("调整后重量：67.5", txn.remark)

    def test_raw_special_out_is_visible_in_inventory_transaction_ledger(self) -> None:
        create_special_inventory_adjustment(
            self.db,
            self._adjustment_payload(
                direction="OUT",
                warehouse_id=self.raw_warehouse.id,
                item_id=self.raw_item.id,
                lot_id=self.raw_lot.id,
                adjust_weight_kg=Decimal("6.5"),
                reason="研发打样领出",
            ),
            user_id=9,
        )

        rows = self.db.execute(
            get_inventory_txn_ledger_query(warehouse_type="RAW", txn_type="SPECIAL_OUT")
        ).mappings().all()

        self.assertEqual(len(rows), 1)
        row = rows[0]
        self.assertEqual(row["direction"], "OUT")
        self.assertEqual(row["txn_type"], "SPECIAL_OUT")
        self.assertIn("特殊出库说明：研发打样领出", row["remark"])

    def test_special_adjustment_generates_archive_for_each_adjustment_transaction(self) -> None:
        second_lot = self._seed_second_raw_lot()

        result = create_special_warehouse_adjustment(
            SpecialWarehouseAdjustmentCreate(
                warehouse_id=self.raw_warehouse.id,
                direction="OUT",
                reason="研发打样多批次领出",
                lines=[
                    SpecialWarehouseAdjustmentLineCreate(
                        item_id=self.raw_item.id,
                        lot_id=self.raw_lot.id,
                        adjust_weight_kg=Decimal("5"),
                    ),
                    SpecialWarehouseAdjustmentLineCreate(
                        item_id=self.raw_item.id,
                        lot_id=second_lot.id,
                        adjust_weight_kg=Decimal("7"),
                    ),
                ],
            ),
            context=self._auth_context(),
            db=self.db,
        )

        txns = self.db.scalars(
            select(InventoryTxn).where(InventoryTxn.txn_type == "SPECIAL_OUT").order_by(InventoryTxn.id)
        ).all()
        self.assertEqual(len(txns), 2)
        self.assertEqual(result.archive_status, ARCHIVE_STATUS_READY)
        self.assertEqual(result.archive_document_type, DOCUMENT_TYPE_WAREHOUSE_OPERATION)
        self.assertEqual(result.archive_business_id, txns[0].id)
        archive_business_ids = set(
            self.db.scalars(
                select(DocumentArchive.business_id).where(
                    DocumentArchive.document_type == DOCUMENT_TYPE_WAREHOUSE_OPERATION,
                    DocumentArchive.business_id.in_([txn.id for txn in txns]),
                )
            ).all()
        )
        self.assertEqual(archive_business_ids, {txn.id for txn in txns})

    def test_special_out_rejects_quantity_greater_than_auxiliary_available_quantity(self) -> None:
        with self.assertRaises(ValueError) as exc:
            create_special_inventory_adjustment(
                self.db,
                self._adjustment_payload(
                    direction="OUT",
                    warehouse_id=self.aux_warehouse.id,
                    item_id=self.aux_item.id,
                    lot_id=self.aux_lot.id,
                    adjust_qty=Decimal("7"),
                    reason="盘亏调整",
                ),
                user_id=9,
            )

        self.assertIn("特殊出库数量不能超过当前可用数量", str(exc.exception))
        self.db.refresh(self.aux_lot)
        self.db.refresh(self.aux_balance)
        self.assertEqual(self.aux_lot.remaining_qty, Decimal("6.000000"))
        self.assertEqual(self.aux_balance.qty_available, Decimal("6.000000"))
        self.assertIsNone(self._only_txn("SPECIAL_OUT"))

    def test_auxiliary_special_in_increases_quantity_only_and_writes_ledger(self) -> None:
        create_special_inventory_adjustment(
            self.db,
            self._adjustment_payload(
                direction="IN",
                warehouse_id=self.aux_warehouse.id,
                item_id=self.aux_item.id,
                location_id=self.aux_location.id,
                adjust_qty=Decimal("4"),
                adjust_weight_kg=Decimal("99"),
                reason="供应商补送",
                unit_cost=Decimal("2.5"),
            ),
            user_id=9,
        )

        lot = self.db.scalar(
            select(StockLot).where(
                StockLot.item_id == self.aux_item.id,
                StockLot.warehouse_id == self.aux_warehouse.id,
                StockLot.source_doc_type == "SPECIAL_ADJUSTMENT",
            )
        )
        self.db.refresh(self.aux_balance)
        txn = self._only_txn("SPECIAL_IN")
        self.assertIsNotNone(lot)
        self.assertEqual(lot.inbound_qty, Decimal("4.000000"))
        self.assertEqual(lot.remaining_qty, Decimal("4.000000"))
        self.assertEqual(lot.inbound_weight_kg, Decimal("0.000000"))
        self.assertEqual(lot.remaining_weight_kg, Decimal("0.000000"))
        self.assertEqual(self.aux_balance.qty_on_hand, Decimal("10.000000"))
        self.assertEqual(self.aux_balance.qty_available, Decimal("10.000000"))
        self.assertEqual(self.aux_balance.weight_on_hand_kg, Decimal("0.000000"))
        self.assertEqual(self.aux_balance.weight_available_kg, Decimal("0.000000"))
        self.assertIsNotNone(txn)
        self.assertEqual(txn.qty_change, Decimal("4.000000"))
        self.assertEqual(txn.weight_change_kg, Decimal("0.000000"))
        self.assertIn("特殊入库说明：供应商补送", txn.remark)

    def test_raw_special_in_generates_global_raw_lot_number(self) -> None:
        create_special_inventory_adjustment(
            self.db,
            self._adjustment_payload(
                direction="IN",
                warehouse_id=self.raw_warehouse.id,
                item_id=self.raw_item.id,
                location_id=self.raw_location.id,
                adjust_weight_kg=Decimal("15"),
                reason="供应商补送原料",
                unit_cost=Decimal("10"),
            ),
            user_id=9,
        )

        lot = self.db.scalar(
            select(StockLot).where(
                StockLot.item_id == self.raw_item.id,
                StockLot.warehouse_id == self.raw_warehouse.id,
                StockLot.source_doc_type == "SPECIAL_ADJUSTMENT",
            )
        )

        self.assertIsNotNone(lot)
        self.assertEqual(lot.lot_no, "YL0001")

    def test_finished_special_out_deducts_quantity_and_weight_together(self) -> None:
        create_special_inventory_adjustment(
            self.db,
            self._adjustment_payload(
                direction="OUT",
                warehouse_id=self.finished_warehouse.id,
                item_id=self.finished_item.id,
                lot_id=self.finished_lot.id,
                adjust_qty=Decimal("3"),
                adjust_weight_kg=Decimal("7.5"),
                reason="展会样品",
            ),
            user_id=9,
        )

        self.db.refresh(self.finished_lot)
        self.db.refresh(self.finished_balance)
        txn = self._only_txn("SPECIAL_OUT")
        self.assertEqual(self.finished_lot.remaining_qty, Decimal("9.000000"))
        self.assertEqual(self.finished_lot.remaining_weight_kg, Decimal("22.500000"))
        self.assertEqual(self.finished_balance.qty_on_hand, Decimal("9.000000"))
        self.assertEqual(self.finished_balance.qty_available, Decimal("9.000000"))
        self.assertEqual(self.finished_balance.weight_on_hand_kg, Decimal("22.500000"))
        self.assertEqual(self.finished_balance.weight_available_kg, Decimal("22.500000"))
        self.assertIsNotNone(txn)
        self.assertEqual(txn.qty_change, Decimal("-3.000000"))
        self.assertEqual(txn.weight_change_kg, Decimal("-7.500000"))
        self.assertIn("特殊出库说明：展会样品", txn.remark)


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