from __future__ import annotations

import unittest
from datetime import UTC, datetime
from decimal import Decimal
from io import BytesIO

from openpyxl import Workbook, load_workbook
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.operations  # noqa: E402,F401
from app.models.base import Base  # noqa: E402
from app.models.master_data import Item, StockBalance, Warehouse  # noqa: E402
from app.models.operations import InventoryTxn, StockLot, WarehouseLocation  # noqa: E402


class OpeningInventoryImportTest(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()

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

    def _seed(self) -> None:
        now = datetime.now(UTC)
        self.raw_warehouse = Warehouse(
            id=1,
            warehouse_code="WH-RAW",
            warehouse_name="原材料仓",
            warehouse_type="RAW",
            status="ACTIVE",
            created_at=now,
            updated_at=now,
        )
        self.finished_warehouse = Warehouse(
            id=2,
            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.finished_location = WarehouseLocation(
            id=2,
            warehouse_id=2,
            location_code="FG-A",
            location_name="成品主库位",
            is_default=1,
            is_locked=0,
            status="ACTIVE",
            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("1"),
            safety_stock_weight_kg=Decimal("0"),
            scrap_sale_price=Decimal("0"),
            status="ACTIVE",
            created_at=now,
            updated_at=now,
        )
        self.finished_item = Item(
            id=2,
            item_code="产品需规00001",
            item_name="五金支架",
            item_type="FINISHED_GOOD",
            unit_weight_kg=Decimal("2.5"),
            safety_stock_weight_kg=Decimal("0"),
            scrap_sale_price=Decimal("0"),
            status="ACTIVE",
            created_at=now,
            updated_at=now,
        )
        self.db.add_all([
            self.raw_warehouse,
            self.finished_warehouse,
            self.raw_location,
            self.finished_location,
            self.raw_item,
            self.finished_item,
        ])
        self.db.commit()

    def _workbook_bytes(self, rows: list[list[object]]) -> bytes:
        from app.api.routes.inventory import OPENING_INVENTORY_HEADERS

        workbook = Workbook()
        worksheet = workbook.active
        worksheet.title = "期初导入"
        worksheet.append(OPENING_INVENTORY_HEADERS)
        for row in rows:
            worksheet.append(row)
        output = BytesIO()
        workbook.save(output)
        return output.getvalue()

    def test_opening_template_exports_expected_headers(self) -> None:
        from app.api.routes.inventory import OPENING_INVENTORY_HEADERS, build_opening_inventory_template

        workbook = build_opening_inventory_template(self.db, "FINISHED")
        output = BytesIO()
        workbook.save(output)
        loaded = load_workbook(BytesIO(output.getvalue()), read_only=True, data_only=True)
        rows = list(loaded["成品库期初导入"].iter_rows(values_only=True))
        headers = list(rows[0])

        self.assertEqual(headers, OPENING_INVENTORY_HEADERS)
        self.assertEqual(len(rows), 3)
        self.assertEqual(rows[1][headers.index("仓库类型")], "成品库")
        self.assertEqual(rows[1][headers.index("批次号")], "示例-FG-0001")
        self.assertEqual(rows[2][headers.index("批次号")], "示例-FG-0002")

    def test_raw_opening_template_uses_weight_only_headers(self) -> None:
        from app.api.routes.inventory import build_opening_inventory_template

        workbook = build_opening_inventory_template(self.db, "RAW")
        output = BytesIO()
        workbook.save(output)
        loaded = load_workbook(BytesIO(output.getvalue()), read_only=True, data_only=True)
        rows = list(loaded["原材料库期初导入"].iter_rows(values_only=True))
        headers = list(rows[0])

        self.assertNotIn("数量", headers)
        self.assertIn("重量(kg)", headers)
        self.assertEqual(rows[1][headers.index("仓库类型")], "原材料库")
        self.assertEqual(rows[1][headers.index("重量(kg)")], 500)

    def test_import_opening_template_ignores_sample_rows(self) -> None:
        from app.api.routes.inventory import build_opening_inventory_template, import_opening_inventory_workbook

        workbook = build_opening_inventory_template(self.db, "AUX")
        output = BytesIO()
        workbook.save(output)

        with self.assertRaises(ValueError) as exc:
            import_opening_inventory_workbook(self.db, "AUX", output.getvalue(), user_id=1)

        self.assertIn("没有可导入的期初库存明细", str(exc.exception))

    def test_import_opening_inventory_writes_finished_qty_weight_balance_and_txn(self) -> None:
        from app.api.routes.inventory import import_opening_inventory_workbook

        content = self._workbook_bytes(
            [[
                "成品库",
                "成品仓",
                "成品主库位",
                "产品需规00001",
                "五金支架",
                "OPEN-FG-001",
                12,
                30,
                8.5,
                "初始化清点",
                "期初成品",
            ]]
        )

        result = import_opening_inventory_workbook(self.db, "FINISHED", content, user_id=1)

        lot = self.db.scalar(select(StockLot).where(StockLot.lot_no == "OPEN-FG-001"))
        balance = self.db.scalar(select(StockBalance).where(StockBalance.item_id == self.finished_item.id))
        txn = self.db.scalar(select(InventoryTxn).where(InventoryTxn.lot_id == lot.id))

        self.assertEqual(result["imported"], 1)
        self.assertEqual(float(lot.inbound_qty), 12)
        self.assertEqual(float(lot.inbound_weight_kg), 30)
        self.assertEqual(float(lot.remaining_qty), 12)
        self.assertEqual(float(lot.remaining_weight_kg), 30)
        self.assertEqual(float(balance.qty_on_hand), 12)
        self.assertEqual(float(balance.weight_on_hand_kg), 30)
        self.assertEqual(float(txn.qty_change), 12)
        self.assertEqual(float(txn.weight_change_kg), 30)
        self.assertEqual(txn.txn_type, "OPENING_IN")

    def test_import_opening_inventory_keeps_raw_material_quantity_zero(self) -> None:
        from app.api.routes.inventory import import_opening_inventory_workbook

        content = self._workbook_bytes(
            [[
                "原材料库",
                "原材料仓",
                "原料主库位",
                "原材料00001",
                "304不锈钢",
                "OPEN-RAW-001",
                99,
                500,
                4.2,
                "初始化清点",
                "期初原材料",
            ]]
        )

        import_opening_inventory_workbook(self.db, "RAW", content, user_id=1)

        lot = self.db.scalar(select(StockLot).where(StockLot.lot_no == "YL0001"))
        balance = self.db.scalar(select(StockBalance).where(StockBalance.item_id == self.raw_item.id))
        txn = self.db.scalar(select(InventoryTxn).where(InventoryTxn.lot_id == lot.id))

        self.assertIsNotNone(lot)
        self.assertEqual(float(lot.inbound_qty), 0)
        self.assertEqual(float(lot.remaining_qty), 0)
        self.assertEqual(float(balance.qty_on_hand), 0)
        self.assertEqual(float(txn.qty_change), 0)
        self.assertEqual(float(lot.inbound_weight_kg), 500)

    def test_import_opening_inventory_generates_global_raw_lot_numbers(self) -> None:
        from app.api.routes.inventory import import_opening_inventory_workbook

        content = self._workbook_bytes(
            [
                [
                    "原材料库",
                    "原材料仓",
                    "原料主库位",
                    "原材料00001",
                    "304不锈钢",
                    "用户手填RAW-001",
                    0,
                    500,
                    4.2,
                    "初始化清点",
                    "期初原材料1",
                ],
                [
                    "原材料库",
                    "原材料仓",
                    "原料主库位",
                    "原材料00001",
                    "304不锈钢",
                    "用户手填RAW-002",
                    0,
                    600,
                    4.2,
                    "初始化清点",
                    "期初原材料2",
                ],
            ]
        )

        import_opening_inventory_workbook(self.db, "RAW", content, user_id=1)

        lot_nos = self.db.scalars(
            select(StockLot.lot_no)
            .where(
                StockLot.item_id == self.raw_item.id,
                StockLot.source_doc_type == "OPENING_STOCK",
            )
            .order_by(StockLot.id)
        ).all()

        self.assertEqual(lot_nos, ["YL0001", "YL0002"])

    def test_import_opening_inventory_accepts_raw_weight_only_template_without_quantity(self) -> None:
        from app.api.routes.inventory import import_opening_inventory_workbook

        workbook = Workbook()
        worksheet = workbook.active
        worksheet.title = "原材料库期初导入"
        worksheet.append(["仓库类型", "仓库名称", "库位", "物料编码", "物料名称", "批次号", "重量(kg)", "单价", "来源说明", "备注"])
        worksheet.append(["原材料库", "原材料仓", "原料主库位", "原材料00001", "304不锈钢", "OPEN-RAW-WEIGHT-ONLY", 600, 4.2, "初始化清点", "无数量列"])
        output = BytesIO()
        workbook.save(output)

        import_opening_inventory_workbook(self.db, "RAW", output.getvalue(), user_id=1)

        lot = self.db.scalar(select(StockLot).where(StockLot.lot_no == "YL0001"))
        balance = self.db.scalar(select(StockBalance).where(StockBalance.item_id == self.raw_item.id))

        self.assertIsNotNone(lot)
        self.assertEqual(float(lot.inbound_qty), 0)
        self.assertEqual(float(lot.remaining_qty), 0)
        self.assertEqual(float(lot.inbound_weight_kg), 600)
        self.assertEqual(float(balance.qty_on_hand), 0)
        self.assertEqual(float(balance.weight_on_hand_kg), 600)


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