#include "agent_target.h"
#include "button_gesture.h"
#include "shell_input_policy.h"
#include "ui_state.h"
#include "speech_control.h"
#include "input_sender_timing.h"
#include "voice_capture_limits.h"
#include "page_redraw_policy.h"
#include <cassert>
#include <cstring>
#include <cstdio>

// Host composition of production gesture/menu/identity code. Display drawing,
// task scheduling and USB must still be checked on the physical device.
struct Device {
    enum class Page { PI, NETWORK } page = Page::PI;
    enum class Modal { NONE, SETTINGS, VOICE } modal = Modal::NONE;
    unsigned epoch = 1, cancellations = 0;
    bool recording = false;
    SpeechMenu menu;
    void snapshot(const char *agent, const char *session) {
        if (agent_target_changed(true, "pi", "session-one", agent, session)) {
            ++cancellations;
            modal = Modal::NONE;
            recording = false;
            page = Page::PI;
        }
    }
};

static Gesture click(ButtonGesture &button, unsigned epoch, uint64_t start) {
    assert(button.update(true,start,epoch,false)==Gesture::NONE);
    assert(!button.down());
    assert(button.update(true,start+30,epoch,false)==Gesture::NONE);
    assert(button.down());
    assert(button.update(false,start+100,epoch,false)==Gesture::NONE);
    assert(button.down());
    const auto result=button.update(false,start+130,epoch,false);
    assert(!button.down());
    return result;
}

static Gesture hold(ButtonGesture &button, uint64_t epoch, uint64_t start,
                    InputModal modal, bool approval=false) {
    const bool allowed=shell_long_press_allowed(modal,approval);
    assert(button.update(true,start,epoch,allowed)==Gesture::NONE);
    assert(button.update(true,start+29,epoch,allowed)==Gesture::NONE && !button.down());
    assert(button.update(true,start+30,epoch,allowed)==Gesture::NONE && button.down());
    assert(button.update(true,start+629,epoch,allowed)==Gesture::NONE);
    return button.update(true,start+631,epoch,allowed);
}

static void settings_hold_regression() {
    wearable::UiState ui;ui.boot({});ui.app=wearable::App::Agent;
    unsigned voice_starts=0;
    for(auto menu:{wearable::Menu::Root,wearable::Menu::Agent,wearable::Menu::Device,
                   wearable::Menu::Display,wearable::Menu::System,wearable::Menu::Edit}) {
        ui.open_settings();ui.menu=menu;ui.editor_parent=wearable::Menu::Device;
        ui.draft.brightness=10;
        ButtonGesture a;
        const auto g=hold(a,1,0,InputModal::SETTINGS);
        assert(g==Gesture::LONG);
        // Same modal ownership order as production: Settings handles LONG first.
        if(g==Gesture::LONG && ui.settings_open) ui.settings_back();
        else if(g==Gesture::LONG) ++voice_starts;
        assert(ui.app==wearable::App::Agent && voice_starts==0);
        assert(ui.draft.brightness==ui.preferences.brightness);
        if(menu==wearable::Menu::Root) assert(!ui.settings_open);
        else assert(ui.menu==(menu==wearable::Menu::Edit?wearable::Menu::Device:wearable::Menu::Root));
        assert(a.update(false,700,2,false)==Gesture::NONE);
        assert(a.update(false,730,2,false)==Gesture::LONG_RELEASE);
        assert(a.update(false,1000,2,false)==Gesture::NONE); // No trailing CLICK.
    }
    ButtonGesture normal;assert(hold(normal,1,0,InputModal::NONE)==Gesture::LONG);
    ++voice_starts;assert(voice_starts==1);
    ButtonGesture approval;assert(hold(approval,2,0,InputModal::NONE,true)==Gesture::NONE);
    assert(!shell_long_press_allowed(InputModal::SETTINGS,true));
    ButtonGesture voice;assert(hold(voice,3,0,InputModal::VOICE)==Gesture::NONE);
    assert(!shell_long_press_allowed(InputModal::ERROR,false));
    // Double A/B only on the normal top-level Shell. Modals, approval and any
    // overlay keep immediate single CLICKs and never a DOUBLE_CLICK.
    assert(shell_double_press_allowed(InputModal::NONE,false,false));
    for(auto modal:{InputModal::VOICE,InputModal::SETTINGS,InputModal::ERROR})
        assert(!shell_double_press_allowed(modal,false,false));
    assert(!shell_double_press_allowed(InputModal::NONE,true,false));
    assert(!shell_double_press_allowed(InputModal::NONE,false,true));
    std::puts("Raw GPIO / 30ms debounce / >600ms Hold A / Settings Back / no Voice or trailing CLICK PASS (host)");
}

