/* eslint-disable */
/**
 * Feedback Modules Audit (exploratory, run-to-completion)
 *
 * Exercises the full lifecycle of the 5 patient-feedback modules:
 *   Complaint, Inquiry, Observation, Suggestion, Appreciation
 * across multiple roles in the isolated E2E hospital.
 *
 * Style: every step is guarded (try/catch + .count()); failures are recorded as
 * observations via expect.soft + observe(), so the suite runs to completion and
 * collects ALL issues instead of aborting at the first one.
 *
 * Run headed:
 *   E2E_SLOWMO=120 npx playwright test --headed --project chromium feedback-modules-audit --workers=1
 */
import { test, expect } from '@playwright/test';
import * as fs from 'fs';
import * as path from 'path';
import {
  attachObservers,
  bodyHasTraceback,
  getE2EHospitalId,
  loginAndScope,
  observe,
  selectE2EHospital,
  OBS,
  E2E_HOSPITAL_NAME,
  BASE_URL,
} from '../../helpers/audit';
import { RoleAuthHelper, RoleName } from '../../helpers/helpers';

const DEEP_ROLES: RoleName[] = ['px_admin', 'hospital_admin', 'dept_manager', 'px_employee', 'staff'];
const ALL_ROLES: RoleName[] = [
  'px_admin', 'hospital_admin', 'dept_manager', 'px_employee',
  'physician', 'nurse', 'staff', 'viewer', 'source_user', 'champion',
];

const MODULE_ROOTS = [
  { module: 'Complaint', path: '/complaints/' },
  { module: 'Inquiry', path: '/inquiries/' },
  { module: 'Observation', path: '/observations/' },
  { module: 'Suggestion', path: '/suggestions/' },
  { module: 'Appreciation', path: '/appreciation/' },
];

const TS = () => Date.now();
const suffix = ` (E2E audit ${TS()})`;

