"""
Integration tests for Appointments and Slot Booking.
"""

from datetime import UTC, datetime, timedelta
from zoneinfo import ZoneInfo

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.lab import Lab
from app.models.patient import Gender, Patient
from app.models.user import User, UserRole
from app.repositories.permission_repository import PermissionRepository


@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 appt_lab(db_session: AsyncSession) -> Lab:
    """Create test laboratory."""
    lab = Lab(name="Appointments Lab", address="Kathmandu")
    db_session.add(lab)
    await db_session.commit()
    await db_session.refresh(lab)
    return lab


@pytest_asyncio.fixture
async def appt_patient(db_session: AsyncSession, appt_lab: Lab) -> Patient:
    """Create test patient."""
    p = Patient(
        lab_id=appt_lab.id,
        patient_code="P/26/0000000099",
        name="Test Patient",
        gender=Gender.MALE,
        phone="9816000099",
    )
    db_session.add(p)
    await db_session.commit()
    await db_session.refresh(p)
    return p


@pytest_asyncio.fixture
async def admin_token(db_session: AsyncSession, appt_lab: Lab) -> str:
    """Create admin user token."""
    admin = User(
        lab_id=appt_lab.id,
        name="Appointment Admin",
        phone="9816000001",
        role=UserRole.SUPERADMIN,
        hashed_password=hash_password("AdminPass123!"),
        is_active=True,
    )
    db_session.add(admin)
    await db_session.commit()
    await db_session.refresh(admin)

    return create_access_token(
        subject=str(admin.id),
        extra_claims={
            "phone": admin.phone,
            "role": admin.role.value,
            "lab_id": str(appt_lab.id),
        },
    )


@pytest_asyncio.fixture
async def general_user_token(db_session: AsyncSession, appt_lab: Lab) -> str:
    """Create general user token without appointment:write."""
    user = User(
        lab_id=appt_lab.id,
        name="Staff User",
        phone="9816000002",
        role=UserRole.GENERAL_USER,
        hashed_password=hash_password("StaffPass123!"),
        is_active=True,
    )
    db_session.add(user)
    await db_session.commit()
    await db_session.refresh(user)

    return create_access_token(
        subject=str(user.id),
        extra_claims={
            "phone": user.phone,
            "role": user.role.value,
            "lab_id": str(appt_lab.id),
        },
    )


@pytest_asyncio.fixture
async def receptionist_token(db_session: AsyncSession, appt_lab: Lab) -> str:
    receptionist = User(
        lab_id=appt_lab.id,
        name="Appointment Receptionist",
        phone="9816000003",
        role=UserRole.RECEPTIONIST,
        hashed_password=hash_password("ReceptionPass123!"),
        is_active=True,
    )
    db_session.add(receptionist)
    await db_session.commit()
    await db_session.refresh(receptionist)
    return create_access_token(
        subject=str(receptionist.id),
        extra_claims={
            "phone": receptionist.phone,
            "role": receptionist.role.value,
            "lab_id": str(appt_lab.id),
        },
    )


