"""
Inventory Module — Model Unit Tests.
تست‌های واحد برای مدل‌های ماژول انبار و موجودی.
"""
import uuid
from datetime import date
from decimal import Decimal

import pytest
from django.db import IntegrityError

from modules.inventory.backend.infrastructure.persistence.models import (
    ItemTypeModel,
    ItemGroupModel,
    UnitOfMeasureModel,
    ItemModel,
    ItemAttributeModel,
    ItemUOMModel,
    WarehouseModel,
    StorageLocationModel,
    ItemWarehouseModel,
)
from modules.inventory.backend.infrastructure.persistence.models_extended import (
    CostingMethodModel,
    StockStatusModel,
    BatchModel,
    SerialNumberModel,
    SerialNumberHistoryModel,
    StockLedgerEntryModel,
    StockBalanceModel,
    ItemCostingModel,
    CostLayerModel,
    FinancialPostingModel,
    StockReservationModel,
    StockAllocationModel,
    PhysicalInventoryModel,
    PhysicalInventoryLineModel,
    CycleCountScheduleModel,
    InspectionLotModel,
    InspectionResultModel,
    QualityActionModel,
    StockMovementSummaryModel,
    StockAgingBucketModel,
)

pytestmark = pytest.mark.django_db


# ═══════════════════════════════════════════════════════════════
# 1. ITEM TYPE
# ═══════════════════════════════════════════════════════════════


class TestItemTypeModel:
    def test_create(self, item_type):
        assert item_type.pk is not None
        assert item_type.type_code == "RAW"
        assert item_type.type_name == "ماده اولیه"
        assert item_type.is_active is True

    def test_str(self, item_type):
        assert str(item_type) == "ماده اولیه"

    def test_unique_constraint(self, tenant, item_type):
        with pytest.raises(IntegrityError):
            ItemTypeModel.objects.create(
                id=uuid.uuid4(), tenant=tenant,
                type_code="RAW", type_name="تکراری",
            )


# ═══════════════════════════════════════════════════════════════
# 2. ITEM GROUP
# ═══════════════════════════════════════════════════════════════


class TestItemGroupModel:
    def test_create(self, item_group):
        assert item_group.pk is not None
        assert item_group.group_code == "MTL"
        assert item_group.group_name == "مواد"
        assert item_group.level == 0
        assert item_group.parent_group is None

    def test_str(self, item_group):
        assert str(item_group) == "مواد"

    def test_hierarchy(self, item_group, item_group_child):
        assert item_group_child.parent_group == item_group
        assert item_group_child.level == 1

    def test_unique_constraint(self, tenant, item_group, costing_method):
        with pytest.raises(IntegrityError):
            ItemGroupModel.objects.create(
                id=uuid.uuid4(), tenant=tenant,
                group_code="MTL", group_name="duplicate",
                level=0,
            )

    def test_costing_method_relation(self, item_group, costing_method):
        assert item_group.default_costing_method == costing_method


# ═══════════════════════════════════════════════════════════════
# 3. UNIT OF MEASURE
# ═══════════════════════════════════════════════════════════════


class TestUnitOfMeasureModel:
    def test_create(self, uom):
        assert uom.pk is not None
        assert uom.uom_code == "KG"
        assert uom.uom_name == "Kilogram"
        assert uom.uom_name_local == "کیلوگرم"
        assert uom.uom_type == UnitOfMeasureModel.UOMType.WEIGHT
        assert uom.decimal_places == 2

    def test_str(self, uom):
        assert "KG" in str(uom) or "Kilogram" in str(uom)

    def test_uom_types(self, tenant):
        for uom_type in UnitOfMeasureModel.UOMType:
            obj = UnitOfMeasureModel.objects.create(
                id=uuid.uuid4(), tenant=tenant,
                uom_code=f"T-{uom_type.value[:3]}", uom_name=uom_type.label,
                uom_type=uom_type,
            )
            assert obj.uom_type == uom_type

    def test_unique_constraint(self, tenant, uom):
        with pytest.raises(IntegrityError):
            UnitOfMeasureModel.objects.create(
                id=uuid.uuid4(), tenant=tenant,
                uom_code="KG", uom_name="duplicate",
            )