// ============================================================================
// 1. COMPLAINT
// ============================================================================
test.describe('Complaint flow', () => {
  test.describe.configure({ mode: 'serial' });
  const M = 'Complaint';
  let reference = '';

  test('public submit + reference + success', async ({ page }) => {
    attachObservers(page, M, 'anonymous');
    try {
      await page.goto('/complaints/public/submit/');
      await page.waitForSelector('#public_complaint_form, form', { timeout: 15000 });
      const ts = TS();
      const f = async (sel: string, val: string) => { const l = page.locator(sel).first(); if (await l.count()) await l.fill(val).catch(() => {}); };
      const selIdx = async (sel: string, idx: number) => { const l = page.locator(sel).first(); if (await l.count()) await l.selectOption({ index: idx }).catch(() => {}); };
      await f('input[name="complainant_name"]', `E2E Audit ${ts}`);
      await selIdx('select[name="relation_to_patient"]', 1);
      await f('input[name="email"]', `e2e-complaint-${ts}@test.com`);
      await f('input[name="mobile_number"]', '0551234567');
      await f('input[name="patient_name"]', `E2E Patient ${ts}`);
      await f('input[name="national_id"]', `E2E${ts}`);
      await f('input[name="incident_date"], #id_incident_date', '2026-01-15');
      // hospital = E2E-HOSP specifically (avoid polluting real hospitals)
      await selectE2EHospital(page, 'select[name="hospital"], #id_hospital');
      await page.waitForTimeout(600);
      await selIdx('select[name="location_type"], #id_location_type', 1);
      await selIdx('select[name="area"], #id_area', 1);
      await page.waitForTimeout(800); // department may load via AJAX after area
      await selIdx('select[name="category"], #id_category', 1);
      await selIdx('select[name="department"], #id_department', 1);
      await f('textarea[name="complaint_details"]', `E2E audit complaint ${ts}. Automated - please ignore.`);
      await page.click('#submit_btn');
      await page.waitForLoadState('networkidle', { timeout: 8000 }).catch(() => {});
      await page.waitForTimeout(2500);
      const tb = await bodyHasTraceback(page);
      if (tb) return observe(M, 'public-submit', 'FAIL', `traceback after submit: ${tb}`, { role: 'anonymous' });
      const body = (await page.textContent('body')) || '';
      const refSource = `${body} ${page.url()}`;
      const match = refSource.match(/CMP-[A-Z0-9-]{6,}/);
      reference = match ? match[0] : '';
      if (match || /thank|success|received|submitted/i.test(body)) {
        observe(M, 'public-submit', 'PASS', `submitted${reference ? `, ref=${reference}` : ', no CMP ref captured'}`, {
          role: 'anonymous',
          url: page.url(),
        });
      } else {
        observe(M, 'public-submit', 'FAIL', `no success indicator. body head: ${body.slice(0, 200)}`, {
          role: 'anonymous',
          url: page.url(),
        });
      }
      expect.soft(reference || 'ok').toBeTruthy();
    } catch (e) {
      observe(M, 'public-submit', 'FAIL', `exception: ${(e as Error).message}`, { role: 'anonymous' });
    }
  });

  test('public track by reference', async ({ page }) => {
    attachObservers(page, M, 'anonymous');
    try {
      await page.goto('/complaints/public/track/');
      await page.waitForLoadState('domcontentloaded');
      const input = page.locator('input[name="reference_number"]');
      if ((await input.count()) === 0) return observe(M, 'public-track', 'WARN', 'track input not found', { role: 'anonymous' });
      await input.fill(reference || 'CMP-00000000-000000');
      await page.click('button[type="submit"]').catch(() => {});
      await page.waitForLoadState('networkidle', { timeout: 8000 }).catch(() => {});
      await page.waitForTimeout(1500);
      const tb = await bodyHasTraceback(page);
      const body = (await page.textContent('body')) || '';
      const ok = reference ? body.includes(reference) : true;
      observe(M, 'public-track', tb ? 'FAIL' : ok ? 'PASS' : 'WARN',
        tb ? `traceback: ${tb}` : `track response${reference ? ` includes ${reference}` : ' (no ref, used dummy)'}; head: ${body.slice(0, 120)}`,
        { role: 'anonymous', url: page.url() });
    } catch (e) {
      observe(M, 'public-track', 'FAIL', `exception: ${(e as Error).message}`, { role: 'anonymous' });
    }
  });

  test('list/search/filter across deep roles', async ({ page }) => {
    for (const role of DEEP_ROLES) {
      attachObservers(page, M, role);
      try {
        await loginAndScope(page, role, M);
        await page.goto('/complaints/');
        await page.waitForLoadState('networkidle', { timeout: 8000 }).catch(() => {});
        await page.waitForTimeout(1500);
        const tb = await bodyHasTraceback(page);
        const onLogin = page.url().includes('login');
        const table = page.locator('table');
        const hasTable = await table.count().then((c) => c > 0);
        const search = page.locator('#searchInput, input[name="search"]').first();
        if (await search.count()) {
          await search.fill(reference || 'CMP-');
          await search.press('Enter').catch(() => {});
          await page.waitForLoadState('networkidle', { timeout: 8000 }).catch(() => {});
          await page.waitForTimeout(1000);
        }
        const badges = await page.locator('span.rounded-full, .badge, span[class*="bg-"]').count();
        observe(M, 'list', tb ? 'FAIL' : onLogin ? 'WARN' : 'PASS',
          tb ? `traceback: ${tb}` : `${role}: ${onLogin ? 'redirected to login' : `table=${hasTable}, badges=${badges}`}`,
          { role, url: page.url() });
        await page.goto('/accounts/logout/').catch(() => {});
      } catch (e) {
        observe(M, 'list', 'FAIL', `${role}: exception: ${(e as Error).message}`, { role });
        await page.goto('/accounts/logout/').catch(() => {});
      }
    }
  });

  test('detail open + activate + note + status (workflow)', async ({ page }) => {
    attachObservers(page, M, 'px_employee');
    try {
      await loginAndScope(page, 'px_employee', M);
      await page.goto('/complaints/');
      await page.waitForLoadState('networkidle', { timeout: 8000 }).catch(() => {});
      await page.waitForTimeout(1500);
      const row = page.locator('table tbody tr').first();
      if (!(await row.count())) return observe(M, 'workflow', 'SKIP', 'no complaint rows to open', { role: 'px_employee' });
      const link = row.locator('a[href*="complaint_detail"], a[href*="/complaints/"]').first();
      await (await link.count() ? link : row).click();
      await page.waitForLoadState('networkidle', { timeout: 8000 }).catch(() => {});
      await page.waitForTimeout(1500);
      const tbDetail = await bodyHasTraceback(page);
      observe(M, 'detail', tbDetail ? 'FAIL' : 'PASS', tbDetail ? `traceback: ${tbDetail}` : 'detail opened', { role: 'px_employee', url: page.url() });

      // activate (self-assign)
      const activate = page.locator('form[action*="complaint_activate"] button[type="submit"], button:has-text("Activate")');
      if (await activate.count()) {
        await activate.first().click().catch(() => {});
        await page.waitForLoadState('networkidle', { timeout: 8000 }).catch(() => {});
        await page.waitForTimeout(1200);
        const body = (await page.textContent('body')) || '';
        observe(M, 'activate', /in_progress|InProgress/i.test(body) ? 'PASS' : 'WARN',
          `activate done; status text present: ${/in_progress|InProgress/i.test(body)}`, { role: 'px_employee' });
      } else observe(M, 'activate', 'SKIP', 'no activate button', { role: 'px_employee' });

      // note
      const followUp = page.locator('button[onclick="showFollowUpModal()"], button:has-text("Follow"), button:has-text("Note")');
      if (await followUp.count()) {
        await followUp.first().click().catch(() => {});
        const modal = page.locator('#followUpModal, .modal').first();
        await modal.waitFor({ state: 'visible', timeout: 4000 }).catch(() => {});
        const ta = page.locator('#followUpModal textarea[name="note"], textarea[name="note"]').first();
        if (await ta.count()) {
          await ta.fill(`E2E audit note ${TS()}`);
          await page.locator('#followUpModal button[type="submit"], .modal button[type="submit"]').first().click().catch(() => {});
          await page.waitForLoadState('networkidle', { timeout: 8000 }).catch(() => {});
          await page.waitForTimeout(1500);
          observe(M, 'add-note', 'PASS', 'note submitted', { role: 'px_employee' });
        } else observe(M, 'add-note', 'SKIP', 'note textarea missing', { role: 'px_employee' });
      } else observe(M, 'add-note', 'SKIP', 'no follow-up button', { role: 'px_employee' });
      await page.goto('/accounts/logout/').catch(() => {});
    } catch (e) {
      observe(M, 'workflow', 'FAIL', `exception: ${(e as Error).message}`, { role: 'px_employee' });
    }
  });

  test('auth create form reachable', async ({ page }) => {
    attachObservers(page, M, 'hospital_admin');
    try {
      await loginAndScope(page, 'hospital_admin', M);
      await page.goto('/complaints/new/');
      await page.waitForLoadState('networkidle', { timeout: 8000 }).catch(() => {});
      await page.waitForTimeout(1500);
      const tb = await bodyHasTraceback(page);
      const form = await page.locator('#complaintForm, form[action*="complaint_create"], form').count();
      observe(M, 'auth-create', tb ? 'FAIL' : form ? 'PASS' : 'WARN', tb ? `traceback: ${tb}` : `create form present: ${form > 0}`, { role: 'hospital_admin', url: page.url() });
      await page.goto('/accounts/logout/').catch(() => {});
    } catch (e) {
      observe(M, 'auth-create', 'FAIL', `exception: ${(e as Error).message}`, { role: 'hospital_admin' });
    }
  });

  test('CSV export', async ({ page }) => {
    attachObservers(page, M, 'hospital_admin');
    try {
      await loginAndScope(page, 'hospital_admin', M);
      const resp = await page.context().request.get(`${BASE_URL}/complaints/export/csv/`);
      const ok = resp.status() === 200;
      const ct = resp.headers()['content-type'] || '';
      const text = await resp.text().catch(() => '');
      observe(M, 'csv-export', ok && ct.includes('csv') ? 'PASS' : 'FAIL',
        `status=${resp.status()} ct=${ct} lines=${text.trim().split('\n').length}`, { role: 'hospital_admin' });
      await page.goto('/accounts/logout/').catch(() => {});
    } catch (e) {
      observe(M, 'csv-export', 'FAIL', `exception: ${(e as Error).message}`, { role: 'hospital_admin' });
    }
  });
});

