"""Comprehensive backend tests for Minovis HMS."""
import os
import time
import random
from datetime import datetime, timezone, timedelta
from zoneinfo import ZoneInfo

import pytest
import requests

BASE_URL = os.environ.get("REACT_APP_BACKEND_URL", "").rstrip("/")
if not BASE_URL:
    # Fallback for pytest from backend dir
    try:
        with open("/app/frontend/.env") as f:
            for line in f:
                if line.startswith("REACT_APP_BACKEND_URL="):
                    BASE_URL = line.split("=", 1)[1].strip().rstrip("/")
                    break
    except Exception:
        pass

API = f"{BASE_URL}/api"
IST = ZoneInfo("Asia/Kolkata")


@pytest.fixture(scope="session")
def s():
    sess = requests.Session()
    sess.headers.update({"Content-Type": "application/json"})
    return sess


def _mobile():
    return "9" + "".join(str(random.randint(0, 9)) for _ in range(9))


# ---------- Dashboard & seeded state ----------
class TestDashboardAndSeed:
    def test_dashboard_overview(self, s):
        r = s.get(f"{API}/dashboard/overview")
        assert r.status_code == 200
        d = r.json()
        assert "kpis" in d
        k = d["kpis"]
        for key in ("opd_visits_today", "patients_waiting", "active_admissions", "available_beds", "occupancy"):
            assert key in k, f"missing kpi {key}"
        # Note: tests may run after other tests; we check seeded minimums only on first run
        assert k["active_admissions"] >= 1
        assert k["available_beds"] >= 1
        assert "queue" in d and "ward_availability" in d and "active_admissions" in d
        assert "recent_activity" in d and "opd_chart" in d
        assert len(d["opd_chart"]) == 7, f"opd_chart should have 7 entries, got {len(d['opd_chart'])}"

    def test_beds_summary_totals(self, s):
        r = s.get(f"{API}/beds/summary")
        assert r.status_code == 200
        sm = r.json()
        assert sm["total"] == 22
        assert sm["Available"] + sm["Occupied"] + sm["Cleaning"] + sm["Maintenance"] == 22


# ---------- Patients ----------
class TestPatients:
    def test_register_and_search_patient(self, s):
        name = f"TEST_Patient_{random.randint(1000, 9999)}"
        payload = {"full_name": name, "dob": "1990-05-10", "gender": "Male", "mobile": _mobile()}
        r = s.post(f"{API}/patients", json=payload)
        assert r.status_code == 200, r.text
        data = r.json()
        assert data["full_name"] == name
        assert data["patient_id"].startswith("MNV-")
        assert len(data["patient_id"]) == 10  # MNV-XXXXXX
        pid = data["id"]
        # search
        r2 = s.get(f"{API}/patients", params={"search": name})
        assert r2.status_code == 200
        found = r2.json()
        # response may be dict or list
        items = found.get("patients") if isinstance(found, dict) else found
        assert any(p["id"] == pid for p in items)

    def test_patient_future_dob_rejected(self, s):
        future = (datetime.now() + timedelta(days=5)).date().isoformat()
        r = s.post(f"{API}/patients", json={"full_name": "TEST_FutDob", "dob": future, "mobile": _mobile()})
        assert r.status_code == 422

    def test_patient_invalid_mobile_rejected(self, s):
        r = s.post(f"{API}/patients", json={"full_name": "TEST_BadMob", "dob": "1990-01-01", "mobile": "12345"})
        assert r.status_code == 422

    def test_patient_duplicate_mobile_flow(self, s):
        m = _mobile()
        r1 = s.post(f"{API}/patients", json={"full_name": "TEST_Dup1", "dob": "1990-01-01", "mobile": m})
        assert r1.status_code == 200
        r2 = s.post(f"{API}/patients", json={"full_name": "TEST_Dup2", "dob": "1991-02-02", "mobile": m})
        assert r2.status_code == 409
        r3 = s.post(f"{API}/patients", json={"full_name": "TEST_Dup2", "dob": "1991-02-02", "mobile": m, "confirm_duplicate": True})
        assert r3.status_code == 200

    def test_update_patient_persists(self, s):
        r = s.post(f"{API}/patients", json={"full_name": "TEST_Upd", "dob": "1985-01-01", "mobile": _mobile()})
        pid = r.json()["id"]
        r2 = s.put(f"{API}/patients/{pid}", json={"full_name": "TEST_Upd2", "city": "Bengaluru"})
        assert r2.status_code == 200
        r3 = s.get(f"{API}/patients/{pid}")
        assert r3.status_code == 200
        assert r3.json()["full_name"] == "TEST_Upd2"
        assert r3.json()["city"] == "Bengaluru"


