import datetime

from apps.audit.models import AuditLog
from apps.crm.models import Customer
from apps.finance.models import FinanceAccount, Transaction
from apps.invoicing.models import Invoice
from apps.modules import services as modules
from apps.payroll.models import PayrollItem, PayrollRun
from tests.base import CoreTestCase

F = "/api/v1/finance/"


class FinanceTests(CoreTestCase):
    def setUp(self):
        super().setUp()
        modules.enable_module(actor=self.root, organization=self.org_a, module_code="finance")
        self.mgr = self.api(self.manager_a, self.org_a)
        self.emp = self.api(self.employee_a, self.org_a)
        self.bank = self.mgr.post(f"{F}accounts/", {"name": "Bank", "type": "bank", "opening_balance": 1_000_000}, format="json").data

    def tx(self, **extra):
        body = {"kind": "income", "amount": 500_000, "account": self.bank["id"], "date": "2025-03-25", "description": "x"}
        body.update(extra)
        return self.mgr.post(f"{F}transactions/", body, format="json")

    # ---- access
    def test_employee_has_no_finance_access(self):
        for url in ("summary/", "accounts/", "transactions/", "categories/", "unrecorded/"):
            self.assertEqual(self.emp.get(F + url).status_code, 403, url)
        self.assertEqual(self.emp.post(f"{F}transactions/", {"kind": "income"}, format="json").status_code, 403)

    def test_self_scope_is_not_enough(self):
        from apps.access.models import Permission, RolePermission

        role = self.role(self.org_a, "employee")
        for code in ("finance.view", "finance.create"):
            RolePermission.objects.create(role=role, permission=Permission.objects.get(code=code), scope="self")
        self.assertEqual(self.emp.get(f"{F}transactions/").status_code, 403)

    def test_module_gate_and_tenant_isolation(self):
        t = self.tx().data
        self.assertEqual(self.api(self.manager_b, self.org_b).get(f"{F}transactions/").status_code, 403)
        modules.enable_module(actor=self.root, organization=self.org_b, module_code="finance")
        other = self.api(self.manager_b, self.org_b)
        self.assertEqual(other.get(f"{F}transactions/").data["count"], 0)
        self.assertEqual(other.patch(f"{F}transactions/{t['id']}/", {"description": "h"}, format="json").status_code, 404)
        self.assertEqual(other.delete(f"{F}transactions/{t['id']}/").status_code, 404)
        self.assertEqual(other.post(f"{F}transactions/", {"kind": "income", "amount": 1, "account": self.bank["id"], "date": "2025-01-01"}, format="json").status_code, 400)
        modules.disable_module(actor=self.root, organization=self.org_a, module_code="finance")
        self.assertEqual(self.mgr.get(f"{F}transactions/").status_code, 403)
        self.assertEqual(Transaction.objects.count(), 1)

    # ---- transactions & balances
    def test_balance_and_summary(self):
        cash = self.mgr.post(f"{F}accounts/", {"name": "Cash", "type": "cash"}, format="json").data
        self.tx(kind="income", amount=2_000_000)
        self.tx(kind="expense", amount=300_000)
        self.tx(kind="expense", amount=100_000, account=cash["id"])
        accounts = {a["name"]: a["balance"] for a in self.mgr.get(f"{F}accounts/").data}
        self.assertEqual(accounts, {"Bank": 1_000_000 + 2_000_000 - 300_000, "Cash": -100_000})
        s = self.mgr.get(f"{F}summary/").data
        self.assertEqual((s["income"], s["expense"], s["net"]), (2_000_000, 400_000, 1_600_000))
        self.assertEqual(s["balance_total"], 2_600_000)

    def test_summary_filters_by_jalali_month(self):
        self.tx(amount=100, date="2025-03-25")  # 1404/01/05
        self.tx(amount=700, date="2025-04-25")  # 1404/02/05
        self.assertEqual(self.mgr.get(f"{F}summary/", {"year": 1404, "month": 1}).data["income"], 100)
        self.assertEqual(self.mgr.get(f"{F}summary/", {"year": 1404, "month": 2}).data["income"], 700)
        self.assertEqual(self.mgr.get(f"{F}summary/", {"year": 1404}).data["income"], 800)

    def test_transaction_validation(self):
        for bad in ({"amount": 0}, {"amount": -5}, {"kind": "transfer"}, {"account": 999}, {"date": None}):
            body = {"kind": "income", "amount": 5, "account": self.bank["id"], "date": "2025-01-01", **bad}
            self.assertEqual(self.mgr.post(f"{F}transactions/", body, format="json").status_code, 400, bad)
        cats = {c["name"]: c["id"] for c in self.mgr.get(f"{F}categories/").data}
        self.assertEqual(self.tx(kind="income", category=cats["اجاره"]).status_code, 400)  # expense category on income
        self.assertEqual(self.tx(kind="expense", category=cats["اجاره"]).status_code, 201)

    def test_inactive_account_cannot_receive_but_keeps_history(self):
        self.tx()
        self.mgr.patch(f"{F}accounts/{self.bank['id']}/", {"is_active": False}, format="json")
        self.assertEqual(self.tx().status_code, 400)
        self.assertEqual(self.mgr.get(f"{F}transactions/").data["count"], 1)

    def test_update_and_delete_are_audited(self):
        t = self.tx().data
        r = self.mgr.patch(f"{F}transactions/{t['id']}/", {"amount": 900}, format="json")
        self.assertEqual(r.data["amount"], 900)
        self.assertEqual(self.mgr.delete(f"{F}transactions/{t['id']}/").status_code, 204)
        actions = set(AuditLog.objects.filter(organization=self.org_a).values_list("action", flat=True))
        self.assertTrue({"finance.transaction_create", "finance.transaction_update", "finance.transaction_delete"} <= actions)

    def test_account_and_category_rules(self):
        self.assertEqual(self.mgr.post(f"{F}accounts/", {"name": "Bank"}, format="json").status_code, 400)  # duplicate
        self.assertEqual(self.mgr.post(f"{F}accounts/", {"name": " "}, format="json").status_code, 400)
        self.assertEqual(self.mgr.post(f"{F}categories/", {"name": "X", "kind": "bad"}, format="json").status_code, 400)
        self.assertEqual(self.mgr.post(f"{F}categories/", {"name": "X", "kind": "income"}, format="json").status_code, 201)
        self.assertEqual(self.mgr.post(f"{F}categories/", {"name": "X", "kind": "income"}, format="json").status_code, 400)

    def test_filters(self):
        self.tx(kind="income", description="alpha")
        self.tx(kind="expense", description="beta")
        self.assertEqual(self.mgr.get(f"{F}transactions/", {"kind": "expense"}).data["count"], 1)
        self.assertEqual(self.mgr.get(f"{F}transactions/", {"search": "alp"}).data["count"], 1)

    # ---- recording invoices / payroll
    def _paid_invoice(self):
        for code in ("crm", "invoicing"):
            modules.enable_module(actor=self.root, organization=self.org_a, module_code=code)
        customer = Customer.objects.create(organization=self.org_a, name="Acme", created_by=self.manager_a)
        return Invoice.objects.create(
            organization=self.org_a, customer=customer, number=7, status="paid", issue_date=datetime.date(2025, 1, 1),
            total=1_210_000, created_by=self.manager_a,
        )

    def test_paid_invoice_can_be_recorded_once(self):
        inv = self._paid_invoice()
        self.assertEqual([i["id"] for i in self.mgr.get(f"{F}unrecorded/").data["invoices"]], [inv.pk])
        body = {"source": "invoice", "source_id": inv.pk, "account": self.bank["id"]}
        r = self.mgr.post(f"{F}record/", body, format="json")
        self.assertEqual((r.status_code, r.data["kind"], r.data["amount"]), (201, "income", 1_210_000))
        self.assertEqual(self.mgr.post(f"{F}record/", body, format="json").status_code, 400)  # only once
        self.assertEqual(self.mgr.get(f"{F}unrecorded/").data["invoices"], [])
        # amount is locked, but deleting the transaction frees the invoice again
        self.assertEqual(self.mgr.patch(f"{F}transactions/{r.data['id']}/", {"amount": 1}, format="json").status_code, 400)
        self.mgr.delete(f"{F}transactions/{r.data['id']}/")
        self.assertEqual(len(self.mgr.get(f"{F}unrecorded/").data["invoices"]), 1)

    def test_unpaid_or_foreign_invoice_cannot_be_recorded(self):
        inv = self._paid_invoice()
        Invoice.objects.filter(pk=inv.pk).update(status="issued")
        body = {"source": "invoice", "source_id": inv.pk, "account": self.bank["id"]}
        self.assertEqual(self.mgr.post(f"{F}record/", body, format="json").status_code, 400)
        Invoice.objects.filter(pk=inv.pk).update(status="paid")
        modules.enable_module(actor=self.root, organization=self.org_b, module_code="finance")
        bank_b = self.api(self.manager_b, self.org_b).post(f"{F}accounts/", {"name": "B"}, format="json").data
        other = self.api(self.manager_b, self.org_b).post(f"{F}record/", {**body, "account": bank_b["id"]}, format="json")
        self.assertEqual(other.status_code, 400)

    def test_approved_payroll_is_recorded_as_expense(self):
        for code in ("personnel", "payroll"):
            modules.enable_module(actor=self.root, organization=self.org_a, module_code=code)
        run = PayrollRun.objects.create(organization=self.org_a, year=1403, month=1, status="draft", created_by=self.manager_a)
        PayrollItem.objects.create(run=run, membership=self.membership(self.employee_a, self.org_a), net=5_000_000)
        PayrollItem.objects.create(run=run, membership=self.membership(self.manager_a, self.org_a), net=7_000_000)
        self.assertEqual(self.mgr.get(f"{F}unrecorded/").data["payroll_runs"], [])  # draft is not recorded
        PayrollRun.objects.filter(pk=run.pk).update(status="approved")
        r = self.mgr.post(f"{F}record/", {"source": "payroll", "source_id": run.pk, "account": self.bank["id"]}, format="json")
        self.assertEqual((r.data["kind"], r.data["amount"]), ("expense", 12_000_000))

    def test_unrecorded_hides_modules_that_are_off(self):
        self.assertEqual(self.mgr.get(f"{F}unrecorded/").data, {"invoices": [], "payroll_runs": []})
