#include "wearable_renderer.h"
#include "qr_code.h"
#include "wifi_selection.h"
#include <algorithm>
#include <cassert>
#include <cstdio>
#include <string>
#include <vector>

struct Draw {std::string text;int x,y,w,h;};
struct Tri {int x0,y0,x1,y1,x2,y2;uint16_t color;};
struct Display {
    mutable size_t measured_characters=0;
    int pixels=16,line_height=16;std::vector<Draw> drawn;std::vector<Draw> boxes;
    std::vector<Tri> triangles;
    void setTextColor(unsigned,unsigned) {}
    void setTextDatum(int) {}
    int fontHeight() const {return line_height;}
    int textWidth(const char* t) const {
        measured_characters+=std::strlen(t);
        int width=0;
        for(const auto* p=t;*p;++p) {
            const auto c=static_cast<unsigned char>(*p);
            if((c&0xc0)==0x80) continue;
            width+=c<128?(pixels*11+19)/20:pixels;
        }
        return width;
    }
    void drawString(const char* t,int x,int y) {drawn.push_back({t,x,y,textWidth(t),fontHeight()});}
    void fillRoundRect(int x,int y,int w,int h,int,unsigned) {record(x,y,w,h);}
    void drawRoundRect(int x,int y,int w,int h,int,unsigned) {record(x,y,w,h);}
    void fillRect(int x,int y,int w,int h,unsigned) {record(x,y,w,h);}
    void drawRect(int x,int y,int w,int h,unsigned) {record(x,y,w,h);}
    // The primitive takes an explicit colour; the test records it so a missing
    // or wrong colour cannot pass just because fillTriangle was called.
    void fillTriangle(int x0,int y0,int x1,int y1,int x2,int y2,uint16_t color) {
        const int x=std::min({x0,x1,x2}),y=std::min({y0,y1,y2});
        const int w=std::max({x0,x1,x2})-x,h=std::max({y0,y1,y2})-y;
        triangles.push_back({x0,y0,x1,y1,x2,y2,color});
        record(x,y,w,h);
    }
    void record(int x,int y,int w,int h) {boxes.push_back({"",x,y,w,h});check(x,y,w,h);}
    static void check(int x,int y,int w,int h) {
        for(int px:{x,x+w}) for(int py:{y,y+h}) {
            if((px-234)*(px-234)+(py-233)*(py-233)>217*217) {
                std::fprintf(stderr,"unsafe box x=%d y=%d w=%d h=%d\n",x,y,w,h);assert(false);
            }
        }
    }
    void verify() {for(const auto& d:drawn) check(d.x,d.y,d.w,d.h);drawn.clear();boxes.clear();triangles.clear();}
    bool has(const char* t) const {for(const auto& d:drawn) if(d.text.find(t)!=std::string::npos) return true;return false;}
    // Count primitives touching a y-band, used to prove the charging bolt is drawn.
    int boxes_in_band(int y,int tolerance=14) const {
        int count=0;for(const auto& b:boxes) if(b.y>=y-tolerance && b.y<=y+tolerance) ++count;return count;
    }
    // The drawn text baseline for one exact string, or -1 when absent.
    int y_of(const char* t) const {for(const auto& d:drawn) if(d.text==t) return d.y;return -1;}
    int height_of(const char* t) const {for(const auto& d:drawn) if(d.text==t) return d.h;return -1;}
    bool any_box_at(int y) const {for(const auto& b:boxes) if(b.y==y) return true;return false;}
    bool has_box_near(int y) const {for(const auto& b:boxes) if(b.y<=y && y<=b.y+b.h) return true;return false;}
};
struct FakeFonts {
    template<class Gfx> void apply(Gfx& g,wearable::TypeRole role,wearable::TextSize size,
                                   unsigned,unsigned=0) const {
        g.pixels=wearable::nominal_pixels(role,size);
        g.line_height=role==wearable::TypeRole::WatchTime?129:g.pixels;
    }
};