# ═══════════════════════════════════════════════════════════════
# 4. ITEM
# ═══════════════════════════════════════════════════════════════


class TestItemModel:
    def test_create(self, item):
        assert item.pk is not None
        assert item.item_code == "MAT-001"
        assert item.item_name == "Steel Rod"
        assert item.is_batch_managed is True
        assert item.is_serial_managed is False
        assert item.item_status == ItemModel.ItemStatus.ACTIVE

    def test_str(self, item):
        assert "MAT-001" in str(item) or "Steel Rod" in str(item)

    def test_unique_constraint(self, tenant, item, item_type, item_group, uom):
        with pytest.raises(IntegrityError):
            ItemModel.objects.create(
                id=uuid.uuid4(), tenant=tenant,
                item_code="MAT-001", item_name="dup",
                item_type=item_type, item_group=item_group,
                base_uom=uom,
            )

    def test_relationships(self, item, item_type, item_group, uom):
        assert item.item_type == item_type
        assert item.item_group == item_group
        assert item.base_uom == uom

    def test_item_statuses(self):
        statuses = [c[0] for c in ItemModel.ItemStatus.choices]
        assert "ACTIVE" in statuses
        assert "INACTIVE" in statuses
        assert "OBSOLETE" in statuses


# ═══════════════════════════════════════════════════════════════
# 5. ITEM ATTRIBUTE
# ═══════════════════════════════════════════════════════════════


class TestItemAttributeModel:
    def test_create(self, item_attribute):
        assert item_attribute.pk is not None
        assert item_attribute.attribute_name == "قطر"
        assert item_attribute.attribute_value == "12mm"

    def test_item_relation(self, item_attribute, item):
        assert item_attribute.item == item


# ═══════════════════════════════════════════════════════════════
# 6. ITEM UOM CONVERSION
# ═══════════════════════════════════════════════════════════════


class TestItemUOMModel:
    def test_create(self, item_uom):
        assert item_uom.pk is not None
        assert item_uom.conversion_factor == Decimal("1.0")
        assert item_uom.is_base_uom is True

    def test_relations(self, item_uom, item, uom):
        assert item_uom.item == item
        assert item_uom.uom == uom


# ═══════════════════════════════════════════════════════════════
# 7. WAREHOUSE
# ═══════════════════════════════════════════════════════════════


class TestWarehouseModel:
    def test_create(self, warehouse):
        assert warehouse.pk is not None
        assert warehouse.warehouse_code == "WH-01"
        assert warehouse.warehouse_name == "انبار مرکزی"
        assert warehouse.warehouse_type == WarehouseModel.WarehouseType.PHYSICAL
        assert warehouse.is_negative_stock_allowed is False

    def test_str(self, warehouse):
        assert "WH-01" in str(warehouse) or "انبار" in str(warehouse)

    def test_unique_constraint(self, tenant, warehouse):
        with pytest.raises(IntegrityError):
            WarehouseModel.objects.create(
                id=uuid.uuid4(), tenant=tenant,
                warehouse_code="WH-01", warehouse_name="dup",
            )

    def test_warehouse_types(self):
        types = [c[0] for c in WarehouseModel.WarehouseType.choices]
        assert "PHYSICAL" in types
        assert "VIRTUAL" in types
        assert "TRANSIT" in types
        assert "QUARANTINE" in types


# ═══════════════════════════════════════════════════════════════
# 8. STORAGE LOCATION
# ═══════════════════════════════════════════════════════════════


class TestStorageLocationModel:
    def test_create(self, storage_location):
        assert storage_location.pk is not None
        assert storage_location.location_code == "A-01-01"
        assert storage_location.location_type == StorageLocationModel.LocationType.RACK
        assert storage_location.is_picking_location is True

    def test_str(self, storage_location):
        assert "A-01-01" in str(storage_location) or "ناحیه" in str(storage_location)

    def test_warehouse_relation(self, storage_location, warehouse):
        assert storage_location.warehouse == warehouse

    def test_unique_constraint(self, tenant, warehouse, storage_location):
        with pytest.raises(IntegrityError):
            StorageLocationModel.objects.create(
                id=uuid.uuid4(), tenant=tenant,
                location_code="A-01-01", location_name="dup",
                warehouse=warehouse,
            )


