"""Donor portal: scoping (fail-closed), review workflow, summary endpoints,
and donor-side team management."""
from datetime import date, timedelta
from decimal import Decimal

import pytest
from django.core.files.uploadedfile import SimpleUploadedFile
from rest_framework.test import APIClient

from apps.companies.models import Company, Organization, OrganizationMembership
from apps.core.models import Invitation, User
from apps.meetings.models import Meeting
from apps.projects.models import (
    Deliverable,
    FundReport,
    IndicatorRecord,
    Project,
    ProjectFund,
    ProjectIndicator,
    ProjectRisk,
    ProjectSchedule,
    SubmissionVersion,
)


def api(user):
    client = APIClient()
    client.force_authenticate(user)
    return client


def unwrap(data):
    """Paginated or plain-list response -> list of rows."""
    return data["results"] if isinstance(data, dict) and "results" in data else data


@pytest.fixture
def org(db):
    return Organization.objects.create(name="CSIS")


@pytest.fixture
def company_a(org):
    return Company.objects.create(name="Donor A", organization=org, company_type="donor")


@pytest.fixture
def company_b(org):
    return Company.objects.create(name="Donor B", organization=org, company_type="donor")


@pytest.fixture
def donor_admin_a(org, company_a):
    u = User.objects.create_user(
        email="admin-a@donor.test", password="p",
        organization_role="donor", donor_company=company_a, donor_role="admin",
    )
    OrganizationMembership.objects.create(organization=org, user=u)
    return u


@pytest.fixture
def donor_viewer_a(org, company_a):
    u = User.objects.create_user(
        email="viewer-a@donor.test", password="p",
        organization_role="donor", donor_company=company_a, donor_role="viewer",
    )
    OrganizationMembership.objects.create(organization=org, user=u)
    return u


@pytest.fixture
def donor_b(org, company_b):
    u = User.objects.create_user(
        email="admin-b@donor.test", password="p",
        organization_role="donor", donor_company=company_b, donor_role="admin",
    )
    OrganizationMembership.objects.create(organization=org, user=u)
    return u


@pytest.fixture
def staff(org):
    u = User.objects.create_user(email="staff@csis.test", password="p", organization_role="director")
    OrganizationMembership.objects.create(organization=org, user=u)
    return u


@pytest.fixture
def project_a(org):
    return Project.objects.create(organization=org, name="Project A")


@pytest.fixture
def project_b(org):
    return Project.objects.create(organization=org, name="Project B")


@pytest.fixture
def fund_a(org, company_a, project_a):
    return ProjectFund.objects.create(
        organization=org, project=project_a, source="Donor A", donor=company_a,
        amount=Decimal("100000"), currency="USD", grant_number="GA-001",
    )


@pytest.fixture
def fund_b(org, company_b, project_b):
    return ProjectFund.objects.create(
        organization=org, project=project_b, source="Donor B", donor=company_b,
        amount=Decimal("50000"), currency="USD",
    )


@pytest.mark.django_db
class TestDonorScoping:
    """Every donor list endpoint must return only the requesting donor's rows
    and must be closed to staff accounts."""

    def test_funds_scoped_to_own_company(self, donor_admin_a, fund_a, fund_b):
        rows = unwrap(api(donor_admin_a).get("/api/donor/funds/").json())
        assert [r["grant_number"] for r in rows] == ["GA-001"]

    def test_staff_rejected(self, staff, fund_a):
        # Directors are not donors; the donor surface stays closed to them.
        assert api(staff).get("/api/donor/funds/").status_code == 403

    def test_anonymous_rejected(self, db):
        assert APIClient().get("/api/donor/funds/").status_code in (401, 403)

    def test_deliverables_scoped(self, donor_admin_a, fund_a, fund_b):
        Deliverable.objects.create(fund=fund_a, title="Policy Brief A")
        Deliverable.objects.create(fund=fund_b, title="Policy Brief B")
        Deliverable.objects.create(fund=fund_a, title="Hidden", donor_visible=False)
        rows = unwrap(api(donor_admin_a).get("/api/donor/deliverables/").json())
        assert [r["title"] for r in rows] == ["Policy Brief A"]

    def test_indicators_scoped_and_visible_only(self, donor_admin_a, fund_a, project_a, project_b, fund_b):
        ProjectIndicator.objects.create(project=project_a, name="Publications", target=10)
        ProjectIndicator.objects.create(project=project_a, name="Secret", target=1, donor_visible=False)
        ProjectIndicator.objects.create(project=project_b, name="Other donor", target=5)
        rows = unwrap(api(donor_admin_a).get("/api/donor/indicators/").json())
        assert [r["name"] for r in rows] == ["Publications"]

    def test_risks_fail_closed(self, donor_admin_a, fund_a, project_a):
        ProjectRisk.objects.create(project=project_a, title="Internal only")  # default hidden
        ProjectRisk.objects.create(project=project_a, title="Shared", donor_visible=True)
        rows = unwrap(api(donor_admin_a).get("/api/donor/risks/").json())
        assert [r["title"] for r in rows] == ["Shared"]

    def test_milestones_scoped(self, donor_admin_a, fund_a, project_a):
        ProjectSchedule.objects.create(project=project_a, title="Phase 1")
        ProjectSchedule.objects.create(project=project_a, title="Internal", donor_visible=False)
        rows = unwrap(api(donor_admin_a).get("/api/donor/milestones/").json())
        assert [r["title"] for r in rows] == ["Phase 1"]

    def test_meetings_scoped(self, donor_admin_a, org, fund_a, fund_b, staff):
        from django.utils import timezone
        now = timezone.now()
        Meeting.objects.create(
            organization=org, title="Grant review A", fund=fund_a, donor_visible=True,
            start_time=now, end_time=now + timedelta(hours=1), organizer=staff,
        )
        Meeting.objects.create(
            organization=org, title="Internal", fund=fund_a, donor_visible=False,
            start_time=now, end_time=now + timedelta(hours=1), organizer=staff,
        )
        Meeting.objects.create(
            organization=org, title="Grant review B", fund=fund_b, donor_visible=True,
            start_time=now, end_time=now + timedelta(hours=1), organizer=staff,
        )
        rows = unwrap(api(donor_admin_a).get("/api/donor/meetings/").json())
        assert [r["title"] for r in rows] == ["Grant review A"]


