import pytest
from django.contrib.auth import get_user_model
from django.test.utils import CaptureQueriesContext
from django.db import connection
from datetime import date, timedelta
from decimal import Decimal
from rest_framework.test import APIClient
from apps.companies.models import Organization, OrganizationMembership
from apps.procurements.models import Requisition, Approval, RFQRFP, PurchaseOrder, ProcurementContract, GoodsReceipt, Invoice, Payment
from .conftest import seed_chart_of_accounts_for


@pytest.fixture
def organization(db):
    org = Organization.objects.create(name="Test Org")
    seed_chart_of_accounts_for(org)
    return org


@pytest.fixture
def user(db, organization):
    User = get_user_model()
    u = User.objects.create_user(email="proc@example.com", password="pass123")
    OrganizationMembership.objects.create(organization=organization, user=u)
    return u


@pytest.fixture
def vendor(db, organization):
    from apps.asset_management.models import Vendor
    return Vendor.objects.create(organization=organization, name="Supplier Co")


@pytest.fixture
def budget_item(db, organization):
    from apps.finance.models import BudgetCategory, BudgetItem
    category = BudgetCategory.objects.create(organization=organization, code="OPS", name="Operations")
    return BudgetItem.objects.create(category=category, name="Office Supplies")


@pytest.fixture
def api_client(db, organization):
    User = get_user_model()
    admin = User.objects.create_superuser(email="proc-admin@test.local", password="x")
    auto_org = admin.organizations.first()
    OrganizationMembership.objects.filter(organization=auto_org, user=admin).update(organization=organization)
    auto_org.delete()
    c = APIClient()
    c.force_authenticate(admin)
    return c


@pytest.mark.django_db
class TestRequisitionModel:
    def test_create_requisition(self, user, organization):
        req = Requisition.objects.create(
            organization=organization,
            title="Office supplies",
            requested_by=user
        )
        assert req.status == "draft"


@pytest.mark.django_db
class TestApprovalTenantIsolation:
    def test_approval_cannot_be_retargeted_to_another_organization(
        self, api_client, user, organization
    ):
        User = get_user_model()
        foreign_organization = Organization.objects.create(name="Foreign Procurement Org")
        foreign_requester = User.objects.create_user(
            email="foreign-requester@proc-security.test", password="pass123"
        )
        OrganizationMembership.objects.create(
            organization=foreign_organization, user=foreign_requester
        )
        own_requisition = Requisition.objects.create(
            organization=organization,
            title="Own requisition",
            requested_by=user,
        )
        foreign_requisition = Requisition.objects.create(
            organization=foreign_organization,
            title="Foreign requisition",
            requested_by=foreign_requester,
        )
        approval = Approval.objects.create(requisition=own_requisition)

        response = api_client.patch(
            f"/api/procurement-approvals/{approval.pk}/",
            {"requisition": str(foreign_requisition.pk)},
            format="json",
        )

        assert response.status_code == 200
        approval.refresh_from_db()
        assert approval.requisition_id.hex == str(own_requisition.pk).replace("-", "")


@pytest.mark.django_db
class TestRFQRFPModel:
    def test_create_rfq(self, user, organization):
        rfq = RFQRFP.objects.create(
            organization=organization,
            title="Request for Quote",
            rfq_rfp_type="rfq",
            description="Need 100 laptops",
            requirements="Specs here",
            deadline=date.today() + timedelta(days=7),
            created_by=user
        )
        assert rfq.rfq_rfp_type == "rfq"
        assert rfq.status == "draft"


@pytest.mark.django_db
class TestPurchaseOrderModel:
    def test_create_po(self, user, vendor, organization):
        po = PurchaseOrder.objects.create(
            organization=organization,
            order_number="PO-001",
            vendor=vendor,
            subtotal=Decimal("1000.00"),
            tax=Decimal("100.00"),
            total=Decimal("1100.00")
        )
        assert po.order_number == "PO-001"
        assert po.status == "draft"


