<?php

declare(strict_types=1);

/**
 * DentalCare 0.6.1–0.6.8 role-flow QA and release-readiness helpers.
 *
 * This layer does not bypass permissions and does not create clinic data. It
 * gives the owner/admin an operational QA command center so a clinic cannot be
 * treated as sell-ready just because features exist.
 */

if (!function_exists('dentalcare_role_flow_qa_items')) {
    function dentalcare_role_flow_qa_items(): array
    {
        return [
            'reception_intake' => [
                'area' => 'Reception / Intake',
                'owner_role' => 'Receptionist',
                'blocker' => true,
                'test' => 'Book an appointment, approve an online request, check in a confirmed appointment, and check in a walk-in with no appointment selected.',
                'pass' => 'Each path lands in the correct patient/queue state without forcing the wrong form, jumping to service fields, or losing the selected patient.',
                'routes' => ['appointments.php','appointment_requests.php','queue.php','patients.php'],
            ],
            'clinical_chairside' => [
                'area' => 'Dentist chairside treatment',
                'owner_role' => 'Dentist',
                'blocker' => true,
                'test' => 'Start a seated patient, choose Assistant or None, review vitals/alerts, chart odontogram, diagnose, record procedure outcome, decide prescription, and complete visit.',
                'pass' => 'The Dentist can finish the whole visit by following the guided rail without guessing the next section or being scrolled away.',
                'routes' => ['doctor_treatment.php','clinical_workspace.php'],
            ],
            'assistant_support' => [
                'area' => 'Dental Assistant support',
                'owner_role' => 'Dental Assistant',
                'blocker' => true,
                'test' => 'Switch Active role to Dental Assistant, open only assigned encounters, capture vitals, upload X-ray/media, add support notes, and confirm that Create Walk-in, Add Supplier, inventory catalogue, payment, prescription, and finalization controls are absent.',
                'pass' => 'Assistant can support chairside work but cannot create a walk-in, manage suppliers/inventory, receive payment, or finalize Dentist-only diagnosis, procedure completion, or prescription.',
                'routes' => ['doctor_treatment.php','clinical_workspace.php'],
            ],
            'general_staff_combined' => [
                'area' => 'General Staff reception and cashier combination',
                'owner_role' => 'General Staff',
                'blocker' => true,
                'test' => 'Register or find a patient, schedule or check in an appointment/walk-in, manage the queue, open the signed-in user cashier session, receive a payment, print the receipt, complete checkout, and close the drawer.',
                'pass' => 'General Staff can complete Receptionist and Cashier duties under one identity, but cannot diagnose, prescribe, finalize encounters, approve discounts/refunds/waivers, manage inventory, manage users, or change protected setup.',
                'routes' => ['patients.php','appointments.php','queue.php','billing.php','print_center.php'],
            ],
            'cashier_pos' => [
                'area' => 'Cashier POS transaction',
                'owner_role' => 'Cashier',
                'blocker' => true,
                'test' => 'Open a cashier window, call a patient, lock the transaction, collect cash and digital payment with reference, print an itemized receipt, and complete/cancel safely.',
                'pass' => 'Patient switching and unrelated navigation are blocked while the transaction is active; payment method reference rules are enforced.',
                'routes' => ['billing.php'],
            ],
            'email_neutrality' => [
                'area' => 'Email / notification provider neutrality',
                'owner_role' => 'Admin / Owner',
                'blocker' => true,
                'test' => 'Configure Gmail API, domain SMTP, log-only mode, template preview, test delivery, and retry/log review using client-safe wording.',
                'pass' => 'No screen implies the system requires Z.com, cPanel, Gmail, or one host; diagnostics explain the selected provider only.',
                'routes' => ['email_settings.php','email_templates.php','system_setup.php'],
            ],
            'navigation_permissions' => [
                'area' => 'Role navigation and permissions',
                'owner_role' => 'Superuser / Admin',
                'blocker' => true,
                'test' => 'For every multi-role account, switch Active role one role at a time and click every visible sidebar/action link. Confirm permissions do not merge with the primary or other assigned roles.',
                'pass' => 'Only the selected role lane is visible and writable. No permission union, dead-end link, white screen, wrong sidebar group, or unrelated workflow remains.',
                'routes' => ['system_setup.php','permission_matrix.php','go_live_checklist.php'],
            ],
            'global_navigation_scroll' => [
                'area' => 'Global navigation and scroll behavior',
                'owner_role' => 'All roles',
                'blocker' => true,
                'test' => 'From the bottom of long forms/lists, click pagination, manage/view/close links, same-page tabs, filters, modal openers, clinical rails, and anchor links; then test sidebar links separately.',
                'pass' => 'Sidebar/main navigation opens the new page normally, while internal page controls preserve the current workspace or move only to the intended in-page target. No form, modal, tab, odontogram, or cashier action jumps unexpectedly to the top.',
                'routes' => ['appointments.php','patients.php','queue.php','clinical_workspace.php','billing.php','inventory.php','system_setup.php','go_live_checklist.php'],
            ],
            'queue_schedule_offline_guard' => [
                'area' => 'Queue cancellation, schedule sync, and offline guard',
                'owner_role' => 'Reception / Cashier / Admin',
                'blocker' => true,
                'test' => 'Check in a confirmed appointment, leave it waiting, cancel/mark left from Queue with a reason, then verify appointment/service segments close, active schedule view hides it by default, and Show cancelled/no-show/left displays it as non-blocking history. Repeat with browser offline simulation before a POST transaction.',
                'pass' => 'The waiting patient can be removed from Queue; the linked appointment is closed as left/cancelled, active schedule tiles and resource conflicts disappear automatically, terminal history remains reviewable with the calendar toggle, and offline cached-page submissions are blocked with manual-log guidance.',
                'routes' => ['queue.php','appointments.php','billing.php','go_live_checklist.php'],
            ],
            'calendar_history_click_targets' => [
                'area' => 'Calendar history and click-target governance',
                'owner_role' => 'Reception / Admin / Dentist',
                'blocker' => true,
                'test' => 'On Weekly and Monthly schedule, cancel/no-show/left an appointment and use Show cancelled/no-show/left. Then switch odontogram Permanent/Primary/Mixed, bottom filters, page controls, and manage/close links from mid-page and bottom-of-page positions.',
                'pass' => 'Cancelled/no-show/left appointments are visible only when requested, appear as non-blocking history, do not block chair/provider conflicts, and every internal click targets its workspace instead of page top.',
                'routes' => ['appointments.php','odontogram.php','clinical_workspace.php','queue.php','go_live_checklist.php'],
            ],
            'numbering_reset' => [
                'area' => 'Patient numbering and reset lifecycle',
                'owner_role' => 'Superuser',
                'blocker' => true,
                'test' => 'Create patients, verify PYYYY0001 sequence, run demo reset, clinic activity reset, and factory reset on staging/demo data.',
                'pass' => 'Full reset restarts the next visible live patient as PYYYY0001; demo reset does not reuse live numbers accidentally.',
                'routes' => ['data_tools.php','patients.php','system_setup.php'],
            ],
            'demo_reset_email_cache' => [
                'area' => 'Demo reset, email paths, and performance cache',
                'owner_role' => 'Superuser / Admin',
                'blocker' => true,
                'test' => 'Seed full demo clinic data, verify a demo patient such as DEMO-PAT-003 and its linked queue/request/billing samples exist, run Remove demo data, then search patient records, appointments, queue, invoices, payments, diagnostics, and calendar. Open Email Settings and verify Gmail API, Domain SMTP, PHP mail, Log only, Queue, and Diagnostics are divided into separate paths. Repeat heavy pages with several users in staging and confirm no repeated setting-read lag is visible.',
                'pass' => 'Demo reset removes the fictional patient and all linked transactions; Email Settings is no longer one long mixed form; request-level caching keeps setup/dashboard/settings pages responsive during simultaneous use.',
                'routes' => ['data_tools.php','demo_accounts.php','email_settings.php','system_setup.php','go_live_checklist.php'],
            ],
            'landing_page_control' => [
                'area' => 'Public landing page control',
                'owner_role' => 'Superuser / Admin',
                'blocker' => true,
                'test' => 'Edit every public landing page block: hero, clinic information card, appointment cards, dentist/team, services, care flow, contact, downloads, developer, portal buttons, visibility, order, and section images. Preview the public home page as a non-user.',
                'pass' => 'No public landing copy is stuck as hardcoded text; Admin/Superuser can change wording, hide/reorder sections, assign images, and preview without editing PHP files.',
                'routes' => ['settings.php','website_pages.php','website_media.php','branding.php','index.php'],
            ],
            'role_permission_navigation_hardening' => [
                'area' => 'Role permission and navigation hardening',
                'owner_role' => 'Superuser / Admin / All roles',
                'blocker' => true,
                'test' => 'Open Permission Matrix, review the v0.6.7 route audit, then log in or task-switch through Receptionist, Dentist, Dental Assistant, Cashier, Inventory Officer, Admin, and Superuser. Click every visible sidebar, context tab, quick action, row action, and form action each role can see. Also try direct URLs for pages the role should not use.',
                'pass' => 'Every visible control matches the role lane; no visible link lands on access denied/blank/wrong sidebar group; direct URL access is blocked or auto-aligned; cashier cannot access clinical/patient-edit areas; assistant cannot prescribe/finalize; Receptionist cannot post payments; General Staff can post payments only through its own cashier session; inventory cannot browse clinical patient records.',
                'routes' => ['permission_matrix.php','dashboard.php','patients.php','appointments.php','queue.php','clinical_workspace.php','billing.php','inventory.php','system_setup.php'],
            ],
            'update_chain_self_healing' => [
                'area' => 'Update chain self-healing',
                'owner_role' => 'Superuser',
                'blocker' => true,
                'test' => 'Install the package, click Apply all pending updates once, refresh, then reopen System Setup → Updates and review Skipped-update recovery.',
                'pass' => 'Installed version equals package release, Missing update chain is None detected, Terminal apply check is Complete, Apply function chain is Complete, and the update button no longer repeats without a real pending reason.',
                'routes' => ['system_setup.php'],
            ],
            'inventory_treatment_reconciliation' => [
                'area' => 'Inventory-to-treatment reconciliation',
                'owner_role' => 'Dentist / Assistant / Inventory Officer / Cashier',
                'blocker' => true,
                'test' => 'During a treatment encounter, add medicines/materials/equipment/diagnostic usage, confirm the treatment, then reconcile the matching inventory issue and billing handoff.',
                'pass' => 'Clinical usage lines are visible in the workspace, stock issue responsibility is clear, confirmed-but-unissued usage is flagged, and billing/receipt lines remain tied to the clinical source without silently deducting stock twice.',
                'routes' => ['clinical_workspace.php','inventory.php','billing.php','go_live_checklist.php'],
            ],
            'odontogram_usability' => [
                'area' => 'Odontogram usability',
                'owner_role' => 'Dentist',
                'blocker' => true,
                'test' => 'Click teeth/surfaces repeatedly in read/write and read-only modes, use keyboard tooth navigation, and save entries.',
                'pass' => 'No browser reload, no scroll-to-top, selected tooth remains visible, and clinical context is preserved.',
                'routes' => ['doctor_treatment.php','clinical_workspace.php'],
            ],
            'inventory_sterilization' => [
                'area' => 'Inventory / sterilization / supplies',
                'owner_role' => 'Assistant / Inventory',
                'blocker' => false,
                'test' => 'Issue materials for a procedure, record sterilization/instrument usage, chair turnover, and low-stock exception review.',
                'pass' => 'Clinical usage is traceable and stock/sterilization warnings are visible before release.',
                'routes' => ['inventory.php','sterilization.php','doctor_treatment.php'],
            ],
            'patient_portal' => [
                'area' => 'Patient portal / public flow',
                'owner_role' => 'Patient / Public',
                'blocker' => false,
                'test' => 'Submit a public appointment request, receive/confirm email, then review lifetime appointments, completed-care visit cards, itemized invoices, payment allocations, returns, prescriptions, follow-ups, and shared documents in the portal.',
                'pass' => 'The patient sees a complete paginated appointment-and-transaction history for authorized records, while draft clinical notes and staff-only data remain hidden.',
                'routes' => ['index.php','portal_requests.php','appointment_requests.php'],
            ],
        ];
    }
}

