from __future__ import annotations

from datetime import date
from pathlib import Path
import json
import zipfile

from docx import Document
from docx.enum.table import WD_CELL_VERTICAL_ALIGNMENT, WD_TABLE_ALIGNMENT
from docx.enum.text import WD_ALIGN_PARAGRAPH
from docx.shared import Inches, Pt, RGBColor
from openpyxl import Workbook
from openpyxl.styles import Alignment, Border, Font, PatternFill, Side
from openpyxl.utils import get_column_letter
from openpyxl.worksheet.datavalidation import DataValidation


ROOT = Path(__file__).resolve().parents[1]
STAMP = "20260526"
PROJECT = "Zavvion Events Commercial Lite MVP"
BASE_URL = "http://localhost/zavvion-events/public/"
OUT = ROOT / "handoff" / f"human-testing-commercial-lite-{STAMP}"
OUT.mkdir(parents=True, exist_ok=True)

ROLES = [
    ["Public visitor", "No login", "Browse events, event pages, booking flow until sign-in/payment gates.", "Must not access account, organiser, admin, draft, finance, or private APIs."],
    ["Customer", "customer@zavvion.test", "Register/sign in, buy tickets, select seats, view orders/tickets/profile.", "Must not access organiser/admin/platform screens or another customer's orders."],
    ["Organiser", "organiser@zavvion.test", "Create venues, seat maps, events, ticket allocations, media, Stripe readiness, finance view.", "Must only see its own organisation data; no platform admin controls."],
    ["Platform organiser", "platform.organiser@zavvion.test", "Global organiser support account that can enter organiser contexts as permitted.", "Must not gain platform admin, fee rule, super admin, or unrelated customer access."],
    ["Platform admin", "platform.admin@zavvion.test", "Approve organisers, manage platform fee rules, monitor readiness and system health.", "Must not bypass Stripe-synced production readiness, expose secrets, or edit platform_super_admin unsafely."],
]

HEADERS = [
    "Test ID", "Priority", "Role", "Area", "Workflow", "Scenario", "Preconditions", "Steps",
    "Expected result", "Device/browser", "Result", "Tester", "Date", "Evidence / screenshot", "Issue ID", "Notes",
]


def tc(test_id: str, priority: str, role: str, area: str, workflow: str, *values: str) -> list[str]:
    if len(values) == 5:
        scenario, preconditions, steps, expected, device = values
    elif len(values) == 4:
        scenario = workflow
        preconditions, steps, expected, device = values
    elif len(values) == 3:
        scenario = workflow
        preconditions = ""
        steps, expected, device = values
    else:
        raise ValueError(f"{test_id} has {len(values)} scenario values; expected 4 or 5")
    return [test_id, priority, role, area, workflow, scenario, preconditions, steps, expected, device]


