"""
Integration and unit tests for Notification service, email templates, and staff audit endpoints.
Covers:
- Jinja2 HTML email template rendering
- Resilient failure handling (missing patient email, SMTP errors)
- Automated notification trigger on report release and appointment confirmation
- Staff notification audit list and detail with branch scoping
- Manual staff notification dispatch
- RBAC permission enforcement
"""

from datetime import UTC, datetime
from unittest.mock import AsyncMock, patch

import pytest
import pytest_asyncio
from httpx import AsyncClient
from sqlalchemy.ext.asyncio import AsyncSession

from app.core.security import create_access_token, hash_password
from app.models.appointment import Appointment, AppointmentSlot, AppointmentStatus, BookingChannel
from app.models.lab import Lab
from app.models.notification import Notification, NotificationChannel, NotificationStatus
from app.models.order import OrderStatus, VisitOrder
from app.models.patient import Gender, Patient
from app.models.report import Report, ReportStatus
from app.models.user import User, UserRole
from app.repositories.permission_repository import PermissionRepository
from app.schemas.report import ReportReleaseRequest
from app.services.email_service import EmailService
from app.services.notification_service import NotificationService
from app.services.report_service import ReportService


@pytest.fixture
def successful_email_delivery(monkeypatch: pytest.MonkeyPatch) -> AsyncMock:
    """Success-path tests isolate delivery from real SMTP accounts and inboxes."""
    dispatch = AsyncMock(return_value=(True, None))
    monkeypatch.setattr(EmailService, "send_email", dispatch)
    return dispatch


@pytest_asyncio.fixture(autouse=True)
async def seed_perms(db_session: AsyncSession) -> None:
    """Seed permissions for RBAC checks."""
    repo = PermissionRepository()
    await repo.seed_permissions(db_session)


@pytest_asyncio.fixture
async def test_lab(db_session: AsyncSession) -> Lab:
    """Create test laboratory."""
    lab = Lab(name="Munal Main Laboratory", address="Kathmandu, Nepal")
    db_session.add(lab)
    await db_session.commit()
    await db_session.refresh(lab)
    return lab


@pytest_asyncio.fixture
async def other_lab(db_session: AsyncSession) -> Lab:
    """Create a second laboratory branch for tenant isolation tests."""
    lab = Lab(name="Munal Satellite Lab", address="Lalitpur, Nepal")
    db_session.add(lab)
    await db_session.commit()
    await db_session.refresh(lab)
    return lab


@pytest_asyncio.fixture
async def admin_user(db_session: AsyncSession, test_lab: Lab) -> tuple[User, str]:
    """Create admin user with full access to test_lab."""
    user = User(
        lab_id=test_lab.id,
        name="Admin Narayan",
        phone="9841000001",
        email="admin@munal.test",
        role=UserRole.RECEPTIONIST,
        hashed_password=hash_password("Pass123!"),
        is_active=True,
    )
    db_session.add(user)
    await db_session.commit()
    await db_session.refresh(user)
    token = create_access_token(
        subject=str(user.id),
        extra_claims={"email": user.email, "role": user.role.value, "lab_id": str(user.lab_id)},
    )
    return user, token


@pytest_asyncio.fixture
async def other_branch_admin(db_session: AsyncSession, other_lab: Lab) -> tuple[User, str]:
    """Create admin user for other_lab."""
    user = User(
        lab_id=other_lab.id,
        name="Admin Lalitpur",
        phone="9841000002",
        email="admin_lalit@munal.test",
        role=UserRole.RECEPTIONIST,
        hashed_password=hash_password("Pass123!"),
        is_active=True,
    )
    db_session.add(user)
    await db_session.commit()
    await db_session.refresh(user)
    token = create_access_token(
        subject=str(user.id),
        extra_claims={"email": user.email, "role": user.role.value, "lab_id": str(user.lab_id)},
    )
    return user, token


@pytest_asyncio.fixture
async def superadmin_user(db_session: AsyncSession, test_lab: Lab) -> tuple[User, str]:
    """Create superadmin user with cross-branch visibility."""
    user = User(
        lab_id=test_lab.id,
        name="Super Admin",
        phone="9841000003",
        email="super@munal.test",
        role=UserRole.SUPERADMIN,
        hashed_password=hash_password("Pass123!"),
        is_active=True,
    )
    db_session.add(user)
    await db_session.commit()
    await db_session.refresh(user)
    token = create_access_token(
        subject=str(user.id),
        extra_claims={"email": user.email, "role": user.role.value, "lab_id": str(user.lab_id)},
    )
    return user, token