if (!function_exists('dentalcare_role_flow_qa_state')) {
    function dentalcare_role_flow_qa_state(): array
    {
        $raw = (string) setting_get('system', 'role_flow_qa_v061_state_json', '{}');
        $decoded = json_decode($raw, true);
        return is_array($decoded) ? $decoded : [];
    }
}

if (!function_exists('dentalcare_role_flow_qa_status')) {
    function dentalcare_role_flow_qa_status(string $key, ?array $state = null): array
    {
        $state ??= dentalcare_role_flow_qa_state();
        $row = $state[$key] ?? [];
        return is_array($row) ? $row : [];
    }
}

if (!function_exists('dentalcare_role_flow_qa_save')) {
    function dentalcare_role_flow_qa_save(string $key, string $status, string $note, int $userId): void
    {
        $items = dentalcare_role_flow_qa_items();
        if (!isset($items[$key])) {
            throw new RuntimeException('Unknown role-flow QA item.');
        }
        $allowed = ['untested','passed','failed','blocked'];
        if (!in_array($status, $allowed, true)) {
            throw new RuntimeException('Choose a valid QA status.');
        }
        if ($status !== 'passed' && mb_strlen(trim($note)) < 8) {
            throw new RuntimeException('Add a short note for failed, blocked, or untested QA items.');
        }
        $state = dentalcare_role_flow_qa_state();
        $state[$key] = [
            'status' => $status,
            'note' => mb_substr(trim($note), 0, 800),
            'updated_at' => date('Y-m-d H:i:s'),
            'updated_by' => $userId,
        ];
        setting_set('system', 'role_flow_qa_v061_state_json', json_encode($state, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES));
        setting_set('system', 'role_flow_qa_v061_last_reviewed_at', date('Y-m-d H:i:s'));
        setting_set('system', 'role_flow_qa_v061_last_reviewed_by', (string)$userId);
        audit('role_flow_qa_item_updated', 'system', null, ['item' => $key, 'status' => $status]);
    }
}

