/**
 * @file ui_verbs.c
 * @brief Tests the group K verbs against the recording mock backend.
 *
 * Same arrangement as tests/graphics_verbs.c and for the same reason: these
 * verbs produce nothing a golden file can compare, so the assertions are on what
 * reached the device. Running a real BASIC line rather than calling the handler
 * directly exercises the dispatch-table row and the parse handler too, which is
 * where an added verb is most likely to be wrong.
 *
 * The one thing a mock has to fake is a *choice*: a real activation comes from a
 * keystroke libakgl consumed, and there is no keyboard here. mock_menu_choose()
 * is that, and it is the only thing in this file that is not a BASIC program.
 */

#include <string.h>

#include <akbasic/error.h>
#include <akbasic/runtime.h>
#include <akbasic/ui.h>

#include "harness.h"
#include "mockdevice.h"
#include "testutil.h"

/** @brief Bring up a runtime with the mock UI attached and a program loaded. */
static akerr_ErrorContext AKERR_NOIGNORE *run_program(const char *source)
{
    PREPARE_ERROR(errctx);

    PASS(errctx, harness_start(NULL));
    mock_devices_init();
    PASS(errctx, akbasic_runtime_set_ui(&HARNESS_RUNTIME, &MOCK_UI));
    PASS(errctx, akbasic_runtime_load(&HARNESS_RUNTIME, source));
    PASS(errctx, akbasic_runtime_start(&HARNESS_RUNTIME, AKBASIC_MODE_RUN));
    PASS(errctx, akbasic_runtime_run(&HARNESS_RUNTIME, 0));
    SUCCEED_RETURN(errctx);
}

/** @brief The white of palette index 2, and the black of index 1. */
#define WHITE "#ffffff"
#define BLACK "#000000"

/**
 * @brief MENU defines, redefines, retires one and retires all.
 */
static void test_menu(void)
{
    TEST_REQUIRE_OK(run_program("10 MENU 1, \"START\", \"QUIT\"\n"));
    TEST_REQUIRE_STR(MOCK.log, "menu 0 count 2 [START] [QUIT]\n");
    TEST_REQUIRE(HARNESS_RUNTIME.ui_state.menudefined[0],
		 "MENU 1 should have marked menu 1 defined");
    harness_stop();

    /* A number and nothing else retires that one. */
    TEST_REQUIRE_OK(run_program("10 MENU 2, \"A\"\n20 MENU 2\n"));
    TEST_REQUIRE_STR(MOCK.log,
		     "menu 1 count 1 [A]\n"
		     "menu 1 count 0\n");
    TEST_REQUIRE(!HARNESS_RUNTIME.ui_state.menudefined[1],
		 "a bare MENU n should have retired menu n");
    harness_stop();

    /* No arguments retires them all, the way a bare SOLID does. */
    TEST_REQUIRE_OK(run_program("10 MENU 1, \"A\"\n20 MENU 3, \"B\"\n30 MENU\n"));
    TEST_REQUIRE_STR(MOCK.log,
		     "menu 0 count 1 [A]\n"
		     "menu 2 count 1 [B]\n"
		     "menu 0 count 0\n"
		     "menu 1 count 0\n"
		     "menu 2 count 0\n"
		     "menu 3 count 0\n");
    harness_stop();

    /* Entries can be expressions, not only literals. */
    TEST_REQUIRE_OK(run_program("10 A$ = \"LOAD\"\n20 MENU 1, A$ + \" GAME\"\n"));
    TEST_REQUIRE_STR(MOCK.log, "menu 0 count 1 [LOAD GAME]\n");
    harness_stop();
}

/**
 * @brief MENU refuses a slot outside 1..4, a number where a string belongs, and
 *        more entries than libakgl's menu can hold.
 */
