"""S4.2 — Leave Management backend tests.

Covers:
- Working-day counting (count_working_days helper)
- LeaveType services (create, update, delete, unique code constraint)
- PublicHoliday services (create, delete)
- LeaveBalance services (allocate, update)
- LeaveRequest services:
  - create (date validation, working-day calc)
  - submit (overlap check, balance check, auto-approve)
  - approve / reject (requires note) / cancel
  - double-cancel / double-approve guard
- Registered permissions (3 new leave codenames present after sync)
- API endpoints:
  - leave-types CRUD
  - public-holidays CRUD
  - leave-requests CRUD + action verbs
  - leave-balances list + allocate
  - leave-calendar
"""

from __future__ import annotations

from datetime import date

import pytest

from simorgh.apps.hr_core.models import (
    LeaveBalance,
    LeaveRequest,
    LeaveRequestStatus,
    LeaveType,
    PublicHoliday,
)
from simorgh.apps.hr_core import services, selectors as sel
from simorgh.apps.hr_core.permissions import (
    PERM_LEAVE_APPROVE,
    PERM_LEAVE_MANAGE,
    PERM_LEAVE_REQUEST,
)
from simorgh.apps.iam.registry import sync_registry_to_db
from simorgh.apps.iam.models import Permission, Role
from simorgh.apps.memberships.models import Membership
from rest_framework.test import APIClient


# ---------------------------------------------------------------------------
# Helpers
# ---------------------------------------------------------------------------

def _api(user=None):
    client = APIClient()
    if user:
        client.force_login(user)
    return client


def _with_tenant(client, tenant):
    client.credentials(HTTP_X_TENANT=tenant.slug)
    return client


# ---------------------------------------------------------------------------
# Fixtures
# ---------------------------------------------------------------------------

@pytest.fixture
def perms_synced(db):
    sync_registry_to_db()
    return {p.codename: p for p in Permission.objects.all()}


@pytest.fixture
def role_leave_manager(tenant_acme, perms_synced):
    """Has all three leave permissions."""
    role = Role.objects.create(tenant=tenant_acme, code="leave_mgr", name="Leave Manager")
    role.permissions.set([
        perms_synced[PERM_LEAVE_REQUEST],
        perms_synced[PERM_LEAVE_APPROVE],
        perms_synced[PERM_LEAVE_MANAGE],
    ])
    return role


@pytest.fixture
def role_leave_requester(tenant_acme, perms_synced):
    """Can only submit requests."""
    role = Role.objects.create(tenant=tenant_acme, code="leave_req", name="Leave Requester")
    role.permissions.set([perms_synced[PERM_LEAVE_REQUEST]])
    return role


@pytest.fixture
def alice_leave_mgr(alice, tenant_acme, acme_tree, role_leave_manager):
    m = Membership.objects.create(
        tenant=tenant_acme,
        organization_node=acme_tree["root"],
        role=role_leave_manager,
    )
    m.users.add(alice)
    return m


@pytest.fixture
def alice_leave_req(alice, tenant_acme, acme_tree, role_leave_requester):
    m = Membership.objects.create(
        tenant=tenant_acme,
        organization_node=acme_tree["root"],
        role=role_leave_requester,
    )
    m.users.add(alice)
    return m


@pytest.fixture
def leave_type_annual(tenant_acme):
    return services.create_leave_type(
        tenant_id=tenant_acme.pk,
        name="Annual",
        code="annual",
        is_paid=True,
        requires_approval=True,
        max_days_per_year=20,
        carry_forward_days=5,
    )


@pytest.fixture
def leave_type_sick(tenant_acme):
    return services.create_leave_type(
        tenant_id=tenant_acme.pk,
        name="Sick",
        code="sick",
        is_paid=True,
        requires_approval=False,
        max_days_per_year=10,
    )


@pytest.fixture
def employee(tenant_acme, acme_tree):
    from simorgh.apps.hr_core import services as hr_svc
    return hr_svc.create_employee(
        tenant_id=tenant_acme.pk,
        organization_node_id=acme_tree["root"].pk,
        first_name="Reza",
        last_name="Ahmadi",
        hire_date=date(2020, 1, 1),
        employment_type="full_time",
        work_email="reza@acme.example",
    )


