"""
Unit Tests — SCM Domain Entities.

تست‌های واحد موجودیت‌های دامنه ماژول خرید و تدارکات.
"""
import pytest
from datetime import date, datetime
from decimal import Decimal
from uuid import uuid4

from modules.scm.backend.domain.entities.vendor import (
    Vendor, VendorStatus, VendorType, VendorCode, NationalId, IBAN,
    VendorCategory, VendorContact, VendorBankAccount, VendorAddress, AddressType,
)
from modules.scm.backend.domain.entities.purchase_requisition import (
    PurchaseRequisition, PurchaseRequisitionLine, PRStatus, PRPriority,
)
from modules.scm.backend.domain.entities.purchase_order import (
    PurchaseOrder, PurchaseOrderLine, POStatus, POPriority,
)


# ═══════════════════════════════════════════════════
# Value Object Tests
# ═══════════════════════════════════════════════════

class TestVendorCode:
    def test_valid(self):
        vc = VendorCode(value="V001")
        assert vc.value == "V001"

    def test_empty_raises(self):
        with pytest.raises(ValueError, match="1-50"):
            VendorCode(value="")

    def test_too_long_raises(self):
        with pytest.raises(ValueError, match="1-50"):
            VendorCode(value="X" * 51)


class TestNationalId:
    def test_valid(self):
        nid = NationalId(value="12345678")
        assert nid.value == "12345678"

    def test_too_long_raises(self):
        with pytest.raises(ValueError, match="20"):
            NationalId(value="X" * 21)

    def test_empty_ok(self):
        nid = NationalId(value="")
        assert nid.value == ""


class TestIBAN:
    def test_valid(self):
        iban = IBAN(value="IR" + "0" * 22)
        assert len(iban.value) == 24

    def test_too_short_raises(self):
        with pytest.raises(ValueError, match="24-34"):
            IBAN(value="IR123")

    def test_too_long_raises(self):
        with pytest.raises(ValueError, match="24-34"):
            IBAN(value="IR" + "0" * 33)

    def test_empty_ok(self):
        iban = IBAN(value="")
        assert iban.value == ""


# ═══════════════════════════════════════════════════
# Vendor Tests
# ═══════════════════════════════════════════════════

class TestVendor:
    def _make_vendor(self, **overrides):
        defaults = {
            "id": uuid4(),
            "tenant_id": uuid4(),
            "vendor_code": "V001",
            "vendor_name": "شرکت آزمایشی",
            "vendor_type": VendorType.COMPANY,
            "status": VendorStatus.PENDING_APPROVAL,
        }
        defaults.update(overrides)
        return Vendor(**defaults)

    def test_activate(self):
        vendor = self._make_vendor()
        vendor.activate()
        assert vendor.status == VendorStatus.ACTIVE
        assert vendor.is_active is True

    def test_block(self):
        vendor = self._make_vendor(status=VendorStatus.ACTIVE)
        vendor.block("تخلف در ارسال")
        assert vendor.status == VendorStatus.BLOCKED
        assert vendor.is_active is False
        assert "تخلف" in vendor.notes

    def test_block_without_reason(self):
        vendor = self._make_vendor(status=VendorStatus.ACTIVE)
        vendor.block()
        assert vendor.status == VendorStatus.BLOCKED

    def test_deactivate(self):
        vendor = self._make_vendor(status=VendorStatus.ACTIVE)
        vendor.deactivate()
        assert vendor.status == VendorStatus.INACTIVE
        assert vendor.is_active is False

    def test_validate_valid(self):
        vendor = self._make_vendor()
        vendor.validate()

    def test_validate_missing_code(self):
        vendor = self._make_vendor(vendor_code="")
        with pytest.raises(ValueError, match="code"):
            vendor.validate()

    def test_validate_missing_name(self):
        vendor = self._make_vendor(vendor_name="")
        with pytest.raises(ValueError, match="name"):
            vendor.validate()


# ═══════════════════════════════════════════════════
# Purchase Requisition Tests
# ═══════════════════════════════════════════════════