# ═══════════════════════════════════════════════════════════════
# 9. ITEM WAREHOUSE
# ═══════════════════════════════════════════════════════════════


class TestItemWarehouseModel:
    def test_create(self, item_warehouse):
        assert item_warehouse.pk is not None
        assert item_warehouse.min_stock_level == Decimal("100.0000")
        assert item_warehouse.reorder_point == Decimal("500.0000")

    def test_relations(self, item_warehouse, item, warehouse, storage_location):
        assert item_warehouse.item == item
        assert item_warehouse.warehouse == warehouse
        assert item_warehouse.default_storage_location == storage_location


# ═══════════════════════════════════════════════════════════════
# 10. COSTING METHOD
# ═══════════════════════════════════════════════════════════════


class TestCostingMethodModel:
    def test_create(self, costing_method):
        assert costing_method.pk is not None
        assert costing_method.method_code == "FIFO"
        assert costing_method.is_active is True

    def test_str(self, costing_method):
        assert str(costing_method) == "اولین ورود اولین خروج"

    def test_unique_constraint(self, tenant, costing_method):
        with pytest.raises(IntegrityError):
            CostingMethodModel.objects.create(
                id=uuid.uuid4(), tenant=tenant,
                method_code="FIFO", method_name="dup",
            )


# ═══════════════════════════════════════════════════════════════
# 11. STOCK STATUS
# ═══════════════════════════════════════════════════════════════


class TestStockStatusModel:
    def test_create(self, stock_status):
        assert stock_status.pk is not None
        assert stock_status.status_code == "AVAIL"
        assert stock_status.status_type == StockStatusModel.StatusType.AVAILABLE
        assert stock_status.is_available_for_sales is True

    def test_blocked_status(self, stock_status_blocked):
        assert stock_status_blocked.is_blocked_for_movement is True
        assert stock_status_blocked.is_available_for_sales is False

    def test_unique_constraint(self, tenant, stock_status):
        with pytest.raises(IntegrityError):
            StockStatusModel.objects.create(
                id=uuid.uuid4(), tenant=tenant,
                status_code="AVAIL", status_name="dup",
            )


# ═══════════════════════════════════════════════════════════════
# 12. BATCH
# ═══════════════════════════════════════════════════════════════


class TestBatchModel:
    def test_create(self, batch):
        assert batch.pk is not None
        assert batch.batch_number == "B-2024-001"
        assert batch.manufacturing_date == date(2024, 1, 1)
        assert batch.expiry_date == date(2025, 1, 1)
        assert batch.batch_status == BatchModel.BatchStatus.ACTIVE
        assert batch.quality_status == BatchModel.QualityStatus.APPROVED

    def test_str(self, batch):
        assert "B-2024-001" in str(batch)

    def test_item_relation(self, batch, item):
        assert batch.item == item

    def test_unique_constraint(self, tenant, batch, item):
        with pytest.raises(IntegrityError):
            BatchModel.objects.create(
                id=uuid.uuid4(), tenant=tenant,
                batch_number="B-2024-001", item=item,
            )


# ═══════════════════════════════════════════════════════════════
# 13. SERIAL NUMBER
# ═══════════════════════════════════════════════════════════════


class TestSerialNumberModel:
    def test_create(self, serial_number):
        assert serial_number.pk is not None
        assert serial_number.serial_no == "SN-2024-0001"
        assert serial_number.serial_status == SerialNumberModel.SerialStatus.IN_STOCK

    def test_str(self, serial_number):
        assert "SN-2024-0001" in str(serial_number)

    def test_relations(self, serial_number, item_serial_managed, warehouse):
        assert serial_number.item == item_serial_managed
        assert serial_number.current_warehouse == warehouse

    def test_unique_constraint(self, tenant, serial_number, item_serial_managed):
        with pytest.raises(IntegrityError):
            SerialNumberModel.objects.create(
                id=uuid.uuid4(), tenant=tenant,
                serial_no="SN-2024-0001", item=item_serial_managed,
            )