static void test_menu_refusals(void)
{
    TEST_REQUIRE_OK(run_program("10 MENU 0, \"A\"\n"));
    TEST_REQUIRE(strstr(HARNESS_OUTPUT, "menu 0 is outside 1..4") != NULL,
		 "MENU 0 should refuse by name, got \"%s\"", HARNESS_OUTPUT);
    harness_stop();

    TEST_REQUIRE_OK(run_program("10 MENU 5, \"A\"\n"));
    TEST_REQUIRE(strstr(HARNESS_OUTPUT, "menu 5 is outside 1..4") != NULL,
		 "MENU 5 should refuse by name, got \"%s\"", HARNESS_OUTPUT);
    harness_stop();

    TEST_REQUIRE_OK(run_program("10 MENU 1, 42\n"));
    TEST_REQUIRE(strstr(HARNESS_OUTPUT, "MENU expected a string") != NULL,
		 "a numeric MENU entry should be a type error, got \"%s\"", HARNESS_OUTPUT);
    harness_stop();

    /*
     * A seventeenth entry is refused by the verb, and there is deliberately no
     * test for it here: AKBASIC_MAX_TOKENS is 32, so seventeen strings and their
     * commas exceed the line's token ceiling and the scanner refuses first. The
     * check in akbasic_cmd_menu() is unreachable from BASIC as the scanner
     * stands and is kept anyway -- it is the thing that would matter the day
     * that ceiling moves, and a MENU that silently wrote past
     * akgl_UiMenu::items would be a memory error rather than a diagnostic.
     */
}

/**
 * @brief RMENU reads the highlight without consuming it, and the latch with.
 *
 * The read-and-clear on field 1 is the whole contract: without it a program
 * polling in a loop sees the same choice forever. Same rule BUMP() carries.
 */
static void test_rmenu(void)
{
    TEST_REQUIRE_OK(harness_start(NULL));
    mock_devices_init();
    TEST_REQUIRE_OK(akbasic_runtime_set_ui(&HARNESS_RUNTIME, &MOCK_UI));
    TEST_REQUIRE_OK(akbasic_runtime_load(&HARNESS_RUNTIME,
					 "10 MENU 1, \"A\", \"B\", \"C\"\n"
					 "20 PRINT RMENU(1,0)\n"
					 "30 PRINT RMENU(1,1)\n"
					 "40 PRINT RMENU(1,1)\n"
					 "50 PRINT RMENU(1,0)\n"));
    TEST_REQUIRE_OK(akbasic_runtime_start(&HARNESS_RUNTIME, AKBASIC_MODE_RUN));
    /*
     * The MENU line first, *then* the choice. Defining a menu resets its
     * selection and its latch, so a choice made before line 10 runs is a choice
     * into a menu that does not exist yet -- and line 10 would wipe it.
     *
     * Fifteen steps, not one. A step advances the program counter by one *line
     * number*, not one statement, so line 10 runs on step 11 and line 20 on step
     * 21; fifteen lands between them. Same reason the frontend's per-frame
     * budget is 256.
     */
    TEST_REQUIRE_OK(akbasic_runtime_run(&HARNESS_RUNTIME, 15));
    mock_menu_choose(0, 1);
    TEST_REQUIRE_OK(akbasic_runtime_run(&HARNESS_RUNTIME, 0));
    /*
     * 2 -- entries are numbered from one, so the second is 2 and not 1.
     * -1 -- BASIC true, once.
     * 0 -- because reading it cleared it.
     * 2 -- and reading the latch did not disturb the highlight.
     */
    TEST_REQUIRE_STR(HARNESS_OUTPUT, "2\n-1\n0\n2\n");
    harness_stop();
}

/** @brief RMENU refuses a bad slot and a field outside 0..1. */
static void test_rmenu_refusals(void)
{
    TEST_REQUIRE_OK(run_program("10 PRINT RMENU(9,0)\n"));
    TEST_REQUIRE(strstr(HARNESS_OUTPUT, "menu 9 is outside 1..4") != NULL,
		 "RMENU should refuse menu 9, got \"%s\"", HARNESS_OUTPUT);
    harness_stop();

    TEST_REQUIRE_OK(run_program("10 PRINT RMENU(1,7)\n"));
    TEST_REQUIRE(strstr(HARNESS_OUTPUT, "field 7 is outside 0..1") != NULL,
		 "RMENU should refuse field 7, got \"%s\"", HARNESS_OUTPUT);
    harness_stop();
}