@pytest.mark.django_db
class TestProcurementContractModel:
    def test_create_contract(self, user, vendor, organization):
        contract = ProcurementContract.objects.create(
            organization=organization,
            title="Annual Service Contract",
            contract_number="CON-001",
            vendor=vendor,
            value=Decimal("50000.00"),
            start_date=date.today()
        )
        assert contract.status == "draft"


@pytest.mark.django_db
class TestGoodsReceiptModel:
    def test_create_receipt(self, user, vendor, organization):
        from apps.procurements.models import PurchaseOrder
        po = PurchaseOrder.objects.create(
            organization=organization,
            order_number="PO-002",
            vendor=vendor,
            subtotal=Decimal("500.00"),
            tax=Decimal("50.00"),
            total=Decimal("550.00")
        )
        receipt = GoodsReceipt.objects.create(
            organization=organization,
            purchase_order=po,
            receipt_number="GR-001",
            received_date=date.today(),
            received_by=user
        )
        assert receipt.status == "pending"


@pytest.mark.django_db
class TestInvoiceModel:
    def test_create_invoice(self, vendor, organization):
        invoice = Invoice.objects.create(
            organization=organization,
            invoice_number="INV-001",
            vendor=vendor,
            amount=Decimal("1000.00"),
            tax=Decimal("100.00"),
            total=Decimal("1100.00"),
            due_date=date.today() + timedelta(days=30)
        )
        assert invoice.status == "pending"


@pytest.mark.django_db
class TestPaymentModel:
    def test_create_payment(self, vendor, user, organization):
        from apps.procurements.models import PurchaseOrder, Invoice
        po = PurchaseOrder.objects.create(
            organization=organization,
            order_number="PO-003",
            vendor=vendor,
            subtotal=Decimal("200.00"),
            tax=Decimal("20.00"),
            total=Decimal("220.00")
        )
        invoice = Invoice.objects.create(
            organization=organization,
            invoice_number="INV-002",
            vendor=vendor,
            amount=Decimal("200.00"),
            tax=Decimal("20.00"),
            total=Decimal("220.00"),
            due_date=date.today() + timedelta(days=30)
        )
        payment = Payment.objects.create(
            organization=organization,
            invoice=invoice,
            amount=Decimal("220.00"),
            method="bank_transfer"
        )
        assert payment.status == "pending"


@pytest.mark.django_db
class TestRequisitionBudgetContext:
    def test_committed_uses_db_aggregate(self, user, budget_item, organization):
        Requisition.objects.create(
            organization=organization,
            title="Laptops", requested_by=user, budget_item=budget_item,
            status="pending", total_amount=Decimal("500.00"),
        )
        req = Requisition.objects.create(
            organization=organization,
            title="Monitors", requested_by=user, budget_item=budget_item,
            status="approved", total_amount=Decimal("300.00"),
        )
        # Rejected/draft requisitions on the same budget item must not count.
        Requisition.objects.create(
            organization=organization,
            title="Rejected item", requested_by=user, budget_item=budget_item,
            status="rejected", total_amount=Decimal("999.00"),
        )
        ctx = req.budget_context()
        assert ctx["committed"] == Decimal("800.00")
        assert ctx["warning"] is None

    def test_no_budget_item_returns_warning(self, user, organization):
        req = Requisition.objects.create(organization=organization, title="Misc", requested_by=user)
        ctx = req.budget_context()
        assert ctx["committed"] is None
        assert ctx["warning"]

    def test_list_committed_matches_model_method_and_avoids_n_plus_one(self, api_client, user, budget_item, organization):
        for i in range(5):
            Requisition.objects.create(
                organization=organization,
                title=f"Item {i}", requested_by=user, budget_item=budget_item,
                status="pending", total_amount=Decimal("100.00"),
            )
        with CaptureQueriesContext(connection) as ctx:
            resp = api_client.get("/api/requisitions/")
        assert resp.status_code == 200, resp.content
        rows = resp.json()
        rows = rows["results"] if isinstance(rows, dict) and "results" in rows else rows
        assert len(rows) == 5
        for row in rows:
            assert Decimal(str(row["budget_context"]["committed"])) == Decimal("500.00")

        # One query per row for get_budget_context would push this well past
        # a small constant — assert it stays flat regardless of row count.
        n_plus_one_queries = [q for q in ctx.captured_queries if "procurements_requisition" in q["sql"].lower()]
        assert len(n_plus_one_queries) <= 3, (
            f"expected a small constant number of requisition queries, got {len(n_plus_one_queries)}: "
            f"{[q['sql'] for q in n_plus_one_queries]}"
        )