if (!function_exists('dentalcare_role_flow_qa_summary')) {
    function dentalcare_role_flow_qa_summary(): array
    {
        $items = dentalcare_role_flow_qa_items();
        $state = dentalcare_role_flow_qa_state();
        $total = count($items);
        $passed = 0;
        $failed = 0;
        $blocked = 0;
        $blockerTotal = 0;
        $blockerPassed = 0;
        $blockerOpen = [];
        foreach ($items as $key => $item) {
            $status = (string)($state[$key]['status'] ?? 'untested');
            if ($status === 'passed') $passed++;
            if ($status === 'failed') $failed++;
            if ($status === 'blocked') $blocked++;
            if (!empty($item['blocker'])) {
                $blockerTotal++;
                if ($status === 'passed') $blockerPassed++;
                else $blockerOpen[] = (string)$item['area'];
            }
        }
        $percent = $total ? (int)round(($passed / $total) * 100) : 0;
        $blockerPercent = $blockerTotal ? (int)round(($blockerPassed / $blockerTotal) * 100) : 0;
        $sellReady = $blockerTotal > 0 && $blockerPassed === $blockerTotal && $failed === 0 && $blocked === 0;
        return [
            'total' => $total,
            'passed' => $passed,
            'failed' => $failed,
            'blocked' => $blocked,
            'untested' => max(0, $total - $passed - $failed - $blocked),
            'percent' => $percent,
            'blocker_total' => $blockerTotal,
            'blocker_passed' => $blockerPassed,
            'blocker_percent' => $blockerPercent,
            'blocker_open' => $blockerOpen,
            'sell_ready' => $sellReady,
        ];
    }
}

