"""S-E (Phase 8) — HR Testing gap coverage.

Covers the critical items from the Phase 8 gap analysis:

8.2 — Feature gating: shift_management and sensitive_data return 403 when disabled
8.3 — Sensitive data masking: national_id / account_no hidden without proper perm
8.5 — Leave state machine edge cases:
        - double approve guard
        - date overlap guard (submit raises when overlapping leave exists)
        - negative balance guard (submit raises when balance insufficient)
"""

from __future__ import annotations

from datetime import date
from decimal import Decimal

import pytest

from simorgh.apps.hr_core.models import (
    Employee,
    LeaveBalance,
    LeaveRequest,
    LeaveRequestStatus,
    LeaveType,
    ShiftDefinition,
)
from simorgh.apps.hr_core import services
from simorgh.apps.hr_core.permissions import (
    PERM_ATTENDANCE_MANAGE,
    PERM_EMPLOYEE_CREATE,
    PERM_EMPLOYEE_SENSITIVE_VIEW,
    PERM_EMPLOYEE_VIEW,
    PERM_LEAVE_APPROVE,
    PERM_LEAVE_MANAGE,
    PERM_LEAVE_REQUEST,
    PERM_SHIFT_MANAGE,
)
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


# ---------------------------------------------------------------------------
# Shared fixtures
# ---------------------------------------------------------------------------

@pytest.fixture
def perms_synced(db):
    sync_registry_to_db()
    return {p.codename: p for p in Permission.objects.all()}


@pytest.fixture
def feature_override_factory(db):
    """Helper factory to toggle a feature for a tenant."""
    from simorgh.apps.modules.models import TenantFeatureOverride

    def _create(tenant, acme_tree, feature_key, enabled):
        return TenantFeatureOverride.objects.create(
            tenant=tenant,
            organization_node=acme_tree["root"],
            feature_key=feature_key,
            name=feature_key.replace(".", "_"),
            enabled=enabled,
        )

    return _create


@pytest.fixture
def role_full_hr(tenant_acme, perms_synced) -> Role:
    """Role with all HR permissions."""
    role = Role.objects.create(tenant=tenant_acme, code="hr_full8", name="HR Full Phase8")
    role.permissions.set(perms_synced.values())
    return role


@pytest.fixture
def alice_full_hr(alice, tenant_acme, acme_tree, role_full_hr) -> Membership:
    m = Membership.objects.create(
        tenant=tenant_acme,
        organization_node=acme_tree["root"],
        role=role_full_hr,
    )
    m.users.add(alice)
    return m


@pytest.fixture
def job_title(tenant_acme) -> "JobTitle":
    from simorgh.apps.hr_core.models import JobTitle
    return JobTitle.objects.create(tenant=tenant_acme, title="Engineer P8", level=3)


@pytest.fixture
def employee(tenant_acme, acme_tree, job_title) -> Employee:
    return services.create_employee(
        tenant_id=tenant_acme.pk,
        organization_node_id=acme_tree["root"].pk,
        first_name="Ali",
        last_name="Norouzi",
        hire_date=date(2023, 1, 1),
        employment_type="full_time",
        national_id="9876543210",
        work_email="ali.norouzi@acme.example",
        job_title_id=job_title.pk,
    )


@pytest.fixture
def leave_type(tenant_acme) -> LeaveType:
    return services.create_leave_type(
        tenant_id=tenant_acme.pk,
        name="Annual Leave P8",
        code="ANNUAL_P8",
        max_days_per_year=21,
    )


# ===========================================================================
# 8.2 — Feature Gating Tests
# ===========================================================================

