import pytest
from django.contrib.auth import get_user_model
from datetime import date, timedelta
from rest_framework.test import APIClient
from apps.hr.models import Employee, Attendance, Payroll, Recruitment, Onboarding, Contract, Communication, Survey, LeaveRequest, BusinessTrip, EmployeeDocument
from apps.companies.models import Organization


@pytest.fixture
def organization(db):
    return Organization.objects.create(name="Test Org")


@pytest.fixture
def user(db, organization):
    from apps.companies.models import OrganizationMembership
    from apps.hr.signals import ensure_employee_stub
    User = get_user_model()
    u = User.objects.create_user(email="hr@example.com", password="pass123")
    OrganizationMembership.objects.create(organization=organization, user=u)
    ensure_employee_stub(u)
    return u


@pytest.fixture
def employee(user):
    # The hr post_save signal auto-creates an Employee stub per user — reuse it
    # (a second Employee for the same user violates the OneToOne constraint).
    emp = Employee.objects.get(user=user)
    emp.employee_id = "EMP001"
    emp.position = "Developer"
    emp.department = "IT"
    emp.hire_date = date.today()
    emp.save()
    return emp


@pytest.fixture
def hr_approver(db, organization):
    """User with full hr scope (view/approve/delete), distinct from the
    requester — get_queryset requires the domain's `delete` permission as
    its manager-level proxy (apps.core.permissions.user_has_full_scope),
    not just `approve`, or the approver can't even see other people's
    pending requests to act on them."""
    from apps.core.models import Permission, Role, UserRole
    from apps.companies.models import OrganizationMembership
    User = get_user_model()
    approver = User.objects.create_user(email="hr-approver@example.com", password="pass123")
    OrganizationMembership.objects.create(organization=organization, user=approver)
    role = Role.objects.create(name="HR Approver", slug="hr-approver-test")
    perm, _ = Permission.objects.get_or_create(domain="hr", action="*", defaults={"label": "All HR"})
    role.permissions.add(perm)
    UserRole.objects.create(user=approver, role=role, organization=organization)
    return approver


@pytest.mark.django_db
class TestEmployeeModel:
    def test_employee_auto_created_and_updatable(self, user):
        # Signal creates the stub on user creation.
        emp = Employee.objects.get(user=user)
        emp.employee_id = "EMP002"
        emp.position = "Designer"
        emp.department = "Design"
        emp.save()
        emp.refresh_from_db()
        assert emp.employee_id == "EMP002"
        assert emp.position == "Designer"

    def test_employee_unique_id(self, employee, organization):
        from apps.companies.models import OrganizationMembership
        from apps.hr.signals import ensure_employee_stub
        User = get_user_model()
        other = User.objects.create_user(email="hr2@example.com", password="pass123")
        OrganizationMembership.objects.create(organization=organization, user=other)
        ensure_employee_stub(other)
        dup = Employee.objects.get(user=other)
        dup.employee_id = "EMP001"
        with pytest.raises(Exception):
            dup.save()

    def test_employee_str(self, employee):
        assert "EMP001" in str(employee)


@pytest.mark.django_db
class TestAttendanceModel:
    def test_create_attendance(self, employee):
        att = Attendance.objects.create(
            employee=employee,
            type="check_in",
            date=date.today()
        )
        assert att.type == "check_in"

    def test_attendance_unique_constraint(self, employee):
        Attendance.objects.create(
            employee=employee,
            type="check_in",
            date=date.today()
        )
        with pytest.raises(Exception):
            Attendance.objects.create(
                employee=employee,
                type="check_in",
                date=date.today()
            )