@pytest.mark.django_db
class TestNotifyCreatorBack:
    def _client_for(self, u):
        c = APIClient()
        c.force_authenticate(u)
        return c

    def _join(self, u, organization):
        OrganizationMembership.objects.get_or_create(organization=organization, user=u)

    def test_invoice_approve_notifies_creator(self, vendor, user, organization):
        from apps.notifications.models import Notification
        from apps.accounting.models import Account
        from apps.finance.models import BudgetCategory, BudgetItem
        approver = get_user_model().objects.create_user(email="inv-approver@test.local", password="p", organization_role="director")
        self._join(approver, organization)
        category = BudgetCategory.objects.create(organization=organization, code="NOTIFY-INV", name="Notify Invoice Test", expense_account=Account.objects.get(organization=organization, code="5400"))
        budget = BudgetItem.objects.create(category=category, name="Notify invoice budget", allocated_amount=1000)
        req = Requisition.objects.create(organization=organization, title="Posting purchase", requested_by=user, status="approved", budget_item=budget)
        po = PurchaseOrder.objects.create(
            organization=organization,
            order_number="PO-NOTIFY-1", vendor=vendor, requisition=req, status="sent",
            items=[{"sku": "A", "quantity": 1, "unit_price": 100}], subtotal=100, total=110,
        )
        receipt = GoodsReceipt.objects.create(
            organization=organization,
            purchase_order=po, receipt_number="GR-NOTIFY-1", received_date=date.today(), status="received",
            received_by=user, items=[{"sku": "A", "quantity": 1, "unit_price": 100}],
        )
        invoice = Invoice.objects.create(
            organization=organization,
            invoice_number="INV-NOTIFY-1", vendor=vendor, purchase_order=po, goods_receipt=receipt,
            items=[{"sku": "A", "quantity": 1, "unit_price": 100}], amount=100, tax=10, total=110,
            due_date=date.today() + timedelta(days=30), status="pending", created_by=user,
        )
        resp = self._client_for(approver).post(f"/api/invoices/{invoice.id}/approve/", {}, format="json")
        assert resp.status_code == 200, resp.content
        assert Notification.objects.filter(user=user, notification_type="procurement").exists()

    def test_payment_release_notifies_preparer(self, vendor, user, organization):
        from apps.notifications.models import Notification
        releaser = get_user_model().objects.create_user(email="pay-releaser@test.local", password="p", organization_role="director")
        self._join(releaser, organization)
        req = Requisition.objects.create(organization=organization, title="Matched purchase", requested_by=user, status="approved")
        po = PurchaseOrder.objects.create(organization=organization, order_number="PO-NOTIFY-2", vendor=vendor, requisition=req, subtotal=100, total=100, status="sent")
        receipt = GoodsReceipt.objects.create(
            organization=organization,
            purchase_order=po, receipt_number="GR-NOTIFY-2", received_date=date.today(),
            status="received", items=[{"name": "Item", "quantity": 1, "unit_price": 100}], received_by=releaser,
        )
        invoice = Invoice.objects.create(
            organization=organization,
            invoice_number="INV-NOTIFY-2", vendor=vendor, purchase_order=po, goods_receipt=receipt,
            amount=100, total=100, due_date=date.today() + timedelta(days=30), status="approved",
        )
        payment = Payment.objects.create(organization=organization, invoice=invoice, amount=100, method="bank_transfer", prepared_by=user, fund_approved_by=releaser)
        resp = self._client_for(releaser).post(f"/api/payments/{payment.id}/release/", {"comment": "Reviewed"}, format="json")
        assert resp.status_code == 200, resp.content
        assert Notification.objects.filter(user=user, notification_type="procurement").exists()

    def test_approve_funds_notifies_preparer(self, vendor, user, organization):
        from apps.notifications.models import Notification
        from apps.companies.models import Department
        approver = get_user_model().objects.create_user(email="fund-approver@test.local", password="p", organization_role="director")
        self._join(approver, organization)
        department = Department.objects.create(organization=organization, name="Ops Notify Test")
        req = Requisition.objects.create(organization=organization, title="Project expense", requested_by=user, status="approved", requesting_department=department)
        po = PurchaseOrder.objects.create(organization=organization, order_number="PO-NOTIFY-3", vendor=vendor, requisition=req, subtotal=100, total=100, status="sent")
        receipt = GoodsReceipt.objects.create(organization=organization, purchase_order=po, receipt_number="GR-NOTIFY-3", received_date=date.today(), status="received", items=[{"name": "Item", "quantity": 1, "unit_price": 100}], received_by=user)
        invoice = Invoice.objects.create(organization=organization, invoice_number="INV-NOTIFY-3", vendor=vendor, purchase_order=po, goods_receipt=receipt, amount=100, total=100, due_date=date.today(), status="approved")
        payment = Payment.objects.create(organization=organization, invoice=invoice, amount=100, method="bank_transfer", prepared_by=user)
        resp = self._client_for(approver).post(f"/api/payments/{payment.id}/approve-funds/", {"comment": "OK"}, format="json")
        assert resp.status_code == 200, resp.content
        assert Notification.objects.filter(user=user, notification_type="procurement").exists()

    def test_no_notify_when_approver_is_creator(self, vendor, user, organization):
        """The creator approving is disallowed at the workflow layer (self-approval
        guard), so there is nothing to assert about notify-suppression here beyond
        confirming the request is rejected rather than silently notifying self."""
        req = Requisition.objects.create(organization=organization, title="Self posting", requested_by=user, status="approved")
        po = PurchaseOrder.objects.create(
            organization=organization,
            order_number="PO-NOTIFY-4", vendor=vendor, requisition=req, status="sent",
            items=[{"sku": "A", "quantity": 1, "unit_price": 100}], subtotal=100, total=110,
        )
        receipt = GoodsReceipt.objects.create(
            organization=organization,
            purchase_order=po, receipt_number="GR-NOTIFY-4", received_date=date.today(), status="received",
            received_by=user, items=[{"sku": "A", "quantity": 1, "unit_price": 100}],
        )
        invoice = Invoice.objects.create(
            organization=organization,
            invoice_number="INV-NOTIFY-4", vendor=vendor, purchase_order=po, goods_receipt=receipt,
            items=[{"sku": "A", "quantity": 1, "unit_price": 100}], amount=100, tax=10, total=110,
            due_date=date.today() + timedelta(days=30), status="pending", created_by=user,
        )
        from apps.core.models import Role, UserRole, Permission
        role = Role.objects.create(name="Self Approver", slug="self-approver-notify-test")
        perm, _ = Permission.objects.get_or_create(domain="procurements", action="approve", defaults={"label": "x"})
        role.permissions.add(perm)
        UserRole.objects.create(user=user, role=role, organization=organization)
        resp = self._client_for(user).post(f"/api/invoices/{invoice.id}/approve/", {}, format="json")
        assert resp.status_code in (400, 403)

    def test_issue_purchase_order_notifies_requester(self, vendor, user, budget_item, organization):
        from apps.notifications.models import Notification
        from apps.finance.models import BudgetItem
        BudgetItem.objects.filter(pk=budget_item.pk).update(allocated_amount="2000")
        issuer = get_user_model().objects.create_user(email="po-issuer@test.local", password="p", organization_role="director")
        self._join(issuer, organization)
        req = Requisition.objects.create(organization=organization, title="Issue notify test", requested_by=user, status="approved", total_amount="500", budget_item=budget_item)
        from apps.procurements.models import RequisitionQuote
        RequisitionQuote.objects.create(requisition=req, vendor=vendor, quoted_amount="500", is_selected=True)
        po = PurchaseOrder.objects.create(organization=organization, order_number="PO-NOTIFY-ISSUE", vendor=vendor, requisition=req, subtotal=500, total=500)
        resp = self._client_for(issuer).post(f"/api/purchase-orders/{po.id}/issue/", {}, format="json")
        assert resp.status_code == 200, resp.content
        assert Notification.objects.filter(user=user, notification_type="procurement", title="Purchase order issued").exists()

    def test_cancel_purchase_order_notifies_requester(self, vendor, user, organization):
        from apps.notifications.models import Notification
        canceller = get_user_model().objects.create_user(email="po-canceller@test.local", password="p", organization_role="director")
        self._join(canceller, organization)
        req = Requisition.objects.create(organization=organization, title="Cancel notify test", requested_by=user, status="approved")
        po = PurchaseOrder.objects.create(organization=organization, order_number="PO-NOTIFY-CANCEL", vendor=vendor, requisition=req, subtotal=100, total=100, status="draft")
        resp = self._client_for(canceller).post(f"/api/purchase-orders/{po.id}/cancel/", {"reason": "No longer needed"}, format="json")
        assert resp.status_code == 200, resp.content
        assert Notification.objects.filter(user=user, notification_type="procurement", title="Purchase order cancelled").exists()

    def test_sign_contract_notifies_requester(self, vendor, user, budget_item, organization):
        from apps.notifications.models import Notification
        from apps.finance.models import BudgetItem
        BudgetItem.objects.filter(pk=budget_item.pk).update(allocated_amount="2000")
        signer = get_user_model().objects.create_user(email="contract-signer@test.local", password="p", organization_role="director")
        self._join(signer, organization)
        req = Requisition.objects.create(organization=organization, title="Sign notify test", requested_by=user, status="approved", total_amount="500", budget_item=budget_item)
        from apps.procurements.models import RequisitionQuote
        RequisitionQuote.objects.create(requisition=req, vendor=vendor, quoted_amount="500", is_selected=True)
        contract = ProcurementContract.objects.create(organization=organization, title="Sign notify contract", contract_number="CT-NOTIFY-1", vendor=vendor, requisition=req, value=500, start_date=date.today())
        resp = self._client_for(signer).post(f"/api/procurement-contracts/{contract.id}/sign/", {}, format="json")
        assert resp.status_code == 200, resp.content
        assert Notification.objects.filter(user=user, notification_type="procurement", title="Contract signed").exists()

    def test_goods_receipt_accept_notifies_requester(self, vendor, user, organization):
        from apps.notifications.models import Notification
        receiver = get_user_model().objects.create_user(email="gr-receiver@test.local", password="p", organization_role="director")
        self._join(receiver, organization)
        req = Requisition.objects.create(organization=organization, title="GR notify test", requested_by=user, status="approved")
        po = PurchaseOrder.objects.create(
            organization=organization,
            order_number="PO-NOTIFY-GR", vendor=vendor, requisition=req, status="sent",
            items=[{"sku": "A", "quantity": 1, "unit_price": 100}], subtotal=100, total=100,
        )
        receipt = GoodsReceipt.objects.create(
            organization=organization,
            purchase_order=po, receipt_number="GR-NOTIFY-ACCEPT", received_date=date.today(), status="pending",
            items=[{"sku": "A", "quantity": 1, "unit_price": 100}],
        )
        resp = self._client_for(receiver).post(f"/api/goods-receipts/{receipt.id}/accept/", {"decision": "received"}, format="json")
        assert resp.status_code == 200, resp.content
        assert Notification.objects.filter(user=user, notification_type="procurement", title="Goods receipt received").exists()

    def test_award_quote_notifies_requester(self, vendor, user, organization):
        from apps.notifications.models import Notification
        from apps.procurements.models import RequisitionQuote
        req = Requisition.objects.create(organization=organization, title="Award notify test", requested_by=user, status="approved", procurement_method="direct_purchase")
        quote = RequisitionQuote.objects.create(requisition=req, vendor=vendor, quoted_amount="100")
        resp = self._client_for(user).post(f"/api/requisition-quotes/{quote.id}/award/", {}, format="json")
        assert resp.status_code == 200, resp.content
        assert Notification.objects.filter(user=user, notification_type="procurement", title="Quote awarded").exists()