if (!function_exists('dentalcare_route_file_exists')) {
    function dentalcare_route_file_exists(string $route): bool
    {
        $route = ltrim(str_replace(['..', '\\'], ['', '/'], $route), '/');
        if ($route === '') return false;
        $paths = [
            DENTAL_ROOT . '/' . $route,
            DENTAL_ROOT . '/modules/administration/' . $route,
            DENTAL_ROOT . '/modules/patient-care/' . $route,
            DENTAL_ROOT . '/modules/clinical/' . $route,
            DENTAL_ROOT . '/modules/finance/' . $route,
            DENTAL_ROOT . '/modules/inventory/' . $route,
            DENTAL_ROOT . '/modules/operations/' . $route,
            DENTAL_ROOT . '/modules/public/' . $route,
        ];
        foreach ($paths as $path) {
            if (is_file($path)) return true;
        }
        return false;
    }
}

if (!function_exists('dentalcare_role_flow_route_audit')) {
    function dentalcare_role_flow_route_audit(): array
    {
        $routes = [];
        foreach (dentalcare_role_flow_qa_items() as $item) {
            foreach ((array)($item['routes'] ?? []) as $route) {
                $routes[$route] = true;
            }
        }
        $rows = [];
        foreach (array_keys($routes) as $route) {
            $rows[] = ['route' => $route, 'exists' => dentalcare_route_file_exists($route)];
        }
        usort($rows, static fn(array $a, array $b): int => strcmp((string)$a['route'], (string)$b['route']));
        return $rows;
    }
}

