// Host regression for the bounded Agent history cache and History view model.
#include "agent_history.h"
#include "history_view.h"
#include <algorithm>
#include <cassert>
#include <cstdio>
#include <string>
using namespace wearable;

namespace {
HistoryPage page_with(std::initializer_list<const char*> texts, const std::string& next) {
    HistoryPage page; page.valid = true; page.next_cursor = next; page.generation = 1;
    bool user = true;
    for (const char* text : texts) {
        HistoryMessage m;
        m.role = user ? HistoryRole::User : HistoryRole::Assistant;
        m.agent = "codex"; m.text = text;
        page.messages.push_back(m);
        user = !user;
    }
    return page;
}

void test_cache_bound() {
    AgentHistoryCache cache;
    assert(cache.empty());
    cache.reset(page_with({"a", "b"}, "c1"));
    // A single page is both the newest and the oldest entry in the cache.
    assert(cache.size() == 1 && cache.at_newest() && cache.at_oldest());
    // append_older makes the fetched older page current (see test_append_older_is_current).
    assert(cache.append_older(page_with({"c", "d"}, "c2")));
    assert(cache.index() == 1 && cache.at_oldest() && !cache.at_newest());
    assert(cache.append_older(page_with({"e", "f"}, "c3")));
    assert(cache.size() == AgentHistoryCache::kCachePages);
    // A fourth page evicts the oldest (newest end) and keeps the index valid.
    assert(cache.append_older(page_with({"g", "h"}, "c4")));
    assert(cache.size() == AgentHistoryCache::kCachePages);
    assert(cache.index() < cache.size());
    std::puts("history cache never exceeds kCachePages PASS");
}

// B2: the page returned by the server must become the page that is displayed.
// Cache content and the displayed page are one state, never two that drift.
void test_append_older_is_current() {
    AgentHistoryCache cache;
    cache.reset(page_with({"newest"}, "c1"));
    assert(cache.index() == 0 && cache.current().messages[0].text == "newest");
    assert(cache.append_older(page_with({"older-1"}, "c2")));
    assert(cache.index() == 1);
    assert(cache.current().messages[0].text == "older-1");   // page 2 is shown
    assert(cache.append_older(page_with({"older-2"}, "c3")));
    assert(cache.index() == 2);
    assert(cache.current().messages[0].text == "older-2");
    // Cross-page traversal is linear in both directions and never refetches.
    assert(cache.newer() && cache.current().messages[0].text == "older-1");
    assert(cache.newer() && cache.current().messages[0].text == "newest");
    assert(!cache.newer());
    assert(cache.older() && cache.older());
    assert(cache.at_oldest() && cache.current().messages[0].text == "older-2");
    std::puts("history append_older makes the fetched page current PASS");
}

// B2: eviction over kCachePages must keep the index in range while Older/Newer
// still walk the bounded cache linearly.
void test_cache_eviction_keeps_traversal_valid() {
    AgentHistoryCache cache;
    cache.reset(page_with({"p0"}, ""));                       // window start
    for (int i = 1; i <= 4; ++i)                              // p1..p4 -> evicts p0,p1
        assert(cache.append_older(page_with({i % 2 ? "p1" : "p2"}, "")));
    // 5 pages appended, at most kCachePages retained, index always in range.
    assert(cache.size() == AgentHistoryCache::kCachePages);
    assert(cache.index() < cache.size());
    assert(!cache.at_newest());
    // Newer walks toward the retained newest page and stops at the boundary;
    // Older walks back to the retained oldest page and stops there.
    size_t seen = 1;
    while (cache.newer()) ++seen;
    assert(cache.index() == 0 && cache.at_newest());
    size_t back = 1;
    while (cache.older()) ++back;
    assert(back == seen && cache.at_oldest() && cache.index() == cache.size() - 1);
    std::puts("history cache eviction keeps traversal linear and in range PASS");
}

void test_cache_paging() {
    AgentHistoryCache cache;
    cache.reset(page_with({"newest"}, ""));
    assert(cache.at_oldest() && cache.at_newest());
    assert(!cache.older());
    cache.reset(page_with({"n"}, "c1"));
    cache.append_older(page_with({"o"}, "c2"));
    // The append moved the current page onto the older page.
    assert(cache.at_oldest() && cache.index() == 1);
    assert(!cache.older());
    assert(cache.newer() && cache.at_newest() && cache.index() == 0);
    assert(!cache.newer());
    std::puts("history cache older/newer boundaries PASS");
}

void test_browse_state() {
    AgentBrowse browse;
    assert(browse.live_tail() && !browse.manual);
    browse.set_pages(3);
    assert(browse.older());                 // 0 -> 1
    assert(browse.manual && browse.page == 1);
    assert(browse.older());                 // 1 -> 2
    assert(browse.at_oldest_local());
    assert(!browse.older());                // caller may now enter History
    assert(browse.newer() && browse.page == 1);
    assert(browse.newer() && browse.page == 0);
    assert(!browse.manual);                 // returning to newest resumes Live Tail
    assert(!browse.newer());
    // A target change resets manual paging.
    browse.older(); browse.reset();
    assert(browse.live_tail() && browse.page == 0 && browse.page_count == 1);
    // Page count shrinks under the current page.
    AgentBrowse shrink; shrink.set_pages(4); shrink.older(); shrink.older(); shrink.older();
    assert(shrink.page == 3);
    shrink.set_pages(2);
    assert(shrink.page == 1);
    std::puts("AgentBrowse manual page / live tail restore / reset PASS");
}

void test_history_screen() {
    HistoryScreen screen;
    screen.load({"USER", "ASSISTANT"}, {"pi", "pi"}, {"hello", "hi there"}, {1, 1});
    assert(screen.count() == 2);
    assert(screen.header() == "USER" && screen.body() == "hello");
    assert(screen.position() == "1 / 2");
    assert(screen.agent_header() == "PI \xc2\xb7 HISTORY");
    assert(screen.newer() == false);        // already newest
    assert(screen.older());
    assert(screen.header() == "ASSISTANT" && screen.position() == "2 / 2");
    assert(screen.at_oldest());
    assert(!screen.older());                // no message older than the last
    assert(screen.newer() && screen.at_newest());
    assert(!screen.newer());
    std::puts("HistoryScreen linear paging and position PASS");
}

// A long message that needs several screens is traversed fully before the next
// older message, and the reverse is exactly reversible.
void test_history_multi_screen_message() {
    HistoryScreen screen;
    // Message 0 (newest) needs 3 screens, message 1 needs 2 screens, message 2
    // needs 1 screen.
    screen.load({"ASSISTANT", "USER", "ASSISTANT"}, {"pi", "pi", "pi"},
                {"long newest", "medium", "short"}, {3, 2, 1});
    assert(screen.count() == 3);
    assert(screen.page_count() == 3);
    assert(screen.position() == "1 / 3 \xc2\xb7 1 / 3");
    // Walk Older: 3 screens of msg0, then 2 of msg1, then 1 of msg2.
    const std::vector<std::string> expected = {
        "1 / 3 \xc2\xb7 1 / 3", "1 / 3 \xc2\xb7 2 / 3", "1 / 3 \xc2\xb7 3 / 3",
        "2 / 3 \xc2\xb7 1 / 2", "2 / 3 \xc2\xb7 2 / 2",
        "3 / 3",
    };
    std::vector<std::string> visited{screen.position()};
    while (screen.older()) visited.push_back(screen.position());
    assert(visited == expected);
    assert(screen.at_oldest());
    assert(!screen.older());
    // Walk Newer: exactly the reverse, screen for screen.
    std::vector<std::string> back;
    while (true) {
        back.push_back(screen.position());
        if (!screen.newer()) break;
    }
    std::reverse(back.begin(), back.end());
    assert(back == visited);
    assert(screen.at_newest());
    // Crossing a message boundary going newer lands on that message's last screen.
    HistoryScreen boundary;
    boundary.load({"A", "B"}, {"pi", "pi"}, {"aa", "bb"}, {1, 4});
    assert(boundary.page_count() == 1);
    assert(boundary.older());                    // msg0 screen0 -> msg1 screen0
    assert(boundary.index == 1 && boundary.page == 0 && boundary.page_count() == 4);
    assert(boundary.newer());                    // msg1 screen0 -> msg0 (single screen)
    assert(boundary.index == 0 && boundary.page == 0);
    // A single-screen message keeps the compact position form.
    HistoryScreen compact;
    compact.load({"USER"}, {"pi"}, {"one"}, {1});
    assert(compact.position() == "1 / 1");
    // A new page resets the text page and the message index.
    HistoryScreen reset;
    reset.load({"A", "B"}, {"pi", "pi"}, {"aa", "bb"}, {5, 5});
    reset.older(); reset.older();
    assert(reset.page != 0 || reset.index != 0);
    reset.load({"C"}, {"pi"}, {"cc"}, {2});
    assert(reset.index == 0 && reset.page == 0 && reset.page_count() == 2);
    assert(reset.position() == "1 / 1 \xc2\xb7 1 / 2");
    std::puts("HistoryScreen multi-screen message paging is linear and reversible PASS");
}

void test_utf8_bounds() {
    // A multi-byte code point must never be split by the byte limit.
    const std::string text = "\xe4\xbd\xa0\xe5\xa5\xbd\xe5\x90\x97";   // 9 bytes, 3 glyphs
    bool truncated = false;
    const auto exact = history_bounded_text(text, 9, truncated);
    assert(!truncated && exact == text);
    const auto cut = history_bounded_text(text, 4, truncated);
    assert(truncated);
    assert(cut.size() == 3);                 // 4 would split the second glyph
    assert(cut == "\xe4\xbd\xa0");
    std::puts("history_bounded_text truncates on a UTF-8 boundary PASS");
}

} // namespace

int main() {
    test_cache_bound();
    test_append_older_is_current();
    test_cache_eviction_keeps_traversal_valid();
    test_cache_paging();
    test_browse_state();
    test_history_screen();
    test_history_multi_screen_message();
    test_utf8_bounds();
    std::puts("Bounded Agent history cache / view / UTF-8 PASS (host)");
}