@pytest.fixture
def balance_annual(employee, leave_type_annual):
    return services.allocate_leave_balance(
        employee=employee,
        leave_type=leave_type_annual,
        year=2025,
        days=20,
    )


@pytest.fixture
def leave_request_draft(tenant_acme, employee, leave_type_annual, balance_annual):
    return services.create_leave_request(
        tenant_id=tenant_acme.pk,
        employee=employee,
        leave_type=leave_type_annual,
        from_date=date(2025, 6, 2),   # Monday
        to_date=date(2025, 6, 6),     # Friday  → 5 working days
        reason="Vacation",
    )


# ===========================================================================
# Tests
# ===========================================================================

# ---------------------------------------------------------------------------
# Working day counting
# ---------------------------------------------------------------------------

@pytest.mark.django_db
class TestWorkingDays:
    def test_full_week(self):
        # 2025-06-02 Mon → 2025-06-06 Fri = 5 days
        assert services.count_working_days(date(2025, 6, 2), date(2025, 6, 6)) == 5

    def test_includes_no_weekend(self):
        # Sat+Sun = 0
        assert services.count_working_days(date(2025, 6, 7), date(2025, 6, 8)) == 0

    def test_with_holiday(self):
        holidays = {date(2025, 6, 4)}  # Wed
        assert services.count_working_days(
            date(2025, 6, 2), date(2025, 6, 6), holiday_dates=holidays
        ) == 4

    def test_same_day_workday(self):
        assert services.count_working_days(date(2025, 6, 2), date(2025, 6, 2)) == 1

    def test_same_day_weekend(self):
        assert services.count_working_days(date(2025, 6, 7), date(2025, 6, 7)) == 0

    def test_from_after_to(self):
        assert services.count_working_days(date(2025, 6, 6), date(2025, 6, 2)) == 0


# ---------------------------------------------------------------------------
# LeaveType services
# ---------------------------------------------------------------------------

@pytest.mark.django_db
class TestLeaveTypeServices:
    def test_create(self, tenant_acme):
        lt = services.create_leave_type(tenant_id=tenant_acme.pk, name="Maternity", code="mat")
        assert lt.pk is not None
        assert lt.name == "Maternity"
        assert lt.code == "mat"

    def test_create_unique_code_constraint(self, tenant_acme):
        services.create_leave_type(tenant_id=tenant_acme.pk, name="A", code="dup")
        with pytest.raises(Exception):
            services.create_leave_type(tenant_id=tenant_acme.pk, name="B", code="dup")

    def test_update(self, tenant_acme):
        lt = services.create_leave_type(tenant_id=tenant_acme.pk, name="X", code="x1")
        updated = services.update_leave_type(lt, name="Y", is_paid=False)
        assert updated.name == "Y"
        assert updated.is_paid is False

    def test_delete(self, tenant_acme):
        lt = services.create_leave_type(tenant_id=tenant_acme.pk, name="Tmp", code="tmp1")
        pk = lt.pk
        services.delete_leave_type(lt)
        assert not LeaveType.objects.filter(pk=pk).exists()

    def test_selector_list(self, tenant_acme, leave_type_annual, leave_type_sick):
        types = list(sel.leave_types_for_tenant(tenant_acme.pk))
        codes = {lt.code for lt in types}
        assert "annual" in codes
        assert "sick" in codes

    def test_selector_by_id(self, tenant_acme, leave_type_annual):
        lt = sel.leave_type_by_id(tenant_acme.pk, leave_type_annual.pk)
        assert lt is not None
        assert lt.code == "annual"


# ---------------------------------------------------------------------------
# PublicHoliday services
# ---------------------------------------------------------------------------

@pytest.mark.django_db
class TestPublicHolidayServices:
    def test_create(self, tenant_acme):
        h = services.create_public_holiday(
            tenant_id=tenant_acme.pk,
            date=date(2025, 3, 21),
            name="Nowruz",
        )
        assert h.pk is not None
        assert h.name == "Nowruz"

    def test_delete(self, tenant_acme):
        h = services.create_public_holiday(
            tenant_id=tenant_acme.pk, date=date(2025, 4, 1), name="April Fools"
        )
        pk = h.pk
        services.delete_public_holiday(h)
        assert not PublicHoliday.objects.filter(pk=pk).exists()

    def test_unique_date_calendar_constraint(self, tenant_acme):
        services.create_public_holiday(tenant_id=tenant_acme.pk, date=date(2025, 5, 1), name="Labor Day")
        with pytest.raises(Exception):
            services.create_public_holiday(tenant_id=tenant_acme.pk, date=date(2025, 5, 1), name="Dup")