@pytest_asyncio.fixture
async def phlebotomist_user(db_session: AsyncSession, test_lab: Lab) -> tuple[User, str]:
    """Create phlebotomist user without notification permissions."""
    user = User(
        lab_id=test_lab.id,
        name="Phlebotomist Shyam",
        phone="9841000004",
        email="shyam@munal.test",
        role=UserRole.PHLEBOTOMIST,
        hashed_password=hash_password("Pass123!"),
        is_active=True,
    )
    db_session.add(user)
    await db_session.commit()
    await db_session.refresh(user)
    token = create_access_token(
        subject=str(user.id),
        extra_claims={"email": user.email, "role": user.role.value, "lab_id": str(user.lab_id)},
    )
    return user, token


@pytest_asyncio.fixture
async def patient_with_email(db_session: AsyncSession, test_lab: Lab) -> Patient:
    """Create patient with valid email address."""
    patient = Patient(
        lab_id=test_lab.id,
        patient_code="P/26/0000000501",
        name="Bikash Thapa",
        gender=Gender.MALE,
        age=34,
        phone="9841555555",
        email="bikash@example.com",
    )
    db_session.add(patient)
    await db_session.commit()
    await db_session.refresh(patient)
    return patient


@pytest_asyncio.fixture
async def patient_without_email(db_session: AsyncSession, test_lab: Lab) -> Patient:
    """Create patient without email address."""
    patient = Patient(
        lab_id=test_lab.id,
        patient_code="P/26/0000000502",
        name="Sunita Gurung",
        gender=Gender.FEMALE,
        age=42,
        phone="9841666666",
        email=None,
    )
    db_session.add(patient)
    await db_session.commit()
    await db_session.refresh(patient)
    return patient


@pytest.mark.asyncio
class TestNotificationTemplatesAndService:
    """Unit tests for email template rendering and service resilience."""

    async def test_email_template_rendering(self, test_lab: Lab, patient_with_email: Patient) -> None:
        email_svc = EmailService()
        rendered = email_svc.render_template(
            "report_ready.html",
            {
                "lab": test_lab,
                "patient": patient_with_email,
                "order": {"order_number": "ORD/26/000777"},
                "report": {"verified_at": datetime.now(UTC)},
                "report_view_url": "http://localhost:5173/client/reports/sample_token",
            },
        )
        assert patient_with_email.name in rendered
        assert "ORD/26/000777" in rendered
        assert "http://localhost:5173/client/reports/sample_token" in rendered
        assert test_lab.name in rendered

    async def test_notification_service_handles_missing_email_gracefully(
        self, db_session: AsyncSession, test_lab: Lab, patient_without_email: Patient
    ) -> None:
        order = VisitOrder(
            lab_id=test_lab.id,
            patient_id=patient_without_email.id,
            order_number="ORD/26/000888",
            status=OrderStatus.COMPLETED,
        )
        db_session.add(order)
        await db_session.flush()

        report = Report(
            order_id=order.id,
            version=1,
            status=ReportStatus.RELEASED,
        )
        db_session.add(report)
        await db_session.commit()
        await db_session.refresh(report)

        notif_svc = NotificationService()
        notif = await notif_svc.send_report_released_notification(db_session, report)

        assert notif.status == NotificationStatus.FAILED
        assert "no registered email" in (notif.error_message or "")
        assert notif.patient_id == patient_without_email.id

    async def test_notification_service_handles_smtp_error_gracefully(
        self, db_session: AsyncSession, test_lab: Lab, patient_with_email: Patient
    ) -> None:
        order = VisitOrder(
            lab_id=test_lab.id,
            patient_id=patient_with_email.id,
            order_number="ORD/26/000889",
            status=OrderStatus.COMPLETED,
        )
        db_session.add(order)
        await db_session.flush()

        report = Report(
            order_id=order.id,
            version=1,
            status=ReportStatus.RELEASED,
        )
        db_session.add(report)
        await db_session.commit()
        await db_session.refresh(report)

        notif_svc = NotificationService()
        with patch.object(notif_svc.email_service, "send_email", new_callable=AsyncMock) as mock_send:
            mock_send.return_value = (False, "SMTP Connection Timeout")
            notif = await notif_svc.send_report_released_notification(db_session, report)

        assert notif.status == NotificationStatus.FAILED
        assert "SMTP Connection Timeout" in (notif.error_message or "")