# ═══════════════════════════════════════════════════════════════
# 14. STOCK LEDGER ENTRY
# ═══════════════════════════════════════════════════════════════


class TestStockLedgerEntryModel:
    def test_create(self, stock_ledger_entry):
        assert stock_ledger_entry.pk is not None
        assert stock_ledger_entry.entry_number == "SLE-2024-001"
        assert stock_ledger_entry.posting_date == date(2024, 1, 15)
        assert stock_ledger_entry.transaction_type == StockLedgerEntryModel.TransactionType.GR
        assert stock_ledger_entry.quantity == Decimal("1000.0000")

    def test_str(self, stock_ledger_entry):
        assert "SLE-2024-001" in str(stock_ledger_entry)

    def test_relations(self, stock_ledger_entry, item, warehouse, uom, stock_status):
        assert stock_ledger_entry.item == item
        assert stock_ledger_entry.warehouse == warehouse
        assert stock_ledger_entry.uom == uom
        assert stock_ledger_entry.stock_status == stock_status

    def test_transaction_types(self):
        types = [c[0] for c in StockLedgerEntryModel.TransactionType.choices]
        assert "GR" in types
        assert "GI" in types
        assert "TRANSFER" in types
        assert "ADJUSTMENT" in types


# ═══════════════════════════════════════════════════════════════
# 15. STOCK BALANCE
# ═══════════════════════════════════════════════════════════════


class TestStockBalanceModel:
    def test_create(self, stock_balance):
        assert stock_balance.pk is not None
        assert stock_balance.quantity_on_hand == Decimal("1000.0000")
        assert stock_balance.quantity_reserved == Decimal("100.0000")
        assert stock_balance.quantity_available == Decimal("850.0000")

    def test_str(self, stock_balance):
        assert "MAT-001" in str(stock_balance)
        assert "WH-01" in str(stock_balance)

    def test_relations(self, stock_balance, item, warehouse, uom):
        assert stock_balance.item == item
        assert stock_balance.warehouse == warehouse
        assert stock_balance.uom == uom


# ═══════════════════════════════════════════════════════════════
# 16. ITEM COSTING
# ═══════════════════════════════════════════════════════════════


class TestItemCostingModel:
    def test_create(self, item_costing):
        assert item_costing.pk is not None
        assert item_costing.standard_cost == Decimal("50000.0000")
        assert item_costing.moving_average_cost == Decimal("48000.0000")
        assert item_costing.last_purchase_cost == Decimal("52000.0000")
        assert item_costing.currency_code == "IRR"

    def test_str(self, item_costing):
        assert "MAT-001" in str(item_costing)

    def test_one_to_one(self, item_costing, item):
        assert item_costing.item == item
        assert item.costing == item_costing


# ═══════════════════════════════════════════════════════════════
# 17. COST LAYER
# ═══════════════════════════════════════════════════════════════


class TestCostLayerModel:
    def test_create(self, cost_layer):
        assert cost_layer.pk is not None
        assert cost_layer.unit_cost == Decimal("50000.0000")
        assert cost_layer.original_quantity == Decimal("1000.0000")
        assert cost_layer.remaining_quantity == Decimal("800.0000")
        assert cost_layer.is_fully_consumed is False

    def test_str(self, cost_layer):
        assert "MAT-001" in str(cost_layer)

    def test_relations(self, cost_layer, item, warehouse, stock_ledger_entry):
        assert cost_layer.item == item
        assert cost_layer.warehouse == warehouse
        assert cost_layer.receipt_sle == stock_ledger_entry


# ═══════════════════════════════════════════════════════════════
# 18. FINANCIAL POSTING
# ═══════════════════════════════════════════════════════════════


class TestFinancialPostingModel:
    def test_create(self, financial_posting):
        assert financial_posting.pk is not None
        assert financial_posting.debit_account == "1301"
        assert financial_posting.credit_account == "2101"
        assert financial_posting.amount == Decimal("50000000.0000")
        assert financial_posting.posting_status == FinancialPostingModel.PostingStatus.PENDING

    def test_str(self, financial_posting):
        assert "SLE-2024-001" in str(financial_posting)

    def test_relation(self, financial_posting, stock_ledger_entry):
        assert financial_posting.stock_ledger == stock_ledger_entry