# ---------------------------------------------------------------------------
# LeaveBalance services
# ---------------------------------------------------------------------------

@pytest.mark.django_db
class TestLeaveBalanceServices:
    def test_allocate_creates(self, employee, leave_type_annual):
        bal = services.allocate_leave_balance(
            employee=employee,
            leave_type=leave_type_annual,
            year=2025,
            days=20,
        )
        assert bal.allocated == 20
        assert bal.used == 0

    def test_allocate_updates_existing(self, employee, leave_type_annual):
        services.allocate_leave_balance(employee=employee, leave_type=leave_type_annual, year=2025, days=10)
        bal = services.allocate_leave_balance(employee=employee, leave_type=leave_type_annual, year=2025, days=20)
        assert bal.allocated == 20

    def test_remaining_property(self, employee, leave_type_annual):
        bal = services.allocate_leave_balance(employee=employee, leave_type=leave_type_annual, year=2025, days=20)
        assert bal.remaining == 20

    def test_selector(self, tenant_acme, employee, leave_type_annual, balance_annual):
        qs = list(sel.leave_balances(tenant_acme.pk, employee_id=employee.pk))
        assert len(qs) == 1
        assert qs[0].allocated == 20


# ---------------------------------------------------------------------------
# LeaveRequest services
# ---------------------------------------------------------------------------