CASES: dict[str, list[list[str]]] = {
    "01 Public Visitor": [
        tc("PV-001", "P0", "Public visitor", "Home", "Landing/navigation", "Open home page and confirm brand/header/nav load", "Staging URL available", "Open the base URL. Verify logo, Discover, Sign in, Browse actions.", "Page loads without console errors; no test wording or default credentials are visible.", "Desktop + mobile"),
        tc("PV-002", "P0", "Public visitor", "Events", "Discover events", "Browse event listing", "Published demo/test events exist", "Open Discover/Events. Use sort and filter by category/status/seating.", "Only published/public events appear; cards are readable and responsive.", "Desktop + mobile"),
        tc("PV-003", "P1", "Public visitor", "Events", "Filtering", "Search by title/venue/organiser", "At least four demo events exist", "Search for a known event, unknown term, venue name, and organiser name.", "Known event filters in; unknown search shows clear empty state; no broken layout.", "Desktop"),
        tc("PV-004", "P0", "Public visitor", "Event detail", "Public event page", "Open reserved/non-reserved mixed event", "Event with reserved and non-reserved sections exists", "Open event detail. Review About, Booking, Seat-map tabs, non-reserved tickets, reserved seats.", "Only allocated event ticket types appear; sections match attached seat map; no unrelated tickets.", "Desktop + mobile"),
        tc("PV-005", "P0", "Public visitor", "Event detail", "Seat-map tab", "Seat-map tab blank when no uploaded venue PDF/image", "Event has no customer-facing map asset", "Open Seat-map tab.", "No fabricated structured seat map is shown; a clear empty state explains no uploaded map.", "Desktop + mobile"),
        tc("PV-006", "P0", "Public visitor", "Event detail", "Seat-map tab", "Uploaded customer-facing PDF/image renders", "Seat map asset uploaded to the selected reusable seat map", "Open Seat-map tab. Use zoom in/out/reset. Resize viewport.", "PDF/image is visible, fits device width, zoom controls work, no local path exposed.", "Desktop + mobile"),
        tc("PV-007", "P0", "Public visitor", "Booking", "Reserved seats", "Select one reserved seat", "Event has reserved section", "Open Booking tab. Select reserved section tab. Tap/click a seat.", "Seat becomes selected; side basket shows seat label on one line and asks for ticket type.", "Desktop + mobile"),
        tc("PV-008", "P0", "Public visitor", "Booking", "Reserved seats", "Assign ticket type to selected seat", "Allocated ticket types exist", "Select seat, choose an allocated ticket type from dropdown.", "Only allocated ticket types appear; price/subtotal update; no unallocated catalogue types appear.", "Desktop"),
        tc("PV-009", "P0", "Public visitor", "Booking", "Non reserved seats", "Buy non-reserved quantity", "Event has non-reserved section", "Use non-reserved ticket controls. Increase/decrease quantities.", "Quantities update; no exact seat required; inventory and max/order enforced.", "Desktop + mobile"),
        tc("PV-010", "P0", "Public visitor", "Booking", "Mixed booking", "Combine reserved and non-reserved tickets", "Mixed event exists", "Select reserved seat and non-reserved ticket. Continue checkout.", "Basket groups both correctly; total is correct; checkout requires valid payment path.", "Desktop"),
        tc("PV-011", "P0", "Public visitor", "Seat hold", "15-minute hold display", "Reserved seat selected", "Select reserved seat and observe countdown.", "Countdown starts at 15:00 or less, never long values; release action works.", "Desktop + mobile"),
        tc("PV-012", "P0", "Public visitor", "Seat hold", "Release seat", "Reserved seat selected", "Click Release/Remove seat.", "Seat returns available; basket and subtotal clear; no stale selected state.", "Desktop"),
        tc("PV-013", "P1", "Public visitor", "Checkout", "Promo code empty/invalid", "Event checkout visible", "Enter invalid promo code; apply.", "Clear error/no discount; promo row hidden when no promo value.", "Desktop"),
        tc("PV-014", "P1", "Public visitor", "Checkout", "Donation hidden when not configured", "Event without donation option", "Open event page and basket.", "Donation line/field does not appear when no donation values configured.", "Desktop"),
        tc("PV-015", "P1", "Public visitor", "Merchandise", "Merchandise hidden when organiser did not opt in", "Event with no merchandise", "Open event detail below booking area.", "No Add merchandise panel is shown.", "Desktop + mobile"),
        tc("PV-016", "P0", "Public visitor", "Payment safety", "Public cash/counter payment hidden", "Checkout configured", "Proceed to payment options.", "Cash/external terminal/counter methods are never exposed to public checkout.", "Desktop"),
        tc("PV-017", "P0", "Public visitor", "Security", "Unauthenticated admin URL", "None", "Open /admin.html, /organiser.html, sensitive API URLs in a private window.", "Redirects to sign-in or 401/403; no data leak.", "Desktop"),
        tc("PV-018", "P2", "Public visitor", "Accessibility", "Keyboard browse", "None", "Navigate header, filters, event cards, booking controls with keyboard only.", "Visible focus, logical tab order, no keyboard traps.", "Desktop"),
        tc("PV-019", "P1", "Public visitor", "Responsive", "Mobile event detail", "Mobile viewport", "Open event page at 320, 390, 430 widths.", "No horizontal page overflow except intentional seat-map pan; text readable; buttons tappable.", "Mobile"),
        tc("PV-020", "P2", "Public visitor", "Errors", "Network/API failure messaging", "DevTools can block API", "Simulate API failure or disconnect; refresh event page.", "Friendly error/empty state; no raw stack traces or secrets.", "Desktop"),
    ],
    "02 Customer": [
        tc("CU-001", "P0", "Customer", "Registration", "Create account", "New customer registration", "Use Sign in/Register path with unique email.", "Register with valid details and invalid details.", "Account created or submitted cleanly; validation errors clear; no admin/organiser role granted.", "Desktop + mobile"),
        tc("CU-002", "P0", "Customer", "Auth", "Login/logout", "Customer session starts and ends safely", "Customer test credentials supplied by architect", "Log in, confirm My tickets link, sign out, then use Back/refresh.", "Session starts/ends correctly; protected pages cannot be viewed after logout.", "Desktop"),
        tc("CU-003", "P0", "Customer", "Booking", "Complete non-reserved purchase", "Non-reserved paid checkout", "Payment test path configured", "Add non-reserved ticket, proceed checkout, complete test payment.", "Order paid only after trusted payment/webhook; ticket/order visible in My tickets.", "Desktop"),
        tc("CU-004", "P0", "Customer", "Booking", "Complete reserved purchase", "Reserved paid checkout", "Reserved event and payment test path configured", "Select seat, assign type, complete checkout.", "Ticket has correct event, seat, ticket type, QR; seat unavailable afterwards.", "Desktop + mobile"),
        tc("CU-005", "P0", "Customer", "Booking", "Abandon checkout", "Close tab before payment", "Reserved seat selected", "Select seat, start checkout, close tab/leave page.", "Seat remains held until expiry/release; no ticket issued without payment.", "Desktop"),
        tc("CU-006", "P0", "Customer", "Double booking", "Concurrent same seat", "Two browsers/sessions", "Customer A holds seat. Customer B attempts same seat.", "Second selection blocked or sees held/sold; no double booking.", "Desktop"),
        tc("CU-007", "P1", "Customer", "Orders", "Order list", "Customer has at least one order", "Open My tickets -> Orders/Past/Upcoming.", "Only customer's own orders appear; totals and status correct.", "Desktop + mobile"),
        tc("CU-008", "P1", "Customer", "Tickets", "Ticket/QR view", "Customer has issued ticket", "Open ticket details/QR.", "QR visible, correct event/seat/name; no other customer data.", "Desktop + mobile"),
        tc("CU-009", "P1", "Customer", "Profile", "Edit profile", "Customer logged in", "Edit first name/last name/mobile and save.", "Change persists after refresh/login; validation works; no launch-blocking deferred message.", "Desktop"),
        tc("CU-010", "P1", "Customer", "Privacy", "Export data request", "Customer logged in", "Use Export my data/request export.", "Request is accepted/logged or clearly marked as support request; no crash.", "Desktop"),
        tc("CU-011", "P1", "Customer", "Privacy", "Delete account request", "Customer logged in", "Request deletion.", "Clear confirmation/support request; no accidental immediate deletion unless designed.", "Desktop"),
        tc("CU-012", "P0", "Customer", "Authorization", "Organiser/admin URL blocked", "Customer logged in", "Open organiser/admin URLs directly.", "403/redirect; no menu/admin data rendered.", "Desktop"),
        tc("CU-013", "P0", "Customer", "IDOR", "Other order guessing", "Two customers/orders exist", "Attempt to view/change another customer's order/ticket ID via URL/API.", "403/404; no data leak.", "Desktop/API"),
        tc("CU-014", "P2", "Customer", "Usability", "Refresh during basket", "Basket contains selected items", "Refresh page; navigate away/back.", "Basket either persists safely or clears with clear message; no stale wrong totals.", "Desktop + mobile"),
        tc("CU-015", "P1", "Customer", "Payment failure", "Failed test payment", "Stripe test failure card available", "Run checkout with failing test payment.", "No ticket issued; order remains failed/pending; seat release behavior correct.", "Desktop"),
        tc("CU-016", "P1", "Customer", "Mobile", "Checkout on phone", "Mobile viewport/device", "Run full booking path on mobile.", "Sticky basket readable; dropdowns and buttons fit; no clipped seat labels.", "Mobile"),
    ],
    "03 Organiser": [
        tc("ORG-001", "P0", "Organiser", "Auth", "Login and menu visibility", "Approved organiser login", "Approved organiser account", "Log in as organiser.", "Shows organiser dashboard/events/venues/ticketing/finance/profile only; no admin controls.", "Desktop + mobile"),
        tc("ORG-002", "P0", "Organiser", "Venues", "Create venue", "Save new venue", "Organiser logged in", "Create venue with name/address.", "Venue saves, appears in venue list, refresh persists.", "Desktop"),
        tc("ORG-003", "P0", "Organiser", "Seat-plan builder", "Create reserved section", "Reserved grid section", "Venue exists", "Create reusable seat map with reserved section rows/seats.", "Section appears with reserved badge; structured seats count correct.", "Desktop"),
        tc("ORG-004", "P0", "Organiser", "Seat-plan builder", "Create non-reserved section", "Non-reserved section", "Venue exists", "Add non-reserved seating section.", "Section appears with non-reserved badge; no individual sellable seats created for it.", "Desktop"),
        tc("ORG-005", "P0", "Organiser", "Seat-map library", "Upload/replace customer-facing PDF/image per seat map", "Reusable seat map exists", "Use Upload/Replace PDF/image on one seat map.", "Asset links to that exact seat map, not venue globally; other maps under venue remain unchanged.", "Desktop"),
        tc("ORG-006", "P1", "Organiser", "Seat-map library", "Edit seat map labels", "Reusable seat map exists", "Use Edit; rename seat map and section labels.", "Safe edits save; section admission type and dimensions remain locked when required.", "Desktop"),
        tc("ORG-007", "P0", "Organiser", "Events", "Create future event", "Draft save", "Venue and seat map exist", "Create event dated tomorrow/future, select exact seat map, save draft.", "Draft saves to DB and is visible after refresh; venue follows selected seat map.", "Desktop"),
        tc("ORG-008", "P0", "Organiser", "Events", "Reject past event date", "Past date validation", "Organiser logged in", "Attempt to create event before current date.", "Save/publish blocked with clear date validation.", "Desktop"),
        tc("ORG-009", "P0", "Organiser", "Events", "Edit only current event data", "Multiple events exist", "Edit Event A media/details; check Event B.", "Only Event A data shown/edited; no unrelated event dropdown required in media area.", "Desktop"),
        tc("ORG-010", "P0", "Organiser", "Events", "Attach exact seat map", "Venue has multiple seat maps", "Select one seat map on event form; save; preview public.", "Public booking uses selected map/sections, not a different venue/default map.", "Desktop"),
        tc("ORG-011", "P0", "Organiser", "Ticketing", "Reusable catalogue has no event prices", "Ticketing page", "Create/edit reusable ticket type rules/name/category.", "Price, quantity, max/order are not catalogue-level; action wording is 'Edit'.", "Desktop"),
        tc("ORG-012", "P0", "Organiser", "Ticketing", "Event-level price/quantity/max order", "Event exists", "Configure Adult/Child/etc for selected event.", "Price, quantity, max/order save at event allocation level and public page uses those values.", "Desktop"),
        tc("ORG-013", "P0", "Organiser", "Ticketing", "Remove allocated ticket from event", "Event has allocated ticket types", "Click Remove from event for one allocation.", "Allocation disappears after refresh and public page no longer offers it.", "Desktop"),
        tc("ORG-014", "P0", "Organiser", "Ticketing", "Duplicate allocation prevention", "Event has Child allocated", "Attempt to add same reusable ticket type again.", "Either update existing allocation or block duplicate; public page has no duplicate rows.", "Desktop"),
        tc("ORG-015", "P0", "Organiser", "Ticketing", "Section-specific ticket allocation", "Event has mixed sections", "Enable section-specific rules; map ticket types to specific sections.", "Public reserved seat dropdown and non-reserved list show only mapped ticket types.", "Desktop"),
        tc("ORG-016", "P1", "Organiser", "Ticketing layout", "Panel order", "Ticketing page", "Check catalogue, event-level prices, promo, section allocation, event allocations.", "Promo appears after event ticket allocation/pricing; heavier allocation panels lower on page.", "Desktop"),
        tc("ORG-017", "P1", "Organiser", "Promo", "Create promo code", "Event exists", "Add valid promo code with usage limits/date.", "Promo saves and applies only within configured limits.", "Desktop"),
        tc("ORG-018", "P1", "Organiser", "Donations", "Configure donation", "Event exists", "Enable donation label/amounts; preview public.", "Donation appears only when configured and totals update safely.", "Desktop"),
        tc("ORG-019", "P1", "Organiser", "Media", "Upload event media", "Event exists", "Upload poster/banner/gallery images.", "Files validate type/size, display only for selected event, no paths exposed.", "Desktop"),
        tc("ORG-020", "P1", "Organiser", "Media", "Invalid media upload", "Event exists", "Upload invalid extension/oversized file.", "Rejected with clear message; no executable content served.", "Desktop"),
        tc("ORG-021", "P0", "Organiser", "Publishing", "Publication readiness", "Draft event exists", "Try publish without required Stripe/readiness data.", "Blocked or warned according to launch policy; no unsafe paid event goes live.", "Desktop"),
        tc("ORG-022", "P0", "Organiser", "Stripe", "Connect Stripe onboarding", "Organiser account", "Open finance/Stripe readiness, start Connect setup.", "Onboarding link/flow available; readiness reflects Stripe state, not manual browser flagging.", "Desktop"),
        tc("ORG-023", "P1", "Organiser", "Finance", "Sales/ledger view", "Event has sales", "Open finance dashboard.", "Organiser sees own event totals, fees, payouts; no other organiser/customer private data.", "Desktop"),
        tc("ORG-024", "P0", "Organiser", "Tenant isolation", "Other organiser event/venue IDs known", "Attempt direct URL/API access to other organiser objects.", "403/404; no data leakage or edits.", "Desktop"),
        tc("ORG-025", "P2", "Organiser", "Responsive", "Mobile organiser workflow", "Mobile/tablet", "Create venue/event and configure ticketing at 390 and 820 widths.", "Forms usable, tables readable/scroll intentionally, buttons not clipped.", "Mobile + tablet"),
    ],
    "04 Platform Organiser": [
        tc("PO-001", "P0", "Platform organiser", "Auth", "Login", "Platform organiser account", "Log in and review header/context selector.", "Can enter permitted organiser contexts; no platform admin menu.", "Desktop"),
        tc("PO-002", "P0", "Platform organiser", "Context", "Switch organisation", "Multiple approved organisers exist", "Switch between organiser contexts.", "Data changes to selected organiser only; no cross-contamination.", "Desktop"),
        tc("PO-003", "P0", "Platform organiser", "Events", "Create event for selected organiser", "Context selected", "Create draft event/venue/ticketing.", "Event belongs to selected organiser and appears in that organiser's views.", "Desktop"),
        tc("PO-004", "P0", "Platform organiser", "Authorization", "Admin controls blocked", "Logged in as platform organiser", "Attempt admin fee rules/organiser approvals/super admin URLs.", "Blocked with 403/redirect; no admin data rendered.", "Desktop"),
        tc("PO-005", "P1", "Platform organiser", "Audit", "Trace support changes", "Action performed in organiser context", "Review audit/log/admin visibility if available.", "Actions are attributable to platform organiser/support account.", "Desktop"),
        tc("PO-006", "P2", "Platform organiser", "Mobile", "Context and menus on tablet", "Tablet viewport", "Use organiser screens at 768/820 widths.", "Context selector and side menu remain usable.", "Tablet"),
    ],
    "05 Platform Admin": [
        tc("ADM-001", "P0", "Platform admin", "Auth", "Login/menu", "Platform admin credentials", "Log in as platform admin.", "Admin dashboard/organisers/fees/settings visible; organiser-only actions separated.", "Desktop"),
        tc("ADM-002", "P0", "Platform admin", "Organisers", "Review organiser applications", "Pending application exists", "Open organisers, approve/reject/request info.", "Status changes persist; applicant/organiser visibility correct.", "Desktop"),
        tc("ADM-003", "P0", "Platform admin", "Organiser application", "Public apply page style and submit", "No login", "Open /organiser/apply, submit valid and invalid forms.", "Styled page; validation clear; valid application appears for admin review.", "Desktop + mobile"),
        tc("ADM-004", "P0", "Platform admin", "Fee rules", "Create global country fee rule", "No active conflict", "Add global fee rule for a country/currency.", "Saves with validated ISO country/currency and visible status.", "Desktop"),
        tc("ADM-005", "P0", "Platform admin", "Fee rules", "Block conflicting active global rule", "Active country rule exists", "Attempt second active rule for same country.", "Save blocked with clear conflict message.", "Desktop"),
        tc("ADM-006", "P0", "Platform admin", "Fee rules", "Create event override", "Event exists", "Add event-specific fee override.", "At most one active override per event; conflict blocked.", "Desktop"),
        tc("ADM-007", "P0", "Platform admin", "Fee rules", "Edit/deactivate/delete", "Existing rules exist", "Edit amount/date/status, deactivate, delete inactive rule.", "Changes persist; active conflict checks run on edit/add; deletion safe/audited.", "Desktop"),
        tc("ADM-008", "P0", "Platform admin", "Security", "Super admin protection", "Admin account without super admin power", "Attempt assign/edit platform_super_admin role.", "Blocked unless explicitly super-admin-authorised; cannot self-escalate.", "Desktop"),
        tc("ADM-009", "P0", "Platform admin", "Stripe", "Production readiness cannot be faked", "APP_ENV production/staging", "Try manually marking organiser Stripe ready via browser payload/devtools.", "Production preserves Stripe-synced state or pending; browser cannot mark charges/payouts ready.", "Desktop"),
        tc("ADM-010", "P1", "Platform admin", "System health", "Readiness dashboard", "Admin logged in", "Open deployment/readiness/system health views.", "Warnings/critical states accurate; no secrets exposed.", "Desktop"),
        tc("ADM-011", "P1", "Platform admin", "Reports", "Platform overview", "Events and orders exist", "Review sales/finance summaries.", "Totals reconcile with orders/ledger; VAT/fee allocation clear.", "Desktop"),
        tc("ADM-012", "P0", "Platform admin", "Customer privacy", "No unnecessary PII exposure", "Customers/orders exist", "Review admin screens and exports.", "Only necessary data shown; no passwords/secrets/tokens; export access controlled.", "Desktop"),
        tc("ADM-013", "P2", "Platform admin", "Responsive", "Admin on tablet", "Tablet viewport", "Use organiser approvals and fee rules at 768/820 widths.", "Tables/forms usable, no critical action hidden/clipped.", "Tablet"),
    ],
    "06 Booking Seat Maps": [
        tc("SM-001", "P0", "Mixed", "Seat maps", "Venue has many seat maps", "Venue with at least two reusable maps", "Attach Map A to Event A and Map B to Event B. Preview both.", "Each event uses its exact selected map and sections.", "Desktop"),
        tc("SM-002", "P0", "Mixed", "Seat maps", "Section tabs", "Event has two reserved and one non-reserved section", "Open booking. Switch section tabs.", "Reserved tabs show grids; non-reserved tab explains buy from non-reserved list; styling readable.", "Desktop + mobile"),
        tc("SM-003", "P0", "Mixed", "Seat maps", "Seat labels readable", "Reserved section with labels", "Select seats like Sec RR C9, Sec WW A7.", "Seat labels and 'Choose type for Sec RR' stay on one line where possible and do not overflow.", "Desktop + mobile"),
        tc("SM-004", "P0", "Mixed", "Seat maps", "Ticket eligibility by section", "Section-specific allocation enabled", "Select a seat in each reserved section; compare dropdown options.", "Only ticket types mapped to that section are available.", "Desktop"),
        tc("SM-005", "P0", "Mixed", "Seat maps", "Non-reserved ticket types", "Non-reserved section and mappings exist", "Review non-reserved list and add tickets.", "Only allocated/mapped ticket types are sold; no unrelated catalogue types.", "Desktop"),
        tc("SM-006", "P0", "Mixed", "Seat holds", "Hold expiry", "Reserved seat selected", "Wait for expiry or shorten in test config if available.", "Seat releases after 15 minutes if payment incomplete; no ticket issued.", "Desktop"),
        tc("SM-007", "P0", "Mixed", "Seat holds", "Held/sold status", "One ticket sold", "Open event in second browser after sale.", "Sold seat cannot be selected; legend/state correct.", "Desktop"),
        tc("SM-008", "P1", "Mixed", "Seat map asset", "Replace PDF/image", "Seat map has existing PDF/image", "Upload replacement PDF/image; open public Seat-map tab.", "New asset displayed; old asset no longer shown for that map; other maps unaffected.", "Desktop"),
        tc("SM-009", "P1", "Mixed", "Seat map asset", "Invalid PDF/image", "Seat map edit available", "Upload .php/.html/oversized/invalid mime file.", "Rejected safely; no executable upload; no server path revealed.", "Desktop"),
        tc("SM-010", "P1", "Mixed", "Seat map asset", "PDF/image responsiveness", "Uploaded map asset", "Open Seat-map tab at 320/390/768/1440; zoom/pan/reset.", "Map fits device, zoom is usable, no blank/oversized canvas.", "Responsive"),
    ],
    "07 Payments Stripe": [
        tc("PAY-001", "P0", "Payment", "Stripe config", "Missing env", "Staging/production env without keys", "Run readiness checks/admin health.", "Payment readiness shows blocked/warning; no silent success.", "Server"),
        tc("PAY-002", "P0", "Payment", "Stripe Connect", "Connected organiser", "Stripe test keys and connected account configured", "Complete onboarding/refresh readiness.", "Charges/payout readiness comes from Stripe state; organiser payout routing configured.", "Desktop"),
        tc("PAY-003", "P0", "Payment", "Signed webhook", "Real Stripe test mode", "Run one checkout with signed webhook event.", "Order paid, ticket/QR issued, ledger entries recorded only after signed webhook.", "Desktop"),
        tc("PAY-004", "P0", "Payment", "Unsigned webhook", "Webhook endpoint reachable", "Send unsigned actionable webhook payload.", "Recorded/rejected as signature_required; no ticket issued.", "API"),
        tc("PAY-005", "P0", "Payment", "Redirect safety", "Checkout success redirect URL", "Open/forge success redirect without webhook.", "Does not mark paid or issue tickets from redirect alone.", "Desktop"),
        tc("PAY-006", "P0", "Payment", "Duplicate webhook", "Paid order exists", "Replay same signed webhook event.", "Idempotent; no duplicate tickets, QR, ledger entries.", "API"),
        tc("PAY-007", "P0", "Payment", "Fee/tax allocation", "Fee rule active", "Run test checkout with service fee/VAT scenario.", "Customer total, organiser payout, VAT, platform fee match configured rules and ledger.", "Desktop"),
        tc("PAY-008", "P1", "Payment", "Payment failure", "Stripe test failure card", "Complete checkout with failing card.", "No tickets; clear error; order/hold state correct.", "Desktop"),
        tc("PAY-009", "P1", "Payment", "Currency", "Non-GBP test event if supported", "Create event with non-GBP currency and fee rule.", "Currency consistent across event, tickets, checkout, Stripe, reports.", "Desktop"),
        tc("PAY-010", "P1", "Payment", "Refund/deferred", "If refund flow exists", "Attempt refund/cancel/resale path.", "Either works correctly or is clearly marked deferred; no misleading actions.", "Desktop"),
    ],
    "08 Security Negative": [
        tc("SEC-001", "P0", "All", "Auth", "Protected pages direct access", "No login", "Open organiser/admin/account URLs and APIs directly.", "401/403/redirect; no private data in HTML/API.", "Desktop"),
        tc("SEC-002", "P0", "All", "Role boundary", "Customer to organiser/admin", "Customer logged in", "Directly request organiser/admin endpoints.", "Denied; no role escalation.", "Desktop/API"),
        tc("SEC-003", "P0", "All", "Role boundary", "Organiser to admin", "Organiser logged in", "Request admin fee/organiser approval URLs and APIs.", "Denied.", "Desktop/API"),
        tc("SEC-004", "P0", "All", "Tenant isolation", "Organiser A vs B", "Two organisers exist", "Swap IDs in event/venue/seat map/media/ticketing APIs.", "Denied or not found.", "API"),
        tc("SEC-005", "P0", "All", "CSRF", "Mutation without CSRF", "Authenticated session", "Replay POST/PATCH/DELETE without token/header.", "Rejected for protected mutations.", "API"),
        tc("SEC-006", "P0", "All", "XSS", "Labels and upload text", "Create labels with script/quotes/unicode", "Use event title, section names, ticket names, media captions.", "Escaped everywhere; no script execution in public/admin/organiser pages.", "Desktop"),
        tc("SEC-007", "P0", "All", "SQL injection", "Search/filter inputs", "Access to search/forms", "Use SQL-like payloads in search, slugs, IDs, promo codes.", "No SQL errors; no data leak; validation messages safe.", "Desktop/API"),
        tc("SEC-008", "P0", "All", "File upload", "Dangerous uploads", "Upload controls available", "Upload .php, polyglot, oversized, wrong mime, renamed extension.", "Rejected or stored safely; not executable; no path exposure.", "Desktop"),
        tc("SEC-009", "P0", "Payment", "Checkout draft exposure", "Draft IDs/tokens known", "Attempt GET/PATCH other draft/customer data.", "Signed token or ownership required; no customer data leak.", "API"),
        tc("SEC-010", "P0", "QR", "QR abuse", "Ticket QR exists", "Scan wrong event, duplicate scan, revoked/cancelled ticket.", "Wrong event/duplicate/revoked rejected; valid scan transitions once.", "Scanner/API"),
        tc("SEC-011", "P0", "All", "Secrets", "Static files", "Open .env, .env.production, composer.json, composer.lock, composer.phar, phpunit.xml, docs/schema.sql, backup files, runtime logs, and uploaded executable samples from the deployed URL.", "403/404 only; no secrets, source, schema, logs, or sensitive static artifacts exposed.", "Desktop"),
        tc("SEC-013", "P0", "All", "Production exclusions", "Public review/showcase pages blocked", "APP_ENV=production or staging production-like mode", "Request mvp-profiles.html, mvp-showcase.html, full-experience.html, launchpad.html, product-map.html, platform-map.html, and any password-free role review path.", "All local/demo/review conveniences are removed, blocked, redirected, or explicitly production-safe; no password-free role access.", "Desktop"),
        tc("SEC-012", "P1", "All", "Rate/abuse", "Login and checkout", "Attempt repeated failed logins/promo/checkout mutations.", "Reasonable throttling or logged controls; no crash.", "Desktop/API"),
    ],
    "09 Responsive": [],
    "10 Regression": [
        tc("REG-001", "P0", "All", "Smoke", "Main nav smoke", "Fresh deploy", "Open home, events, event, login, account, organiser, admin, apply.", "All pages return 200 or intended auth redirect; no broken links.", "Desktop"),
        tc("REG-002", "P0", "All", "Database persistence", "Create/edit workflows", "Create venue/event/ticket/media; refresh/relogin.", "Saved items persist in actual app database; MVP showcase notes if local-only/demo-only.", "Desktop"),
        tc("REG-003", "P0", "All", "Cache", "Fresh query params", "Use normal URL and cache-busting fresh URL.", "Behavior same; no stale old JS/CSS issues.", "Desktop"),
        tc("REG-004", "P1", "All", "Browser compatibility", "Chrome/Edge/Safari/Firefox", "Run smoke and booking on each target browser.", "No browser-specific blockers.", "Desktop + mobile"),
        tc("REG-005", "P1", "All", "Error logging", "Induced validation/API errors", "Trigger expected validation failures.", "Errors are user-friendly; server logs useful and do not contain secrets.", "Desktop/API"),
        tc("REG-006", "P0", "All", "Production mode", "APP_ENV=production APP_DEBUG=false", "Open key pages and errors.", "No debug stack traces; test/demo-only bypasses disabled where required.", "Server"),
    ],
    "11 Accessibility": [
        tc("A11Y-001", "P0", "All", "Keyboard", "Public booking keyboard-only", "Public event available", "Use Tab/Shift+Tab/Enter/Space to browse event, switch tabs, select seats, change ticket quantities, and reach checkout.", "Logical order, visible focus, no keyboard trap, controls operable.", "Desktop"),
        tc("A11Y-002", "P0", "Customer", "Keyboard", "Customer account keyboard-only", "Customer logged in", "Navigate My tickets, Orders, Profile, privacy actions, and logout with keyboard only.", "All controls reachable; focus remains visible and meaningful.", "Desktop"),
        tc("A11Y-003", "P0", "Organiser", "Keyboard", "Organiser forms keyboard-only", "Organiser logged in", "Create/edit event, venue, ticket allocation, and seat-map upload using keyboard only.", "Fields, selects, buttons, dialogs, and uploads are usable without mouse.", "Desktop"),
        tc("A11Y-004", "P0", "Platform admin", "Keyboard", "Admin forms keyboard-only", "Admin logged in", "Use organiser approvals and fee rule add/edit/delete controls with keyboard only.", "All controls reachable; destructive actions require deliberate confirmation where applicable.", "Desktop"),
        tc("A11Y-005", "P1", "All", "Focus management", "Dialog/modal focus", "Any modal/dialog flow available", "Open and close dialogs such as map/upload/ticketing actions.", "Focus moves into dialog, stays inside while open, and returns to trigger after close.", "Desktop"),
        tc("A11Y-006", "P1", "All", "Labels", "Form labels and button names", "Forms available", "Inspect inputs/buttons using browser accessibility tree or screen reader basics.", "Inputs have labels; icon buttons have names; errors reference affected fields.", "Desktop"),
        tc("A11Y-007", "P1", "All", "Contrast", "Text and control contrast", "Pages loaded", "Check key text, badges, selected seats, disabled controls, and error messages.", "Contrast is readable in normal and high brightness conditions.", "Desktop + mobile"),
        tc("A11Y-008", "P1", "All", "Zoom", "200 percent browser zoom", "Desktop browser", "Set zoom to 200 percent on home, event booking, organiser ticketing, admin fee rules.", "No loss of core functionality; content reflows or scrolls intentionally.", "Desktop"),
        tc("A11Y-009", "P1", "All", "Touch targets", "Mobile touch size", "Mobile device", "Tap nav, filters, section tabs, seats, basket selects, plus/minus, upload/edit buttons.", "Targets are large enough and not too close together.", "Mobile"),
        tc("A11Y-010", "P2", "All", "Reduced motion", "Motion sensitivity", "OS/browser reduced motion available", "Enable reduced motion and browse pages.", "No essential information depends on animation; no distracting motion.", "Desktop + mobile"),
    ],
    "12 State Error Matrix": [
        tc("STATE-001", "P0", "Public visitor", "Loading", "Events loading", "Throttle network", "Open events and event detail while network is slow.", "Loading states appear; layout does not jump badly; no raw errors.", "Desktop + mobile"),
        tc("STATE-002", "P0", "Public visitor", "Empty", "No events/no search results", "Use unmatched search/filter", "Search impossible term and extreme filters.", "Clear empty state and reset path.", "Desktop"),
        tc("STATE-003", "P0", "Public visitor", "Payment", "Payment cancelled/failed/pending", "Stripe test flow", "Cancel checkout, fail payment, leave pending if possible.", "No ticket issued; clear recovery path; seat hold behavior correct.", "Desktop"),
        tc("STATE-004", "P0", "Customer", "Session", "Expired session during checkout", "Customer logged in", "Expire session or use another tab logout during checkout.", "User is prompted to sign in again; no duplicate order/ticket.", "Desktop"),
        tc("STATE-005", "P0", "Organiser", "Validation", "Required event fields missing", "Organiser logged in", "Save event with missing/invalid date, title, seat map, ticket data.", "Clear field-level errors; no partial unsafe publish.", "Desktop"),
        tc("STATE-006", "P0", "Organiser", "Upload", "Rejected media/PDF upload", "Upload controls available", "Upload invalid MIME, oversized file, renamed executable.", "Rejected safely with clear message; no path/stack trace.", "Desktop"),
        tc("STATE-007", "P0", "Organiser", "Seat hold stale", "Seat holds exist", "Wait past hold expiry and refresh public/organiser views.", "Expired holds released; inventory/availability correct.", "Desktop"),
        tc("STATE-008", "P1", "Platform admin", "Permission denied", "Non-admin/invalid context", "Attempt admin actions with insufficient role.", "403/redirect message is clear; no sensitive data displayed.", "Desktop"),
        tc("STATE-009", "P1", "Platform admin", "Conflict", "Fee rule conflict", "Try conflicting fee rule add/edit.", "Save blocked and conflict explained.", "Desktop"),
        tc("STATE-010", "P1", "All", "API/network failure", "DevTools block API or server down", "Load key pages and save forms with API blocked.", "Friendly failure and retry path; no data corruption.", "Desktop"),
    ],
    "13 Device Journeys": [
        tc("DEV-001", "P0", "Public visitor", "Small phone", "Complete mixed booking journey", "320 or 360px phone", "Browse event, select reserved seat, add non-reserved ticket, assign ticket type, review basket.", "Readable, tappable, no clipped seat labels or unusable basket.", "Real phone preferred"),
        tc("DEV-002", "P0", "Customer", "Small phone", "Retrieve ticket at venue", "Customer with issued ticket", "Open My tickets, locate upcoming ticket, display QR.", "QR and ticket details visible quickly; no login/navigation trap.", "Real phone"),
        tc("DEV-003", "P1", "Organiser", "Tablet portrait", "Create/edit event", "768/820px tablet", "Create future event, select exact seat map, save draft, preview.", "Forms fit, selects usable, save/preview buttons visible.", "Tablet portrait"),
        tc("DEV-004", "P1", "Organiser", "Tablet landscape", "Seat-plan builder", "Tablet landscape", "Create seat map with reserved and non-reserved section, upload PDF/image.", "Builder and library usable without desktop-only assumptions.", "Tablet landscape"),
        tc("DEV-005", "P1", "Platform admin", "Laptop", "Fee rule management", "1280px laptop", "Add/edit/deactivate/delete fee rules and conflict check.", "Tables and actions fit; no hidden critical action.", "Laptop"),
        tc("DEV-006", "P1", "All", "Desktop ultrawide", "Layout space use", "1920px display", "Open home/events/event/organiser/admin.", "Panels use space reasonably; text not tiny; content not stranded in narrow column.", "Desktop"),
    ],
}