// ============================================================================
// 2. INQUIRY
// ============================================================================
test.describe('Inquiry flow', () => {
  test.describe.configure({ mode: 'serial' });
  const M = 'Inquiry';
  let reference = '';

  test('public submit + reference', async ({ page }) => {
    attachObservers(page, M, 'anonymous');
    try {
      await page.goto('/inquiries/public/submit/');
      await page.waitForLoadState('domcontentloaded');
      const ts = TS();
      const fill = async (sel: string, val: string) => {
        const l = page.locator(sel).first();
        if (await l.count()) await l.fill(val).catch(() => {});
      };
      const selectIdx = async (sel: string, idx: number) => {
        const l = page.locator(sel).first();
        if (await l.count()) await l.selectOption({ index: idx }).catch(() => {});
      };
      await fill('input[name="name"], #inquiry_name', `E2E Inquiry ${ts}`);
      await fill('input[name="email"], #inquiry_email', `e2e-inq-${ts}@test.com`);
      await fill('input[name="phone"], #inquiry_phone', '0559876543');
      await selectE2EHospital(page, 'select[name="hospital"], #inquiry_hospital');
      await page.waitForTimeout(600);
      await selectIdx('select[name="location_type"], #inquiry_location_type', 1);
      await selectIdx('select[name="area"], #inquiry_area', 1);
      await page.waitForTimeout(800);
      await selectIdx('select[name="category"], #inquiry_category', 1);
      await selectIdx('select[name="department"], #inquiry_department', 1);
      await fill('input[name="subject"], #inquiry_subject', `E2E Test Inquiry ${ts}`);
      await fill('textarea[name="message"], #inquiry_message', 'E2E audit inquiry - please ignore');
      await page.locator('button[type="submit"], #inquirySubmitBtn').first().click().catch(() => {});
      await page.waitForLoadState('networkidle', { timeout: 8000 }).catch(() => {});
      await page.waitForTimeout(3000);
      const tb = await bodyHasTraceback(page);
      const body = (await page.textContent('body')) || '';
      const m = body.match(/INQ-\w{4,}/);
      reference = m ? m[0] : '';
      observe(M, 'public-submit', tb ? 'FAIL' : m || /success|thank|received/i.test(body) ? 'PASS' : 'WARN',
        tb ? `traceback: ${tb}` : `submitted${reference ? ` ref=${reference}` : ' (no INQ ref)'}`, { role: 'anonymous', url: page.url() });
    } catch (e) {
      observe(M, 'public-submit', 'FAIL', `exception: ${(e as Error).message}`, { role: 'anonymous' });
    }
  });

  test('list/detail/respond across deep roles', async ({ page }) => {
    for (const role of DEEP_ROLES) {
      attachObservers(page, M, role);
      try {
        await loginAndScope(page, role, M);
        await page.goto('/inquiries/');
        await page.waitForLoadState('networkidle', { timeout: 8000 }).catch(() => {});
        await page.waitForTimeout(1500);
        const tb = await bodyHasTraceback(page);
        const table = await page.locator('table').count().then((c) => c > 0);
        observe(M, 'list', tb ? 'FAIL' : 'PASS', tb ? `traceback: ${tb}` : `${role}: table=${table}`, { role, url: page.url() });

        // open first row
        const row = page.locator('table tbody tr').first();
        if (await row.count()) {
          const link = row.locator('a').first();
          await (await link.count() ? link : row).click().catch(() => {});
          await page.waitForLoadState('networkidle', { timeout: 8000 }).catch(() => {});
          await page.waitForTimeout(1200);
          const tbD = await bodyHasTraceback(page);
          observe(M, 'detail', tbD ? 'FAIL' : 'PASS', tbD ? `traceback: ${tbD}` : `${role}: detail opened`, { role, url: page.url() });

          // respond modal
          const respond = page.locator('button[onclick="showRespondModal()"], button:has-text("Respond")');
          if (await respond.count()) {
            await respond.first().click().catch(() => {});
            const modal = page.locator('#respondModal, .modal').first();
            await modal.waitFor({ state: 'visible', timeout: 4000 }).catch(() => {});
            const vis = await modal.isVisible().catch(() => false);
            observe(M, 'respond-modal', vis ? 'PASS' : 'WARN', `${role}: respond modal visible=${vis}`, { role });
          } else observe(M, 'respond-modal', 'SKIP', `${role}: no respond button`, { role });
        } else observe(M, 'detail', 'SKIP', `${role}: no rows`, { role });
        await page.goto('/accounts/logout/').catch(() => {});
      } catch (e) {
        observe(M, 'list', 'FAIL', `${role}: exception: ${(e as Error).message}`, { role });
        await page.goto('/accounts/logout/').catch(() => {});
      }
    }
  });
});