@pytest.mark.django_db
class TestLeaveRequestServices:
    def test_create_draft(self, tenant_acme, employee, leave_type_annual, balance_annual):
        req = services.create_leave_request(
            tenant_id=tenant_acme.pk,
            employee=employee,
            leave_type=leave_type_annual,
            from_date=date(2025, 6, 2),
            to_date=date(2025, 6, 6),
        )
        assert req.status == LeaveRequestStatus.DRAFT
        assert req.days_requested == 5

    def test_create_invalid_date_range(self, tenant_acme, employee, leave_type_annual):
        with pytest.raises(ValueError, match="from_date must be before"):
            services.create_leave_request(
                tenant_id=tenant_acme.pk,
                employee=employee,
                leave_type=leave_type_annual,
                from_date=date(2025, 6, 6),
                to_date=date(2025, 6, 2),
            )

    def test_submit_sets_pending(self, tenant_acme, employee, leave_type_annual, balance_annual):
        req = services.create_leave_request(
            tenant_id=tenant_acme.pk,
            employee=employee,
            leave_type=leave_type_annual,
            from_date=date(2025, 6, 2),
            to_date=date(2025, 6, 6),
        )
        req = services.submit_leave_request(req)
        assert req.status == LeaveRequestStatus.PENDING
        assert req.submitted_at is not None

    def test_auto_approve_no_approval_required(self, tenant_acme, employee, leave_type_sick):
        services.allocate_leave_balance(employee=employee, leave_type=leave_type_sick, year=2025, days=10)
        req = services.create_leave_request(
            tenant_id=tenant_acme.pk,
            employee=employee,
            leave_type=leave_type_sick,
            from_date=date(2025, 7, 7),  # Mon
            to_date=date(2025, 7, 7),
        )
        req = services.submit_leave_request(req)
        assert req.status == LeaveRequestStatus.APPROVED

    def test_overlap_check(self, tenant_acme, employee, leave_type_annual, balance_annual):
        r1 = services.create_leave_request(
            tenant_id=tenant_acme.pk, employee=employee, leave_type=leave_type_annual,
            from_date=date(2025, 6, 2), to_date=date(2025, 6, 6),
        )
        services.submit_leave_request(r1)

        r2 = services.create_leave_request(
            tenant_id=tenant_acme.pk, employee=employee, leave_type=leave_type_annual,
            from_date=date(2025, 6, 4), to_date=date(2025, 6, 10),
        )
        with pytest.raises(ValueError, match="already have a leave request"):
            services.submit_leave_request(r2)

    def test_insufficient_balance(self, tenant_acme, employee, leave_type_annual):
        services.allocate_leave_balance(employee=employee, leave_type=leave_type_annual, year=2025, days=2)
        req = services.create_leave_request(
            tenant_id=tenant_acme.pk, employee=employee, leave_type=leave_type_annual,
            from_date=date(2025, 8, 4), to_date=date(2025, 8, 8),  # 5 days
        )
        with pytest.raises(ValueError, match="Insufficient balance"):
            services.submit_leave_request(req)

    def test_submit_non_draft_fails(self, tenant_acme, employee, leave_type_annual, leave_request_draft):
        req = services.submit_leave_request(leave_request_draft)
        with pytest.raises(ValueError, match="Only draft"):
            services.submit_leave_request(req)

    def test_approve(self, alice, tenant_acme, employee, leave_type_annual, leave_request_draft):
        req = services.submit_leave_request(leave_request_draft)
        approved = services.approve_leave_request(req, reviewed_by=alice, note="OK")
        assert approved.status == LeaveRequestStatus.APPROVED
        assert approved.review_note == "OK"

    def test_approve_non_pending_fails(self, alice, tenant_acme, employee, leave_type_annual, leave_request_draft):
        req = services.submit_leave_request(leave_request_draft)
        services.approve_leave_request(req, reviewed_by=alice)
        with pytest.raises(ValueError, match="pending"):
            services.approve_leave_request(req, reviewed_by=alice)

    def test_reject_requires_note(self, alice, tenant_acme, employee, leave_type_annual, leave_request_draft):
        req = services.submit_leave_request(leave_request_draft)
        with pytest.raises(ValueError, match="rejection note"):
            services.reject_leave_request(req, reviewed_by=alice, note="")

    def test_reject(self, alice, tenant_acme, employee, leave_type_annual, leave_request_draft):
        req = services.submit_leave_request(leave_request_draft)
        rejected = services.reject_leave_request(req, reviewed_by=alice, note="Not approved")
        assert rejected.status == LeaveRequestStatus.REJECTED

    def test_cancel_draft(self, tenant_acme, employee, leave_type_annual, leave_request_draft):
        cancelled = services.cancel_leave_request(leave_request_draft)
        assert cancelled.status == LeaveRequestStatus.CANCELLED

    def test_cancel_pending(self, tenant_acme, employee, leave_type_annual, leave_request_draft):
        req = services.submit_leave_request(leave_request_draft)
        cancelled = services.cancel_leave_request(req)
        assert cancelled.status == LeaveRequestStatus.CANCELLED

    def test_cancel_already_cancelled(self, tenant_acme, employee, leave_type_annual, leave_request_draft):
        req = services.cancel_leave_request(leave_request_draft)
        with pytest.raises(ValueError, match="already cancelled"):
            services.cancel_leave_request(req)

    def test_cancel_rejected_fails(self, alice, tenant_acme, employee, leave_type_annual, leave_request_draft):
        req = services.submit_leave_request(leave_request_draft)
        req = services.reject_leave_request(req, reviewed_by=alice, note="No")
        with pytest.raises(ValueError, match="cannot be cancelled"):
            services.cancel_leave_request(req)

    def test_balance_deducted_on_approve(self, alice, tenant_acme, employee, leave_type_annual, balance_annual, leave_request_draft):
        req = services.submit_leave_request(leave_request_draft)
        services.approve_leave_request(req, reviewed_by=alice)
        balance_annual.refresh_from_db()
        assert balance_annual.used == 5
        assert balance_annual.pending == 0

    def test_balance_restored_on_cancel(self, tenant_acme, employee, leave_type_annual, balance_annual, leave_request_draft):
        req = services.submit_leave_request(leave_request_draft)
        # pending should be 5 now
        balance_annual.refresh_from_db()
        assert balance_annual.pending == 5
        services.cancel_leave_request(req)
        balance_annual.refresh_from_db()
        assert balance_annual.pending == 0


# ---------------------------------------------------------------------------
# Selectors
# ---------------------------------------------------------------------------