VIEWPORTS = [320, 360, 375, 390, 414, 430, 768, 820, 1024, 1280, 1440, 1920]
RESPONSIVE_PAGES = [
    ("Home", "/public/home.html"),
    ("Discover events", "/public/events.html"),
    ("Event booking", "/public/event.html?slug=ui-workflow-event-292430"),
    ("Customer tickets/profile", "/public/account.html"),
    ("Organiser events", "/public/organiser.html#events"),
    ("Organiser venue/seat-map builder", "/public/organiser.html#venues"),
    ("Organiser ticketing", "/public/organiser.html#ticketing"),
    ("Platform admin fee rules", "/public/admin.html#fees"),
    ("Organiser apply", "/public/organiser/apply"),
]
for page_number, (name, path) in enumerate(RESPONSIVE_PAGES, 1):
    for viewport in VIEWPORTS:
        CASES["09 Responsive"].append(tc(
            f"RSP-{page_number:02d}-{viewport}",
            "P1" if viewport in [320, 390, 768, 1440] else "P2",
            "Responsive tester",
            "Responsive",
            name,
            f"Viewport {viewport}px",
            f"Open {path}",
            f"Set viewport width to {viewport}px. Check navigation, text size, panel width, tables, seat maps, sticky basket, form controls, and scrolling.",
            "No unintended horizontal page overflow; text readable; controls tappable; important content not clipped; intentional seat map pan/zoom works.",
            f"{viewport}px",
        ))


