"""S4.1 — HR Core / Employee Master backend tests.

Covers:
- JobTitle model / services (create, update, delete, unique constraint)
- Employee model / services (create, update, terminate)
- Employee selectors (list, by_public_id, direct_reports, org_chart)
- EmployeeDocument services (add, verify, delete)
- EmployeeBankAccount services (add, update, delete, default logic)
- Encryption round-trip (national_id, account_no, iban)
- Employee number generation (prefix + digits from settings)
- Sensitive field masking in API serializers
- Permission gates (view, create, update, terminate, sensitive.view)
- API endpoints (job-titles, employees, terminate, direct-reports, org-chart,
  documents, bank-accounts)
- Registered permissions (all 7 codenames present in DB after sync)
"""

from __future__ import annotations

import json
from datetime import date

import pytest

from simorgh.apps.hr.models import Employee, EmployeeBankAccount, EmployeeDocument, JobTitle
from simorgh.apps.hr import services, selectors
from simorgh.apps.hr.services import encrypt_field, decrypt_field
from simorgh.apps.hr.permissions import (
    PERM_EMPLOYEE_VIEW,
    PERM_EMPLOYEE_CREATE,
    PERM_EMPLOYEE_UPDATE,
    PERM_EMPLOYEE_TERMINATE,
    PERM_EMPLOYEE_SENSITIVE_VIEW,
    PERM_DOCUMENT_MANAGE,
    PERM_JOB_TITLE_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


# ---------------------------------------------------------------------------
# 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_hr_admin(tenant_acme, perms_synced) -> Role:
    role = Role.objects.create(tenant=tenant_acme, code="hr_admin", name="HR Admin")
    role.permissions.set(perms_synced.values())
    return role


@pytest.fixture
def role_hr_viewer(tenant_acme, perms_synced) -> Role:
    role = Role.objects.create(tenant=tenant_acme, code="hr_viewer", name="HR Viewer")
    role.permissions.set([perms_synced[PERM_EMPLOYEE_VIEW]])
    return role


@pytest.fixture
def alice_hr_admin(alice, tenant_acme, acme_tree, role_hr_admin) -> Membership:
    m = Membership.objects.create(
        tenant=tenant_acme,
        organization_node=acme_tree["root"],
        role=role_hr_admin,
    )
    m.users.add(alice)
    return m


@pytest.fixture
def alice_hr_viewer(alice, tenant_acme, acme_tree, role_hr_viewer) -> Membership:
    m = Membership.objects.create(
        tenant=tenant_acme,
        organization_node=acme_tree["root"],
        role=role_hr_viewer,
    )
    m.users.add(alice)
    return m


@pytest.fixture
def job_title(tenant_acme) -> JobTitle:
    return JobTitle.objects.create(tenant=tenant_acme, title="Engineer", level=3)


@pytest.fixture
def hr_sensitive_data_enabled(db, tenant_acme, acme_tree):
    """Enable hr.sensitive_data feature for tenant_acme via TenantFeatureOverride."""
    from simorgh.apps.modules.models import TenantFeatureOverride
    return TenantFeatureOverride.objects.create(
        tenant=tenant_acme,
        organization_node=acme_tree["root"],
        feature_key="hr.sensitive_data",
        name="sensitive_data",
        enabled=True,
    )


@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="Reza",
        last_name="Ahmadi",
        hire_date=date(2022, 1, 1),
        employment_type="full_time",
        national_id="1234567890",
        work_email="reza@acme.example",
        job_title_id=job_title.pk,
    )


# ---------------------------------------------------------------------------
# Encryption
# ---------------------------------------------------------------------------


@pytest.mark.django_db
class TestEncryption:
    def test_round_trip_ascii(self):
        ct = encrypt_field("Hello World")
        assert decrypt_field(ct) == "Hello World"

    def test_round_trip_unicode(self):
        ct = encrypt_field("رضا احمدی")
        assert decrypt_field(ct) == "رضا احمدی"

    def test_empty_string(self):
        ct = encrypt_field("")
        assert decrypt_field(ct) == ""

    def test_ciphertext_differs_from_plaintext(self):
        plaintext = "secret123"
        assert encrypt_field(plaintext) != plaintext