@pytest.mark.django_db
class TestAttendanceCrossTenantWrite:
    """BE-002: an org-A caller must not be able to create/update/reassign
    attendance for an org-B employee by submitting their employee ID."""

    @pytest.fixture
    def org_b_employee(self, db):
        from apps.companies.models import Organization, OrganizationMembership
        from apps.hr.signals import ensure_employee_stub

        org_b = Organization.objects.create(name="Org B")
        User = get_user_model()
        user_b = User.objects.create_user(email="orgb@example.com", password="pass123")
        OrganizationMembership.objects.create(organization=org_b, user=user_b)
        ensure_employee_stub(user_b)
        return Employee.objects.get(user=user_b)

    def _client_for(self, user):
        client = APIClient()
        client.force_authenticate(user=user)
        return client

    def test_create_rejects_cross_tenant_employee_id(self, hr_approver, org_b_employee):
        client = self._client_for(hr_approver)
        resp = client.post("/api/attendance/", {
            "employee": str(org_b_employee.id),
            "type": "absent",
            "date": date.today().isoformat(),
            "notes": "cross-tenant write probe",
        }, format="json")
        assert resp.status_code == 400
        assert not Attendance.objects.filter(employee=org_b_employee).exists()

    def test_create_same_organization_still_succeeds(self, hr_approver, employee):
        client = self._client_for(hr_approver)
        resp = client.post("/api/attendance/", {
            "employee": str(employee.id),
            "type": "absent",
            "date": date.today().isoformat(),
        }, format="json")
        assert resp.status_code == 201, resp.content
        assert Attendance.objects.filter(employee=employee).exists()

    def test_update_rejects_reassignment_to_cross_tenant_employee(self, hr_approver, employee, org_b_employee):
        att = Attendance.objects.create(employee=employee, type="check_in", date=date.today())
        client = self._client_for(hr_approver)
        resp = client.patch(f"/api/attendance/{att.id}/", {
            "employee": str(org_b_employee.id),
        }, format="json")
        assert resp.status_code == 400
        att.refresh_from_db()
        assert att.employee_id == employee.id

    def test_nonexistent_and_cross_tenant_ids_return_the_same_error_shape(self, hr_approver, org_b_employee):
        import uuid
        client = self._client_for(hr_approver)
        cross_tenant_resp = client.post("/api/attendance/", {
            "employee": str(org_b_employee.id), "type": "absent", "date": date.today().isoformat(),
        }, format="json")
        nonexistent_resp = client.post("/api/attendance/", {
            "employee": str(uuid.uuid4()), "type": "absent", "date": date.today().isoformat(),
        }, format="json")
        assert cross_tenant_resp.status_code == nonexistent_resp.status_code == 400
        # Same error code and message shape (only the embedded PK differs) —
        # a caller cannot distinguish "exists in another org" from "doesn't exist".
        assert cross_tenant_resp.data["employee"][0].code == nonexistent_resp.data["employee"][0].code == "does_not_exist"
        assert str(cross_tenant_resp.data["employee"][0]).endswith('" - object does not exist.')
        assert str(nonexistent_resp.data["employee"][0]).endswith('" - object does not exist.')


@pytest.mark.django_db
class TestFamilyCrossTenantWrite:
    """BE-002: an org-A caller must not be able to attach a Family record to
    an org-B employee by submitting their employee ID."""

    @pytest.fixture
    def org_b_employee(self, db):
        from apps.companies.models import Organization, OrganizationMembership
        from apps.hr.signals import ensure_employee_stub

        org_b = Organization.objects.create(name="Family Org B")
        User = get_user_model()
        user_b = User.objects.create_user(email="family-orgb@example.com", password="pass123")
        OrganizationMembership.objects.create(organization=org_b, user=user_b)
        ensure_employee_stub(user_b)
        return Employee.objects.get(user=user_b)

    def _client_for(self, user):
        client = APIClient()
        client.force_authenticate(user=user)
        return client

    def test_create_rejects_cross_tenant_employee_id(self, hr_approver, org_b_employee):
        from apps.hr.models import Family

        client = self._client_for(hr_approver)
        resp = client.post("/api/families/", {
            "employee": str(org_b_employee.id), "name": "Probe", "relationship": "spouse",
        }, format="json")
        assert resp.status_code == 400
        assert not Family.objects.filter(employee=org_b_employee).exists()

    def test_create_same_organization_still_succeeds(self, hr_approver, employee):
        from apps.hr.models import Family

        client = self._client_for(hr_approver)
        resp = client.post("/api/families/", {
            "employee": str(employee.id), "name": "Spouse", "relationship": "spouse",
        }, format="json")
        assert resp.status_code == 201, resp.content
        assert Family.objects.filter(employee=employee).exists()