TEST_DATA_ROWS = [
    ["TD-001", "Event", "Reserved-only event", "One venue, one seat map, two reserved sections, no non-reserved section", "Public booking must show Reserved seats only."],
    ["TD-002", "Event", "Non-reserved-only event", "No reserved grid; one non-reserved section and several ticket types", "Public booking must show Non reserved seats/tickets only."],
    ["TD-003", "Event", "Mixed seating event", "Two reserved sections plus one non-reserved section; section-specific ticket mappings", "Core commercial-lite scenario."],
    ["TD-004", "Event", "No uploaded seat-map asset event", "Structured booking map exists but no customer-facing PDF/image", "Seat-map tab should be blank/empty state, not fabricated map."],
    ["TD-005", "Event", "Uploaded venue map event", "Reusable seat map has PDF/image uploaded", "Seat-map tab displays floor-plan image/PDF with zoom controls."],
    ["TD-006", "Venue", "Venue with multiple seat maps", "Same venue has at least two reusable maps", "Verify event selects exact map, not venue-level default."],
    ["TD-007", "Tickets", "Adult/Child/Senior/Student", "Reusable ticket catalogue with age/ID rules", "Event-level price, quantity, max/order configured per event."],
    ["TD-008", "Promo", "Valid and invalid promo", "At least one valid active promo and one expired/invalid code", "Apply rules, limits and hidden empty rows."],
    ["TD-009", "Stripe", "Connected organiser", "Stripe test connected account with charges/payouts readiness", "Required for paid checkout validation."],
    ["TD-010", "Security", "Two organisers, two customers", "Distinct records and accounts", "Required for IDOR/tenant isolation testing."],
]