class TestPurchaseRequisition:
    def _make_pr_line(self, **overrides):
        defaults = {
            "id": uuid4(),
            "tenant_id": uuid4(),
            "item_code": "ITEM001",
            "item_name": "کالای آزمایشی",
            "quantity": Decimal("10"),
            "uom_code": "PCS",
            "estimated_unit_price": Decimal("50000"),
        }
        defaults.update(overrides)
        return PurchaseRequisitionLine(**defaults)

    def _make_pr(self, with_lines=True, **overrides):
        defaults = {
            "id": uuid4(),
            "tenant_id": uuid4(),
            "pr_number": "PR-2024-001",
            "title": "درخواست خرید آزمایشی",
            "status": PRStatus.DRAFT,
            "priority": PRPriority.NORMAL,
            "requester_id": uuid4(),
            "requester_name": "علی احمدی",
        }
        defaults.update(overrides)
        pr = PurchaseRequisition(**defaults)
        if with_lines:
            pr.lines = [self._make_pr_line()]
        return pr

    def test_calculate_totals(self):
        line = self._make_pr_line(quantity=Decimal("10"), estimated_unit_price=Decimal("5000"))
        line.calculate_total()
        assert line.estimated_total_price == Decimal("50000")

        pr = self._make_pr(with_lines=False)
        pr.lines = [line]
        pr.calculate_totals()
        assert pr.total_estimated_amount == Decimal("50000")

    def test_submit_from_draft(self):
        pr = self._make_pr()
        pr.submit()
        assert pr.status == PRStatus.PENDING_APPROVAL

    def test_submit_without_lines_raises(self):
        pr = self._make_pr(with_lines=False)
        with pytest.raises(ValueError, match="at least one line"):
            pr.submit()

    def test_submit_non_draft_raises(self):
        pr = self._make_pr(status=PRStatus.APPROVED)
        with pytest.raises(ValueError, match="DRAFT"):
            pr.submit()

    def test_approve(self):
        pr = self._make_pr(status=PRStatus.PENDING_APPROVAL)
        approver = uuid4()
        pr.approve(approver)
        assert pr.status == PRStatus.APPROVED
        assert pr.approved_by_id == approver
        assert pr.approved_at is not None

    def test_approve_non_pending_raises(self):
        pr = self._make_pr(status=PRStatus.DRAFT)
        with pytest.raises(ValueError, match="PENDING_APPROVAL"):
            pr.approve(uuid4())

    def test_reject(self):
        pr = self._make_pr(status=PRStatus.PENDING_APPROVAL)
        pr.reject("کالا موجود نیست")
        assert pr.status == PRStatus.REJECTED
        assert pr.rejection_reason == "کالا موجود نیست"

    def test_reject_non_pending_raises(self):
        pr = self._make_pr(status=PRStatus.DRAFT)
        with pytest.raises(ValueError, match="PENDING_APPROVAL"):
            pr.reject("دلیل")

    def test_cancel(self):
        pr = self._make_pr(status=PRStatus.DRAFT)
        pr.cancel()
        assert pr.status == PRStatus.CANCELLED

    def test_cancel_converted_raises(self):
        pr = self._make_pr(status=PRStatus.CONVERTED)
        with pytest.raises(ValueError, match="cancel"):
            pr.cancel()

    def test_cancel_already_cancelled_raises(self):
        pr = self._make_pr(status=PRStatus.CANCELLED)
        with pytest.raises(ValueError, match="cancel"):
            pr.cancel()

    def test_mark_converted_all(self):
        line1 = self._make_pr_line(is_converted=True)
        line2 = self._make_pr_line(is_converted=True)
        pr = self._make_pr(with_lines=False)
        pr.lines = [line1, line2]
        pr.mark_converted()
        assert pr.status == PRStatus.CONVERTED

    def test_mark_converted_partial(self):
        line1 = self._make_pr_line(is_converted=True)
        line2 = self._make_pr_line(is_converted=False)
        pr = self._make_pr(with_lines=False)
        pr.lines = [line1, line2]
        pr.mark_converted()
        assert pr.status == PRStatus.PARTIALLY_CONVERTED

    def test_validate_valid(self):
        pr = self._make_pr()
        pr.validate()

    def test_validate_missing_number(self):
        pr = self._make_pr(pr_number="")
        with pytest.raises(ValueError, match="number"):
            pr.validate()

    def test_validate_missing_requester(self):
        pr = self._make_pr(requester_id=None)
        with pytest.raises(ValueError, match="equester"):
            pr.validate()


# ═══════════════════════════════════════════════════
# Purchase Order Tests
# ═══════════════════════════════════════════════════