@pytest.mark.django_db
class TestLeaveSelectors:
    def test_leave_requests_filter_by_status(self, tenant_acme, employee, leave_type_annual, balance_annual, leave_request_draft):
        services.submit_leave_request(leave_request_draft)
        qs = sel.leave_requests(tenant_acme.pk, status="pending")
        assert qs.count() == 1

    def test_leave_request_by_public_id(self, tenant_acme, employee, leave_type_annual, balance_annual, leave_request_draft):
        r = sel.leave_request_by_public_id(tenant_acme.pk, leave_request_draft.public_id)
        assert r is not None
        assert r.pk == leave_request_draft.pk

    def test_leave_calendar(self, alice, tenant_acme, employee, leave_type_annual, balance_annual, leave_request_draft):
        req = services.submit_leave_request(leave_request_draft)
        services.approve_leave_request(req, reviewed_by=alice)
        qs = sel.leave_calendar(tenant_acme.pk, from_date=date(2025, 6, 1), to_date=date(2025, 6, 30))
        assert qs.count() == 1


# ---------------------------------------------------------------------------
# Permissions registered
# ---------------------------------------------------------------------------

@pytest.mark.django_db
class TestLeavePermissionsRegistered:
    def test_all_three_leave_perms_exist(self, db):
        sync_registry_to_db()
        codenames = set(Permission.objects.values_list("codename", flat=True))
        assert PERM_LEAVE_REQUEST in codenames
        assert PERM_LEAVE_APPROVE in codenames
        assert PERM_LEAVE_MANAGE in codenames


# ---------------------------------------------------------------------------
# API: Leave Types
# ---------------------------------------------------------------------------

@pytest.mark.django_db
class TestLeaveTypeAPI:
    def test_list_requires_auth(self, tenant_acme):
        client = _with_tenant(_api(), tenant_acme)
        resp = client.get("/api/v1/hr/leave-types/")
        assert resp.status_code == 401

    def test_list(self, alice, alice_leave_req, tenant_acme, leave_type_annual):
        client = _with_tenant(_api(alice), tenant_acme)
        resp = client.get("/api/v1/hr/leave-types/")
        assert resp.status_code == 200
        codes = [lt["code"] for lt in resp.json()]
        assert "annual" in codes

    def test_create_requires_manage(self, alice, alice_leave_req, tenant_acme):
        client = _with_tenant(_api(alice), tenant_acme)
        resp = client.post("/api/v1/hr/leave-types/", {"name": "X", "code": "x"}, format="json")
        assert resp.status_code == 403

    def test_create(self, alice, alice_leave_mgr, tenant_acme):
        client = _with_tenant(_api(alice), tenant_acme)
        resp = client.post(
            "/api/v1/hr/leave-types/",
            {"name": "Parental", "code": "parental", "is_paid": True, "requires_approval": True},
            format="json",
        )
        assert resp.status_code == 201
        assert resp.json()["code"] == "parental"

    def test_update(self, alice, alice_leave_mgr, tenant_acme, leave_type_annual):
        client = _with_tenant(_api(alice), tenant_acme)
        resp = client.patch(
            f"/api/v1/hr/leave-types/{leave_type_annual.pk}/",
            {"name": "Annual Leave"},
            format="json",
        )
        assert resp.status_code == 200
        assert resp.json()["name"] == "Annual Leave"

    def test_delete(self, alice, alice_leave_mgr, tenant_acme, leave_type_annual):
        client = _with_tenant(_api(alice), tenant_acme)
        resp = client.delete(f"/api/v1/hr/leave-types/{leave_type_annual.pk}/")
        assert resp.status_code == 204


# ---------------------------------------------------------------------------
# API: Public Holidays
# ---------------------------------------------------------------------------

