"""
Integration and unit tests for public client link pages and token resolution.
Covers:
- Cryptographic token generation and 72-hour Redis storage
- Public client endpoints (reports, PDF download, appointments)
- Token expiration (404), invalid tokens (404), and type mismatches (422)
- Unreleased report access guards (403)
- Token revocation lifecycle
"""

from datetime import UTC, datetime
from uuid import uuid4

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

from app.models.appointment import Appointment, AppointmentSlot, AppointmentStatus, BookingChannel
from app.models.lab import Lab
from app.models.order import OrderStatus, VisitOrder
from app.models.patient import Gender, Patient
from app.models.report import Report, ReportStatus
from app.services.client_link_service import ClientLinkService


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


@pytest_asyncio.fixture
async def test_patient(db_session: AsyncSession, test_lab: Lab) -> Patient:
    """Create test patient."""
    patient = Patient(
        lab_id=test_lab.id,
        patient_code="P/26/0000000099",
        name="Rabina Shrestha",
        gender=Gender.FEMALE,
        age=28,
        phone="9841999999",
        email="rabina@example.com",
    )
    db_session.add(patient)
    await db_session.commit()
    await db_session.refresh(patient)
    return patient


@pytest_asyncio.fixture
async def released_report(db_session: AsyncSession, test_lab: Lab, test_patient: Patient) -> Report:
    """Create test released diagnostic report."""
    order = VisitOrder(
        lab_id=test_lab.id,
        patient_id=test_patient.id,
        order_number="ORD/26/000099",
        status=OrderStatus.COMPLETED,
    )
    db_session.add(order)
    await db_session.flush()

    report = Report(
        order_id=order.id,
        version=1,
        status=ReportStatus.RELEASED,
        verified_at=datetime.now(UTC),
        released_at=datetime.now(UTC),
        result_data={
            "parameters": {
                "param_1": {
                    "parameter_name": "Hemoglobin",
                    "value": "13.5",
                    "unit": "g/dL",
                    "reference_interval": "12.0 - 15.5",
                    "flag": "N",
                    "is_abnormal": False,
                }
            }
        },
    )
    db_session.add(report)
    await db_session.commit()
    await db_session.refresh(report)
    return report


@pytest_asyncio.fixture
async def draft_report(db_session: AsyncSession, test_lab: Lab, test_patient: Patient) -> Report:
    """Create an unreleased draft diagnostic report."""
    order = VisitOrder(
        lab_id=test_lab.id,
        patient_id=test_patient.id,
        order_number="ORD/26/000100",
        status=OrderStatus.PROCESSING,
    )
    db_session.add(order)
    await db_session.flush()

    report = Report(
        order_id=order.id,
        version=1,
        status=ReportStatus.DRAFT,
        result_data={},
    )
    db_session.add(report)
    await db_session.commit()
    await db_session.refresh(report)
    return report


@pytest_asyncio.fixture
async def test_appointment(db_session: AsyncSession, test_lab: Lab, test_patient: Patient) -> Appointment:
    """Create a confirmed test appointment."""
    slot = AppointmentSlot(
        lab_id=test_lab.id,
        start_at=datetime(2026, 10, 1, 9, 0, tzinfo=UTC),
        end_at=datetime(2026, 10, 1, 9, 30, tzinfo=UTC),
        capacity=5,
        booked_count=1,
        is_active=True,
    )
    db_session.add(slot)
    await db_session.flush()

    appointment = Appointment(
        lab_id=test_lab.id,
        patient_id=test_patient.id,
        slot_id=slot.id,
        status=AppointmentStatus.CONFIRMED,
        booked_via=BookingChannel.ONLINE,
        contact_name="Rabina Shrestha",
        contact_phone="9841999999",
        notes="Fasting blood test",
    )
    db_session.add(appointment)
    await db_session.commit()
    await db_session.refresh(appointment)
    return appointment