class TestPurchaseOrder:
    def _make_po_line(self, **overrides):
        defaults = {
            "id": uuid4(),
            "tenant_id": uuid4(),
            "item_code": "ITEM001",
            "item_name": "کالای آزمایشی",
            "quantity": Decimal("100"),
            "uom_code": "PCS",
            "unit_price": Decimal("10000"),
        }
        defaults.update(overrides)
        return PurchaseOrderLine(**defaults)

    def _make_po(self, with_lines=True, **overrides):
        defaults = {
            "id": uuid4(),
            "tenant_id": uuid4(),
            "po_number": "PO-2024-001",
            "vendor_id": uuid4(),
            "status": POStatus.DRAFT,
        }
        defaults.update(overrides)
        po = PurchaseOrder(**defaults)
        if with_lines:
            po.lines = [self._make_po_line()]
        return po

    def test_line_calculate_totals(self):
        line = self._make_po_line(
            quantity=Decimal("10"),
            unit_price=Decimal("5000"),
            discount_percent=Decimal("10"),
            tax_percent=Decimal("9"),
        )
        line.calculate_totals()
        assert line.discount_amount == Decimal("5000")  # 50000 * 10%
        assert line.tax_amount == Decimal("4050")        # 45000 * 9%
        assert line.line_total == Decimal("49050")       # 45000 + 4050

    def test_line_remaining_quantity(self):
        line = self._make_po_line(
            quantity=Decimal("100"),
            received_quantity=Decimal("60"),
            returned_quantity=Decimal("10"),
        )
        assert line.remaining_quantity == Decimal("50")

    def test_line_is_fully_received(self):
        line = self._make_po_line(
            quantity=Decimal("100"),
            received_quantity=Decimal("100"),
        )
        assert line.is_fully_received is True

    def test_line_not_fully_received(self):
        line = self._make_po_line(
            quantity=Decimal("100"),
            received_quantity=Decimal("50"),
        )
        assert line.is_fully_received is False

    def test_po_calculate_totals(self):
        line = self._make_po_line(
            quantity=Decimal("10"),
            unit_price=Decimal("1000"),
        )
        po = self._make_po(with_lines=False)
        po.lines = [line]
        po.calculate_totals()
        assert po.subtotal == Decimal("10000")

    def test_submit_from_draft(self):
        po = self._make_po()
        po.submit()
        assert po.status == POStatus.PENDING_APPROVAL

    def test_submit_without_lines_raises(self):
        po = self._make_po(with_lines=False)
        with pytest.raises(ValueError, match="at least one line"):
            po.submit()

    def test_submit_without_vendor_raises(self):
        po = self._make_po(vendor_id=None)
        with pytest.raises(ValueError, match="Vendor"):
            po.submit()

    def test_submit_non_draft_raises(self):
        po = self._make_po(status=POStatus.APPROVED)
        with pytest.raises(ValueError, match="DRAFT"):
            po.submit()

    def test_approve(self):
        po = self._make_po(status=POStatus.PENDING_APPROVAL)
        approver = uuid4()
        po.approve(approver)
        assert po.status == POStatus.APPROVED
        assert po.approved_by_id == approver
        assert po.approved_at is not None

    def test_approve_non_pending_raises(self):
        po = self._make_po(status=POStatus.DRAFT)
        with pytest.raises(ValueError, match="PENDING_APPROVAL"):
            po.approve(uuid4())

    def test_reject(self):
        po = self._make_po(status=POStatus.PENDING_APPROVAL)
        po.reject("قیمت بالاست")
        assert po.status == POStatus.REJECTED
        assert po.rejection_reason == "قیمت بالاست"

    def test_reject_non_pending_raises(self):
        po = self._make_po(status=POStatus.DRAFT)
        with pytest.raises(ValueError, match="PENDING_APPROVAL"):
            po.reject("دلیل")

    def test_send_to_vendor(self):
        po = self._make_po(status=POStatus.APPROVED)
        po.send_to_vendor()
        assert po.status == POStatus.SENT
        assert po.order_date == date.today()

    def test_send_non_approved_raises(self):
        po = self._make_po(status=POStatus.DRAFT)
        with pytest.raises(ValueError, match="APPROVED"):
            po.send_to_vendor()

    def test_update_receipt_status_fully_received(self):
        line = self._make_po_line(
            quantity=Decimal("100"),
            received_quantity=Decimal("100"),
        )
        po = self._make_po(with_lines=False, status=POStatus.SENT)
        po.lines = [line]
        po.update_receipt_status()
        assert po.status == POStatus.FULLY_RECEIVED

    def test_update_receipt_status_partially_received(self):
        line = self._make_po_line(
            quantity=Decimal("100"),
            received_quantity=Decimal("50"),
        )
        po = self._make_po(with_lines=False, status=POStatus.SENT)
        po.lines = [line]
        po.update_receipt_status()
        assert po.status == POStatus.PARTIALLY_RECEIVED

    def test_cancel(self):
        po = self._make_po(status=POStatus.DRAFT)
        po.cancel()
        assert po.status == POStatus.CANCELLED

    def test_cancel_fully_received_raises(self):
        po = self._make_po(status=POStatus.FULLY_RECEIVED)
        with pytest.raises(ValueError, match="cancel"):
            po.cancel()

    def test_cancel_closed_raises(self):
        po = self._make_po(status=POStatus.CLOSED)
        with pytest.raises(ValueError, match="cancel"):
            po.cancel()

    def test_close(self):
        po = self._make_po(status=POStatus.FULLY_RECEIVED)
        po.close()
        assert po.status == POStatus.CLOSED

    def test_close_draft_raises(self):
        po = self._make_po(status=POStatus.DRAFT)
        with pytest.raises(ValueError, match="close"):
            po.close()

    def test_validate_valid(self):
        po = self._make_po()
        po.validate()

    def test_validate_missing_number(self):
        po = self._make_po(po_number="")
        with pytest.raises(ValueError, match="number"):
            po.validate()

    def test_validate_missing_vendor(self):
        po = self._make_po(vendor_id=None)
        with pytest.raises(ValueError, match="Vendor"):
            po.validate()


# ═══════════════════════════════════════════════════
# Vendor Category Tests
# ═══════════════════════════════════════════════════

class TestVendorCategory:
    def test_creation(self):
        cat = VendorCategory(
            id=uuid4(),
            tenant_id=uuid4(),
            category_code="CAT01",
            category_name="الکترونیک",
        )
        assert cat.category_code == "CAT01"
        assert cat.is_active is True