@pytest.mark.django_db
class TestPublicHolidayAPI:
    def test_list(self, alice, alice_leave_req, tenant_acme):
        client = _with_tenant(_api(alice), tenant_acme)
        resp = client.get("/api/v1/hr/public-holidays/")
        assert resp.status_code == 200

    def test_create(self, alice, alice_leave_mgr, tenant_acme):
        client = _with_tenant(_api(alice), tenant_acme)
        resp = client.post(
            "/api/v1/hr/public-holidays/",
            {"date": "2025-03-21", "name": "Nowruz", "calendar_type": "jalali"},
            format="json",
        )
        assert resp.status_code == 201
        assert resp.json()["name"] == "Nowruz"

    def test_delete(self, alice, alice_leave_mgr, tenant_acme):
        client = _with_tenant(_api(alice), tenant_acme)
        r = client.post(
            "/api/v1/hr/public-holidays/",
            {"date": "2025-11-11", "name": "Veterans Day"},
            format="json",
        )
        hid = r.json()["id"]
        resp = client.delete(f"/api/v1/hr/public-holidays/{hid}/")
        assert resp.status_code == 204


# ---------------------------------------------------------------------------
# API: Leave Requests
# ---------------------------------------------------------------------------

@pytest.mark.django_db
class TestLeaveRequestAPI:
    def test_create_leave_request(self, alice, alice_leave_mgr, tenant_acme, employee, leave_type_annual, balance_annual):
        client = _with_tenant(_api(alice), tenant_acme)
        resp = client.post(
            "/api/v1/hr/leave-requests/",
            {
                "employee_id": employee.pk,
                "leave_type_id": leave_type_annual.pk,
                "from_date": "2025-09-01",
                "to_date": "2025-09-05",
                "reason": "Holiday",
            },
            format="json",
        )
        assert resp.status_code == 201
        data = resp.json()
        assert data["status"] == "draft"
        assert int(data["days_requested"]) == 5

    def test_submit_request(self, alice, alice_leave_mgr, tenant_acme, employee, leave_type_annual, balance_annual):
        client = _with_tenant(_api(alice), tenant_acme)
        r = client.post(
            "/api/v1/hr/leave-requests/",
            {"employee_id": employee.pk, "leave_type_id": leave_type_annual.pk, "from_date": "2025-09-08", "to_date": "2025-09-12"},
            format="json",
        )
        pid = r.json()["public_id"]
        resp = client.post(f"/api/v1/hr/leave-requests/{pid}/submit/")
        assert resp.status_code == 200
        assert resp.json()["status"] == "pending"

    def test_approve_request(self, alice, alice_leave_mgr, tenant_acme, employee, leave_type_annual, balance_annual):
        client = _with_tenant(_api(alice), tenant_acme)
        r = client.post(
            "/api/v1/hr/leave-requests/",
            {"employee_id": employee.pk, "leave_type_id": leave_type_annual.pk, "from_date": "2025-10-06", "to_date": "2025-10-10"},
            format="json",
        )
        pid = r.json()["public_id"]
        client.post(f"/api/v1/hr/leave-requests/{pid}/submit/")
        resp = client.post(f"/api/v1/hr/leave-requests/{pid}/approve/", {"note": "Approved!"}, format="json")
        assert resp.status_code == 200
        assert resp.json()["status"] == "approved"

    def test_reject_request(self, alice, alice_leave_mgr, tenant_acme, employee, leave_type_annual, balance_annual):
        client = _with_tenant(_api(alice), tenant_acme)
        r = client.post(
            "/api/v1/hr/leave-requests/",
            {"employee_id": employee.pk, "leave_type_id": leave_type_annual.pk, "from_date": "2025-11-03", "to_date": "2025-11-07"},
            format="json",
        )
        pid = r.json()["public_id"]
        client.post(f"/api/v1/hr/leave-requests/{pid}/submit/")
        resp = client.post(f"/api/v1/hr/leave-requests/{pid}/reject/", {"note": "Not allowed"}, format="json")
        assert resp.status_code == 200
        assert resp.json()["status"] == "rejected"

    def test_reject_without_note_fails(self, alice, alice_leave_mgr, tenant_acme, employee, leave_type_annual, balance_annual):
        client = _with_tenant(_api(alice), tenant_acme)
        r = client.post(
            "/api/v1/hr/leave-requests/",
            {"employee_id": employee.pk, "leave_type_id": leave_type_annual.pk, "from_date": "2025-12-01", "to_date": "2025-12-05"},
            format="json",
        )
        pid = r.json()["public_id"]
        client.post(f"/api/v1/hr/leave-requests/{pid}/submit/")
        resp = client.post(f"/api/v1/hr/leave-requests/{pid}/reject/", {"note": ""}, format="json")
        assert resp.status_code == 400

    def test_cancel_request(self, alice, alice_leave_mgr, tenant_acme, employee, leave_type_annual, balance_annual):
        client = _with_tenant(_api(alice), tenant_acme)
        r = client.post(
            "/api/v1/hr/leave-requests/",
            {"employee_id": employee.pk, "leave_type_id": leave_type_annual.pk, "from_date": "2026-01-05", "to_date": "2026-01-09"},
            format="json",
        )
        pid = r.json()["public_id"]
        resp = client.post(f"/api/v1/hr/leave-requests/{pid}/cancel/")
        assert resp.status_code == 200
        assert resp.json()["status"] == "cancelled"

    def test_list_filter_by_status(self, alice, alice_leave_mgr, tenant_acme, employee, leave_type_annual, balance_annual):
        client = _with_tenant(_api(alice), tenant_acme)
        r = client.post(
            "/api/v1/hr/leave-requests/",
            {"employee_id": employee.pk, "leave_type_id": leave_type_annual.pk, "from_date": "2026-02-02", "to_date": "2026-02-06"},
            format="json",
        )
        client.post(f"/api/v1/hr/leave-requests/{r.json()['public_id']}/submit/")
        resp = client.get("/api/v1/hr/leave-requests/?status=pending")
        assert resp.status_code == 200
        assert all(item["status"] == "pending" for item in resp.json())