# ═══════════════════════════════════════════════════════════════
# 19. STOCK RESERVATION
# ═══════════════════════════════════════════════════════════════


class TestStockReservationModel:
    def test_create(self, reservation):
        assert reservation.pk is not None
        assert reservation.reservation_number == "RSV-2024-001"
        assert reservation.reserved_quantity == Decimal("100.0000")
        assert reservation.reservation_status == StockReservationModel.ReservationStatus.ACTIVE

    def test_str(self, reservation):
        assert "RSV-2024-001" in str(reservation)

    def test_relations(self, reservation, item, warehouse):
        assert reservation.item == item
        assert reservation.warehouse == warehouse

    def test_statuses(self):
        statuses = [c[0] for c in StockReservationModel.ReservationStatus.choices]
        assert "ACTIVE" in statuses
        assert "EXPIRED" in statuses
        assert "CANCELLED" in statuses


# ═══════════════════════════════════════════════════════════════
# 20. STOCK ALLOCATION
# ═══════════════════════════════════════════════════════════════


class TestStockAllocationModel:
    def test_create(self, allocation):
        assert allocation.pk is not None
        assert allocation.allocation_number == "ALC-2024-001"
        assert allocation.allocated_quantity == Decimal("50.0000")
        assert allocation.picked_quantity == Decimal("0.0000")
        assert allocation.allocation_status == StockAllocationModel.AllocationStatus.ALLOCATED

    def test_str(self, allocation):
        assert "ALC-2024-001" in str(allocation)

    def test_relations(self, allocation, reservation, item, warehouse):
        assert allocation.reservation == reservation
        assert allocation.item == item
        assert allocation.warehouse == warehouse


# ═══════════════════════════════════════════════════════════════
# 21. PHYSICAL INVENTORY
# ═══════════════════════════════════════════════════════════════


class TestPhysicalInventoryModel:
    def test_create(self, physical_inventory):
        assert physical_inventory.pk is not None
        assert physical_inventory.inventory_number == "PI-2024-001"
        assert physical_inventory.inventory_type == PhysicalInventoryModel.InventoryType.FULL
        assert physical_inventory.inventory_status == PhysicalInventoryModel.InventoryStatus.DRAFT
        assert physical_inventory.is_blind_count is False
        assert physical_inventory.variance_threshold_percent == Decimal("5.00")

    def test_str(self, physical_inventory):
        assert "PI-2024-001" in str(physical_inventory)

    def test_relation(self, physical_inventory, warehouse):
        assert physical_inventory.warehouse == warehouse


# ═══════════════════════════════════════════════════════════════
# 22. PHYSICAL INVENTORY LINE
# ═══════════════════════════════════════════════════════════════


class TestPhysicalInventoryLineModel:
    def test_create(self, physical_inventory_line):
        assert physical_inventory_line.pk is not None
        assert physical_inventory_line.line_number == 1
        assert physical_inventory_line.book_quantity == Decimal("1000.0000")
        assert physical_inventory_line.line_status == PhysicalInventoryLineModel.LineStatus.PENDING

    def test_str(self, physical_inventory_line):
        assert "1" in str(physical_inventory_line)

    def test_relations(self, physical_inventory_line, physical_inventory, item):
        assert physical_inventory_line.physical_inventory == physical_inventory
        assert physical_inventory_line.item == item


# ═══════════════════════════════════════════════════════════════
# 23. CYCLE COUNT SCHEDULE
# ═══════════════════════════════════════════════════════════════


class TestCycleCountScheduleModel:
    def test_create(self, cycle_count_schedule):
        assert cycle_count_schedule.pk is not None
        assert cycle_count_schedule.schedule_name == "شمارش ماهانه A"
        assert cycle_count_schedule.count_frequency_days == 30
        assert cycle_count_schedule.abc_class == "A"
        assert cycle_count_schedule.next_count_date == date(2024, 4, 1)
        assert cycle_count_schedule.is_active is True

    def test_str(self, cycle_count_schedule):
        assert "شمارش ماهانه A" in str(cycle_count_schedule)

    def test_relation(self, cycle_count_schedule, warehouse):
        assert cycle_count_schedule.warehouse == warehouse