int main() {
    settings_hold_regression();
    for (const auto page:{DisplayPage::OVERVIEW,DisplayPage::CPU,DisplayPage::MEMORY,DisplayPage::DISK,DisplayPage::NETWORK}) {
        assert(server_snapshot_needs_redraw(page,false));
        assert(!server_snapshot_needs_redraw(page,true));
        assert(!agent_snapshot_needs_redraw(page,false,true));
    }
    assert(!server_snapshot_needs_redraw(DisplayPage::PI,false));
    // The Watch (Page::PI + ShellApp::Watch) redraws only on a derived change.
    assert(watch_snapshot_needs_redraw(DisplayPage::PI,ShellApp::Watch,false,true));
    assert(!watch_snapshot_needs_redraw(DisplayPage::PI,ShellApp::Watch,false,false));
    assert(!watch_snapshot_needs_redraw(DisplayPage::PI,ShellApp::Watch,true,true));
    // The Agent page never repaints on a server snapshot.
    assert(!watch_snapshot_needs_redraw(DisplayPage::PI,ShellApp::Agent,false,true));
    assert(!watch_snapshot_needs_redraw(DisplayPage::OVERVIEW,ShellApp::Monitor,false,true));
    assert(!agent_snapshot_needs_redraw(DisplayPage::PI,false,false));
    assert(!agent_snapshot_needs_redraw(DisplayPage::PI,true,true));
    assert(agent_snapshot_needs_redraw(DisplayPage::PI,false,true));
    assert(!agent_snapshot_needs_redraw(DisplayPage::PI,false,true,ShellApp::Watch));
    assert(!agent_snapshot_needs_redraw(DisplayPage::PI,false,true,ShellApp::Help));
    assert(agent_snapshot_needs_redraw(DisplayPage::PI,false,true,ShellApp::Agent));
    // Actual ESP-IDF config: 100 Hz, so 5 ms truncates to zero ticks.
    // A runnable priority-5 sender can starve the priority-1 button loop.
    constexpr unsigned tick_hz = 100;
    constexpr unsigned converted = 5U * tick_hz / 1000U;
    static_assert(converted == 0);
    static_assert(input_sender_idle_ticks(converted) == 1);
    static_assert(input_sender_idle_ticks(5U * 1000U / 1000U) == 5);
    // The old eight-frame queue covered only 320 ms, less than the observed
    // startup write stalls. Buffer at least the actual bounded send timeout.
    static_assert(8U * VOICE_CAPTURE_FRAME_MS < VOICE_SEND_TIMEOUT_MS);
    static_assert(VOICE_CAPTURE_QUEUE_FRAMES * VOICE_CAPTURE_FRAME_MS > VOICE_SEND_TIMEOUT_MS);

    // A: PI -> B CLICK -> NETWORK; repeated same target must not reset it.
    Device d;
    ButtonGesture b;
    assert(click(b,d.epoch,0)==Gesture::CLICK);
    d.page=Device::Page::NETWORK;
    for (unsigned i=0;i<10;++i) d.snapshot("pi","session-one");
    assert(d.page==Device::Page::NETWORK && d.cancellations==0);

    // B: long B -> Settings; release has no trailing select; A advances root.
    b.update(true,200,d.epoch,true);
    b.update(true,230,d.epoch,true);
    assert(b.update(true,830,d.epoch,true)==Gesture::LONG);
    d.modal=Device::Modal::SETTINGS;d.menu.open();++d.epoch;
    b.update(false,900,d.epoch,true);
    assert(b.update(false,930,d.epoch,true)==Gesture::LONG_RELEASE);
    assert(d.modal==Device::Modal::SETTINGS && d.menu.label()=="AGENT SETTINGS");
    ButtonGesture a;
    assert(click(a,d.epoch,1000)==Gesture::CLICK);
    assert(d.menu.click(true)==SpeechMenuAction::NONE);
    assert(d.menu.label()=="SPEECH");

    // C: B selects speech; selecting the first root item requests agent settings.
    assert(click(b,d.epoch,1200)==Gesture::CLICK);
    assert(d.menu.click(false)==SpeechMenuAction::LOAD_SPEECH);
    assert(d.menu.page==SpeechMenu::Page::SPEECH);
    d.menu.open();
    assert(click(b,d.epoch,1400)==Gesture::CLICK);
    assert(d.menu.click(false)==SpeechMenuAction::AGENT_SETTINGS);

    // D: long A -> recording; same-target snapshots preserve the Voice modal.
    d.modal=Device::Modal::NONE;
    a.update(true,1600,d.epoch,true);a.update(true,1630,d.epoch,true);
    assert(a.update(true,2230,d.epoch,true)==Gesture::LONG);
    d.modal=Device::Modal::VOICE;d.recording=true;++d.epoch;
    for (unsigned i=0;i<10;++i) d.snapshot("pi","session-one");
    assert(d.modal==Device::Modal::VOICE && d.recording && d.cancellations==0);
    a.update(false,2300,d.epoch,false);
    assert(a.update(false,2330,d.epoch,false)==Gesture::LONG_RELEASE);

    // Real target changes must retain the cancellation/reset contract.
    d.snapshot("codex","session-one");
    assert(d.page==Device::Page::PI && d.modal==Device::Modal::NONE);
    assert(!d.recording && d.cancellations==1);
    assert(agent_target_changed(true,"pi","session-one","pi","session-two"));
    assert(agent_target_changed(false,"pi","session-one","pi","session-one"));
    assert(!agent_target_changed(true,"pi","unknown",nullptr,nullptr));
}