# ---------- OPD ----------
@pytest.fixture(scope="session")
def doctors(s):
    r = s.get(f"{API}/doctors")
    assert r.status_code == 200
    return r.json()


@pytest.fixture(scope="session")
def wards(s):
    r = s.get(f"{API}/wards")
    assert r.status_code == 200
    return r.json()


@pytest.fixture(scope="session")
def test_patient(s):
    r = s.post(f"{API}/patients", json={
        "full_name": f"TEST_OpdPat_{random.randint(1000,9999)}",
        "dob": "1988-03-15", "mobile": _mobile(),
    })
    assert r.status_code == 200
    return r.json()


class TestOPD:
    def test_walkin_lifecycle(self, s, test_patient, doctors):
        doc = doctors[0]
        payload = {
            "patient_id": test_patient["id"],
            "doctor_id": doc["id"],
            "department": doc["department"],
            "visit_mode": "walk-in",
            "reason": "Fever",
        }
        r = s.post(f"{API}/opd", json=payload)
        assert r.status_code == 200, r.text
        v = r.json()
        assert v["status"] == "Waiting"
        assert v["token"].startswith("OPD-")
        eid = v["id"]
        # start
        r2 = s.post(f"{API}/opd/{eid}/start")
        assert r2.status_code == 200
        assert r2.json()["status"] == "In consultation"
        # complete
        r3 = s.post(f"{API}/opd/{eid}/complete")
        assert r3.status_code == 200
        assert r3.json()["status"] == "Completed"
        # ensure not in queue
        today = datetime.now(IST).date().isoformat()
        r4 = s.get(f"{API}/opd", params={"view": "queue", "date": today})
        assert r4.status_code == 200
        assert not any(e["id"] == eid for e in r4.json())

    def test_appointment_conflict_and_past(self, s, test_patient, doctors):
        doc = doctors[1 % len(doctors)]
        today = datetime.now(IST).date()
        # find available slot tomorrow
        tomorrow = (today + timedelta(days=1)).isoformat()
        r = s.get(f"{API}/doctors/{doc['id']}/slots", params={"date": tomorrow})
        assert r.status_code == 200
        slots = r.json()
        avail = [sl for sl in slots if sl["available"]]
        assert avail, "No available slots tomorrow"
        slot = avail[0]
        # create new patient to avoid double active
        p2 = s.post(f"{API}/patients", json={"full_name": "TEST_Appt1", "dob": "1990-01-01", "mobile": _mobile()}).json()
        p3 = s.post(f"{API}/patients", json={"full_name": "TEST_Appt2", "dob": "1990-01-01", "mobile": _mobile()}).json()
        payload = {
            "patient_id": p2["id"],
            "doctor_id": doc["id"],
            "department": doc["department"],
            "visit_mode": "appointment",
            "scheduled_time": slot["time"],
        }
        r1 = s.post(f"{API}/opd", json=payload)
        assert r1.status_code == 200, r1.text
        # Conflict same slot
        payload2 = dict(payload, patient_id=p3["id"])
        r2 = s.post(f"{API}/opd", json=payload2)
        assert r2.status_code == 409
        # Past time
        past = (datetime.now(timezone.utc) - timedelta(hours=1)).isoformat()
        r3 = s.post(f"{API}/opd", json=dict(payload, patient_id=p3["id"], scheduled_time=past))
        assert r3.status_code == 422

    def test_cancel_removes_from_queue(self, s, doctors):
        doc = doctors[0]
        p = s.post(f"{API}/patients", json={"full_name": "TEST_Cancel", "dob": "1990-01-01", "mobile": _mobile()}).json()
        r = s.post(f"{API}/opd", json={
            "patient_id": p["id"], "doctor_id": doc["id"],
            "department": doc["department"], "visit_mode": "walk-in"
        })
        eid = r.json()["id"]
        token = r.json()["token"]
        rc = s.post(f"{API}/opd/{eid}/cancel")
        assert rc.status_code == 200
        assert rc.json()["status"] == "Cancelled"
        today = datetime.now(IST).date().isoformat()
        rq = s.get(f"{API}/opd", params={"view": "queue", "date": today})
        assert not any(e["id"] == eid for e in rq.json())