/**
 * @brief GETMENU holds the step loop, then assigns the entry that was chosen.
 *
 * The property being asserted is that the step *returns* while the program does
 * not advance -- section 1.6's whole point. A run of a hundred steps with
 * nothing chosen must leave the program on the GETMENU line and must come back.
 */
static void test_getmenu_holds(void)
{
    TEST_REQUIRE_OK(harness_start(NULL));
    mock_devices_init();
    TEST_REQUIRE_OK(akbasic_runtime_set_ui(&HARNESS_RUNTIME, &MOCK_UI));
    TEST_REQUIRE_OK(akbasic_runtime_load(&HARNESS_RUNTIME,
					 "10 MENU 1, \"A\", \"B\", \"C\"\n"
					 "20 GETMENU 1, C%\n"
					 "30 PRINT C%\n"));
    TEST_REQUIRE_OK(akbasic_runtime_start(&HARNESS_RUNTIME, AKBASIC_MODE_RUN));

    TEST_REQUIRE_OK(akbasic_runtime_run(&HARNESS_RUNTIME, 100));
    TEST_REQUIRE(HARNESS_RUNTIME.ui_state.waiting,
		 "GETMENU should still be holding after a hundred steps");
    TEST_REQUIRE_STR(HARNESS_OUTPUT, "");

    mock_menu_choose(0, 2);
    TEST_REQUIRE_OK(akbasic_runtime_run(&HARNESS_RUNTIME, 0));
    TEST_REQUIRE(!HARNESS_RUNTIME.ui_state.waiting, "GETMENU should have released");
    TEST_REQUIRE_STR(HARNESS_OUTPUT, "3\n");
    harness_stop();
}

/**
 * @brief A GETMENU that arrives after the choice does not wait at all.
 *
 * The latch survives between frames precisely so an unread activation is never
 * lost, so a program that gets round to its GETMENU late must find the answer
 * waiting rather than park for a second one.
 */
static void test_getmenu_takes_a_waiting_choice(void)
{
    TEST_REQUIRE_OK(harness_start(NULL));
    mock_devices_init();
    TEST_REQUIRE_OK(akbasic_runtime_set_ui(&HARNESS_RUNTIME, &MOCK_UI));
    TEST_REQUIRE_OK(akbasic_runtime_load(&HARNESS_RUNTIME,
					 "10 MENU 1, \"A\", \"B\", \"C\"\n"
					 "20 GETMENU 1, C%\n"
					 "30 PRINT C%\n"));
    TEST_REQUIRE_OK(akbasic_runtime_start(&HARNESS_RUNTIME, AKBASIC_MODE_RUN));

    /* Through line 10 only, then choose before line 20 ever runs. */
    TEST_REQUIRE_OK(akbasic_runtime_run(&HARNESS_RUNTIME, 15));
    mock_menu_choose(0, 2);
    TEST_REQUIRE_OK(akbasic_runtime_run(&HARNESS_RUNTIME, 0));
    TEST_REQUIRE(!HARNESS_RUNTIME.ui_state.waiting,
		 "GETMENU should not have held when the choice was already made");
    TEST_REQUIRE_STR(HARNESS_OUTPUT, "3\n");
    harness_stop();
}

/**
 * @brief Retiring the menu underneath a holding GETMENU releases it with 0.
 *
 * Reached from BASIC only through an interrupt handler, which is the one thing
 * that can run a line while the program is parked; the menu is retired directly
 * here because building a collision interrupt to do it would be testing
 * something else.
 */
