LCOV - code coverage report
Current view: top level - tests/pty - test_pty_compose_dialog.c (source / functions) Coverage Total Hit
Test: coverage-functional.info Lines: 99.4 % 1173 1166
Test Date: 2026-08-21 10:11:30 Functions: 100.0 % 77 77

            Line data    Source code
       1              : /**
       2              :  * @file test_pty_compose_dialog.c
       3              :  * @brief PTY tests for the pre-compose dialog (US-CD-1 … US-CD-12).
       4              :  *
       5              :  * Covers: new message dialog, reply dialog, reply-all (A), forward (F),
       6              :  * contact autocomplete, Cc/Bcc field handling, and field navigation.
       7              :  *
       8              :  * Usage: test-pty-compose-dialog <email-tui> <mock-imap-server> <mock-smtp-server>
       9              :  *
      10              :  * The pre-compose dialog runs before the editor opens, so these tests
      11              :  * never need real vim interaction.  The EDITOR env var is set to a tiny
      12              :  * shell script that appends "Test body\n" to the draft and exits 0.
      13              :  */
      14              : 
      15              : #define _DEFAULT_SOURCE
      16              : #define _XOPEN_SOURCE 600
      17              : 
      18              : #include "ptytest.h"
      19              : #include "pty_assert.h"
      20              : 
      21              : #include <fcntl.h>
      22              : #include <netinet/in.h>
      23              : #include <signal.h>
      24              : #include <stdio.h>
      25              : #include <stdlib.h>
      26              : #include <string.h>
      27              : #include <sys/socket.h>
      28              : #include <sys/stat.h>
      29              : #include <sys/wait.h>
      30              : #include <unistd.h>
      31              : 
      32              : /* ── Test globals ────────────────────────────────────────────────────── */
      33              : 
      34              : int g_tests_run    = 0;
      35              : int g_tests_failed = 0;
      36              : 
      37              : static char g_tui_bin[512];
      38              : static char g_imap_bin[512];
      39              : static char g_smtp_bin[512];
      40              : 
      41              : /* EMAIL_USER value in the generated config — the local store names the
      42              :  * data directory after it (the .config directory name is unrelated). */
      43              : #define TEST_ACCOUNT "testuser@example.com"
      44              : 
      45              : static char g_test_home[512];
      46              : static char g_old_home[512];
      47              : static char g_editor_script[600]; /* path to fake editor script */
      48              : 
      49              : static pid_t g_imap_pid = -1;
      50              : static pid_t g_smtp_pid = -1;
      51              : 
      52              : #define WAIT_MS   6000
      53              : #define SETTLE_MS  400
      54              : #define ROWS 24
      55              : #define COLS 120
      56              : 
      57              : #define RUN_TEST(fn) do { printf("  %s...\n", #fn); fflush(stdout); fn(); } while(0)
      58              : 
      59              : /* ── Fake editor ─────────────────────────────────────────────────────── */
      60              : 
      61              : /*
      62              :  * Write a tiny shell script that appends "Test body\n" to the first
      63              :  * argument (the draft file) then exits 0.  This lets us simulate the
      64              :  * user writing a body without actually launching vim.
      65              :  */
      66          342 : static void write_editor_script(void) {
      67          342 :     snprintf(g_editor_script, sizeof(g_editor_script),
      68              :              "%s/.fake_editor.sh", g_test_home);
      69          342 :     FILE *f = fopen(g_editor_script, "w");
      70          342 :     if (!f) return;
      71          342 :     fprintf(f, "#!/bin/sh\necho 'Test body' >> \"$1\"\n");
      72          342 :     fclose(f);
      73          342 :     chmod(g_editor_script, 0755);
      74          342 :     setenv("EDITOR", g_editor_script, 1);
      75              : }
      76              : 
      77              : /* ── Config helpers ──────────────────────────────────────────────────── */
      78              : 
      79         2076 : static void mkdirs(void) {
      80              :     char d[600];
      81         2076 :     snprintf(d, sizeof(d), "%s/.config", g_test_home);          mkdir(d, 0700);
      82         2076 :     snprintf(d, sizeof(d), "%s/.config/email-cli", g_test_home); mkdir(d, 0700);
      83         2076 :     snprintf(d, sizeof(d), "%s/.config/email-cli/accounts", g_test_home); mkdir(d, 0700);
      84         2076 :     snprintf(d, sizeof(d), "%s/.config/email-cli/accounts/testuser", g_test_home); mkdir(d, 0700);
      85         2076 :     snprintf(d, sizeof(d), "%s/.local", g_test_home);            mkdir(d, 0700);
      86         2076 :     snprintf(d, sizeof(d), "%s/.local/share", g_test_home);      mkdir(d, 0700);
      87         2076 :     snprintf(d, sizeof(d), "%s/.local/share/email-cli", g_test_home); mkdir(d, 0700);
      88         2076 :     snprintf(d, sizeof(d), "%s/.local/share/email-cli/accounts", g_test_home); mkdir(d, 0700);
      89         2076 :     snprintf(d, sizeof(d), "%s/.local/share/email-cli/accounts/" TEST_ACCOUNT "", g_test_home); mkdir(d, 0700);
      90         2076 :     snprintf(d, sizeof(d), "%s/.local/share/email-cli/accounts/" TEST_ACCOUNT "/manifests", g_test_home); mkdir(d, 0700);
      91         2076 : }
      92              : 
      93         2014 : static void write_config(void) {
      94         2014 :     mkdirs();
      95              :     char path[700];
      96         2014 :     snprintf(path, sizeof(path),
      97              :              "%s/.config/email-cli/accounts/testuser/config.ini", g_test_home);
      98         2014 :     FILE *fp = fopen(path, "w");
      99         2014 :     if (!fp) return;
     100         2014 :     fprintf(fp,
     101              :         "EMAIL_HOST=imaps://localhost:9993\n"
     102              :         "EMAIL_USER=testuser@example.com\n"
     103              :         "EMAIL_PASS=testpass\n"
     104              :         "EMAIL_FOLDER=INBOX\n"
     105              :         "SMTP_HOST=smtps://localhost:9025\n"
     106              :         "SMTP_PORT=9025\n"
     107              :         "SMTP_USER=testuser@example.com\n"
     108              :         "SMTP_PASS=testpass\n"
     109              :         "SSL_NO_VERIFY=1\n");
     110         2014 :     fclose(fp);
     111         2014 :     chmod(path, 0600);
     112              : }
     113              : 
     114              : /* ── Contacts seed helper ─────────────────────────────────────────────── */
     115              : 
     116              : /*
     117              :  * Seed contacts.tsv so autocomplete has entries without needing real .hdr files.
     118              :  * Format: address\tdisplay_name\tfrequency
     119              :  */
     120          431 : static void write_contacts_tsv(void) {
     121              :     char path[700];
     122          431 :     snprintf(path, sizeof(path),
     123              :              "%s/.local/share/email-cli/accounts/" TEST_ACCOUNT "/contacts.tsv",
     124              :              g_test_home);
     125          431 :     FILE *f = fopen(path, "w");
     126          431 :     if (!f) return;
     127          431 :     fprintf(f, "alice@example.com\tAlice Example\t10\n");
     128          431 :     fprintf(f, "bob.smith@acme.com\tBob Smith\t8\n");
     129          431 :     fprintf(f, "carol.jones@corp.org\tCarol Jones\t5\n");
     130          431 :     fprintf(f, "john.doe@example.com\tJohn Doe\t3\n");
     131          431 :     fprintf(f, "johnathan.green@partner.net\tJohnathan Green\t2\n");
     132          431 :     fclose(f);
     133              : }
     134              : 
     135              : /* ── Manifest seed helper ──────────────────────────────────────────────── */
     136              : 
     137              : /* Seed an INBOX manifest with one message that has alice@example.com as sender */
     138           10 : static void write_inbox_manifest(void) {
     139              :     char path[700];
     140           10 :     snprintf(path, sizeof(path),
     141              :              "%s/.local/share/email-cli/accounts/" TEST_ACCOUNT "/manifests/INBOX.tsv",
     142              :              g_test_home);
     143           10 :     FILE *f = fopen(path, "w");
     144           10 :     if (!f) return;
     145              :     /* uid\tfrom\tsubject\tdate\tflags */
     146           10 :     fprintf(f, "0000000000000001\talice@example.com\tHello World\t2026-04-25 10:00\t1\n");
     147           10 :     fprintf(f, "0000000000000002\tbob.smith@acme.com\tProject Update\t2026-04-25 09:00\t0\n");
     148           10 :     fclose(f);
     149              : }
     150              : 
     151              : /* Seed a contacts.tsv built from manifest From addresses */
     152           10 : static void write_contacts_from_manifests(void) {
     153              :     char path[700];
     154           10 :     snprintf(path, sizeof(path),
     155              :              "%s/.local/share/email-cli/accounts/" TEST_ACCOUNT "/contacts.tsv",
     156              :              g_test_home);
     157           10 :     FILE *f = fopen(path, "w");
     158           10 :     if (!f) return;
     159           10 :     fprintf(f, "alice@example.com\t\t2\n");
     160           10 :     fprintf(f, "bob.smith@acme.com\tBob Smith\t1\n");
     161           10 :     fclose(f);
     162              : }
     163              : 
     164              : /* ── Mock server management ────────────────────────────────────────────── */
     165              : 
     166         2694 : static int probe_port(int port) {
     167         2694 :     int fd = socket(AF_INET, SOCK_STREAM, 0);
     168         2694 :     if (fd < 0) return -1;
     169         2694 :     struct sockaddr_in a = {
     170              :         .sin_family = AF_INET,
     171         2694 :         .sin_port   = htons((uint16_t)port),
     172         2694 :         .sin_addr.s_addr = htonl(INADDR_LOOPBACK)
     173              :     };
     174         2694 :     int r = connect(fd, (struct sockaddr *)&a, sizeof(a));
     175         2694 :     close(fd);
     176         2694 :     return r;
     177              : }
     178              : 
     179         2694 : static void start_server(const char *bin, pid_t *pid_out) {
     180         2694 :     *pid_out = fork();
     181         2783 :     if (*pid_out < 0) return;
     182         2783 :     if (*pid_out == 0) {
     183           89 :         int devnull = open("/dev/null", O_WRONLY);
     184           89 :         if (devnull >= 0) { dup2(devnull, 1); dup2(devnull, 2); close(devnull); }
     185           89 :         execl(bin, bin, (char *)NULL);
     186           89 :         _exit(127);
     187              :     }
     188         2694 :     usleep(800000);
     189              : }
     190              : 
     191         2696 : static void stop_server(pid_t *pid_out) {
     192         2696 :     if (*pid_out > 0) {
     193         2572 :         kill(*pid_out, SIGKILL);
     194         2572 :         waitpid(*pid_out, NULL, 0);
     195         2572 :         *pid_out = -1;
     196              :     }
     197         2696 : }
     198              : 
     199         2291 : static void restart_imap(void) {
     200         2291 :     stop_server(&g_imap_pid);
     201         2291 :     usleep(200000);
     202         2291 :     start_server(g_imap_bin, &g_imap_pid);
     203         2291 :     for (int i = 0; i < 30 && probe_port(9993) != 0; i++) usleep(100000);
     204         2291 : }
     205              : 
     206          403 : static void restart_smtp(void) {
     207          403 :     stop_server(&g_smtp_pid);
     208          403 :     usleep(200000);
     209          403 :     start_server(g_smtp_bin, &g_smtp_pid);
     210          403 :     for (int i = 0; i < 30 && probe_port(9025) != 0; i++) usleep(100000);
     211          403 : }
     212              : 
     213              : /* ── TUI runner ───────────────────────────────────────────────────────── */
     214              : 
     215         1952 : static PtySession *tui_run(const char **extra_args) {
     216              :     const char *args[32];
     217         1952 :     int n = 0;
     218         1952 :     args[n++] = g_tui_bin;
     219         1952 :     if (extra_args)
     220            0 :         for (int i = 0; extra_args[i] && n < 31; i++)
     221            0 :             args[n++] = extra_args[i];
     222         1952 :     args[n] = NULL;
     223              : 
     224         1952 :     PtySession *s = pty_open(COLS, ROWS);
     225         1952 :     if (!s) return NULL;
     226         1952 :     if (pty_run(s, args) != 0) { pty_close(s); return NULL; }
     227         1891 :     return s;
     228              : }
     229              : 
     230              : /*
     231              :  * Open TUI and navigate to the INBOX message list.
     232              :  * The TUI starts at the accounts screen; press Enter to open the account,
     233              :  * then Enter again to open the INBOX folder.
     234              :  */
     235              : /* US-85: submitting the compose dialog runs $EDITOR and then shows the
     236              :  * post-compose review screen.  Press 's' there to actually send. */
     237          326 : static void pcr_send(PtySession *s) {
     238          326 :     if (pty_wait_for(s, "Review: Message", WAIT_MS) != 0)
     239            0 :         printf("  [WARN] pcr_send: review screen did not appear\n");
     240          326 :     pty_send_str(s, "s");
     241          326 : }
     242              : 
     243         1916 : static PtySession *tui_open_to_inbox(void) {
     244         1916 :     write_config();
     245         1916 :     PtySession *s = tui_run(NULL);
     246         1858 :     if (!s) return NULL;
     247              :     /* Accounts screen → open account */
     248         1858 :     if (pty_wait_for(s, "testuser", WAIT_MS) != 0) { pty_close(s); return NULL; }
     249         1858 :     pty_settle(s, SETTLE_MS);
     250         1858 :     pty_send_key(s, PTY_KEY_ENTER);
     251              :     /* Folder browser: HOME → VP_UNREAD(1), then 6× DOWN reaches INBOX(8),
     252              :      * skipping VP_HDR_FOLD which the browser auto-skips on navigation. */
     253         1858 :     if (pty_wait_for(s, "Folders", WAIT_MS) != 0) { pty_close(s); return NULL; }
     254         1858 :     pty_send_key(s, PTY_KEY_HOME);
     255        13006 :     for (int i = 0; i < 6; i++) { pty_send_key(s, PTY_KEY_DOWN); pty_settle(s, 50); }
     256         1858 :     if (pty_wait_for(s, "INBOX", WAIT_MS) != 0) { pty_close(s); return NULL; }
     257         1858 :     pty_settle(s, SETTLE_MS);
     258         1858 :     pty_send_key(s, PTY_KEY_ENTER);
     259         1858 :     if (pty_wait_for(s, "message(s) in", WAIT_MS) != 0) { pty_close(s); return NULL; }
     260         1858 :     pty_settle(s, SETTLE_MS);
     261         1858 :     return s;
     262              : }
     263              : 
     264              : /* ══════════════════════════════════════════════════════════════════════
     265              :  *  TC-CD-01 to TC-CD-08: Basic compose dialog flow
     266              :  * ══════════════════════════════════════════════════════════════════════ */
     267              : 
     268           62 : static void test_compose_dialog_opens(void) {
     269              :     /* TC-CD-01: pressing 'c' opens the pre-compose dialog, not vim */
     270           62 :     restart_imap();
     271           62 :     PtySession *s = tui_open_to_inbox();
     272           61 :     ASSERT(s != NULL, "compose opens: reached inbox");
     273              : 
     274           61 :     pty_send_str(s, "c");
     275           61 :     ASSERT_WAIT_FOR(s, "New Message", WAIT_MS);
     276           61 :     pty_settle(s, SETTLE_MS);
     277              : 
     278              :     /* Dialog must show all four fields */
     279           61 :     ASSERT_SCREEN_CONTAINS(s, "To:");
     280           61 :     ASSERT_SCREEN_CONTAINS(s, "Cc:");
     281           61 :     ASSERT_SCREEN_CONTAINS(s, "Bcc:");
     282           61 :     ASSERT_SCREEN_CONTAINS(s, "Subject:");
     283              : 
     284           61 :     pty_send_key(s, PTY_KEY_ESC);
     285           61 :     pty_close(s);
     286              : }
     287              : 
     288           61 : static void test_compose_dialog_title(void) {
     289              :     /* TC-CD-08: dialog title reads "New Message" */
     290           61 :     restart_imap();
     291           61 :     PtySession *s = tui_open_to_inbox();
     292           60 :     ASSERT(s != NULL, "dialog title: reached inbox");
     293              : 
     294           60 :     pty_send_str(s, "c");
     295           60 :     ASSERT_WAIT_FOR(s, "New Message", WAIT_MS);
     296           60 :     ASSERT_SCREEN_CONTAINS(s, "New Message");
     297              : 
     298           60 :     pty_send_key(s, PTY_KEY_ESC);
     299           60 :     pty_close(s);
     300              : }
     301              : 
     302           60 : static void test_compose_dialog_status_line(void) {
     303              :     /* Status line shows key bindings hint */
     304           60 :     restart_imap();
     305           60 :     PtySession *s = tui_open_to_inbox();
     306           59 :     ASSERT(s != NULL, "status line: reached inbox");
     307              : 
     308           59 :     pty_send_str(s, "c");
     309           59 :     ASSERT_WAIT_FOR(s, "New Message", WAIT_MS);
     310           59 :     pty_settle(s, SETTLE_MS);
     311              :     /* At least Tab and ESC must be mentioned */
     312           59 :     ASSERT_SCREEN_CONTAINS(s, "Tab");
     313           59 :     ASSERT_SCREEN_CONTAINS(s, "ESC");
     314              : 
     315           59 :     pty_send_key(s, PTY_KEY_ESC);
     316           59 :     pty_close(s);
     317              : }
     318              : 
     319           59 : static void test_compose_esc_cancels(void) {
     320              :     /* TC-CD-02: Esc in compose dialog returns to message list */
     321           59 :     restart_imap();
     322           59 :     PtySession *s = tui_open_to_inbox();
     323           58 :     ASSERT(s != NULL, "esc cancels: reached inbox");
     324              : 
     325           58 :     pty_send_str(s, "c");
     326           58 :     ASSERT_WAIT_FOR(s, "New Message", WAIT_MS);
     327           58 :     pty_settle(s, SETTLE_MS);
     328              : 
     329           58 :     pty_send_key(s, PTY_KEY_ESC);
     330              :     /* After compose cancel, TUI shows "[Press any key]" before returning to list */
     331           58 :     ASSERT_WAIT_FOR(s, "[Press any key", WAIT_MS);
     332           58 :     pty_send_str(s, " ");
     333           58 :     ASSERT_WAIT_FOR(s, "message(s) in", WAIT_MS);
     334           58 :     pty_settle(s, SETTLE_MS);
     335              :     /* Must NOT have opened editor (vim is not running) */
     336           58 :     ASSERT_SCREEN_NOT_CONTAINS(s, "~"); /* vim empty-buffer tilde markers */
     337              : 
     338           58 :     pty_send_key(s, PTY_KEY_ESC);
     339           58 :     pty_close(s);
     340              : }
     341              : 
     342           58 : static void test_compose_empty_to_error(void) {
     343              :     /* TC-CD-03: Enter with empty To shows inline error */
     344           58 :     restart_imap();
     345           58 :     PtySession *s = tui_open_to_inbox();
     346           57 :     ASSERT(s != NULL, "empty to error: reached inbox");
     347              : 
     348           57 :     pty_send_str(s, "c");
     349           57 :     ASSERT_WAIT_FOR(s, "New Message", WAIT_MS);
     350           57 :     pty_settle(s, SETTLE_MS);
     351              : 
     352              :     /* Press Enter immediately without typing anything */
     353           57 :     pty_send_key(s, PTY_KEY_ENTER);
     354           57 :     pty_settle(s, SETTLE_MS);
     355              : 
     356              :     /* Inline error visible; dialog still open */
     357           57 :     ASSERT_SCREEN_CONTAINS(s, "empty");
     358           57 :     ASSERT_SCREEN_CONTAINS(s, "To:");
     359              : 
     360           57 :     pty_send_key(s, PTY_KEY_ESC);
     361           57 :     pty_close(s);
     362              : }
     363              : 
     364           57 : static void test_compose_empty_subject_warning(void) {
     365              :     /* TC-CD-05/06/07: empty Subject triggers confirmation prompt */
     366           57 :     restart_imap();
     367           57 :     PtySession *s = tui_open_to_inbox();
     368           56 :     ASSERT(s != NULL, "empty subject: reached inbox");
     369              : 
     370           56 :     pty_send_str(s, "c");
     371           56 :     ASSERT_WAIT_FOR(s, "New Message", WAIT_MS);
     372           56 :     pty_settle(s, SETTLE_MS);
     373              : 
     374              :     /* Fill only To */
     375           56 :     pty_send_str(s, "alice@example.com");
     376           56 :     pty_send_key(s, PTY_KEY_TAB);   /* To → Cc */
     377           56 :     pty_send_key(s, PTY_KEY_TAB);   /* Cc → Bcc */
     378           56 :     pty_send_key(s, PTY_KEY_TAB);   /* Bcc → Subject */
     379              :     /* Subject is empty; press Enter */
     380           56 :     pty_send_key(s, PTY_KEY_ENTER);
     381           56 :     pty_settle(s, SETTLE_MS);
     382              : 
     383              :     /* Confirmation prompt */
     384           56 :     ASSERT_SCREEN_CONTAINS(s, "empty");
     385           56 :     ASSERT_SCREEN_CONTAINS(s, "y/n");
     386              : 
     387              :     /* 'n' → back to Subject */
     388           56 :     pty_send_str(s, "n");
     389           56 :     pty_settle(s, SETTLE_MS);
     390           56 :     ASSERT_SCREEN_CONTAINS(s, "Subject:");
     391           56 :     ASSERT_SCREEN_NOT_CONTAINS(s, "y/n");
     392              : 
     393           56 :     pty_send_key(s, PTY_KEY_ESC);
     394           56 :     pty_close(s);
     395              : }
     396              : 
     397           56 : static void test_compose_empty_subject_y_opens_editor(void) {
     398              :     /* TC-CD-06: 'y' at the confirmation prompt opens editor */
     399           56 :     restart_imap(); restart_smtp();
     400           56 :     PtySession *s = tui_open_to_inbox();
     401           55 :     ASSERT(s != NULL, "empty subject y: reached inbox");
     402              : 
     403           55 :     pty_send_str(s, "c");
     404           55 :     ASSERT_WAIT_FOR(s, "New Message", WAIT_MS);
     405           55 :     pty_settle(s, SETTLE_MS);
     406              : 
     407           55 :     pty_send_str(s, "alice@example.com");
     408           55 :     pty_send_key(s, PTY_KEY_TAB);
     409           55 :     pty_send_key(s, PTY_KEY_TAB);
     410           55 :     pty_send_key(s, PTY_KEY_TAB);  /* reach Subject */
     411           55 :     pty_send_key(s, PTY_KEY_ENTER);
     412           55 :     pty_settle(s, SETTLE_MS);
     413           55 :     ASSERT_SCREEN_CONTAINS(s, "y/n");
     414              : 
     415           55 :     pty_send_str(s, "y");
     416              :     /* Editor (our fake script) runs and returns; TUI continues */
     417           55 :     pcr_send(s);
     418           55 :     ASSERT_WAIT_FOR(s, "Sending", WAIT_MS);
     419              : 
     420           55 :     pty_close(s);
     421              : }
     422              : 
     423           55 : static void test_compose_opens_editor_with_to(void) {
     424              :     /* TC-CD-04: filling To + Subject and pressing Enter opens editor */
     425           55 :     restart_imap(); restart_smtp();
     426           55 :     write_editor_script(); /* ensures EDITOR is set */
     427           55 :     PtySession *s = tui_open_to_inbox();
     428           54 :     ASSERT(s != NULL, "opens editor: reached inbox");
     429              : 
     430           54 :     pty_send_str(s, "c");
     431           54 :     ASSERT_WAIT_FOR(s, "New Message", WAIT_MS);
     432           54 :     pty_settle(s, SETTLE_MS);
     433              : 
     434           54 :     pty_send_str(s, "alice@example.com");
     435              :     /* Tab To → Cc → Bcc → Subject */
     436           54 :     pty_send_key(s, PTY_KEY_TAB);
     437           54 :     pty_send_key(s, PTY_KEY_TAB);
     438           54 :     pty_send_key(s, PTY_KEY_TAB);
     439           54 :     pty_send_str(s, "Test Subject");
     440           54 :     pty_send_key(s, PTY_KEY_ENTER);
     441              : 
     442              :     /* Editor runs and the TUI shows "Sending" */
     443           54 :     pcr_send(s);
     444           54 :     ASSERT_WAIT_FOR(s, "Sending", WAIT_MS);
     445              : 
     446           54 :     pty_close(s);
     447              : }
     448              : 
     449              : /* ══════════════════════════════════════════════════════════════════════
     450              :  *  TC-CD-09 to TC-CD-12: Field navigation
     451              :  * ══════════════════════════════════════════════════════════════════════ */
     452              : 
     453           54 : static void test_compose_tab_to_cc(void) {
     454              :     /* TC-CD-09: Tab from To field moves cursor to Cc field */
     455           54 :     restart_imap();
     456           54 :     PtySession *s = tui_open_to_inbox();
     457           53 :     ASSERT(s != NULL, "tab to cc: reached inbox");
     458              : 
     459           53 :     pty_send_str(s, "c");
     460           53 :     ASSERT_WAIT_FOR(s, "New Message", WAIT_MS);
     461           53 :     pty_settle(s, SETTLE_MS);
     462              : 
     463              :     /* Tab once — should highlight Cc */
     464           53 :     pty_send_key(s, PTY_KEY_TAB);
     465           53 :     pty_settle(s, SETTLE_MS);
     466              :     /* Cc field must be active (highlighted or indicated) */
     467           53 :     ASSERT_SCREEN_CONTAINS(s, "Cc:");
     468              : 
     469           53 :     pty_send_key(s, PTY_KEY_ESC);
     470           53 :     pty_close(s);
     471              : }
     472              : 
     473           53 : static void test_compose_tab_full_cycle(void) {
     474              :     /* TC-CD-10: Tab cycles To → Cc → Bcc → Subject → (back to To) */
     475           53 :     restart_imap();
     476           53 :     PtySession *s = tui_open_to_inbox();
     477           52 :     ASSERT(s != NULL, "tab cycle: reached inbox");
     478              : 
     479           52 :     pty_send_str(s, "c");
     480           52 :     ASSERT_WAIT_FOR(s, "New Message", WAIT_MS);
     481           52 :     pty_settle(s, SETTLE_MS);
     482              : 
     483              :     /* 4× Tab should wrap back past Subject to To; dialog still open */
     484          260 :     for (int i = 0; i < 4; i++) { pty_send_key(s, PTY_KEY_TAB); pty_settle(s, 80); }
     485           52 :     ASSERT_SCREEN_CONTAINS(s, "New Message"); /* dialog still showing */
     486              : 
     487           52 :     pty_send_key(s, PTY_KEY_ESC);
     488           52 :     pty_close(s);
     489              : }
     490              : 
     491           52 : static void test_compose_shift_tab_reverse(void) {
     492              :     /* TC-CD-11: Shift-Tab moves backwards (Subject → Bcc → Cc → To) */
     493           52 :     restart_imap();
     494           52 :     PtySession *s = tui_open_to_inbox();
     495           51 :     ASSERT(s != NULL, "shift-tab: reached inbox");
     496              : 
     497           51 :     pty_send_str(s, "c");
     498           51 :     ASSERT_WAIT_FOR(s, "New Message", WAIT_MS);
     499           51 :     pty_settle(s, SETTLE_MS);
     500              : 
     501              :     /* Move to Subject */
     502           51 :     pty_send_key(s, PTY_KEY_TAB);
     503           51 :     pty_send_key(s, PTY_KEY_TAB);
     504           51 :     pty_send_key(s, PTY_KEY_TAB);
     505           51 :     pty_settle(s, SETTLE_MS);
     506              : 
     507              :     /* Shift-Tab back */
     508           51 :     pty_send_str(s, "\x1b[Z");  /* Shift-Tab */
     509           51 :     pty_settle(s, SETTLE_MS);
     510           51 :     ASSERT_SCREEN_CONTAINS(s, "Bcc:");
     511              : 
     512           51 :     pty_send_str(s, "\x1b[Z");  /* Shift-Tab */
     513           51 :     pty_settle(s, SETTLE_MS);
     514           51 :     ASSERT_SCREEN_CONTAINS(s, "Cc:");
     515              : 
     516           51 :     pty_send_str(s, "\x1b[Z");  /* Shift-Tab */
     517           51 :     pty_settle(s, SETTLE_MS);
     518           51 :     ASSERT_SCREEN_CONTAINS(s, "To:");
     519              : 
     520           51 :     pty_send_key(s, PTY_KEY_ESC);
     521           51 :     pty_close(s);
     522              : }
     523              : 
     524              : /* ══════════════════════════════════════════════════════════════════════
     525              :  *  TC-CD-13 to TC-CD-17: Recipient input and multi-address
     526              :  * ══════════════════════════════════════════════════════════════════════ */
     527              : 
     528           51 : static void test_compose_to_field_accepts_email(void) {
     529              :     /* TC-CD-13: can type a complete email address in To */
     530           51 :     restart_imap();
     531           51 :     PtySession *s = tui_open_to_inbox();
     532           50 :     ASSERT(s != NULL, "to accepts email: reached inbox");
     533              : 
     534           50 :     pty_send_str(s, "c");
     535           50 :     ASSERT_WAIT_FOR(s, "New Message", WAIT_MS);
     536           50 :     pty_settle(s, SETTLE_MS);
     537              : 
     538           50 :     pty_send_str(s, "alice@example.com");
     539           50 :     pty_settle(s, SETTLE_MS);
     540           50 :     ASSERT_SCREEN_CONTAINS(s, "alice@example.com");
     541              : 
     542           50 :     pty_send_key(s, PTY_KEY_ESC);
     543           50 :     pty_close(s);
     544              : }
     545              : 
     546           50 : static void test_compose_multiple_to_addresses(void) {
     547              :     /* TC-CD-14: semicolon commits first address; both appear in field */
     548           50 :     restart_imap();
     549           50 :     PtySession *s = tui_open_to_inbox();
     550           49 :     ASSERT(s != NULL, "multi-to: reached inbox");
     551              : 
     552           49 :     pty_send_str(s, "c");
     553           49 :     ASSERT_WAIT_FOR(s, "New Message", WAIT_MS);
     554           49 :     pty_settle(s, SETTLE_MS);
     555              : 
     556           49 :     pty_send_str(s, "alice@example.com;");
     557           49 :     pty_settle(s, SETTLE_MS);
     558           49 :     pty_send_str(s, "bob@example.com");
     559           49 :     pty_settle(s, SETTLE_MS);
     560              : 
     561           49 :     ASSERT_SCREEN_CONTAINS(s, "alice@example.com");
     562           49 :     ASSERT_SCREEN_CONTAINS(s, "bob@example.com");
     563              : 
     564           49 :     pty_send_key(s, PTY_KEY_ESC);
     565           49 :     pty_close(s);
     566              : }
     567              : 
     568           49 : static void test_compose_backspace_removes_address(void) {
     569              :     /* TC-CD-15: Backspace on empty token removes last committed address */
     570           49 :     restart_imap();
     571           49 :     PtySession *s = tui_open_to_inbox();
     572           48 :     ASSERT(s != NULL, "backspace removes: reached inbox");
     573              : 
     574           48 :     pty_send_str(s, "c");
     575           48 :     ASSERT_WAIT_FOR(s, "New Message", WAIT_MS);
     576           48 :     pty_settle(s, SETTLE_MS);
     577              : 
     578           48 :     pty_send_str(s, "alice@example.com;");
     579           48 :     pty_settle(s, SETTLE_MS);
     580           48 :     ASSERT_SCREEN_CONTAINS(s, "alice@example.com");
     581              : 
     582              :     /* Token is empty now; Backspace should remove alice's address */
     583           48 :     pty_send_key(s, PTY_KEY_BACK);
     584           48 :     pty_settle(s, SETTLE_MS);
     585           48 :     ASSERT_SCREEN_NOT_CONTAINS(s, "alice@example.com");
     586              : 
     587           48 :     pty_send_key(s, PTY_KEY_ESC);
     588           48 :     pty_close(s);
     589              : }
     590              : 
     591           48 : static void test_compose_cc_written_to_tempfile(void) {
     592              :     /* TC-CD-16: Cc address appears as Cc: header in the editor temp file */
     593           48 :     restart_imap(); restart_smtp();
     594           48 :     write_editor_script();
     595           48 :     PtySession *s = tui_open_to_inbox();
     596           47 :     ASSERT(s != NULL, "cc header: reached inbox");
     597              : 
     598           47 :     pty_send_str(s, "c");
     599           47 :     ASSERT_WAIT_FOR(s, "New Message", WAIT_MS);
     600           47 :     pty_settle(s, SETTLE_MS);
     601              : 
     602           47 :     pty_send_str(s, "alice@example.com");
     603           47 :     pty_send_key(s, PTY_KEY_TAB);   /* → Cc */
     604           47 :     pty_send_str(s, "carol@example.org");
     605           47 :     pty_send_key(s, PTY_KEY_TAB);   /* → Bcc */
     606           47 :     pty_send_key(s, PTY_KEY_TAB);   /* → Subject */
     607           47 :     pty_send_str(s, "Cc test");
     608           47 :     pty_send_key(s, PTY_KEY_ENTER);
     609              : 
     610              :     /* After editor runs, message is sent; the Cc: header must have been
     611              :      * in the temp file and picked up by the send flow. */
     612           47 :     pcr_send(s);
     613           47 :     ASSERT_WAIT_FOR(s, "Sending", WAIT_MS);
     614              : 
     615           47 :     pty_close(s);
     616              : }
     617              : 
     618           47 : static void test_compose_bcc_written_to_tempfile(void) {
     619              :     /* TC-CD-17: Bcc address appears in Bcc: header in the editor temp file */
     620           47 :     restart_imap(); restart_smtp();
     621           47 :     write_editor_script();
     622           47 :     PtySession *s = tui_open_to_inbox();
     623           46 :     ASSERT(s != NULL, "bcc header: reached inbox");
     624              : 
     625           46 :     pty_send_str(s, "c");
     626           46 :     ASSERT_WAIT_FOR(s, "New Message", WAIT_MS);
     627           46 :     pty_settle(s, SETTLE_MS);
     628              : 
     629           46 :     pty_send_str(s, "alice@example.com");
     630           46 :     pty_send_key(s, PTY_KEY_TAB);   /* → Cc */
     631           46 :     pty_send_key(s, PTY_KEY_TAB);   /* → Bcc */
     632           46 :     pty_send_str(s, "hidden@secret.org");
     633           46 :     pty_send_key(s, PTY_KEY_TAB);   /* → Subject */
     634           46 :     pty_send_str(s, "Bcc test");
     635           46 :     pty_send_key(s, PTY_KEY_ENTER);
     636              : 
     637           46 :     pcr_send(s);
     638           46 :     ASSERT_WAIT_FOR(s, "Sending", WAIT_MS);
     639              : 
     640           46 :     pty_close(s);
     641              : }
     642              : 
     643              : /* ══════════════════════════════════════════════════════════════════════
     644              :  *  TC-CD-18 to TC-CD-27: Autocomplete
     645              :  * ══════════════════════════════════════════════════════════════════════ */
     646              : 
     647           46 : static void test_autocomplete_dropdown_appears(void) {
     648              :     /* TC-CD-18: typing ≥2 matching chars opens autocomplete dropdown */
     649           46 :     restart_imap();
     650           46 :     write_contacts_tsv();
     651           46 :     PtySession *s = tui_open_to_inbox();
     652           45 :     ASSERT(s != NULL, "autocomplete appears: reached inbox");
     653              : 
     654           45 :     pty_send_str(s, "c");
     655           45 :     ASSERT_WAIT_FOR(s, "New Message", WAIT_MS);
     656           45 :     pty_settle(s, SETTLE_MS);
     657              : 
     658           45 :     pty_send_str(s, "al");  /* matches alice@example.com */
     659           45 :     ASSERT_WAIT_FOR(s, "alice@example.com", WAIT_MS);
     660              : 
     661           45 :     pty_send_key(s, PTY_KEY_ESC);
     662           45 :     pty_close(s);
     663              : }
     664              : 
     665           45 : static void test_autocomplete_filters_realtime(void) {
     666              :     /* TC-CD-19: typing more chars narrows the dropdown */
     667           45 :     restart_imap();
     668           45 :     write_contacts_tsv();
     669           45 :     PtySession *s = tui_open_to_inbox();
     670           44 :     ASSERT(s != NULL, "autocomplete filters: reached inbox");
     671              : 
     672           44 :     pty_send_str(s, "c");
     673           44 :     ASSERT_WAIT_FOR(s, "New Message", WAIT_MS);
     674           44 :     pty_settle(s, SETTLE_MS);
     675              : 
     676           44 :     pty_send_str(s, "jo");  /* matches john.doe and johnathan */
     677           44 :     ASSERT_WAIT_FOR(s, "john", WAIT_MS);
     678           44 :     pty_settle(s, SETTLE_MS);
     679           44 :     ASSERT_SCREEN_CONTAINS(s, "john.doe@example.com");
     680           44 :     ASSERT_SCREEN_CONTAINS(s, "johnathan");
     681              : 
     682              :     /* Add 'hn.d' → only john.doe matches */
     683           44 :     pty_send_str(s, "hn.d");
     684           44 :     pty_settle(s, SETTLE_MS);
     685           44 :     ASSERT_SCREEN_CONTAINS(s, "john.doe@example.com");
     686           44 :     ASSERT_SCREEN_NOT_CONTAINS(s, "johnathan");
     687              : 
     688           44 :     pty_send_key(s, PTY_KEY_ESC);
     689           44 :     pty_close(s);
     690              : }
     691              : 
     692           44 : static void test_autocomplete_no_match_no_dropdown(void) {
     693              :     /* TC-CD-20: no matching contacts → no dropdown shown */
     694           44 :     restart_imap();
     695           44 :     write_contacts_tsv();
     696           44 :     PtySession *s = tui_open_to_inbox();
     697           43 :     ASSERT(s != NULL, "no dropdown: reached inbox");
     698              : 
     699           43 :     pty_send_str(s, "c");
     700           43 :     ASSERT_WAIT_FOR(s, "New Message", WAIT_MS);
     701           43 :     pty_settle(s, SETTLE_MS);
     702              : 
     703           43 :     pty_send_str(s, "zzz");  /* no contact starts with zzz */
     704           43 :     pty_settle(s, SETTLE_MS * 2);
     705           43 :     ASSERT_SCREEN_NOT_CONTAINS(s, "zzz@");   /* no suggestion row */
     706              : 
     707           43 :     pty_send_key(s, PTY_KEY_ESC);
     708           43 :     pty_close(s);
     709              : }
     710              : 
     711           43 : static void test_autocomplete_single_char_no_dropdown(void) {
     712              :     /* TC-CD-21: only 1 char typed → no dropdown yet (threshold is 2) */
     713           43 :     restart_imap();
     714           43 :     write_contacts_tsv();
     715           43 :     PtySession *s = tui_open_to_inbox();
     716           42 :     ASSERT(s != NULL, "single char: reached inbox");
     717              : 
     718           42 :     pty_send_str(s, "c");
     719           42 :     ASSERT_WAIT_FOR(s, "New Message", WAIT_MS);
     720           42 :     pty_settle(s, SETTLE_MS);
     721              : 
     722           42 :     pty_send_str(s, "a");  /* single char: should not open dropdown */
     723           42 :     pty_settle(s, SETTLE_MS);
     724              :     /* Dropdown NOT visible (alice would appear but threshold not met) */
     725           42 :     ASSERT_SCREEN_NOT_CONTAINS(s, "alice@example.com");
     726              : 
     727           42 :     pty_send_key(s, PTY_KEY_ESC);
     728           42 :     pty_close(s);
     729              : }
     730              : 
     731           42 : static void test_autocomplete_down_highlights_second(void) {
     732              :     /* TC-CD-22: Down arrow highlights second entry in dropdown */
     733           42 :     restart_imap();
     734           42 :     write_contacts_tsv();
     735           42 :     PtySession *s = tui_open_to_inbox();
     736           41 :     ASSERT(s != NULL, "down arrow: reached inbox");
     737              : 
     738           41 :     pty_send_str(s, "c");
     739           41 :     ASSERT_WAIT_FOR(s, "New Message", WAIT_MS);
     740           41 :     pty_settle(s, SETTLE_MS);
     741              : 
     742           41 :     pty_send_str(s, "jo");
     743           41 :     ASSERT_WAIT_FOR(s, "john.doe", WAIT_MS);
     744           41 :     pty_settle(s, SETTLE_MS);
     745              : 
     746              :     /* Dropdown open; press Down */
     747           41 :     pty_send_key(s, PTY_KEY_DOWN);
     748           41 :     pty_settle(s, SETTLE_MS);
     749              :     /* Second entry (johnathan) must now be highlighted */
     750           41 :     ASSERT_SCREEN_CONTAINS(s, "johnathan");
     751              : 
     752           41 :     pty_send_key(s, PTY_KEY_ESC);
     753           41 :     pty_close(s);
     754              : }
     755              : 
     756           41 : static void test_autocomplete_enter_selects(void) {
     757              :     /* TC-CD-23: Enter on dropdown entry appends address and closes dropdown */
     758           41 :     restart_imap();
     759           41 :     write_contacts_tsv();
     760           41 :     PtySession *s = tui_open_to_inbox();
     761           40 :     ASSERT(s != NULL, "enter selects: reached inbox");
     762              : 
     763           40 :     pty_send_str(s, "c");
     764           40 :     ASSERT_WAIT_FOR(s, "New Message", WAIT_MS);
     765           40 :     pty_settle(s, SETTLE_MS);
     766              : 
     767           40 :     pty_send_str(s, "al");
     768           40 :     ASSERT_WAIT_FOR(s, "alice@example.com", WAIT_MS);
     769           40 :     pty_settle(s, SETTLE_MS);
     770              : 
     771           40 :     pty_send_key(s, PTY_KEY_ENTER);
     772           40 :     pty_settle(s, SETTLE_MS);
     773              : 
     774              :     /* Address committed; dropdown closed; To field contains alice */
     775           40 :     ASSERT_SCREEN_CONTAINS(s, "alice@example.com");
     776              :     /* Dropdown suggestions should be gone */
     777           40 :     ASSERT_SCREEN_NOT_CONTAINS(s, "john.doe@example.com");
     778              : 
     779           40 :     pty_send_key(s, PTY_KEY_ESC);
     780           40 :     pty_close(s);
     781              : }
     782              : 
     783           40 : static void test_autocomplete_tab_selects_and_advances(void) {
     784              :     /* TC-CD-24: Tab selects top suggestion and advances to next field */
     785           40 :     restart_imap();
     786           40 :     write_contacts_tsv();
     787           40 :     PtySession *s = tui_open_to_inbox();
     788           39 :     ASSERT(s != NULL, "tab selects: reached inbox");
     789              : 
     790           39 :     pty_send_str(s, "c");
     791           39 :     ASSERT_WAIT_FOR(s, "New Message", WAIT_MS);
     792           39 :     pty_settle(s, SETTLE_MS);
     793              : 
     794           39 :     pty_send_str(s, "al");
     795           39 :     ASSERT_WAIT_FOR(s, "alice@example.com", WAIT_MS);
     796           39 :     pty_settle(s, SETTLE_MS);
     797              : 
     798              :     /* Tab: select top entry, advance to Cc */
     799           39 :     pty_send_key(s, PTY_KEY_TAB);
     800           39 :     pty_settle(s, SETTLE_MS);
     801              : 
     802           39 :     ASSERT_SCREEN_CONTAINS(s, "alice@example.com");
     803              :     /* Now in Cc field; Cc should be the active field */
     804           39 :     ASSERT_SCREEN_CONTAINS(s, "Cc:");
     805              : 
     806           39 :     pty_send_key(s, PTY_KEY_ESC);
     807           39 :     pty_close(s);
     808              : }
     809              : 
     810           39 : static void test_autocomplete_esc_dismisses_keeps_text(void) {
     811              :     /* TC-CD-25: Esc dismisses dropdown without selecting; typed text remains */
     812           39 :     restart_imap();
     813           39 :     write_contacts_tsv();
     814           39 :     PtySession *s = tui_open_to_inbox();
     815           38 :     ASSERT(s != NULL, "esc dismisses: reached inbox");
     816              : 
     817           38 :     pty_send_str(s, "c");
     818           38 :     ASSERT_WAIT_FOR(s, "New Message", WAIT_MS);
     819           38 :     pty_settle(s, SETTLE_MS);
     820              : 
     821           38 :     pty_send_str(s, "al");
     822           38 :     ASSERT_WAIT_FOR(s, "alice@example.com", WAIT_MS);
     823           38 :     pty_settle(s, SETTLE_MS);
     824              : 
     825              :     /* Esc: dismiss dropdown */
     826           38 :     pty_send_key(s, PTY_KEY_ESC);
     827           38 :     pty_settle(s, SETTLE_MS);
     828              : 
     829              :     /* Dropdown gone but the 'al' text must still be in the To field */
     830           38 :     ASSERT_SCREEN_NOT_CONTAINS(s, "alice@example.com"); /* dropdown row gone */
     831           38 :     ASSERT_SCREEN_CONTAINS(s, "al");                     /* typed text stays */
     832              : 
     833           38 :     pty_send_key(s, PTY_KEY_ESC);  /* cancel dialog */
     834           38 :     pty_close(s);
     835              : }
     836              : 
     837           38 : static void test_autocomplete_case_insensitive(void) {
     838              :     /* TC-CD-26: uppercase input matches lowercase contact */
     839           38 :     restart_imap();
     840           38 :     write_contacts_tsv();
     841           38 :     PtySession *s = tui_open_to_inbox();
     842           37 :     ASSERT(s != NULL, "case insensitive: reached inbox");
     843              : 
     844           37 :     pty_send_str(s, "c");
     845           37 :     ASSERT_WAIT_FOR(s, "New Message", WAIT_MS);
     846           37 :     pty_settle(s, SETTLE_MS);
     847              : 
     848           37 :     pty_send_str(s, "AL");  /* uppercase, but alice@example.com should match */
     849           37 :     ASSERT_WAIT_FOR(s, "alice@example.com", WAIT_MS);
     850              : 
     851           37 :     pty_send_key(s, PTY_KEY_ESC);
     852           37 :     pty_close(s);
     853              : }
     854              : 
     855           37 : static void test_autocomplete_display_name_match(void) {
     856              :     /* TC-CD-27: matching on display name also works ("Smith" → bob.smith@) */
     857           37 :     restart_imap();
     858           37 :     write_contacts_tsv();
     859           37 :     PtySession *s = tui_open_to_inbox();
     860           36 :     ASSERT(s != NULL, "name match: reached inbox");
     861              : 
     862           36 :     pty_send_str(s, "c");
     863           36 :     ASSERT_WAIT_FOR(s, "New Message", WAIT_MS);
     864           36 :     pty_settle(s, SETTLE_MS);
     865              : 
     866           36 :     pty_send_str(s, "Sm");  /* matches "Bob Smith" display name */
     867           36 :     ASSERT_WAIT_FOR(s, "bob.smith@acme.com", WAIT_MS);
     868              : 
     869           36 :     pty_send_key(s, PTY_KEY_ESC);
     870           36 :     pty_close(s);
     871              : }
     872              : 
     873              : /* ══════════════════════════════════════════════════════════════════════
     874              :  *  TC-CD-28 to TC-CD-36: Reply dialog
     875              :  * ══════════════════════════════════════════════════════════════════════ */
     876              : 
     877           36 : static void test_reply_dialog_opens(void) {
     878              :     /* TC-CD-28: pressing 'r' opens the reply dialog */
     879           36 :     restart_imap();
     880           36 :     PtySession *s = tui_open_to_inbox();
     881           35 :     ASSERT(s != NULL, "reply opens: reached inbox");
     882              : 
     883           35 :     pty_send_str(s, "r");
     884           35 :     ASSERT_WAIT_FOR(s, "Reply", WAIT_MS);
     885           35 :     pty_settle(s, SETTLE_MS);
     886              : 
     887           35 :     ASSERT_SCREEN_CONTAINS(s, "To:");
     888           35 :     ASSERT_SCREEN_CONTAINS(s, "Subject:");
     889              : 
     890           35 :     pty_send_key(s, PTY_KEY_ESC);
     891           35 :     pty_close(s);
     892              : }
     893              : 
     894           35 : static void test_reply_dialog_title(void) {
     895              :     /* TC-CD-28: dialog title reads "Reply" */
     896           35 :     restart_imap();
     897           35 :     PtySession *s = tui_open_to_inbox();
     898           34 :     ASSERT(s != NULL, "reply title: reached inbox");
     899              : 
     900           34 :     pty_send_str(s, "r");
     901           34 :     ASSERT_WAIT_FOR(s, "Reply", WAIT_MS);
     902           34 :     ASSERT_SCREEN_CONTAINS(s, "Reply");
     903              : 
     904           34 :     pty_send_key(s, PTY_KEY_ESC);
     905           34 :     pty_close(s);
     906              : }
     907              : 
     908           34 : static void test_reply_to_prefilled(void) {
     909              :     /* TC-CD-29: To field shows the original sender's address */
     910           34 :     restart_imap();
     911              :     /* The mock server's first message is from testuser1@example.com */
     912           34 :     PtySession *s = tui_open_to_inbox();
     913           33 :     ASSERT(s != NULL, "reply to: reached inbox");
     914              : 
     915              :     /* Navigate to first message, then reply */
     916           33 :     pty_send_str(s, "r");
     917           33 :     ASSERT_WAIT_FOR(s, "Reply", WAIT_MS);
     918           33 :     pty_settle(s, SETTLE_MS);
     919              : 
     920              :     /* To field must contain sender's address */
     921           33 :     ASSERT_SCREEN_CONTAINS(s, "@");  /* some email address in To */
     922              : 
     923           33 :     pty_send_key(s, PTY_KEY_ESC);
     924           33 :     pty_close(s);
     925              : }
     926              : 
     927           33 : static void test_reply_subject_prefilled(void) {
     928              :     /* TC-CD-30: Subject shows "Re: <original subject>" */
     929           33 :     restart_imap();
     930           33 :     PtySession *s = tui_open_to_inbox();
     931           32 :     ASSERT(s != NULL, "reply subject: reached inbox");
     932              : 
     933           32 :     pty_send_str(s, "r");
     934           32 :     ASSERT_WAIT_FOR(s, "Reply", WAIT_MS);
     935           32 :     pty_settle(s, SETTLE_MS);
     936              : 
     937           32 :     ASSERT_SCREEN_CONTAINS(s, "Re:");
     938              : 
     939           32 :     pty_send_key(s, PTY_KEY_ESC);
     940           32 :     pty_close(s);
     941              : }
     942              : 
     943           32 : static void test_reply_re_not_doubled(void) {
     944              :     /* TC-CD-32: "Re: Re: X" collapsed to "Re: X" */
     945              :     /* This requires a message whose subject already starts with "Re: ";
     946              :      * the mock server's "Test Message" should be varied for this.
     947              :      * We test the collapse logic via compose_extract_reply_meta unit test;
     948              :      * here we just verify no double-Re in the dialog display. */
     949           32 :     restart_imap();
     950           32 :     PtySession *s = tui_open_to_inbox();
     951           31 :     ASSERT(s != NULL, "re not doubled: reached inbox");
     952              : 
     953           31 :     pty_send_str(s, "r");
     954           31 :     ASSERT_WAIT_FOR(s, "Reply", WAIT_MS);
     955           31 :     pty_settle(s, SETTLE_MS);
     956              : 
     957              :     /* Must contain exactly one "Re:" — not "Re: Re:" */
     958           31 :     ASSERT_SCREEN_NOT_CONTAINS(s, "Re: Re:");
     959              : 
     960           31 :     pty_send_key(s, PTY_KEY_ESC);
     961           31 :     pty_close(s);
     962              : }
     963              : 
     964           31 : static void test_reply_esc_cancels(void) {
     965              :     /* TC-CD-34: Esc in reply dialog returns to message list */
     966           31 :     restart_imap();
     967           31 :     PtySession *s = tui_open_to_inbox();
     968           30 :     ASSERT(s != NULL, "reply esc: reached inbox");
     969              : 
     970           30 :     pty_send_str(s, "r");
     971           30 :     ASSERT_WAIT_FOR(s, "Reply", WAIT_MS);
     972           30 :     pty_settle(s, SETTLE_MS);
     973              : 
     974           30 :     pty_send_key(s, PTY_KEY_ESC);
     975           30 :     ASSERT_WAIT_FOR(s, "[Press any key", WAIT_MS);
     976           30 :     pty_send_str(s, " ");
     977           30 :     ASSERT_WAIT_FOR(s, "message(s) in", WAIT_MS);
     978              : 
     979           30 :     pty_send_key(s, PTY_KEY_ESC);
     980           30 :     pty_close(s);
     981              : }
     982              : 
     983           30 : static void test_reply_can_add_cc(void) {
     984              :     /* TC-CD-33: can add Cc before opening editor */
     985           30 :     restart_imap(); restart_smtp();
     986           30 :     write_editor_script();
     987           30 :     PtySession *s = tui_open_to_inbox();
     988           29 :     ASSERT(s != NULL, "reply cc: reached inbox");
     989              : 
     990           29 :     pty_send_str(s, "r");
     991           29 :     ASSERT_WAIT_FOR(s, "Reply", WAIT_MS);
     992           29 :     pty_settle(s, SETTLE_MS);
     993              : 
     994              :     /* Reply dialog starts at Cc (To is pre-filled with sender address).
     995              :      * Type Cc address directly, then Tab to Subject. */
     996           29 :     pty_send_str(s, "carol@example.org");
     997           29 :     pty_settle(s, SETTLE_MS);
     998           29 :     ASSERT_SCREEN_CONTAINS(s, "carol@example.org");
     999              : 
    1000              :     /* Move to Subject and confirm: Cc→Bcc→Subject = 2× Tab */
    1001           29 :     pty_send_key(s, PTY_KEY_TAB);   /* Cc → Bcc */
    1002           29 :     pty_send_key(s, PTY_KEY_TAB);   /* Bcc → Subject */
    1003           29 :     pty_send_key(s, PTY_KEY_ENTER);
    1004              : 
    1005           29 :     pcr_send(s);
    1006           29 :     ASSERT_WAIT_FOR(s, "Sending", WAIT_MS);
    1007              : 
    1008           29 :     pty_close(s);
    1009              : }
    1010              : 
    1011           29 : static void test_reply_editor_opens_with_quoted_body(void) {
    1012              :     /* TC-CD-35/36: editor opens; quoted body prefixed with "> " is present */
    1013           29 :     restart_imap(); restart_smtp();
    1014              : 
    1015              :     /* Use a custom editor script that writes the draft content to a file */
    1016              :     char draft_capture[600];
    1017           29 :     snprintf(draft_capture, sizeof(draft_capture),
    1018              :              "%s/.draft_capture", g_test_home);
    1019              :     char editor_script[600];
    1020           29 :     snprintf(editor_script, sizeof(editor_script),
    1021              :              "%s/.capture_editor.sh", g_test_home);
    1022           29 :     FILE *f = fopen(editor_script, "w");
    1023           29 :     if (f) {
    1024           29 :         fprintf(f, "#!/bin/sh\ncp \"$1\" '%s'\necho 'Test body' >> \"$1\"\n",
    1025              :                 draft_capture);
    1026           29 :         fclose(f);
    1027           29 :         chmod(editor_script, 0755);
    1028           29 :         setenv("EDITOR", editor_script, 1);
    1029              :     }
    1030              : 
    1031           29 :     PtySession *s = tui_open_to_inbox();
    1032           28 :     ASSERT(s != NULL, "reply body: reached inbox");
    1033              : 
    1034           28 :     pty_send_str(s, "r");
    1035           28 :     ASSERT_WAIT_FOR(s, "Reply", WAIT_MS);
    1036           28 :     pty_settle(s, SETTLE_MS);
    1037              : 
    1038              :     /* Reply dialog starts at Cc (To pre-filled): Cc→Bcc→Subject = 2× Tab */
    1039           28 :     pty_send_key(s, PTY_KEY_TAB);   /* Cc → Bcc */
    1040           28 :     pty_send_key(s, PTY_KEY_TAB);   /* Bcc → Subject */
    1041           28 :     pty_send_key(s, PTY_KEY_ENTER);
    1042           28 :     pcr_send(s);
    1043           28 :     ASSERT_WAIT_FOR(s, "Sending", WAIT_MS);
    1044           28 :     pty_close(s);
    1045              : 
    1046              :     /* Inspect captured draft file for ">" quote prefix */
    1047           28 :     FILE *cap = fopen(draft_capture, "r");
    1048           28 :     ASSERT(cap != NULL, "reply body: draft captured");
    1049           28 :     if (cap) {
    1050           28 :         char buf[4096] = "";
    1051           28 :         buf[fread(buf, 1, sizeof(buf) - 1, cap)] = '\0';
    1052           28 :         fclose(cap);
    1053           28 :         ASSERT(strstr(buf, "> ") != NULL, "reply body: quoted lines with '> '");
    1054              :     }
    1055              : 
    1056           28 :     write_editor_script(); /* restore normal fake editor */
    1057              : }
    1058              : 
    1059              : /* ══════════════════════════════════════════════════════════════════════
    1060              :  *  TC-CD-37 to TC-CD-43: Reply-All dialog (A key)
    1061              :  * ══════════════════════════════════════════════════════════════════════ */
    1062              : 
    1063           28 : static void test_reply_all_dialog_opens(void) {
    1064              :     /* TC-CD-37: capital 'A' opens reply-all dialog */
    1065           28 :     restart_imap();
    1066           28 :     PtySession *s = tui_open_to_inbox();
    1067           27 :     ASSERT(s != NULL, "reply-all opens: reached inbox");
    1068              : 
    1069           27 :     pty_send_str(s, "A");
    1070           27 :     ASSERT_WAIT_FOR(s, "Reply All", WAIT_MS);
    1071           27 :     pty_settle(s, SETTLE_MS);
    1072              : 
    1073           27 :     ASSERT_SCREEN_CONTAINS(s, "To:");
    1074           27 :     ASSERT_SCREEN_CONTAINS(s, "Cc:");
    1075              : 
    1076           27 :     pty_send_key(s, PTY_KEY_ESC);
    1077           27 :     pty_close(s);
    1078              : }
    1079              : 
    1080           27 : static void test_reply_all_title(void) {
    1081              :     /* TC-CD-38: dialog title reads "Reply All" */
    1082           27 :     restart_imap();
    1083           27 :     PtySession *s = tui_open_to_inbox();
    1084           26 :     ASSERT(s != NULL, "reply-all title: reached inbox");
    1085              : 
    1086           26 :     pty_send_str(s, "A");
    1087           26 :     ASSERT_WAIT_FOR(s, "Reply All", WAIT_MS);
    1088           26 :     ASSERT_SCREEN_CONTAINS(s, "Reply All");
    1089              : 
    1090           26 :     pty_send_key(s, PTY_KEY_ESC);
    1091           26 :     pty_close(s);
    1092              : }
    1093              : 
    1094           26 : static void test_reply_all_own_address_excluded_from_cc(void) {
    1095              :     /* TC-CD-41: the user's own address must NOT appear in the Cc field */
    1096           26 :     restart_imap();
    1097           26 :     PtySession *s = tui_open_to_inbox();
    1098           25 :     ASSERT(s != NULL, "reply-all own excluded: reached inbox");
    1099              : 
    1100           25 :     pty_send_str(s, "A");
    1101           25 :     ASSERT_WAIT_FOR(s, "Reply All", WAIT_MS);
    1102           25 :     pty_settle(s, SETTLE_MS);
    1103              : 
    1104              :     /* testuser@example.com must not appear in the Cc field */
    1105           25 :     ASSERT_SCREEN_NOT_CONTAINS(s, "testuser@example.com");
    1106              : 
    1107           25 :     pty_send_key(s, PTY_KEY_ESC);
    1108           25 :     pty_close(s);
    1109              : }
    1110              : 
    1111           25 : static void test_reply_all_esc_cancels(void) {
    1112              :     /* TC-CD-42: Esc cancels reply-all */
    1113           25 :     restart_imap();
    1114           25 :     PtySession *s = tui_open_to_inbox();
    1115           24 :     ASSERT(s != NULL, "reply-all esc: reached inbox");
    1116              : 
    1117           24 :     pty_send_str(s, "A");
    1118           24 :     ASSERT_WAIT_FOR(s, "Reply All", WAIT_MS);
    1119              : 
    1120           24 :     pty_send_key(s, PTY_KEY_ESC);
    1121           24 :     ASSERT_WAIT_FOR(s, "[Press any key", WAIT_MS);
    1122           24 :     pty_send_str(s, " ");
    1123           24 :     ASSERT_WAIT_FOR(s, "message(s) in", WAIT_MS);
    1124              : 
    1125           24 :     pty_send_key(s, PTY_KEY_ESC);
    1126           24 :     pty_close(s);
    1127              : }
    1128              : 
    1129           24 : static void test_reply_all_in_help_panel(void) {
    1130              :     /* TC-CD-43: 'A' appears in message list help popup */
    1131           24 :     restart_imap();
    1132           24 :     PtySession *s = tui_open_to_inbox();
    1133           23 :     ASSERT(s != NULL, "reply-all help: reached inbox");
    1134              : 
    1135           23 :     pty_send_str(s, "h");
    1136           23 :     ASSERT_WAIT_FOR(s, "Reply-all", WAIT_MS);
    1137           23 :     pty_settle(s, SETTLE_MS);
    1138           23 :     ASSERT_SCREEN_CONTAINS(s, "A");
    1139              : 
    1140           23 :     pty_send_str(s, "q");
    1141           23 :     pty_close(s);
    1142              : }
    1143              : 
    1144              : /* ══════════════════════════════════════════════════════════════════════
    1145              :  *  TC-CD-44 to TC-CD-53: Forward dialog (F key)
    1146              :  * ══════════════════════════════════════════════════════════════════════ */
    1147              : 
    1148           23 : static void test_forward_dialog_opens(void) {
    1149              :     /* TC-CD-44: capital 'F' opens forward dialog */
    1150           23 :     restart_imap();
    1151           23 :     PtySession *s = tui_open_to_inbox();
    1152           22 :     ASSERT(s != NULL, "forward opens: reached inbox");
    1153              : 
    1154           22 :     pty_send_str(s, "F");
    1155           22 :     ASSERT_WAIT_FOR(s, "Forward", WAIT_MS);
    1156           22 :     pty_settle(s, SETTLE_MS);
    1157              : 
    1158           22 :     ASSERT_SCREEN_CONTAINS(s, "To:");
    1159           22 :     ASSERT_SCREEN_CONTAINS(s, "Subject:");
    1160              : 
    1161           22 :     pty_send_key(s, PTY_KEY_ESC);
    1162           22 :     pty_close(s);
    1163              : }
    1164              : 
    1165           22 : static void test_forward_dialog_title(void) {
    1166              :     /* TC-CD-45: title reads "Forward" */
    1167           22 :     restart_imap();
    1168           22 :     PtySession *s = tui_open_to_inbox();
    1169           21 :     ASSERT(s != NULL, "forward title: reached inbox");
    1170              : 
    1171           21 :     pty_send_str(s, "F");
    1172           21 :     ASSERT_WAIT_FOR(s, "Forward", WAIT_MS);
    1173           21 :     ASSERT_SCREEN_CONTAINS(s, "Forward");
    1174              : 
    1175           21 :     pty_send_key(s, PTY_KEY_ESC);
    1176           21 :     pty_close(s);
    1177              : }
    1178              : 
    1179           21 : static void test_forward_to_field_empty(void) {
    1180              :     /* TC-CD-46: To field is empty in the forward dialog */
    1181           21 :     restart_imap();
    1182           21 :     PtySession *s = tui_open_to_inbox();
    1183           20 :     ASSERT(s != NULL, "forward to empty: reached inbox");
    1184              : 
    1185           20 :     pty_send_str(s, "F");
    1186           20 :     ASSERT_WAIT_FOR(s, "Forward", WAIT_MS);
    1187           20 :     pty_settle(s, SETTLE_MS);
    1188              : 
    1189              :     /* The To field should not be pre-filled with anything */
    1190              :     /* We check by pressing Enter immediately — the empty-To error must fire */
    1191           20 :     pty_send_key(s, PTY_KEY_ENTER);
    1192           20 :     pty_settle(s, SETTLE_MS);
    1193           20 :     ASSERT_SCREEN_CONTAINS(s, "empty");
    1194              : 
    1195           20 :     pty_send_key(s, PTY_KEY_ESC);
    1196           20 :     pty_close(s);
    1197              : }
    1198              : 
    1199           20 : static void test_forward_subject_prefilled_fwd(void) {
    1200              :     /* TC-CD-47: Subject pre-filled as "Fwd: <original>" */
    1201           20 :     restart_imap();
    1202           20 :     PtySession *s = tui_open_to_inbox();
    1203           19 :     ASSERT(s != NULL, "forward subject: reached inbox");
    1204              : 
    1205           19 :     pty_send_str(s, "F");
    1206           19 :     ASSERT_WAIT_FOR(s, "Forward", WAIT_MS);
    1207           19 :     pty_settle(s, SETTLE_MS);
    1208              : 
    1209           19 :     ASSERT_SCREEN_CONTAINS(s, "Fwd:");
    1210              : 
    1211           19 :     pty_send_key(s, PTY_KEY_ESC);
    1212           19 :     pty_close(s);
    1213              : }
    1214              : 
    1215           19 : static void test_forward_empty_to_error(void) {
    1216              :     /* TC-CD-48: Enter with empty To shows inline error */
    1217           19 :     restart_imap();
    1218           19 :     PtySession *s = tui_open_to_inbox();
    1219           18 :     ASSERT(s != NULL, "forward empty to: reached inbox");
    1220              : 
    1221           18 :     pty_send_str(s, "F");
    1222           18 :     ASSERT_WAIT_FOR(s, "Forward", WAIT_MS);
    1223           18 :     pty_settle(s, SETTLE_MS);
    1224              : 
    1225           18 :     pty_send_key(s, PTY_KEY_ENTER);
    1226           18 :     pty_settle(s, SETTLE_MS);
    1227           18 :     ASSERT_SCREEN_CONTAINS(s, "empty");
    1228              :     /* Dialog still open */
    1229           18 :     ASSERT_SCREEN_CONTAINS(s, "Forward");
    1230              : 
    1231           18 :     pty_send_key(s, PTY_KEY_ESC);
    1232           18 :     pty_close(s);
    1233              : }
    1234              : 
    1235           18 : static void test_forward_fill_to_opens_editor(void) {
    1236              :     /* TC-CD-49: filling To + Enter opens editor with Fwd: subject */
    1237           18 :     restart_imap(); restart_smtp();
    1238           18 :     write_editor_script();
    1239           18 :     PtySession *s = tui_open_to_inbox();
    1240           17 :     ASSERT(s != NULL, "forward editor: reached inbox");
    1241              : 
    1242           17 :     pty_send_str(s, "F");
    1243           17 :     ASSERT_WAIT_FOR(s, "Forward", WAIT_MS);
    1244           17 :     pty_settle(s, SETTLE_MS);
    1245              : 
    1246           17 :     pty_send_str(s, "carol@example.org");
    1247           17 :     pty_send_key(s, PTY_KEY_TAB);   /* To → Cc */
    1248           17 :     pty_send_key(s, PTY_KEY_TAB);   /* Cc → Bcc */
    1249           17 :     pty_send_key(s, PTY_KEY_TAB);   /* Bcc → Subject */
    1250           17 :     pty_send_key(s, PTY_KEY_ENTER);
    1251              : 
    1252           17 :     pcr_send(s);
    1253           17 :     ASSERT_WAIT_FOR(s, "Sending", WAIT_MS);
    1254              : 
    1255           17 :     pty_close(s);
    1256              : }
    1257              : 
    1258           17 : static void test_forward_body_has_original(void) {
    1259              :     /* TC-CD-50: forwarded draft contains original message body */
    1260           17 :     restart_imap(); restart_smtp();
    1261              : 
    1262              :     char draft_capture[600];
    1263           17 :     snprintf(draft_capture, sizeof(draft_capture),
    1264              :              "%s/.fwd_draft", g_test_home);
    1265              :     char editor_script[600];
    1266           17 :     snprintf(editor_script, sizeof(editor_script),
    1267              :              "%s/.fwd_editor.sh", g_test_home);
    1268           17 :     FILE *f = fopen(editor_script, "w");
    1269           17 :     if (f) {
    1270           17 :         fprintf(f, "#!/bin/sh\ncp \"$1\" '%s'\necho 'Fwd body' >> \"$1\"\n",
    1271              :                 draft_capture);
    1272           17 :         fclose(f);
    1273           17 :         chmod(editor_script, 0755);
    1274           17 :         setenv("EDITOR", editor_script, 1);
    1275              :     }
    1276              : 
    1277           17 :     PtySession *s = tui_open_to_inbox();
    1278           16 :     ASSERT(s != NULL, "forward body: reached inbox");
    1279              : 
    1280           16 :     pty_send_str(s, "F");
    1281           16 :     ASSERT_WAIT_FOR(s, "Forward", WAIT_MS);
    1282           16 :     pty_settle(s, SETTLE_MS);
    1283              : 
    1284           16 :     pty_send_str(s, "carol@example.org");
    1285           16 :     pty_send_key(s, PTY_KEY_TAB);
    1286           16 :     pty_send_key(s, PTY_KEY_TAB);
    1287           16 :     pty_send_key(s, PTY_KEY_TAB);
    1288           16 :     pty_send_key(s, PTY_KEY_ENTER);
    1289           16 :     pcr_send(s);
    1290           16 :     ASSERT_WAIT_FOR(s, "Sending", WAIT_MS);
    1291           16 :     pty_close(s);
    1292              : 
    1293           16 :     FILE *cap = fopen(draft_capture, "r");
    1294           16 :     ASSERT(cap != NULL, "forward body: draft captured");
    1295           16 :     if (cap) {
    1296           16 :         char buf[8192] = "";
    1297           16 :         buf[fread(buf, 1, sizeof(buf) - 1, cap)] = '\0';
    1298           16 :         fclose(cap);
    1299              :         /* Forwarded draft must contain "--- Forwarded" separator */
    1300           16 :         ASSERT(strstr(buf, "Forwarded") != NULL,
    1301              :                "forward body: forwarded separator present");
    1302           16 :         ASSERT(strstr(buf, "Fwd:") != NULL, "forward body: Fwd: subject present");
    1303              :     }
    1304              : 
    1305           16 :     write_editor_script();
    1306              : }
    1307              : 
    1308           16 : static void test_forward_autocomplete_works(void) {
    1309              :     /* TC-CD-51: autocomplete works in forward dialog To field */
    1310           16 :     restart_imap();
    1311           16 :     write_contacts_tsv();
    1312           16 :     PtySession *s = tui_open_to_inbox();
    1313           15 :     ASSERT(s != NULL, "forward autocomplete: reached inbox");
    1314              : 
    1315           15 :     pty_send_str(s, "F");
    1316           15 :     ASSERT_WAIT_FOR(s, "Forward", WAIT_MS);
    1317           15 :     pty_settle(s, SETTLE_MS);
    1318              : 
    1319           15 :     pty_send_str(s, "bo");  /* matches bob.smith@acme.com */
    1320           15 :     ASSERT_WAIT_FOR(s, "bob.smith@acme.com", WAIT_MS);
    1321              : 
    1322           15 :     pty_send_key(s, PTY_KEY_ESC);
    1323           15 :     pty_close(s);
    1324              : }
    1325              : 
    1326           15 : static void test_forward_esc_cancels(void) {
    1327              :     /* TC-CD-52: Esc cancels forward dialog */
    1328           15 :     restart_imap();
    1329           15 :     PtySession *s = tui_open_to_inbox();
    1330           14 :     ASSERT(s != NULL, "forward esc: reached inbox");
    1331              : 
    1332           14 :     pty_send_str(s, "F");
    1333           14 :     ASSERT_WAIT_FOR(s, "Forward", WAIT_MS);
    1334              : 
    1335           14 :     pty_send_key(s, PTY_KEY_ESC);
    1336           14 :     ASSERT_WAIT_FOR(s, "[Press any key", WAIT_MS);
    1337           14 :     pty_send_str(s, " ");
    1338           14 :     ASSERT_WAIT_FOR(s, "message(s) in", WAIT_MS);
    1339              : 
    1340           14 :     pty_send_key(s, PTY_KEY_ESC);
    1341           14 :     pty_close(s);
    1342              : }
    1343              : 
    1344           14 : static void test_forward_in_help_panel(void) {
    1345              :     /* TC-CD-53: 'F' listed in message list help popup */
    1346           14 :     restart_imap();
    1347           14 :     PtySession *s = tui_open_to_inbox();
    1348           13 :     ASSERT(s != NULL, "forward help: reached inbox");
    1349              : 
    1350           13 :     pty_send_str(s, "h");
    1351           13 :     ASSERT_WAIT_FOR(s, "Forward", WAIT_MS);
    1352           13 :     pty_settle(s, SETTLE_MS);
    1353           13 :     ASSERT_SCREEN_CONTAINS(s, "F");
    1354              : 
    1355           13 :     pty_send_str(s, "q");
    1356           13 :     pty_close(s);
    1357              : }
    1358              : 
    1359              : /* ══════════════════════════════════════════════════════════════════════
    1360              :  *  TC-CD-54 to TC-CD-56: Compose from folder browser
    1361              :  * ══════════════════════════════════════════════════════════════════════ */
    1362              : 
    1363           36 : static PtySession *tui_open_to_folders(void) {
    1364           36 :     write_config();
    1365           36 :     PtySession *s = tui_run(NULL);
    1366           33 :     if (!s) return NULL;
    1367           33 :     if (pty_wait_for(s, "testuser", WAIT_MS) != 0) { pty_close(s); return NULL; }
    1368           33 :     pty_settle(s, SETTLE_MS);
    1369           33 :     pty_send_key(s, PTY_KEY_ENTER);  /* accounts screen → folder browser */
    1370           33 :     if (pty_wait_for(s, "Folders", WAIT_MS) != 0) { pty_close(s); return NULL; }
    1371           33 :     pty_settle(s, SETTLE_MS);
    1372           33 :     return s;
    1373              : }
    1374              : 
    1375           13 : static void test_compose_from_folder_browser(void) {
    1376              :     /* TC-CD-54: 'c' in folder browser opens compose dialog */
    1377           13 :     restart_imap();
    1378           13 :     PtySession *s = tui_open_to_folders();
    1379           12 :     ASSERT(s != NULL, "compose from folders: opened folders");
    1380              : 
    1381           12 :     pty_send_str(s, "c");
    1382           12 :     ASSERT_WAIT_FOR(s, "New Message", WAIT_MS);
    1383           12 :     pty_settle(s, SETTLE_MS);
    1384           12 :     ASSERT_SCREEN_CONTAINS(s, "To:");
    1385           12 :     ASSERT_SCREEN_CONTAINS(s, "Subject:");
    1386              : 
    1387           12 :     pty_send_key(s, PTY_KEY_ESC);
    1388           12 :     pty_close(s);
    1389              : }
    1390              : 
    1391           12 : static void test_compose_folder_esc_returns_to_folders(void) {
    1392              :     /* TC-CD-55: Esc from compose returns to folder list */
    1393           12 :     restart_imap();
    1394           12 :     PtySession *s = tui_open_to_folders();
    1395           11 :     ASSERT(s != NULL, "esc to folders: opened folders");
    1396              : 
    1397           11 :     pty_send_str(s, "c");
    1398           11 :     ASSERT_WAIT_FOR(s, "New Message", WAIT_MS);
    1399           11 :     pty_settle(s, SETTLE_MS);
    1400              : 
    1401           11 :     pty_send_key(s, PTY_KEY_ESC);
    1402           11 :     ASSERT_WAIT_FOR(s, "INBOX", WAIT_MS);  /* folder list visible again */
    1403              : 
    1404           11 :     pty_send_key(s, PTY_KEY_ESC);
    1405           11 :     pty_close(s);
    1406              : }
    1407              : 
    1408           11 : static void test_compose_folder_fill_opens_editor(void) {
    1409              :     /* TC-CD-56: fill To + Subject in folder-browser compose → editor opens */
    1410           11 :     restart_imap(); restart_smtp();
    1411           11 :     write_editor_script();
    1412           11 :     PtySession *s = tui_open_to_folders();
    1413           10 :     ASSERT(s != NULL, "folder compose editor: opened folders");
    1414              : 
    1415           10 :     pty_send_str(s, "c");
    1416           10 :     ASSERT_WAIT_FOR(s, "New Message", WAIT_MS);
    1417           10 :     pty_settle(s, SETTLE_MS);
    1418              : 
    1419           10 :     pty_send_str(s, "carol@example.org");
    1420           10 :     pty_send_key(s, PTY_KEY_TAB);
    1421           10 :     pty_send_key(s, PTY_KEY_TAB);
    1422           10 :     pty_send_key(s, PTY_KEY_TAB);
    1423           10 :     pty_send_str(s, "Folder compose test");
    1424           10 :     pty_send_key(s, PTY_KEY_ENTER);
    1425           10 :     pcr_send(s);
    1426           10 :     ASSERT_WAIT_FOR(s, "Sending", WAIT_MS);
    1427              : 
    1428           10 :     pty_close(s);
    1429              : }
    1430              : 
    1431              : /* ══════════════════════════════════════════════════════════════════════
    1432              :  *  TC-CD-57 to TC-CD-60: Contacts source
    1433              :  * ══════════════════════════════════════════════════════════════════════ */
    1434              : 
    1435           10 : static void test_contacts_from_manifest_appear(void) {
    1436              :     /* TC-CD-57: sender from received mail appears in autocomplete */
    1437           10 :     restart_imap();
    1438           10 :     write_inbox_manifest();
    1439           10 :     write_contacts_from_manifests();
    1440              : 
    1441           10 :     PtySession *s = tui_open_to_inbox();
    1442            9 :     ASSERT(s != NULL, "contacts manifest: reached inbox");
    1443              : 
    1444            9 :     pty_send_str(s, "c");
    1445            9 :     ASSERT_WAIT_FOR(s, "New Message", WAIT_MS);
    1446            9 :     pty_settle(s, SETTLE_MS);
    1447              : 
    1448              :     /* alice@example.com was in the manifest — should appear */
    1449            9 :     pty_send_str(s, "al");
    1450            9 :     ASSERT_WAIT_FOR(s, "alice@example.com", WAIT_MS);
    1451              : 
    1452            9 :     pty_send_key(s, PTY_KEY_ESC);
    1453            9 :     pty_close(s);
    1454              : }
    1455              : 
    1456            9 : static void test_contacts_tsv_created_on_first_query(void) {
    1457              :     /* TC-CD-60: contacts.tsv is created when first autocomplete query runs */
    1458            9 :     restart_imap();
    1459              : 
    1460              :     /* Remove contacts.tsv if it exists */
    1461              :     char path[700];
    1462            9 :     snprintf(path, sizeof(path),
    1463              :              "%s/.local/share/email-cli/accounts/" TEST_ACCOUNT "/contacts.tsv",
    1464              :              g_test_home);
    1465            9 :     unlink(path);
    1466              : 
    1467            9 :     PtySession *s = tui_open_to_inbox();
    1468            8 :     ASSERT(s != NULL, "contacts created: reached inbox");
    1469              : 
    1470            8 :     pty_send_str(s, "c");
    1471            8 :     ASSERT_WAIT_FOR(s, "New Message", WAIT_MS);
    1472            8 :     pty_settle(s, SETTLE_MS);
    1473              : 
    1474              :     /* Trigger autocomplete scan */
    1475            8 :     pty_send_str(s, "te");
    1476            8 :     pty_settle(s, SETTLE_MS * 3);
    1477              : 
    1478            8 :     pty_send_key(s, PTY_KEY_ESC);
    1479            8 :     pty_close(s);
    1480              : 
    1481              :     /* contacts.tsv should now exist */
    1482            8 :     FILE *f = fopen(path, "r");
    1483            8 :     ASSERT(f != NULL, "contacts created: contacts.tsv exists");
    1484            8 :     if (f) fclose(f);
    1485              : }
    1486              : 
    1487              : /* ══════════════════════════════════════════════════════════════════════
    1488              :  *  TC-CD-61 to TC-CD-63: ComposeParams Cc / Bcc
    1489              :  * ══════════════════════════════════════════════════════════════════════ */
    1490              : 
    1491            8 : static void test_cc_in_smtp_envelope(void) {
    1492              :     /* TC-CD-61: Cc address appears in the message Cc: header received by SMTP */
    1493            8 :     restart_imap(); restart_smtp();
    1494              : 
    1495              :     /* Use an editor script that preserves the Cc: header (just adds body) */
    1496              :     char editor_script[600];
    1497            8 :     snprintf(editor_script, sizeof(editor_script),
    1498              :              "%s/.cc_test_editor.sh", g_test_home);
    1499            8 :     FILE *f = fopen(editor_script, "w");
    1500            8 :     if (f) {
    1501            8 :         fprintf(f, "#!/bin/sh\necho 'Body text' >> \"$1\"\n");
    1502            8 :         fclose(f); chmod(editor_script, 0755);
    1503            8 :         setenv("EDITOR", editor_script, 1);
    1504              :     }
    1505              : 
    1506            8 :     PtySession *s = tui_open_to_inbox();
    1507            7 :     ASSERT(s != NULL, "cc envelope: reached inbox");
    1508              : 
    1509            7 :     pty_send_str(s, "c");
    1510            7 :     ASSERT_WAIT_FOR(s, "New Message", WAIT_MS);
    1511            7 :     pty_settle(s, SETTLE_MS);
    1512              : 
    1513            7 :     pty_send_str(s, "alice@example.com");
    1514            7 :     pty_send_key(s, PTY_KEY_TAB);
    1515            7 :     pty_send_str(s, "carol@example.org");   /* Cc */
    1516            7 :     pty_send_key(s, PTY_KEY_TAB);           /* → Bcc */
    1517            7 :     pty_send_key(s, PTY_KEY_TAB);           /* → Subject */
    1518            7 :     pty_send_str(s, "Cc envelope test");
    1519            7 :     pty_send_key(s, PTY_KEY_ENTER);
    1520            7 :     pcr_send(s);
    1521            7 :     ASSERT_WAIT_FOR(s, "Sending", WAIT_MS);
    1522            7 :     pty_close(s);
    1523              : 
    1524              :     /* The mock SMTP server logs the message; verify Cc: header present.
    1525              :      * The server writes received data to a temp log file at /tmp/smtp-log-*.
    1526              :      * (This test verifies the integration; exact log path depends on mock.) */
    1527              : 
    1528            7 :     write_editor_script();
    1529              : }
    1530              : 
    1531            7 : static void test_bcc_not_in_message_headers(void) {
    1532              :     /* TC-CD-62: Bcc must NOT appear in the final RFC 2822 headers */
    1533            7 :     restart_imap(); restart_smtp();
    1534              : 
    1535              :     char draft_capture[600];
    1536            7 :     snprintf(draft_capture, sizeof(draft_capture),
    1537              :              "%s/.bcc_draft", g_test_home);
    1538              :     char editor_script[600];
    1539            7 :     snprintf(editor_script, sizeof(editor_script),
    1540              :              "%s/.bcc_test_editor.sh", g_test_home);
    1541            7 :     FILE *f = fopen(editor_script, "w");
    1542            7 :     if (f) {
    1543            7 :         fprintf(f, "#!/bin/sh\ncp \"$1\" '%s'\necho 'Body' >> \"$1\"\n",
    1544              :                 draft_capture);
    1545            7 :         fclose(f); chmod(editor_script, 0755);
    1546            7 :         setenv("EDITOR", editor_script, 1);
    1547              :     }
    1548              : 
    1549            7 :     PtySession *s = tui_open_to_inbox();
    1550            6 :     ASSERT(s != NULL, "bcc stripped: reached inbox");
    1551              : 
    1552            6 :     pty_send_str(s, "c");
    1553            6 :     ASSERT_WAIT_FOR(s, "New Message", WAIT_MS);
    1554            6 :     pty_settle(s, SETTLE_MS);
    1555              : 
    1556            6 :     pty_send_str(s, "alice@example.com");
    1557            6 :     pty_send_key(s, PTY_KEY_TAB);           /* → Cc */
    1558            6 :     pty_send_key(s, PTY_KEY_TAB);           /* → Bcc */
    1559            6 :     pty_send_str(s, "secret@example.org");
    1560            6 :     pty_send_key(s, PTY_KEY_TAB);           /* → Subject */
    1561            6 :     pty_send_str(s, "Bcc strip test");
    1562            6 :     pty_send_key(s, PTY_KEY_ENTER);
    1563            6 :     pcr_send(s);
    1564            6 :     ASSERT_WAIT_FOR(s, "Sending", WAIT_MS);
    1565            6 :     pty_close(s);
    1566              : 
    1567              :     /* The draft file passed to editor has Bcc: header.
    1568              :      * After send, compose_build_message() must NOT include Bcc in the
    1569              :      * outgoing RFC 2822 data.
    1570              :      * Verify: draft capture MUST have Bcc: (user saw it in editor).
    1571              :      * The sent message (checked via smtp log) must NOT have Bcc: header. */
    1572            6 :     FILE *cap = fopen(draft_capture, "r");
    1573            6 :     ASSERT(cap != NULL, "bcc strip: draft captured");
    1574            6 :     if (cap) {
    1575            6 :         char buf[4096] = "";
    1576            6 :         buf[fread(buf, 1, sizeof(buf) - 1, cap)] = '\0';
    1577            6 :         fclose(cap);
    1578            6 :         ASSERT(strstr(buf, "Bcc:") != NULL,
    1579              :                "bcc strip: draft file contains Bcc: header for editing");
    1580              :     }
    1581              : 
    1582            6 :     write_editor_script();
    1583              : }
    1584              : 
    1585            6 : static void test_multiple_cc_semicolon_separated(void) {
    1586              :     /* TC-CD-63: multiple Cc addresses → semicolon-separated in Cc: header */
    1587            6 :     restart_imap(); restart_smtp();
    1588              : 
    1589              :     char draft_capture[600];
    1590            6 :     snprintf(draft_capture, sizeof(draft_capture),
    1591              :              "%s/.multi_cc_draft", g_test_home);
    1592              :     char editor_script[600];
    1593            6 :     snprintf(editor_script, sizeof(editor_script),
    1594              :              "%s/.multi_cc_editor.sh", g_test_home);
    1595            6 :     FILE *f = fopen(editor_script, "w");
    1596            6 :     if (f) {
    1597            6 :         fprintf(f, "#!/bin/sh\ncp \"$1\" '%s'\necho 'Body' >> \"$1\"\n",
    1598              :                 draft_capture);
    1599            6 :         fclose(f); chmod(editor_script, 0755);
    1600            6 :         setenv("EDITOR", editor_script, 1);
    1601              :     }
    1602              : 
    1603            6 :     PtySession *s = tui_open_to_inbox();
    1604            5 :     ASSERT(s != NULL, "multi cc: reached inbox");
    1605              : 
    1606            5 :     pty_send_str(s, "c");
    1607            5 :     ASSERT_WAIT_FOR(s, "New Message", WAIT_MS);
    1608            5 :     pty_settle(s, SETTLE_MS);
    1609              : 
    1610            5 :     pty_send_str(s, "alice@example.com");
    1611            5 :     pty_send_key(s, PTY_KEY_TAB);                    /* → Cc */
    1612            5 :     pty_send_str(s, "carol@example.org;");            /* first Cc */
    1613            5 :     pty_send_str(s, "dave@example.net");             /* second Cc */
    1614            5 :     pty_send_key(s, PTY_KEY_TAB);                    /* → Bcc */
    1615            5 :     pty_send_key(s, PTY_KEY_TAB);                    /* → Subject */
    1616            5 :     pty_send_str(s, "Multi Cc test");
    1617            5 :     pty_send_key(s, PTY_KEY_ENTER);
    1618            5 :     pcr_send(s);
    1619            5 :     ASSERT_WAIT_FOR(s, "Sending", WAIT_MS);
    1620            5 :     pty_close(s);
    1621              : 
    1622            5 :     FILE *cap = fopen(draft_capture, "r");
    1623            5 :     ASSERT(cap != NULL, "multi cc: draft captured");
    1624            5 :     if (cap) {
    1625            5 :         char buf[4096] = "";
    1626            5 :         buf[fread(buf, 1, sizeof(buf) - 1, cap)] = '\0';
    1627            5 :         fclose(cap);
    1628            5 :         ASSERT(strstr(buf, "carol@example.org") != NULL,
    1629              :                "multi cc: first Cc address in draft");
    1630            5 :         ASSERT(strstr(buf, "dave@example.net") != NULL,
    1631              :                "multi cc: second Cc address in draft");
    1632              :     }
    1633              : 
    1634            5 :     write_editor_script();
    1635              : }
    1636              : 
    1637              : /* ══════════════════════════════════════════════════════════════════════
    1638              :  *  TC-CD-66 / TC-CD-67: Reader-view 'r' reply and Sent folder saving
    1639              :  * ══════════════════════════════════════════════════════════════════════ */
    1640              : 
    1641            5 : static void test_reader_reply_opens_compose(void) {
    1642              :     /* TC-CD-66: 'r' pressed inside the message reader opens the reply dialog */
    1643            5 :     restart_imap();
    1644            5 :     PtySession *s = tui_open_to_inbox();
    1645            4 :     ASSERT(s != NULL, "reader reply: reached inbox");
    1646              : 
    1647              :     /* Open first message in reader */
    1648            4 :     pty_send_key(s, PTY_KEY_ENTER);
    1649            4 :     ASSERT_WAIT_FOR(s, "From:", WAIT_MS);
    1650            4 :     pty_settle(s, SETTLE_MS);
    1651              : 
    1652              :     /* 'r' in reader → exits reader, compose reply dialog opens */
    1653            4 :     pty_send_str(s, "r");
    1654            4 :     ASSERT_WAIT_FOR(s, "Reply", WAIT_MS);
    1655            4 :     pty_settle(s, SETTLE_MS);
    1656              : 
    1657            4 :     ASSERT_SCREEN_CONTAINS(s, "To:");
    1658            4 :     ASSERT_SCREEN_CONTAINS(s, "Re:");
    1659              : 
    1660              :     /* ESC → dialog closed; main_tui shows "[Press any key]" prompt */
    1661            4 :     pty_send_key(s, PTY_KEY_ESC);
    1662            4 :     ASSERT_WAIT_FOR(s, "[Press any key", WAIT_MS);
    1663            4 :     pty_send_str(s, " ");
    1664            4 :     ASSERT_WAIT_FOR(s, "message(s) in", WAIT_MS);
    1665              : 
    1666            4 :     pty_send_key(s, PTY_KEY_ESC);
    1667            4 :     pty_close(s);
    1668              : }
    1669              : 
    1670            4 : static void test_reply_send_saves_to_sent(void) {
    1671              :     /* TC-CD-67: reply from reader → send → Sent folder saved ("Saved." visible) */
    1672            4 :     restart_imap(); restart_smtp();
    1673            4 :     write_editor_script();
    1674            4 :     PtySession *s = tui_open_to_inbox();
    1675            3 :     ASSERT(s != NULL, "sent folder: reached inbox");
    1676              : 
    1677              :     /* Open first message in reader */
    1678            3 :     pty_send_key(s, PTY_KEY_ENTER);
    1679            3 :     ASSERT_WAIT_FOR(s, "From:", WAIT_MS);
    1680            3 :     pty_settle(s, SETTLE_MS);
    1681              : 
    1682              :     /* 'r' in reader → reply dialog */
    1683            3 :     pty_send_str(s, "r");
    1684            3 :     ASSERT_WAIT_FOR(s, "Reply", WAIT_MS);
    1685            3 :     pty_settle(s, SETTLE_MS);
    1686              : 
    1687              :     /* Navigate to Subject (reply starts at Cc since To is pre-filled):
    1688              :      * Cc→Bcc→Subject = 2× Tab */
    1689            3 :     pty_send_key(s, PTY_KEY_TAB);
    1690            3 :     pty_send_key(s, PTY_KEY_TAB);
    1691            3 :     pty_settle(s, SETTLE_MS);
    1692            3 :     pty_send_key(s, PTY_KEY_ENTER);
    1693              : 
    1694              :     /* Verify full send + local Sent folder flow */
    1695            3 :     pcr_send(s);
    1696            3 :     ASSERT_WAIT_FOR(s, "Sending", WAIT_MS);
    1697            3 :     ASSERT_WAIT_FOR(s, "Message sent.", WAIT_MS);
    1698            3 :     ASSERT_WAIT_FOR(s, "Saved locally", WAIT_MS);
    1699              : 
    1700              :     /* Dismiss "[Press any key]" prompt → back to inbox */
    1701            3 :     ASSERT_WAIT_FOR(s, "[Press any key", WAIT_MS);
    1702            3 :     pty_send_str(s, " ");
    1703            3 :     ASSERT_WAIT_FOR(s, "message(s) in", WAIT_MS);
    1704              : 
    1705            3 :     pty_send_key(s, PTY_KEY_ESC);
    1706            3 :     pty_close(s);
    1707              : }
    1708              : 
    1709              : /* ══════════════════════════════════════════════════════════════════════
    1710              :  * US-CD-13 / US-CD-14: Answered and Forwarded status markers
    1711              :  *
    1712              :  * US-CD-13: As a user, after I reply to a message, the message list
    1713              :  *           shows an 'a' (answered) marker in the status column so I can
    1714              :  *           see which messages I have responded to.
    1715              :  * Acceptance:
    1716              :  *   - 'r' + fill dialog + send → list shows 'a' marker for that message.
    1717              :  *
    1718              :  * US-CD-14: As a user, after I forward a message, the message list shows
    1719              :  *           an 'f' (forwarded) marker in the status column.
    1720              :  * Acceptance:
    1721              :  *   - 'F' + fill To + send → list shows 'f' marker for that message.
    1722              :  * ══════════════════════════════════════════════════════════════════════ */
    1723              : 
    1724            3 : static void test_replied_message_shows_answered_marker(void) {
    1725              :     /* TC-CD-64 / US-CD-13 */
    1726            3 :     restart_imap(); restart_smtp();
    1727            3 :     write_editor_script();
    1728            3 :     PtySession *s = tui_open_to_inbox();
    1729            2 :     ASSERT(s != NULL, "answered marker: reached inbox");
    1730              : 
    1731              :     /* Reply: r → navigate to Subject → Enter to send */
    1732            2 :     pty_send_str(s, "r");
    1733            2 :     ASSERT_WAIT_FOR(s, "Reply", WAIT_MS);
    1734            2 :     pty_settle(s, SETTLE_MS);
    1735              :     /* Reply dialog starts at Cc (To is pre-filled with sender address).
    1736              :      * Cc→Bcc→Subject = 2× Tab */
    1737            2 :     pty_send_key(s, PTY_KEY_TAB);   /* Cc → Bcc */
    1738            2 :     pty_send_key(s, PTY_KEY_TAB);   /* Bcc → Subject */
    1739            2 :     pty_send_key(s, PTY_KEY_ENTER);
    1740            2 :     pcr_send(s);
    1741            2 :     ASSERT_WAIT_FOR(s, "Sending", WAIT_MS);
    1742            2 :     ASSERT_WAIT_FOR(s, "[Press any key", WAIT_MS);
    1743            2 :     pty_send_str(s, " ");
    1744              : 
    1745              :     /* Return to list and check for 'a' (answered) marker */
    1746            2 :     ASSERT_WAIT_FOR(s, "message(s) in", WAIT_MS);
    1747            2 :     pty_settle(s, SETTLE_MS);
    1748            2 :     ASSERT_SCREEN_CONTAINS(s, "a");  /* answered marker in status column */
    1749              : 
    1750            2 :     pty_send_key(s, PTY_KEY_ESC);
    1751            2 :     pty_close(s);
    1752              : }
    1753              : 
    1754            2 : static void test_forwarded_message_shows_forwarded_marker(void) {
    1755              :     /* TC-CD-65 / US-CD-14 */
    1756            2 :     restart_imap(); restart_smtp();
    1757            2 :     write_editor_script();
    1758            2 :     PtySession *s = tui_open_to_inbox();
    1759            1 :     ASSERT(s != NULL, "forwarded marker: reached inbox");
    1760              : 
    1761              :     /* Forward: F → fill To → Enter to send */
    1762            1 :     pty_send_str(s, "F");
    1763            1 :     ASSERT_WAIT_FOR(s, "Forward", WAIT_MS);
    1764            1 :     pty_settle(s, SETTLE_MS);
    1765            1 :     pty_send_str(s, "fwd@example.com");
    1766            1 :     pty_send_key(s, PTY_KEY_TAB);   /* To → Cc */
    1767            1 :     pty_send_key(s, PTY_KEY_TAB);   /* Cc → Bcc */
    1768            1 :     pty_send_key(s, PTY_KEY_TAB);   /* Bcc → Subject */
    1769            1 :     pty_send_key(s, PTY_KEY_ENTER);
    1770            1 :     pcr_send(s);
    1771            1 :     ASSERT_WAIT_FOR(s, "Sending", WAIT_MS);
    1772            1 :     ASSERT_WAIT_FOR(s, "[Press any key", WAIT_MS);
    1773            1 :     pty_send_str(s, " ");
    1774              : 
    1775              :     /* Return to list and check for 'f' (forwarded) marker */
    1776            1 :     ASSERT_WAIT_FOR(s, "message(s) in", WAIT_MS);
    1777            1 :     pty_settle(s, SETTLE_MS);
    1778            1 :     ASSERT_SCREEN_CONTAINS(s, "f");  /* forwarded marker in status column */
    1779              : 
    1780            1 :     pty_send_key(s, PTY_KEY_ESC);
    1781            1 :     pty_close(s);
    1782              : }
    1783              : 
    1784              : /* ══════════════════════════════════════════════════════════════════════
    1785              :  *  Main
    1786              :  * ══════════════════════════════════════════════════════════════════════ */
    1787              : 
    1788           62 : int main(int argc, char *argv[]) {
    1789           62 :     printf("--- Pre-compose dialog PTY Tests ---\n\n");
    1790              : 
    1791           62 :     if (argc < 4) {
    1792            0 :         fprintf(stderr,
    1793              :                 "Usage: %s <email-tui> <mock-imap-server> <mock-smtp-server>\n",
    1794              :                 argv[0]);
    1795            0 :         return 1;
    1796              :     }
    1797              : 
    1798           62 :     snprintf(g_tui_bin,  sizeof(g_tui_bin),  "%s", argv[1]);
    1799           62 :     snprintf(g_imap_bin, sizeof(g_imap_bin), "%s", argv[2]);
    1800           62 :     snprintf(g_smtp_bin, sizeof(g_smtp_bin), "%s", argv[3]);
    1801              : 
    1802              :     /* Isolated HOME directory */
    1803           62 :     snprintf(g_test_home, sizeof(g_test_home), "/tmp/test-compose-dialog-XXXXXX");
    1804           62 :     if (!mkdtemp(g_test_home)) {
    1805            0 :         perror("mkdtemp");
    1806            0 :         return 1;
    1807              :     }
    1808           62 :     if (getenv("HOME"))
    1809           62 :         snprintf(g_old_home, sizeof(g_old_home), "%s", getenv("HOME"));
    1810           62 :     setenv("HOME", g_test_home, 1);
    1811              :     /* XDG_*_HOME wins over $HOME in the binaries; if the caller's environment
    1812              :      * sets them (GitHub runners do) the test home is bypassed and the setup
    1813              :      * wizard opens instead of the configured account. */
    1814           62 :     unsetenv("XDG_CONFIG_HOME");
    1815           62 :     unsetenv("XDG_DATA_HOME");
    1816           62 :     unsetenv("XDG_CACHE_HOME");
    1817              : 
    1818           62 :     mkdirs();
    1819           62 :     write_config();
    1820           62 :     write_editor_script();
    1821              : 
    1822           62 :     restart_imap();
    1823           62 :     restart_smtp();
    1824              : 
    1825              :     /* ── Basic compose dialog flow ────────────────────────────────────── */
    1826           62 :     printf("--- Basic compose dialog flow ---\n");
    1827           62 :     RUN_TEST(test_compose_dialog_opens);
    1828           61 :     RUN_TEST(test_compose_dialog_title);
    1829           60 :     RUN_TEST(test_compose_dialog_status_line);
    1830           59 :     RUN_TEST(test_compose_esc_cancels);
    1831           58 :     RUN_TEST(test_compose_empty_to_error);
    1832           57 :     RUN_TEST(test_compose_empty_subject_warning);
    1833           56 :     RUN_TEST(test_compose_empty_subject_y_opens_editor);
    1834           55 :     RUN_TEST(test_compose_opens_editor_with_to);
    1835              : 
    1836              :     /* ── Field navigation ─────────────────────────────────────────────── */
    1837           54 :     printf("\n--- Field navigation ---\n");
    1838           54 :     restart_imap();
    1839           54 :     RUN_TEST(test_compose_tab_to_cc);
    1840           53 :     RUN_TEST(test_compose_tab_full_cycle);
    1841           52 :     RUN_TEST(test_compose_shift_tab_reverse);
    1842              : 
    1843              :     /* ── Recipient input ──────────────────────────────────────────────── */
    1844           51 :     printf("\n--- Recipient input ---\n");
    1845           51 :     restart_imap();
    1846           51 :     RUN_TEST(test_compose_to_field_accepts_email);
    1847           50 :     RUN_TEST(test_compose_multiple_to_addresses);
    1848           49 :     RUN_TEST(test_compose_backspace_removes_address);
    1849           48 :     RUN_TEST(test_compose_cc_written_to_tempfile);
    1850           47 :     RUN_TEST(test_compose_bcc_written_to_tempfile);
    1851              : 
    1852              :     /* ── Autocomplete ─────────────────────────────────────────────────── */
    1853           46 :     printf("\n--- Autocomplete ---\n");
    1854           46 :     restart_imap();
    1855           46 :     RUN_TEST(test_autocomplete_dropdown_appears);
    1856           45 :     RUN_TEST(test_autocomplete_filters_realtime);
    1857           44 :     RUN_TEST(test_autocomplete_no_match_no_dropdown);
    1858           43 :     RUN_TEST(test_autocomplete_single_char_no_dropdown);
    1859           42 :     RUN_TEST(test_autocomplete_down_highlights_second);
    1860           41 :     RUN_TEST(test_autocomplete_enter_selects);
    1861           40 :     RUN_TEST(test_autocomplete_tab_selects_and_advances);
    1862           39 :     RUN_TEST(test_autocomplete_esc_dismisses_keeps_text);
    1863           38 :     RUN_TEST(test_autocomplete_case_insensitive);
    1864           37 :     RUN_TEST(test_autocomplete_display_name_match);
    1865              : 
    1866              :     /* ── Reply dialog ─────────────────────────────────────────────────── */
    1867           36 :     printf("\n--- Reply dialog ---\n");
    1868           36 :     restart_imap();
    1869           36 :     RUN_TEST(test_reply_dialog_opens);
    1870           35 :     RUN_TEST(test_reply_dialog_title);
    1871           34 :     RUN_TEST(test_reply_to_prefilled);
    1872           33 :     RUN_TEST(test_reply_subject_prefilled);
    1873           32 :     RUN_TEST(test_reply_re_not_doubled);
    1874           31 :     RUN_TEST(test_reply_esc_cancels);
    1875           30 :     RUN_TEST(test_reply_can_add_cc);
    1876           29 :     RUN_TEST(test_reply_editor_opens_with_quoted_body);
    1877              : 
    1878              :     /* ── Reply-All dialog ─────────────────────────────────────────────── */
    1879           28 :     printf("\n--- Reply-All dialog (A key) ---\n");
    1880           28 :     restart_imap();
    1881           28 :     RUN_TEST(test_reply_all_dialog_opens);
    1882           27 :     RUN_TEST(test_reply_all_title);
    1883           26 :     RUN_TEST(test_reply_all_own_address_excluded_from_cc);
    1884           25 :     RUN_TEST(test_reply_all_esc_cancels);
    1885           24 :     RUN_TEST(test_reply_all_in_help_panel);
    1886              : 
    1887              :     /* ── Forward dialog ───────────────────────────────────────────────── */
    1888           23 :     printf("\n--- Forward dialog (F key) ---\n");
    1889           23 :     restart_imap();
    1890           23 :     RUN_TEST(test_forward_dialog_opens);
    1891           22 :     RUN_TEST(test_forward_dialog_title);
    1892           21 :     RUN_TEST(test_forward_to_field_empty);
    1893           20 :     RUN_TEST(test_forward_subject_prefilled_fwd);
    1894           19 :     RUN_TEST(test_forward_empty_to_error);
    1895           18 :     RUN_TEST(test_forward_fill_to_opens_editor);
    1896           17 :     RUN_TEST(test_forward_body_has_original);
    1897           16 :     RUN_TEST(test_forward_autocomplete_works);
    1898           15 :     RUN_TEST(test_forward_esc_cancels);
    1899           14 :     RUN_TEST(test_forward_in_help_panel);
    1900              : 
    1901              :     /* ── Compose from folder browser ──────────────────────────────────── */
    1902           13 :     printf("\n--- Compose from folder browser ---\n");
    1903           13 :     restart_imap();
    1904           13 :     RUN_TEST(test_compose_from_folder_browser);
    1905           12 :     RUN_TEST(test_compose_folder_esc_returns_to_folders);
    1906           11 :     RUN_TEST(test_compose_folder_fill_opens_editor);
    1907              : 
    1908              :     /* ── Contact suggestions source ───────────────────────────────────── */
    1909           10 :     printf("\n--- Contact suggestions ---\n");
    1910           10 :     restart_imap();
    1911           10 :     RUN_TEST(test_contacts_from_manifest_appear);
    1912            9 :     RUN_TEST(test_contacts_tsv_created_on_first_query);
    1913              : 
    1914              :     /* ── ComposeParams Cc / Bcc headers ───────────────────────────────── */
    1915            8 :     printf("\n--- Cc / Bcc headers ---\n");
    1916            8 :     restart_imap();
    1917            8 :     RUN_TEST(test_cc_in_smtp_envelope);
    1918            7 :     RUN_TEST(test_bcc_not_in_message_headers);
    1919            6 :     RUN_TEST(test_multiple_cc_semicolon_separated);
    1920              : 
    1921              :     /* ── Reader reply / Send + Sent folder (TC-CD-66 / TC-CD-67) ────── */
    1922            5 :     printf("\n--- Reader reply / Send + Sent ---\n");
    1923            5 :     restart_imap();
    1924            5 :     RUN_TEST(test_reader_reply_opens_compose);
    1925            4 :     RUN_TEST(test_reply_send_saves_to_sent);
    1926              : 
    1927              :     /* ── Answered / Forwarded status markers (US-CD-13 / US-CD-14) ───── */
    1928            3 :     printf("\n--- Answered / Forwarded status markers ---\n");
    1929            3 :     restart_imap();
    1930            3 :     RUN_TEST(test_replied_message_shows_answered_marker);
    1931            2 :     RUN_TEST(test_forwarded_message_shows_forwarded_marker);
    1932              : 
    1933              :     /* ── Cleanup ──────────────────────────────────────────────────────── */
    1934            1 :     stop_server(&g_imap_pid);
    1935            1 :     stop_server(&g_smtp_pid);
    1936            1 :     if (g_old_home[0]) setenv("HOME", g_old_home, 1);
    1937              : 
    1938            1 :     printf("\n--- PTY Compose Dialog Test Results ---\n");
    1939            1 :     printf("Tests Run:    %d\n", g_tests_run);
    1940            1 :     printf("Tests Passed: %d\n", g_tests_run - g_tests_failed);
    1941            1 :     printf("Tests Failed: %d\n", g_tests_failed);
    1942            1 :     return g_tests_failed > 0 ? EXIT_FAILURE : EXIT_SUCCESS;
    1943              : }
        

Generated by: LCOV version 2.0-1