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

struct Draw {std::string text;int x,y,w,h;};
struct Display {
    int pixels=16,line_height=16;std::vector<Draw> drawn;std::vector<Draw> boxes;
    void setTextColor(unsigned,unsigned) {}
    void setTextDatum(int) {}
    int fontHeight() const {return line_height;}
    int textWidth(const char* t) const {
        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);}
    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();}
    bool has(const char* t) const {for(const auto& d:drawn) if(d.text.find(t)!=std::string::npos) return true;return false;}
    // 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() {
    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.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.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"));g.verify();
                for(auto menu:{wearable::Menu::Root,wearable::Menu::Agent,wearable::Menu::Device,
                              wearable::Menu::Display,wearable::Menu::System}) {
                    ui.menu=menu;
                    for(unsigned i=0;i<ui.items();++i) {ui.index=i;renderer.settings();g.verify();}
                }
                ui.menu=wearable::Menu::Edit;
                for(unsigned i=0;i<5;++i) {ui.field=wearable::Field(i);renderer.settings();g.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;

    // 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();
    }
    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");
}