@pytest.mark.django_db
class TestFeatureGating:
    """Verify that feature-gated endpoints return 403 when the feature is disabled."""

    def test_shift_list_requires_feature_enabled(
        self, tenant_acme, alice, alice_full_hr, acme_tree, feature_override_factory
    ):
        """GET /hr/shifts/ → 403 when hr.shift_management is not enabled."""
        # Explicitly disable the feature (default is False for paid features)
        feature_override_factory(tenant_acme, acme_tree, "hr.shift_management", False)

        client = _with_tenant(_api(alice), tenant_acme)
        resp = client.get("/api/v1/hr/shifts/")
        assert resp.status_code == 403
        assert resp.data.get("feature") == "hr.shift_management"

    def test_shift_list_allowed_when_feature_enabled(
        self, tenant_acme, alice, alice_full_hr, acme_tree, feature_override_factory
    ):
        """GET /hr/shifts/ → 200 when hr.shift_management is enabled."""
        feature_override_factory(tenant_acme, acme_tree, "hr.shift_management", True)

        client = _with_tenant(_api(alice), tenant_acme)
        resp = client.get("/api/v1/hr/shifts/")
        assert resp.status_code == 200

    def test_bank_account_list_requires_sensitive_data_feature(
        self, tenant_acme, alice, alice_full_hr, acme_tree, employee, feature_override_factory
    ):
        """GET /hr/employees/{id}/bank-accounts/ → 403 when hr.sensitive_data disabled."""
        feature_override_factory(tenant_acme, acme_tree, "hr.sensitive_data", False)

        client = _with_tenant(_api(alice), tenant_acme)
        resp = client.get(f"/api/v1/hr/employees/{employee.public_id}/bank-accounts/")
        assert resp.status_code == 403
        assert resp.data.get("feature") == "hr.sensitive_data"

    def test_bank_account_add_requires_sensitive_data_feature(
        self, tenant_acme, alice, alice_full_hr, acme_tree, employee, feature_override_factory
    ):
        """POST /hr/employees/{id}/bank-accounts/ → 403 when hr.sensitive_data disabled."""
        feature_override_factory(tenant_acme, acme_tree, "hr.sensitive_data", False)

        client = _with_tenant(_api(alice), tenant_acme)
        resp = client.post(
            f"/api/v1/hr/employees/{employee.public_id}/bank-accounts/",
            data={"bank_name": "Mellat", "account_no": "123456"},
            format="json",
        )
        assert resp.status_code == 403


# ===========================================================================
# 8.3 — Sensitive Data Masking Tests
# ===========================================================================

@pytest.mark.django_db
class TestSensitiveDataMasking:
    """Ensure sensitive fields are masked in the API when the caller lacks the perm."""

    @pytest.fixture
    def role_viewer_only(self, tenant_acme, perms_synced) -> Role:
        """Role with view-only: can see employees but NOT sensitive data."""
        role = Role.objects.create(tenant=tenant_acme, code="hr_viewer8", name="HR Viewer P8")
        role.permissions.set([perms_synced[PERM_EMPLOYEE_VIEW]])
        return role

    @pytest.fixture
    def alice_viewer(self, alice, tenant_acme, acme_tree, role_viewer_only) -> Membership:
        m = Membership.objects.create(
            tenant=tenant_acme,
            organization_node=acme_tree["root"],
            role=role_viewer_only,
        )
        m.users.add(alice)
        return m

    def test_national_id_masked_without_sensitive_perm(
        self, tenant_acme, alice, alice_viewer, employee
    ):
        """national_id should be '****' for users without sensitive.view."""
        client = _with_tenant(_api(alice), tenant_acme)
        resp = client.get(f"/api/v1/hr/employees/{employee.public_id}/")
        assert resp.status_code == 200
        assert resp.data["national_id"] == "****"

    def test_national_id_revealed_with_sensitive_perm(
        self, tenant_acme, alice, alice_full_hr, employee
    ):
        """national_id should be decrypted for users with sensitive.view perm."""
        client = _with_tenant(_api(alice), tenant_acme)
        resp = client.get(f"/api/v1/hr/employees/{employee.public_id}/")
        assert resp.status_code == 200
        # The employee fixture was created with national_id="9876543210"
        assert resp.data["national_id"] == "9876543210"

    def test_personal_email_masked_without_sensitive_perm(
        self, tenant_acme, alice, alice_viewer, employee
    ):
        """personal_email should be masked for users without sensitive.view."""
        # Add a personal email first (update the employee)
        services.update_employee(employee, personal_email="ali.private@gmail.com")

        client = _with_tenant(_api(alice), tenant_acme)
        resp = client.get(f"/api/v1/hr/employees/{employee.public_id}/")
        assert resp.status_code == 200
        # Without sensitive perm, personal_email is masked
        assert resp.data.get("personal_email") in ("****", "")

    def test_national_id_absent_if_not_set_and_no_perm(
        self, tenant_acme, alice, alice_viewer, acme_tree, job_title
    ):
        """national_id should be '' (not '****') when not set and no perm."""
        emp_no_nid = services.create_employee(
            tenant_id=tenant_acme.pk,
            organization_node_id=acme_tree["root"].pk,
            first_name="Nour",
            last_name="Sadeghi",
            hire_date=date(2024, 1, 1),
            employment_type="full_time",
            # no national_id
        )
        client = _with_tenant(_api(alice), tenant_acme)
        resp = client.get(f"/api/v1/hr/employees/{emp_no_nid.public_id}/")
        assert resp.status_code == 200
        assert resp.data["national_id"] == ""


