"""Performance regression tests: project list fund fields, /mine, cash statement
date pushdown, finance summary."""

from datetime import date

import pytest
from django.contrib.auth import get_user_model
from django.db import connection
from django.test.utils import CaptureQueriesContext
from rest_framework.test import APIClient

from apps.projects.models import Project, ProjectFinance, ProjectFund, ProjectTeamMember
from apps.projects.views import _collect_cash_transactions, _date_bound_q
from apps.companies.models import Organization

User = get_user_model()


@pytest.fixture
def admin(db):
    return User.objects.create_superuser(email="admin@test.local", password="x")


@pytest.fixture
def client(admin):
    c = APIClient()
    c.force_authenticate(admin)
    return c


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


def _rows(resp):
    body = resp.json()
    return body["results"] if isinstance(body, dict) and "results" in body else body


@pytest.mark.django_db
class TestProjectListFundFields:
    def test_fund_count_totals_donors(self, client, admin):
        org = admin.organizations.first()
        p = Project.objects.create(organization=org, name="P1", status="active")
        ProjectFund.objects.create(organization=org, project=p, source="Donor A", amount=100, status="received")
        ProjectFund.objects.create(organization=org, project=p, source="Donor B", amount=50,
                                   currency="USD", status="prospect")

        row = [r for r in _rows(client.get("/api/projects/")) if r["name"] == "P1"][0]
        assert row["fund_count"] == 2
        assert row["fund_totals"] == {"IDR": 100.0, "USD": 50.0}
        # One fund secured -> donors list comes from secured funds only.
        assert row["fund_donors"] == ["Donor A"]

    def test_donors_fall_back_to_pipeline_when_nothing_secured(self, client, admin):
        org = admin.organizations.first()
        p = Project.objects.create(organization=org, name="P2", status="active")
        ProjectFund.objects.create(organization=org, project=p, source="Prospect X", amount=10, status="prospect")
        row = [r for r in _rows(client.get("/api/projects/")) if r["name"] == "P2"][0]
        assert row["fund_donors"] == ["Prospect X"]

    def test_queries_constant_in_rows(self, client, admin):
        org = admin.organizations.first()
        p = Project.objects.create(organization=org, name="P0", status="active")
        ProjectFund.objects.create(organization=org, project=p, source="D", amount=1, status="received")
        with CaptureQueriesContext(connection) as small:
            client.get("/api/projects/")
        for i in range(1, 7):
            pi = Project.objects.create(organization=org, name=f"PX{i}", status="active")
            ProjectFund.objects.create(organization=org, project=pi, source=f"D{i}", amount=i, status="received")
        with CaptureQueriesContext(connection) as big:
            client.get("/api/projects/")
        assert len(big.captured_queries) == len(small.captured_queries)


@pytest.mark.django_db
class TestProjectsMine:
    def test_returns_member_projects_with_fund_fields(self, client, admin):
        org = admin.organizations.first()
        mine = Project.objects.create(organization=org, name="Mine", status="active")
        ProjectTeamMember.objects.create(project=mine, user=admin)
        ProjectFund.objects.create(organization=org, project=mine, source="D", amount=42, status="received")
        Project.objects.create(organization=org, name="NotMine", status="active")

        rows = client.get("/api/projects/mine/").json()
        names = [r["name"] for r in rows]
        assert names == ["Mine"]
        assert rows[0]["fund_count"] == 1

    def test_queries_constant_in_rows(self, client, admin):
        org = admin.organizations.first()
        p = Project.objects.create(organization=org, name="M0", status="active")
        ProjectTeamMember.objects.create(project=p, user=admin)
        ProjectFund.objects.create(organization=org, project=p, source="D", amount=1, status="received")
        with CaptureQueriesContext(connection) as small:
            client.get("/api/projects/mine/")
        for i in range(1, 6):
            pi = Project.objects.create(organization=org, name=f"M{i}", status="active")
            ProjectTeamMember.objects.create(project=pi, user=admin)
            ProjectFund.objects.create(organization=org, project=pi, source=f"D{i}", amount=i, status="received")
        with CaptureQueriesContext(connection) as big:
            client.get("/api/projects/mine/")
        assert len(big.captured_queries) == len(small.captured_queries)


