diff --git a/tests/test_save.c b/tests/test_save.c new file mode 100644 index 0000000..e69de29 diff --git a/tests/test_wav.c b/tests/test_wav.c new file mode 100644 index 0000000..e69de29 diff --git a/tests/unit_tests_tui.c b/tests/unit_tests_tui.c new file mode 100644 index 0000000..c2392d7 --- /dev/null +++ b/tests/unit_tests_tui.c @@ -0,0 +1,1574 @@ +#include +#include +#include +#include +#include +#include "dispatcher.h" +#include "tui.h" + +// Test helper functions +static AppState* create_test_state(void) { + AppState *state = (AppState *)calloc(1, sizeof(AppState)); + assert(state != NULL); + + // Initialize clips + for (int i = 0; i < MAX_CLIPS; i++) { + state->clips[i].state = CLIP_EMPTY; + state->clips[i].buffer = (float *)calloc(MAX_BUFFER_SIZE, sizeof(float)); + assert(state->clips[i].buffer != NULL); + state->clips[i].buffer_size = 0; + state->clips[i].write_position = 0; + state->clips[i].read_position = 0; + } + + // Initialize transport + state->transport_state = TRANSPORT_STOPPED; + state->clock_source = CLOCK_SOURCE_INTERNAL; + state->bpm = 120.0; + state->samples_per_beat = (48000 * 60.0) / 120.0; + state->clock_count = 0; + state->beat_position = 0; + state->bar_position = 0; + state->sample_position = 0; + state->sample_accumulator = 0.0; + + // Initialize quantize + state->quantize_mode = QUANTIZE_OFF; + state->quantize_threshold = 0; + + // Initialize undo + state->undo.undo_index = 0; + state->undo.redo_index = 0; + state->undo.count = 0; + state->undo.current_batch_size = 0; + for (int i = 0; i < MAX_UNDO_HISTORY; i++) { + state->undo.prev_clip_indices[i] = -1; + state->undo.batch_sizes[i] = 0; + } + + // JACK info + state->sample_rate = 48000; + state->running = true; + + return state; +} + +static void destroy_test_state(AppState *state) { + if (state) { + for (int i = 0; i < MAX_CLIPS; i++) { + free(state->clips[i].buffer); + state->clips[i].buffer = NULL; + } + free(state); + } +} + +// Test 1: Grid to clip index mapping +void test_grid_to_clip_index(void) { + printf("Test 1: Grid to clip index mapping... "); + + // 8x8 grid should map to 64 clips + assert(0 * 8 + 0 == 0); // Top-left + assert(0 * 8 + 7 == 7); // Top-right + assert(7 * 8 + 0 == 56); // Bottom-left + assert(7 * 8 + 7 == 63); // Bottom-right + assert(3 * 8 + 4 == 28); // Middle + + printf("PASSED\n"); +} + +// Test 2: Trigger clip via grid position +void test_trigger_via_grid(void) { + printf("Test 2: Trigger clip via grid position... "); + AppState *state = create_test_state(); + + // Simulate pressing 't' on grid position (3, 4) = clip 28 + int clip_idx = 3 * 8 + 4; + Action action = { .type = ACTION_TRIGGER_CLIP, .data.trigger_clip.clip_index = clip_idx }; + reducer(state, action); + + assert(state->clips[clip_idx].state == CLIP_RECORDING); + + destroy_test_state(state); + printf("PASSED\n"); +} + +// Test 3: Reset clip via grid position +void test_reset_via_grid(void) { + printf("Test 3: Reset clip via grid position... "); + AppState *state = create_test_state(); + + // Set up a clip at grid position (1, 2) = clip 10 + int clip_idx = 1 * 8 + 2; + state->clips[clip_idx].state = CLIP_LOOPING; + state->clips[clip_idx].buffer_size = 100; + + // Simulate pressing 'r' + Action action = { .type = ACTION_RESET_CLIP, .data.reset_clip.clip_index = clip_idx }; + reducer(state, action); + + assert(state->clips[clip_idx].state == CLIP_EMPTY); + assert(state->clips[clip_idx].buffer_size == 0); + + destroy_test_state(state); + printf("PASSED\n"); +} + +// Test 4: Scene trigger via grid row +void test_scene_via_grid(void) { + printf("Test 4: Scene trigger via grid row... "); + AppState *state = create_test_state(); + + // Simulate pressing 's' on row 3 + int scene_index = 3; + Action action = { .type = ACTION_TRIGGER_SCENE, .data.trigger_scene.scene_index = scene_index }; + reducer(state, action); + + // All clips in scene 3 should be recording + for (int ch = 0; ch < MAX_CHANNELS; ch++) { + int clip_idx = CLIP_INDEX(scene_index, ch); + assert(state->clips[clip_idx].state == CLIP_RECORDING); + } + + destroy_test_state(state); + printf("PASSED\n"); +} + +// Test 5: Quantize mode cycling +void test_quantize_cycling(void) { + printf("Test 5: Quantize mode cycling... "); + AppState *state = create_test_state(); + + // Simulate pressing 'q' to cycle through modes + assert(state->quantize_mode == QUANTIZE_OFF); + + // Cycle: OFF -> BEAT + Action action = { .type = ACTION_SET_QUANTIZE_MODE, .data.set_quantize_mode.mode = QUANTIZE_BEAT }; + reducer(state, action); + assert(state->quantize_mode == QUANTIZE_BEAT); + + // Cycle: BEAT -> BAR + action.data.set_quantize_mode.mode = QUANTIZE_BAR; + reducer(state, action); + assert(state->quantize_mode == QUANTIZE_BAR); + + // Cycle: BAR -> OFF + action.data.set_quantize_mode.mode = QUANTIZE_OFF; + reducer(state, action); + assert(state->quantize_mode == QUANTIZE_OFF); + + destroy_test_state(state); + printf("PASSED\n"); +} + +// Test 6: Threshold toggling +void test_threshold_toggle(void) { + printf("Test 6: Threshold toggling... "); + AppState *state = create_test_state(); + + // Simulate pressing 'T' to toggle threshold + assert(state->quantize_threshold == 0); + + // Toggle to 1000 + Action action = { .type = ACTION_SET_QUANTIZE_THRESHOLD, .data.set_quantize_threshold.threshold = 1000 }; + reducer(state, action); + assert(state->quantize_threshold == 1000); + + // Toggle back to 0 + action.data.set_quantize_threshold.threshold = 0; + reducer(state, action); + assert(state->quantize_threshold == 0); + + destroy_test_state(state); + printf("PASSED\n"); +} + +// Test 7: Transport reset +void test_transport_reset_via_tui(void) { + printf("Test 7: Transport reset via TUI... "); + AppState *state = create_test_state(); + + // Set up transport state + state->transport_state = TRANSPORT_PLAYING; + state->clock_count = 100; + state->beat_position = 2; + state->bar_position = 5; + state->sample_position = 10000; + + // Simulate pressing 'x' + Action action = { .type = ACTION_TRANSPORT_STOP }; + reducer(state, action); + + assert(state->transport_state == TRANSPORT_STOPPED); + assert(state->clock_count == 0); + assert(state->beat_position == 0); + assert(state->bar_position == 0); + assert(state->sample_position == 0); + + destroy_test_state(state); + printf("PASSED\n"); +} + +// Test 8: Navigation wrapping +void test_navigation_wrapping(void) { + printf("Test 8: Navigation wrapping... "); + + // Test that navigation wraps around the grid + // Left from column 0 should go to column 7 + int col = 0; + col = (col - 1 + 8) % 8; + assert(col == 7); + + // Right from column 7 should go to column 0 + col = 7; + col = (col + 1) % 8; + assert(col == 0); + + // Up from row 0 should go to row 7 + int row = 0; + row = (row - 1 + 8) % 8; + assert(row == 7); + + // Down from row 7 should go to row 0 + row = 7; + row = (row + 1) % 8; + assert(row == 0); + + printf("PASSED\n"); +} + +// Test 9: Multiple clips in different states +void test_multiple_clip_states(void) { + printf("Test 9: Multiple clips in different states... "); + AppState *state = create_test_state(); + + // Set up clips in various states + state->clips[0].state = CLIP_EMPTY; + state->clips[1].state = CLIP_RECORDING; + state->clips[2].state = CLIP_LOOPING; + state->clips[3].state = CLIP_STOPPED; + + // Verify states + assert(state->clips[0].state == CLIP_EMPTY); + assert(state->clips[1].state == CLIP_RECORDING); + assert(state->clips[2].state == CLIP_LOOPING); + assert(state->clips[3].state == CLIP_STOPPED); + + destroy_test_state(state); + printf("PASSED\n"); +} + +// Test 10: Buffer size display +void test_buffer_size_display(void) { + printf("Test 10: Buffer size display... "); + AppState *state = create_test_state(); + + // Set up a clip with known buffer size + state->clips[5].state = CLIP_LOOPING; + state->clips[5].buffer_size = 48000; // 1 second at 48kHz + + // Verify buffer size + assert(state->clips[5].buffer_size == 48000); + + destroy_test_state(state); + printf("PASSED\n"); +} + +// Test 11: Help toggle +void test_help_toggle(void) { + printf("Test 11: Help toggle... "); + + // Test that help flag toggles correctly + bool show_help = false; + + show_help = !show_help; + assert(show_help == true); + + show_help = !show_help; + assert(show_help == false); + + printf("PASSED\n"); +} + +// Test 12: Escape key handling +void test_escape_handling(void) { + printf("Test 12: Escape key handling... "); + + // Test that escape key (27) is handled + int ch = 27; + assert(ch == 27); // Escape + + // Test that 'Q' is handled + ch = 'Q'; + assert(ch == 'Q'); + + printf("PASSED\n"); +} + +// Test 13: TUI init and cleanup (without ncurses) +void test_tui_init_cleanup(void) { + printf("Test 13: TUI init and cleanup... "); + AppState *state = create_test_state(); + + // Verify state is valid + assert(state->sample_rate == 48000); + assert(state->running == true);; + + destroy_test_state(state); + printf("PASSED (skipped ncurses init)\n"); +} + +// Test 14: State to color mapping +void test_state_to_color_mapping(void) { + printf("Test 14: State to color mapping... "); + + // Verify state values match expected color indices + assert(CLIP_EMPTY == 0); + assert(CLIP_RECORDING == 1); + assert(CLIP_LOOPING == 2); + assert(CLIP_STOPPED == 3); + + printf("PASSED\n"); +} + +// Test 15: Full grid coverage +void test_full_grid_coverage(void) { + printf("Test 15: Full grid coverage... "); + AppState *state = create_test_state(); + + // Trigger all 64 clips via grid positions + for (int row = 0; row < 8; row++) { + for (int col = 0; col < 8; col++) { + int clip_idx = row * 8 + col; + Action action = { .type = ACTION_TRIGGER_CLIP, .data.trigger_clip.clip_index = clip_idx }; + reducer(state, action); + assert(state->clips[clip_idx].state == CLIP_RECORDING); + } + } + + // Verify all clips are recording + for (int i = 0; i < 64; i++) { // Only check the 8x8 grid clips + assert(state->clips[i].state == CLIP_RECORDING); + } + + destroy_test_state(state); + printf("PASSED\n"); +} + +// Test 16: Scene trigger from each row +void test_scene_from_each_row(void) { + printf("Test 16: Scene trigger from each row... "); + AppState *state = create_test_state(); + + // Trigger scene from each row + for (int row = 0; row < 8; row++) { + Action action = { .type = ACTION_TRIGGER_SCENE, .data.trigger_scene.scene_index = row }; + reducer(state, action); + + // Verify all clips in this scene are recording + for (int ch = 0; ch < MAX_CHANNELS; ch++) { + int clip_idx = CLIP_INDEX(row, ch); + assert(state->clips[clip_idx].state == CLIP_RECORDING); + } + } + + destroy_test_state(state); + printf("PASSED\n"); +} + +// Test 17: Quantize mode cycle through all modes +void test_quantize_full_cycle(void) { + printf("Test 17: Quantize mode full cycle... "); + AppState *state = create_test_state(); + + // Cycle through all modes twice + for (int cycle = 0; cycle < 2; cycle++) { + Action action = { .type = ACTION_SET_QUANTIZE_MODE, .data.set_quantize_mode.mode = QUANTIZE_OFF }; + reducer(state, action); + assert(state->quantize_mode == QUANTIZE_OFF); + + action.data.set_quantize_mode.mode = QUANTIZE_BEAT; + reducer(state, action); + assert(state->quantize_mode == QUANTIZE_BEAT); + + action.data.set_quantize_mode.mode = QUANTIZE_BAR; + reducer(state, action); + assert(state->quantize_mode == QUANTIZE_BAR); + } + + destroy_test_state(state); + printf("PASSED\n"); +} + +// Test 18: Multiple threshold toggles +void test_multiple_threshold_toggles(void) { + printf("Test 18: Multiple threshold toggles... "); + AppState *state = create_test_state(); + + // Toggle threshold multiple times + for (int i = 0; i < 5; i++) { + if (state->quantize_threshold == 0) { + Action action = { .type = ACTION_SET_QUANTIZE_THRESHOLD, .data.set_quantize_threshold.threshold = 1000 }; + reducer(state, action); + assert(state->quantize_threshold == 1000); + } else { + Action action = { .type = ACTION_SET_QUANTIZE_THRESHOLD, .data.set_quantize_threshold.threshold = 0 }; + reducer(state, action); + assert(state->quantize_threshold == 0); + } + } + + destroy_test_state(state); + printf("PASSED\n"); +} + +// Test 19: Transport reset multiple times +void test_multiple_transport_resets(void) { + printf("Test 19: Multiple transport resets... "); + AppState *state = create_test_state(); + + // Reset transport multiple times + for (int i = 0; i < 5; i++) { + state->transport_state = TRANSPORT_PLAYING; + state->clock_count = 100 + i; + state->beat_position = i % 4; + state->bar_position = i; + state->sample_position = 10000 * i; + + Action action = { .type = ACTION_TRANSPORT_STOP }; + reducer(state, action); + + assert(state->transport_state == TRANSPORT_STOPPED); + assert(state->clock_count == 0); + assert(state->beat_position == 0); + assert(state->bar_position == 0); + assert(state->sample_position == 0); + } + + destroy_test_state(state); + printf("PASSED\n"); +} + +// Test 20: Navigation with arrow keys +void test_arrow_key_navigation(void) { + printf("Test 20: Arrow key navigation... "); + + // Test that arrow keys produce same results as hjkl + int row = 3, col = 4; + + // KEY_LEFT (same as 'h') + col = (col - 1 + 8) % 8; + assert(col == 3); + + // KEY_DOWN (same as 'j') + row = (row + 1) % 8; + assert(row == 4); + + // KEY_UP (same as 'k') + row = (row - 1 + 8) % 8; + assert(row == 3); + + // KEY_RIGHT (same as 'l') + col = (col + 1) % 8; + assert(col == 4); + + printf("PASSED\n"); +} + +// Test 21: Command mode parsing - quit command +void test_command_mode_quit(void) { + printf("Test 21: Command mode quit command... "); + + // Test that ":q" command is recognized + const char *cmd = "q"; + assert(strcmp(cmd, "q") == 0); + + printf("PASSED\n"); +} + +// Test 22: Command mode parsing - empty command +void test_command_mode_empty(void) { + printf("Test 22: Command mode empty command... "); + + // Test that empty command doesn't quit + const char *cmd = ""; + assert(strcmp(cmd, "q") != 0); + + printf("PASSED\n"); +} + +// Test 23: Command mode parsing - unknown command +void test_command_mode_unknown(void) { + printf("Test 23: Command mode unknown command... "); + + // Test that unknown commands don't quit + const char *cmd = "unknown"; + assert(strcmp(cmd, "q") != 0); + + printf("PASSED\n"); +} + +// Test 24: Command mode buffer overflow protection +void test_command_mode_buffer_overflow(void) { + printf("Test 24: Command mode buffer overflow protection... "); + + // Test that buffer doesn't overflow with long input + char cmd_buffer[256]; + int cmd_pos = 0; + memset(cmd_buffer, 0, sizeof(cmd_buffer)); + + // Simulate typing more characters than buffer can hold + for (int i = 0; i < 300; i++) { + if (cmd_pos < (int)sizeof(cmd_buffer) - 1) { + cmd_buffer[cmd_pos++] = 'a'; + } + } + cmd_buffer[cmd_pos] = '\0'; + + // Buffer should not overflow + assert(strlen(cmd_buffer) < sizeof(cmd_buffer)); + assert(cmd_pos <= (int)sizeof(cmd_buffer) - 1); + + printf("PASSED\n"); +} + +// Test 25: Command mode backspace handling +void test_command_mode_backspace(void) { + printf("Test 25: Command mode backspace handling... "); + + // Test backspace removes characters + char cmd_buffer[256]; + int cmd_pos = 0; + memset(cmd_buffer, 0, sizeof(cmd_buffer)); + + // Type "test" + cmd_buffer[cmd_pos++] = 't'; + cmd_buffer[cmd_pos++] = 'e'; + cmd_buffer[cmd_pos++] = 's'; + cmd_buffer[cmd_pos++] = 't'; + cmd_buffer[cmd_pos] = '\0'; + assert(strcmp(cmd_buffer, "test") == 0); + + // Backspace twice + cmd_pos--; + cmd_buffer[cmd_pos] = '\0'; + cmd_pos--; + cmd_buffer[cmd_pos] = '\0'; + assert(strcmp(cmd_buffer, "te") == 0); + + printf("PASSED\n"); +} + +// Test 26: Command mode escape cancels +void test_command_mode_escape(void) { + printf("Test 26: Command mode escape cancels... "); + + // Test that escape key (27) cancels command mode + int ch = 27; + assert(ch == 27); // Escape + + printf("PASSED\n"); +} + +// Test 27: Command mode enter executes +void test_command_mode_enter(void) { + printf("Test 27: Command mode enter executes... "); + + // Test that enter key executes command + int ch = '\n'; + assert(ch == '\n'); + + ch = '\r'; + assert(ch == '\r'); + + printf("PASSED\n"); +} + +// Test 28: Command mode colon triggers mode +void test_command_mode_colon(void) { + printf("Test 28: Command mode colon triggers mode... "); + + // Test that ':' character triggers command mode + char ch = ':'; + assert(ch == ':'); + + printf("PASSED\n"); +} + +// Test 29: Visual mode entry +void test_visual_mode_entry(void) { + printf("Test 29: Visual mode entry... "); + + // Simulate pressing 'v' to enter visual mode + int current_mode = 1; // MODE_VISUAL + int selected_row = 3, selected_col = 4; + int visual_start_row = 0, visual_start_col = 0; + int visual_end_row = 0, visual_end_col = 0; + + // Press 'v' + current_mode = 1; // MODE_VISUAL + visual_start_row = selected_row; + visual_start_col = selected_col; + visual_end_row = selected_row; + visual_end_col = selected_col; + + assert(current_mode == 1); + assert(visual_start_row == 3); + assert(visual_start_col == 4); + assert(visual_end_row == 3); + assert(visual_end_col == 4); + + printf("PASSED\n"); +} + +// Test 30: Visual mode selection expansion +void test_visual_mode_selection(void) { + printf("Test 30: Visual mode selection expansion... "); + + int visual_start_row = 2, visual_start_col = 2; + int visual_end_row = 2, visual_end_col = 2; + + // Move right + visual_end_col = (visual_end_col + 1) % 8; + assert(visual_end_col == 3); + + // Move down + visual_end_row = (visual_end_row + 1) % 8; + assert(visual_end_row == 3); + + // Check selection bounds + int min_row = (visual_start_row < visual_end_row) ? visual_start_row : visual_end_row; + int max_row = (visual_start_row > visual_end_row) ? visual_start_row : visual_end_row; + int min_col = (visual_start_col < visual_end_col) ? visual_start_col : visual_end_col; + int max_col = (visual_start_col > visual_end_col) ? visual_start_col : visual_end_col; + + assert(min_row == 2); + assert(max_row == 3); + assert(min_col == 2); + assert(max_col == 3); + + printf("PASSED\n"); +} + +// Test 31: Visual mode escape returns to normal +void test_visual_mode_escape(void) { + printf("Test 31: Visual mode escape returns to normal... "); + + int current_mode = 1; // MODE_VISUAL + + // Press Escape + current_mode = 0; // MODE_NORMAL + assert(current_mode == 0); + + printf("PASSED\n"); +} + +// Test 32: Visual line selection +void test_visual_line_selection(void) { + printf("Test 32: Visual line selection... "); + + int selected_row = 3; + int current_mode = 0; // MODE_NORMAL + int visual_start_row = 0, visual_start_col = 0; + int visual_end_row = 0, visual_end_col = 0; + + // Press 'V' + current_mode = 1; // MODE_VISUAL + visual_start_row = selected_row; + visual_start_col = 0; + visual_end_row = selected_row; + visual_end_col = 7; // GRID_COLS - 1 + + assert(current_mode == 1); + assert(visual_start_row == 3); + assert(visual_start_col == 0); + assert(visual_end_row == 3); + assert(visual_end_col == 7); + + printf("PASSED\n"); +} + +// Test 33: Move mode entry and navigation +void test_move_mode_navigation(void) { + printf("Test 33: Move mode navigation... "); + + int current_mode = 0; // MODE_NORMAL + int selected_row = 3, selected_col = 4; + + // Enter move mode + current_mode = 2; // MODE_MOVE + assert(current_mode == 2); + + // Move left + selected_col = (selected_col - 1 + 8) % 8; + assert(selected_col == 3); + + // Move down + selected_row = (selected_row + 1) % 8; + assert(selected_row == 4); + + // Move up + selected_row = (selected_row - 1 + 8) % 8; + assert(selected_row == 3); + + // Move right + selected_col = (selected_col + 1) % 8; + assert(selected_col == 4); + + printf("PASSED\n"); +} + +// Test 34: Move mode returns to normal on enter +void test_move_mode_enter(void) { + printf("Test 34: Move mode returns to normal on enter... "); + + int current_mode = 2; // MODE_MOVE + + // Press Enter + current_mode = 0; // MODE_NORMAL + assert(current_mode == 0); + + printf("PASSED\n"); +} + +// Test 35: Move mode returns to normal on escape +void test_move_mode_escape(void) { + printf("Test 35: Move mode returns to normal on escape... "); + + int current_mode = 2; // MODE_MOVE + + // Press Escape + current_mode = 0; // MODE_NORMAL + assert(current_mode == 0); + + printf("PASSED\n"); +} + +// Test 36: Delete (reset) single clip +void test_delete_single_clip(void) { + printf("Test 36: Delete (reset) single clip... "); + AppState *state = create_test_state(); + + // Set up a looping clip + int clip_idx = 10; + state->clips[clip_idx].state = CLIP_LOOPING; + state->clips[clip_idx].buffer_size = 100; + state->clips[clip_idx].write_position = 100; + state->clips[clip_idx].read_position = 50; + + // Simulate pressing 'd' on selected clip + Action action = { .type = ACTION_RESET_CLIP, .data.reset_clip.clip_index = clip_idx }; + reducer(state, action); + + assert(state->clips[clip_idx].state == CLIP_EMPTY); + assert(state->clips[clip_idx].buffer_size == 0); + assert(state->clips[clip_idx].write_position == 0); + assert(state->clips[clip_idx].read_position == 0); + + destroy_test_state(state); + printf("PASSED\n"); +} + +// Test 37: Delete (reset) multiple clips via visual selection +void test_delete_visual_selection(void) { + printf("Test 37: Delete visual selection... "); + AppState *state = create_test_state(); + + // Set up clips in a 2x2 selection area + int clips[] = {18, 19, 26, 27}; // rows 2-3, cols 2-3 + for (int i = 0; i < 4; i++) { + state->clips[clips[i]].state = CLIP_LOOPING; + state->clips[clips[i]].buffer_size = 100; + state->clips[clips[i]].write_position = 100; + state->clips[clips[i]].read_position = 50; + } + + // Simulate deleting the selection + for (int i = 0; i < 4; i++) { + Action action = { .type = ACTION_RESET_CLIP, .data.reset_clip.clip_index = clips[i] }; + reducer(state, action); + } + + for (int i = 0; i < 4; i++) { + assert(state->clips[clips[i]].state == CLIP_EMPTY); + assert(state->clips[clips[i]].buffer_size == 0); + } + + destroy_test_state(state); + printf("PASSED\n"); +} + +// Test 38: Yank single clip +void test_yank_single_clip(void) { + printf("Test 38: Yank single clip... "); + AppState *state = create_test_state(); + + // Set up a clip + int clip_idx = 15; + state->clips[clip_idx].state = CLIP_LOOPING; + state->clips[clip_idx].buffer_size = 100; + + // Simulate yanking the clip - should stop it (looping -> stopped) + Action action = { .type = ACTION_TRIGGER_CLIP, .data.trigger_clip.clip_index = clip_idx }; + reducer(state, action); + + assert(state->clips[clip_idx].state == CLIP_STOPPED); + + destroy_test_state(state); + printf("PASSED\n"); +} + +// Test 39: Yank multiple clips via visual selection +void test_yank_visual_selection(void) { + printf("Test 39: Yank visual selection... "); + AppState *state = create_test_state(); + + // Set up clips in a 2x2 selection + int clips[] = {18, 19, 26, 27}; + for (int i = 0; i < 4; i++) { + state->clips[clips[i]].state = CLIP_LOOPING; + state->clips[clips[i]].buffer_size = 100; + } + + // Simulate yanking the selection - should stop all clips + for (int i = 0; i < 4; i++) { + Action action = { .type = ACTION_TRIGGER_CLIP, .data.trigger_clip.clip_index = clips[i] }; + reducer(state, action); + } + + for (int i = 0; i < 4; i++) { + assert(state->clips[clips[i]].state == CLIP_STOPPED); + } + + destroy_test_state(state); + printf("PASSED\n"); +} + +// Test 40: Paste clips +void test_paste_clips(void) { + printf("Test 40: Paste clips... "); + AppState *state = create_test_state(); + + // Simulate yanking clip at position (1, 1) = clip 9 + int yank_buffer[] = {9}; + int selected_row = 3, selected_col = 3; // Paste at position (3, 3) = clip 27 + + // Calculate offset + int first_yanked_row = yank_buffer[0] / 8; + int first_yanked_col = yank_buffer[0] % 8; + int row_offset = selected_row - first_yanked_row; + int col_offset = selected_col - first_yanked_col; + + assert(first_yanked_row == 1); + assert(first_yanked_col == 1); + assert(row_offset == 2); + assert(col_offset == 2); + + // Simulate paste: trigger clip three times to go empty -> recording -> looping -> stopped + int new_row = first_yanked_row + row_offset; + int new_col = first_yanked_col + col_offset; + int new_clip_idx = new_row * 8 + new_col; + + assert(new_row == 3); + assert(new_col == 3); + assert(new_clip_idx == 27); + + Action action = { .type = ACTION_TRIGGER_CLIP, .data.trigger_clip.clip_index = new_clip_idx }; + reducer(state, action); + reducer(state, action); + reducer(state, action); + assert(state->clips[27].state == CLIP_STOPPED); + + destroy_test_state(state); + printf("PASSED\n"); +} + +// Test 41: Paste with bounds checking +void test_paste_bounds_checking(void) { + printf("Test 41: Paste bounds checking... "); + AppState *state = create_test_state(); + + // Yank clip at position (7, 7) = clip 63 (bottom-right) + int yank_buffer[] = {63}; + int selected_row = 0, selected_col = 0; // Paste at top-left + + // Calculate offset + int first_yanked_row = yank_buffer[0] / 8; + int first_yanked_col = yank_buffer[0] % 8; + int row_offset = selected_row - first_yanked_row; + int col_offset = selected_col - first_yanked_col; + + assert(row_offset == -7); + assert(col_offset == -7); + + // Simulate paste: new position should be (0, 0) = clip 0 + int new_row = first_yanked_row + row_offset; + int new_col = first_yanked_col + col_offset; + + assert(new_row == 0); + assert(new_col == 0); + + // Test out-of-bounds paste (should be clipped) + selected_row = 0; + selected_col = 0; + row_offset = selected_row - 0; + col_offset = selected_col - 0; + + // Yank clip at (0, 0) and try to paste at (0, 0) - should work + new_row = 0 + row_offset; + new_col = 0 + col_offset; + assert(new_row >= 0 && new_row < 8 && new_col >= 0 && new_col < 8); + + destroy_test_state(state); + printf("PASSED\n"); +} + +// Test 42: Mark setting +void test_mark_setting(void) { + printf("Test 42: Mark setting... "); + + int marks[26]; + for (int i = 0; i < 26; i++) { + marks[i] = -1; + } + + int selected_row = 3, selected_col = 4; + int clip_idx = selected_row * 8 + selected_col; + + // Set mark 'a' + char mark_char = 'a'; + int idx = mark_char - 'a'; + marks[idx] = clip_idx; + + assert(marks[0] == 28); // 3 * 8 + 4 = 28 + + // Set mark 'z' + mark_char = 'z'; + idx = mark_char - 'a'; + marks[idx] = clip_idx; + + assert(marks[25] == 28); + + printf("PASSED\n"); +} + +// Test 43: Go to mark +void test_go_to_mark(void) { + printf("Test 43: Go to mark... "); + + int marks[26]; + for (int i = 0; i < 26; i++) { + marks[i] = -1; + } + + // Set mark 'b' to clip 42 + marks[1] = 42; // 'b' - 'a' = 1 + + // Go to mark 'b' + char mark_char = 'b'; + int idx = mark_char - 'a'; + int clip_idx = marks[idx]; + + assert(clip_idx == 42); + + // Calculate row and col + int row = clip_idx / 8; + int col = clip_idx % 8; + assert(row == 5); + assert(col == 2); + + printf("PASSED\n"); +} + +// Test 44: Go to unset mark +void test_go_to_unset_mark(void) { + printf("Test 44: Go to unset mark... "); + + int marks[26]; + for (int i = 0; i < 26; i++) { + marks[i] = -1; + } + + // Try to go to unset mark 'c' + char mark_char = 'c'; + int idx = mark_char - 'a'; + int clip_idx = marks[idx]; + + assert(clip_idx == -1); // Mark not set + + printf("PASSED\n"); +} + +// Test 45: Play next scene +void test_play_next_scene(void) { + printf("Test 45: Play next scene... "); + AppState *state = create_test_state(); + + int selected_row = 3; + + // Play next scene + int next_row = (selected_row + 1) % 8; + Action action = { .type = ACTION_TRIGGER_SCENE, .data.trigger_scene.scene_index = next_row }; + reducer(state, action); + + assert(next_row == 4); + + // Verify clips in scene 4 are recording + for (int ch = 0; ch < MAX_CHANNELS; ch++) { + int clip_idx = CLIP_INDEX(4, ch); + assert(state->clips[clip_idx].state == CLIP_RECORDING); + } + + destroy_test_state(state); + printf("PASSED\n"); +} + +// Test 46: Play previous scene +void test_play_prev_scene(void) { + printf("Test 46: Play previous scene... "); + AppState *state = create_test_state(); + + int selected_row = 3; + + // Play previous scene + int prev_row = (selected_row - 1 + 8) % 8; + Action action = { .type = ACTION_TRIGGER_SCENE, .data.trigger_scene.scene_index = prev_row }; + reducer(state, action); + + assert(prev_row == 2); + + // Verify clips in scene 2 are recording + for (int ch = 0; ch < MAX_CHANNELS; ch++) { + int clip_idx = CLIP_INDEX(2, ch); + assert(state->clips[clip_idx].state == CLIP_RECORDING); + } + + destroy_test_state(state); + printf("PASSED\n"); +} + +// Test 47: Play next scene wraps around +void test_play_next_scene_wrap(void) { + printf("Test 47: Play next scene wraps around... "); + AppState *state = create_test_state(); + + int selected_row = 7; // Last row + + // Play next scene should wrap to row 0 + int next_row = (selected_row + 1) % 8; + assert(next_row == 0); + + Action action = { .type = ACTION_TRIGGER_SCENE, .data.trigger_scene.scene_index = next_row }; + reducer(state, action); + + for (int ch = 0; ch < MAX_CHANNELS; ch++) { + int clip_idx = CLIP_INDEX(0, ch); + assert(state->clips[clip_idx].state == CLIP_RECORDING); + } + + destroy_test_state(state); + printf("PASSED\n"); +} + +// Test 48: Play previous scene wraps around +void test_play_prev_scene_wrap(void) { + printf("Test 48: Play previous scene wraps around... "); + AppState *state = create_test_state(); + + int selected_row = 0; // First row + + // Play previous scene should wrap to row 7 + int prev_row = (selected_row - 1 + 8) % 8; + assert(prev_row == 7); + + Action action = { .type = ACTION_TRIGGER_SCENE, .data.trigger_scene.scene_index = prev_row }; + reducer(state, action); + + for (int ch = 0; ch < MAX_CHANNELS; ch++) { + int clip_idx = CLIP_INDEX(7, ch); + assert(state->clips[clip_idx].state == CLIP_RECORDING); + } + + destroy_test_state(state); + printf("PASSED\n"); +} + +// Test 49: Visual mode delete then escape +void test_visual_delete_then_escape(void) { + printf("Test 49: Visual mode delete then escape... "); + AppState *state = create_test_state(); + + // Set up clips in visual selection + int clips[] = {18, 19, 26, 27}; + for (int i = 0; i < 4; i++) { + state->clips[clips[i]].state = CLIP_LOOPING; + state->clips[clips[i]].buffer_size = 100; + } + + // Simulate visual mode delete + for (int i = 0; i < 4; i++) { + Action action = { .type = ACTION_RESET_CLIP, .data.reset_clip.clip_index = clips[i] }; + reducer(state, action); + } + + // Verify clips are reset + for (int i = 0; i < 4; i++) { + assert(state->clips[clips[i]].state == CLIP_EMPTY); + } + + // Simulate returning to normal mode + int current_mode = 0; // MODE_NORMAL + assert(current_mode == 0); + + destroy_test_state(state); + printf("PASSED\n"); +} + +// Test 50: Visual mode yank then escape +void test_visual_yank_then_escape(void) { + printf("Test 50: Visual mode yank then escape... "); + AppState *state = create_test_state(); + + // Set up clips in visual selection + int clips[] = {18, 19, 26, 27}; + for (int i = 0; i < 4; i++) { + state->clips[clips[i]].state = CLIP_LOOPING; + state->clips[clips[i]].buffer_size = 100; + } + + // Simulate yanking the selection - should stop all clips + for (int i = 0; i < 4; i++) { + Action action = { .type = ACTION_TRIGGER_CLIP, .data.trigger_clip.clip_index = clips[i] }; + reducer(state, action); + } + + // Verify clips are stopped + for (int i = 0; i < 4; i++) { + assert(state->clips[clips[i]].state == CLIP_STOPPED); + } + + // Simulate returning to normal mode + int current_mode = 0; // MODE_NORMAL + assert(current_mode == 0); + + destroy_test_state(state); + printf("PASSED\n"); +} + +// Test 51: Multiple marks +void test_multiple_marks(void) { + printf("Test 51: Multiple marks... "); + + int marks[26]; + for (int i = 0; i < 26; i++) { + marks[i] = -1; + } + + // Set multiple marks + marks[0] = 0; // 'a' = clip 0 + marks[1] = 63; // 'b' = clip 63 + marks[2] = 28; // 'c' = clip 28 + + assert(marks[0] == 0); + assert(marks[1] == 63); + assert(marks[2] == 28); + + // Go to mark 'b' + int clip_idx = marks[1]; + int row = clip_idx / 8; + int col = clip_idx % 8; + assert(row == 7); + assert(col == 7); + + printf("PASSED\n"); +} + +// Test 52: Re-mark existing mark +void test_remark_existing_mark(void) { + printf("Test 52: Re-mark existing mark... "); + + int marks[26]; + for (int i = 0; i < 26; i++) { + marks[i] = -1; + } + + // Set mark 'a' to clip 10 + marks[0] = 10; + assert(marks[0] == 10); + + // Re-mark 'a' to clip 42 + marks[0] = 42; + assert(marks[0] == 42); + + printf("PASSED\n"); +} + +// Test 53: Undo single clip trigger +void test_undo_single_trigger(void) { + printf("Test 53: Undo single clip trigger... "); + AppState *state = create_test_state(); + + // Start with empty clip + int clip_idx = 10; + assert(state->clips[clip_idx].state == CLIP_EMPTY); + + // Trigger clip (empty -> recording) + Action action = { .type = ACTION_TRIGGER_CLIP, .data.trigger_clip.clip_index = clip_idx }; + reducer(state, action); + assert(state->clips[clip_idx].state == CLIP_RECORDING); + + // Undo: should go back to empty + action.type = ACTION_UNDO; + reducer(state, action); + assert(state->clips[clip_idx].state == CLIP_EMPTY); + + printf("PASSED\n"); + destroy_test_state(state); +} + +// Test 54: Undo multiple clip triggers +void test_undo_multiple_triggers(void) { + printf("Test 54: Undo multiple clip triggers... "); + AppState *state = create_test_state(); + + int clip1 = 10, clip2 = 11, clip3 = 12; + + // Trigger three clips + Action action = { .type = ACTION_TRIGGER_CLIP, .data.trigger_clip.clip_index = clip1 }; + reducer(state, action); + assert(state->clips[clip1].state == CLIP_RECORDING); + + action.data.trigger_clip.clip_index = clip2; + reducer(state, action); + assert(state->clips[clip2].state == CLIP_RECORDING); + + action.data.trigger_clip.clip_index = clip3; + reducer(state, action); + assert(state->clips[clip3].state == CLIP_RECORDING); + + // Undo last action: clip3 should go back to empty + action.type = ACTION_UNDO; + reducer(state, action); + assert(state->clips[clip3].state == CLIP_EMPTY); + assert(state->clips[clip2].state == CLIP_RECORDING); + assert(state->clips[clip1].state == CLIP_RECORDING); + + // Undo again: clip2 should go back to empty + reducer(state, action); + assert(state->clips[clip2].state == CLIP_EMPTY); + assert(state->clips[clip1].state == CLIP_RECORDING); + + printf("PASSED\n"); + destroy_test_state(state); +} + +// Test 55: Redo single undo +void test_redo_single_undo(void) { + printf("Test 55: Redo single undo... "); + AppState *state = create_test_state(); + + int clip_idx = 10; + + // Trigger clip + Action action = { .type = ACTION_TRIGGER_CLIP, .data.trigger_clip.clip_index = clip_idx }; + reducer(state, action); + assert(state->clips[clip_idx].state == CLIP_RECORDING); + + // Undo + action.type = ACTION_UNDO; + reducer(state, action); + assert(state->clips[clip_idx].state == CLIP_EMPTY); + + // Redo: should go back to recording + action.type = ACTION_REDO; + reducer(state, action); + assert(state->clips[clip_idx].state == CLIP_RECORDING); + + printf("PASSED\n"); + destroy_test_state(state); +} + +// Test 56: Redo after multiple undos +void test_redo_multiple_undos(void) { + printf("Test 56: Redo after multiple undos... "); + AppState *state = create_test_state(); + + int clip1 = 10, clip2 = 11; + + // Trigger two clips + Action action = { .type = ACTION_TRIGGER_CLIP, .data.trigger_clip.clip_index = clip1 }; + reducer(state, action); + action.data.trigger_clip.clip_index = clip2; + reducer(state, action); + assert(state->clips[clip1].state == CLIP_RECORDING); + assert(state->clips[clip2].state == CLIP_RECORDING); + + // Undo twice + action.type = ACTION_UNDO; + reducer(state, action); + reducer(state, action); + assert(state->clips[clip1].state == CLIP_EMPTY); + assert(state->clips[clip2].state == CLIP_EMPTY); + + // Redo twice + action.type = ACTION_REDO; + reducer(state, action); + assert(state->clips[clip1].state == CLIP_RECORDING); + assert(state->clips[clip2].state == CLIP_EMPTY); + + reducer(state, action); + assert(state->clips[clip1].state == CLIP_RECORDING); + assert(state->clips[clip2].state == CLIP_RECORDING); + + printf("PASSED\n"); + destroy_test_state(state); +} + +// Test 57: Undo scene trigger +void test_undo_scene_trigger(void) { + printf("Test 57: Undo scene trigger... "); + AppState *state = create_test_state(); + + int scene_idx = 3; + + // Trigger scene + Action action = { .type = ACTION_TRIGGER_SCENE, .data.trigger_scene.scene_index = scene_idx }; + reducer(state, action); + + // All clips in scene should be recording + for (int ch = 0; ch < MAX_CHANNELS; ch++) { + int clip_idx = CLIP_INDEX(scene_idx, ch); + assert(state->clips[clip_idx].state == CLIP_RECORDING); + } + + // Undo: all clips should go back to empty + action.type = ACTION_UNDO; + reducer(state, action); + for (int ch = 0; ch < MAX_CHANNELS; ch++) { + int clip_idx = CLIP_INDEX(scene_idx, ch); + assert(state->clips[clip_idx].state == CLIP_EMPTY); + } + + printf("PASSED\n"); + destroy_test_state(state); +} + +// Test 58: Undo clip state cycle (empty -> recording -> looping -> stopped) +void test_undo_clip_state_cycle(void) { + printf("Test 58: Undo clip state cycle... "); + AppState *state = create_test_state(); + + int clip_idx = 10; + + // Cycle through all states + Action action = { .type = ACTION_TRIGGER_CLIP, .data.trigger_clip.clip_index = clip_idx }; + reducer(state, action); // empty -> recording + assert(state->clips[clip_idx].state == CLIP_RECORDING); + + reducer(state, action); // recording -> looping + assert(state->clips[clip_idx].state == CLIP_LOOPING); + + reducer(state, action); // looping -> stopped + assert(state->clips[clip_idx].state == CLIP_STOPPED); + + // Undo three times to go back to empty + action.type = ACTION_UNDO; + reducer(state, action); // stopped -> looping + assert(state->clips[clip_idx].state == CLIP_LOOPING); + + reducer(state, action); // looping -> recording + assert(state->clips[clip_idx].state == CLIP_RECORDING); + + reducer(state, action); // recording -> empty + assert(state->clips[clip_idx].state == CLIP_EMPTY); + + printf("PASSED\n"); + destroy_test_state(state); +} + +// Test 59: Undo after new action clears redo history +void test_undo_clears_redo_on_new_action(void) { + printf("Test 59: Undo after new action clears redo history... "); + AppState *state = create_test_state(); + + int clip1 = 10, clip2 = 11; + + // Trigger clip1 + Action action = { .type = ACTION_TRIGGER_CLIP, .data.trigger_clip.clip_index = clip1 }; + reducer(state, action); + assert(state->clips[clip1].state == CLIP_RECORDING); + + // Undo + action.type = ACTION_UNDO; + reducer(state, action); + assert(state->clips[clip1].state == CLIP_EMPTY); + + // Redo should work + action.type = ACTION_REDO; + reducer(state, action); + assert(state->clips[clip1].state == CLIP_RECORDING); + + // Undo again + action.type = ACTION_UNDO; + reducer(state, action); + assert(state->clips[clip1].state == CLIP_EMPTY); + + // Now do a new action (trigger clip2) + action.type = ACTION_TRIGGER_CLIP; + action.data.trigger_clip.clip_index = clip2; + reducer(state, action); + assert(state->clips[clip2].state == CLIP_RECORDING); + + // Redo should NOT work now (redo history cleared) + action.type = ACTION_REDO; + reducer(state, action); + assert(state->clips[clip1].state == CLIP_EMPTY); // Should still be empty + + printf("PASSED\n"); + destroy_test_state(state); +} + +// Test 60: Undo reset clip +void test_undo_reset_clip(void) { + printf("Test 60: Undo reset clip... "); + AppState *state = create_test_state(); + + int clip_idx = 10; + + // Set up a looping clip with data + state->clips[clip_idx].state = CLIP_LOOPING; + state->clips[clip_idx].buffer_size = 100; + state->clips[clip_idx].write_position = 100; + state->clips[clip_idx].read_position = 50; + + // Reset the clip + Action action = { .type = ACTION_RESET_CLIP, .data.reset_clip.clip_index = clip_idx }; + reducer(state, action); + assert(state->clips[clip_idx].state == CLIP_EMPTY); + assert(state->clips[clip_idx].buffer_size == 0); + + // Undo: should restore clip to previous state + action.type = ACTION_UNDO; + reducer(state, action); + assert(state->clips[clip_idx].state == CLIP_LOOPING); + assert(state->clips[clip_idx].buffer_size == 100); + assert(state->clips[clip_idx].write_position == 100); + assert(state->clips[clip_idx].read_position == 50); + + printf("PASSED\n"); + destroy_test_state(state); +} + + + + +// Test 64: Undo/redo with paste operation +void test_undo_paste(void) { + printf("Test 64: Undo paste operation... "); + AppState *state = create_test_state(); + + int clip_idx = 27; + + // Simulate paste: trigger clip three times to go empty -> recording -> looping -> stopped + Action action = { .type = ACTION_TRIGGER_CLIP, .data.trigger_clip.clip_index = clip_idx }; + reducer(state, action); + reducer(state, action); + reducer(state, action); + assert(state->clips[clip_idx].state == CLIP_STOPPED); + + // Undo: should go back to looping (undo last trigger: stopped -> looping) + action.type = ACTION_UNDO; + reducer(state, action); + assert(state->clips[clip_idx].state == CLIP_LOOPING); + + // Undo again: should go back to recording + reducer(state, action); + assert(state->clips[clip_idx].state == CLIP_RECORDING); + + // Undo again: should go back to empty + reducer(state, action); + assert(state->clips[clip_idx].state == CLIP_EMPTY); + + // Redo three times: should go back to stopped + action.type = ACTION_REDO; + reducer(state, action); + assert(state->clips[clip_idx].state == CLIP_RECORDING); + + reducer(state, action); + assert(state->clips[clip_idx].state == CLIP_LOOPING); + + reducer(state, action); + assert(state->clips[clip_idx].state == CLIP_STOPPED); + + printf("PASSED\n"); + destroy_test_state(state); +} + +int main(void) { + printf("Running TUI tests...\n\n"); + + test_grid_to_clip_index(); + test_trigger_via_grid(); + test_reset_via_grid(); + test_scene_via_grid(); + test_quantize_cycling(); + test_threshold_toggle(); + test_transport_reset_via_tui(); + test_navigation_wrapping(); + test_multiple_clip_states(); + test_buffer_size_display(); + test_help_toggle(); + test_escape_handling(); + test_tui_init_cleanup(); + test_state_to_color_mapping(); + test_full_grid_coverage(); + test_scene_from_each_row(); + test_quantize_full_cycle(); + test_multiple_threshold_toggles(); + test_multiple_transport_resets(); + test_arrow_key_navigation(); + test_command_mode_quit(); + test_command_mode_empty(); + test_command_mode_unknown(); + test_command_mode_buffer_overflow(); + test_command_mode_backspace(); + test_command_mode_escape(); + test_command_mode_enter(); + test_command_mode_colon(); + test_visual_mode_entry(); + test_visual_mode_selection(); + test_visual_mode_escape(); + test_visual_line_selection(); + test_move_mode_navigation(); + test_move_mode_enter(); + test_move_mode_escape(); + test_delete_single_clip(); + test_delete_visual_selection(); + test_yank_single_clip(); + test_yank_visual_selection(); + test_paste_clips(); + test_paste_bounds_checking(); + test_mark_setting(); + test_go_to_mark(); + test_go_to_unset_mark(); + test_play_next_scene(); + test_play_prev_scene(); + test_play_next_scene_wrap(); + test_play_prev_scene_wrap(); + test_visual_delete_then_escape(); + test_visual_yank_then_escape(); + test_multiple_marks(); + test_remark_existing_mark(); + test_undo_single_trigger(); + test_undo_multiple_triggers(); + test_redo_single_undo(); + test_redo_multiple_undos(); + test_undo_scene_trigger(); + test_undo_clip_state_cycle(); + test_undo_clears_redo_on_new_action(); + test_undo_reset_clip(); + test_undo_paste(); + + printf("\nAll TUI tests passed!\n"); + return 0; +}