static void test_getmenu_released_by_retirement(void)
{
    TEST_REQUIRE_OK(harness_start(NULL));
    mock_devices_init();
    TEST_REQUIRE_OK(akbasic_runtime_set_ui(&HARNESS_RUNTIME, &MOCK_UI));
    TEST_REQUIRE_OK(akbasic_runtime_load(&HARNESS_RUNTIME,
					 "10 MENU 1, \"A\", \"B\"\n"
					 "20 C% = 9\n"
					 "30 GETMENU 1, C%\n"
					 "40 PRINT C%\n"));
    TEST_REQUIRE_OK(akbasic_runtime_start(&HARNESS_RUNTIME, AKBASIC_MODE_RUN));
    TEST_REQUIRE_OK(akbasic_runtime_run(&HARNESS_RUNTIME, 35));
    TEST_REQUIRE(HARNESS_RUNTIME.ui_state.waiting, "GETMENU should be holding");

    TEST_REQUIRE_OK(MOCK_UI.menu(&MOCK_UI, 0, NULL, 0));
    HARNESS_RUNTIME.ui_state.menudefined[0] = false;
    TEST_REQUIRE_OK(akbasic_runtime_run(&HARNESS_RUNTIME, 0));
    TEST_REQUIRE(!HARNESS_RUNTIME.ui_state.waiting,
		 "retiring the menu should have released the GETMENU");
    TEST_REQUIRE_STR(HARNESS_OUTPUT, "0\n");
    harness_stop();
}

/**
 * @brief Withdrawing the device releases a holding GETMENU rather than wedging it.
 *
 * A host is allowed to change its mind about what it lends out, and a script
 * parked forever on a device that no longer exists is the worse outcome. Same
 * rule akbasic_input_service() keeps for a withdrawn keyboard.
 */
static void test_getmenu_released_by_withdrawal(void)
{
    TEST_REQUIRE_OK(harness_start(NULL));
    mock_devices_init();
    TEST_REQUIRE_OK(akbasic_runtime_set_ui(&HARNESS_RUNTIME, &MOCK_UI));
    TEST_REQUIRE_OK(akbasic_runtime_load(&HARNESS_RUNTIME,
					 "10 MENU 1, \"A\", \"B\"\n"
					 "20 C% = 9\n"
					 "30 GETMENU 1, C%\n"
					 "40 PRINT C%\n"));
    TEST_REQUIRE_OK(akbasic_runtime_start(&HARNESS_RUNTIME, AKBASIC_MODE_RUN));
    /* Through line 30, where the GETMENU is; see the note in test_rmenu(). */
    TEST_REQUIRE_OK(akbasic_runtime_run(&HARNESS_RUNTIME, 35));
    TEST_REQUIRE(HARNESS_RUNTIME.ui_state.waiting, "GETMENU should be holding");

    TEST_REQUIRE_OK(akbasic_runtime_set_ui(&HARNESS_RUNTIME, NULL));
    TEST_REQUIRE_OK(akbasic_runtime_run(&HARNESS_RUNTIME, 0));
    TEST_REQUIRE(!HARNESS_RUNTIME.ui_state.waiting,
		 "withdrawing the UI should have released the GETMENU");
    /* Released, not chosen: 0 rather than the 9 the variable held. */
    TEST_REQUIRE_STR(HARNESS_OUTPUT, "0\n");
    harness_stop();
}

/** @brief GETMENU refuses a menu that has no entries rather than holding forever. */
static void test_getmenu_needs_a_menu(void)
{
    TEST_REQUIRE_OK(run_program("10 GETMENU 1, C%\n"));
    TEST_REQUIRE(strstr(HARNESS_OUTPUT, "has no entries") != NULL,
		 "GETMENU on an undefined menu should refuse, got \"%s\"", HARNESS_OUTPUT);
    harness_stop();

    TEST_REQUIRE_OK(run_program("10 MENU 1, \"A\"\n20 GETMENU 1, A$\n"));
    TEST_REQUIRE(strstr(HARNESS_OUTPUT, "expected a numeric variable") != NULL,
		 "GETMENU into a string should be a type error, got \"%s\"", HARNESS_OUTPUT);
    harness_stop();
}