@pytest.mark.django_db
class TestShiftTemplateCrossTenantMembers:
    """BE-002: member_ids on create/update and the set_members action must
    not accept employees from another organization."""

    @pytest.fixture
    def org_b_employee(self, db):
        from apps.companies.models import Organization, OrganizationMembership
        from apps.hr.signals import ensure_employee_stub

        org_b = Organization.objects.create(name="Shift Org B")
        User = get_user_model()
        user_b = User.objects.create_user(email="shift-orgb@example.com", password="pass123")
        OrganizationMembership.objects.create(organization=org_b, user=user_b)
        ensure_employee_stub(user_b)
        return Employee.objects.get(user=user_b)

    def _client_for(self, user):
        client = APIClient()
        client.force_authenticate(user=user)
        return client

    def test_create_excludes_cross_tenant_member(self, hr_approver, employee, org_b_employee):
        client = self._client_for(hr_approver)
        resp = client.post("/api/shifts/", {
            "name": "Morning", "start_time": "08:00", "end_time": "16:00",
            "working_days": "Mon-Fri",
            "member_ids": [str(employee.id), str(org_b_employee.id)],
        }, format="json")
        assert resp.status_code == 201, resp.content
        assert resp.data["member_ids"] == [str(employee.id)]

    def test_set_members_excludes_cross_tenant_member(self, hr_approver, employee, organization, org_b_employee):
        from apps.hr.models import ShiftTemplate

        shift = ShiftTemplate.objects.create(
            organization=organization,
            name="Evening", start_time="16:00", end_time="00:00", working_days=[1, 2, 3, 4, 5],
        )
        client = self._client_for(hr_approver)
        resp = client.put(f"/api/shifts/{shift.id}/members/", {
            "member_ids": [str(employee.id), str(org_b_employee.id)],
        }, format="json")
        assert resp.status_code == 200, resp.content
        assert resp.data["member_ids"] == [str(employee.id)]
        assert list(shift.members.values_list("id", flat=True)) == [employee.id]


@pytest.mark.django_db
class TestPayrollModel:
    def test_create_payroll(self, employee):
        payroll = Payroll.objects.create(
            employee=employee,
            period_start=date.today(),
            period_end=date.today() + timedelta(days=30),
            base_salary=5000000,
            net_salary=4500000
        )
        assert payroll.base_salary == 5000000

    def test_payroll_status_defaults(self, employee):
        payroll = Payroll.objects.create(
            employee=employee,
            period_start=date.today(),
            period_end=date.today() + timedelta(days=30),
            base_salary=5000000,
            net_salary=4500000
        )
        assert payroll.status == "draft"


@pytest.mark.django_db
class TestRecruitmentModel:
    def test_create_recruitment(self, user, organization):
        rec = Recruitment.objects.create(
            organization=organization,
            position="Senior Developer",
            department="IT",
            description="Job description",
            requirements="Requirements",
            hiring_manager=user
        )
        assert rec.position == "Senior Developer"
        assert rec.status == "open"


@pytest.mark.django_db
class TestContractModel:
    def test_create_contract(self, employee):
        contract = Contract.objects.create(
            employee=employee,
            contract_type="full_time",
            start_date=date.today(),
            end_date=date.today() + timedelta(days=365)
        )
        assert contract.contract_type == "full_time"
        assert contract.status == "active"


@pytest.mark.django_db
class TestCommunicationModel:
    def test_create_communication(self, user, organization):
        comm = Communication.objects.create(
            organization=organization,
            title="Announcement",
            content="Important announcement",
            communication_type="announcement",
            author=user
        )
        assert comm.title == "Announcement"


@pytest.mark.django_db
class TestSurveyModel:
    def test_create_survey(self, user, organization):
        survey = Survey.objects.create(
            organization=organization,
            title="Employee Survey",
            description="Annual survey",
            questions=[{"question": "How satisfied?"}],
            created_by=user
        )
        assert survey.status == "draft"
        assert len(survey.questions) == 1