// ============================================================================
// 3. OBSERVATION
// ============================================================================
test.describe('Observation flow', () => {
  test.describe.configure({ mode: 'serial' });
  const M = 'Observation';
  let trackingCode = '';

  test('public submit + tracking code', async ({ page }) => {
    attachObservers(page, M, 'anonymous');
    try {
      // 1. UI form renders
      await page.goto('/observations/new/', { timeout: 15000 });
      await page.waitForLoadState('domcontentloaded');
      const formVisible = await page.locator('form').count().then((c) => c > 0);
      observe(M, 'public-form-render', formVisible ? 'PASS' : 'FAIL', `observation public form rendered: ${formVisible}`, { role: 'anonymous' });

      // 2. Direct endpoint POST (the UI cascade hospital->area->dept is JS-heavy
      //    and finicky; we test the submission endpoint directly for reliability,
      //    exactly as a real form POST would do). Use page.context().request so
      //    the csrftoken cookie is sent (required by Django CSRF).
      const hospitalId = await getE2EHospitalId(page);
      const csrf = await page.context().cookies().then((c) => c.find((x) => x.name === 'csrftoken')?.value || '');
      const ts = TS();
      const resp = await page.context().request.post(`${BASE_URL}/observations/new/`, {
        maxRedirects: 0,
        headers: { Referer: `${BASE_URL}/observations/new/`, ...(csrf ? { 'X-CSRFToken': csrf } : {}) },
        multipart: {
          csrfmiddlewaretoken: csrf,
          hospital: hospitalId,
          location_type: 'OP',
          description: `E2E audit observation ${ts} - please ignore`,
          location_text: 'E2E Area',
          incident_datetime: '2026-06-13 12:00:00',
          reporter_name: `E2E Reporter ${ts}`,
          reporter_phone: '0550000000',
        },
      });
      const status = resp.status();
      const loc = resp.headers()['location'] || '';
      const codeMatch = loc.match(/submitted\/([A-Z0-9-]+)/i);
      trackingCode = codeMatch ? codeMatch[1] : '';
      observe(M, 'public-submit', status === 302 || status === 301 ? 'PASS' : status === 200 ? 'WARN' : 'FAIL',
        `POST status=${status} location=${loc || '-'} trackingCode=${trackingCode || '-'} (200=form re-rendered/invalid)`,
        { role: 'anonymous', http: status, url: loc });
      expect.soft(status).toBeLessThan(500);
    } catch (e) {
      observe(M, 'public-submit', 'FAIL', `exception: ${(e as Error).message}`, { role: 'anonymous' });
    }
  });

  test('public track', async ({ page }) => {
    attachObservers(page, M, 'anonymous');
    try {
      await page.goto('/observations/track/');
      await page.waitForLoadState('domcontentloaded');
      const input = page.locator('input[name="tracking_code"], input[type="text"]').first();
      if (await input.count()) {
        await input.fill(trackingCode || 'OBS-DUMMY');
        await page.locator('button[type="submit"]').first().click().catch(() => {});
        await page.waitForLoadState('networkidle', { timeout: 8000 }).catch(() => {});
        await page.waitForTimeout(1500);
      }
      const tb = await bodyHasTraceback(page);
      observe(M, 'public-track', tb ? 'FAIL' : 'PASS', tb ? `traceback: ${tb}` : 'track page responded', { role: 'anonymous', url: page.url() });
    } catch (e) {
      observe(M, 'public-track', 'FAIL', `exception: ${(e as Error).message}`, { role: 'anonymous' });
    }
  });

  test('list/detail/triage/status/note across deep roles', async ({ page }) => {
    for (const role of DEEP_ROLES) {
      attachObservers(page, M, role);
      try {
        await loginAndScope(page, role, M);
        await page.goto('/observations/');
        await page.waitForLoadState('networkidle', { timeout: 8000 }).catch(() => {});
        await page.waitForTimeout(1500);
        const tb = await bodyHasTraceback(page);
        const table = await page.locator('table').count().then((c) => c > 0);
        observe(M, 'list', tb ? 'FAIL' : 'PASS', tb ? `traceback: ${tb}` : `${role}: table=${table}`, { role, url: page.url() });

        const row = page.locator('table tbody tr').first();
        if (await row.count()) {
          const link = row.locator('a').first();
          await (await link.count() ? link : row).click().catch(() => {});
          await page.waitForLoadState('networkidle', { timeout: 8000 }).catch(() => {});
          await page.waitForTimeout(1200);
          const tbD = await bodyHasTraceback(page);
          observe(M, 'detail', tbD ? 'FAIL' : 'PASS', tbD ? `traceback: ${tbD}` : `${role}: detail opened`, { role, url: page.url() });
        } else observe(M, 'detail', 'SKIP', `${role}: no rows`, { role });
        await page.goto('/accounts/logout/').catch(() => {});
      } catch (e) {
        observe(M, 'list', 'FAIL', `${role}: exception: ${(e as Error).message}`, { role });
        await page.goto('/accounts/logout/').catch(() => {});
      }
    }
  });
});