# ===========================================================================
# 8.5 — Leave State Machine Edge Cases
# ===========================================================================

@pytest.mark.django_db
class TestLeaveStateMachineEdgeCases:
    """
    Edge cases for the leave state machine:
    - Double approve guard
    - Date overlap guard
    - Negative balance guard
    """

    @pytest.fixture
    def alice_leave_mgr(self, alice, tenant_acme, acme_tree, perms_synced):
        role = Role.objects.create(tenant=tenant_acme, code="lv_mgr8", name="LM P8")
        role.permissions.set([
            perms_synced[PERM_LEAVE_REQUEST],
            perms_synced[PERM_LEAVE_APPROVE],
            perms_synced[PERM_LEAVE_MANAGE],
        ])
        m = Membership.objects.create(
            tenant=tenant_acme,
            organization_node=acme_tree["root"],
            role=role,
        )
        m.users.add(alice)
        return m

    @pytest.fixture
    def approved_leave_request(self, tenant_acme, employee, leave_type, alice, alice_leave_mgr):
        """A request that has already been submitted and approved."""
        req = services.create_leave_request(
            tenant_id=tenant_acme.pk,
            employee=employee,
            leave_type=leave_type,
            from_date=date(2025, 8, 1),
            to_date=date(2025, 8, 5),
        )
        req = services.submit_leave_request(req)
        req = services.approve_leave_request(req, reviewed_by=alice, note="Good")
        return req

    # ── 8.5a Double approve ──────────────────────────────────────────────────

    def test_double_approve_raises(
        self, tenant_acme, employee, leave_type, alice, alice_leave_mgr
    ):
        """Approving an already-approved request must raise ValueError."""
        req = services.create_leave_request(
            tenant_id=tenant_acme.pk,
            employee=employee,
            leave_type=leave_type,
            from_date=date(2025, 9, 1),
            to_date=date(2025, 9, 3),
        )
        req = services.submit_leave_request(req)
        req = services.approve_leave_request(req, reviewed_by=alice, note="OK")

        with pytest.raises(ValueError, match="Only pending requests can be approved"):
            services.approve_leave_request(req, reviewed_by=alice, note="Again")

    def test_double_cancel_raises(self, tenant_acme, employee, leave_type, alice_leave_mgr):
        """Cancelling an already-cancelled request must raise ValueError."""
        req = services.create_leave_request(
            tenant_id=tenant_acme.pk,
            employee=employee,
            leave_type=leave_type,
            from_date=date(2025, 10, 6),
            to_date=date(2025, 10, 7),
        )
        services.submit_leave_request(req)
        services.cancel_leave_request(req)

        with pytest.raises(ValueError, match="already cancelled"):
            services.cancel_leave_request(req)

    # ── 8.5b Date overlap guard ──────────────────────────────────────────────

    def test_submit_raises_on_date_overlap(
        self, tenant_acme, employee, leave_type, alice, alice_leave_mgr, approved_leave_request
    ):
        """Submitting a request whose dates overlap an approved request must raise."""
        # Overlapping dates with the approved request (2025-08-01 to 2025-08-05)
        overlapping = services.create_leave_request(
            tenant_id=tenant_acme.pk,
            employee=employee,
            leave_type=leave_type,
            from_date=date(2025, 8, 3),  # overlaps
            to_date=date(2025, 8, 7),
        )
        with pytest.raises(ValueError, match="already have a leave request"):
            services.submit_leave_request(overlapping)

    def test_submit_ok_for_non_overlapping_dates(
        self, tenant_acme, employee, leave_type, alice, alice_leave_mgr, approved_leave_request
    ):
        """Submitting a request with non-overlapping dates should succeed."""
        non_overlapping = services.create_leave_request(
            tenant_id=tenant_acme.pk,
            employee=employee,
            leave_type=leave_type,
            from_date=date(2025, 8, 11),  # after 2025-08-05
            to_date=date(2025, 8, 12),
        )
        result = services.submit_leave_request(non_overlapping)
        assert result.status in (LeaveRequestStatus.PENDING, LeaveRequestStatus.APPROVED)

    # ── 8.5c Negative balance guard ─────────────────────────────────────────

    def test_submit_raises_on_insufficient_balance(
        self, tenant_acme, employee, leave_type, alice_leave_mgr
    ):
        """Submitting when allocated balance is 0 must raise ValueError."""
        # Allocate zero days
        LeaveBalance.objects.create(
            tenant_id=tenant_acme.pk,
            organization_node_id=employee.organization_node_id,
            employee=employee,
            leave_type=leave_type,
            year=2025,
            allocated=Decimal("0"),
            used=Decimal("0"),
            pending=Decimal("0"),
        )
        req = services.create_leave_request(
            tenant_id=tenant_acme.pk,
            employee=employee,
            leave_type=leave_type,
            from_date=date(2025, 11, 3),
            to_date=date(2025, 11, 5),
        )
        with pytest.raises(ValueError, match="Insufficient balance"):
            services.submit_leave_request(req)

    def test_submit_succeeds_when_balance_sufficient(
        self, tenant_acme, employee, leave_type, alice_leave_mgr
    ):
        """Submit succeeds when there are enough allocated days."""
        LeaveBalance.objects.create(
            tenant_id=tenant_acme.pk,
            organization_node_id=employee.organization_node_id,
            employee=employee,
            leave_type=leave_type,
            year=2025,
            allocated=Decimal("21"),
            used=Decimal("0"),
            pending=Decimal("0"),
        )
        req = services.create_leave_request(
            tenant_id=tenant_acme.pk,
            employee=employee,
            leave_type=leave_type,
            from_date=date(2025, 12, 1),
            to_date=date(2025, 12, 3),
        )
        result = services.submit_leave_request(req)
        assert result.status in (LeaveRequestStatus.PENDING, LeaveRequestStatus.APPROVED)
        # Pending days should have been reserved
        balance = LeaveBalance.objects.get(
            employee=employee, leave_type=leave_type, year=2025
        )
        assert balance.pending >= Decimal("1")

    def test_cancel_approved_restores_used_balance(
        self, tenant_acme, employee, leave_type, alice, alice_leave_mgr
    ):
        """Cancelling an approved request returns used days to the balance."""
        LeaveBalance.objects.create(
            tenant_id=tenant_acme.pk,
            organization_node_id=employee.organization_node_id,
            employee=employee,
            leave_type=leave_type,
            year=2026,
            allocated=Decimal("10"),
            used=Decimal("0"),
            pending=Decimal("0"),
        )
        req = services.create_leave_request(
            tenant_id=tenant_acme.pk,
            employee=employee,
            leave_type=leave_type,
            from_date=date(2026, 3, 2),
            to_date=date(2026, 3, 4),
        )
        req = services.submit_leave_request(req)
        req = services.approve_leave_request(req, reviewed_by=alice, note="OK")
        assert req.status == LeaveRequestStatus.APPROVED

        services.cancel_leave_request(req)

        balance = LeaveBalance.objects.get(
            employee=employee, leave_type=leave_type, year=2026
        )
        # used days should be restored to 0
        assert balance.used == Decimal("0")

    def test_reject_without_note_raises(
        self, tenant_acme, employee, leave_type, alice, alice_leave_mgr
    ):
        """Rejecting a pending request without a note must raise ValueError."""
        req = services.create_leave_request(
            tenant_id=tenant_acme.pk,
            employee=employee,
            leave_type=leave_type,
            from_date=date(2025, 7, 7),
            to_date=date(2025, 7, 8),
        )
        req = services.submit_leave_request(req)

        with pytest.raises(ValueError, match="note is required"):
            services.reject_leave_request(req, reviewed_by=alice, note="")