@pytest.mark.django_db
class TestDateBoundQ:
    """SQL date pushdown must match the Python `field_a or field_b` fallback."""

    def test_primary_field_set(self, organization):
        p = Project.objects.create(organization=organization, name="P", status="active")
        ProjectFinance.objects.create(project=p, description="in", amount=1,
                                      type="income", date=date(2026, 3, 1))
        ProjectFinance.objects.create(project=p, description="out", amount=1,
                                      type="income", date=date(2025, 1, 1))
        q = _date_bound_q(["date", "created_at__date"], date(2026, 1, 1), date(2026, 12, 31))
        assert list(ProjectFinance.objects.filter(q).values_list("description", flat=True)) == ["in"]

    def test_falls_back_to_created_at_when_date_null(self, organization):
        p = Project.objects.create(organization=organization, name="P", status="active")
        e = ProjectFinance.objects.create(project=p, description="nulldate", amount=1,
                                          type="income", date=None)
        today = e.created_at.date()
        q_hit = _date_bound_q(["date", "created_at__date"], today, today)
        q_miss = _date_bound_q(["date", "created_at__date"], date(2000, 1, 1), date(2000, 1, 2))
        assert ProjectFinance.objects.filter(q_hit).count() == 1
        assert ProjectFinance.objects.filter(q_miss).count() == 0

    def test_three_level_fallback_chain(self, organization):
        p = Project.objects.create(organization=organization, name="P", status="active")
        ProjectFund.objects.create(organization=organization, project=p, source="received", amount=1, status="received",
                                   received_date=date(2026, 2, 1))
        ProjectFund.objects.create(organization=organization, project=p, source="agreement", amount=1, status="received",
                                   agreement_date=date(2026, 3, 1))
        created_only = ProjectFund.objects.create(organization=organization, project=p, source="created", amount=1,
                                                  status="received")
        lookups = ["received_date", "agreement_date", "created_at__date"]
        q = _date_bound_q(lookups, date(2026, 1, 1), date(2026, 12, 31))
        got = set(ProjectFund.objects.filter(q).values_list("source", flat=True))
        expected = {"received", "agreement"}
        if date(2026, 1, 1) <= created_only.created_at.date() <= date(2026, 12, 31):
            expected.add("created")
        assert got == expected

    def test_start_only_and_end_only(self, organization):
        p = Project.objects.create(organization=organization, name="P", status="active")
        ProjectFinance.objects.create(project=p, description="old", amount=1,
                                      type="income", date=date(2024, 1, 1))
        ProjectFinance.objects.create(project=p, description="new", amount=1,
                                      type="income", date=date(2026, 1, 1))
        only_start = _date_bound_q(["date", "created_at__date"], date(2025, 1, 1), None)
        only_end = _date_bound_q(["date", "created_at__date"], None, date(2025, 1, 1))
        assert list(ProjectFinance.objects.filter(only_start)
                    .values_list("description", flat=True)) == ["new"]
        assert list(ProjectFinance.objects.filter(only_end)
                    .values_list("description", flat=True)) == ["old"]