@pytest.mark.django_db
class TestEmployeeManagerField:
    def test_manager_round_trips_through_serializer(self, employee, user, organization):
        from apps.hr.serializers import EmployeeSerializer
        from apps.companies.models import OrganizationMembership
        from apps.hr.signals import ensure_employee_stub
        User = get_user_model()
        mgr_user = User.objects.create_user(email="mgr@example.com", password="pass123")
        OrganizationMembership.objects.create(organization=organization, user=mgr_user)
        ensure_employee_stub(mgr_user)
        manager = Employee.objects.get(user=mgr_user)
        manager.employee_id = "EMP-MGR"
        manager.position = "Manager"
        manager.department = "IT"
        manager.hire_date = date.today()
        manager.save()

        serializer = EmployeeSerializer(employee, data={"manager": str(manager.id)}, partial=True)
        assert serializer.is_valid(), serializer.errors
        serializer.save()
        employee.refresh_from_db()
        assert employee.manager_id == manager.id


@pytest.mark.django_db
class TestSelfServiceApprovalNotifyBack:
    def _client_for(self, user):
        client = APIClient()
        client.force_authenticate(user=user)
        return client

    def test_leave_request_approve_notifies_employee(self, employee, hr_approver):
        from apps.notifications.models import Notification
        leave = LeaveRequest.objects.create(
            employee=employee, leave_type="annual", status="pending",
            start_date=date.today() + timedelta(days=5),
            end_date=date.today() + timedelta(days=7),
        )
        client = self._client_for(hr_approver)
        resp = client.post(f"/api/leave-requests/{leave.id}/approve/")
        assert resp.status_code == 200
        assert Notification.objects.filter(user=employee.user, notification_type="hr").exists()

    def test_leave_request_reject_notifies_employee(self, employee, hr_approver):
        from apps.notifications.models import Notification
        leave = LeaveRequest.objects.create(
            employee=employee, leave_type="annual", status="pending",
            start_date=date.today() + timedelta(days=5),
            end_date=date.today() + timedelta(days=7),
        )
        client = self._client_for(hr_approver)
        resp = client.post(f"/api/leave-requests/{leave.id}/reject/")
        assert resp.status_code == 200
        assert Notification.objects.filter(user=employee.user, notification_type="hr").exists()

    def test_business_trip_approve_notifies_employee(self, employee, hr_approver):
        from apps.notifications.models import Notification
        trip = BusinessTrip.objects.create(
            employee=employee, title="Client visit", status="pending",
            start_date=date.today() + timedelta(days=5),
            end_date=date.today() + timedelta(days=7),
        )
        client = self._client_for(hr_approver)
        resp = client.post(f"/api/business-trips/{trip.id}/approve/")
        assert resp.status_code == 200
        assert Notification.objects.filter(user=employee.user, notification_type="hr").exists()

    def test_employee_document_approve_notifies_employee(self, employee, hr_approver):
        from apps.notifications.models import Notification
        doc = EmployeeDocument.objects.create(
            employee=employee, title="ID Card", document_type="id_card",
            status="pending", submitted_by_employee=True,
        )
        client = self._client_for(hr_approver)
        resp = client.post(f"/api/employee-documents/{doc.id}/approve/")
        assert resp.status_code == 200
        assert Notification.objects.filter(user=employee.user, notification_type="hr").exists()

    def test_approve_denied_without_hr_permission(self, employee):
        leave = LeaveRequest.objects.create(
            employee=employee, leave_type="annual", status="pending",
            start_date=date.today() + timedelta(days=5),
            end_date=date.today() + timedelta(days=7),
        )
        client = self._client_for(employee.user)
        resp = client.post(f"/api/leave-requests/{leave.id}/approve/")
        assert resp.status_code == 403