def write_text_files() -> None:
    plan_md = f"""# {PROJECT} - Human Functional Testing Plan

**Date:** {date.today().isoformat()}  
**Target release:** Commercial Lite MVP  
**Default local base URL:** `{BASE_URL}`  
**Primary objective:** prove that the platform works safely and clearly for real public buyers, customers, organisers, platform organisers, and platform admins before commercial launch.

## How To Use This Pack

1. Ask the architect/deployment owner to fill in the staging URL and role credentials in the workbook `Credentials` sheet.
2. Run the tests in priority order: all `P0` tests first, then `P1`, then `P2`.
3. Every failed or blocked scenario must have an issue row with screenshot/video evidence.
4. Retest every fixed issue and update the same issue row with retest evidence.
5. Do not mark the release complete until every `P0` passes, every `P1` is either passed or accepted with a named owner, and all payment/role/security critical workflows are verified on the deployed server.

## Release Exit Criteria

- Public buyers can browse, select reserved seats, buy non-reserved ticket quantities, and complete checkout without seeing unallocated tickets or unsafe payment methods.
- Organisers can create venues, reusable seat maps, section grids, event-level ticket allocations, media, and publish only valid future events.
- Platform admins can approve organisers and manage fee rules, with conflict checks enforced.
- Role boundaries are verified: customers cannot access organiser/admin areas; organisers cannot access platform admin controls; platform organiser cannot become platform admin.
- Stripe test checkout has been completed through a signed webhook on staging, with order, ticket, QR token, and ledger verified.
- Mobile/tablet views work at 320, 360, 375, 390, 414, 430, 768, 820, 1024, 1280, 1440, and 1920 px widths.
- Accessibility basics pass: keyboard-only navigation, focus management, labelled controls, readable contrast, touch targets, and 200 percent zoom.
- Loading, empty, validation, permission denied, failed payment, rejected upload, and network failure states are tested for major workflows.
- Uploaded seat-map PDF/image behavior is verified per reusable seat map, not per venue.
- No secrets, stack traces, raw local paths, or private customer data are exposed.

## Severity Definitions

| Severity | Meaning | Examples |
|---|---|---|
| Critical | Blocks commercial launch or creates financial/security risk | Payment issued without signed webhook, double booking, role escalation, secrets exposed |
| High | Blocks a core workflow for a role | Organiser cannot save event, public cannot buy, seat map mismatch |
| Medium | Important usability/data issue with workaround | Responsive layout poor on tablet, confusing validation |
| Low | Cosmetic/minor wording | Spacing, copy polish, minor alignment |

## Required Test Data

- Reserved-only event.
- Non-reserved-only event.
- Mixed reserved and non-reserved event.
- Event with no uploaded seat-map asset.
- Event with uploaded seat-map PDF/image.
- Venue with multiple reusable seat maps.
- At least two organisers and two customers for isolation tests.
- Stripe test connected organiser account for payment tests.

## Workflow Coverage Summary

| Role | Core workflows |
|---|---|
| Public visitor | Home, Discover, event detail, booking tabs, seat-map tab, register/sign in paths |
| Customer | Register, login, checkout, reserved seats, non-reserved seats, My tickets, orders, profile/privacy |
| Organiser | Venues, reusable seat maps, PDF/image per seat map, event creation/editing, event-level ticket pricing, media, Stripe readiness, finance |
| Platform organiser | Global organiser support login, organiser context switching, organiser workflows without admin powers |
| Platform admin | Organiser applications, fee rules add/edit/delete, conflict checks, system readiness, role/privacy boundaries |

## Key Commercial Risks To Target

- Public page shows ticket types that are not allocated to the event.
- Event uses the wrong seat map after organiser selects a different reusable map.
- Non-reserved sections disappear from public booking.
- Customer-facing Seat-map tab shows a fake/generated structured map when no PDF/image was uploaded.
- Payment success redirect issues tickets without signed webhook.
- Organiser/admin role boundaries leak data or controls.
- Platform fee rules allow conflicting active policies.
- Mobile checkout basket clips labels or makes controls unusable.
- Event date validation allows events before the current date.
"""
    (OUT / f"Zavvion-Commercial-Lite-Human-Test-Plan-{STAMP}.md").write_text(plan_md, encoding="utf-8")

    (OUT / "README-FIRST.md").write_text(f"""# Human Testing Pack - {PROJECT}

Open these in order:

1. `Zavvion-Commercial-Lite-Human-Test-Plan-{STAMP}.docx` or `.md` for the test approach.
2. `Zavvion-Commercial-Lite-Test-Matrix-{STAMP}.xlsx` for executable pass/fail logs.
3. `Zavvion-Commercial-Lite-Site-Workflow-Map-{STAMP}.html` for the graphical site/workflow map.
4. `Zavvion-Commercial-Lite-Test-Execution-Summary-{STAMP}.md` at the end of testing to record the release verdict.

Before sending to testers, the human architect should fill in staging URL, role credentials, Stripe test-card guidance, demo event names/slugs, and screenshot upload location.

Do not give testers production secrets or Stripe secret keys.
""", encoding="utf-8")

    (OUT / f"Zavvion-Commercial-Lite-Tester-Quick-Start-{STAMP}.md").write_text(f"""# {PROJECT} - Tester Quick Start

The human architect/test lead must complete this page before sending the pack to testers.

## Test Environment

- Staging URL:
- Build/commit:
- Test window:
- Screenshot/video upload folder:
- Issue tracker link:
- Emergency contact:

## Role Assignments

| Tester | Role | Account alias/email | Workbook sheet | Devices |
|---|---|---|---|---|
| | Public visitor | No login | `01 Public Visitor`, `09 Responsive`, `12 State Error Matrix` | |
| | Customer | customer account | `02 Customer`, `11 Accessibility`, `13 Device Journeys` | |
| | Organiser | organiser account | `03 Organiser`, `06 Booking Seat Maps`, `11 Accessibility` | |
| | Platform organiser | platform organiser account | `04 Platform Organiser`, `08 Security Negative` | |
| | Platform admin | platform admin account | `05 Platform Admin`, `07 Payments Stripe`, `08 Security Negative` | |

## Canonical Demo/Test Events

| Scenario | Event name/slug | Venue | Seat map | Notes |
|---|---|---|---|---|
| Reserved-only | | | | |
| Non-reserved-only | | | | |
| Mixed reserved/non-reserved | | | | |
| Uploaded seat-map PDF/image | | | | |
| No uploaded seat-map PDF/image | | | | |

## Exact Route Entry Points

- Home:
- Events/discover:
- Mixed event:
- Customer account:
- Organiser dashboard:
- Organiser venues:
- Organiser ticketing:
- Platform admin:
- Organiser application:

## First Test For Each Role

1. Public visitor: open Home -> Discover -> Mixed event -> Booking tab.
2. Customer: sign in -> My tickets -> buy one non-reserved ticket.
3. Organiser: sign in -> Venues -> create/check reusable seat map -> Events -> create/edit event.
4. Platform organiser: sign in -> switch organiser context -> verify no admin menu.
5. Platform admin: sign in -> organiser applications -> fee rules conflict check.

## Evidence Rule

Every failed or blocked test must include URL, role, screenshot/video, steps, expected result, actual result, and browser/device.
""", encoding="utf-8")

    (OUT / f"Zavvion-Commercial-Lite-Test-Execution-Summary-{STAMP}.md").write_text(f"""# {PROJECT} - Test Execution Summary

**Build/version tested:**  
**Environment URL:**  
**Test start date:**  
**Test end date:**  
**Lead tester:**  
**Release candidate:** Yes / No

## Overall Verdict

- [ ] Ready for commercial lite launch
- [ ] Conditional - launch only after listed issues fixed
- [ ] Not ready

## Pass/Fail Summary

| Area | Total | Passed | Failed | Blocked | Not run | Notes |
|---|---:|---:|---:|---:|---:|---|
| Public visitor | | | | | | |
| Customer | | | | | | |
| Organiser | | | | | | |
| Platform organiser | | | | | | |
| Platform admin | | | | | | |
| Booking/seat maps | | | | | | |
| Stripe/payment | | | | | | |
| Security negative | | | | | | |
| Accessibility | | | | | | |
| State/error matrix | | | | | | |
| Device journeys | | | | | | |
| Responsive/device | | | | | | |
| Regression | | | | | | |

## Open Critical/High Issues

| Issue ID | Severity | Area | Summary | Owner | Target fix date | Retest status |
|---|---|---|---|---|---|---|
| | | | | | | |

## Sign-Off Checklist

- [ ] All P0 tests passed.
- [ ] No open Critical issues.
- [ ] No open High issues without named owner and accepted launch decision.
- [ ] Stripe signed webhook test passed on deployed server.
- [ ] Organiser connected account readiness verified.
- [ ] Seat-map PDF/image behavior verified per reusable seat map.
- [ ] Role boundaries verified for customer, organiser, platform organiser, and platform admin.
- [ ] Mobile/tablet testing completed on real devices, not only browser emulation.
- [ ] Production/staging `.env` reviewed without exposing secrets.
- [ ] PHP GD and media scanning confirmed on deployed server.
- [ ] Static secret/source/artifact URL checks return 403/404.
- [ ] Local review/showcase/password-free role pages are removed, blocked, or proven production-safe.
""", encoding="utf-8")

    (OUT / f"Zavvion-Commercial-Lite-Role-Walkthrough-Scripts-{STAMP}.md").write_text(f"""# {PROJECT} - Role Walkthrough Scripts

Use these scripts as human tester run-books. Testers should still record results in the Excel workbook.

## Public Visitor Script

1. Open the public home page.
2. Confirm the logo, navigation, Discover, Sign in, and Host events paths are visible and correctly styled.
3. Browse events.
4. Filter by category, status, reserved seating, and non-reserved seating.
5. Open the mixed seating event.
6. Confirm About, Booking, and Seat-map tabs.
7. Confirm the Seat-map tab is blank when no PDF/image was uploaded, and shows the uploaded PDF/image only when one exists for that exact reusable seat map.
8. In Booking, select a reserved seat and confirm the basket asks for a ticket type.
9. Add non-reserved tickets and confirm no exact seat is needed.
10. Confirm only event-allocated ticket types appear.
11. Attempt checkout and confirm unsafe public payment methods such as cash/counter terminal are not available.

## Customer Script

1. Register a new customer, then log out and log back in.
2. Open My tickets and confirm no other customer data appears.
3. Buy one non-reserved ticket.
4. Buy one reserved seat ticket.
5. Confirm tickets/orders appear under the customer account.
6. Confirm selected reserved seats become unavailable to another browser/customer.
7. Edit profile details and refresh.
8. Request data export/delete account support flows if present.
9. Attempt direct organiser/admin URLs and record the 403/redirect result.

## Organiser Script

1. Log in as organiser.
2. Confirm the menu only shows organiser functions.
3. Create a venue.
4. Create a reusable seat map under that venue.
5. Add at least one reserved seating section and one non-reserved seating section.
6. Upload or replace a customer-facing PDF/image for that exact reusable seat map.
7. Create a future event and select the exact reusable seat map.
8. Try to create a past event and confirm validation blocks it.
9. Configure event-level ticket prices, quantities, and max/order values.
10. Remove one allocated ticket from the event and confirm the public event no longer offers it.
11. Enable section-specific ticket allocation and map ticket types to reserved/non-reserved sections.
12. Upload event media and confirm only that event's media is shown.
13. Preview and publish only when readiness checks allow it.
14. Open finance/Stripe readiness and verify the organiser can start Stripe Connect setup.

## Platform Organiser Script

1. Log in as platform organiser.
2. Switch organiser context.
3. Create/edit a venue or event within the selected organiser context.
4. Switch context and confirm the previous organiser's data does not appear.
5. Attempt to open admin fee rules, organiser approval, or super admin controls.
6. Record whether access is blocked cleanly.

## Platform Admin Script

1. Log in as platform admin.
2. Review organiser applications.
3. Submit an organiser application from `/organiser/apply`, then approve/reject/request info from admin.
4. Create a global fee rule for a country/currency.
5. Attempt to create a conflicting active fee rule and confirm the platform blocks it.
6. Edit, deactivate, and delete fee rules where allowed.
7. Confirm platform admin cannot unsafely assign/edit `platform_super_admin`.
8. Review deployment/system health.
9. Confirm no secrets, private tokens, passwords, or local paths are visible.

## Live Event-Day Stress Script

1. Two customers attempt the same reserved seat at the same time.
2. One customer abandons payment; another tries after hold expiry.
3. Organiser checks sales totals while buyers purchase.
4. Customer refreshes checkout during seat hold.
5. Mobile buyer selects seats and checks out on a small phone.
6. Admin checks readiness and payment logs.
7. If scanner is enabled, scan valid, duplicate, wrong-event, and cancelled tickets.
""", encoding="utf-8")

    (OUT / f"Zavvion-Commercial-Lite-Defect-Report-Template-{STAMP}.md").write_text(f"""# {PROJECT} - Defect Report Template

Copy this for each issue or use the `Issue Log` sheet in the workbook.

## Issue Summary

- **Issue ID:**
- **Raised by:**
- **Date/time:**
- **Environment URL:**
- **Role/account used:**
- **Browser/device/viewport:**
- **Severity:** Critical / High / Medium / Low
- **Priority:** P0 / P1 / P2

## Problem

Describe what happened in plain language.

## Steps To Reproduce

1.
2.
3.

## Expected Result

What should have happened?

## Actual Result

What actually happened?

## Evidence

- Screenshot/video filename or link:
- Console/server log excerpt, with secrets redacted:

## Data Used

- Event:
- Venue:
- Seat map:
- Section:
- Ticket type:
- Order/payment reference, redacted:

## Retest

- **Fix version/build:**
- **Retest result:** Passed / Failed / Blocked
- **Retest evidence:**
- **Retested by/date:**
""", encoding="utf-8")

    (OUT / f"Zavvion-Commercial-Lite-Test-Data-Setup-Checklist-{STAMP}.md").write_text(f"""# {PROJECT} - Test Data Setup Checklist

The test lead or human architect should prepare this data before assigning testers.

## Accounts

- [ ] Public visitor: no login.
- [ ] Customer 1 with no orders.
- [ ] Customer 2 with at least one completed order.
- [ ] Organiser A with approved status.
- [ ] Organiser B with approved status for tenant isolation tests.
- [ ] Platform organiser support account.
- [ ] Platform admin account.
- [ ] Optional scanner/check-in account if scanner is included in the deployment.

## Events

- [ ] Reserved-only event with two reserved sections.
- [ ] Non-reserved-only event with at least three ticket types.
- [ ] Mixed event with two reserved sections and one non-reserved section.
- [ ] Event with uploaded customer-facing seat-map PDF/image.
- [ ] Event without uploaded customer-facing seat-map PDF/image.
- [ ] Event with donations enabled.
- [ ] Event with donations disabled.
- [ ] Event with merchandise enabled if merchandise is in scope.
- [ ] Event with merchandise disabled.
- [ ] Past-date attempt for validation testing.

## Venues And Seat Maps

- [ ] Venue with at least two reusable seat maps.
- [ ] Reusable seat map A: reserved section only.
- [ ] Reusable seat map B: mixed reserved and non-reserved sections.
- [ ] Seat map with uploaded PDF/image.
- [ ] Seat map with no uploaded PDF/image.
- [ ] Reserved section names that prove section tabs work, for example Sec RR and Sec WW.
- [ ] Non-reserved section, for example Stalls.

## Ticketing

- [ ] Reusable catalogue types: Adult, Child, Senior, Student, VIP, Carer/Companion, Family.
- [ ] Event-level price, quantity, and max/order for each allocated ticket.
- [ ] Removed/unallocated ticket type to prove it does not appear publicly.
- [ ] Section-specific ticket mappings for at least one event.
- [ ] Duplicate allocation attempt data.

## Payment

- [ ] Stripe test publishable key configured.
- [ ] Stripe test secret key configured on server.
- [ ] Stripe webhook signing secret configured.
- [ ] Connected organiser account with test readiness.
- [ ] Test card for success.
- [ ] Test card for failure.
- [ ] Test event/order for duplicate webhook replay.

## Security

- [ ] Known object IDs for organiser A and organiser B.
- [ ] Two customer orders for IDOR tests.
- [ ] XSS label test strings, stored in test notes only.
- [ ] Invalid upload samples: oversized file, wrong MIME file, renamed executable, malformed image/PDF.
""", encoding="utf-8")

    (OUT / f"Zavvion-Commercial-Lite-Site-Workflow-Map-{STAMP}.mmd").write_text("""flowchart TD
    Home["Home / Public Entry Brain"]
    Home --> Public["Public Visitor"]
    Home --> Auth["Sign in / Register"]
    Home --> Host["Host events with Zavvion"]

    Public --> Discover["Discover Events"]
    Discover --> Filters["Search / Filter / Sort"]
    Discover --> EventPage["Event Detail"]
    EventPage --> Booking["Booking Tab"]
    EventPage --> SeatMapTab["Seat-map Tab"]
    Booking --> Reserved["Reserved Seats"]
    Booking --> NonReserved["Non Reserved Seats"]
    Booking --> Checkout["Checkout"]

    Auth --> Customer["Customer"]
    Customer --> MyTickets["My Tickets"]
    Customer --> Orders["Orders"]
    Customer --> Profile["Profile / Privacy"]
    Customer --> CustomerBooking["Customer Booking Flow"]

    Host --> Apply["Organiser Application"]
    Auth --> Organiser["Organiser"]
    Organiser --> OrgEvents["Events"]
    Organiser --> Venues["Venues"]
    Organiser --> Ticketing["Ticketing"]
    Organiser --> Finance["Finance / Stripe"]
    Venues --> SeatBuilder["Seat-plan Builder"]
    SeatBuilder --> ReusableMaps["Reusable Seat Maps"]
    ReusableMaps --> MapPdf["Upload / Replace PDF or Image Per Seat Map"]
    OrgEvents --> EventSeatMap["Select Exact Seat Map"]
    Ticketing --> EventPrices["Event-Level Price / Quantity / Max Order"]
    Ticketing --> SectionRules["Section Ticket Allocation"]

    Auth --> PlatformOrganiser["Platform Organiser"]
    PlatformOrganiser --> ContextSwitch["Organiser Context Switch"]
    ContextSwitch --> Organiser

    Auth --> Admin["Platform Admin"]
    Admin --> Applications["Organiser Applications"]
    Admin --> FeeRules["Fee Rules"]
    Admin --> Readiness["System / Deployment Readiness"]
    Admin --> Security["Role / Payment / Privacy Oversight"]

    Checkout --> Stripe["Stripe Checkout"]
    Stripe --> SignedWebhook["Signed Webhook"]
    SignedWebhook --> TicketQr["Ticket + QR + Ledger"]
""", encoding="utf-8")

    (OUT / f"Zavvion-Commercial-Lite-Role-Permission-Matrix-{STAMP}.md").write_text("""# Role Permission Matrix

Human testers must verify both allowed and forbidden actions.

| Role | Must be able to access | Must be blocked from |
|---|---|---|
| Public visitor | Home, Discover, public event pages, organiser application page, sign in/register | Account pages, organiser console, admin console, private APIs, checkout draft/customer data |
| Customer | My tickets, orders, profile/privacy, booking checkout | Organiser console, admin console, other customer orders/tickets, fee rules, venue/event management |
| Organiser | Own organiser events, venues, reusable seat maps, ticketing, finance/Stripe readiness, own media | Platform admin, other organisers' data, customer lists outside allowed order context, super admin roles |
| Platform organiser | Permitted organiser contexts, organiser workflows in selected context | Platform admin, fee rule management, organiser approval, super admin, unrelated customer PII |
| Platform admin | Organiser applications, fee rules, readiness, platform monitoring | Unsafe self-escalation to platform_super_admin, secret values, direct customer payment method data |

## Required Negative Tests

- Change IDs in URLs/API calls for events, venues, seat maps, media, orders, tickets, checkout drafts.
- Open admin pages as customer/organiser/platform organiser.
- Open organiser pages as customer/public.
- Use stale browser tab after switching platform organiser context.
- Attempt role/permission changes outside allowed scope.
- Confirm every denial is 403/redirect/404 without private data.
""", encoding="utf-8")

    (OUT / f"Zavvion-Commercial-Lite-Staging-Test-Setup-And-Reset-Runbook-{STAMP}.md").write_text("""# Staging Test Setup And Reset Runbook

## Before A Test Cycle

1. Confirm deployed branch/commit.
2. Confirm staging `.env` uses production-like settings with no real customer data.
3. Confirm database backup/snapshot.
4. Confirm seed/demo data set is present.
5. Confirm Stripe test mode only.
6. Confirm media scanning/GD readiness if uploads are in scope.
7. Fill `Tester Quick Start` with staging URL, accounts, demo events, and evidence folder.

## Reset Between Cycles

- Do not use destructive reset on production-like data without architect approval.
- Clear or archive test orders only if the architect confirms accounting impact.
- Release stale seat holds using the documented scheduler/command.
- Remove test uploads if retention/media scan tests require a clean state.
- Recreate required demo events listed in `Test Data Setup`.
- Record reset time, owner, and any data intentionally preserved.

## Repeatability Requirements

- Every tester must know which event/venue/seat map to use.
- Payment tests must use Stripe test mode.
- Failed/blocked tests must be reproducible after reset.
- Test data names should include a date/time suffix where practical.
""", encoding="utf-8")

    (OUT / f"Zavvion-Commercial-Lite-Stripe-Payment-Webhook-Test-Runbook-{STAMP}.md").write_text("""# Stripe Payment And Webhook Test Runbook

## Required Stripe Dashboard Setup

- Test publishable key configured.
- Test secret key configured server-side only.
- Webhook signing secret configured server-side only.
- Connected organiser account configured in test mode.
- Endpoint subscribes to the exact event types used by the app, including checkout completion and any expiry/failure events the app handles.

## Required Tests

1. Successful Checkout Session completion.
   - Expected: signed webhook marks order paid and creates ticket, QR token, and ledger entries.
2. Unsigned actionable webhook.
   - Expected: rejected/signature_required; no ticket issuance.
3. Success redirect without webhook.
   - Expected: no paid status, no ticket issuance.
4. Duplicate webhook replay.
   - Expected: idempotent; no duplicate tickets/ledger.
5. Failed payment.
   - Expected: no ticket; clear customer state; seat hold release policy correct.
6. Expired Checkout Session.
   - Expected: order/hold state correct.
7. Connected account incomplete/disabled.
   - Expected: paid event publishing or checkout blocks safely.
8. Amount/metadata tampering attempt.
   - Expected: server-calculated totals and metadata win; no underpayment.
9. SCA/3DS test card if supported.
   - Expected: customer challenge and final webhook state handled.

## Evidence To Capture

- Stripe event ID.
- Checkout Session ID.
- Order ID.
- Ticket IDs.
- QR token row count.
- Ledger rows and totals.
- Connected account ID redacted.
- Screenshots/log snippets with secrets redacted.
""", encoding="utf-8")

    (OUT / f"Zavvion-Commercial-Lite-Real-Device-Browser-Matrix-{STAMP}.md").write_text("""# Real Device And Browser Matrix

The width matrix in Excel is not enough by itself. Assign real people/devices here.

| Device/browser | Owner | Required journeys | Result | Evidence |
|---|---|---|---|---|
| iPhone Safari portrait | | Public mixed booking, customer ticket QR | | |
| iPhone Safari landscape | | Seat-map tab zoom/pan, basket readability | | |
| Android Chrome portrait | | Public checkout, non-reserved ticket quantity | | |
| Android Chrome landscape | | Reserved seat selection and ticket assignment | | |
| iPad Safari portrait | | Organiser event creation and venue seat-map builder | | |
| iPad Safari landscape | | Organiser ticketing and upload/replace seat-map PDF/image | | |
| Windows Edge | | Admin fee rules and organiser approvals | | |
| Windows Chrome | | Full public/customer/organiser smoke | | |
| Firefox desktop | | Public browse and account/profile smoke | | |
| 200 percent zoom desktop | | Home, event booking, organiser ticketing, admin fee rules | | |

Record screenshots/videos for failures and for at least one successful full mobile checkout.
""", encoding="utf-8")