// ============================================================================
// 4. SUGGESTION (feedback app)
// ============================================================================
test.describe('Suggestion flow', () => {
  test.describe.configure({ mode: 'serial' });
  const M = 'Suggestion';

  test('public endpoint behaviour (GET should be 405 = POST-only)', async ({ request }) => {
    try {
      const get = await request.get(`${BASE_URL}/suggestions/public/suggestion/`);
      const post = await request.post(`${BASE_URL}/suggestions/public/suggestion/`, { data: {} });
      observe(M, 'public-endpoint', 'INFO',
        `GET=${get.status()} (405 expected = POST-only), POST(no data)=${post.status()}`,
        { role: 'anonymous' });
      // 405 on GET means there is NO public GET form — a UX finding
      if (get.status() === 405) observe(M, 'public-form', 'WARN', 'No public GET form for suggestions (POST-only endpoint)', { role: 'anonymous' });
    } catch (e) {
      observe(M, 'public-endpoint', 'FAIL', `exception: ${(e as Error).message}`, { role: 'anonymous' });
    }
  });

  test('auth create (feedback_type=suggestion)', async ({ page }) => {
    attachObservers(page, M, 'hospital_admin');
    try {
      await loginAndScope(page, 'hospital_admin', M);
      await page.goto('/suggestions/create/');
      await page.waitForLoadState('networkidle', { timeout: 8000 }).catch(() => {});
      await page.waitForTimeout(1500);
      const tb = await bodyHasTraceback(page);
      const ts = TS();
      const fill = async (sel: string, val: string) => {
        const l = page.locator(sel).first();
        if (await l.count()) await l.fill(val).catch(() => {});
      };
      const selectIdx = async (sel: string, idx: number) => {
        const l = page.locator(sel).first();
        if (await l.count()) await l.selectOption({ index: idx }).catch(() => {});
      };
      // set feedback_type to suggestion if present
      const ft = page.locator('select[name="feedback_type"], #id_feedback_type').first();
      if (await ft.count()) {
        await ft.selectOption({ value: 'suggestion' }).catch(() => {});
      }
      await fill('input[name="contact_name"], #id_contact_name', `E2E Suggester ${ts}`);
      await selectE2EHospital(page, 'select[name="hospital"], #id_hospital');
      await page.waitForTimeout(500);
      await selectIdx('select[name="category"], #id_category', 1);
      await fill('input[name="title"], #id_title', `E2E Suggestion ${ts}`);
      await fill('textarea[name="message"], #id_message', `E2E audit suggestion ${ts} - please ignore`);
      await page.locator('button[type="submit"], input[type="submit"]').first().click().catch(() => {});
      await page.waitForLoadState('networkidle', { timeout: 8000 }).catch(() => {});
      await page.waitForTimeout(2500);
      const tb2 = await bodyHasTraceback(page);
      const body = (await page.textContent('body')) || '';
      const ok = /success|thank|created|received|submitted|saved/i.test(body) || (await page.locator('table').count()) > 0;
      observe(M, 'auth-create', tb || tb2 ? 'FAIL' : ok ? 'PASS' : 'WARN',
        tb || tb2 ? `traceback: ${tb || tb2}` : `create submitted; success indicator: ${ok}; head: ${body.slice(0, 140)}`,
        { role: 'hospital_admin', url: page.url() });
      await page.goto('/accounts/logout/').catch(() => {});
    } catch (e) {
      observe(M, 'auth-create', 'FAIL', `exception: ${(e as Error).message}`, { role: 'hospital_admin' });
    }
  });

  test('list/detail across deep roles', async ({ page }) => {
    for (const role of DEEP_ROLES) {
      attachObservers(page, M, role);
      try {
        await loginAndScope(page, role, M);
        await page.goto('/suggestions/');
        await page.waitForLoadState('networkidle', { timeout: 8000 }).catch(() => {});
        await page.waitForTimeout(1500);
        const tb = await bodyHasTraceback(page);
        const table = await page.locator('table').count().then((c) => c > 0);
        observe(M, 'list', tb ? 'FAIL' : 'PASS', tb ? `traceback: ${tb}` : `${role}: table=${table}`, { role, url: page.url() });
        await page.goto('/accounts/logout/').catch(() => {});
      } catch (e) {
        observe(M, 'list', 'FAIL', `${role}: exception: ${(e as Error).message}`, { role });
        await page.goto('/accounts/logout/').catch(() => {});
      }
    }
  });
});