# ---------- Admissions / Beds ----------
class TestAdmissionsAndBeds:
    def test_admit_transfer_discharge_cycle(self, s, doctors, wards):
        before_summary = s.get(f"{API}/beds/summary").json()

        # pick an Available bed
        r = s.get(f"{API}/beds", params={"status": "Available"})
        assert r.status_code == 200
        avail_beds = r.json()
        assert len(avail_beds) >= 2
        bed = avail_beds[0]
        bed2 = avail_beds[1]
        doc = doctors[0]

        # Fresh patient
        p = s.post(f"{API}/patients", json={"full_name": "TEST_Adm", "dob": "1980-05-05", "mobile": _mobile()}).json()
        adm_time = datetime.now(timezone.utc).isoformat()
        payload = {
            "patient_id": p["id"], "doctor_id": doc["id"],
            "department": doc["department"], "admission_time": adm_time,
            "ward_id": bed["ward_id"], "bed_id": bed["id"], "reason": "Observation"
        }
        ra = s.post(f"{API}/admissions", json=payload)
        assert ra.status_code == 200, ra.text
        adm = ra.json()
        assert adm["admission_id"].startswith("IPD-")
        aid = adm["id"]
        orig_adm_id = adm["admission_id"]

        # verify bed occupied
        rb = s.get(f"{API}/beds/{bed['id']}")
        assert rb.status_code == 200
        assert rb.json()["status"] == "Occupied"

        after_summary = s.get(f"{API}/beds/summary").json()
        assert after_summary["Occupied"] == before_summary["Occupied"] + 1
        assert after_summary["Available"] == before_summary["Available"] - 1

        # Second patient to same bed => 409
        p2 = s.post(f"{API}/patients", json={"full_name": "TEST_Adm2", "dob": "1980-01-01", "mobile": _mobile()}).json()
        r409 = s.post(f"{API}/admissions", json=dict(payload, patient_id=p2["id"]))
        assert r409.status_code == 409

        # Second active admission for same patient => 409
        other_avail = s.get(f"{API}/beds", params={"status": "Available"}).json()
        if other_avail:
            r_dup = s.post(f"{API}/admissions", json=dict(payload, bed_id=other_avail[0]["id"], ward_id=other_avail[0]["ward_id"]))
            assert r_dup.status_code == 409

        # Admit to Cleaning or Maintenance bed => 409
        cleaning_beds = s.get(f"{API}/beds", params={"status": "Cleaning"}).json()
        maint_beds = s.get(f"{API}/beds", params={"status": "Maintenance"}).json()
        bad_bed = (cleaning_beds + maint_beds)[0] if (cleaning_beds or maint_beds) else None
        if bad_bed:
            p3 = s.post(f"{API}/patients", json={"full_name": "TEST_Adm3", "dob": "1980-01-01", "mobile": _mobile()}).json()
            rbad = s.post(f"{API}/admissions", json={
                "patient_id": p3["id"], "doctor_id": doc["id"], "department": doc["department"],
                "admission_time": adm_time, "ward_id": bad_bed["ward_id"], "bed_id": bad_bed["id"],
            })
            assert rbad.status_code == 409

        # Future admission_time
        p4 = s.post(f"{API}/patients", json={"full_name": "TEST_AdmFut", "dob": "1980-01-01", "mobile": _mobile()}).json()
        fut = (datetime.now(timezone.utc) + timedelta(hours=2)).isoformat()
        rfut_avail = s.get(f"{API}/beds", params={"status": "Available"}).json()
        if rfut_avail:
            rfut = s.post(f"{API}/admissions", json={
                "patient_id": p4["id"], "doctor_id": doc["id"], "department": doc["department"],
                "admission_time": fut, "ward_id": rfut_avail[0]["ward_id"], "bed_id": rfut_avail[0]["id"]
            })
            assert rfut.status_code == 422

        # Transfer
        rt = s.post(f"{API}/admissions/{aid}/transfer", json={"bed_id": bed2["id"]})
        assert rt.status_code == 200, rt.text
        tr = rt.json()
        assert tr["admission_id"] == orig_adm_id
        assert len(tr["bed_assignments"]) == 2
        assert tr["bed_id"] == bed2["id"]
        assert s.get(f"{API}/beds/{bed['id']}").json()["status"] == "Cleaning"
        assert s.get(f"{API}/beds/{bed2['id']}").json()["status"] == "Occupied"

        # Discharge errors
        before_disc = s.get(f"{API}/admissions/{aid}").json()
        before_t = parse_iso(before_disc["admission_time"])
        early = (before_t - timedelta(hours=1)).isoformat()
        rde = s.post(f"{API}/admissions/{aid}/discharge", json={"discharge_time": early})
        assert rde.status_code == 422
        future_t = (datetime.now(timezone.utc) + timedelta(hours=2)).isoformat()
        rdf = s.post(f"{API}/admissions/{aid}/discharge", json={"discharge_time": future_t})
        assert rdf.status_code == 422

        # Successful discharge
        now_iso = datetime.now(timezone.utc).isoformat()
        rd = s.post(f"{API}/admissions/{aid}/discharge", json={"discharge_time": now_iso, "note": "Recovered"})
        assert rd.status_code == 200
        assert rd.json()["status"] == "Discharged"
        assert s.get(f"{API}/beds/{bed2['id']}").json()["status"] == "Cleaning"

        # Mark-available cycle
        rma = s.post(f"{API}/beds/{bed2['id']}/mark-available")
        assert rma.status_code == 200
        assert rma.json()["status"] == "Available"

        # Totals still 22
        final = s.get(f"{API}/beds/summary").json()
        assert final["total"] == 22
        assert final["Available"] + final["Occupied"] + final["Cleaning"] + final["Maintenance"] == 22

    def test_bed_status_transitions(self, s):
        # grab Available
        avail = s.get(f"{API}/beds", params={"status": "Available"}).json()
        assert avail
        b = avail[0]
        # mark-available on Available should fail
        r1 = s.post(f"{API}/beds/{b['id']}/mark-available")
        assert r1.status_code == 409
        # move to Maintenance
        r2 = s.post(f"{API}/beds/{b['id']}/maintenance")
        assert r2.status_code == 200
        assert r2.json()["status"] == "Maintenance"
        # maintenance->maintenance (invalid) should fail
        r3 = s.post(f"{API}/beds/{b['id']}/maintenance")
        assert r3.status_code == 409
        # return-to-cleaning
        r4 = s.post(f"{API}/beds/{b['id']}/return-to-cleaning")
        assert r4.status_code == 200
        assert r4.json()["status"] == "Cleaning"
        # mark-available
        r5 = s.post(f"{API}/beds/{b['id']}/mark-available")
        assert r5.status_code == 200
        assert r5.json()["status"] == "Available"
        # occupied bed cannot be marked Available directly
        occ = s.get(f"{API}/beds", params={"status": "Occupied"}).json()
        if occ:
            r6 = s.post(f"{API}/beds/{occ[0]['id']}/mark-available")
            assert r6.status_code == 409


def parse_iso(s_: str):
    dt = datetime.fromisoformat(s_.replace("Z", "+00:00"))
    if dt.tzinfo is None:
        dt = dt.replace(tzinfo=timezone.utc)
    return dt