def write_site_map() -> None:
    html = f"""<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>{PROJECT} - Site Workflow Map</title>
<style>
:root {{ --bg:#07120f; --panel:#111312; --line:#c9a34b; --text:#f4ead8; --muted:#b6a985; --accent:#d4aa4f; }}
body {{ margin:0; font-family: Inter, Arial, sans-serif; background: radial-gradient(circle at 50% 0%, #153d30, var(--bg) 48%, #030504); color:var(--text); }}
header {{ padding:24px 32px; border-bottom:1px solid rgba(201,163,75,.35); background:rgba(0,0,0,.45); position:sticky; top:0; z-index:2; }}
h1 {{ margin:0; font-size:28px; }}
p {{ color:var(--muted); line-height:1.55; }}
.map {{ padding:32px; display:grid; gap:24px; }}
.brain {{ margin:auto; width:min(420px,90vw); min-height:150px; border:2px solid var(--accent); border-radius:28px; display:grid; place-items:center; text-align:center; background:rgba(17,19,18,.88); box-shadow:0 20px 60px rgba(0,0,0,.45); }}
.brain strong {{ font-size:36px; display:block; }}
.branches {{ display:grid; grid-template-columns:repeat(5,minmax(220px,1fr)); gap:18px; align-items:start; }}
.branch {{ background:rgba(17,19,18,.90); border:1px solid rgba(201,163,75,.32); border-radius:18px; padding:18px; min-height:280px; }}
.branch h2 {{ margin:0 0 12px; font-size:18px; color:var(--accent); }}
.branch ul {{ margin:0; padding-left:20px; }}
.branch li {{ margin:9px 0; color:#eee2c8; }}
.branch li ul {{ margin-top:6px; }}
.badge {{ display:inline-block; padding:4px 8px; border-radius:999px; background:rgba(212,170,79,.18); color:#f1d38a; font-size:12px; margin-bottom:8px; }}
.flow {{ margin:0 32px 32px; background:rgba(17,19,18,.92); border:1px solid rgba(201,163,75,.35); border-radius:18px; padding:20px; overflow:auto; }}
.flow-grid {{ display:grid; grid-template-columns:repeat(4,minmax(230px,1fr)); gap:14px; }}
.step {{ border:1px solid rgba(201,163,75,.3); border-radius:14px; padding:14px; background:#0c1713; }}
.step strong {{ color:var(--accent); }}
@media (max-width:1200px) {{ .branches {{ grid-template-columns:repeat(2,minmax(220px,1fr)); }} .flow-grid {{ grid-template-columns:repeat(2,minmax(220px,1fr)); }} }}
@media (max-width:640px) {{ header,.map,.flow {{ padding:18px; margin:0; }} .branches,.flow-grid {{ grid-template-columns:1fr; }} .brain strong {{ font-size:28px; }} }}
</style>
</head>
<body>
<header><h1>{PROJECT} - Graphical Site And Workflow Map</h1><p>Home is the central brain. Each branch shows role entry points, menu items, and workflows testers must verify.</p></header>
<main class="map">
  <section class="brain"><div><span class="badge">Central brain</span><strong>Home</strong><p>Brand header, Discover, Sign in/Register, Host events, public navigation, role routing.</p></div></section>
  <section class="branches">
    <article class="branch"><h2>Public Visitor</h2><ul><li>Home<ul><li>Discover events</li><li>Host events CTA</li><li>Sign in/Register</li></ul></li><li>Events listing<ul><li>Search/filter/sort</li><li>Open event detail</li></ul></li><li>Event detail<ul><li>About</li><li>Booking tab</li><li>Seat-map tab</li><li>Checkout entry</li></ul></li></ul></article>
    <article class="branch"><h2>Customer Login</h2><ul><li>My tickets<ul><li>Upcoming</li><li>Past</li><li>Orders</li><li>Ticket/QR</li></ul></li><li>Profile<ul><li>Contact details</li><li>Communication preferences</li><li>Privacy/data requests</li></ul></li><li>Booking<ul><li>Reserved seats</li><li>Non reserved seats</li><li>Promo/donation if configured</li><li>Stripe checkout</li></ul></li></ul></article>
    <article class="branch"><h2>Organiser Login</h2><ul><li>Events<ul><li>Create future event</li><li>Edit event data</li><li>Media per event</li><li>Publish checks</li></ul></li><li>Venues<ul><li>Create venue</li><li>Seat-plan builder</li><li>Reusable seat maps</li><li>Upload/replace PDF/image per map</li></ul></li><li>Ticketing<ul><li>Reusable catalogue</li><li>Event-level price/quantity/max order</li><li>Section ticket allocation</li><li>Remove event allocations</li></ul></li><li>Finance/Stripe<ul><li>Connect Stripe</li><li>Readiness</li><li>Sales summaries</li></ul></li></ul></article>
    <article class="branch"><h2>Platform Organiser</h2><ul><li>Global support login<ul><li>Select organiser context</li><li>Use organiser workflows</li><li>Audit support changes</li></ul></li><li>Boundaries<ul><li>No fee rules</li><li>No organiser approvals</li><li>No super admin controls</li><li>No customer PII beyond allowed views</li></ul></li></ul></article>
    <article class="branch"><h2>Platform Admin</h2><ul><li>Admin dashboard<ul><li>System health</li><li>Deployment readiness</li></ul></li><li>Organisers<ul><li>Review applications</li><li>Approve/reject/request info</li></ul></li><li>Fee rules<ul><li>Add/edit/deactivate/delete</li><li>Conflict blocking</li></ul></li><li>Security oversight<ul><li>Role boundaries</li><li>Payment readiness</li><li>Audit logs</li></ul></li></ul></article>
  </section>
</main>
<section class="flow"><h2>Critical End-To-End Workflows</h2><div class="flow-grid">
  <div class="step"><strong>1. Organiser launch</strong><p>Create venue -> create reusable seat map -> create reserved/non-reserved sections -> upload seat-map PDF/image -> create future event -> attach exact seat map -> configure event ticket prices -> publish.</p></div>
  <div class="step"><strong>2. Public purchase</strong><p>Discover event -> choose Booking tab -> select reserved seat or non-reserved quantity -> assign ticket types -> checkout -> signed Stripe webhook -> ticket/QR issued.</p></div>
  <div class="step"><strong>3. Admin governance</strong><p>Review organiser application -> approve -> configure non-conflicting fee rules -> monitor Stripe/readiness -> verify no secrets or unsafe states.</p></div>
  <div class="step"><strong>4. Security challenge</strong><p>Try role escalation, ID swapping, forged payment redirects, unsigned webhooks, XSS labels, unsafe uploads, and cross-tenant access.</p></div>
</div></section>
</body>
</html>
"""
    (OUT / f"Zavvion-Commercial-Lite-Site-Workflow-Map-{STAMP}.html").write_text(html, encoding="utf-8")