@pytest.mark.django_db
class TestCollectCashTransactions:
    def test_bounded_equals_python_filtered(self, organization):
        p = Project.objects.create(organization=organization, name="P", status="active")
        ProjectFinance.objects.create(project=p, description="a", amount=10,
                                      type="income", date=date(2026, 2, 1))
        ProjectFinance.objects.create(project=p, description="b", amount=5,
                                      type="expense", date=date(2024, 2, 1))
        ProjectFund.objects.create(organization=organization, project=p, source="F", amount=99, status="received",
                                   received_date=date(2026, 3, 3))

        start, end = date(2026, 1, 1), date(2026, 12, 31)
        bounded = _collect_cash_transactions(organization, start=start, end=end)
        everything = _collect_cash_transactions(organization)
        expected = [r for r in everything
                    if start.isoformat() <= r["date"] <= end.isoformat()]
        assert sorted(r["id"] for r in bounded) == sorted(r["id"] for r in expected)
        assert {r["id"] for r in bounded} == {
            r["id"] for r in everything if r["date"].startswith("2026")
        }

    def test_sorted_desc(self, organization):
        p = Project.objects.create(organization=organization, name="P", status="active")
        ProjectFinance.objects.create(project=p, description="a", amount=1,
                                      type="income", date=date(2026, 1, 1))
        ProjectFinance.objects.create(project=p, description="b", amount=1,
                                      type="income", date=date(2026, 5, 1))
        rows = _collect_cash_transactions(organization)
        dates = [r["date"] for r in rows]
        assert dates == sorted(dates, reverse=True)

    def test_reimbursement_and_payment_request_scoped_to_org(self, organization):
        """Regression: paid Reimbursements/PaymentRequests from another org
        must not leak into this org's cash-transaction feed."""
        from apps.administrations.models import Reimbursement, PaymentRequest

        other_org = Organization.objects.create(name="Other Org")
        requester = User.objects.create_user(email="req@test.local", password="x")
        Reimbursement.objects.create(
            organization=organization, requester=requester, title="Mine", description="d",
            amount=100, status="paid",
        )
        Reimbursement.objects.create(
            organization=other_org, requester=requester, title="Theirs", description="d",
            amount=200, status="paid",
        )
        PaymentRequest.objects.create(
            organization=organization, requester=requester, title="Mine PR", description="d",
            amount=100, payee="V", status="paid",
        )
        PaymentRequest.objects.create(
            organization=other_org, requester=requester, title="Theirs PR", description="d",
            amount=200, payee="V", status="paid",
        )

        rows = _collect_cash_transactions(organization)
        descriptions = {r["description"] for r in rows}
        assert "Mine" in descriptions and "Mine PR" in descriptions
        assert "Theirs" not in descriptions and "Theirs PR" not in descriptions

    def test_all_six_sources_scoped_to_org(self, organization):
        """PERF-003: ProjectFinance, procurement Payment, Payroll, and
        ProjectFund were confirmed missing an organization predicate — a
        mixed-tenant fixture across every source must prove none of another
        org's rows enter this org's cash-transaction feed."""
        from apps.procurements.models import Payment, Invoice
        from apps.asset_management.models import Vendor
        from apps.hr.models import Payroll, Employee
        from apps.companies.models import OrganizationMembership

        other_org = Organization.objects.create(name="Other Org")

        p_mine = Project.objects.create(organization=organization, name="Mine", status="active")
        p_theirs = Project.objects.create(organization=other_org, name="Theirs", status="active")
        ProjectFinance.objects.create(
            project=p_mine, description="Finance Mine", amount=10, type="income", date=date(2026, 2, 1),
        )
        ProjectFinance.objects.create(
            project=p_theirs, description="Finance Theirs", amount=20, type="income", date=date(2026, 2, 1),
        )

        vendor_mine = Vendor.objects.create(organization=organization, name="Vendor Mine")
        vendor_theirs = Vendor.objects.create(organization=other_org, name="Vendor Theirs")
        invoice_mine = Invoice.objects.create(
            organization=organization, vendor=vendor_mine, invoice_number="INV-M",
            amount=10, total=10, due_date=date(2026, 2, 1),
        )
        invoice_theirs = Invoice.objects.create(
            organization=other_org, vendor=vendor_theirs, invoice_number="INV-T",
            amount=20, total=20, due_date=date(2026, 2, 1),
        )
        Payment.objects.create(
            organization=organization, invoice=invoice_mine, amount=10, method="bank_transfer",
            status="completed", paid_at="2026-02-01T00:00:00Z",
        )
        Payment.objects.create(
            organization=other_org, invoice=invoice_theirs, amount=20, method="bank_transfer",
            status="completed", paid_at="2026-02-01T00:00:00Z",
        )

        from apps.hr.signals import ensure_employee_stub

        user_mine = User.objects.create_user(email="emp-mine@test.local", password="x")
        OrganizationMembership.objects.create(organization=organization, user=user_mine)
        ensure_employee_stub(user_mine)
        user_theirs = User.objects.create_user(email="emp-theirs@test.local", password="x")
        OrganizationMembership.objects.create(organization=other_org, user=user_theirs)
        ensure_employee_stub(user_theirs)
        emp_mine = Employee.objects.get(user=user_mine)
        emp_theirs = Employee.objects.get(user=user_theirs)
        Payroll.objects.create(
            employee=emp_mine, period_start=date(2026, 2, 1), period_end=date(2026, 2, 28),
            base_salary=100, net_salary=100, status="paid", paid_at="2026-02-15T00:00:00Z",
        )
        Payroll.objects.create(
            employee=emp_theirs, period_start=date(2026, 2, 1), period_end=date(2026, 2, 28),
            base_salary=200, net_salary=200, status="paid", paid_at="2026-02-15T00:00:00Z",
        )

        ProjectFund.objects.create(
            organization=organization, project=p_mine, source="Fund Mine", amount=30,
            status="received", received_date=date(2026, 2, 10),
        )
        ProjectFund.objects.create(
            organization=other_org, project=p_theirs, source="Fund Theirs", amount=40,
            status="received", received_date=date(2026, 2, 10),
        )

        rows = _collect_cash_transactions(organization)
        descriptions = {r["description"] for r in rows}
        assert any("Mine" in d for d in descriptions)
        assert not any("Theirs" in d for d in descriptions)
        amounts = {r["amount"] for r in rows}
        assert 20 not in amounts and 40 not in amounts and 200 not in amounts

    def test_stable_tie_breaker_on_identical_dates(self, organization):
        """Rows sharing a date must still sort deterministically — `id` is
        the tie-breaker so pagination never reorders or duplicates them."""
        p = Project.objects.create(organization=organization, name="P", status="active")
        same_date = date(2026, 4, 1)
        ProjectFinance.objects.create(project=p, description="x", amount=1, type="income", date=same_date)
        ProjectFinance.objects.create(project=p, description="y", amount=2, type="income", date=same_date)
        ProjectFinance.objects.create(project=p, description="z", amount=3, type="income", date=same_date)

        first = _collect_cash_transactions(organization)
        second = _collect_cash_transactions(organization)
        assert [r["id"] for r in first] == [r["id"] for r in second]