@pytest.mark.django_db
class TestIndicators:
    def test_current_value_sum_and_latest(self, project_a):
        cumulative = ProjectIndicator.objects.create(project=project_a, name="Participants", aggregation="sum", target=100)
        IndicatorRecord.objects.create(indicator=cumulative, period_date=date(2026, 3, 31), value=30)
        IndicatorRecord.objects.create(indicator=cumulative, period_date=date(2026, 6, 30), value=20)
        assert cumulative.current_value() == 50

        latest = ProjectIndicator.objects.create(project=project_a, name="Score", aggregation="latest", target=10)
        IndicatorRecord.objects.create(indicator=latest, period_date=date(2026, 3, 31), value=4)
        IndicatorRecord.objects.create(indicator=latest, period_date=date(2026, 6, 30), value=7)
        assert latest.current_value() == 7

    def test_indicator_serializer_achievement(self, donor_admin_a, fund_a, project_a):
        ind = ProjectIndicator.objects.create(project=project_a, name="Publications", target=10, aggregation="sum")
        IndicatorRecord.objects.create(indicator=ind, period_date=date(2026, 6, 30), value=4)
        rows = unwrap(api(donor_admin_a).get("/api/donor/indicators/").json())
        assert rows[0]["current_value"] == "4.00"
        assert rows[0]["achievement_pct"] == 40.0