# ---------------------------------------------------------------------------
# JobTitle services
# ---------------------------------------------------------------------------


@pytest.mark.django_db
class TestJobTitleServices:
    def test_create_job_title(self, tenant_acme):
        jt = services.create_job_title(
            tenant_id=tenant_acme.pk,
            title="Manager",
            level=5,
        )
        assert jt.pk is not None
        assert jt.title == "Manager"
        assert jt.level == 5
        assert jt.tenant_id == tenant_acme.pk

    def test_create_duplicate_title_raises(self, tenant_acme):
        services.create_job_title(tenant_id=tenant_acme.pk, title="Analyst", level=2)
        with pytest.raises(Exception):
            services.create_job_title(tenant_id=tenant_acme.pk, title="Analyst", level=2)

    def test_update_job_title(self, tenant_acme, job_title):
        updated = services.update_job_title(job_title, title="Senior Engineer", level=4)
        assert updated.title == "Senior Engineer"
        assert updated.level == 4

    def test_delete_job_title(self, tenant_acme, job_title):
        pk = job_title.pk
        services.delete_job_title(job_title)
        assert not JobTitle.objects.filter(pk=pk).exists()

    def test_selectors_job_titles_for_tenant(self, tenant_acme, tenant_globex, job_title):
        JobTitle.objects.create(tenant=tenant_globex, title="CEO", level=10)
        qs = selectors.job_titles_for_tenant(tenant_acme.pk)
        titles = list(qs)
        assert job_title in titles
        assert all(jt.tenant_id == tenant_acme.pk for jt in titles)

    def test_selector_job_title_by_id_not_found(self, tenant_acme):
        result = selectors.job_title_by_id(tenant_acme.pk, 99999999)
        assert result is None


# ---------------------------------------------------------------------------
# Employee services
# ---------------------------------------------------------------------------


@pytest.mark.django_db
class TestEmployeeServices:
    def test_create_employee_minimal(self, tenant_acme, acme_tree):
        emp = services.create_employee(
            tenant_id=tenant_acme.pk,
            organization_node_id=acme_tree["root"].pk,
            first_name="Ali",
            last_name="Rezaei",
            hire_date=date(2023, 6, 1),
        )
        assert emp.pk is not None
        assert emp.first_name == "Ali"
        assert emp.last_name == "Rezaei"
        assert emp.status == "active"
        assert emp.tenant_id == tenant_acme.pk
        assert emp.employee_no.startswith("EMP")

    def test_employee_no_unique_per_tenant(self, tenant_acme, acme_tree):
        e1 = services.create_employee(
            tenant_id=tenant_acme.pk,
            organization_node_id=acme_tree["root"].pk,
            first_name="A",
            last_name="B",
            hire_date=date(2023, 1, 1),
        )
        e2 = services.create_employee(
            tenant_id=tenant_acme.pk,
            organization_node_id=acme_tree["root"].pk,
            first_name="C",
            last_name="D",
            hire_date=date(2023, 1, 2),
        )
        assert e1.employee_no != e2.employee_no

    def test_national_id_encrypted_at_rest(self, employee):
        raw = Employee.objects.filter(pk=employee.pk).values("national_id").get()["national_id"]
        # stored value must NOT be the plaintext
        assert raw != "1234567890"

    def test_update_employee_fields(self, employee):
        updated = services.update_employee(
            employee,
            first_name="Mohammad",
            last_name="Hosseini",
            mobile="+989121234567",
        )
        assert updated.first_name == "Mohammad"
        assert updated.last_name == "Hosseini"
        assert updated.mobile == "+989121234567"

    def test_terminate_employee(self, employee):
        terminated = services.terminate_employee(
            employee,
            termination_date=date(2024, 12, 31),
            reason="Resignation",
        )
        assert terminated.status == "terminated"
        assert terminated.termination_date == date(2024, 12, 31)

    def test_terminate_already_terminated_raises(self, employee):
        services.terminate_employee(
            employee, termination_date=date(2024, 1, 1), reason="Quit"
        )
        employee.refresh_from_db()
        with pytest.raises(ValueError):
            services.terminate_employee(
                employee, termination_date=date(2024, 6, 1), reason="Again"
            )

    def test_public_id_generated(self, employee):
        assert employee.public_id is not None