@pytest.mark.django_db
class TestHrAiToolsTenantScoping:
    """Regression: hr_list_employees / _resolve_target_employee had zero
    organization filter — any authenticated user's chatbot session could see
    the full cross-tenant employee directory."""

    def test_hr_list_employees_excludes_other_orgs(self, employee, hr_approver):
        from apps.companies.models import OrganizationMembership
        from apps.hr.ai_tools import hr_list_employees
        from apps.hr.signals import ensure_employee_stub

        other_org = Organization.objects.create(name="Other Org")
        other_user = get_user_model().objects.create_user(email="other-emp@example.com", password="pass123")
        OrganizationMembership.objects.create(organization=other_org, user=other_user)
        ensure_employee_stub(other_user)
        other_emp = Employee.objects.get(user=other_user)
        other_emp.employee_id = "OTHER-001"
        other_emp.save()

        names = {
            row["employee_id"]
            for row in hr_list_employees(hr_approver, employee.organization)
        }
        assert employee.employee_id in names
        assert "OTHER-001" not in names

    def test_resolve_target_employee_by_name_excludes_other_orgs(self, employee, hr_approver):
        from apps.companies.models import OrganizationMembership
        from apps.hr.ai_tools import _resolve_target_employee
        from apps.hr.signals import ensure_employee_stub

        other_org = Organization.objects.create(name="Other Org")
        other_user = get_user_model().objects.create_user(
            email="other-emp2@example.com", password="pass123", first_name="Zaphod",
        )
        OrganizationMembership.objects.create(organization=other_org, user=other_user)
        ensure_employee_stub(other_user)

        emp, error = _resolve_target_employee(
            hr_approver, employee.organization, "Zaphod"
        )
        assert emp is None
        assert error is not None

@pytest.mark.django_db
class TestCommunicationLimit:
    """`?limit=N` caps the communications list server-side.

    The sidebar shows five news items. Without this it downloaded every
    communication in the organization and sliced client-side, so the payload
    grew indefinitely while the rendered output stayed at five rows.
    """

    def _client_for(self, user):
        client = APIClient()
        client.force_authenticate(user=user)
        return client

    def _make(self, organization, user, n):
        return [
            Communication.objects.create(
                organization=organization,
                title=f"Notice {i}",
                content="body",
                communication_type="announcement",
                author=user,
            )
            for i in range(n)
        ]

    def test_limit_caps_the_response(self, hr_approver, organization):
        self._make(organization, hr_approver, 8)
        resp = self._client_for(hr_approver).get("/api/communications/?limit=5")

        assert resp.status_code == 200, resp.content
        assert len(resp.data) == 5

    def test_without_limit_everything_is_returned(self, hr_approver, organization):
        self._make(organization, hr_approver, 8)
        resp = self._client_for(hr_approver).get("/api/communications/")

        assert resp.status_code == 200, resp.content
        assert len(resp.data) == 8

    def test_limit_respects_model_ordering(self, hr_approver, organization):
        """Pinned first, then newest — a slice must not reorder that."""
        self._make(organization, hr_approver, 4)
        pinned = Communication.objects.create(
            organization=organization, title="Pinned", content="b",
            communication_type="announcement", author=hr_approver, is_pinned=True,
        )
        resp = self._client_for(hr_approver).get("/api/communications/?limit=2")

        assert resp.status_code == 200, resp.content
        assert len(resp.data) == 2
        assert resp.data[0]["title"] == pinned.title

    def test_invalid_limit_is_ignored_not_fatal(self, hr_approver, organization):
        self._make(organization, hr_approver, 3)
        resp = self._client_for(hr_approver).get("/api/communications/?limit=abc")

        assert resp.status_code == 200, resp.content
        assert len(resp.data) == 3

    def test_limit_cannot_widen_tenant_scope(self, hr_approver, organization):
        """A large limit must not reach into another organization."""
        from apps.companies.models import OrganizationMembership

        other_org = Organization.objects.create(name="Comms Other Org")
        other_user = get_user_model().objects.create_user(
            email="comms-other@example.com", password="x"
        )
        OrganizationMembership.objects.create(organization=other_org, user=other_user)
        Communication.objects.create(
            organization=other_org, title="Foreign", content="b",
            communication_type="announcement", author=other_user,
        )
        self._make(organization, hr_approver, 2)

        resp = self._client_for(hr_approver).get("/api/communications/?limit=100")

        assert resp.status_code == 200, resp.content
        assert all(c["title"] != "Foreign" for c in resp.data)
        assert len(resp.data) == 2