# ═══════════════════════════════════════════════════════════════
# 24. INSPECTION LOT
# ═══════════════════════════════════════════════════════════════


class TestInspectionLotModel:
    def test_create(self, inspection_lot):
        assert inspection_lot.pk is not None
        assert inspection_lot.inspection_lot_number == "QC-2024-001"
        assert inspection_lot.inspection_type == InspectionLotModel.InspectionType.INCOMING
        assert inspection_lot.inspection_status == InspectionLotModel.InspectionStatus.PENDING
        assert inspection_lot.quality_decision == InspectionLotModel.QualityDecision.PENDING

    def test_str(self, inspection_lot):
        assert "QC-2024-001" in str(inspection_lot)

    def test_relations(self, inspection_lot, item, warehouse):
        assert inspection_lot.item == item
        assert inspection_lot.warehouse == warehouse


# ═══════════════════════════════════════════════════════════════
# 25. INSPECTION RESULT
# ═══════════════════════════════════════════════════════════════


class TestInspectionResultModel:
    def test_create(self, inspection_result):
        assert inspection_result.pk is not None
        assert inspection_result.inspection_characteristic == "قطر"
        assert inspection_result.result_status == InspectionResultModel.ResultStatus.PASS

    def test_str(self, inspection_result):
        assert "قطر" in str(inspection_result)

    def test_relation(self, inspection_result, inspection_lot):
        assert inspection_result.inspection_lot == inspection_lot


# ═══════════════════════════════════════════════════════════════
# 26. QUALITY ACTION
# ═══════════════════════════════════════════════════════════════


class TestQualityActionModel:
    def test_create(self, quality_action):
        assert quality_action.pk is not None
        assert quality_action.action_type == QualityActionModel.ActionType.RELEASE
        assert quality_action.action_quantity == Decimal("100.0000")

    def test_str(self, quality_action):
        assert "RELEASE" in str(quality_action)

    def test_relation(self, quality_action, inspection_lot):
        assert quality_action.inspection_lot == inspection_lot


# ═══════════════════════════════════════════════════════════════
# 27. STOCK MOVEMENT SUMMARY
# ═══════════════════════════════════════════════════════════════


class TestStockMovementSummaryModel:
    def test_create(self, stock_movement_summary):
        assert stock_movement_summary.pk is not None
        assert stock_movement_summary.summary_date == date(2024, 1, 31)
        assert stock_movement_summary.total_in_quantity == Decimal("5000.0000")
        assert stock_movement_summary.total_out_quantity == Decimal("2000.0000")
        assert stock_movement_summary.net_quantity == Decimal("3000.0000")
        assert stock_movement_summary.transaction_count == 15

    def test_str(self, stock_movement_summary):
        assert "MAT-001" in str(stock_movement_summary)

    def test_relations(self, stock_movement_summary, item, warehouse):
        assert stock_movement_summary.item == item
        assert stock_movement_summary.warehouse == warehouse


# ═══════════════════════════════════════════════════════════════
# 28. STOCK AGING BUCKET
# ═══════════════════════════════════════════════════════════════


class TestStockAgingBucketModel:
    def test_create(self, stock_aging_bucket):
        assert stock_aging_bucket.pk is not None
        assert stock_aging_bucket.age_in_days == 45
        assert stock_aging_bucket.aging_category == "31-60"
        assert stock_aging_bucket.quantity == Decimal("500.0000")
        assert stock_aging_bucket.snapshot_date == date(2024, 2, 15)

    def test_str(self, stock_aging_bucket):
        assert "MAT-001" in str(stock_aging_bucket)

    def test_relations(self, stock_aging_bucket, item, warehouse, batch):
        assert stock_aging_bucket.item == item
        assert stock_aging_bucket.warehouse == warehouse
        assert stock_aging_bucket.batch == batch