# ---------------------------------------------------------------------------
# Employee selectors
# ---------------------------------------------------------------------------


@pytest.mark.django_db
class TestEmployeeSelectors:
    def test_employees_for_tenant_scoped(self, tenant_acme, tenant_globex, acme_tree, employee):
        globex_node = __import__(
            "simorgh.apps.organizations.services", fromlist=["create_node"]
        ).create_node(tenant_id=tenant_globex.pk, name="Globex HQ")
        services.create_employee(
            tenant_id=tenant_globex.pk,
            organization_node_id=globex_node.pk,
            first_name="Foreign",
            last_name="Worker",
            hire_date=date(2023, 1, 1),
        )
        qs = selectors.employees_for_tenant(tenant_acme.pk)
        assert employee in qs
        assert all(e.tenant_id == tenant_acme.pk for e in qs)

    def test_employees_filter_by_status(self, tenant_acme, acme_tree, employee):
        services.terminate_employee(employee, termination_date=date(2024, 1, 1), reason="Q")
        employee.refresh_from_db()
        active = selectors.employees_for_tenant(tenant_acme.pk, status="active")
        terminated = selectors.employees_for_tenant(tenant_acme.pk, status="terminated")
        assert employee not in active
        assert employee in terminated

    def test_employee_by_public_id(self, tenant_acme, employee):
        found = selectors.employee_by_public_id(tenant_acme.pk, str(employee.public_id))
        assert found.pk == employee.pk

    def test_employee_by_public_id_not_found(self, tenant_acme):
        import uuid
        result = selectors.employee_by_public_id(tenant_acme.pk, str(uuid.uuid4()))
        assert result is None

    def test_direct_reports(self, tenant_acme, acme_tree, employee):
        report = services.create_employee(
            tenant_id=tenant_acme.pk,
            organization_node_id=acme_tree["root"].pk,
            first_name="Bob",
            last_name="Smith",
            hire_date=date(2023, 3, 1),
            manager_id=employee.pk,
        )
        reports = list(selectors.direct_reports(employee))
        assert report in reports
        assert len(reports) == 1

    def test_org_chart_tree_returns_list(self, tenant_acme, employee):
        tree = selectors.org_chart_tree(tenant_acme.pk)
        assert isinstance(tree, list)


# ---------------------------------------------------------------------------
# Document services
# ---------------------------------------------------------------------------


@pytest.mark.django_db
class TestDocumentServices:
    def test_add_document(self, employee):
        doc = services.add_employee_document(
            employee=employee,
            doc_type="national_id",
            dms_file_id="00000000-0000-0000-0000-000000000001",
            title="National ID Card",
            expiry_date=None,
        )
        assert doc.pk is not None
        assert doc.is_verified is False
        assert doc.employee_id == employee.pk

    def test_verify_document(self, employee):
        doc = services.add_employee_document(
            employee=employee,
            doc_type="national_id",
            dms_file_id="00000000-0000-0000-0000-000000000002",
            title="Passport",
        )
        verified = services.verify_employee_document(doc)
        assert verified.is_verified is True

    def test_delete_document(self, employee):
        doc = services.add_employee_document(
            employee=employee,
            doc_type="contract",
            dms_file_id="00000000-0000-0000-0000-000000000003",
            title="Contract",
        )
        pk = doc.pk
        services.delete_employee_document(doc)
        assert not EmployeeDocument.objects.filter(pk=pk).exists()

    def test_documents_for_employee_selector(self, employee):
        services.add_employee_document(
            employee=employee,
            doc_type="contract",
            dms_file_id="00000000-0000-0000-0000-000000000004",
            title="Contract A",
        )
        docs = selectors.documents_for_employee(employee)
        assert docs.count() == 1