/** @brief DIALOG puts the panel up with a string and takes it down with nothing. */
static void test_dialog(void)
{
    TEST_REQUIRE_OK(run_program("10 DIALOG \"HELLO\"\n20 DIALOG\n"));
    TEST_REQUIRE_STR(MOCK.log,
		     "dialog HELLO\n"
		     "dialog (none)\n");
    harness_stop();

    TEST_REQUIRE_OK(run_program("10 DIALOG 42\n"));
    TEST_REQUIRE(strstr(HARNESS_OUTPUT, "DIALOG expected a string") != NULL,
		 "a numeric DIALOG should be a type error, got \"%s\"", HARNESS_OUTPUT);
    harness_stop();
}

/**
 * @brief HUD sets each of the five anchors, retires one, and retires all.
 *
 * Five and not seven. libakgl's akgl_UiAnchor has the four corners and dead
 * centre; there is no top-centre or bottom-centre, and this asserts that 5 is
 * refused rather than quietly mapped onto something.
 */
static void test_hud(void)
{
    TEST_REQUIRE_OK(run_program("10 HUD 1, 0, \"TL\"\n"
				"20 HUD 2, 1, \"TR\"\n"
				"30 HUD 3, 2, \"BL\"\n"
				"40 HUD 4, 3, \"BR\"\n"
				"50 HUD 5, 4, \"MID\"\n"));
    TEST_REQUIRE_STR(MOCK.log,
		     "label 0 anchor 0 TL\n"
		     "label 1 anchor 1 TR\n"
		     "label 2 anchor 2 BL\n"
		     "label 3 anchor 3 BR\n"
		     "label 4 anchor 4 MID\n");
    harness_stop();

    TEST_REQUIRE_OK(run_program("10 HUD 2, 0, \"X\"\n20 HUD 2\n"));
    TEST_REQUIRE_STR(MOCK.log,
		     "label 1 anchor 0 X\n"
		     "label 1 anchor 0 (none)\n");
    harness_stop();

    /* No arguments retires all eight, the way a bare MENU retires all four. */
    TEST_REQUIRE_OK(run_program("10 HUD 1, 0, \"X\"\n20 HUD\n"));
    TEST_REQUIRE_STR(MOCK.log,
		     "label 0 anchor 0 X\n"
		     "label 0 anchor 0 (none)\n"
		     "label 1 anchor 0 (none)\n"
		     "label 2 anchor 0 (none)\n"
		     "label 3 anchor 0 (none)\n"
		     "label 4 anchor 0 (none)\n"
		     "label 5 anchor 0 (none)\n"
		     "label 6 anchor 0 (none)\n"
		     "label 7 anchor 0 (none)\n");
    harness_stop();

    TEST_REQUIRE_OK(run_program("10 HUD 1, 5, \"NOPE\"\n"));
    TEST_REQUIRE(strstr(HARNESS_OUTPUT, "anchor 5 is outside 0..4") != NULL,
		 "HUD should refuse anchor 5, got \"%s\"", HARNESS_OUTPUT);
    harness_stop();

    TEST_REQUIRE_OK(run_program("10 HUD 9, 0, \"NOPE\"\n"));
    TEST_REQUIRE(strstr(HARNESS_OUTPUT, "slot 9 is outside 1..8") != NULL,
		 "HUD should refuse slot 9, got \"%s\"", HARNESS_OUTPUT);
    harness_stop();

    /* A slot and an anchor with nothing to say is a typo, not an empty label. */
    TEST_REQUIRE_OK(run_program("10 HUD 1, 0\n"));
    TEST_REQUIRE(strstr(HARNESS_OUTPUT, "a slot, an anchor and a string") != NULL,
		 "a two-argument HUD should be refused, got \"%s\"", HARNESS_OUTPUT);
    harness_stop();
}