// ============================================================================
// 5. APPRECIATION
// ============================================================================
test.describe('Appreciation flow', () => {
  test.describe.configure({ mode: 'serial' });
  const M = 'Appreciation';
  let draftPk = '';

  test('public submit creates DRAFT (JSON POST)', async ({ page, request }) => {
    attachObservers(page, M, 'anonymous');
    try {
      const hospitalId = await getE2EHospitalId(page);
      // fetch a CSRF cookie
      await page.goto('/core/public/submit/').catch(() => {});
      const csrf = await page.context().cookies().then((c) => c.find((x) => x.name === 'csrftoken')?.value || '');
      const resp = await request.post(`${BASE_URL}/appreciation/public/submit/`, {
        headers: { 'Content-Type': 'application/json', ...(csrf ? { 'X-CSRFToken': csrf, 'X-Requested-With': 'XMLHttpRequest' } : {}) },
        data: {
          contact_name: `E2E Public ${TS()}`,
          contact_phone: '0551234567',
          message: `E2E audit public appreciation ${TS()} - please ignore`,
          hospital: hospitalId,
          staff_name: 'E2E Staff',
        },
      });
      const body = await resp.json().catch(() => ({}));
      draftPk = body.reference ? body.reference.replace('APR-', '') : '';
      observe(M, 'public-submit', resp.status() === 200 && body.success ? 'PASS' : 'FAIL',
        `status=${resp.status()} success=${body.success} ref=${body.reference || '-'} msg=${body.message || '-'} csrf=${csrf ? 'yes' : 'no'}`,
        { role: 'anonymous', http: resp.status() });
    } catch (e) {
      observe(M, 'public-submit', 'FAIL', `exception: ${(e as Error).message}`, { role: 'anonymous' });
    }
  });

  test('list/detail/activate/send/acknowledge workflow', async ({ page }) => {
    attachObservers(page, M, 'hospital_admin');
    try {
      await loginAndScope(page, 'hospital_admin', M);
      await page.goto('/appreciation/');
      await page.waitForLoadState('networkidle', { timeout: 8000 }).catch(() => {});
      await page.waitForTimeout(1500);
      const tb = await bodyHasTraceback(page);
      const table = await page.locator('table').count().then((c) => c > 0);
      observe(M, 'list', tb ? 'FAIL' : 'PASS', tb ? `traceback: ${tb}` : `table=${table}`, { role: 'hospital_admin', url: page.url() });

      // open first draft row
      const row = page.locator('table tbody tr').first();
      if (await row.count()) {
        const link = row.locator('a[href*="/appreciation/detail/"], a').first();
        await (await link.count() ? link : row).click().catch(() => {});
        await page.waitForLoadState('networkidle', { timeout: 8000 }).catch(() => {});
        await page.waitForTimeout(1500);
        const tbD = await bodyHasTraceback(page);
        observe(M, 'detail', tbD ? 'FAIL' : 'PASS', tbD ? `traceback: ${tbD}` : 'detail opened', { role: 'hospital_admin', url: page.url() });

        // activate (select first staff option then activate form)
        const staffSel = page.locator('select[name="staff"], select#staff').first();
        if (await staffSel.count()) await staffSel.selectOption({ index: 1 }).catch(() => {});
        const activate = page.locator('form[action*="activate"] button[type="submit"], button:has-text("Activate")');
        if (await activate.count()) {
          await activate.first().click().catch(() => {});
          await page.waitForLoadState('networkidle', { timeout: 8000 }).catch(() => {});
          await page.waitForTimeout(2500);
          const body = (await page.textContent('body')) || '';
          observe(M, 'activate', /activated|sent|analyzed/i.test(body) ? 'PASS' : 'WARN',
            `activate done; body hints: ${(/activated|sent|analyzed/i.test(body))}`, { role: 'hospital_admin' });

          // send
          const send = page.locator('form[action*="send"] button[type="submit"], button:has-text("Send")');
          if (await send.count()) {
            await send.first().click().catch(() => {});
            await page.waitForLoadState('networkidle', { timeout: 8000 }).catch(() => {});
            await page.waitForTimeout(1500);
            const b2 = (await page.textContent('body')) || '';
            observe(M, 'send', /sent|acknowledg/i.test(b2) ? 'PASS' : 'WARN', `send done`, { role: 'hospital_admin' });
          } else observe(M, 'send', 'SKIP', 'no send button (maybe AI analysis pending)', { role: 'hospital_admin' });
        } else observe(M, 'activate', 'SKIP', 'no activate button (not a draft?)', { role: 'hospital_admin' });
      } else observe(M, 'detail', 'SKIP', 'no appreciation rows', { role: 'hospital_admin' });
      await page.goto('/accounts/logout/').catch(() => {});
    } catch (e) {
      observe(M, 'workflow', 'FAIL', `exception: ${(e as Error).message}`, { role: 'hospital_admin' });
    }
  });
});