# ---------------------------------------------------------------------------
# BankAccount services
# ---------------------------------------------------------------------------


@pytest.mark.django_db
class TestBankAccountServices:
    def test_add_bank_account_encrypts(self, employee):
        acct = services.add_bank_account(
            employee=employee,
            bank_name="Mellat",
            account_no="1234567890",
            iban="IR0601234567890000000000",
            is_default=True,
        )
        raw = (
            EmployeeBankAccount.objects.filter(pk=acct.pk)
            .values("account_no", "iban")
            .get()
        )
        assert raw["account_no"] != "1234567890"
        assert raw["iban"] != "IR0601234567890000000000"

    def test_only_one_default_per_employee(self, employee):
        acct1 = services.add_bank_account(
            employee=employee,
            bank_name="Bank A",
            account_no="111",
            iban="IR01",
            is_default=True,
        )
        acct2 = services.add_bank_account(
            employee=employee,
            bank_name="Bank B",
            account_no="222",
            iban="IR02",
            is_default=True,
        )
        acct1.refresh_from_db()
        assert acct1.is_default is False
        assert acct2.is_default is True

    def test_delete_bank_account(self, employee):
        acct = services.add_bank_account(
            employee=employee,
            bank_name="Bank C",
            account_no="333",
            iban="IR03",
            is_default=False,
        )
        pk = acct.pk
        services.delete_bank_account(acct)
        assert not EmployeeBankAccount.objects.filter(pk=pk).exists()

    def test_bank_accounts_selector(self, employee):
        services.add_bank_account(
            employee=employee,
            bank_name="Bank D",
            account_no="444",
            iban="IR04",
        )
        accounts = selectors.bank_accounts_for_employee(employee)
        assert accounts.count() == 1


# ---------------------------------------------------------------------------
# Permissions
# ---------------------------------------------------------------------------


@pytest.mark.django_db
class TestPermissionsRegistered:
    def test_all_hr_permissions_registered(self, db):
        sync_registry_to_db()
        codenames = [p.codename for p in Permission.objects.filter(codename__startswith="hr.")]
        for perm in [
            PERM_EMPLOYEE_VIEW,
            PERM_EMPLOYEE_CREATE,
            PERM_EMPLOYEE_UPDATE,
            PERM_EMPLOYEE_TERMINATE,
            PERM_EMPLOYEE_SENSITIVE_VIEW,
            PERM_DOCUMENT_MANAGE,
            PERM_JOB_TITLE_MANAGE,
        ]:
            assert perm in codenames, f"Missing permission: {perm}"


# ---------------------------------------------------------------------------
# API — job titles
# ---------------------------------------------------------------------------


@pytest.mark.django_db
class TestJobTitleAPI:
    BASE = "/api/v1/hr/job-titles/"

    def test_list_requires_auth(self, tenant_acme):
        resp = _api().get(self.BASE, HTTP_X_TENANT=tenant_acme.slug)
        assert resp.status_code == 401

    def test_list_job_titles(self, alice, tenant_acme, acme_tree, job_title, alice_hr_admin):
        resp = _api(alice).get(self.BASE, HTTP_X_TENANT=tenant_acme.slug)
        assert resp.status_code == 200
        data = resp.json()
        assert isinstance(data, list)
        assert any(jt["title"] == "Engineer" for jt in data)

    def test_create_job_title(self, alice, tenant_acme, acme_tree, alice_hr_admin, perms_synced):
        payload = {"title": "Director", "level": 8}
        resp = _api(alice).post(
            self.BASE, data=payload, format="json", HTTP_X_TENANT=tenant_acme.slug
        )
        assert resp.status_code == 201
        assert resp.json()["title"] == "Director"

    def test_create_job_title_permission_denied(self, alice, tenant_acme, acme_tree, alice_hr_viewer):
        payload = {"title": "VP", "level": 9}
        resp = _api(alice).post(
            self.BASE, data=payload, format="json", HTTP_X_TENANT=tenant_acme.slug
        )
        assert resp.status_code == 403

    def test_update_job_title(self, alice, tenant_acme, acme_tree, job_title, alice_hr_admin, perms_synced):
        url = f"{self.BASE}{job_title.pk}/"
        resp = _api(alice).patch(
            url, data={"title": "Lead Engineer", "level": 5},
            format="json", HTTP_X_TENANT=tenant_acme.slug
        )
        assert resp.status_code == 200
        assert resp.json()["title"] == "Lead Engineer"

    def test_delete_job_title(self, alice, tenant_acme, acme_tree, job_title, alice_hr_admin, perms_synced):
        url = f"{self.BASE}{job_title.pk}/"
        resp = _api(alice).delete(url, HTTP_X_TENANT=tenant_acme.slug)
        assert resp.status_code == 204
        assert not JobTitle.objects.filter(pk=job_title.pk).exists()