/** @brief UISTYLE converts palette indices, defaults its tail, and resets. */
static void test_uistyle(void)
{
    TEST_REQUIRE_OK(run_program("10 UISTYLE 1, 2, 2\n"));
    TEST_REQUIRE_STR(MOCK.log,
		     "style " BLACK " " WHITE " " WHITE " pad 8.0 radius 0.0\n");
    harness_stop();

    TEST_REQUIRE_OK(run_program("10 UISTYLE 1, 2, 2, 12, 4\n"));
    TEST_REQUIRE_STR(MOCK.log,
		     "style " BLACK " " WHITE " " WHITE " pad 12.0 radius 4.0\n");
    harness_stop();

    TEST_REQUIRE_OK(run_program("10 UISTYLE\n"));
    TEST_REQUIRE_STR(MOCK.log, "style default\n");
    harness_stop();

    TEST_REQUIRE_OK(run_program("10 UISTYLE 17, 2, 2\n"));
    TEST_REQUIRE(strstr(HARNESS_OUTPUT, "RUNTIME ERROR") != NULL,
		 "UISTYLE should refuse palette index 17, got \"%s\"", HARNESS_OUTPUT);
    harness_stop();

    TEST_REQUIRE_OK(run_program("10 UISTYLE 1, 2\n"));
    TEST_REQUIRE(strstr(HARNESS_OUTPUT, "a fill, an edge and an ink") != NULL,
		 "UISTYLE with two colours should be refused, got \"%s\"", HARNESS_OUTPUT);
    harness_stop();
}

/** @brief NEW takes every widget down; a deleted program's menu must not survive it. */
static void test_new_retires_everything(void)
{
    TEST_REQUIRE_OK(harness_start(NULL));
    mock_devices_init();
    TEST_REQUIRE_OK(akbasic_runtime_set_ui(&HARNESS_RUNTIME, &MOCK_UI));
    TEST_REQUIRE_OK(akbasic_runtime_load(&HARNESS_RUNTIME, "10 MENU 1, \"A\"\n"));
    TEST_REQUIRE_OK(akbasic_runtime_start(&HARNESS_RUNTIME, AKBASIC_MODE_RUN));
    TEST_REQUIRE_OK(akbasic_runtime_run(&HARNESS_RUNTIME, 0));
    TEST_REQUIRE(HARNESS_RUNTIME.ui_state.menudefined[0], "the menu should be up");

    mock_log_reset();
    TEST_REQUIRE_OK(akbasic_runtime_load(&HARNESS_RUNTIME, "10 NEW\n"));
    TEST_REQUIRE_OK(akbasic_runtime_start(&HARNESS_RUNTIME, AKBASIC_MODE_RUN));
    TEST_REQUIRE_OK(akbasic_runtime_run(&HARNESS_RUNTIME, 0));
    TEST_REQUIRE(strstr(MOCK.log, "uiclear") != NULL,
		 "NEW should have retired the widgets, log was \"%s\"", MOCK.log);
    TEST_REQUIRE(!HARNESS_RUNTIME.ui_state.menudefined[0],
		 "NEW should have forgotten the menu");
    harness_stop();
}

/**
 * @brief Every verb in the group refuses by name when no UI device was lent.
 *
 * This is the standalone driver's situation and every no-SDL build's, so it is
 * the common path rather than an edge case.
 */