@pytest.mark.asyncio
class TestWorkflowNotificationHooks:
    """Integration tests verifying notification auto-dispatch on workflow milestones."""

    async def test_report_release_automatically_triggers_notification_and_token(
        self,
        db_session: AsyncSession,
        test_lab: Lab,
        patient_with_email: Patient,
        admin_user: tuple[User, str],
        client: AsyncClient,
        successful_email_delivery: AsyncMock,
    ) -> None:
        admin, _ = admin_user
        order = VisitOrder(
            lab_id=test_lab.id,
            patient_id=patient_with_email.id,
            order_number="ORD/26/000999",
            status=OrderStatus.PROCESSING,
        )
        db_session.add(order)
        await db_session.flush()

        report = Report(
            order_id=order.id,
            version=1,
            status=ReportStatus.VERIFIED,
            verified_at=datetime.now(UTC),
            verified_by=admin.id,
            result_data={"parameters": {}},
        )
        db_session.add(report)
        await db_session.commit()

        # Release report via ReportService
        report_svc = ReportService()
        res = await report_svc.release_report(
            db=db_session,
            report_id=report.id,
            data=ReportReleaseRequest(notes="Patient notified"),
            current_user=admin,
        )
        assert res.status == "released"

        # Verify Notification record was automatically persisted
        from app.repositories.notification_repository import NotificationRepository

        notifs, total = await NotificationRepository().list(
            db_session, lab_id=test_lab.id, patient_id=patient_with_email.id
        )
        assert total >= 1
        latest_notif = notifs[0]
        assert latest_notif.report_id == report.id
        assert latest_notif.status == NotificationStatus.SENT

    async def test_appointment_confirmation_automatically_triggers_notification(
        self,
        db_session: AsyncSession,
        test_lab: Lab,
        patient_with_email: Patient,
    ) -> None:
        slot = AppointmentSlot(
            lab_id=test_lab.id,
            start_at=datetime(2026, 11, 1, 10, 0, tzinfo=UTC),
            end_at=datetime(2026, 11, 1, 10, 30, tzinfo=UTC),
            capacity=4,
            booked_count=0,
            is_active=True,
        )
        db_session.add(slot)
        await db_session.flush()

        apt = Appointment(
            lab_id=test_lab.id,
            patient_id=patient_with_email.id,
            slot_id=slot.id,
            status=AppointmentStatus.PENDING,
            booked_via=BookingChannel.ONLINE,
            contact_name=patient_with_email.name,
            contact_phone=patient_with_email.phone,
        )
        db_session.add(apt)
        await db_session.commit()

        from app.services.appointment_service import AppointmentService

        apt_svc = AppointmentService()
        confirmed = await apt_svc.confirm_appointment(db_session, apt.id)
        assert confirmed.status == AppointmentStatus.CONFIRMED

        from app.repositories.notification_repository import NotificationRepository

        notifs, total = await NotificationRepository().list(
            db_session, lab_id=test_lab.id, patient_id=patient_with_email.id
        )
        assert total >= 1
        assert any("appointment" in n.subject.lower() for n in notifs if n.subject)