# ---------------------------------------------------------------------------
# API — employees
# ---------------------------------------------------------------------------


@pytest.mark.django_db
class TestEmployeeAPI:
    BASE = "/api/v1/hr/employees/"

    def test_list_employees(self, alice, tenant_acme, acme_tree, employee, alice_hr_admin, perms_synced):
        resp = _api(alice).get(self.BASE, HTTP_X_TENANT=tenant_acme.slug)
        assert resp.status_code == 200
        data = resp.json()
        assert data["count"] >= 1

    def test_create_employee(self, alice, tenant_acme, acme_tree, job_title, alice_hr_admin, perms_synced):
        payload = {
            "first_name": "Sara",
            "last_name": "Mousavi",
            "hire_date": "2024-01-15",
            "employment_type": "full_time",
        }
        resp = _api(alice).post(
            self.BASE, data=payload, format="json", HTTP_X_TENANT=tenant_acme.slug
        )
        assert resp.status_code == 201
        assert resp.json()["first_name"] == "Sara"

    def test_get_employee_detail(self, alice, tenant_acme, acme_tree, employee, alice_hr_admin, perms_synced):
        url = f"{self.BASE}{employee.public_id}/"
        resp = _api(alice).get(url, HTTP_X_TENANT=tenant_acme.slug)
        assert resp.status_code == 200
        assert resp.json()["employee_no"] == employee.employee_no

    def test_national_id_masked_without_sensitive_perm(
        self, alice, tenant_acme, acme_tree, employee, alice_hr_viewer
    ):
        url = f"{self.BASE}{employee.public_id}/"
        resp = _api(alice).get(url, HTTP_X_TENANT=tenant_acme.slug)
        assert resp.status_code == 200
        data = resp.json()
        assert data.get("national_id") == "****"

    def test_national_id_revealed_with_sensitive_perm(
        self, alice, tenant_acme, acme_tree, employee, alice_hr_admin, perms_synced
    ):
        url = f"{self.BASE}{employee.public_id}/"
        resp = _api(alice).get(url, HTTP_X_TENANT=tenant_acme.slug)
        assert resp.status_code == 200
        data = resp.json()
        assert data.get("national_id") == "1234567890"

    def test_terminate_employee(self, alice, tenant_acme, acme_tree, employee, alice_hr_admin, perms_synced):
        url = f"{self.BASE}{employee.public_id}/terminate/"
        resp = _api(alice).post(
            url,
            data={"termination_date": "2024-12-31", "reason": "Resignation"},
            format="json",
            HTTP_X_TENANT=tenant_acme.slug,
        )
        assert resp.status_code == 200
        assert resp.json()["status"] == "terminated"

    def test_terminate_permission_denied(self, alice, tenant_acme, acme_tree, employee, alice_hr_viewer):
        url = f"{self.BASE}{employee.public_id}/terminate/"
        resp = _api(alice).post(
            url,
            data={"termination_date": "2024-12-31", "reason": "X"},
            format="json",
            HTTP_X_TENANT=tenant_acme.slug,
        )
        assert resp.status_code == 403

    def test_direct_reports(self, alice, tenant_acme, acme_tree, employee, alice_hr_admin, perms_synced):
        services.create_employee(
            tenant_id=tenant_acme.pk,
            organization_node_id=acme_tree["root"].pk,
            first_name="Report",
            last_name="Person",
            hire_date=date(2024, 1, 1),
            manager_id=employee.pk,
        )
        url = f"{self.BASE}{employee.public_id}/direct-reports/"
        resp = _api(alice).get(url, HTTP_X_TENANT=tenant_acme.slug)
        assert resp.status_code == 200
        assert len(resp.json()) >= 1

    def test_org_chart(self, alice, tenant_acme, acme_tree, employee, alice_hr_admin, perms_synced):
        resp = _api(alice).get("/api/v1/hr/org-chart/", HTTP_X_TENANT=tenant_acme.slug)
        assert resp.status_code == 200
        assert isinstance(resp.json(), list)


