from __future__ import annotations

import unittest
from datetime import datetime
from decimal import Decimal
from tempfile import TemporaryDirectory
from types import SimpleNamespace
from unittest.mock import patch

from sqlalchemy import BigInteger, create_engine, select
from sqlalchemy.ext.compiler import compiles
from sqlalchemy.orm import Session, sessionmaker
from sqlalchemy.pool import StaticPool


@compiles(BigInteger, "sqlite")
def _compile_big_integer_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, StockBalance, Warehouse  # noqa: E402
from app.models.operations import InventoryTxn, StockLot, WarehouseLocation  # noqa: E402
from app.api.routes.inventory import create_warehouse_inbound, create_warehouse_outbound  # noqa: E402
from app.schemas.operations import WarehouseInboundCreate, WarehouseOutboundCreate  # noqa: E402
from app.services.document_archives import (  # noqa: E402
    ARCHIVE_STATUS_READY,
    DOCUMENT_TYPE_WAREHOUSE_OPERATION,
    collect_archive_context,
    collect_warehouse_operation_archive_context,
    generate_document_archive,
)


class WarehouseOperationDocumentArchiveTest(unittest.TestCase):
    def setUp(self) -> None:
        engine = create_engine(
            "sqlite+pysqlite:///:memory:",
            connect_args={"check_same_thread": False},
            poolclass=StaticPool,
            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, 11, 9, 0, 0)

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

    def _seed_customer_supplied_txn(self) -> InventoryTxn:
        warehouse = Warehouse(
            id=1,
            warehouse_code="RAW",
            warehouse_name="原材料库",
            warehouse_type="RAW",
            status="ACTIVE",
            created_at=self.now,
            updated_at=self.now,
        )
        location = WarehouseLocation(
            id=2,
            warehouse_id=warehouse.id,
            location_code="RAW-A",
            location_name="原材料暂存位",
            status="ACTIVE",
            created_at=self.now,
            updated_at=self.now,
        )
        item = Item(
            id=3,
            item_code="RM00001",
            item_name="冷轧钢板",
            item_type="RAW_MATERIAL",
            specification="1.2mm",
            unit_weight_kg=Decimal("1"),
            status="ACTIVE",
            created_at=self.now,
            updated_at=self.now,
        )
        source_lot = StockLot(
            id=4,
            lot_no="SRC-YL0001",
            lot_role="RAW_MATERIAL",
            item_id=item.id,
            warehouse_id=warehouse.id,
            source_doc_type="期初入库",
            source_doc_id=1,
            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("0"),
            quality_status="PASS",
            status="AVAILABLE",
            created_at=self.now,
            updated_at=self.now,
        )
        lot = StockLot(
            id=5,
            lot_no="YL0001",
            lot_role="RAW_MATERIAL",
            item_id=item.id,
            warehouse_id=warehouse.id,
            location_id=location.id,
            source_doc_type="客料入库",
            source_doc_id=1,
            source_material_lot_id=source_lot.id,
            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("0"),
            quality_status="PASS",
            status="AVAILABLE",
            created_at=self.now,
            updated_at=self.now,
        )
        txn = InventoryTxn(
            id=6,
            txn_no="TXN-CUSTOMER-001",
            txn_type="CUSTOMER_SUPPLIED_IN",
            item_id=item.id,
            warehouse_id=warehouse.id,
            location_id=location.id,
            lot_id=lot.id,
            qty_change=Decimal("0"),
            weight_change_kg=Decimal("100"),
            unit_cost=Decimal("0"),
            amount=Decimal("0"),
            source_doc_type="客料入库",
            source_doc_id=1,
            logistics_waybill_no="SF123",
            logistics_freight_amount=Decimal("12.5"),
            logistics_photo_url="/uploads/demo.jpg",
            biz_time=self.now,
            remark="客户来料测试",
            created_at=self.now,
            updated_at=self.now,
        )
        balance = StockBalance(
            id=7,
            item_id=item.id,
            warehouse_id=warehouse.id,
            location_id=location.id,
            qty_on_hand=Decimal("0"),
            weight_on_hand_kg=Decimal("100"),
            qty_available=Decimal("0"),
            weight_available_kg=Decimal("100"),
            qty_allocated=Decimal("0"),
            weight_allocated_kg=Decimal("0"),
            avg_unit_cost=Decimal("0"),
            updated_at=self.now,
        )
        self.db.add_all([warehouse, location, item, source_lot, lot, txn, balance])
        self.db.commit()
        return txn

    def test_collect_warehouse_operation_archive_context_uses_inventory_txn(self) -> None:
        txn = self._seed_customer_supplied_txn()

        context = collect_warehouse_operation_archive_context(self.db, txn.id)

        self.assertEqual(context.document_type, DOCUMENT_TYPE_WAREHOUSE_OPERATION)
        self.assertEqual(context.business_id, txn.id)
        self.assertEqual(context.title, "客料入库单归档")
        self.assertEqual(context.document_no, "客料入库-TXN-CUSTOMER-001")
        self.assertEqual(context.partner_label, "仓库")
        self.assertEqual(context.partner_name, "原材料库")
        self.assertEqual(context.status, "客料入库")
        self.assertEqual(context.address, "原材料库 / 原材料暂存位")
        self.assertEqual(context.total_amount, Decimal("0"))
        self.assertEqual(len(context.lines), 1)
        line = context.lines[0]
        self.assertEqual(line.item_code, "RM00001")
        self.assertEqual(line.item_name, "冷轧钢板")
        self.assertEqual(line.quantity, Decimal("100.000000"))
        self.assertEqual(line.delivered_or_received_quantity, Decimal("100.000000"))
        self.assertIn("库存流水号：TXN-CUSTOMER-001", line.remark or "")
        self.assertIn("库存批次号：YL0001", line.remark or "")
        self.assertIn("来源库存批次号：SRC-YL0001", line.remark or "")
        self.assertIn("库位：原材料暂存位", line.remark or "")
        self.assertIn("运单号：SF123", line.remark or "")
        self.assertIn("运费：12.5", line.remark or "")
        self.assertIn("辅助照片：已上传", line.remark or "")
        self.assertIn("客户来料测试", line.remark or "")

    def test_collect_archive_context_dispatches_warehouse_operation(self) -> None:
        txn = self._seed_customer_supplied_txn()

        context = collect_archive_context(self.db, DOCUMENT_TYPE_WAREHOUSE_OPERATION, txn.id)

        self.assertEqual(context.document_type, DOCUMENT_TYPE_WAREHOUSE_OPERATION)
        self.assertEqual(context.business_id, txn.id)

    def test_unknown_english_txn_type_is_not_exposed_in_context(self) -> None:
        txn = self._seed_customer_supplied_txn()
        txn.txn_type = "unknown_code"
        self.db.commit()

        context = collect_warehouse_operation_archive_context(self.db, txn.id)

        self.assertEqual(context.status, "未识别仓库操作")
        self.assertEqual(context.title, "未识别仓库操作单归档")
        self.assertEqual(context.document_no, "未识别仓库操作-TXN-CUSTOMER-001")
        self.assertNotIn("unknown_code", context.remark)
        self.assertNotIn("unknown_code", context.lines[0].remark or "")

    def test_generate_document_archive_creates_pdf_for_warehouse_operation(self) -> None:
        txn = self._seed_customer_supplied_txn()

        with TemporaryDirectory() as tmp_dir, patch("app.services.document_archives.default_archive_root", return_value=tmp_dir):
            result = generate_document_archive(self.db, DOCUMENT_TYPE_WAREHOUSE_OPERATION, txn.id)

            archive = self.db.scalar(
                select(DocumentArchive).where(
                    DocumentArchive.document_type == DOCUMENT_TYPE_WAREHOUSE_OPERATION,
                    DocumentArchive.business_id == txn.id,
                )
            )
            self.assertIsNotNone(archive)
            self.assertEqual(result.archive_status, ARCHIVE_STATUS_READY)
            self.assertEqual(archive.status, ARCHIVE_STATUS_READY)
            self.assertTrue(archive.file_name.endswith(".pdf"))
            with open(archive.file_path, "rb") as pdf_file:
                self.assertEqual(pdf_file.read(4), b"%PDF")

    def test_customer_supplied_inbound_save_generates_warehouse_archive(self) -> None:
        self._seed_customer_supplied_txn()
        payload = WarehouseInboundCreate(
            biz_type="CUSTOMER_SUPPLIED",
            item_id=3,
            warehouse_id=1,
            location_id=2,
            inbound_weight_kg=25,
            inbound_qty=0,
            unit_cost=0,
            provider_name="测试客户",
            waybill_no="SF999",
            freight_amount=Decimal("8.5"),
            remark="保存时生成仓库出入库单",
        )
        context = SimpleNamespace(user=SimpleNamespace(id=99))

        with TemporaryDirectory() as tmp_dir, patch("app.services.document_archives.default_archive_root", return_value=tmp_dir):
            result = create_warehouse_inbound(payload=payload, context=context, db=self.db)

            self.assertEqual(result.archive_status, ARCHIVE_STATUS_READY)
            self.assertEqual(result.archive_document_type, DOCUMENT_TYPE_WAREHOUSE_OPERATION)
            self.assertIsNotNone(result.archive_business_id)
            archive = self.db.scalar(
                select(DocumentArchive).where(
                    DocumentArchive.document_type == DOCUMENT_TYPE_WAREHOUSE_OPERATION,
                    DocumentArchive.business_id == result.archive_business_id,
                )
            )
            self.assertIsNotNone(archive)
            self.assertTrue(archive.file_name.endswith(".pdf"))

    def test_outsourcing_outbound_save_generates_warehouse_archive(self) -> None:
        self._seed_customer_supplied_txn()
        payload = WarehouseOutboundCreate(
            biz_type="OUTSOURCING_OUT",
            item_id=3,
            warehouse_id=1,
            location_id=2,
            outbound_weight_kg=10,
            outsourcing_party_name="测试委外厂",
            waybill_no="SF-WW-001",
            freight_amount=Decimal("9.9"),
            remark="委外出库保存归档",
        )
        context = SimpleNamespace(user=SimpleNamespace(id=99))

        with TemporaryDirectory() as tmp_dir, patch("app.services.document_archives.default_archive_root", return_value=tmp_dir):
            result = create_warehouse_outbound(payload=payload, context=context, db=self.db)

            self.assertEqual(result.archive_status, ARCHIVE_STATUS_READY)
            self.assertEqual(result.archive_document_type, DOCUMENT_TYPE_WAREHOUSE_OPERATION)
            self.assertIsNotNone(result.archive_business_id)
            archive = self.db.scalar(
                select(DocumentArchive).where(
                    DocumentArchive.document_type == DOCUMENT_TYPE_WAREHOUSE_OPERATION,
                    DocumentArchive.business_id == result.archive_business_id,
                )
            )
            self.assertIsNotNone(archive)
            self.assertTrue(archive.file_name.endswith(".pdf"))


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