@pytest.mark.asyncio
class TestClientTokenService:
    """Unit tests for ClientLinkService token lifecycle and Redis operations."""

    async def test_client_token_generation_and_redis_storage(self) -> None:
        service = ClientLinkService()
        report_id = uuid4()
        patient_id = uuid4()
        lab_id = uuid4()

        token, url, expires_at = await service.create_report_token(report_id, patient_id, lab_id)
        assert len(token) >= 32
        assert token in url
        assert expires_at > datetime.now(UTC)

        payload = await service.verify_token(token, expected_type="report")
        assert payload["token_type"] == "report"
        assert payload["target_id"] == str(report_id)
        assert payload["patient_id"] == str(patient_id)
        assert payload["lab_id"] == str(lab_id)

    async def test_client_token_type_mismatch_raises_validation_exception(self) -> None:
        service = ClientLinkService()
        token, _, _ = await service.create_report_token(uuid4(), uuid4(), uuid4())

        with pytest.raises(Exception) as exc_info:
            await service.verify_token(token, expected_type="appointment")
        assert "Invalid link type" in str(exc_info.value)

    async def test_client_token_revocation(self) -> None:
        service = ClientLinkService()
        token, _, _ = await service.create_report_token(uuid4(), uuid4(), uuid4())

        await service.revoke_token(token)

        with pytest.raises(Exception) as exc_info:
            await service.verify_token(token)
        assert "expired or is invalid" in str(exc_info.value)


@pytest.mark.asyncio
class TestClientReportEndpoints:
    """Integration tests for public client report viewing and PDF download."""

    async def test_client_can_view_released_report_with_valid_token(
        self, client: AsyncClient, released_report: Report, test_patient: Patient
    ) -> None:
        service = ClientLinkService()
        token, _, _ = await service.create_report_token(
            report_id=released_report.id,
            patient_id=test_patient.id,
            lab_id=released_report.order.lab_id,
        )

        res = await client.get(f"/api/v1/client/reports/{token}")
        assert res.status_code == 200
        body = res.json()
        assert body["success"] is True
        data = body["data"]
        assert data["report_id"] == str(released_report.id)
        assert data["order_number"] == released_report.order.order_number
        assert data["status"] == "released"
        assert data["patient"]["name"] == test_patient.name
        assert data["patient"]["patient_code"] == test_patient.patient_code
        assert "parameters" in data["result_data"]

    async def test_client_cannot_view_unreleased_report(
        self, client: AsyncClient, draft_report: Report, test_patient: Patient
    ) -> None:
        service = ClientLinkService()
        token, _, _ = await service.create_report_token(
            report_id=draft_report.id,
            patient_id=test_patient.id,
            lab_id=draft_report.order.lab_id,
        )

        res = await client.get(f"/api/v1/client/reports/{token}")
        assert res.status_code == 403
        body = res.json()
        assert body["success"] is False
        assert body["error"]["code"] == "FORBIDDEN"

    async def test_client_can_stream_pdf_with_valid_token(
        self, client: AsyncClient, released_report: Report, test_patient: Patient
    ) -> None:
        service = ClientLinkService()
        token, _, _ = await service.create_report_token(
            report_id=released_report.id,
            patient_id=test_patient.id,
            lab_id=released_report.order.lab_id,
        )

        res = await client.get(f"/api/v1/client/reports/{token}/pdf")
        assert res.status_code == 200
        assert res.headers["content-type"] == "application/pdf"
        assert res.content.startswith(b"%PDF")
        assert "Content-Disposition" in res.headers

    async def test_client_report_endpoint_returns_404_for_invalid_token(self, client: AsyncClient) -> None:
        res = await client.get("/api/v1/client/reports/invalid_or_expired_token_12345")
        assert res.status_code == 404
        body = res.json()
        assert body["success"] is False
        assert body["error"]["code"] == "NOT_FOUND"

    async def test_client_report_endpoint_rejects_appointment_token(
        self, client: AsyncClient, test_appointment: Appointment, test_patient: Patient
    ) -> None:
        service = ClientLinkService()
        token, _, _ = await service.create_appointment_token(
            appointment_id=test_appointment.id,
            patient_id=test_patient.id,
            lab_id=test_appointment.lab_id,
        )

        res = await client.get(f"/api/v1/client/reports/{token}")
        assert res.status_code == 404
        body = res.json()
        assert body["success"] is False
        assert body["error"]["code"] == "NOT_FOUND"