# ---------------------------------------------------------------------------
# API — documents
# ---------------------------------------------------------------------------


@pytest.mark.django_db
class TestDocumentAPI:
    def _base(self, employee):
        return f"/api/v1/hr/employees/{employee.public_id}/documents/"

    def test_list_documents(self, alice, tenant_acme, acme_tree, employee, alice_hr_admin, perms_synced):
        resp = _api(alice).get(self._base(employee), HTTP_X_TENANT=tenant_acme.slug)
        assert resp.status_code == 200

    def test_add_document(self, alice, tenant_acme, acme_tree, employee, alice_hr_admin, perms_synced):
        payload = {
            "doc_type": "national_id",
            "dms_file_id": "00000000-0000-0000-0000-000000000099",
            "title": "National ID",
        }
        resp = _api(alice).post(
            self._base(employee), data=payload, format="json", HTTP_X_TENANT=tenant_acme.slug
        )
        assert resp.status_code == 201
        assert resp.json()["title"] == "National ID"

    def test_delete_document(self, alice, tenant_acme, acme_tree, employee, alice_hr_admin, perms_synced):
        doc = services.add_employee_document(
            employee=employee,
            doc_type="contract",
            dms_file_id="00000000-0000-0000-0000-000000000050",
            title="Contract",
        )
        url = f"{self._base(employee)}{doc.pk}/"
        resp = _api(alice).delete(url, HTTP_X_TENANT=tenant_acme.slug)
        assert resp.status_code == 204


# ---------------------------------------------------------------------------
# API — bank accounts
# ---------------------------------------------------------------------------


@pytest.mark.django_db
class TestBankAccountAPI:
    def _base(self, employee):
        return f"/api/v1/hr/employees/{employee.public_id}/bank-accounts/"

    def test_add_bank_account(self, alice, tenant_acme, acme_tree, employee, alice_hr_admin, perms_synced, hr_sensitive_data_enabled):
        payload = {
            "bank_name": "Mellat",
            "account_no": "9876543210",
            "iban": "IR0698765432100000000000",
            "is_default": True,
        }
        resp = _api(alice).post(
            self._base(employee), data=payload, format="json", HTTP_X_TENANT=tenant_acme.slug
        )
        assert resp.status_code == 201

    def test_bank_account_sensitive_masked(
        self, alice, tenant_acme, acme_tree, employee, alice_hr_viewer, hr_sensitive_data_enabled
    ):
        services.add_bank_account(
            employee=employee,
            bank_name="Bank X",
            account_no="SECRETNO",
            iban="SECRETIBAN",
            is_default=False,
        )
        resp = _api(alice).get(self._base(employee), HTTP_X_TENANT=tenant_acme.slug)
        assert resp.status_code == 200
        accounts = resp.json()
        assert all(a["account_no"] == "****" for a in accounts)

    def test_bank_account_sensitive_revealed(
        self, alice, tenant_acme, acme_tree, employee, alice_hr_admin, perms_synced, hr_sensitive_data_enabled
    ):
        services.add_bank_account(
            employee=employee,
            bank_name="Bank Y",
            account_no="CLEARNO",
            iban="CLEARIBAN",
            is_default=False,
        )
        resp = _api(alice).get(self._base(employee), HTTP_X_TENANT=tenant_acme.slug)
        assert resp.status_code == 200
        accounts = resp.json()
        assert any(a["account_no"] == "CLEARNO" for a in accounts)