@pytest.mark.asyncio
class TestStaffNotificationEndpoints:
    """Integration tests for staff listing, detail, and manual dispatch endpoints."""

    async def test_staff_can_list_notifications_with_branch_scoping(
        self,
        client: AsyncClient,
        db_session: AsyncSession,
        test_lab: Lab,
        other_lab: Lab,
        patient_with_email: Patient,
        admin_user: tuple[User, str],
        other_branch_admin: tuple[User, str],
        superadmin_user: tuple[User, str],
    ) -> None:
        _, admin_token = admin_user
        _, other_admin_token = other_branch_admin
        _, super_token = superadmin_user

        # Create notification in test_lab
        notif_a = Notification(
            patient_id=patient_with_email.id,
            channel=NotificationChannel.EMAIL,
            status=NotificationStatus.SENT,
            recipient="patient_a@example.com",
            subject="Branch A Notification",
            message="Content A",
        )
        db_session.add(notif_a)

        # Create patient and notification in other_lab
        patient_b = Patient(
            lab_id=other_lab.id,
            patient_code="P/26/0000000999",
            name="Branch B Patient",
            gender=Gender.MALE,
            phone="9841888888",
            email="patient_b@example.com",
        )
        db_session.add(patient_b)
        await db_session.flush()

        notif_b = Notification(
            patient_id=patient_b.id,
            channel=NotificationChannel.EMAIL,
            status=NotificationStatus.SENT,
            recipient="patient_b@example.com",
            subject="Branch B Notification",
            message="Content B",
        )
        db_session.add(notif_b)
        await db_session.commit()

        # Admin A sees only notif_a
        res_a = await client.get(
            "/api/v1/notifications",
            headers={"Authorization": f"Bearer {admin_token}"},
        )
        assert res_a.status_code == 200
        items_a = res_a.json()["data"]
        ids_a = [item["id"] for item in items_a]
        assert str(notif_a.id) in ids_a
        assert str(notif_b.id) not in ids_a

        # Admin B sees only notif_b
        res_b = await client.get(
            "/api/v1/notifications",
            headers={"Authorization": f"Bearer {other_admin_token}"},
        )
        assert res_b.status_code == 200
        items_b = res_b.json()["data"]
        ids_b = [item["id"] for item in items_b]
        assert str(notif_b.id) in ids_b
        assert str(notif_a.id) not in ids_b

        # Superadmin sees both
        res_super = await client.get(
            "/api/v1/notifications",
            headers={"Authorization": f"Bearer {super_token}"},
        )
        assert res_super.status_code == 200
        items_super = res_super.json()["data"]
        ids_super = [item["id"] for item in items_super]
        assert str(notif_a.id) in ids_super
        assert str(notif_b.id) in ids_super

    async def test_staff_can_view_single_notification_detail(
        self,
        client: AsyncClient,
        db_session: AsyncSession,
        patient_with_email: Patient,
        admin_user: tuple[User, str],
    ) -> None:
        _, token = admin_user
        notif = Notification(
            patient_id=patient_with_email.id,
            channel=NotificationChannel.EMAIL,
            status=NotificationStatus.SENT,
            recipient=patient_with_email.email or "test@example.com",
            subject="Test Detail View",
            message="Audit message body content",
        )
        db_session.add(notif)
        await db_session.commit()

        res = await client.get(
            f"/api/v1/notifications/{notif.id}",
            headers={"Authorization": f"Bearer {token}"},
        )
        assert res.status_code == 200
        data = res.json()["data"]
        assert data["id"] == str(notif.id)
        assert data["subject"] == "Test Detail View"
        assert data["message"] == "Audit message body content"

    async def test_staff_cannot_view_other_branch_notification(
        self,
        client: AsyncClient,
        db_session: AsyncSession,
        other_lab: Lab,
        admin_user: tuple[User, str],
    ) -> None:
        _, token = admin_user
        other_patient = Patient(
            lab_id=other_lab.id,
            patient_code="P/26/0000000777",
            name="Other Patient",
            gender=Gender.FEMALE,
            phone="9841777777",
            email="other@example.com",
        )
        db_session.add(other_patient)
        await db_session.flush()

        other_notif = Notification(
            patient_id=other_patient.id,
            channel=NotificationChannel.EMAIL,
            status=NotificationStatus.SENT,
            recipient="other@example.com",
            subject="Other Branch Note",
            message="Secret info",
        )
        db_session.add(other_notif)
        await db_session.commit()

        res = await client.get(
            f"/api/v1/notifications/{other_notif.id}",
            headers={"Authorization": f"Bearer {token}"},
        )
        assert res.status_code == 404

    async def test_staff_can_send_manual_custom_notification(
        self,
        client: AsyncClient,
        patient_with_email: Patient,
        admin_user: tuple[User, str],
        successful_email_delivery: AsyncMock,
    ) -> None:
        _, token = admin_user
        payload = {
            "patient_id": str(patient_with_email.id),
            "channel": "email",
            "recipient": "bikash@example.com",
            "subject": "Follow-up Test Reminder",
            "message": "Please remember to schedule your 6-month checkup.",
        }
        res = await client.post(
            "/api/v1/notifications/send",
            json=payload,
            headers={"Authorization": f"Bearer {token}"},
        )
        assert res.status_code == 201
        data = res.json()["data"]
        assert data["subject"] == "Follow-up Test Reminder"
        assert data["status"] == "sent"

    async def test_unauthorized_role_cannot_access_notifications(
        self,
        client: AsyncClient,
        phlebotomist_user: tuple[User, str],
    ) -> None:
        _, token = phlebotomist_user
        res = await client.get(
            "/api/v1/notifications",
            headers={"Authorization": f"Bearer {token}"},
        )
        assert res.status_code == 403
        assert res.json()["error"]["code"] == "FORBIDDEN"
