"""
Integration tests for Sample collection, specimen tracking, and barcode lookup.
"""

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

from app.core.security import create_access_token, hash_password
from app.models.audit import AuditCategory, AuditLog
from app.models.lab import Lab
from app.models.order import OrderStatus, VisitOrder
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 sample_lab(db_session: AsyncSession) -> Lab:
    """Create test laboratory."""
    lab = Lab(name="Sample Test Lab", address="Kathmandu")
    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 separate branch laboratory for cross-branch testing."""
    lab = Lab(name="Branch 2 Lab", address="Pokhara")
    db_session.add(lab)
    await db_session.commit()
    await db_session.refresh(lab)
    return lab


@pytest_asyncio.fixture
async def sample_patient(db_session: AsyncSession, sample_lab: Lab) -> Patient:
    """Create test patient."""
    p = Patient(
        lab_id=sample_lab.id,
        patient_code="P26-0099",
        name="Sunita Shrestha",
        gender=Gender.FEMALE,
        phone="9841999999",
    )
    db_session.add(p)
    await db_session.commit()
    await db_session.refresh(p)
    return p


@pytest_asyncio.fixture
async def sample_order(db_session: AsyncSession, sample_lab: Lab, sample_patient: Patient) -> VisitOrder:
    """Create test order."""
    order = VisitOrder(
        lab_id=sample_lab.id,
        patient_id=sample_patient.id,
        order_number="ORD/26/000099",
        status=OrderStatus.PENDING,
    )
    db_session.add(order)
    await db_session.commit()
    await db_session.refresh(order)
    return order


@pytest_asyncio.fixture
async def phlebotomist_token(db_session: AsyncSession, sample_lab: Lab) -> tuple[User, str]:
    """Create phlebotomist user and bearer token."""
    user = User(
        lab_id=sample_lab.id,
        name="Phlebotomist Maya",
        phone="9801122334",
        email="maya@munal.com",
        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={
            "phone": user.phone,
            "email": user.email,
            "role": user.role.value,
            "lab_id": str(user.lab_id),
        },
    )
    return user, token


@pytest_asyncio.fixture
async def other_branch_token(db_session: AsyncSession, other_lab: Lab) -> str:
    """Create staff token scoped to Branch 2."""
    user = User(
        lab_id=other_lab.id,
        name="Other Staff",
        phone="9809988776",
        email="other@munal.com",
        role=UserRole.RECEPTIONIST,
        hashed_password=hash_password("Pass123!"),
        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,
            "email": user.email,
            "role": user.role.value,
            "lab_id": str(user.lab_id),
        },
    )


@pytest.mark.asyncio
class TestSampleWorkflow:
    """Test suite for physical specimen collection, barcode lookup, and tracking."""

    async def test_create_sample_for_order(
        self,
        client: AsyncClient,
        sample_order: VisitOrder,
        phlebotomist_token: tuple[User, str],
    ) -> None:
        """Staff can create a sample record under an existing order."""
        _, token = phlebotomist_token
        headers = {"Authorization": f"Bearer {token}"}

        payload = {
            "sample_type": "EDTA Whole Blood",
            "notes": "Fasting sample required",
        }
        resp = await client.post(
            f"/api/v1/orders/{sample_order.id}/samples",
            json=payload,
            headers=headers,
        )

        assert resp.status_code == 201
        data = resp.json()["data"]
        assert data["order_id"] == str(sample_order.id)
        assert data["sample_type"] == "EDTA Whole Blood"
        assert data["status"] == "pending"
        assert data["barcode"] == data["sample_code"]
        assert data["sample_code"].startswith("S")
        assert data["sample_code"][1:].isdigit()
        assert len(data["sample_code"]) == 9
        assert data["order_number"] == sample_order.order_number
        assert data["patient_name"] == "Sunita Shrestha"

    async def test_collect_sample_advances_order_status(
        self,
        client: AsyncClient,
        db_session: AsyncSession,
        sample_order: VisitOrder,
        phlebotomist_token: tuple[User, str],
    ) -> None:
        """Phlebotomist marks sample collected; order status advances to sample_collected."""
        user, token = phlebotomist_token
        headers = {"Authorization": f"Bearer {token}"}

        # 1. Create sample
        create_resp = await client.post(
            f"/api/v1/orders/{sample_order.id}/samples",
            json={"sample_type": "Serum"},
            headers=headers,
        )
        sample_id = create_resp.json()["data"]["id"]

        # 2. Mark collected
        collect_resp = await client.post(
            f"/api/v1/samples/{sample_id}/collect",
            json={"notes": "Smooth venipuncture"},
            headers=headers,
        )

        assert collect_resp.status_code == 200
        collected_data = collect_resp.json()["data"]
        assert collected_data["status"] == "collected"
        assert collected_data["collected_by"] == str(user.id)
        assert collected_data["collector_name"] == user.name
        assert collected_data["collected_at"] is not None

        audit_log = (
            await db_session.execute(
                select(AuditLog).where(
                    AuditLog.action == "SPECIMEN_COLLECTED",
                    AuditLog.target_id == sample_id,
                )
            )
        ).scalar_one()
        assert audit_log.category == AuditCategory.CLINICAL.value
        assert audit_log.actor_id == user.id
        assert audit_log.details == {"status": "collected", "previous_status": "pending"}

        # 3. Verify order status advanced
        order_resp = await client.get(f"/api/v1/orders/{sample_order.id}", headers=headers)
        assert order_resp.status_code == 200
        assert order_resp.json()["data"]["status"] == "sample_collected"

    async def test_collect_sample_already_collected_raises_422(
        self,
        client: AsyncClient,
        sample_order: VisitOrder,
        phlebotomist_token: tuple[User, str],
    ) -> None:
        """Attempting to collect an already-collected sample raises 422."""
        _, token = phlebotomist_token
        headers = {"Authorization": f"Bearer {token}"}

        create_resp = await client.post(
            f"/api/v1/orders/{sample_order.id}/samples",
            json={"sample_type": "Urine"},
            headers=headers,
        )
        sample_id = create_resp.json()["data"]["id"]

        # Collect once
        await client.post(f"/api/v1/samples/{sample_id}/collect", json={}, headers=headers)

        # Attempt to collect again -> 422
        repeat_resp = await client.post(f"/api/v1/samples/{sample_id}/collect", json={}, headers=headers)
        assert repeat_resp.status_code == 422
        assert "pending" in repeat_resp.json()["error"]["message"]

    async def test_receive_sample_in_lab(
        self,
        client: AsyncClient,
        sample_order: VisitOrder,
        phlebotomist_token: tuple[User, str],
    ) -> None:
        """Technician receives collected sample into laboratory; advances status to processing."""
        _, token = phlebotomist_token
        headers = {"Authorization": f"Bearer {token}"}

        create_resp = await client.post(
            f"/api/v1/orders/{sample_order.id}/samples",
            json={"sample_type": "Plasma"},
            headers=headers,
        )
        sample_id = create_resp.json()["data"]["id"]

        # Collect first
        await client.post(f"/api/v1/samples/{sample_id}/collect", json={}, headers=headers)

        # Receive into lab
        receive_resp = await client.post(f"/api/v1/samples/{sample_id}/receive", headers=headers)
        assert receive_resp.status_code == 200
        assert receive_resp.json()["data"]["status"] == "processing"

        # Verify parent order status is processing
        order_resp = await client.get(f"/api/v1/orders/{sample_order.id}", headers=headers)
        assert order_resp.json()["data"]["status"] == "processing"

    async def test_direct_receive_records_collection(
        self,
        client: AsyncClient,
        sample_order: VisitOrder,
        phlebotomist_token: tuple[User, str],
    ) -> None:
        """Direct laboratory receipt records collection and moves to processing."""
        _, token = phlebotomist_token
        headers = {"Authorization": f"Bearer {token}"}

        create_resp = await client.post(
            f"/api/v1/orders/{sample_order.id}/samples",
            json={"sample_type": "CSF"},
            headers=headers,
        )
        sample_id = create_resp.json()["data"]["id"]

        receive_resp = await client.post(f"/api/v1/samples/{sample_id}/receive", headers=headers)
        assert receive_resp.status_code == 200
        received = receive_resp.json()["data"]
        assert received["status"] == "processing"
        assert received["collected_at"] is not None
        assert received["collected_by"] == str(phlebotomist_token[0].id)

    async def test_complete_sample_testing(
        self,
        client: AsyncClient,
        sample_order: VisitOrder,
        phlebotomist_token: tuple[User, str],
    ) -> None:
        """Technician marks sample analysis completed."""
        _, token = phlebotomist_token
        headers = {"Authorization": f"Bearer {token}"}

        create_resp = await client.post(
            f"/api/v1/orders/{sample_order.id}/samples",
            json={"sample_type": "Stool"},
            headers=headers,
        )
        sample_id = create_resp.json()["data"]["id"]

        await client.post(f"/api/v1/samples/{sample_id}/collect", json={}, headers=headers)
        await client.post(f"/api/v1/samples/{sample_id}/receive", headers=headers)

        complete_resp = await client.post(f"/api/v1/samples/{sample_id}/complete", headers=headers)
        assert complete_resp.status_code == 200
        assert complete_resp.json()["data"]["status"] == "completed"

    async def test_reject_sample_documents_reason(
        self,
        client: AsyncClient,
        db_session: AsyncSession,
        sample_order: VisitOrder,
        phlebotomist_token: tuple[User, str],
    ) -> None:
        """Rejecting a defective sample appends the rejection reason to notes."""
        _, token = phlebotomist_token
        headers = {"Authorization": f"Bearer {token}"}

        create_resp = await client.post(
            f"/api/v1/orders/{sample_order.id}/samples",
            json={"sample_type": "Blood"},
            headers=headers,
        )
        sample_id = create_resp.json()["data"]["id"]

        reject_resp = await client.post(
            f"/api/v1/samples/{sample_id}/reject",
            json={"notes": "Grossly hemolyzed; specimen unusable for potassium"},
            headers=headers,
        )
        assert reject_resp.status_code == 200
        assert "REJECTED" in reject_resp.json()["data"]["notes"]
        assert "Grossly hemolyzed" in reject_resp.json()["data"]["notes"]
        audit_log = (
            await db_session.execute(
                select(AuditLog).where(
                    AuditLog.action == "SPECIMEN_REJECTED",
                    AuditLog.target_id == sample_id,
                )
            )
        ).scalar_one()
        assert audit_log.category == AuditCategory.CLINICAL.value
        assert audit_log.actor_id == phlebotomist_token[0].id
        assert audit_log.details == {"status": "pending", "previous_status": "pending"}
        assert "Grossly hemolyzed" not in str(audit_log.details)

    async def test_barcode_scanner_lookup(
        self,
        client: AsyncClient,
        sample_order: VisitOrder,
        phlebotomist_token: tuple[User, str],
    ) -> None:
        """Scanner lookup returns exact specimen with patient demographics and order info."""
        _, token = phlebotomist_token
        headers = {"Authorization": f"Bearer {token}"}

        create_resp = await client.post(
            f"/api/v1/orders/{sample_order.id}/samples",
            json={"sample_type": "Sputum"},
            headers=headers,
        )
        barcode = create_resp.json()["data"]["barcode"]

        lookup_resp = await client.get(f"/api/v1/samples/barcode/{barcode}", headers=headers)
        assert lookup_resp.status_code == 200
        data = lookup_resp.json()["data"]
        assert data["barcode"] == barcode
        assert data["patient_name"] == "Sunita Shrestha"
        assert data["patient_phone"] == "9841999999"
        assert data["order_number"] == sample_order.order_number

    async def test_barcode_not_found_raises_404(
        self,
        client: AsyncClient,
        phlebotomist_token: tuple[User, str],
    ) -> None:
        """Scanning a non-existent barcode returns 404."""
        _, token = phlebotomist_token
        headers = {"Authorization": f"Bearer {token}"}

        resp = await client.get("/api/v1/samples/barcode/SAM-99999999-999999", headers=headers)
        assert resp.status_code == 404

    async def test_list_samples_with_filters(
        self,
        client: AsyncClient,
        sample_order: VisitOrder,
        phlebotomist_token: tuple[User, str],
    ) -> None:
        """List samples with pagination and type filtering."""
        _, token = phlebotomist_token
        headers = {"Authorization": f"Bearer {token}"}

        await client.post(
            f"/api/v1/orders/{sample_order.id}/samples",
            json={"sample_type": "Synovial Fluid"},
            headers=headers,
        )

        resp = await client.get("/api/v1/samples?sample_type=Synovial", headers=headers)
        assert resp.status_code == 200
        assert len(resp.json()["data"]) >= 1

    async def test_cross_branch_access_forbidden(
        self,
        client: AsyncClient,
        sample_order: VisitOrder,
        phlebotomist_token: tuple[User, str],
        other_branch_token: str,
    ) -> None:
        """Staff from another branch cannot access or collect samples from this branch."""
        _, token = phlebotomist_token
        create_resp = await client.post(
            f"/api/v1/orders/{sample_order.id}/samples",
            json={"sample_type": "Blood"},
            headers={"Authorization": f"Bearer {token}"},
        )
        sample_id = create_resp.json()["data"]["id"]

        # Other branch staff attempts to collect
        cross_resp = await client.post(
            f"/api/v1/samples/{sample_id}/collect",
            json={},
            headers={"Authorization": f"Bearer {other_branch_token}"},
        )
        assert cross_resp.status_code == 404

    async def test_unauthenticated_access_rejected(
        self,
        client: AsyncClient,
    ) -> None:
        """Unauthenticated requests are rejected with 401."""
        resp = await client.get("/api/v1/samples")
        assert resp.status_code == 401


@pytest.mark.asyncio
async def test_sequential_labels_are_global_and_completed_samples_cannot_be_received(
    client: AsyncClient,
    db_session: AsyncSession,
    sample_order: VisitOrder,
    phlebotomist_token: tuple[User, str],
    other_lab: Lab,
) -> None:
    from app.models.order import Sample, SampleStatus
    from app.repositories.sample_repository import SampleRepository

    headers = {"Authorization": f"Bearer {phlebotomist_token[1]}"}
    labels = []
    for kind in ("Blood", "Urine"):
        response = await client.post(
            f"/api/v1/orders/{sample_order.id}/samples", json={"sample_type": kind}, headers=headers
        )
        assert response.status_code == 201, response.text
        labels.append(response.json()["data"])
    first, second = labels
    assert int(second["sample_code"][3:]) == int(first["sample_code"][3:]) + 1
    next_branch = await SampleRepository().get_next_sample_code(db_session, other_lab.id)
    assert next_branch not in {first["sample_code"], second["sample_code"]}
    specimen = (await db_session.execute(select(Sample).where(Sample.id == first["id"]))).scalar_one()
    specimen.status = SampleStatus.COMPLETED
    await db_session.commit()
    response = await client.post(f"/api/v1/samples/{first['id']}/receive", headers=headers)
    assert response.status_code == 422


@pytest.mark.asyncio
async def test_concurrent_specimen_allocation_has_no_duplicate_codes(
    db_session: AsyncSession,
    sample_order: VisitOrder,
) -> None:
    import asyncio

    from sqlalchemy.ext.asyncio import async_sessionmaker

    from app.models.order import Sample, SampleStatus
    from app.repositories.sample_repository import SampleRepository

    sessions = async_sessionmaker(db_session.bind, expire_on_commit=False)

    async def allocate() -> str:
        async with sessions() as session:
            code = await SampleRepository().get_next_sample_code(session, sample_order.lab_id)
            session.add(
                Sample(
                    order_id=sample_order.id,
                    lab_id=sample_order.lab_id,
                    sample_code=code,
                    barcode=code,
                    sample_type="Blood",
                    status=SampleStatus.PENDING,
                )
            )
            await session.commit()
            return code

    results = await asyncio.gather(allocate(), allocate(), return_exceptions=True)
    assert all(isinstance(result, str) for result in results), results
    assert results[0] != results[1]


@pytest.mark.asyncio
async def test_legacy_long_specimen_suffix_does_not_overflow_allocation(
    db_session: AsyncSession,
    sample_order: VisitOrder,
) -> None:
    from datetime import UTC, datetime

    from app.models.order import Sample, SampleStatus
    from app.repositories.sample_repository import SampleRepository

    prefix = "S" + datetime.now(UTC).strftime("%y")
    # Stay inside VARCHAR(30), while still exceeding bigint's numeric range.
    suffix = "26" * 10 + "000001"
    db_session.add(
        Sample(
            order_id=sample_order.id,
            lab_id=sample_order.lab_id,
            sample_code=prefix + suffix,
            barcode=prefix + suffix,
            sample_type="Blood",
            status=SampleStatus.PENDING,
        )
    )
    await db_session.commit()
    next_code = await SampleRepository().get_next_sample_code(db_session, sample_order.lab_id)
    assert next_code == prefix + str(int(suffix) + 1)
