"""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

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


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):
        p = Project.objects.create(name="P1", status="active")
        ProjectFund.objects.create(project=p, source="Donor A", amount=100, status="received")
        ProjectFund.objects.create(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):
        p = Project.objects.create(name="P2", status="active")
        ProjectFund.objects.create(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):
        p = Project.objects.create(name="P0", status="active")
        ProjectFund.objects.create(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(name=f"PX{i}", status="active")
            ProjectFund.objects.create(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):
        mine = Project.objects.create(name="Mine", status="active")
        ProjectTeamMember.objects.create(project=mine, user=admin)
        ProjectFund.objects.create(project=mine, source="D", amount=42, status="received")
        Project.objects.create(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):
        p = Project.objects.create(name="M0", status="active")
        ProjectTeamMember.objects.create(project=p, user=admin)
        ProjectFund.objects.create(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(name=f"M{i}", status="active")
            ProjectTeamMember.objects.create(project=pi, user=admin)
            ProjectFund.objects.create(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):
        p = Project.objects.create(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):
        p = Project.objects.create(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):
        p = Project.objects.create(name="P", status="active")
        ProjectFund.objects.create(project=p, source="received", amount=1, status="received",
                                   received_date=date(2026, 2, 1))
        ProjectFund.objects.create(project=p, source="agreement", amount=1, status="received",
                                   agreement_date=date(2026, 3, 1))
        created_only = ProjectFund.objects.create(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):
        p = Project.objects.create(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):
        p = Project.objects.create(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(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(start=start, end=end)
        everything = _collect_cash_transactions()
        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):
        p = Project.objects.create(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()
        dates = [r["date"] for r in rows]
        assert dates == sorted(dates, reverse=True)


@pytest.mark.django_db
class TestFinanceSummary:
    def test_per_project_rollup(self, client):
        from django.core.cache import cache
        cache.clear()  # endpoint caches for 60s; stale entries cross test boundaries
        p = Project.objects.create(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