// ============================================================================
// 6. ACCESS MATRIX — all 10 roles × 5 module roots
// ============================================================================
test.describe('Access matrix (all roles)', () => {
  for (const role of ALL_ROLES) {
    test(`${role} can/cannot reach each module`, async ({ page }) => {
      for (const { module, path } of MODULE_ROOTS) {
        attachObservers(page, module, role);
        try {
          await loginAndScope(page, role, module);
          await page.goto(path);
          await page.waitForLoadState('domcontentloaded').catch(() => {});
          await page.waitForTimeout(800);
          const tb = await bodyHasTraceback(page);
          const onLogin = page.url().includes('/accounts/login');
          const expected = !['source_user'].includes(role); // source_user is restricted to px-sources typically
          const status: 'PASS' | 'WARN' | 'FAIL' = tb ? 'FAIL' : onLogin === expected ? 'PASS' : 'WARN';
          observe(`${module}/access`, role, status,
            tb ? `traceback: ${tb}` : `${role} -> ${path}: ${onLogin ? 'BLOCKED(login)' : 'ALLOWED'}`,
            { role, url: page.url() });
        } catch (e) {
          observe(`${module}/access`, role, 'FAIL', `exception: ${(e as Error).message}`, { role });
        }
        await page.goto('/accounts/logout/').catch(() => {});
        await page.waitForTimeout(200);
      }
    });
  }
});

// ============================================================================
// 7. DUMP observations to disk
// ============================================================================
test.afterAll(async () => {
  const counts = OBS.reduce<Record<string, number>>((a, o) => ((a[o.status] = (a[o.status] || 0) + 1), a), {});
  console.log('\n=========== AUDIT SUMMARY ===========');
  console.log('Total observations:', OBS.length, JSON.stringify(counts));
  console.log('=====================================\n');
  try {
    const outDir = path.resolve(__dirname, '..', '..', 'results');
    if (!fs.existsSync(outDir)) fs.mkdirSync(outDir, { recursive: true });
    const outFile = path.join(outDir, 'audit-observations.json');
    fs.writeFileSync(outFile, JSON.stringify({ generatedAt: new Date().toISOString(), counts, observations: OBS }, null, 2));
    console.log('Observations written to:', outFile);
  } catch (e) {
    console.log('Failed to write observations file:', (e as Error).message);
  }
  (globalThis as any).__AUDIT_OBS__ = OBS;
});