# ---------------------------------------------------------------------------
# API: Leave Balances
# ---------------------------------------------------------------------------

@pytest.mark.django_db
class TestLeaveBalanceAPI:
    def test_allocate(self, alice, alice_leave_mgr, tenant_acme, employee, leave_type_annual):
        client = _with_tenant(_api(alice), tenant_acme)
        resp = client.post(
            "/api/v1/hr/leave-balances/allocate/",
            {
                "employee_id": employee.pk,
                "leave_type_id": leave_type_annual.pk,
                "year": 2025,
                "days": 20,
            },
            format="json",
        )
        assert resp.status_code == 201
        assert resp.json()["allocated"] == "20"

    def test_list_balances(self, alice, alice_leave_mgr, tenant_acme, employee, leave_type_annual, balance_annual):
        client = _with_tenant(_api(alice), tenant_acme)
        resp = client.get(f"/api/v1/hr/leave-balances/?employee={employee.pk}")
        assert resp.status_code == 200
        assert len(resp.json()) == 1

    def test_allocate_requires_manage(self, alice, alice_leave_req, tenant_acme, employee, leave_type_annual):
        client = _with_tenant(_api(alice), tenant_acme)
        resp = client.post(
            "/api/v1/hr/leave-balances/allocate/",
            {"employee_id": employee.pk, "leave_type_id": leave_type_annual.pk, "year": 2025, "days": 5},
            format="json",
        )
        assert resp.status_code == 403


# ---------------------------------------------------------------------------
# API: Leave Calendar
# ---------------------------------------------------------------------------

@pytest.mark.django_db
class TestLeaveCalendarAPI:
    def test_calendar_approved_only(self, alice, alice_leave_mgr, tenant_acme, employee, leave_type_annual, balance_annual):
        client = _with_tenant(_api(alice), tenant_acme)
        # Create + submit + approve a request
        r = client.post(
            "/api/v1/hr/leave-requests/",
            {"employee_id": employee.pk, "leave_type_id": leave_type_annual.pk, "from_date": "2025-06-02", "to_date": "2025-06-06"},
            format="json",
        )
        pid = r.json()["public_id"]
        client.post(f"/api/v1/hr/leave-requests/{pid}/submit/")
        client.post(f"/api/v1/hr/leave-requests/{pid}/approve/", {"note": "OK"}, format="json")

        resp = client.get("/api/v1/hr/leave-calendar/?from=2025-06-01&to=2025-06-30")
        assert resp.status_code == 200
        data = resp.json()
        assert len(data) == 1
        assert data[0]["status"] == "approved"

    def test_calendar_missing_params(self, alice, alice_leave_req, tenant_acme):
        client = _with_tenant(_api(alice), tenant_acme)
        resp = client.get("/api/v1/hr/leave-calendar/")
        assert resp.status_code == 400