def write_docx() -> Path:
    doc = Document()
    section = doc.sections[0]
    section.top_margin = Inches(0.7)
    section.bottom_margin = Inches(0.7)
    section.left_margin = Inches(0.75)
    section.right_margin = Inches(0.75)
    doc.styles["Normal"].font.name = "Arial"
    doc.styles["Normal"].font.size = Pt(10)
    for style_name, size in [("Title", 22), ("Heading 1", 15), ("Heading 2", 12)]:
        doc.styles[style_name].font.name = "Arial"
        doc.styles[style_name].font.size = Pt(size)

    p = doc.add_paragraph()
    p.alignment = WD_ALIGN_PARAGRAPH.CENTER
    r = p.add_run(PROJECT)
    r.bold = True
    r.font.size = Pt(22)
    r.font.color.rgb = RGBColor(55, 70, 55)
    p2 = doc.add_paragraph()
    p2.alignment = WD_ALIGN_PARAGRAPH.CENTER
    p2.add_run("Human Functional Testing Plan and Execution Guide").italic = True
    p3 = doc.add_paragraph()
    p3.alignment = WD_ALIGN_PARAGRAPH.CENTER
    p3.add_run(f"Generated {date.today().isoformat()} | Commercial Lite MVP")

    doc.add_heading("Purpose", 1)
    doc.add_paragraph("This document tells human testers exactly how to validate the Zavvion Events Commercial Lite MVP across public booking, customer accounts, organiser setup, platform organiser support, platform administration, payments, security boundaries, and responsive/device behaviour.")
    doc.add_heading("Testing Rules", 1)
    for item in [
        "Run P0 tests before anything else. A failed P0 means no commercial launch until fixed and retested.",
        "Record every test result in the workbook. Use Passed, Failed, Blocked, Not Run, or Retest Passed.",
        "Every failed or blocked result needs a screenshot or screen recording and a clear reproduction path.",
        "Do not use production secrets in the test log. Redact payment keys, tokens, cookies, and personal data.",
        "Test on deployed staging, not only local XAMPP, before launch approval.",
    ]:
        doc.add_paragraph(item, style="List Bullet")

    doc.add_heading("Role Coverage", 1)
    t = doc.add_table(rows=1, cols=4)
    t.alignment = WD_TABLE_ALIGNMENT.CENTER
    for i, h in enumerate(["Role", "Login identity", "What to test", "Must not happen"]):
        t.rows[0].cells[i].text = h
        t.rows[0].cells[i].paragraphs[0].runs[0].bold = True
    for row in ROLES:
        cells = t.add_row().cells
        for i, value in enumerate(row):
            cells[i].text = value
            cells[i].vertical_alignment = WD_CELL_VERTICAL_ALIGNMENT.CENTER

    doc.add_heading("Critical Workflows", 1)
    t2 = doc.add_table(rows=1, cols=2)
    t2.alignment = WD_TABLE_ALIGNMENT.CENTER
    for i, h in enumerate(["Workflow", "Expected evidence"]):
        t2.rows[0].cells[i].text = h
        t2.rows[0].cells[i].paragraphs[0].runs[0].bold = True
    for workflow, evidence in [
        ("Organiser launch", "Venue -> seat map -> sections -> PDF/image per seat map -> event -> exact seat map -> event-level ticket pricing -> publish."),
        ("Public booking", "Discover -> event detail -> Booking tab -> reserved/non-reserved selection -> checkout -> signed webhook -> ticket/QR."),
        ("Admin governance", "Organiser applications -> fee rules -> conflict checks -> readiness/security oversight."),
        ("Security challenge", "Role boundaries, IDOR, CSRF, XSS, upload safety, unsigned webhook, fake redirect, private data exposure."),
    ]:
        cells = t2.add_row().cells
        cells[0].text = workflow
        cells[1].text = evidence

    doc.add_heading("Exit Criteria", 1)
    for item in [
        "All P0 tests pass on the deployed server.",
        "Stripe test checkout through signed webhook proves order, ticket, QR, and ledger creation.",
        "No Critical or unresolved High issue remains.",
        "Mobile/tablet tests are completed on real devices or accepted emulation plus at least one real phone check.",
        "The final execution summary is completed and signed by product, engineering, and QA owner.",
    ]:
        doc.add_paragraph(item, style="List Bullet")

    doc.add_heading("Detailed Logs", 1)
    doc.add_paragraph("Use the accompanying Excel workbook for the full scenario matrix, credentials template, issue log, responsive matrix, and daily sign-off sheets. Use the HTML site map to orient testers before execution.")
    path = OUT / f"Zavvion-Commercial-Lite-Human-Test-Plan-{STAMP}.docx"
    doc.save(path)
    return path