static void test_no_device(void)
{
    TEST_REQUIRE_OK(harness_start(NULL));
    TEST_REQUIRE_OK(akbasic_runtime_load(&HARNESS_RUNTIME, "10 MENU 1, \"A\"\n"));
    TEST_REQUIRE_OK(akbasic_runtime_start(&HARNESS_RUNTIME, AKBASIC_MODE_RUN));
    TEST_REQUIRE_OK(akbasic_runtime_run(&HARNESS_RUNTIME, 0));
    TEST_REQUIRE(strstr(HARNESS_OUTPUT, "MENU needs a UI device") != NULL,
		 "MENU without a device should name itself, got \"%s\"", HARNESS_OUTPUT);
    harness_stop();

    TEST_REQUIRE_OK(harness_start(NULL));
    TEST_REQUIRE_OK(akbasic_runtime_load(&HARNESS_RUNTIME, "10 DIALOG \"X\"\n"));
    TEST_REQUIRE_OK(akbasic_runtime_start(&HARNESS_RUNTIME, AKBASIC_MODE_RUN));
    TEST_REQUIRE_OK(akbasic_runtime_run(&HARNESS_RUNTIME, 0));
    TEST_REQUIRE(strstr(HARNESS_OUTPUT, "DIALOG needs a UI device") != NULL,
		 "DIALOG without a device should name itself, got \"%s\"", HARNESS_OUTPUT);
    harness_stop();

    TEST_REQUIRE_OK(harness_start(NULL));
    TEST_REQUIRE_OK(akbasic_runtime_load(&HARNESS_RUNTIME, "10 HUD 1, 0, \"X\"\n"));
    TEST_REQUIRE_OK(akbasic_runtime_start(&HARNESS_RUNTIME, AKBASIC_MODE_RUN));
    TEST_REQUIRE_OK(akbasic_runtime_run(&HARNESS_RUNTIME, 0));
    TEST_REQUIRE(strstr(HARNESS_OUTPUT, "HUD needs a UI device") != NULL,
		 "HUD without a device should name itself, got \"%s\"", HARNESS_OUTPUT);
    harness_stop();

    TEST_REQUIRE_OK(harness_start(NULL));
    TEST_REQUIRE_OK(akbasic_runtime_load(&HARNESS_RUNTIME, "10 UISTYLE 1, 2, 2\n"));
    TEST_REQUIRE_OK(akbasic_runtime_start(&HARNESS_RUNTIME, AKBASIC_MODE_RUN));
    TEST_REQUIRE_OK(akbasic_runtime_run(&HARNESS_RUNTIME, 0));
    TEST_REQUIRE(strstr(HARNESS_OUTPUT, "UISTYLE needs a UI device") != NULL,
		 "UISTYLE without a device should name itself, got \"%s\"", HARNESS_OUTPUT);
    harness_stop();

    TEST_REQUIRE_OK(harness_start(NULL));
    TEST_REQUIRE_OK(akbasic_runtime_load(&HARNESS_RUNTIME, "10 GETMENU 1, C%\n"));
    TEST_REQUIRE_OK(akbasic_runtime_start(&HARNESS_RUNTIME, AKBASIC_MODE_RUN));
    TEST_REQUIRE_OK(akbasic_runtime_run(&HARNESS_RUNTIME, 0));
    TEST_REQUIRE(strstr(HARNESS_OUTPUT, "GETMENU needs a UI device") != NULL,
		 "GETMENU without a device should name itself, got \"%s\"", HARNESS_OUTPUT);
    harness_stop();

    TEST_REQUIRE_OK(harness_start(NULL));
    TEST_REQUIRE_OK(akbasic_runtime_load(&HARNESS_RUNTIME, "10 PRINT RMENU(1,0)\n"));
    TEST_REQUIRE_OK(akbasic_runtime_start(&HARNESS_RUNTIME, AKBASIC_MODE_RUN));
    TEST_REQUIRE_OK(akbasic_runtime_run(&HARNESS_RUNTIME, 0));
    TEST_REQUIRE(strstr(HARNESS_OUTPUT, "RMENU needs a UI device") != NULL,
		 "RMENU without a device should name itself, got \"%s\"", HARNESS_OUTPUT);
    harness_stop();
}

/** @brief A NULL runtime is refused rather than dereferenced. */
static void test_set_ui_validation(void)
{
    TEST_REQUIRE_STATUS(akbasic_runtime_set_ui(NULL, NULL), AKERR_NULLPOINTER);
    TEST_REQUIRE_STATUS(akbasic_ui_state_init(NULL), AKERR_NULLPOINTER);
}

int main(void)
{
    test_menu();
    test_menu_refusals();
    test_rmenu();
    test_rmenu_refusals();
    test_getmenu_holds();
    test_getmenu_takes_a_waiting_choice();
    test_getmenu_released_by_retirement();
    test_getmenu_released_by_withdrawal();
    test_getmenu_needs_a_menu();
    test_dialog();
    test_hud();
    test_uistyle();
    test_new_retires_everything();
    test_no_device();
    test_set_ui_validation();
    return akbasic_test_failures;
}