int main() {
    {
        Display display;FakeFonts fonts;wearable::UiState ui;wearable::ShellView view;
        std::string text;for(unsigned i=0;i<2000;++i)text+="Long response paragraph. ";
        auto layout=wearable::ShellRenderer<Display,FakeFonts>::agent_body_layout(display,fonts,ui,text.c_str());
        view.selected=true;view.final_text=text.c_str();view.state="DONE";view.agent_lines=&layout.lines;
        display.measured_characters=0;
        wearable::ShellRenderer<Display,FakeFonts>(display,fonts,ui,view).agent();
        assert(display.measured_characters<2000); // only heading and visible rows, not the full body
    }

    wearable::UiState ui;ui.boot({});wearable::ShellView v;
    v.selected=true;v.agent="PI";v.project="esp32-dev";v.session="Long session 中文名稱";
    v.model="gpt-5.6-sol";v.effort="high";v.state="RUNNING";v.activity_type="tool";
    v.activity="Running tests...";v.recent="正在检查中文。\nMixed English activity。";
    v.time="18:42";v.date="Sun Oct 04";v.battery="BAT 67%";v.battery_detail="Battery 67%  3.91V";v.firmware="0.6.0";
    v.build_detail="fw 0.6.0  2026-10-08 09:30 UTC";
    v.battery_valid=true;v.battery_percent=67;
    v.host_state="Busy";v.task_state="Running";v.codex_percent="72%";v.codex_reset="23:40";
v.wifi_state="CONNECTED";v.wifi_ssid_detail="Home-WiFi";
    v.host_tone=wearable::TaskTone::Primary;v.task_tone=wearable::TaskTone::Accent;v.codex_valid=true;
    Display g;FakeFonts fonts;
    for(auto size:{wearable::TextSize::Compact,wearable::TextSize::Normal,wearable::TextSize::Large}) {
        ui.preferences.text_size=size;
        for(auto theme:{wearable::Theme::Dark,wearable::Theme::Dim}) {
            ui.preferences.theme=theme;
            for(bool online:{false,true}) {
                v.gateway=online;v.wifi=online;
                wearable::ShellRenderer<Display,FakeFonts> renderer(g,fonts,ui,v);
                for(auto rates:{std::pair<float,float>{842,10000},{123400000,98700000},{1e15f,1e18f},{0,0}}) {
                    renderer.monitor({"LIGHTHOUSE",true,18,53,72,rates.first,rates.second});
                    assert(g.has("CPU") && g.has("MEM") && g.has("DISK") && g.has("NET") && g.has("RX") && g.has("TX"));
                    for(const auto& d:g.drawn) if(d.text.rfind("RX",0)==0) assert(d.h==wearable::nominal_pixels(wearable::TypeRole::AgentBodyLarge,size));
                    g.verify();
                }
                renderer.watch();
                // Watch information hierarchy.
                assert(g.has("Host") && g.has("Task") && g.has("Codex"));
                assert(g.has("Busy") && g.has("Running") && g.has("72%") && g.has("23:40"));
                // The time keeps the largest role; the date is clearly smaller.
                const int time_h=g.height_of("18:42"), date_h=g.height_of("Sun Oct 04");
                assert(time_h==129);
                assert(date_h==wearable::nominal_pixels(wearable::TypeRole::WatchDate,size));
                assert(time_h>date_h);
                // Date sits above the time, which sits above the three rows.
                const int date_y=g.y_of("Sun Oct 04"), time_y=g.y_of("18:42");
                const int host_y=g.y_of("Host"), task_y=g.y_of("Task"), codex_y=g.y_of("Codex");
                assert(date_y<time_y && time_y<host_y && host_y<task_y && task_y<codex_y);
                // All label starts and value ends share the same safe column edges.
                for(const auto& d:g.drawn) {
                    if(d.text=="Host" || d.text=="Task" || d.text=="Codex")
                        assert(d.x==90 && d.h==24);
                    if(d.text=="Busy" || d.text=="Running  00:00" || d.text=="72% 23:40")
                        assert(d.x+d.w==378 && d.h==24);
                }
                assert(date_y==86 && time_y==144);
                assert(time_y+time_h>=466*0.58 && time_y+time_h<=466*0.60);
                assert(time_y+time_h<host_y);
                // Battery icon primitives exist and the numeric percentage stays visible.
                assert(g.any_box_at(44-8+1) || !g.boxes.empty());
                assert(g.has("67%"));
                // Normal Wi-Fi / Agent online text is gone from the Watch.
                assert(!g.has("Wi-Fi") && !g.has("Agent online") && !g.has("Agent offline"));
                assert(!g.has("BAT"));
                g.verify();
                renderer.agent();assert(g.has("TOOL ACTIVITY"));g.verify();
                renderer.help();assert(g.has("Wearable Agent Console") && g.has("A  Next page"));
                assert(g.has("fw 0.6.0  2026-10-08 09:30 UTC"));g.verify();
                for(auto menu:{wearable::Menu::Root,wearable::Menu::Agent,wearable::Menu::View,wearable::Menu::Device,
                              wearable::Menu::Network,wearable::Menu::KnownNetworks,
                              wearable::Menu::Display,wearable::Menu::System}) {
                    ui.menu=menu;
                    if(menu==wearable::Menu::KnownNetworks)
                        ui.set_known({"Home-WiFi","Office","Phone-Hotspot"});
                    for(unsigned i=0;i<ui.items();++i) {
                        ui.index=i;renderer.settings();
                        // The Network page names the current network and offers
                        // both actions; the Known Networks page lists SSIDs and
                        // a confirmed Forget All row.
                        if(menu==wearable::Menu::Network && i==0) {
                            assert(g.has("Wi-Fi Setup") && g.has("Known Networks"));
                            // The current network row leads with "Network"; a
                            // very large text size may truncate the SSID, which
                            // is acceptable degradation (System shows it in full).
                            assert(g.has("Network"));
                        }
                        if(menu==wearable::Menu::KnownNetworks && i==0) {
                            assert(g.has("Home-WiFi") || g.has("Office") ||
                                   g.has("Phone-Hotspot") || g.has("Forget All"));
                        }
                        g.verify();
                    }
                }
                ui.menu=wearable::Menu::Edit;
                for(unsigned i=0;i<5;++i) {ui.field=wearable::Field(i);renderer.settings();g.verify();}
            }
        }
    }
    // The Network settings row shows the current SSID at the default text size.
    {
        Display ng;FakeFonts nf;wearable::UiState nui;nui.boot({});
        nui.settings_open=true;nui.menu=wearable::Menu::Network;nui.index=0;
        wearable::ShellView nv;nv.wifi_state="CONNECTED";nv.wifi_ssid_detail="Home-WiFi";nv.rssi=-48;
        wearable::ShellRenderer<Display,FakeFonts>(ng,nf,nui,nv).settings();
        assert(ng.has("Home-WiFi") && ng.has("Wi-Fi Setup") && ng.has("Known Networks"));
        ng.verify();
    }

    // Battery invalid shows an outline plus "--", never a stale number.
    v.battery_valid=false;v.battery_percent=0;
    wearable::ShellRenderer<Display,FakeFonts>(g,fonts,ui,v).watch();
    assert(g.has("--") && !g.has("67%") && !g.has("BAT"));g.verify();
    v.battery_valid=true;v.battery_percent=67;

    // Charging indicator: a drawn bolt appears only with external power, the
    // numeric percentage is never replaced by a glyph, and every bolt triangle
    // carries the explicit colour the primitive requires (graphics primitives
    // do NOT inherit the text colour).
    {
        Display cg;FakeFonts cf;
        wearable::ShellView cv;cv.battery_valid=true;cv.battery_percent=67;
        // Pin the theme so the expected bolt colours are unambiguous; the
        // shared `ui` was left on Dim by the loop above.
        ui.preferences.theme=wearable::Theme::Dark;
        const auto dark=wearable::palette(wearable::Theme::Dark);
        // Discharging: battery outline only, no bolt triangles at all.
        cv.battery_charge=wearable::ChargeState::Discharging;
        wearable::ShellRenderer<Display,FakeFonts>(cg,cf,ui,cv).watch();
        assert(cg.has("67%"));
        const int idle=cg.boxes_in_band(44);
        assert(cg.triangles.empty());
        cg.verify();
        // Charging: more primitives in the battery band than the plain gauge,
        // and both triangles are accent-coloured (not a default/zero colour).
        cv.battery_charge=wearable::ChargeState::Charging;
        wearable::ShellRenderer<Display,FakeFonts>(cg,cf,ui,cv).watch();
        assert(cg.has("67%"));
        assert(cg.boxes_in_band(44)>idle);
        assert(cg.triangles.size()==2);
        for(const auto& t:cg.triangles) assert(t.color==dark.accent);
        cg.verify();
        // Full at 100%: bolt stays, value is 100%, colour is the settled tone.
        cv.battery_charge=wearable::ChargeState::Full;cv.battery_percent=100;
        wearable::ShellRenderer<Display,FakeFonts>(cg,cf,ui,cv).watch();
        assert(cg.has("100%") && cg.boxes_in_band(44)>idle);
        assert(cg.triangles.size()==2);
        for(const auto& t:cg.triangles) assert(t.color==dark.done);
        cg.verify();
        // ExternalIdle (plugged, charger idle) still shows external power.
        cv.battery_charge=wearable::ChargeState::ExternalIdle;cv.battery_percent=98;
        wearable::ShellRenderer<Display,FakeFonts>(cg,cf,ui,cv).watch();
        assert(cg.has("98%") && cg.boxes_in_band(44)>idle);
        assert(cg.triangles.size()==2);
        for(const auto& t:cg.triangles) assert(t.color==dark.done);
        cg.verify();
    }

    // Codex unavailable degrades to a bare "--" row with no reset time.
    v.codex_valid=false;v.codex_percent="--";v.codex_reset="";
    wearable::ShellRenderer<Display,FakeFonts>(g,fonts,ui,v).watch();
    assert(g.has("Codex") && g.has("--") && !g.has("23:40"));g.verify();
    v.codex_valid=true;v.codex_percent="72%";v.codex_reset="23:40";

    // Cross-day reset keeps its compact weekday form inside the safe width.
    v.codex_percent="100%";v.codex_reset="Tue 08:00";
    wearable::ShellRenderer<Display,FakeFonts>(g,fonts,ui,v).watch();
    assert(g.has("100% Tue 08:00"));g.verify();
    v.codex_percent="72%";v.codex_reset="23:40";

    // Gateway offline surfaces through the rows, not a separate status line.
    v.gateway=false;v.host_state="Offline";v.host_tone=wearable::TaskTone::Secondary;
    v.task_state="Offline";v.task_tone=wearable::TaskTone::Secondary;
    wearable::ShellRenderer<Display,FakeFonts>(g,fonts,ui,v).watch();
    assert(g.has("Offline") && !g.has("Wi-Fi"));g.verify();
    v.gateway=true;v.host_state="Busy";v.task_state="Running";v.host_tone=wearable::TaskTone::Primary;

    ui.preferences.text_size=wearable::TextSize::Large;
    v.state="DONE";v.activity_type="text";
    v.final_text="你好，检查已经完成。English result with enough text to require multiple pages.\n第二段很长的结果保持可读取。第三段还是结果。第四段不丢失。";
    std::string seen;
    for(int page=0;page<20;++page) {
        v.now_ms=page*6000;
        wearable::ShellRenderer<Display,FakeFonts>(g,fonts,ui,v).agent();
        for(const auto& line:g.drawn) if(line.y>=181 && line.y<386) seen+=line.text;
        assert(!g.has("Running tests"));g.verify();
    }
    assert(seen.find("不丢失")!=std::string::npos);

    // B3: Live and manual Agent pagination must be ONE calculation, anchored at
    // the END of the body. Page 0 is exactly the live tail; page p+1 ends where
    // page p starts, so there is no overlap and no gap. Exercised at the text
    // size whose body holds exactly 9 rows, matching the acceptance repro.
    {
        // Collect the body line numbers actually painted for one view state.
        auto painted=[&]() {
            std::vector<int> numbers;
            for(const auto& line:g.drawn) {
                if(line.y<154||line.y>=404) continue;
                if(line.text.size()==3 && line.text[0]=='L')
                    numbers.push_back((line.text[1]-'0')*10+(line.text[2]-'0'));
            }
            return numbers;
        };
        // The body text must outlive the render call; v.final_text is a raw
        // pointer into it, so it is kept in a stable std::string.
        std::string body_storage;
        auto render=[&](int line_count,size_t page,bool manual) {
            body_storage.clear();
            for(int line=0;line<line_count;++line) {
                char entry[16];std::snprintf(entry,sizeof(entry),"L%02d",line);
                body_storage+=entry;body_storage+=(line+1<line_count)?"\n":"";
            }
            v.state="DONE";v.activity_type="text";v.final_text=body_storage.c_str();
            v.manual=manual;v.page_index=page;v.now_ms=0;
            g.drawn.clear();
            wearable::ShellRenderer<Display,FakeFonts>(g,fonts,ui,v).agent();
            // Collect before verify(): verify() clears the recorded draws.
            const auto numbers=painted();
            g.verify();
            return numbers;
        };
        // Every case must tile [0,N) exactly once with no overlap and no gap,
        // starting from the live tail at page 0.
        auto check_case=[&](int line_count,std::vector<std::pair<int,int>> expected) {
            const auto geometry=wearable::ShellRenderer<Display,FakeFonts>::agent_body_layout(
                g,fonts,ui,"probe");            // probe text only for the row count
            assert(geometry.rows==9);            // the acceptance configuration
            // Live must equal manual page 0, boundary for boundary.
            const auto live=render(line_count,0,false);
            const auto page0=render(line_count,0,true);
            assert(live==page0);
            const auto real=wearable::ShellRenderer<Display,FakeFonts>::agent_body_layout(
                g,fonts,ui,body_storage.c_str());
            const auto expected_pages=real.pages;
            assert(expected_pages==std::max<size_t>(1,
                (size_t(line_count)+geometry.rows-1)/geometry.rows));
            // The helper's own ranges must equal the acceptance ranges and tile
            // the body exactly, independently of the drawing path. Page index
            // increases toward OLDER lines, so the ranges are checked as a
            // per-line coverage map rather than a forward walk.
            {
                std::vector<int> hits(real.lines.size(),0);
                for(size_t page=0;page<real.pages;++page) {
                    const auto r=real.range(page);
                    assert(r.end<=real.lines.size());
                    // Page 0 is the live tail: its end is the end of the body.
                    if(page==0) assert(r.end==real.lines.size());
                    if(page+1==real.pages) assert(r.start==0);
                    for(size_t i=r.start;i<r.end;++i) ++hits[i];
                    if(!expected.empty())
                        assert(int(r.start)==expected[page].first && int(r.end-1)==expected[page].second);
                }
                for(size_t i=0;i<hits.size();++i) assert(hits[i]==1);
            }
            assert(expected.empty() || expected.size()==expected_pages);
            std::vector<int> union_all;
            std::vector<int> previous;
            for(size_t page=0;page<expected_pages;++page) {
                const auto range=render(line_count,page,true);
                if(!expected.empty()) {
                    // Exact acceptance ranges (inclusive bounds).
                    const int lo=expected[page].first,hi=expected[page].second;
                    assert(!range.empty() && range.front()==lo && range.back()==hi);
                    assert(range.size()==size_t(hi-lo+1));
                }
                if(page>0) {
                    // Adjacent pages never overlap: the older page's highest line
                    // is strictly below the newer page's lowest line.
                    assert(!previous.empty() && !range.empty());
                    assert(range.back()<previous.front());
                    assert(range.front()==previous.front()-int(range.size()));
                }
                // Within a page the lines are contiguous and increasing.
                for(size_t i=1;i<range.size();++i) assert(range[i]==range[i-1]+1);
                union_all.insert(union_all.end(),range.begin(),range.end());
                previous=range;
            }
            // The pages together contain every body line exactly once.
            std::sort(union_all.begin(),union_all.end());
            for(int line=0;line<line_count;++line) assert(union_all[size_t(line)]==line);
            assert(union_all.size()==size_t(line_count));
            // Key invariant from the acceptance repro: the live tail and the
            // first Older page must not share a single line.
            if(expected_pages>1) {
                const auto first_older=render(line_count,1,true);
                for(int a:live) for(int b:first_older) assert(a!=b);
            }
            // Walking Older then Newer returns exactly the same page sequence,
            // so no page is skipped, repeated or reversed on the way back.
            std::vector<std::vector<int>> forward;
            for(size_t page=0;page<expected_pages;++page) forward.push_back(render(line_count,page,true));
            std::vector<std::vector<int>> backward;
            for(size_t page=expected_pages;page-- >0;) backward.push_back(render(line_count,page,true));
            std::reverse(backward.begin(),backward.end());
            assert(backward==forward);
            assert(render(line_count,0,true)==live);
        };
        ui.preferences.text_size=wearable::TextSize::Normal;
        // Case A: 40 lines / 9 rows -> 5 pages, oldest page short.
        check_case(40,{{31,39},{22,30},{13,21},{4,12},{0,3}});
        // Case B: exact division 36 / 9 -> 4 full pages.
        check_case(36,{{27,35},{18,26},{9,17},{0,8}});
        // Case C: fewer than one page -> single page, Older does not move.
        check_case(5,{{0,4}});
        // Case D: just over one page -> newest page is 1..9, oldest is line 0.
        check_case(10,{{1,9},{0,0}});
        // Case C/D via the live auto-advance too: the rotation can never leave
        // the body or repeat a line.
        for(int line_count:{5,10,40}) {
            body_storage.clear();
            for(int line=0;line<line_count;++line) {
                char entry[16];std::snprintf(entry,sizeof(entry),"L%02d",line);
                body_storage+=entry;body_storage+=(line+1<line_count)?"\n":"";
            }
            v.final_text=body_storage.c_str();v.manual=false;v.page_index=0;
            std::vector<int> all;
            for(int step=0;step<line_count+2;++step) {
                v.now_ms=uint64_t(step)*6000;
                g.drawn.clear();
                wearable::ShellRenderer<Display,FakeFonts>(g,fonts,ui,v).agent();
                const auto live=painted();
                assert(!live.empty());
                for(int a:live) assert(a>=0 && a<line_count);
                all.insert(all.end(),live.begin(),live.end());
                g.verify();
            }
            assert(!all.empty());
        }
        // The footer reports 1/N at newest and N/N at oldest, matching direction.
        std::string footer_body;
        for(int line=0;line<40;++line) {
            char entry[16];std::snprintf(entry,sizeof(entry),"L%02d",line);
            footer_body+=entry;footer_body+=(line+1<40)?"\n":"";
        }
        v.final_text=footer_body.c_str();
        const auto layout=wearable::ShellRenderer<Display,FakeFonts>::agent_body_layout(
            g,fonts,ui,v.final_text);
        assert(layout.pages==5);
        v.manual=true;v.page_index=0;v.now_ms=0;g.drawn.clear();
        wearable::ShellRenderer<Display,FakeFonts>(g,fonts,ui,v).agent();
        assert(g.has("Result 1/5"));g.verify();
        v.page_index=4;g.drawn.clear();
        wearable::ShellRenderer<Display,FakeFonts>(g,fonts,ui,v).agent();
        assert(g.has("Result 5/5"));g.verify();
        // Manual browse and the live view agree at page 0 even with a stale
        // non-zero page index clamped away.
        assert(render(40,0,true)==render(40,0,false));
        v.manual=false;v.page_index=0;v.final_text="";
        ui.preferences.text_size=wearable::TextSize::Large;
    }
    v.final_text="";

    for(const char* state:{"DORMANT","IDLE"}) {
        v.state=state;v.final_text="Existing reply 上次回复";v.now_ms=0;
        wearable::ShellRenderer<Display,FakeFonts>(g,fonts,ui,v).agent();
        assert(g.has(state) && g.has("LAST REPLY") && g.has("Existing reply") && !g.has("Running tests"));g.verify();
        v.final_text="";v.recent="";v.activity="";v.activity_type="text";
        wearable::ShellRenderer<Display,FakeFonts>(g,fonts,ui,v).agent();
        assert(g.has("Waiting for agent") && !g.has("No final response"));g.verify();
    }
    // ---- Wi-Fi Setup provisioning screen --------------------------------
    {
        Display pg;FakeFonts pf;
        wearable::UiState pui; pui.boot({});
        wearable::ShellView pv;
        // A real locally-encoded QR for the provisioning payload.
        static wearable::QrCode qr;
        char payload[160];
        wearable::provisioning_qr_payload("AgentPager-A2F3",
            wearable::PROVISIONING_AP_PASSWORD, payload, sizeof(payload));
        assert(qr.encode(payload, std::strlen(payload)));
        // The renderer takes a bool array; use uint8_t storage and reinterpret.
        static std::vector<uint8_t> modules;
        modules.assign(size_t(qr.size())*size_t(qr.size()), 0);
        for(int y=0;y<qr.size();++y)for(int x=0;x<qr.size();++x)
            modules[size_t(y)*size_t(qr.size())+size_t(x)]=qr.module(x,y)?1:0;
        pv.provisioning=true;
        pv.provision_ssid="AgentPager-A2F3";
        pv.provision_password=wearable::PROVISIONING_AP_PASSWORD;
        pv.provision_address=wearable::PROVISIONING_AP_ADDRESS;
        pv.provision_status="Connect to this Wi-Fi, then open the URL";
        pv.provision_qr_modules=reinterpret_cast<const bool*>(modules.data());
        pv.provision_qr_size=qr.size();
        wearable::ShellRenderer<Display,FakeFonts>(pg,pf,pui,pv).provisioning();
        // SSID, password and address are all shown, not just the QR.
        assert(pg.has("WIFI SETUP") && pg.has("AgentPager-A2F3"));
        assert(pg.has(wearable::PROVISIONING_AP_PASSWORD) && pg.has("192.168.4.1"));
        assert(pg.has("Password"));
        // Every drawn primitive stays round-safe (g.verify asserts this).
        pg.verify();
    }

    std::puts("Wearable renderer round-safe Watch/Agent/Help/Settings; glance rows, battery icon, date/time order; all sizes/themes PASS");
    std::puts("Agent Live == manual page 0; pages tile the body with no overlap/gap PASS");
}