def style_sheet(ws) -> None:
    ws.freeze_panes = "A2"
    ws.sheet_view.showGridLines = False
    header_fill = PatternFill("solid", fgColor="0B1C17")
    header_font = Font(color="F4EAD8", bold=True)
    border = Border(bottom=Side(style="thin", color="C9A34B"))
    for cell in ws[1]:
        cell.fill = header_fill
        cell.font = header_font
        cell.alignment = Alignment(wrap_text=True, vertical="center")
        cell.border = border
    for row in ws.iter_rows(min_row=2):
        for cell in row:
            cell.alignment = Alignment(wrap_text=True, vertical="top")
    for idx, width in {
        1: 14, 2: 10, 3: 18, 4: 18, 5: 24, 6: 34, 7: 34, 8: 54,
        9: 54, 10: 20, 11: 16, 12: 18, 13: 14, 14: 32, 15: 14, 16: 34,
    }.items():
        ws.column_dimensions[get_column_letter(idx)].width = width
    ws.auto_filter.ref = ws.dimensions


def add_matrix_sheet(wb: Workbook, name: str, rows: list[list[str]]) -> None:
    ws = wb.create_sheet(name)
    ws.append(HEADERS)
    for row in rows:
        ws.append(row[:10] + ["Not Run", "", "", "", "", ""])
    style_sheet(ws)
    dv = DataValidation(type="list", formula1='"Not Run,Passed,Failed,Blocked,Retest Passed,Accepted Risk"', allow_blank=False)
    ws.add_data_validation(dv)
    if ws.max_row > 1:
        dv.add(f"K2:K{ws.max_row}")


def write_workbook() -> Path:
    wb = Workbook()
    ws = wb.active
    ws.title = "Instructions"
    for row in [
        ["Field", "Instruction"],
        ["Staging URL", "Architect/test lead fills this before testing. Do not test only local unless explicitly assigned."],
        ["Result", "Use Passed, Failed, Blocked, Retest Passed, Accepted Risk, or Not Run."],
        ["Evidence", "Add screenshot/video/log filename or link for every Failed/Blocked test and important payment/security pass."],
        ["Issue ID", "Use a tracking ID from Jira/GitHub/Excel issue log. Retest against the same ID."],
        ["Secrets", "Never paste passwords, Stripe secret keys, cookies, webhook secrets, or customer personal data into this workbook."],
        ["Execution", "Run all P0 tests first, then P1, then P2. Retest fixes before final sign-off."],
    ]:
        ws.append(row)
    style_sheet(ws)
    ws.column_dimensions["A"].width = 24
    ws.column_dimensions["B"].width = 110

    summary = wb.create_sheet("Coverage Summary", 0)
    summary.append(["Sheet", "Scenario count", "P0 count", "P1 count", "P2 count"])
    for sheet_name, rows in CASES.items():
        summary.append([sheet_name, len(rows), sum(1 for r in rows if r[1] == "P0"), sum(1 for r in rows if r[1] == "P1"), sum(1 for r in rows if r[1] == "P2")])
    summary.append([
        "TOTAL",
        sum(len(v) for v in CASES.values()),
        sum(1 for rows in CASES.values() for r in rows if r[1] == "P0"),
        sum(1 for rows in CASES.values() for r in rows if r[1] == "P1"),
        sum(1 for rows in CASES.values() for r in rows if r[1] == "P2"),
    ])
    style_sheet(summary)
    for i, width in enumerate([32, 18, 14, 14, 14], 1):
        summary.column_dimensions[get_column_letter(i)].width = width

    credentials = wb.create_sheet("Credentials")
    credentials.append(["Role", "Username/email", "Password supplied separately", "Environment URL", "Notes"])
    for role in ROLES:
        credentials.append([role[0], role[1], "Architect/test lead to fill", "", role[2]])
    style_sheet(credentials)
    for i, width in enumerate([24, 34, 30, 44, 70], 1):
        credentials.column_dimensions[get_column_letter(i)].width = width

    data = wb.create_sheet("Test Data Setup")
    data.append(["ID", "Data area", "Required record", "Configuration", "Why needed", "Created?", "Owner", "Notes"])
    for row in TEST_DATA_ROWS:
        data.append(row + ["No", "", ""])
    style_sheet(data)
    for i, width in enumerate([12, 18, 30, 60, 60, 14, 18, 34], 1):
        data.column_dimensions[get_column_letter(i)].width = width

    for sheet_name, rows in CASES.items():
        add_matrix_sheet(wb, sheet_name, rows)

    issue = wb.create_sheet("Issue Log")
    issue_headers = ["Issue ID", "Date raised", "Raised by", "Severity", "Priority", "Role/area", "Summary", "Steps to reproduce", "Expected", "Actual", "Environment/device", "Evidence link", "Owner", "Status", "Fix version", "Retest result", "Retest evidence", "Notes"]
    issue.append(issue_headers)
    for _ in range(50):
        issue.append([""] * len(issue_headers))
    style_sheet(issue)
    for i, width in enumerate([14, 14, 18, 12, 12, 20, 36, 54, 38, 38, 28, 32, 18, 16, 16, 18, 32, 34], 1):
        issue.column_dimensions[get_column_letter(i)].width = width
    dv = DataValidation(type="list", formula1='"New,Triaged,In Progress,Ready for Retest,Closed,Accepted Risk"')
    issue.add_data_validation(dv)
    dv.add("N2:N51")

    signoff = wb.create_sheet("Daily Signoff")
    signoff.append(["Date", "Tester", "Role/area tested", "P0 passed", "P0 failed", "P1/P2 notes", "Open blockers", "Ready to continue?", "Signature/initials"])
    for _ in range(20):
        signoff.append([""] * 9)
    style_sheet(signoff)
    for i, width in enumerate([14, 20, 28, 14, 14, 42, 42, 20, 20], 1):
        signoff.column_dimensions[get_column_letter(i)].width = width

    trace = wb.create_sheet("Acceptance Traceability")
    trace.append(["Requirement", "Workbook coverage", "Critical evidence", "Status", "Owner", "Notes"])
    for row in [
        ["Public buyer can browse and book allocated tickets only", "01 Public Visitor, 06 Booking Seat Maps", "Screenshots of event page and basket; unallocated ticket absence", "Not Started", "", ""],
        ["Reserved seats use 15-minute server hold and prevent double booking", "01 Public Visitor, 02 Customer, 06 Booking Seat Maps", "Concurrent browser test evidence", "Not Started", "", ""],
        ["Non-reserved sections sell quantity without seat selection", "01 Public Visitor, 06 Booking Seat Maps", "Mixed event evidence", "Not Started", "", ""],
        ["Seat map PDF/image is per reusable seat map", "03 Organiser, 06 Booking Seat Maps", "Upload/replace proof and public Seat-map tab", "Not Started", "", ""],
        ["Organiser creates future events only", "03 Organiser", "Past-date validation screenshot", "Not Started", "", ""],
        ["Event-level price, quantity, max/order", "03 Organiser", "Event allocation save and public price proof", "Not Started", "", ""],
        ["Platform admin fee conflicts blocked", "05 Platform Admin", "Conflict error evidence", "Not Started", "", ""],
        ["Role boundaries enforced", "02 Customer, 03 Organiser, 04 Platform Organiser, 08 Security Negative", "403/redirect/404 evidence", "Not Started", "", ""],
        ["Stripe signed webhook required before ticket issuance", "07 Payments Stripe", "Stripe event/order/ticket/QR/ledger evidence", "Not Started", "", ""],
        ["Secrets/static artifacts not publicly served", "08 Security Negative", "403/404 URL check evidence", "Not Started", "", ""],
        ["Public review/showcase routes protected", "08 Security Negative, 10 Regression", "Production-mode route evidence", "Not Started", "", ""],
        ["Mobile/tablet usability", "09 Responsive, 13 Device Journeys", "Real device screenshots/videos", "Not Started", "", ""],
        ["Accessibility basics", "11 Accessibility", "Keyboard/focus/label/zoom evidence", "Not Started", "", ""],
        ["Loading/error/empty states", "12 State Error Matrix", "Screenshots/videos for failures and recovery", "Not Started", "", ""],
    ]:
        trace.append(row)
    style_sheet(trace)
    for i, width in enumerate([42, 42, 52, 16, 18, 34], 1):
        trace.column_dimensions[get_column_letter(i)].width = width
    dv_trace = DataValidation(type="list", formula1='"Not Started,In Progress,Passed,Failed,Accepted Risk"', allow_blank=False)
    trace.add_data_validation(dv_trace)
    dv_trace.add(f"D2:D{trace.max_row}")

    path = OUT / f"Zavvion-Commercial-Lite-Test-Matrix-{STAMP}.xlsx"
    wb.save(path)
    return path


def zip_pack() -> Path:
    manifest = {
        "project": PROJECT,
        "generated": date.today().isoformat(),
        "base_url": BASE_URL,
        "scenario_count": sum(len(v) for v in CASES.values()),
        "roles": [r[0] for r in ROLES],
        "files": sorted(p.name for p in OUT.iterdir() if p.is_file()),
        "notes": [
            "Credentials are placeholders; architect must fill passwords separately.",
            "Payment tests require Stripe test keys, webhook signing secret, and connected account.",
            "Responsive tests must be repeated on deployed server and real devices before launch.",
        ],
    }
    (OUT / "testing-pack-manifest.json").write_text(json.dumps(manifest, indent=2), encoding="utf-8")

    zip_path = ROOT / "handoff" / f"Zavvion-Commercial-Lite-Human-Testing-Pack-{STAMP}.zip"
    if zip_path.exists():
        zip_path.unlink()
    with zipfile.ZipFile(zip_path, "w", zipfile.ZIP_DEFLATED) as archive:
        for file in sorted(OUT.iterdir()):
            if file.is_file():
                archive.write(file, arcname=f"human-testing-commercial-lite-{STAMP}/{file.name}")
    return zip_path


def main() -> None:
    write_text_files()
    write_site_map()
    docx = write_docx()
    workbook = write_workbook()
    zip_path = zip_pack()
    print(f"OUT={OUT}")
    print(f"DOCX={docx}")
    print(f"XLSX={workbook}")
    print(f"ZIP={zip_path}")
    print(f"SCENARIOS={sum(len(v) for v in CASES.values())}")


if __name__ == "__main__":
    main()