@pytest.mark.django_db
class TestAllTransactionsEndpoint:
    def test_row_cap_is_enforced(self, client, admin, monkeypatch):
        """PERF-003: ?all=true must stay bounded by a documented maximum
        without reading every matching source row into Python first."""
        from apps.projects import views as projects_views

        org = admin.organizations.first()
        p = Project.objects.create(organization=org, name="P", status="active")
        for i in range(5):
            ProjectFinance.objects.create(
                project=p, description=f"e{i}", amount=1, type="income", date=date(2026, 1, 1),
            )
        monkeypatch.setattr(projects_views.ProjectViewSet, "ALL_TRANSACTIONS_MAX_ROWS", 3)

        with CaptureQueriesContext(connection) as queries:
            resp = client.get("/api/projects/all-transactions/?all=true")

        assert resp.status_code == 200
        assert len(resp.json()) == 3
        finance_queries = [
            query["sql"] for query in queries.captured_queries
            if 'FROM "project_finance"' in query["sql"]
        ]
        assert len(finance_queries) == 1
        assert "LIMIT 3" in finance_queries[0]


@pytest.mark.django_db
class TestCalendarItemsScopedToOrg:
    def test_tasks_and_activities_scoped_to_org(self, client, admin):
        from apps.tasks.models import Task
        from apps.activities.models import Activity
        from datetime import datetime

        # create_superuser auto-provisions its own Organization; use it so the
        # client's single membership matches get_active_organization's pick.
        organization = admin.organizations.get()
        other_org = Organization.objects.create(name="Other Org")
        p = Project.objects.create(organization=organization, name="P", status="active")
        Task.objects.create(
            organization=organization, title="Mine", created_by=admin, project=p,
            due_date=datetime(2026, 6, 1),
        )
        Task.objects.create(
            organization=other_org, title="Theirs", created_by=admin, project=p,
            due_date=datetime(2026, 6, 1),
        )
        Activity.objects.create(
            organization=organization, title="Mine act", activity_type="call", user=admin,
            project=p, due_date=datetime(2026, 6, 1),
        )
        Activity.objects.create(
            organization=other_org, title="Theirs act", activity_type="call", user=admin,
            project=p, due_date=datetime(2026, 6, 1),
        )

        resp = client.get("/api/projects/calendar-all/")
        titles = {row["title"] for row in resp.json()}
        assert {"Mine", "Mine act"}.issubset(titles)
        assert "Theirs" not in titles and "Theirs act" not in titles


@pytest.mark.django_db
class TestFinanceSummary:
    def test_per_project_rollup(self, client, admin):
        from django.core.cache import cache
        cache.clear()  # endpoint caches for 60s; stale entries cross test boundaries
        p = Project.objects.create(organization=admin.organizations.first(), name="Roll", status="active")
        ProjectFinance.objects.create(project=p, description="i", amount=100,
                                      type="income", date=date(2026, 5, 1))
        ProjectFinance.objects.create(project=p, description="e", amount=30,
                                      type="expense", date=date(2026, 5, 2))

        body = client.get("/api/projects/finance-summary/").json()
        row = [r for r in body["per_project"] if r["name"] == "Roll"][0]
        assert row["income"] == 100.0
        assert row["expense"] == 30.0
        assert row["balance"] == 70.0
        assert row["entry_count"] == 2
        assert row["lead"] is None