@pytest.mark.asyncio
class TestClientAppointmentEndpoints:
    """Integration tests for public client appointment viewing."""

    async def test_client_can_view_confirmed_appointment_with_valid_token(
        self, client: AsyncClient, test_appointment: Appointment, test_patient: Patient
    ) -> None:
        service = ClientLinkService()
        token, _, _ = await service.create_appointment_token(
            appointment_id=test_appointment.id,
            patient_id=test_patient.id,
            lab_id=test_appointment.lab_id,
        )

        res = await client.get(f"/api/v1/client/appointments/{token}")
        assert res.status_code == 200
        body = res.json()
        assert body["success"] is True
        data = body["data"]
        assert data["appointment_id"] == str(test_appointment.id)
        assert data["status"] == "confirmed"
        assert data["patient"]["name"] == test_patient.name
        assert data["lab"]["name"] == test_appointment.lab.name
        assert data["start_at"] is not None

    async def test_client_appointment_endpoint_returns_404_for_invalid_token(
        self, client: AsyncClient
    ) -> None:
        res = await client.get("/api/v1/client/appointments/non_existent_token_999")
        assert res.status_code == 404
        body = res.json()
        assert body["success"] is False
        assert body["error"]["code"] == "NOT_FOUND"

    async def test_client_appointment_endpoint_rejects_report_token(
        self, client: AsyncClient, released_report: Report, test_patient: Patient
    ) -> None:
        service = ClientLinkService()
        token, _, _ = await service.create_report_token(
            report_id=released_report.id,
            patient_id=test_patient.id,
            lab_id=released_report.order.lab_id,
        )

        res = await client.get(f"/api/v1/client/appointments/{token}")
        assert res.status_code == 422
        body = res.json()
        assert body["success"] is False
        assert body["error"]["code"] == "VALIDATION_ERROR"


@pytest.mark.asyncio
class TestOrderTracking:
    """Public order-status lookup guarded by the registered phone number."""

    async def test_track_released_order_shows_full_pipeline(
        self, client: AsyncClient, released_report: Report
    ) -> None:
        res = await client.get(
            "/api/v1/client/orders/track",
            params={"order_number": "ORD/26/000099", "phone": "9841999999"},
        )
        assert res.status_code == 200, res.text
        data = res.json()["data"]
        assert data["order_number"] == "ORD/26/000099"
        assert data["report_ready"] is True
        assert [s["key"] for s in data["stages"]] == [
            "registered",
            "collected",
            "analysis",
            "verified",
            "ready",
        ]

    async def test_track_wrong_phone_returns_404(self, client: AsyncClient, released_report: Report) -> None:
        res = await client.get(
            "/api/v1/client/orders/track",
            params={"order_number": "ORD/26/000099", "phone": "9800000000"},
        )
        assert res.status_code == 404

    async def test_track_unknown_order_returns_404(self, client: AsyncClient) -> None:
        res = await client.get(
            "/api/v1/client/orders/track",
            params={"order_number": "ORD/26/009999", "phone": "9841999999"},
        )
        assert res.status_code == 404


@pytest.mark.asyncio
async def test_tracking_rejects_suffix_and_accepts_country_prefix(
    client: AsyncClient, released_report: Report
) -> None:
    for phone in ("9999999", "x-9----"):
        response = await client.get(
            "/api/v1/client/orders/track", params={"order_number": "ORD/26/000099", "phone": phone}
        )
        assert response.status_code == 404
    response = await client.get(
        "/api/v1/client/orders/track", params={"order_number": "ORD/26/000099", "phone": "+977 9841999999"}
    )
    assert response.status_code == 200