@pytest.mark.asyncio
class TestAppointmentEndpoints:
    """Tests covering slot management and appointment booking workflows."""

    async def test_slot_lifecycle(self, client: AsyncClient, admin_token: str, appt_lab: Lab) -> None:
        headers = {"Authorization": f"Bearer {admin_token}"}
        start = datetime.now(UTC) + timedelta(days=1)
        end = start + timedelta(hours=2)

        # 1. Create slot
        res = await client.post(
            "/api/v1/appointment-slots",
            json={
                "lab_id": str(appt_lab.id),
                "start_at": start.isoformat(),
                "end_at": end.isoformat(),
                "capacity": 3,
                "is_active": True,
            },
            headers=headers,
        )
        assert res.status_code == 201
        slot = res.json()["data"]
        slot_id = slot["id"]
        assert slot["capacity"] == 3
        assert slot["booked_count"] == 0
        assert slot["available_slots"] == 3

        # 2. Get slot
        get_res = await client.get(f"/api/v1/appointment-slots/{slot_id}", headers=headers)
        assert get_res.status_code == 200
        assert get_res.json()["data"]["id"] == slot_id

        # 3. Update slot
        update_res = await client.patch(
            f"/api/v1/appointment-slots/{slot_id}",
            json={"capacity": 5},
            headers=headers,
        )
        assert update_res.status_code == 200
        assert update_res.json()["data"]["capacity"] == 5

        # 4. List slots
        list_res = await client.get("/api/v1/appointment-slots", headers=headers)
        assert list_res.status_code == 200
        assert list_res.json()["meta"]["total"] >= 1

        # 5. Delete slot
        del_res = await client.delete(f"/api/v1/appointment-slots/{slot_id}", headers=headers)
        assert del_res.status_code == 200

        # Verify 404
        assert (await client.get(f"/api/v1/appointment-slots/{slot_id}", headers=headers)).status_code == 404

    async def test_reception_appointment_access_is_scoped_to_assigned_lab(
        self,
        client: AsyncClient,
        db_session: AsyncSession,
        admin_token: str,
        receptionist_token: str,
    ) -> None:
        other_lab = Lab(name="Other Appointments Lab", address="Bhaktapur")
        db_session.add(other_lab)
        await db_session.commit()
        await db_session.refresh(other_lab)
        admin_headers = {"Authorization": f"Bearer {admin_token}"}
        created_patient = await client.post(
            "/api/v1/patients",
            json={
                "lab_id": str(other_lab.id),
                "name": "Other Clinic Patient",
                "gender": "female",
                "phone": "9816000090",
            },
            headers=admin_headers,
        )
        assert created_patient.status_code == 201
        start = datetime.now(UTC) + timedelta(days=1)
        created_slot = await client.post(
            "/api/v1/appointment-slots",
            json={
                "lab_id": str(other_lab.id),
                "start_at": start.isoformat(),
                "end_at": (start + timedelta(minutes=30)).isoformat(),
                "capacity": 1,
            },
            headers=admin_headers,
        )
        assert created_slot.status_code == 201
        created_appointment = await client.post(
            "/api/v1/appointments",
            json={
                "lab_id": str(other_lab.id),
                "patient_id": created_patient.json()["data"]["id"],
                "slot_id": created_slot.json()["data"]["id"],
            },
            headers=admin_headers,
        )
        assert created_appointment.status_code == 201

        receptionist_headers = {"Authorization": f"Bearer {receptionist_token}"}
        list_response = await client.get(
            "/api/v1/appointments",
            params={"lab_id": str(other_lab.id)},
            headers=receptionist_headers,
        )
        check_in_response = await client.post(
            f"/api/v1/appointments/{created_appointment.json()['data']['id']}/check-in",
            headers=receptionist_headers,
        )
        slot_response = await client.get(
            f"/api/v1/appointment-slots/{created_slot.json()['data']['id']}",
            headers=receptionist_headers,
        )

        assert list_response.status_code == 403
        assert check_in_response.status_code == 404
        assert slot_response.status_code == 404

    async def test_appointment_booking_and_capacity(
        self,
        client: AsyncClient,
        admin_token: str,
        appt_lab: Lab,
        appt_patient: Patient,
        db_session: AsyncSession,
    ) -> None:
        headers = {"Authorization": f"Bearer {admin_token}"}
        start = datetime.now(UTC) + timedelta(days=2)
        end = start + timedelta(hours=1)

        # Create a slot with capacity = 1
        slot_res = await client.post(
            "/api/v1/appointment-slots",
            json={
                "lab_id": str(appt_lab.id),
                "start_at": start.isoformat(),
                "end_at": end.isoformat(),
                "capacity": 1,
            },
            headers=headers,
        )
        slot_id = slot_res.json()["data"]["id"]

        # Book first appointment in slot
        book1 = await client.post(
            "/api/v1/appointments",
            json={
                "lab_id": str(appt_lab.id),
                "patient_id": str(appt_patient.id),
                "slot_id": slot_id,
                "booked_via": "online",
                "notes": "Fasting blood collection",
            },
            headers=headers,
        )
        assert book1.status_code == 201
        apt1 = book1.json()["data"]
        apt1_id = apt1["id"]
        assert apt1["status"] == "pending"

        scheduled_day = start.astimezone(ZoneInfo("Asia/Kathmandu")).date().isoformat()
        scheduled_res = await client.get(
            "/api/v1/appointments",
            params={"date": scheduled_day, "active_only": "true"},
            headers=headers,
        )
        assert scheduled_res.status_code == 200
        assert apt1_id in {item["id"] for item in scheduled_res.json()["data"]}

        # Check slot is now full
        slot_check = await client.get(f"/api/v1/appointment-slots/{slot_id}", headers=headers)
        assert slot_check.json()["data"]["is_full"] is True
        assert slot_check.json()["data"]["booked_count"] == 1

        # Attempt to book second appointment in full slot -> 422
        # Use a second patient so test specifically asserts slot capacity
        # rather than single-active-booking constraint
        patient2 = Patient(
            lab_id=appt_lab.id,
            patient_code="P/26/0000000100",
            name="Second Test Patient",
            gender=Gender.FEMALE,
            phone="9816000100",
        )
        db_session.add(patient2)
        await db_session.commit()
        await db_session.refresh(patient2)

        book2 = await client.post(
            "/api/v1/appointments",
            json={
                "lab_id": str(appt_lab.id),
                "patient_id": str(patient2.id),
                "slot_id": slot_id,
                "booked_via": "phone",
            },
            headers=headers,
        )
        assert book2.status_code == 422
        assert "fully booked" in book2.json()["error"]["message"]

        # Cancel appointment 1 -> releases capacity
        cancel_res = await client.post(
            f"/api/v1/appointments/{apt1_id}/cancel",
            headers=headers,
        )
        assert cancel_res.status_code == 200
        assert cancel_res.json()["data"]["status"] == "cancelled"

        # Verify slot is no longer full
        slot_after_cancel = await client.get(f"/api/v1/appointment-slots/{slot_id}", headers=headers)
        assert slot_after_cancel.json()["data"]["is_full"] is False
        assert slot_after_cancel.json()["data"]["booked_count"] == 0

    async def test_appointment_lifecycle_transitions(
        self, client: AsyncClient, admin_token: str, appt_lab: Lab, appt_patient: Patient
    ) -> None:
        headers = {"Authorization": f"Bearer {admin_token}"}

        # Walk-in booking (starts at checked_in)
        walk_in_res = await client.post(
            "/api/v1/appointments",
            json={
                "lab_id": str(appt_lab.id),
                "patient_id": str(appt_patient.id),
                "booked_via": "walk_in",
            },
            headers=headers,
        )
        assert walk_in_res.status_code == 201
        walk_in = walk_in_res.json()["data"]
        apt_id = walk_in["id"]
        assert walk_in["status"] == "checked_in"

        clinic_today = datetime.now(UTC).astimezone(ZoneInfo("Asia/Kathmandu")).date().isoformat()
        today_res = await client.get(
            "/api/v1/appointments",
            params={"date": clinic_today, "active_only": "true"},
            headers=headers,
        )
        assert today_res.status_code == 200
        assert apt_id in {item["id"] for item in today_res.json()["data"]}

        yesterday = (
            datetime.now(UTC).astimezone(ZoneInfo("Asia/Kathmandu")).date() - timedelta(days=1)
        ).isoformat()
        yesterday_res = await client.get(
            "/api/v1/appointments",
            params={"date": yesterday, "active_only": "true"},
            headers=headers,
        )
        assert yesterday_res.status_code == 200
        assert apt_id not in {item["id"] for item in yesterday_res.json()["data"]}

        # Complete appointment
        comp_res = await client.post(f"/api/v1/appointments/{apt_id}/complete", headers=headers)
        assert comp_res.status_code == 200
        assert comp_res.json()["data"]["status"] == "completed"

        # Attempt to cancel completed appointment -> 422
        cancel_invalid = await client.post(f"/api/v1/appointments/{apt_id}/cancel", headers=headers)
        assert cancel_invalid.status_code == 422

    async def test_forbidden_without_permission(
        self, client: AsyncClient, general_user_token: str, appt_lab: Lab
    ) -> None:
        headers = {"Authorization": f"Bearer {general_user_token}"}
        start = datetime.now(UTC) + timedelta(days=1)
        res = await client.post(
            "/api/v1/appointment-slots",
            json={
                "lab_id": str(appt_lab.id),
                "start_at": start.isoformat(),
                "end_at": (start + timedelta(hours=1)).isoformat(),
            },
            headers=headers,
        )
        assert res.status_code == 403