if (!function_exists('dentalcare_render_role_flow_qa_center')) {
    function dentalcare_render_role_flow_qa_center(bool $allowWrite = false): void
    {
        if (function_exists('dental_client_testing_wording_hidden') && dental_client_testing_wording_hidden()) {
            ?><section class="card"><div class="card-head"><div><span class="eyebrow">Production mode</span><h2>Internal role-flow QA is hidden</h2><p>This enterprise copy is in Production mode. Turn on internal QA tools from Administration only when troubleshooting with the owner.</p></div><span class="badge success">Client clean</span></div></section><?php
            return;
        }
        $items = dentalcare_role_flow_qa_items();
        $state = dentalcare_role_flow_qa_state();
        $summary = dentalcare_role_flow_qa_summary();
        $routeAudit = dentalcare_role_flow_route_audit();
        $missingRoutes = array_values(array_filter($routeAudit, static fn(array $row): bool => empty($row['exists'])));
        $canWrite = $allowWrite && function_exists('role_context_has') && (role_context_has('super_admin') || role_context_has('clinic_admin'));
        ?>
        <section class="card role-flow-qa-center" aria-label="Role-flow QA command center">
            <div class="card-head">
                <div><span class="eyebrow">0.6.4 QA enforcement</span><h2>Role-flow QA command center</h2><p>Use this as the real sell-readiness gate. A feature is not considered ready until the responsible role completes the browser test and marks it passed. v0.6.4 adds calendar history visibility and click-target governance checks on top of the global navigation/scroll behavior test.</p></div>
                <span class="badge <?= !empty($summary['sell_ready']) ? 'success' : 'warning' ?>"><?= !empty($summary['sell_ready']) ? 'Sell-ready gate passed' : 'Pilot-only gate' ?></span>
            </div>
            <section class="stats mini-stats role-flow-qa-stats">
                <?php if (function_exists('stat_card')): ?>
                    <?php stat_card('QA passed', (string)$summary['passed'] . ' / ' . (string)$summary['total'], (string)$summary['percent'] . '% overall'); ?>
                    <?php stat_card('Blockers passed', (string)$summary['blocker_passed'] . ' / ' . (string)$summary['blocker_total'], (string)$summary['blocker_percent'] . '% release blockers'); ?>
                    <?php stat_card('Failed / blocked', (string)((int)$summary['failed'] + (int)$summary['blocked']), 'Must be zero before sale'); ?>
                <?php else: ?>
                    <div><strong><?= e((string)$summary['passed']) ?> / <?= e((string)$summary['total']) ?></strong><span>QA passed</span></div>
                    <div><strong><?= e((string)$summary['blocker_passed']) ?> / <?= e((string)$summary['blocker_total']) ?></strong><span>Blockers passed</span></div>
                    <div><strong><?= e((string)((int)$summary['failed'] + (int)$summary['blocked'])) ?></strong><span>Failed / blocked</span></div>
                <?php endif; ?>
            </section>
            <?php if (!empty($summary['blocker_open'])): ?><div class="alert warning"><strong>Still pilot-only.</strong> Open release blockers: <?= e(implode(', ', (array)$summary['blocker_open'])) ?>.</div><?php endif; ?>
            <?php if ($missingRoutes): ?><div class="alert warning"><strong>Route audit warning.</strong> These expected screens were not found in the package path scan: <?= e(implode(', ', array_map(static fn(array $row): string => (string)$row['route'], $missingRoutes))) ?>.</div><?php endif; ?>
            <div class="role-flow-qa-grid">
                <?php foreach ($items as $key => $item): $row = dentalcare_role_flow_qa_status((string)$key, $state); $status = (string)($row['status'] ?? 'untested'); ?>
                <article class="role-flow-qa-item status-<?= e($status) ?>">
                    <div class="role-flow-qa-item-head"><span class="badge <?= $status === 'passed' ? 'success' : ($status === 'failed' ? 'danger' : ($status === 'blocked' ? 'warning' : '')) ?>"><?= e(ucwords(str_replace('_',' ', $status))) ?></span><?php if (!empty($item['blocker'])): ?><span class="badge warning">Release blocker</span><?php endif; ?></div>
                    <h3><?= e((string)$item['area']) ?></h3>
                    <p><strong>Role:</strong> <?= e((string)$item['owner_role']) ?></p>
                    <p><strong>Test:</strong> <?= e((string)$item['test']) ?></p>
                    <p><strong>Pass:</strong> <?= e((string)$item['pass']) ?></p>
                    <?php if (!empty($row['note'])): ?><p class="qa-note"><strong>Note:</strong> <?= e((string)$row['note']) ?></p><?php endif; ?>
                    <?php if ($canWrite): ?>
                    <form method="post" class="role-flow-qa-form">
                        <input type="hidden" name="csrf" value="<?= e(csrf_token()) ?>">
                        <input type="hidden" name="action" value="save_v061_role_qa_item">
                        <input type="hidden" name="qa_key" value="<?= e((string)$key) ?>">
                        <label>Status<select name="qa_status" required><option value="untested" <?= $status==='untested'?'selected':'' ?>>Untested</option><option value="passed" <?= $status==='passed'?'selected':'' ?>>Passed</option><option value="failed" <?= $status==='failed'?'selected':'' ?>>Failed</option><option value="blocked" <?= $status==='blocked'?'selected':'' ?>>Blocked</option></select></label>
                        <label class="wide">QA note<textarea name="qa_note" rows="2" placeholder="Browser, role used, issue found, or sign-off note"><?= e((string)($row['note'] ?? '')) ?></textarea></label>
                        <button class="btn ghost compact" type="submit">Save QA result</button>
                    </form>
                    <?php endif; ?>
                </article>
                <?php endforeach; ?>
            </div>
        </section>
        <?php
    }
}
