import { describe, expect, it } from "vitest";
import type { CheckInStatus, CheckRun, PatientDetail } from "../src/api/types";
import { statusLabels } from "../src/logic/format";
import { cancellable, details, findingsCell, headerLine, notesLabel, quietHoursLabel, statCells, summaryLabel } from "../src/logic/patient-detail";
import { checkInPill, checkInPills, patientStatePill, recordPill, urgentPill } from "../src/logic/pills";
const now = new Date("2026-10-30T10:00:00Z");
const patient = (change: Partial<PatientDetail> = {}): PatientDetail => ({
id: "p-mariam",
kind: "connected",
name: "Mariam Adeyemi",
age: 67,
pronoun: null,
sharing: {
status: "connected",
sharedBy: ["Mariam", "Funke"],
scopes: ["medicines", "doses", "check_ins", "records"],
allowsCheckInCalls: true,
withheldSome: false,
changedAt: "2026-10-30T09:00:00Z",
},
medicines: { count: 3 },
doses: { taken: 12, expected: 14, onTime: 11 },
lastCheckIn: { at: "2026-10-29T09:00:00Z", kind: "yasmar_call", status: "completed" },
attention: { urgent: false, unreadNotes: 0, newSinceLastCheck: false },
lastActivityAt: "2026-10-30T09:00:00Z",
firstName: "Mariam",
dateOfBirth: "1959-03-04",
birthYear: 1959,
sex: "female",
phone: "+234 803 555 0147",
preferredTitle: null,
language: "en",
hasOwnAccount: true,
identityOnFile: "date_of_birth",
timeZone: "Africa/Lagos",
quietHours: { start: "22:00", end: "07:00" },
quietHoursKnown: true,
blockedTimes: [],
checkInLimits: { usedToday: 0, usedThisWeek: 0, maxPerDay: 1, maxPerWeek: 3, nextPossibleAt: null },
activeCheckInId: null,
lastCheckId: "chk-1",
newInformation: null,
...change,
});
const check = (tiers: string[]) =>
({ ranAt: "2026-10-30T08:00:00Z", assessment: { contextual: { findings: tiers.map((tier, i) => ({ id: `f${i}`, tier })) } } }) as unknown as CheckRun;
describe("check-in state pills (YCI-28)", () => {
const all: CheckInStatus[] = ["sent", "ringing", "missed", "later", "in_progress", "completed", "declined", "not_reached", "identity_not_confirmed", "ended_early", "cancelled"];
it("gives every state the backend sends its own words, icon and meaning", () => {
expect(Object.keys(checkInPills).sort()).toEqual([...all].sort());
for (const s of all) {
const pill = checkInPill(s);
expect(pill.label.length).toBeGreaterThan(2);
expect(pill.meaning.length).toBeGreaterThan(10);
}
expect(new Set(all.map((s) => checkInPill(s).icon)).size).toBe(all.length);
});
it("uses the agreed colours", () => {
expect(checkInPill("sent").tone).toBe("grey");
expect(checkInPill("ringing").tone).toBe("indigo");
expect([checkInPill("in_progress").tone, checkInPill("completed").tone]).toEqual(["green", "green"]);
expect([checkInPill("missed").tone, checkInPill("not_reached").tone]).toEqual(["amber", "amber"]);
expect(checkInPill("declined").tone).toBe("purple");
expect(checkInPill("cancelled").tone).toBe("red-outline");
expect(urgentPill).toMatchObject({ label: "Urgent", tone: "red" });
expect(checkInPill("in_progress").label).toBe("Live");
});
it("names a state it does not know in plain words, never a code", () => {
expect(checkInPill("expired").label).toBe("Expired");
expect(checkInPill("waiting_for_phone")).toMatchObject({ label: "Waiting for phone", tone: "grey" });
});
it("keeps the older dot chips in step with the pills", () => {
expect(statusLabels.in_progress).toEqual({ label: "Live", tone: "green" });
expect(statusLabels.cancelled).toEqual({ label: "Cancelled", tone: "red" });
expect(statusLabels.ringing.tone).toBe("");
});
it("never words a meaning as a decision or advice", () => {
for (const pill of [...Object.values(checkInPills), urgentPill]) expect(pill.meaning).not.toMatch(/decide|diagnos|prescrib|recommend/i);
});
});
describe("record and patient pills", () => {
const rec = { reading: "read" as const, heldBack: null, lineCount: 4 };
it("shows each reading state, held back first", () => {
expect(recordPill(rec)).toMatchObject({ label: "Read · 4 lines", tone: "green" });
expect(recordPill({ ...rec, lineCount: 1 }).label).toBe("Read · 1 line");
expect(recordPill({ ...rec, reading: "failed" }).label).toBe("Not read");
expect(recordPill({ ...rec, reading: "reading" }).label).toBe("Reading");
expect(recordPill({ ...rec, reading: "queued" }).label).toBe("Waiting to be read");
expect(recordPill({ ...rec, heldBack: { reason: "x" } }).label).toBe("Held back");
});
it("puts Connected or Clinic-added on the right of the header", () => {
expect(patientStatePill(patient()).label).toBe("Connected");
expect(patientStatePill(patient({ sharing: null })).label).toBe("Clinic-added");
expect(patientStatePill(patient({ sharing: { ...patient().sharing!, status: "paused" } })).label).toBe("Paused");
});
});
describe("the patient header and stat row", () => {
it("says the date of birth, the clinic and where the patient came from", () => {
expect(headerLine(patient(), "Surulere Family Clinic")).toEqual(["Born 4 Mar 1959", "Surulere Family Clinic", "Shared from MARMAR Health"]);
expect(headerLine(patient({ sharing: null, kind: "clinic_added", dateOfBirth: null, birthYear: null }), "Surulere Family Clinic")).toEqual([
"No date of birth on file",
"Surulere Family Clinic",
"Clinic patient",
]);
expect(headerLine(patient({ dateOfBirth: null }), "")[0]).toBe("Born 1959");
});
it("fills medicines, doses (taken of due), last check-in and findings", () => {
const cells = statCells(patient(), check(["today", "this_week"]), false, now);
expect(cells.map((c) => [c.label, c.value])).toEqual([
["Medicines", "3"],
["Doses this week", "12 of 14"],
["Last check-in", "Yesterday"],
["Findings", "2"],
]);
expect(cells[3].note).toBe("Checked today");
});
it('says "Not shared" for each area kept back, and for a clinic patient', () => {
const cells = statCells(patient({ sharing: { ...patient().sharing!, scopes: ["medicines"], allowsCheckInCalls: false }, lastCheckIn: null }), null, false, now);
expect(cells.slice(1, 3).map((c) => c.value)).toEqual(["Not shared", "Not shared"]);
const clinic = statCells(patient({ kind: "clinic_added", sharing: null, lastCheckId: null }), null, false, now);
expect(clinic.map((c) => c.value)).toEqual(["Not shared", "Not shared", "Not shared", "No check yet"]);
});
it("counts urgent review findings and never calls zero safe", () => {
expect(findingsCell(patient(), check(["get_help_now", "today"]), false, now)).toEqual({ value: "2", note: "1 urgent review · checked today" });
expect(findingsCell(patient(), check([]), false, now).value).toBe("0");
expect(JSON.stringify(findingsCell(patient(), check([]), false, now))).not.toMatch(/safe/i);
expect(findingsCell(patient(), null, true, now).value).toBe("…");
});
});
describe("the rail's Details", () => {
it("lists who shares, what, calls, quiet hours and the identity on file", () => {
const d = details(patient(), now);
expect(d.sharedBy).toMatchObject({ label: "Shared by", value: "Mariam and Funke" });
expect(d.scopes!.map((s) => s.shared)).toEqual([true, true, true, true]);
expect([d.calls, d.quietHours, d.identity]).toEqual(["Yasmar may call", "22:00 to 07:00", "Date of birth · 4 Mar 1959"]);
});
it("marks what is kept back and when quiet hours are not known", () => {
const d = details(patient({ sharing: { ...patient().sharing!, scopes: ["medicines", "doses"], allowsCheckInCalls: false } }), now);
expect(d.scopes!.filter((s) => !s.shared).map((s) => s.label)).toEqual(["Check-ins", "Records"]);
expect(d.calls).toBe("Not allowed");
expect(quietHoursLabel(patient({ quietHoursKnown: false }))).toBe("Not known");
expect(quietHoursLabel(patient({ quietHours: { start: "00:00", end: "00:00" } }))).toBe("Off");
expect(details(patient({ kind: "clinic_added", sharing: null }), now)).toMatchObject({ scopes: null, calls: "Needs MARMAR Health", quietHours: "Not known: no app" });
});
});
describe("once sharing stops (INV-8)", () => {
const stopped = patient({ sharing: { ...patient().sharing!, status: "stopped", changedAt: "2026-10-29T08:00:00Z" } });
it("shows nothing live, says since when, and lists no scopes", () => {
expect(statCells(stopped, null, false, now).slice(0, 3).map((c) => `${c.value} ${c.note}`)).toEqual(Array(3).fill("Stopped Since yesterday"));
expect(details(stopped, now)).toMatchObject({ scopes: null, calls: "Not now", scopesNote: "Live access has ended. You keep the checks and check-ins you saved." });
});
});
describe("check-in rows", () => {
it("counts notes, the new ones, and whether a summary can still come", () => {
expect(notesLabel({ noteCount: 6, unreadNotes: 2 })).toBe("6 notes · 2 new");
expect(notesLabel({ noteCount: 0, unreadNotes: 0 })).toBe("None");
expect(summaryLabel({ status: "completed", summaryReady: true })).toBe("Ready");
expect(summaryLabel({ status: "in_progress", summaryReady: false })).toBe("Not yet");
expect(summaryLabel({ status: "declined", summaryReady: false })).toBe("None");
});
it("offers Cancel only before the call starts, as the call view does", () => {
expect(["sent", "ringing", "later", "missed"].every(cancellable)).toBe(true);
expect(["in_progress", "completed", "cancelled"].some(cancellable)).toBe(false);
});
});