@pytest.mark.django_db
class TestReviewWorkflow:
    def _submit(self, staff, report):
        return api(staff).post(
            f"/api/fund-reports/{report.id}/submit/",
            {"file": SimpleUploadedFile("r.pdf", b"pdf"), "note": "first draft"},
            format="multipart",
        )

    def test_staff_submit_creates_version_and_status(self, staff, fund_a):
        report = FundReport.objects.create(fund=fund_a, title="Q1", status="upcoming")
        resp = self._submit(staff, report)
        assert resp.status_code == 200, resp.content
        report.refresh_from_db()
        assert report.status == "submitted"
        assert report.submitted_date == date.today()
        assert SubmissionVersion.objects.filter(object_id=str(report.id)).count() == 1

    def test_donor_admin_approves(self, staff, donor_admin_a, fund_a):
        report = FundReport.objects.create(fund=fund_a, title="Q1", status="upcoming")
        self._submit(staff, report)
        resp = api(donor_admin_a).post(
            f"/api/donor/fund-reports/{report.id}/review/",
            {"action": "approve", "note": "Looks good"}, format="json",
        )
        assert resp.status_code == 200, resp.content
        report.refresh_from_db()
        assert report.status == "approved"
        assert report.reviewed_by == donor_admin_a

    def test_viewer_cannot_review(self, staff, donor_viewer_a, fund_a):
        report = FundReport.objects.create(fund=fund_a, title="Q1", status="upcoming")
        self._submit(staff, report)
        resp = api(donor_viewer_a).post(
            f"/api/donor/fund-reports/{report.id}/review/", {"action": "approve"}, format="json",
        )
        assert resp.status_code == 403

    def test_other_donor_cannot_review(self, staff, donor_b, fund_a):
        report = FundReport.objects.create(fund=fund_a, title="Q1", status="upcoming")
        self._submit(staff, report)
        resp = api(donor_b).post(
            f"/api/donor/fund-reports/{report.id}/review/", {"action": "approve"}, format="json",
        )
        assert resp.status_code == 404

    def test_illegal_transition_rejected(self, donor_admin_a, fund_a):
        report = FundReport.objects.create(fund=fund_a, title="Q1", status="upcoming")
        resp = api(donor_admin_a).post(
            f"/api/donor/fund-reports/{report.id}/review/", {"action": "approve"}, format="json",
        )
        assert resp.status_code == 400

    def test_revision_then_resubmit_bumps_version(self, staff, donor_admin_a, fund_a):
        report = FundReport.objects.create(fund=fund_a, title="Q1", status="upcoming")
        self._submit(staff, report)
        api(donor_admin_a).post(
            f"/api/donor/fund-reports/{report.id}/review/",
            {"action": "request_revision", "note": "fix annex"}, format="json",
        )
        report.refresh_from_db()
        assert report.status == "revision_requested"
        resp = api(staff).post(
            f"/api/fund-reports/{report.id}/submit/",
            {"file": SimpleUploadedFile("r2.pdf", b"pdf2"), "note": "annex fixed"},
            format="multipart",
        )
        assert resp.status_code == 200
        report.refresh_from_db()
        assert report.status == "resubmitted"
        versions = SubmissionVersion.objects.filter(object_id=str(report.id)).order_by("version_number")
        assert [v.version_number for v in versions] == [1, 2]

    def test_deliverable_approval_sets_approved_date(self, staff, donor_admin_a, fund_a):
        d = Deliverable.objects.create(fund=fund_a, title="Policy Brief")
        resp = api(staff).post(
            f"/api/deliverables/{d.id}/submit/",
            {"file": SimpleUploadedFile("d.pdf", b"pdf"), "note": "v1"},
            format="multipart",
        )
        assert resp.status_code == 200, resp.content
        resp = api(donor_admin_a).post(
            f"/api/donor/deliverables/{d.id}/review/", {"action": "approve"}, format="json",
        )
        assert resp.status_code == 200, resp.content
        d.refresh_from_db()
        assert d.status == "approved"
        assert d.approved_date == date.today()

    def test_donor_sees_versions(self, staff, donor_admin_a, fund_a):
        report = FundReport.objects.create(fund=fund_a, title="Q1", status="upcoming")
        self._submit(staff, report)
        detail = api(donor_admin_a).get(f"/api/donor/fund-reports/{report.id}/").json()
        assert len(detail["versions"]) == 1
        assert detail["versions"][0]["version_number"] == 1


@pytest.mark.django_db
class TestDeliverableDonorStatus:
    def test_internal_statuses_collapsed(self, donor_admin_a, fund_a):
        Deliverable.objects.create(fund=fund_a, title="WIP", status="internal_review")
        rows = unwrap(api(donor_admin_a).get("/api/donor/deliverables/").json())
        assert rows[0]["donor_status"] == "in_preparation"


@pytest.mark.django_db
class TestDonorSummary:
    def test_summary_totals(self, donor_admin_a, fund_a, fund_b):
        from apps.projects.models import ProjectFundTranche
        ProjectFundTranche.objects.create(fund=fund_a, label="T1", amount=Decimal("40000"), status="received")
        FundReport.objects.create(fund=fund_a, title="Due soon", status="upcoming", due_date=date.today() + timedelta(days=10))
        resp = api(donor_admin_a).get("/api/donor/summary/")
        assert resp.status_code == 200
        data = resp.json()
        assert data["funding"]["committed"] == "100000.00"
        assert data["funding"]["disbursed"] == "40000.00"
        assert data["obligations"]["reports_due_soon"] == 1
        assert data["projects"]["total"] == 1

    def test_finance_summary_scoped(self, donor_b, fund_a):
        resp = api(donor_b).get(f"/api/donor/finance-summary/?fund={fund_a.id}")
        assert resp.status_code == 404


@pytest.mark.django_db
class TestDonorTeam:
    def test_admin_lists_own_company_users(self, donor_admin_a, donor_viewer_a, donor_b):
        rows = unwrap(api(donor_admin_a).get("/api/donor/team/").json())
        emails = {r["email"] for r in rows}
        assert emails == {"admin-a@donor.test", "viewer-a@donor.test"}

    def test_admin_invites_viewer(self, donor_admin_a, fund_a, company_a):
        resp = api(donor_admin_a).post(
            "/api/donor/team/invite/",
            {"email": "new@donor.test", "donor_role": "viewer"}, format="json",
        )
        assert resp.status_code == 201, resp.content
        invite = Invitation.objects.get(email="new@donor.test")
        assert str(invite.donor_company_id).replace("-", "") == str(company_a.id).replace("-", "")
        assert invite.donor_role == "viewer"

    def test_viewer_cannot_invite(self, donor_viewer_a, fund_a):
        resp = api(donor_viewer_a).post(
            "/api/donor/team/invite/",
            {"email": "x@donor.test", "donor_role": "viewer"}, format="json",
        )
        assert resp.status_code == 403
