llmtrim 0.9.3

Static, deterministic LLM prompt/payload compressor — cut input tokens 30-90% with zero extra model calls
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
1001
1002
1003
1004
1005
1006
1007
1008
1009
1010
1011
1012
1013
1014
1015
1016
1017
1018
1019
1020
1021
1022
1023
1024
1025
1026
1027
1028
1029
1030
1031
1032
1033
1034
1035
1036
1037
1038
1039
1040
1041
1042
1043
1044
1045
1046
1047
1048
1049
1050
1051
1052
1053
1054
1055
1056
1057
1058
1059
1060
1061
1062
1063
1064
1065
1066
1067
1068
1069
1070
1071
1072
1073
1074
1075
1076
1077
1078
1079
1080
1081
1082
1083
1084
1085
1086
1087
1088
1089
1090
1091
1092
1093
1094
1095
1096
1097
1098
1099
1100
1101
1102
1103
1104
1105
1106
1107
1108
1109
1110
1111
1112
1113
1114
1115
1116
1117
1118
1119
1120
1121
1122
1123
1124
1125
1126
1127
1128
1129
1130
1131
1132
1133
1134
1135
1136
1137
1138
1139
1140
1141
1142
1143
1144
1145
1146
1147
1148
1149
1150
1151
1152
1153
1154
1155
1156
1157
1158
1159
1160
1161
1162
1163
1164
1165
1166
1167
1168
1169
1170
1171
1172
1173
1174
1175
1176
1177
1178
1179
1180
1181
1182
1183
1184
1185
1186
1187
1188
1189
1190
1191
1192
1193
1194
1195
1196
1197
1198
1199
1200
1201
1202
1203
1204
1205
1206
1207
1208
1209
1210
1211
1212
1213
1214
1215
1216
1217
1218
1219
1220
1221
1222
1223
1224
1225
1226
1227
1228
1229
1230
1231
1232
1233
1234
1235
1236
1237
1238
1239
1240
1241
1242
1243
1244
1245
1246
1247
1248
1249
1250
1251
1252
1253
1254
1255
1256
1257
1258
1259
1260
1261
1262
1263
1264
1265
1266
1267
1268
1269
1270
1271
1272
1273
1274
1275
1276
1277
1278
1279
1280
1281
1282
1283
1284
1285
1286
1287
1288
1289
1290
1291
1292
1293
1294
1295
1296
1297
1298
1299
1300
1301
1302
1303
1304
1305
1306
1307
1308
1309
1310
1311
1312
1313
1314
1315
1316
1317
1318
1319
1320
1321
1322
1323
1324
1325
1326
1327
1328
1329
1330
1331
1332
1333
1334
1335
1336
1337
1338
1339
1340
1341
1342
1343
1344
1345
1346
1347
1348
1349
1350
1351
1352
1353
1354
1355
1356
1357
1358
1359
1360
1361
1362
1363
1364
1365
1366
1367
1368
1369
1370
1371
1372
1373
1374
1375
1376
1377
1378
1379
1380
1381
1382
1383
1384
1385
1386
1387
1388
1389
1390
1391
1392
1393
1394
1395
1396
1397
1398
1399
1400
1401
1402
1403
1404
1405
1406
1407
1408
1409
1410
1411
1412
1413
1414
1415
1416
1417
1418
1419
1420
1421
1422
1423
1424
1425
1426
1427
1428
1429
1430
1431
1432
1433
1434
1435
1436
1437
1438
1439
1440
1441
1442
1443
1444
1445
1446
1447
1448
1449
1450
1451
1452
1453
1454
1455
1456
1457
1458
1459
1460
1461
1462
1463
1464
1465
1466
1467
1468
1469
1470
1471
1472
1473
1474
1475
1476
1477
1478
1479
1480
1481
1482
1483
1484
1485
1486
1487
1488
1489
1490
1491
1492
1493
1494
1495
1496
1497
1498
1499
1500
1501
1502
1503
1504
1505
1506
1507
1508
1509
1510
1511
1512
1513
1514
1515
1516
1517
1518
1519
1520
1521
1522
1523
1524
1525
1526
1527
1528
1529
1530
1531
1532
1533
1534
1535
1536
1537
1538
1539
1540
1541
1542
1543
1544
1545
1546
1547
1548
1549
1550
1551
1552
1553
1554
1555
1556
1557
1558
1559
1560
1561
1562
1563
1564
1565
1566
1567
1568
1569
1570
1571
1572
1573
1574
1575
1576
1577
1578
1579
1580
1581
1582
1583
1584
1585
1586
1587
1588
1589
1590
1591
1592
1593
1594
1595
1596
1597
1598
1599
1600
1601
1602
1603
1604
1605
1606
1607
1608
1609
1610
1611
1612
1613
1614
1615
1616
1617
1618
1619
1620
1621
1622
1623
1624
1625
1626
1627
1628
1629
1630
1631
1632
1633
1634
1635
1636
1637
1638
1639
1640
1641
1642
1643
1644
1645
1646
1647
1648
1649
1650
1651
1652
1653
1654
1655
1656
1657
1658
1659
1660
1661
1662
1663
1664
1665
1666
1667
1668
1669
1670
1671
1672
1673
1674
1675
1676
1677
1678
1679
1680
1681
1682
1683
1684
1685
1686
1687
1688
1689
1690
1691
1692
1693
1694
1695
1696
1697
1698
1699
1700
1701
1702
1703
1704
1705
1706
1707
1708
1709
1710
1711
1712
1713
1714
1715
1716
1717
1718
1719
1720
1721
1722
1723
1724
1725
1726
1727
1728
1729
1730
1731
1732
1733
1734
1735
1736
1737
1738
1739
1740
1741
1742
1743
1744
1745
1746
1747
1748
1749
1750
1751
1752
1753
1754
1755
1756
1757
1758
1759
1760
1761
1762
1763
1764
1765
1766
1767
1768
1769
1770
1771
1772
1773
1774
1775
1776
1777
1778
1779
1780
1781
1782
1783
1784
1785
1786
1787
1788
1789
1790
1791
1792
1793
1794
1795
1796
1797
1798
1799
1800
1801
1802
1803
1804
1805
1806
1807
1808
1809
1810
1811
1812
1813
1814
1815
1816
1817
1818
1819
1820
1821
1822
1823
1824
1825
1826
1827
1828
1829
1830
1831
1832
1833
1834
1835
1836
1837
1838
1839
1840
1841
1842
1843
1844
1845
1846
1847
1848
1849
1850
1851
1852
1853
1854
1855
1856
1857
1858
1859
1860
1861
1862
1863
1864
1865
1866
1867
1868
1869
1870
1871
1872
1873
1874
1875
1876
1877
1878
1879
1880
1881
1882
1883
1884
1885
1886
1887
1888
1889
1890
1891
1892
1893
1894
1895
1896
1897
1898
1899
1900
1901
1902
1903
1904
1905
1906
1907
1908
1909
1910
1911
1912
1913
1914
1915
1916
1917
1918
1919
1920
1921
1922
1923
1924
1925
1926
1927
1928
1929
1930
1931
1932
1933
1934
1935
1936
1937
1938
1939
1940
1941
1942
1943
1944
1945
1946
1947
1948
1949
1950
1951
1952
1953
1954
1955
1956
1957
1958
1959
1960
1961
1962
1963
1964
1965
1966
1967
1968
1969
1970
1971
1972
1973
1974
1975
1976
1977
1978
1979
1980
1981
1982
1983
1984
1985
1986
1987
1988
1989
1990
1991
1992
1993
1994
1995
1996
1997
1998
1999
2000
2001
2002
2003
2004
2005
2006
2007
2008
2009
2010
2011
2012
2013
2014
2015
2016
2017
2018
2019
2020
2021
2022
2023
2024
2025
2026
2027
2028
2029
2030
2031
2032
2033
2034
2035
2036
2037
2038
2039
2040
2041
2042
2043
2044
2045
2046
2047
2048
2049
2050
2051
2052
2053
2054
2055
2056
2057
2058
2059
2060
2061
2062
2063
2064
2065
2066
2067
2068
2069
2070
2071
2072
2073
2074
2075
2076
2077
2078
2079
2080
2081
2082
2083
2084
2085
2086
2087
2088
2089
2090
2091
2092
2093
2094
2095
2096
2097
2098
2099
2100
2101
2102
2103
2104
2105
2106
2107
2108
2109
2110
2111
2112
2113
2114
2115
2116
2117
2118
2119
2120
2121
2122
2123
2124
2125
2126
2127
2128
2129
2130
2131
2132
2133
2134
2135
2136
2137
2138
2139
2140
2141
2142
2143
2144
2145
2146
2147
2148
2149
2150
2151
2152
2153
2154
2155
2156
2157
2158
2159
2160
2161
2162
2163
2164
2165
2166
2167
2168
2169
2170
2171
2172
2173
2174
2175
2176
2177
2178
2179
2180
2181
2182
2183
2184
2185
2186
2187
2188
2189
2190
2191
2192
2193
2194
2195
2196
2197
2198
2199
2200
2201
2202
2203
2204
2205
2206
2207
2208
2209
2210
2211
2212
2213
2214
2215
2216
2217
2218
2219
2220
2221
2222
2223
2224
2225
2226
2227
2228
2229
2230
2231
2232
2233
2234
2235
2236
2237
2238
2239
2240
2241
2242
2243
2244
2245
2246
2247
2248
2249
2250
2251
2252
2253
2254
2255
2256
2257
2258
2259
2260
2261
2262
2263
2264
2265
2266
2267
2268
2269
2270
2271
2272
2273
2274
2275
2276
2277
2278
2279
2280
2281
2282
2283
2284
2285
2286
2287
2288
2289
2290
2291
2292
2293
2294
2295
2296
2297
2298
2299
2300
2301
2302
2303
2304
2305
2306
2307
2308
2309
2310
2311
2312
2313
2314
2315
2316
2317
2318
2319
2320
2321
2322
2323
2324
2325
2326
2327
2328
2329
2330
2331
2332
2333
2334
2335
2336
2337
2338
2339
2340
2341
2342
2343
2344
2345
2346
2347
2348
2349
2350
2351
2352
2353
2354
2355
2356
2357
2358
2359
2360
2361
2362
2363
2364
2365
2366
2367
2368
2369
2370
2371
2372
2373
2374
2375
2376
2377
2378
2379
2380
2381
2382
2383
2384
2385
2386
2387
2388
2389
2390
2391
2392
2393
2394
2395
2396
2397
2398
2399
2400
2401
2402
2403
2404
2405
2406
2407
2408
2409
2410
2411
2412
2413
2414
2415
2416
2417
2418
2419
2420
2421
2422
2423
2424
2425
2426
2427
2428
2429
2430
2431
2432
2433
2434
2435
2436
2437
2438
2439
2440
2441
2442
2443
2444
2445
2446
2447
2448
2449
2450
2451
2452
2453
2454
2455
2456
2457
2458
2459
2460
2461
2462
2463
2464
2465
2466
2467
2468
2469
2470
2471
2472
2473
2474
2475
2476
2477
2478
2479
2480
2481
2482
2483
2484
2485
2486
2487
2488
2489
2490
2491
2492
2493
2494
2495
2496
2497
2498
2499
2500
2501
2502
2503
2504
2505
2506
2507
2508
2509
2510
2511
2512
2513
2514
2515
2516
2517
2518
2519
2520
2521
2522
2523
2524
2525
2526
2527
2528
2529
2530
2531
2532
2533
2534
2535
2536
2537
2538
2539
2540
2541
2542
2543
2544
2545
2546
2547
2548
2549
2550
2551
2552
2553
2554
2555
2556
2557
2558
2559
2560
2561
2562
2563
2564
2565
2566
2567
2568
2569
2570
2571
2572
2573
2574
2575
2576
2577
2578
2579
2580
2581
2582
2583
2584
2585
2586
2587
2588
2589
2590
2591
2592
2593
2594
2595
2596
2597
2598
2599
2600
2601
2602
2603
2604
2605
2606
2607
2608
2609
2610
//! `llmtrim setup` — the one-command bootstrap. llmtrim is *only* a MITM proxy, so
//! integration is purely at the environment level: it ensures the local CA, then sets
//! `HTTPS_PROXY` + `NODE_EXTRA_CA_CERTS` for the user (POSIX: a managed shell-profile
//! block; Windows: `HKCU\Environment`) so newly-launched tools route through the
//! interceptor and trust the CA — **no IDE settings touched, no sudo** — enables
//! run-at-login, and starts the daemon. On POSIX the block wires the vars only while the
//! daemon is up (gated on `llmtrim _alive`), so a shell opened after `stop` routes directly
//! instead of at a dead proxy; Windows syncs the registry on daemon start/stop instead.
//!
//! Best-effort and idempotent: a step that fails warns and the rest proceeds.

use std::net::{Ipv4Addr, TcpListener};
use std::path::PathBuf;

use anyhow::{Context, Result};

use crate::ui::{self, Tone};

const BEGIN: &str = "# >>> llmtrim >>>";
const END: &str = "# <<< llmtrim <<<";

/// The shell command that clears the interceptor env vars from the *current* shell. A running
/// process can't rewrite its parent shell's environment, so `stop`/`uninstall` print this for
/// the user to run (or they open a new shell, which self-heals via the daemon-gated block).
/// Single source of truth for the five managed vars, in both `NO_PROXY` casings.
pub const UNSET_HINT: &str = "unset HTTPS_PROXY HTTP_PROXY NO_PROXY no_proxy NODE_EXTRA_CA_CERTS SSL_CERT_FILE CURL_CA_BUNDLE";

/// Hosts/ranges that must bypass the interceptor: loopback, link-local, and the private LAN
/// ranges (RFC-1918 + IPv6 ULA). llmtrim only MITMs a fixed set of public LLM API hosts (the
/// CA's domain set — see `serve::should_intercept`), so routing local or LAN traffic through the
/// proxy can only break it: localhost dev servers, intranet, and local device discovery
/// (Plex/DLNA, printers, NAS). Without `NO_PROXY`, *every* proxy-aware program on the machine
/// (not just LLM tools) funnels its local and LAN calls at `127.0.0.1:<port>` and they fail with
/// "couldn't connect" whenever the interceptor is down or on a stale port. Set alongside
/// `HTTPS_PROXY` so the bypass travels with the proxy.
///
/// Portability note: the literal hosts (`localhost`, `127.0.0.1`, `::1`) and the `*.local` suffix
/// are honored by virtually every client. The **CIDR** ranges are not universal — curl and
/// Node/undici match only by exact host or domain suffix, not CIDR (Go and some others honor
/// CIDR). There is no portable way to express "all of 192.168.0.0/16" in `NO_PROXY`, so LAN
/// bypass *by raw IP* is best-effort on non-CIDR clients; loopback/`localhost`/`*.local` is the
/// portable, high-value core. The CIDR entries are harmless where ignored.
const NO_PROXY: &str = "localhost,127.0.0.1,::1,10.0.0.0/8,172.16.0.0/12,192.168.0.0/16,169.254.0.0/16,fd00::/8,*.local";

/// Default interceptor port; the scan for a free port starts here. Chosen to be unassigned
/// by IANA and below the OS ephemeral range (so it isn't grabbed as a transient client port),
/// avoiding clashes with common dev servers. Single source of truth — `main.rs` references it.
pub const DEFAULT_PORT: u16 = 43117;

/// First loopback port that actually binds, scanning `start..=start+span`. A successful bind
/// (immediately dropped) proves the port is usable *right now*; because we accept only `Ok`,
/// this also skips Windows reserved/excluded ranges, which fail the bind with `PermissionDenied`
/// rather than `AddrInUse`. Probes `127.0.0.1` to match exactly what `serve` binds. `None` if the
/// whole window is unusable.
fn first_free_port(start: u16, span: u16) -> Option<u16> {
    (start..=start.saturating_add(span))
        .find(|&p| TcpListener::bind((Ipv4Addr::LOCALHOST, p)).is_ok())
}

/// (pid, process name) of whatever LISTENs on `port`, via the platform's native tools —
/// a cold setup-time path, so shelling out beats adding a process-inspection dependency.
/// Best-effort: any parse failure is `None` and the caller falls back to the plain note.
fn port_holder(port: u16) -> Option<(u32, String)> {
    #[cfg(windows)]
    {
        let out = std::process::Command::new("netstat")
            .args(["-ano"])
            .output()
            .ok()?;
        let text = String::from_utf8_lossy(&out.stdout);
        let needle = format!(":{port}");
        let pid: u32 = text
            .lines()
            .filter(|l| l.contains("LISTENING") && l.contains(&needle))
            .filter_map(|l| l.split_whitespace().last()?.parse().ok())
            .next()?;
        let out = std::process::Command::new("tasklist")
            .args(["/FI", &format!("PID eq {pid}"), "/FO", "CSV", "/NH"])
            .output()
            .ok()?;
        let line = String::from_utf8_lossy(&out.stdout);
        let name = line
            .trim()
            .trim_start_matches('"')
            .split('"')
            .next()?
            .to_string();
        (!name.is_empty()).then_some((pid, name))
    }
    #[cfg(unix)]
    {
        // `ss -ltnp` prints `users:(("name",pid=123,fd=7))`; fall back to lsof.
        if let Ok(out) = std::process::Command::new("ss")
            .args(["-ltnp", &format!("sport = :{port}")])
            .output()
        {
            let text = String::from_utf8_lossy(&out.stdout);
            if let Some(users) = text.split("users:((\"").nth(1) {
                let name = users.split('"').next().unwrap_or_default().to_string();
                if let Some(pid) = users.split("pid=").nth(1).and_then(|s| {
                    s.chars()
                        .take_while(char::is_ascii_digit)
                        .collect::<String>()
                        .parse()
                        .ok()
                }) {
                    return Some((pid, name));
                }
            }
        }
        let out = std::process::Command::new("lsof")
            .args(["-nP", &format!("-iTCP:{port}"), "-sTCP:LISTEN", "-Fpc"])
            .output()
            .ok()?;
        let text = String::from_utf8_lossy(&out.stdout);
        let pid = text.lines().find(|l| l.starts_with('p'))?[1..]
            .parse()
            .ok()?;
        let name = text.lines().find(|l| l.starts_with('c'))?[1..].to_string();
        Some((pid, name))
    }
}

/// Force-kill a process we identified as an orphaned llmtrim. Best-effort by design.
fn kill_pid(pid: u32) {
    #[cfg(windows)]
    {
        let _ = std::process::Command::new("taskkill")
            .args(["/PID", &pid.to_string(), "/F"])
            .output();
    }
    #[cfg(unix)]
    {
        let _ = std::process::Command::new("kill")
            .args(["-9", &pid.to_string()])
            .output();
    }
}

/// Outcome of resolving which port to wire: a definite port to use, or a starting point to
/// scan from for a free one. Split out so the precedence is pure and unit-testable.
#[derive(Debug, PartialEq, Eq)]
enum PortChoice {
    /// Use exactly this port (caller does not scan).
    Use(u16),
    /// No port is pinned — scan upward from here for the first free one.
    ScanFrom(u16),
}

/// Decide the interceptor port, in precedence order — *without* scanning, so it's pure:
///
/// 1. an explicit `--port` (honor the user verbatim),
/// 2. the port a live llmtrim daemon is already serving (reuse it — never migrate a running
///    proxy off the port its clients point at),
/// 3. the port already wired into the environment (`HTTPS_PROXY`), so re-running converges
///    on what existing shells expect,
/// 4. otherwise scan from the default.
///
/// Steps 2–3 are why re-running `setup` is now idempotent: the old code scanned from 8787
/// every time, and since the running daemon *held* 8787 the scan skipped to 8788 — each
/// re-run drifted the port upward and rewrote the env/autostart to match, breaking every
/// already-launched client. Reusing the live/recorded port stops that.
fn choose_port(explicit: Option<u16>, running: Option<u16>, configured: Option<u16>) -> PortChoice {
    if let Some(p) = explicit.or(running).or(configured) {
        PortChoice::Use(p)
    } else {
        PortChoice::ScanFrom(DEFAULT_PORT)
    }
}

/// Resolve the port to wire, scanning for a free one only when nothing is pinned (the
/// first-install case). `running` is the live daemon's port, if any. Used by `setup` and the
/// `start` command so both agree on the same port without drifting.
pub fn resolve_port(explicit: Option<u16>, running: Option<u16>) -> Result<u16> {
    match choose_port(explicit, running, configured_port()) {
        PortChoice::Use(p) => Ok(p),
        PortChoice::ScanFrom(start) => first_free_port(start, 64)
            .with_context(|| format!("no free port in {start}..={}", start.saturating_add(64))),
    }
}

/// Extract the port from a local proxy URL embedded anywhere in `text` — i.e. the number
/// right after `127.0.0.1:`. Lets us read back the port we previously wired into the env
/// (the shell-profile block on POSIX, `HKCU\Environment\HTTPS_PROXY` on Windows). Pure.
pub(crate) fn parse_proxy_port(text: &str) -> Option<u16> {
    let after = text.split("127.0.0.1:").nth(1)?;
    let digits: String = after.chars().take_while(char::is_ascii_digit).collect();
    digits.parse().ok()
}

/// The interceptor port currently wired into the environment, if any — read from the live env
/// source for this platform (POSIX: the shell-profile block; Windows: `HKCU\Environment`).
/// Public so `status`/`doctor` can compare the wired port against the daemon's.
pub fn configured_port() -> Option<u16> {
    #[cfg(windows)]
    {
        user_env_key()
            .ok()
            .and_then(|env| env.get_value::<String, _>("HTTPS_PROXY").ok())
            .and_then(|v| parse_proxy_port(&v))
    }
    #[cfg(not(windows))]
    {
        profile_target()
            .and_then(|(p, _)| std::fs::read_to_string(p).ok())
            .and_then(|t| parse_proxy_port(&t))
    }
}

/// Self-heal an existing managed env block that predates the `NO_PROXY` bypass, *in place* and
/// without re-running `setup`. Called once when the daemon comes up at login: an install wired
/// before the `NO_PROXY` fix has `HTTPS_PROXY` set but no bypass, so every proxy-aware app funnels
/// its LAN/local traffic at the dead-for-LAN interceptor. We rewrite only blocks that already
/// exist and lack the bypass — never create a block (that's `setup`'s job), never touch an install
/// that's already current. Best-effort by design: a heal failure must never stop the proxy serving.
///
/// Note the OS limit this can't beat: a process inherits its env at launch, so already-running
/// apps (Plex, open shells) keep the old, bypass-less env until they restart. This fixes what
/// starts *after* the heal; the one-time restart of running apps is unavoidable.
pub fn heal_managed_env() -> Result<Vec<PathBuf>> {
    #[cfg(windows)]
    {
        let env = user_env_key()?;
        // Only heal a wired install that predates the bypass: HTTPS_PROXY present, NO_PROXY absent.
        if !has_proxy_in(&env) || env.get_value::<String, _>("NO_PROXY").is_ok() {
            return Ok(vec![]);
        }
        let proxy: String = env.get_value("HTTPS_PROXY").context("read HTTPS_PROXY")?;
        let ca: String = env.get_value("NODE_EXTRA_CA_CERTS").unwrap_or_default();
        set_env_in(&env, &proxy, &ca)?; // re-set adds NO_PROXY alongside the existing values
        broadcast_env_change();
        Ok(vec![PathBuf::from("HKCU\\Environment")])
    }
    #[cfg(not(windows))]
    {
        let Ok(home) = std::env::var("HOME") else {
            return Ok(vec![]);
        };
        // Reconstruct the env the block should now carry from what's already wired.
        let Some(port) = configured_port() else {
            return Ok(vec![]); // nothing wired → nothing to heal
        };
        let proxy = format!("http://127.0.0.1:{port}");
        let ca_path = crate::serve::ca_cert_path()?;
        let ca = ca_path.to_string_lossy().into_owned();
        // Best-effort: a bundle build failure just means the heal keeps the Node-only trust.
        let bundle = ensure_ca_bundle(&ca_path).ok().flatten();
        let bundle_str = bundle.as_ref().map(|p| p.to_string_lossy().into_owned());
        heal_profiles_in(
            std::path::Path::new(&home),
            &proxy,
            &ca,
            bundle_str.as_deref(),
        )
    }
}

/// Inner seam for [`heal_managed_env`] (POSIX), against `base` as the home dir so tests stay
/// hermetic. Rewrites the managed block only in profiles that already contain a healable one (see
/// [`managed_block_needs_heal`]); returns the paths actually rewritten.
/// Does `s` contain a *well-formed* managed block (`BEGIN`…`END`) that predates the current block
/// shape and should be rewritten? A block is healable when it lacks either the `NO_PROXY` bypass
/// or the daemon-liveness gate (`llmtrim _alive`) — both are silent rewrites into the current
/// form, and the second is what migrates every pre-gate install so `stop` unwires new shells
/// without the user re-running `setup`. Only a well-formed block qualifies: a malformed
/// BEGIN-without-END is skipped (rewriting it would stack a duplicate), an already-current block
/// (both markers present inside it) is skipped, and a `NO_PROXY`/guard the user placed *outside*
/// the markers does not count — it must be inside the block we manage. Pure/testable.
#[cfg(not(windows))]
fn managed_block_needs_heal(s: &str) -> bool {
    let (mut in_block, mut saw_begin, mut saw_end, mut bypass_in_block, mut gate_in_block) =
        (false, false, false, false, false);
    for line in s.lines() {
        match line.trim() {
            BEGIN => {
                in_block = true;
                saw_begin = true;
            }
            END => {
                if in_block {
                    saw_end = true;
                }
                in_block = false;
            }
            l if in_block && l.contains("llmtrim _alive") => gate_in_block = true,
            l if in_block && l.contains("NO_PROXY") => bypass_in_block = true,
            _ => {}
        }
    }
    saw_begin && saw_end && (!bypass_in_block || !gate_in_block)
}

#[cfg(not(windows))]
fn heal_profiles_in(
    base: &std::path::Path,
    proxy: &str,
    ca: &str,
    bundle: Option<&str>,
) -> Result<Vec<PathBuf>> {
    let block = env_block(proxy, ca, bundle, Syntax::Posix);
    let mut healed = Vec::new();
    for path in candidate_profiles(base) {
        let existing = match std::fs::read_to_string(&path) {
            Ok(s) => s,
            Err(_) => continue, // absent or unreadable — skip
        };
        // Heal only a well-formed old block (markers present, bypass missing *inside the block*).
        // Scoping the check to the block — not the whole file — means a user's own `NO_PROXY`
        // elsewhere can't mask a stale managed block; refusing a BEGIN-without-END block avoids
        // strip_block returning the file unchanged and us stacking a second block onto it.
        if !managed_block_needs_heal(&existing) {
            continue;
        }
        let mut base_content = strip_block(&existing);
        if !base_content.is_empty() && !base_content.ends_with('\n') {
            base_content.push('\n');
        }
        std::fs::write(&path, format!("{base_content}{block}"))
            .with_context(|| format!("failed to write {}", path.display()))?;
        healed.push(path);
    }
    Ok(healed)
}

pub fn run(requested: Option<u16>, force: bool) -> Result<()> {
    let color = ui::color_stdout();

    // 0. Resolve the port *once*, here, before anything is wired. The port is a contract
    //    between three parties that must agree: the profile's HTTPS_PROXY (clients), the
    //    autostart entry (`serve --port N` at login), and the daemon that binds it. We reuse
    //    the port a live daemon already serves (or one already wired into the env) instead of
    //    scanning blindly — otherwise the running daemon holds 8787, the scan drifts to 8788,
    //    and every re-run rewrites the env/autostart to a new port, breaking running clients.
    let running = crate::daemon::running();
    let running_port = running.as_ref().map(|s| s.port);
    let configured = configured_port();
    let pinned = requested.or(running_port).or(configured);
    let mut port = match choose_port(requested, running_port, configured) {
        PortChoice::Use(p) => p,
        PortChoice::ScanFrom(start) => first_free_port(start, 64)
            .with_context(|| format!("no free port in {start}..={}", start.saturating_add(64)))?,
    };
    // Only chatter about the port when we had to pick one nobody asked for (first install,
    // default busy). When we're reusing a pinned port, silence is correct.
    if pinned.is_none() && port != DEFAULT_PORT {
        // Who holds the default? An orphaned llmtrim (binary replaced/removed while the
        // old daemon kept running — e.g. `npm uninstall` can't stop it) is reclaimed
        // instead of silently drifting ports and stranding the zombie.
        match port_holder(DEFAULT_PORT) {
            Some((pid, name)) if name.to_lowercase().contains("llmtrim") => {
                kill_pid(pid);
                // Give the OS a moment to release the socket, then take the default back.
                for _ in 0..20 {
                    std::thread::sleep(std::time::Duration::from_millis(100));
                    if std::net::TcpListener::bind(("127.0.0.1", DEFAULT_PORT)).is_ok() {
                        port = DEFAULT_PORT;
                        break;
                    }
                }
                if port == DEFAULT_PORT {
                    println!(
                        "{}",
                        ui::note(
                            color,
                            &format!(
                                "Stopped an orphaned llmtrim daemon (pid {pid}) holding port {DEFAULT_PORT}."
                            )
                        )
                    );
                } else {
                    println!(
                        "{}",
                        ui::note(
                            color,
                            &format!(
                                "Port {DEFAULT_PORT} held by an old llmtrim (pid {pid}) that wouldn't die — using {port}."
                            )
                        )
                    );
                }
            }
            Some((pid, name)) => {
                println!(
                    "{}",
                    ui::note(
                        color,
                        &format!("Port {DEFAULT_PORT} busy ({name}, pid {pid}) — using {port}.")
                    )
                );
            }
            None => {
                println!(
                    "{}",
                    ui::note(color, &format!("Port {DEFAULT_PORT} busy — using {port}."))
                );
            }
        }
    }

    // Steps are collected as checklist rows and rendered as one summary panel at the
    // end; soft failures become `⚠` rows instead of stderr asides, so the user sees
    // one coherent report.
    let mut rows: Vec<(&str, String, String)> = Vec::new();

    // 1. Local CA (generated on first run, name-constrained to LLM domains).
    crate::serve::ensure_ca()?;
    let ca_path = crate::serve::ca_cert_path()?;
    let ca = ca_path.to_string_lossy().to_string();
    let proxy = format!("http://127.0.0.1:{port}");
    rows.push((ui::OK, "Local CA".into(), ca.clone()));

    // 1b. Combined bundle (OS roots + CA) for native TLS clients that ignore NODE_EXTRA_CA_CERTS
    //     (curl, git, Python, rustls tools like OpenAI Codex). POSIX only; Windows uses the OS
    //     cert store. Best-effort: a missing OS root bundle just skips SSL_CERT_FILE.
    #[cfg(not(windows))]
    let bundle = match ensure_ca_bundle(&ca_path) {
        Ok(Some(p)) => {
            rows.push((ui::OK, "CA bundle".into(), p.to_string_lossy().into_owned()));
            Some(p.to_string_lossy().into_owned())
        }
        Ok(None) => {
            rows.push((
                ui::NOTE,
                "CA bundle".into(),
                "no OS root bundle found — native TLS clients use the OS trust store".into(),
            ));
            None
        }
        Err(e) => {
            rows.push((ui::WARN, "CA bundle".into(), format!("not built: {e}")));
            None
        }
    };

    // 2. Route + trust at the environment level.
    //
    // POSIX: a managed block in the shell rc file (`export …`).
    // Windows: the *user environment* in `HKCU\Environment`, NOT a shell profile — a profile
    //   only helps PowerShell, and ExecutionPolicy can stop it loading entirely (the silent
    //   "no traffic" trap). The registry is read by every process at launch (PS5, pwsh7, Git
    //   Bash, cmd, GUI apps alike), independent of any profile running.
    #[cfg(windows)]
    {
        set_user_env(&proxy, &ca)?;
        rows.push((
            ui::OK,
            "Environment".into(),
            "HKCU\\Environment — HTTPS_PROXY + CA trust".into(),
        ));
        // Upgrade path: drop any legacy managed block a previous version wrote to the
        // PowerShell profile, so a dead (possibly ExecutionPolicy-blocked) block isn't
        // left behind.
        if let Ok(paths) = remove_profile_block() {
            for path in paths {
                rows.push((
                    ui::OK,
                    "Profile".into(),
                    format!("legacy env block removed from {}", path.display()),
                ));
            }
        }
        // Tell Explorer to re-read the environment so freshly-launched terminals/editors
        // inherit it without a logout (a raw registry write alone is invisible to running
        // processes).
        broadcast_env_change();
    }
    #[cfg(not(windows))]
    let manual_env = {
        let paths = write_profile_block(&proxy, &ca, bundle.as_deref())?;
        if paths.is_empty() {
            rows.push((
                ui::NOTE,
                "Profile".into(),
                "no shell profile found — set the env yourself (below)".into(),
            ));
            true
        } else {
            let names = paths
                .iter()
                .filter_map(|p| p.file_name().map(|n| n.to_string_lossy().into_owned()))
                .collect::<Vec<_>>()
                .join(", ");
            rows.push((
                ui::OK,
                "Profile".into(),
                format!("{names} — HTTPS_PROXY + CA trust"),
            ));
            false
        }
    };

    // 3. Run at login (systemd / launchd / Windows, via auto-launch).
    match crate::autostart::configure(true, port) {
        Ok(()) => rows.push((ui::OK, "Autostart".into(), "runs at login".into())),
        Err(e) => rows.push((ui::WARN, "Autostart".into(), format!("not enabled: {e}"))),
    }

    // 3b. Desktop tray. Only offered when the GUI binary is installed next to the
    //     CLI (the desktop bundles ship it; a headless `cargo install` doesn't),
    //     which is also how "only on a desktop OS" is enforced. Default is on;
    //     `--force` / a non-interactive setup take that default without asking.
    if crate::tray::tray_binary().is_some() {
        let want = force || confirm_tray_default_yes();
        match crate::autostart::configure_tray(want) {
            Ok(()) if want => rows.push((
                ui::OK,
                "Tray".into(),
                "opens at login · run `llmtrim tray` to open now".into(),
            )),
            Ok(()) => rows.push((
                ui::OK,
                "Tray".into(),
                "left disabled · enable later with `llmtrim autostart --tray`".into(),
            )),
            Err(e) => rows.push((ui::WARN, "Tray".into(), format!("not enabled: {e}"))),
        }
    }

    // 4. Reconcile the interceptor. If a healthy daemon is already serving the resolved port,
    //    leave it running — re-running `setup` must not drop in-flight requests (the old code
    //    stopped + respawned unconditionally on every run). Restart only when the port is
    //    changing (explicit `--port`, or self-healing a drifted state) or the daemon is gone —
    //    that also picks up a new binary after an update (the silent-stale-update trap).
    let daemon_ok = match &running {
        // `--force` falls through to the restart arm even on a matching port (e.g. to pick up a
        // freshly installed binary); without it a healthy same-port daemon is left untouched.
        Some(state) if state.port == port && !force => {
            rows.push((
                ui::OK,
                "Interceptor".into(),
                format!("already running · pid {} · port {port}", state.pid),
            ));
            true
        }
        _ => {
            // Clear a dead/old-port daemon (or the --force target) and wait for the port to free
            // before respawning, so the new daemon doesn't lose the bind race. A timeout here is
            // surfaced (not silently swallowed) so a confusing spawn failure isn't a mystery.
            if let Ok(false) = crate::daemon::stop_and_wait_free(port) {
                rows.push((
                    ui::WARN,
                    "Interceptor".into(),
                    format!("port {port} still held 5s after stop; starting anyway"),
                ));
            }
            match crate::daemon::spawn_detached(port) {
                Ok(pid) => {
                    rows.push((
                        ui::OK,
                        "Interceptor".into(),
                        format!("running · pid {pid} · port {port}"),
                    ));
                    true
                }
                Err(e) => {
                    rows.push((ui::WARN, "Interceptor".into(), format!("not started: {e}")));
                    false
                }
            }
        }
    };

    print!(
        "{}",
        ui::panel(color, "llmtrim setup", &ui::kv_rows(color, &rows))
    );

    // On Windows the env is written to the registry above, never manually.
    #[cfg(not(windows))]
    if manual_env {
        println!();
        println!("Export these in your shell yourself:");
        println!("    export HTTPS_PROXY={proxy}");
        println!("    export NO_PROXY={NO_PROXY}");
        println!("    export NODE_EXTRA_CA_CERTS={ca}");
        if let Some(b) = bundle.as_deref() {
            println!("    export SSL_CERT_FILE={b}");
            println!("    export CURL_CA_BUNDLE={b}");
        }
    }

    // The env only reaches *future* processes — already-running tools (editors, Claude
    // Code, open terminals) keep their old environment until relaunched. Spell that
    // out: it's the #1 "why don't I see any traffic?" confusion.
    let check = if cfg!(windows) {
        "echo $env:HTTPS_PROXY"
    } else {
        "echo $HTTPS_PROXY"
    };
    println!();
    if daemon_ok {
        println!(
            "{}",
            ui::paint(color, Tone::Bold, "Done — the interceptor is running.")
        );
    } else {
        println!(
            "{}",
            ui::warn(
                color,
                "Setup finished, but the interceptor is not running — see above."
            )
        );
    }
    println!(
        "Only programs started after this pick up the proxy env; already-running\n\
         tools (your editor, Claude Code, open terminals) keep their old environment\n\
         until relaunched. To route one through llmtrim:"
    );
    println!();
    let new_shell = if cfg!(windows) {
        "open a new terminal (any shell — the env is set for your whole user)"
    } else {
        "open a new terminal (or re-source your shell profile)"
    };
    println!("  1. {new_shell}");
    println!("  2. verify it took:  {check}  →  {proxy}");
    println!("  3. launch your tool from that shell");
    println!();
    println!(
        "  {}  llmtrim status",
        ui::paint(color, Tone::Dim, "watch savings")
    );
    // Claude Code users only: hint at the status line without writing their config (setup is
    // client-agnostic and never touches IDE settings). No-op for users of other agents.
    if crate::statusline::claude_code_present() {
        println!(
            "  {}  llmtrim statusline install",
            ui::paint(color, Tone::Dim, "status line  ")
        );
    }
    #[cfg(windows)]
    println!(
        "{}",
        ui::note(
            color,
            &format!(
                "For GUI apps that pin their own trust store, trust the CA system-wide: \
                 certutil -addstore -user Root \"{ca}\" — or see llmtrim ca."
            )
        )
    );
    #[cfg(not(windows))]
    println!(
        "{}",
        ui::note(
            color,
            "GUI apps that ignore the shell env need the CA trusted system-wide — see llmtrim ca."
        )
    );

    // caveman (the output-compression skill) shapes model output on every request. llmtrim's
    // `auto` already shapes output where it pays (code / long context / plain prose), and
    // deliberately skips it on agent traffic — tool-call replies are already short,
    // so terse shaping there saves no tokens (bench/README glaive: cost ~-5%, quality +0pp).
    // So caveman is redundant where output shaping helps and a no-op win where it doesn't.
    // Warn and print caveman's *own* uninstall commands — never run them ourselves: removing
    // another tool is the user's call, not setup's.
    if caveman_installed() {
        println!();
        println!(
            "{}",
            ui::warn(
                color,
                "caveman detected. It shapes model output on every request. llmtrim's auto \
                 mode already shapes output where it pays (code, long context, plain prose) \
                 and skips it on tool-calling agent traffic on purpose: tool-call replies are \
                 already short, so terse shaping there saves no tokens (our bench measured \
                 quality neutral). So caveman adds nothing llmtrim does not already do where \
                 it helps. To remove it:"
            )
        );
        #[cfg(not(windows))]
        println!(
            "    bash <(curl -s https://raw.githubusercontent.com/JuliusBrussee/caveman/main/src/hooks/uninstall.sh)"
        );
        #[cfg(windows)]
        println!(
            "    irm https://raw.githubusercontent.com/JuliusBrussee/caveman/main/src/hooks/uninstall.ps1 | iex"
        );
        println!("    claude plugin disable caveman        # if installed as a Claude Code plugin");
        println!("    npx skills remove caveman            # Cursor, Windsurf, Cline, Copilot, …");
        println!("    gemini extensions uninstall caveman  # Gemini CLI");
    }
    Ok(())
}

/// Ask whether to enable the desktop tray, defaulting to yes. Returns `true`
/// (the default) without asking when stdin isn't a terminal, so a scripted
/// `setup` stays non-interactive and still enables the tray.
fn confirm_tray_default_yes() -> bool {
    use std::io::{IsTerminal, Write};
    if !std::io::stdin().is_terminal() {
        return true;
    }
    print!("Enable the llmtrim desktop tray (opens at login)? [Y/n] ");
    let _ = std::io::stdout().flush();
    let mut line = String::new();
    if std::io::stdin().read_line(&mut line).is_err() {
        // The prompt couldn't be completed (broken pipe, closed stdin). Decline
        // rather than enable an optional component the user never confirmed.
        return false;
    }
    !matches!(line.trim().to_ascii_lowercase().as_str(), "n" | "no")
}

/// Best-effort caveman detection: the flag file its session hook writes, its standalone
/// hook files, or a Claude Code plugin-cache entry. Read-only probes; any I/O failure
/// reads as "not installed" — setup must never fail because of someone else's tool.
fn caveman_installed() -> bool {
    let claude_dir = std::env::var("CLAUDE_CONFIG_DIR")
        .map(PathBuf::from)
        .ok()
        .or_else(|| {
            std::env::var("HOME")
                .or_else(|_| std::env::var("USERPROFILE"))
                .ok()
                .map(|h| PathBuf::from(h).join(".claude"))
        });
    let Some(dir) = claude_dir else {
        return false;
    };
    caveman_installed_in(&dir)
}

/// The detection itself, on an explicit Claude config dir (testable without env games).
fn caveman_installed_in(dir: &std::path::Path) -> bool {
    if dir.join(".caveman-active").is_file()
        || dir.join("hooks").join("caveman-activate.js").is_file()
        || dir.join("hooks").join("caveman-config.js").is_file()
    {
        return true;
    }
    // Plugin install: a "caveman" entry somewhere shallow in the plugin cache.
    fn walk(p: &std::path::Path, depth: u8) -> bool {
        if depth == 0 {
            return false;
        }
        let Ok(rd) = std::fs::read_dir(p) else {
            return false;
        };
        for e in rd.flatten() {
            if e.file_name().to_string_lossy().contains("caveman") {
                return true;
            }
            let path = e.path();
            if path.is_dir() && walk(&path, depth - 1) {
                return true;
            }
        }
        false
    }
    walk(&dir.join("plugins"), 3)
}

/// `llmtrim uninstall` — the transparent inverse of `setup`: stop the daemon, disable
/// autostart, strip the shell-profile block, and remove the CA + state (and, unless told
/// otherwise, the binary itself). Best-effort: a failed step becomes a `⚠` row and the
/// rest proceeds; every action lands in the summary panel, nothing is silent.
pub fn uninstall(purge: bool, keep_binary: bool) -> Result<()> {
    let color = ui::color_stdout();
    let mut rows: Vec<(&str, String, String)> = Vec::new();

    // 1. Stop the running daemon.
    match crate::daemon::stop() {
        Ok(Some(pid)) => rows.push((ui::OK, "Interceptor".into(), format!("stopped (pid {pid})"))),
        Ok(None) => rows.push((
            ui::NOTE,
            "Interceptor".into(),
            "no daemon was running".into(),
        )),
        Err(e) => rows.push((
            ui::WARN,
            "Interceptor".into(),
            format!("could not stop: {e}"),
        )),
    }

    // 2. Disable run-at-login (matched by app name, so the port is irrelevant here).
    match crate::autostart::configure(false, DEFAULT_PORT) {
        Ok(()) => rows.push((ui::OK, "Autostart".into(), "disabled".into())),
        Err(e) => rows.push((ui::WARN, "Autostart".into(), format!("not changed: {e}"))),
    }

    // 2b. Disable the tray's separate login entry too, so uninstall leaves nothing
    //     that revives a GUI at next login.
    match crate::autostart::configure_tray(false) {
        Ok(()) => rows.push((ui::OK, "Tray autostart".into(), "disabled".into())),
        Err(e) => rows.push((
            ui::WARN,
            "Tray autostart".into(),
            format!("not changed: {e}"),
        )),
    }

    // 3. Remove the interceptor env. Windows: the `HKCU\Environment` values (plus any legacy
    //    profile block a prior version left). POSIX: the managed block in the shell rc file.
    #[cfg(windows)]
    {
        match clear_user_env() {
            Ok(true) => rows.push((
                ui::OK,
                "Environment".into(),
                "interceptor env removed from HKCU\\Environment".into(),
            )),
            Ok(false) => rows.push((
                ui::NOTE,
                "Environment".into(),
                "no interceptor env to remove".into(),
            )),
            Err(e) => rows.push((ui::WARN, "Environment".into(), format!("not cleaned: {e}"))),
        }
        if let Ok(paths) = remove_profile_block() {
            for path in paths {
                rows.push((
                    ui::OK,
                    "Profile".into(),
                    format!("legacy env block removed from {}", path.display()),
                ));
            }
        }
        // Refresh Explorer's environment so new processes stop seeing the removed values.
        broadcast_env_change();
    }
    #[cfg(not(windows))]
    match remove_profile_block() {
        Ok(paths) if paths.is_empty() => {
            rows.push((ui::NOTE, "Profile".into(), "no env block to remove".into()))
        }
        Ok(paths) => {
            for path in paths {
                rows.push((
                    ui::OK,
                    "Profile".into(),
                    format!("env block removed from {}", path.display()),
                ));
            }
        }
        Err(e) => rows.push((ui::WARN, "Profile".into(), format!("not cleaned: {e}"))),
    }

    // 4. Remove the CA + daemon state (~/.llmtrim).
    let home = crate::daemon::home_dir()?;
    if home.exists() {
        match std::fs::remove_dir_all(&home) {
            Ok(()) => rows.push((
                ui::OK,
                "State".into(),
                format!("removed {} (CA, key, daemon state)", home.display()),
            )),
            Err(e) => rows.push((
                ui::WARN,
                "State".into(),
                format!("could not remove {}: {e}", home.display()),
            )),
        }
    } else {
        rows.push((
            ui::NOTE,
            "State".into(),
            "no state directory to remove".into(),
        ));
    }

    // 4b. Untrust the CA from the OS trust store. Deleting ca.pem (step 4) leaves any
    //     system-wide trust (`llmtrim ca` → certutil -addstore) dangling — a keyless root CA
    //     still trusted. Pull it out so uninstall actually reverses `llmtrim ca`.
    #[cfg(windows)]
    match untrust_ca_root() {
        Ok(true) => rows.push((
            ui::OK,
            "Trust".into(),
            "removed the CA from your user Root store".into(),
        )),
        Ok(false) => rows.push((
            ui::NOTE,
            "Trust".into(),
            "CA was not trusted system-wide".into(),
        )),
        Err(e) => rows.push((ui::WARN, "Trust".into(), format!("not untrusted: {e}"))),
    }
    #[cfg(target_os = "macos")]
    match untrust_ca_keychain() {
        Ok(true) => rows.push((
            ui::OK,
            "Trust".into(),
            "removed the CA from your login keychain".into(),
        )),
        Ok(false) => rows.push((
            ui::NOTE,
            "Trust".into(),
            "CA was not in the login keychain".into(),
        )),
        Err(e) => rows.push((ui::WARN, "Trust".into(), format!("not untrusted: {e}"))),
    }

    // 5. The savings ledger — kept by default (it's your history), removed with --purge.
    match crate::tracking::db_path() {
        Ok(db) if db.exists() && purge => {
            std::fs::remove_file(&db).ok();
            rows.push((ui::OK, "Ledger".into(), format!("removed {}", db.display())));
        }
        Ok(db) if db.exists() => {
            rows.push((
                ui::NOTE,
                "Ledger".into(),
                format!("kept {} (use --purge to remove)", db.display()),
            ));
        }
        _ => {}
    }

    // 6. The binary itself (Unix can unlink a running executable; Windows can't).
    // Package-manager-owned binaries are NOT deleted: removing the file out from under
    // npm/cargo/brew leaves their bookkeeping broken — print their command instead.
    let manager_cmd = match crate::update::channel() {
        crate::update::Channel::Npm => Some("npm uninstall -g @llmtrim/cli"),
        crate::update::Channel::Cargo => Some("cargo uninstall llmtrim"),
        crate::update::Channel::Homebrew => Some("brew uninstall llmtrim"),
        crate::update::Channel::Binary => None,
    };
    if keep_binary {
        rows.push((ui::NOTE, "Binary".into(), "kept".into()));
    } else if let Some(cmd) = manager_cmd {
        rows.push((
            ui::NOTE,
            "Binary".into(),
            format!("owned by your package manager — finish with: {cmd}"),
        ));
    } else if let Ok(exe) = std::env::current_exe() {
        #[cfg(unix)]
        {
            std::fs::remove_file(&exe).ok();
            rows.push((
                ui::OK,
                "Binary".into(),
                format!("removed {}", exe.display()),
            ));
        }
        // Windows can't unlink a running .exe. But we CAN stop `llmtrim` resolving as a
        // command — drop the installer's bin dir from the user PATH — and schedule the
        // install dir's removal after we exit. Only for installer builds (exe under
        // %LOCALAPPDATA%\llmtrim); a cargo/dev binary elsewhere is left untouched.
        #[cfg(windows)]
        {
            match remove_bin_dir_from_path() {
                Ok(true) => rows.push((
                    ui::OK,
                    "PATH".into(),
                    "removed the llmtrim bin dir from your user PATH".into(),
                )),
                Ok(false) => {}
                Err(e) => rows.push((ui::WARN, "PATH".into(), format!("not cleaned: {e}"))),
            }
            if let Some(dir) = installer_dir_of(&exe) {
                schedule_dir_removal(&dir);
                rows.push((
                    ui::OK,
                    "Binary".into(),
                    format!("scheduled removal of {} after exit", dir.display()),
                ));
            } else {
                // Not an installer build (e.g. `cargo install` → ~/.cargo/bin). A running
                // .exe can't unlink itself, so schedule the lone file's deletion after exit.
                schedule_file_removal(&exe);
                rows.push((
                    ui::OK,
                    "Binary".into(),
                    format!("scheduled removal of {} after exit", exe.display()),
                ));
            }
            broadcast_env_change(); // re-broadcast so the dropped PATH entry takes effect
        }
        #[cfg(all(not(unix), not(windows)))]
        {
            rows.push((
                ui::NOTE,
                "Binary".into(),
                format!("remove manually: {}", exe.display()),
            ));
        }
    }

    print!(
        "{}",
        ui::panel(color, "llmtrim uninstall", &ui::kv_rows(color, &rows))
    );
    println!();
    println!(
        "{}",
        ui::paint(
            color,
            Tone::Bold,
            &format!(
                "Done. Your current shell still has HTTPS_PROXY, HTTP_PROXY, NO_PROXY, and \
                 NODE_EXTRA_CA_CERTS exported. Open a new shell to clear them, or run: {UNSET_HINT}"
            )
        )
    );
    // The env is gone from disk, but processes that were already running inherited it at
    // launch and keep the now-dead `127.0.0.1:<port>` proxy until they restart — on Windows
    // that includes GUI apps and services (browsers, media servers like Plex), which fail
    // every outbound request with "couldn't connect to 127.0.0.1". We can't reach into a
    // running process's environment, so the only honest fix is: restart them, or reboot.
    #[cfg(windows)]
    println!(
        "{}",
        ui::note(
            color,
            "Apps already running (browsers, Plex/media servers, anything networked) keep the \
             old proxy until you restart them — reboot to clear them all at once."
        )
    );
    #[cfg(not(windows))]
    println!(
        "{}",
        ui::note(
            color,
            "If you trusted the CA system-wide manually, remove it from your OS trust store."
        )
    );
    Ok(())
}

/// Every POSIX shell rc file llmtrim may write its managed block into. Setup writes the
/// block to all of these that exist (so whichever shell the terminal launches picks up the
/// env, independent of `$SHELL`); uninstall sweeps the block from all of them. Covers the
/// interactive AND login rc of the two common shells, plus the universal `.profile`:
/// zsh interactive `.zshrc` / login `.zprofile`; bash interactive `.bashrc` / login
/// `.bash_profile`; and `.profile` (sh, and bash-login when no `.bash_profile`). `base` is
/// the home directory; tests pass a temp dir so no real `$HOME` is ever mutated.
#[cfg(not(windows))]
fn candidate_profiles(base: &std::path::Path) -> Vec<PathBuf> {
    [
        ".zshrc",
        ".zprofile",
        ".bashrc",
        ".bash_profile",
        ".profile",
    ]
    .iter()
    .map(|f| base.join(f))
    .collect()
}

/// Strip the llmtrim managed block from **every** POSIX shell profile that contains it,
/// using `base` as the home directory. Returns the paths that were actually cleaned.
/// A file that does not exist or cannot be read is silently skipped; a write failure is
/// returned as an error so the caller can report it. Windows: always returns `Ok(vec![])`.
#[cfg_attr(windows, allow(dead_code))]
fn remove_profile_block_in(base: &std::path::Path) -> Result<Vec<PathBuf>> {
    #[cfg(windows)]
    {
        let _ = base;
        Ok(vec![])
    }
    #[cfg(not(windows))]
    {
        let mut cleaned = Vec::new();
        for path in candidate_profiles(base) {
            let existing = match std::fs::read_to_string(&path) {
                Ok(s) => s,
                Err(_) => continue, // absent or unreadable — skip
            };
            if !existing.contains(BEGIN) {
                continue;
            }
            std::fs::write(&path, strip_block(&existing))
                .with_context(|| format!("failed to write {}", path.display()))?;
            cleaned.push(path);
        }
        Ok(cleaned)
    }
}

/// Strip the llmtrim managed block from all candidate shell profiles under `$HOME`.
/// Thin `$HOME`-reading wrapper around [`remove_profile_block_in`].
/// On Windows only deals with any legacy PowerShell profile block (registry is the live path).
fn remove_profile_block() -> Result<Vec<PathBuf>> {
    #[cfg(windows)]
    {
        // Windows live env is the registry; this only handles a legacy profile block that a
        // prior POSIX-style version may have written to the PowerShell profile.
        let Some((path, _)) = profile_target() else {
            return Ok(vec![]);
        };
        let existing = match std::fs::read_to_string(&path) {
            Ok(s) => s,
            Err(_) => return Ok(vec![]),
        };
        if !existing.contains(BEGIN) {
            return Ok(vec![]);
        }
        std::fs::write(&path, strip_block(&existing))
            .with_context(|| format!("failed to write {}", path.display()))?;
        Ok(vec![path])
    }
    #[cfg(not(windows))]
    {
        let Ok(home) = std::env::var("HOME") else {
            return Ok(vec![]);
        };
        remove_profile_block_in(std::path::Path::new(&home))
    }
}

/// Has `setup` wired the interceptor env for this user? Windows reads the `HKCU\Environment`
/// value; POSIX reports whether the managed block exists in any candidate shell profile. Note
/// that on POSIX block *presence* no longer implies the env is actively exported: the block is
/// gated on `llmtrim _alive`, so it can be present yet inert while the daemon is down. Callers
/// (`start`, `wrap`) use this only as a "has setup run" signal, not a liveness check.
pub fn profile_has_block() -> bool {
    #[cfg(windows)]
    {
        user_env_has_proxy()
    }
    #[cfg(not(windows))]
    {
        let Ok(home) = std::env::var("HOME") else {
            return false;
        };
        profile_has_block_in(std::path::Path::new(&home))
    }
}

/// Inner check used by [`profile_has_block`] and tests. Scans all candidate profiles under
/// `base`; returns `true` if the BEGIN marker is found in any of them.
#[cfg(not(windows))]
fn profile_has_block_in(base: &std::path::Path) -> bool {
    candidate_profiles(base).into_iter().any(|path| {
        std::fs::read_to_string(&path)
            .map(|t| t.contains(BEGIN))
            .unwrap_or(false)
    })
}

// ── Windows user environment (`HKCU\Environment`) ───────────────────────────────
// On Windows the proxy env lives in the registry, not a shell profile: it's inherited by
// every process at launch (PS5, pwsh7, Git Bash, cmd, GUI apps) and survives an
// ExecutionPolicy that would block a profile from running. Only processes started after
// the write see it — that's why setup still says "open a new terminal".

/// The values llmtrim manages in the user environment.
#[cfg(windows)]
const ENV_KEYS: [&str; 4] = [
    "HTTPS_PROXY",
    "HTTP_PROXY",
    "NO_PROXY",
    "NODE_EXTRA_CA_CERTS",
];

/// Open `HKCU\Environment` for read+write (created if somehow absent).
#[cfg(windows)]
fn user_env_key() -> Result<winreg::RegKey> {
    use winreg::RegKey;
    use winreg::enums::{HKEY_CURRENT_USER, KEY_READ, KEY_WRITE};
    let hkcu = RegKey::predef(HKEY_CURRENT_USER);
    let (env, _) = hkcu
        .create_subkey_with_flags("Environment", KEY_READ | KEY_WRITE)
        .context("failed to open HKCU\\Environment")?;
    Ok(env)
}

/// Set `HTTPS_PROXY`/`HTTP_PROXY`/`NODE_EXTRA_CA_CERTS` in the user environment.
#[cfg(windows)]
fn set_user_env(proxy: &str, ca: &str) -> Result<()> {
    set_env_in(&user_env_key()?, proxy, ca)
}

/// Delete the managed values from the user environment. Returns true if anything was
/// removed. Missing values are not an error (idempotent uninstall).
#[cfg(windows)]
fn clear_user_env() -> Result<bool> {
    clear_env_in(&user_env_key()?)
}

/// Does the user environment's `HTTPS_PROXY` point at a local llmtrim interceptor?
#[cfg(windows)]
fn user_env_has_proxy() -> bool {
    user_env_key().is_ok_and(|env| has_proxy_in(&env))
}

/// Windows: wire the interceptor env into `HKCU\Environment` (and broadcast the change) when the
/// daemon comes up. The registry is read at process launch, so unlike POSIX there is no per-shell
/// liveness gate — the env is set on daemon start and cleared on `stop`, keeping newly-launched
/// processes in sync with whether the interceptor is actually running. Called from the supervised
/// `serve` path, which is the single process `start` and login autostart both bring the daemon up
/// through. Best-effort; ensures the CA exists so `NODE_EXTRA_CA_CERTS` points at a real file.
#[cfg(windows)]
pub fn wire_env_windows(port: u16) -> Result<()> {
    crate::serve::ensure_ca()?;
    let ca = crate::serve::ca_cert_path()?.to_string_lossy().to_string();
    let proxy = format!("http://127.0.0.1:{port}");
    set_user_env(&proxy, &ca)?;
    broadcast_env_change();
    Ok(())
}

/// Windows: clear the interceptor env from `HKCU\Environment` (and broadcast) when the daemon
/// stops, so a terminal or app launched afterwards doesn't route at the now-dead proxy. Returns
/// true if anything was set. Idempotent: clearing an already-clear env is not an error.
#[cfg(windows)]
pub fn unwire_env_windows() -> Result<bool> {
    let cleared = clear_user_env()?;
    if cleared {
        broadcast_env_change();
    }
    Ok(cleared)
}

/// Broadcast `WM_SETTINGCHANGE("Environment")` so Explorer (and through it, newly-launched
/// terminals, editors, and GUI apps) re-reads `HKCU\Environment` without a logout — a raw
/// registry write alone is invisible until then (`setx` sends the same message). The call
/// needs `SendMessageTimeout`, which is `unsafe` FFI this crate forbids
/// (`unsafe_code = "forbid"`), so shell out to PowerShell with a one-shot P/Invoke.
/// Best-effort: a failure just means "open a new shell" still applies; never breaks setup.
#[cfg(windows)]
fn broadcast_env_change() {
    // HWND_BROADCAST = 0xffff, WM_SETTINGCHANGE = 0x1A, SMTO_ABORTIFHUNG = 0x2, 5 s timeout.
    // (Keep this comment outside the PS string: the string is one line, so an inline `#`
    // would comment out the rest of it and silently no-op the broadcast.)
    const PS: &str = "\
        $sig = '[DllImport(\"user32.dll\", SetLastError=true, CharSet=CharSet.Auto)]\
        public static extern IntPtr SendMessageTimeout(IntPtr hWnd, uint Msg, UIntPtr wParam, \
        string lParam, uint fuFlags, uint uTimeout, out UIntPtr lpdwResult);';\
        $t = Add-Type -MemberDefinition $sig -Name NativeMethods -Namespace Win32 -PassThru;\
        $r = [UIntPtr]::Zero;\
        [void]$t::SendMessageTimeout([IntPtr]0xffff, 0x1A, [UIntPtr]::Zero, 'Environment', 0x2, 5000, [ref]$r)";
    let _ = std::process::Command::new("powershell")
        .args(["-NoProfile", "-NonInteractive", "-Command", PS])
        .stdout(std::process::Stdio::null())
        .stderr(std::process::Stdio::null())
        .status();
}

// The registry mechanics, taking the key as a seam so tests can exercise them against a
// throwaway subkey instead of the real `HKCU\Environment`.

#[cfg(windows)]
fn set_env_in(env: &winreg::RegKey, proxy: &str, ca: &str) -> Result<()> {
    env.set_value("HTTPS_PROXY", &proxy)
        .context("failed to set HTTPS_PROXY")?;
    env.set_value("HTTP_PROXY", &proxy)
        .context("failed to set HTTP_PROXY")?;
    env.set_value("NO_PROXY", &NO_PROXY)
        .context("failed to set NO_PROXY")?;
    env.set_value("NODE_EXTRA_CA_CERTS", &ca)
        .context("failed to set NODE_EXTRA_CA_CERTS")?;
    Ok(())
}

#[cfg(windows)]
fn clear_env_in(env: &winreg::RegKey) -> Result<bool> {
    let mut removed = false;
    for key in ENV_KEYS {
        match env.delete_value(key) {
            Ok(()) => removed = true,
            Err(e) if e.kind() == std::io::ErrorKind::NotFound => {}
            Err(e) => return Err(e).with_context(|| format!("failed to delete {key}")),
        }
    }
    Ok(removed)
}

#[cfg(windows)]
fn has_proxy_in(env: &winreg::RegKey) -> bool {
    env.get_value::<String, _>("HTTPS_PROXY")
        .is_ok_and(|v| v.contains("127.0.0.1"))
}

// ── Windows OS trust-store cleanup ───────────────────────────────────────────────
// `llmtrim ca` tells GUI/non-Node apps to trust the CA system-wide via
// `certutil -addstore -user Root`. Removing ~/.llmtrim/ca.pem does NOT untrust that copy —
// a stale, now-keyless root CA would linger in the store. Uninstall must pull it back out.

/// The CA's subject CommonName, matched verbatim with the value set at generation in
/// `serve.rs` (`dn.push(DnType::CommonName, "llmtrim local CA")`). Keep the two in sync.
#[cfg(any(windows, target_os = "macos"))]
const CA_SUBJECT_CN: &str = "llmtrim local CA";

/// Remove the llmtrim CA from the current-user **Root** trust store if it was trusted
/// system-wide (via `llmtrim ca` / `certutil -addstore`). Matched by subject CN.
///
/// `Remove-Item Cert:\CurrentUser\Root\..` refuses to run non-interactively ("the operation
/// occurred in the user's main store and the UI is not allowed"), so we shell out to
/// `certutil -delstore`, which deletes from the user store without a prompt and without
/// elevation. Best-effort and idempotent: a non-zero exit is almost always "no certificate
/// matched" — i.e. it was never trusted system-wide — and is reported as `Ok(false)`.
#[cfg(windows)]
fn untrust_ca_root() -> Result<bool> {
    let out = std::process::Command::new("certutil")
        .args(["-user", "-delstore", "Root", CA_SUBJECT_CN])
        .output()
        .context("failed to run certutil -delstore")?;
    Ok(out.status.success())
}

/// macOS: remove the llmtrim CA from the **login** keychain if it was trusted there (via
/// `llmtrim ca` → `security add-trusted-cert`). Matched by common name; no sudo, since the
/// login keychain is the user's own. Best-effort and idempotent: a non-zero exit is "no such
/// certificate" — i.e. it was never trusted — and is reported as `Ok(false)`. A *system*
/// keychain trust (sudo-installed) is left to the printed manual note, as on Linux.
#[cfg(target_os = "macos")]
fn untrust_ca_keychain() -> Result<bool> {
    let out = std::process::Command::new("security")
        .args(["delete-certificate", "-c", CA_SUBJECT_CN])
        .output()
        .context("failed to run security delete-certificate")?;
    Ok(out.status.success())
}

// ── Windows binary + PATH cleanup (the installer's footprint) ────────────────────
// install.ps1 drops llmtrim.exe in %LOCALAPPDATA%\llmtrim\bin and adds that dir to the user
// PATH. Uninstall has to reverse both, or `llmtrim` keeps resolving as a command afterwards.

/// The installer's bin dir, `%LOCALAPPDATA%\llmtrim\bin` (the entry it adds to the user PATH).
#[cfg(windows)]
fn installer_bin_dir() -> Option<PathBuf> {
    std::env::var_os("LOCALAPPDATA").map(|l| PathBuf::from(l).join("llmtrim").join("bin"))
}

/// `%LOCALAPPDATA%\llmtrim` when `exe` lives under it — i.e. this is an installer build, safe
/// to schedule for deletion. A cargo/dev binary elsewhere returns `None` (never self-deleted).
#[cfg(windows)]
fn installer_dir_of(exe: &std::path::Path) -> Option<PathBuf> {
    let root = PathBuf::from(std::env::var_os("LOCALAPPDATA")?).join("llmtrim");
    exe.starts_with(&root).then_some(root)
}

/// UTF-16LE bytes with a trailing NUL — the on-disk form of a `REG_SZ`/`REG_EXPAND_SZ` value,
/// so we can write PATH back in whatever string type it already used.
#[cfg(windows)]
fn encode_utf16_nul(s: &str) -> Vec<u8> {
    s.encode_utf16()
        .chain(std::iter::once(0))
        .flat_map(u16::to_le_bytes)
        .collect()
}

/// Drop the installer's bin dir from the user PATH (`HKCU\Environment\Path`). Returns true if
/// it was present and removed. Preserves the value's registry type (`REG_EXPAND_SZ` stays
/// expandable — rewriting it as plain `REG_SZ` would break any `%VAR%` still in the PATH).
#[cfg(windows)]
fn remove_bin_dir_from_path() -> Result<bool> {
    use winreg::enums::RegType;
    use winreg::types::FromRegValue;
    let Some(bin) = installer_bin_dir() else {
        return Ok(false);
    };
    let env = user_env_key()?;
    let Ok(raw) = env.get_raw_value("Path") else {
        return Ok(false); // no user PATH set → nothing of ours to remove
    };
    if raw.vtype != RegType::REG_SZ && raw.vtype != RegType::REG_EXPAND_SZ {
        return Ok(false); // leave an unexpected type untouched
    }
    let current = String::from_reg_value(&raw).unwrap_or_default();
    let stripped = strip_path_entry(&current, &bin.to_string_lossy());
    if stripped == current {
        return Ok(false);
    }
    let new_raw = winreg::RegValue {
        bytes: encode_utf16_nul(&stripped).into(),
        vtype: raw.vtype,
    };
    env.set_raw_value("Path", &new_raw)
        .context("failed to update the user PATH")?;
    Ok(true)
}

/// Schedule deletion of the install dir once we've exited. A running `.exe` can't be unlinked
/// on Windows, so spawn a detached `cmd` that retries the delete for ~60 s — one shot after a
/// fixed delay loses the race when the shell or Defender still holds the freshly-exited exe.
/// Best-effort: uninstall never fails on this.
#[cfg(windows)]
fn schedule_dir_removal(dir: &std::path::Path) {
    schedule_removal_script(&format!(
        "rmdir /s /q \"{0}\" >nul 2>&1 & if not exist \"{0}\" exit",
        dir.display()
    ));
}

/// Schedule deletion of a single file once we've exited — the running-`.exe` self-delete for
/// builds that live outside `%LOCALAPPDATA%\llmtrim` (e.g. a `cargo install` binary under
/// `~/.cargo/bin`). Same detached-`cmd` trick as [`schedule_dir_removal`], `del` not `rmdir`.
#[cfg(windows)]
fn schedule_file_removal(file: &std::path::Path) {
    schedule_removal_script(&format!(
        "del /f /q \"{0}\" >nul 2>&1 & if not exist \"{0}\" exit",
        file.display()
    ));
}

/// Run `attempt` (which must `exit` on success) every ~2 s for ~60 s in a detached,
/// windowless `cmd`. `ping` is the reliable console-less delay; the retry loop covers the
/// window where the just-exited exe is still locked (shell handle inheritance, Defender
/// scanning fresh executables).
#[cfg(windows)]
fn schedule_removal_script(attempt: &str) {
    use std::os::windows::process::CommandExt;
    use std::process::Stdio;
    const DETACHED_PROCESS: u32 = 0x0000_0008;
    const CREATE_NO_WINDOW: u32 = 0x0800_0000;
    let script = format!("for /l %i in (1,1,30) do (ping 127.0.0.1 -n 3 >nul & {attempt})");
    let _ = std::process::Command::new("cmd")
        .args(["/c", &script])
        .creation_flags(DETACHED_PROCESS | CREATE_NO_WINDOW)
        .stdin(Stdio::null())
        .stdout(Stdio::null())
        .stderr(Stdio::null())
        .spawn();
}

/// Remove every occurrence of `dir` from a `;`-separated PATH string, preserving the other
/// entries and their order. Ignores case and a trailing slash (Windows path semantics). Pure,
/// so it's unit-tested on every platform even though it's only called on Windows.
#[cfg_attr(not(windows), allow(dead_code))]
fn strip_path_entry(path: &str, dir: &str) -> String {
    let norm = |s: &str| s.trim().trim_end_matches(['\\', '/']).to_ascii_lowercase();
    let target = norm(dir);
    // Drop only the matching segment(s); other entries (and any pre-existing empties) keep
    // their original text and order — we touch the PATH as little as possible.
    path.split(';')
        .filter(|seg| norm(seg) != target)
        .collect::<Vec<_>>()
        .join(";")
}

/// Which shell dialect the profile uses, so the managed block is written in its native syntax.
/// Each variant is constructed on only one platform (`Posix` off-Windows, `PowerShell` on
/// Windows), yet both arms of `env_block` are compiled and unit-tested everywhere so the
/// formatting is verifiable on either OS — hence the unconditional `allow(dead_code)`.
#[allow(dead_code)]
#[derive(Clone, Copy)]
enum Syntax {
    Posix,
    PowerShell,
}

/// The rc file for a `$SHELL` value (its basename decides; unknown shells get `.profile`).
/// Single source for the shell→file mapping — used by both [`profile_target`] and
/// [`write_profile_block_in`].
#[cfg(not(windows))]
fn shell_profile_file(shell: &str) -> &'static str {
    if shell.ends_with("zsh") {
        ".zshrc"
    } else if shell.ends_with("bash") {
        ".bashrc"
    } else {
        ".profile"
    }
}

/// The profile file to write the managed env block into, and the syntax it uses. Unix: the
/// `$SHELL` rc file (`export`). Windows: the current-user PowerShell profile (`$env:`).
fn profile_target() -> Option<(PathBuf, Syntax)> {
    #[cfg(not(windows))]
    {
        let home = std::env::var("HOME").ok()?;
        let shell = std::env::var("SHELL").unwrap_or_default();
        let file = shell_profile_file(&shell);
        Some((PathBuf::from(home).join(file), Syntax::Posix))
    }
    #[cfg(windows)]
    {
        powershell_profile().map(|p| (p, Syntax::PowerShell))
    }
}

/// Resolve `$PROFILE.CurrentUserAllHosts` (handles PowerShell 5 vs 7 and a redirected/OneDrive
/// `Documents`), falling back to the conventional location if PowerShell can't be queried.
#[cfg(windows)]
fn powershell_profile() -> Option<PathBuf> {
    if let Ok(out) = std::process::Command::new("powershell")
        .args(["-NoProfile", "-Command", "$PROFILE.CurrentUserAllHosts"])
        .output()
    {
        let path = String::from_utf8_lossy(&out.stdout).trim().to_string();
        if !path.is_empty() {
            return Some(PathBuf::from(path));
        }
    }
    let up = std::env::var("USERPROFILE").ok()?;
    Some(
        PathBuf::from(up)
            .join("Documents")
            .join("PowerShell")
            .join("profile.ps1"),
    )
}

/// Candidate paths to the OS's PEM bundle of trusted root CAs, in probe order — the same list
/// OpenSSL, Go's `crypto/x509`, and `openssl-probe` use. The first that exists is the platform's
/// trust anchor set. POSIX-only: Windows keeps its roots in the schannel cert store, not a file.
#[cfg(not(windows))]
const SYSTEM_CA_CANDIDATES: [&str; 6] = [
    "/etc/ssl/certs/ca-certificates.crt", // Debian, Ubuntu, Arch, Gentoo
    "/etc/pki/tls/certs/ca-bundle.crt",   // Fedora, RHEL, CentOS
    "/etc/ssl/ca-bundle.pem",             // openSUSE
    "/etc/pki/tls/cacert.pem",            // OpenELEC
    "/etc/ssl/cert.pem",                  // Alpine, macOS (Homebrew OpenSSL), *BSD
    "/usr/local/etc/openssl/cert.pem",    // macOS Homebrew openssl@1.1
];

/// First existing OS root bundle, or `None` when none of the well-known paths exist (e.g. a
/// stock macOS with no OpenSSL PEM on disk — trust lives only in the keychain there).
#[cfg(not(windows))]
fn system_ca_bundle() -> Option<PathBuf> {
    SYSTEM_CA_CANDIDATES
        .iter()
        .map(PathBuf::from)
        .find(|p| p.exists())
}

/// Build (or refresh) `~/.llmtrim/ca-bundle.pem` = the OS root bundle **plus** the llmtrim CA,
/// returning its path. Native TLS clients (curl, git, Python, and rustls tools like OpenAI's
/// Codex) don't read `NODE_EXTRA_CA_CERTS` — that's Node-only — they take a full bundle via
/// `SSL_CERT_FILE`/`CURL_CA_BUNDLE`. Because llmtrim MITMs only a fixed host set and
/// blind-tunnels everything else, the bundle MUST carry the real OS roots too, or every
/// non-intercepted HTTPS call would fail to verify — hence a concatenation, not the CA alone.
/// Returns `None` (caller then omits the native vars, since a CA-only file would break tunneled
/// hosts) when no OS bundle can be located.
#[cfg(not(windows))]
fn ensure_ca_bundle(ca_path: &std::path::Path) -> Result<Option<PathBuf>> {
    let Some(system) = system_ca_bundle() else {
        return Ok(None);
    };
    let system_pem = std::fs::read_to_string(&system)
        .with_context(|| format!("failed to read {}", system.display()))?;
    let ca_pem = std::fs::read_to_string(ca_path)
        .with_context(|| format!("failed to read {}", ca_path.display()))?;
    let mut combined = system_pem;
    if !combined.ends_with('\n') {
        combined.push('\n');
    }
    combined.push_str(&ca_pem);
    let bundle_path = ca_path.with_file_name("ca-bundle.pem");
    std::fs::write(&bundle_path, combined)
        .with_context(|| format!("failed to write {}", bundle_path.display()))?;
    Ok(Some(bundle_path))
}

/// The managed env block, in the profile's native syntax. Both variants are unit-tested on
/// every platform; on Windows the live env path is the registry, so this is test-only there.
/// `bundle` (POSIX only) is the combined OS-roots + CA bundle for native TLS clients; when
/// `Some`, it also exports `SSL_CERT_FILE`/`CURL_CA_BUNDLE`.
#[allow(dead_code)]
fn env_block(proxy: &str, ca: &str, bundle: Option<&str>, syntax: Syntax) -> String {
    match syntax {
        // NO_PROXY is set in both cases (lowercase too on POSIX: curl/libcurl, Go, and others
        // only honor `no_proxy`). Windows env vars are case-insensitive, so one suffices there.
        // Wire the env only while the daemon is actually up. A new shell opened after
        // `llmtrim stop` must not route at a now-dead proxy — so the block gates every export
        // behind `llmtrim _alive`, a fast pidfile check (no network, no TCP connect). Daemon
        // down → the vars stay unset → tools talk to the LLM host directly and just work.
        // The current shell can't be fixed this way (a child can't rewrite its parent's env);
        // that's what `stop`'s printed `unset` line and `uninstall` handle.
        Syntax::Posix => {
            // Node trusts the CA via NODE_EXTRA_CA_CERTS; native OpenSSL/rustls clients don't
            // read it, so point their bundle vars at the full combined bundle when we have one.
            // Indented to sit inside the liveness guard alongside the other exports.
            let native = bundle
                .map(|b| {
                    format!(
                        "\x20   export SSL_CERT_FILE=\"{b}\"\n\
                         \x20   export CURL_CA_BUNDLE=\"{b}\"\n"
                    )
                })
                .unwrap_or_default();
            format!(
                "{BEGIN}\n\
                 if command -v llmtrim >/dev/null 2>&1 && llmtrim _alive 2>/dev/null; then\n\
                 \x20   export HTTPS_PROXY=\"{proxy}\"\n\
                 \x20   export HTTP_PROXY=\"{proxy}\"\n\
                 \x20   export NO_PROXY=\"{NO_PROXY}\"\n\
                 \x20   export no_proxy=\"{NO_PROXY}\"\n\
                 \x20   export NODE_EXTRA_CA_CERTS=\"{ca}\"\n\
                 {native}fi\n\
                 {END}\n"
            )
        }
        Syntax::PowerShell => format!(
            "{BEGIN}\n\
             $env:HTTPS_PROXY = \"{proxy}\"\n\
             $env:HTTP_PROXY = \"{proxy}\"\n\
             $env:NO_PROXY = \"{NO_PROXY}\"\n\
             $env:NODE_EXTRA_CA_CERTS = \"{ca}\"\n\
             {END}\n"
        ),
    }
}

/// Inner seam for [`write_profile_block`]: write into a profile file named by `shell` (the
/// basename of `$SHELL`, e.g. `"bash"` → `.bashrc`, `"zsh"` → `.zshrc`) under `base` as the
/// home directory. Sweeps stale blocks from all other candidates under `base` first, then
/// writes the new block. Returns the path written. `base` is never `$HOME` itself; it is
/// always a caller-supplied directory, which tests supply as a temp dir.
#[cfg(not(windows))]
fn write_profile_block_in(
    base: &std::path::Path,
    shell: &str,
    proxy: &str,
    ca: &str,
    bundle: Option<&str>,
) -> Result<Vec<PathBuf>> {
    // Refresh, never accumulate: strip any prior managed block from EVERY candidate first,
    // so a re-run (or a proxy/port change) leaves no stale block behind. Best-effort - a
    // failure to strip one file must not block writing the fresh block.
    let _ = remove_profile_block_in(base);

    // Write the block into every candidate rc file that already EXISTS, plus the file the
    // user's `$SHELL` sources (created if absent). Writing to all existing files is what
    // fixes the macOS trap: setup keyed the target off `$SHELL`, but `$SHELL` is the login
    // shell and can disagree with the shell the terminal actually launches (iTerm running
    // zsh while `$SHELL=/bin/bash`), so the block landed in a file the running shell never
    // sourced. Now whichever shell starts, its rc already carries the env. The managed
    // BEGIN/END markers make a block in several files idempotent and cleanly removable.
    let mut targets: Vec<PathBuf> = candidate_profiles(base)
        .into_iter()
        .filter(|p| p.exists())
        .collect();
    let shell_default = base.join(shell_profile_file(shell));
    if !targets.contains(&shell_default) {
        targets.push(shell_default); // guarantee the running shell's rc gets it, even if absent
    }

    let block = env_block(proxy, ca, bundle, Syntax::Posix);
    let mut written = Vec::with_capacity(targets.len());
    for path in targets {
        // strip_block is a safety net in case remove_profile_block_in skipped a file with a
        // broken BEGIN-without-END that it left intact.
        let existing = std::fs::read_to_string(&path).unwrap_or_default();
        let mut base_content = strip_block(&existing);
        if !base_content.is_empty() && !base_content.ends_with('\n') {
            base_content.push('\n');
        }
        std::fs::write(&path, format!("{base_content}{block}"))
            .with_context(|| format!("failed to write {}", path.display()))?;
        written.push(path);
    }
    Ok(written)
}

/// Replace (or append) the llmtrim managed block in the shell profile. Idempotent — a
/// re-run updates the existing block rather than stacking duplicates. Also sweeps stale
/// blocks from all other candidate profile files (e.g. `.bashrc` when now running zsh),
/// so re-setup under a different shell does not leave a dead proxy block behind.
/// POSIX-only: on Windows the env lives in the registry, so `run()` never calls this there.
#[allow(dead_code)]
fn write_profile_block(proxy: &str, ca: &str, bundle: Option<&str>) -> Result<Vec<PathBuf>> {
    #[cfg(not(windows))]
    {
        // Delegate to the seam so production and tests run the same code path.
        let Ok(home) = std::env::var("HOME") else {
            return Ok(Vec::new());
        };
        let shell = std::env::var("SHELL").unwrap_or_default();
        write_profile_block_in(std::path::Path::new(&home), &shell, proxy, ca, bundle)
    }
    #[cfg(windows)]
    {
        // Legacy PowerShell-profile arm: the live Windows env is the registry; this is
        // only reachable for a profile-style install a prior version may have used.
        let Some((path, syntax)) = profile_target() else {
            return Ok(Vec::new());
        };
        if let Some(parent) = path.parent() {
            let _ = std::fs::create_dir_all(parent); // the PowerShell profile dir may not exist yet
        }
        let existing = std::fs::read_to_string(&path).unwrap_or_default();
        let mut base_content = strip_block(&existing);
        if !base_content.is_empty() && !base_content.ends_with('\n') {
            base_content.push('\n');
        }
        let block = env_block(proxy, ca, bundle, syntax);
        std::fs::write(&path, format!("{base_content}{block}"))
            .with_context(|| format!("failed to write {}", path.display()))?;
        Ok(vec![path])
    }
}

/// Remove any existing llmtrim managed block (between the markers, inclusive).
fn strip_block(s: &str) -> String {
    // If BEGIN exists but END is missing (e.g. user deleted it), return original
    // unchanged rather than silently erasing everything from BEGIN to EOF.
    let has_begin = s.lines().any(|l| l.trim() == BEGIN);
    let has_end = s.lines().any(|l| l.trim() == END);
    if has_begin && !has_end {
        return s.to_string();
    }
    let mut out = String::new();
    let mut skip = false;
    for line in s.lines() {
        match line.trim() {
            BEGIN => skip = true,
            END => skip = false,
            _ if !skip => {
                out.push_str(line);
                out.push('\n');
            }
            _ => {}
        }
    }
    out
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn caveman_detection_covers_flag_hooks_and_plugin() {
        let tmp = std::env::temp_dir().join(format!("llmtrim-caveman-test-{}", std::process::id()));
        let _ = std::fs::remove_dir_all(&tmp);

        // Empty / missing dir → not installed.
        assert!(!caveman_installed_in(&tmp));
        std::fs::create_dir_all(&tmp).expect("create test dir");
        assert!(!caveman_installed_in(&tmp));

        // Flag file written by caveman's session hook.
        std::fs::write(tmp.join(".caveman-active"), "1").expect("write flag");
        assert!(caveman_installed_in(&tmp));
        std::fs::remove_file(tmp.join(".caveman-active")).expect("rm flag");

        // Standalone hook file.
        std::fs::create_dir_all(tmp.join("hooks")).expect("mkdir hooks");
        std::fs::write(tmp.join("hooks/caveman-activate.js"), "//").expect("write hook");
        assert!(caveman_installed_in(&tmp));
        std::fs::remove_file(tmp.join("hooks/caveman-activate.js")).expect("rm hook");
        assert!(!caveman_installed_in(&tmp));

        // Plugin-cache entry, nested one level (within the depth-3 walk).
        std::fs::create_dir_all(tmp.join("plugins/cache/caveman")).expect("mkdir plugin");
        assert!(caveman_installed_in(&tmp));

        let _ = std::fs::remove_dir_all(&tmp);
    }

    #[cfg(not(windows))]
    #[test]
    fn heal_rewrites_old_block_adds_no_proxy_preserving_port_and_surroundings() {
        let tmp = std::env::temp_dir().join(format!("llmtrim-heal-{}", std::process::id()));
        let _ = std::fs::remove_dir_all(&tmp);
        std::fs::create_dir_all(&tmp).expect("mkdir");

        // An old-schema block (no NO_PROXY) wired at port 43117, with user content around it.
        let old = format!(
            "export FOO=bar\n{BEGIN}\n\
             export HTTPS_PROXY=\"http://127.0.0.1:43117\"\n\
             export HTTP_PROXY=\"http://127.0.0.1:43117\"\n\
             export NODE_EXTRA_CA_CERTS=\"/home/u/ca.pem\"\n{END}\nexport BAZ=qux\n"
        );
        let rc = tmp.join(".bashrc");
        std::fs::write(&rc, &old).expect("write rc");

        let healed = heal_profiles_in(&tmp, "http://127.0.0.1:43117", "/home/u/ca.pem", None)
            .expect("heal runs");
        assert_eq!(healed, vec![rc.clone()], "the old block is healed");

        let after = std::fs::read_to_string(&rc).expect("read back");
        assert!(after.contains("export NO_PROXY="), "bypass added");
        assert!(after.contains("127.0.0.1:43117"), "port preserved");
        assert!(after.contains("export FOO=bar") && after.contains("export BAZ=qux"));
        assert_eq!(after.matches(BEGIN).count(), 1, "block not duplicated");

        // Idempotent: a second pass sees the bypass and does nothing.
        let again = heal_profiles_in(&tmp, "http://127.0.0.1:43117", "/home/u/ca.pem", None)
            .expect("second heal");
        assert!(again.is_empty(), "already-current block is left alone");

        let _ = std::fs::remove_dir_all(&tmp);
    }

    #[cfg(not(windows))]
    #[test]
    fn managed_block_needs_heal_distinguishes_old_current_malformed_and_outside() {
        let old = format!("{BEGIN}\nexport HTTPS_PROXY=\"x\"\n{END}\n");
        assert!(managed_block_needs_heal(&old), "old block needs heal");

        // Pre-gate block: has the NO_PROXY bypass but not the `llmtrim _alive` liveness gate. It
        // must still heal so existing installs migrate to the daemon-gated form (the fix that lets
        // `stop` unwire new shells) without the user re-running `setup`.
        let pre_gate = format!("{BEGIN}\nexport NO_PROXY=\"y\"\n{END}\n");
        assert!(
            managed_block_needs_heal(&pre_gate),
            "block missing the _alive gate needs heal"
        );

        // The actually-current block carries both the bypass and the gate, so it is skipped.
        let current = env_block(
            "http://127.0.0.1:8787",
            "/home/u/ca.pem",
            None,
            Syntax::Posix,
        );
        assert!(!managed_block_needs_heal(&current), "current block skipped");

        let malformed = format!("{BEGIN}\nexport HTTPS_PROXY=\"x\"\n"); // no END
        assert!(
            !managed_block_needs_heal(&malformed),
            "malformed block skipped"
        );

        // A user's own NO_PROXY *outside* the markers must not mask a stale managed block.
        let outside = format!("export NO_PROXY=mine\n{BEGIN}\nexport HTTPS_PROXY=\"x\"\n{END}\n");
        assert!(
            managed_block_needs_heal(&outside),
            "NO_PROXY outside the block does not count as healed"
        );

        assert!(!managed_block_needs_heal("export FOO=bar\n"), "no block");
    }

    #[cfg(not(windows))]
    #[test]
    fn heal_skips_malformed_block_without_corrupting_it() {
        let tmp = std::env::temp_dir().join(format!("llmtrim-heal-bad-{}", std::process::id()));
        let _ = std::fs::remove_dir_all(&tmp);
        std::fs::create_dir_all(&tmp).expect("mkdir");
        // BEGIN with no END (user deleted it). strip_block would no-op, so a naive heal would
        // stack a second block; managed_block_needs_heal must reject it.
        let bad = format!("keep\n{BEGIN}\nexport HTTPS_PROXY=\"http://127.0.0.1:43117\"\n");
        std::fs::write(tmp.join(".bashrc"), &bad).expect("write");

        let healed =
            heal_profiles_in(&tmp, "http://127.0.0.1:43117", "/home/u/ca.pem", None).expect("heal");
        assert!(healed.is_empty(), "malformed block is not healed");
        assert_eq!(
            std::fs::read_to_string(tmp.join(".bashrc")).expect("read"),
            bad,
            "malformed file left byte-for-byte untouched"
        );
        let _ = std::fs::remove_dir_all(&tmp);
    }

    #[cfg(not(windows))]
    #[test]
    fn heal_touches_only_the_stale_profile_in_a_multi_shell_setup() {
        let tmp = std::env::temp_dir().join(format!("llmtrim-heal-multi-{}", std::process::id()));
        let _ = std::fs::remove_dir_all(&tmp);
        std::fs::create_dir_all(&tmp).expect("mkdir");
        // .bashrc carries an old block; .zshrc already has the current (NO_PROXY) block.
        let old = format!("{BEGIN}\nexport HTTPS_PROXY=\"http://127.0.0.1:43117\"\n{END}\n");
        let current = env_block(
            "http://127.0.0.1:43117",
            "/home/u/ca.pem",
            None,
            Syntax::Posix,
        );
        std::fs::write(tmp.join(".bashrc"), &old).expect("write bashrc");
        std::fs::write(tmp.join(".zshrc"), &current).expect("write zshrc");

        let healed =
            heal_profiles_in(&tmp, "http://127.0.0.1:43117", "/home/u/ca.pem", None).expect("heal");
        assert_eq!(
            healed,
            vec![tmp.join(".bashrc")],
            "only the stale profile healed"
        );
        assert!(
            std::fs::read_to_string(tmp.join(".bashrc"))
                .expect("read bashrc")
                .contains("NO_PROXY"),
            "stale profile now carries the bypass"
        );
        assert_eq!(
            std::fs::read_to_string(tmp.join(".zshrc")).expect("read zshrc"),
            current,
            "already-current profile left untouched"
        );
        let _ = std::fs::remove_dir_all(&tmp);
    }

    #[cfg(not(windows))]
    #[test]
    fn heal_ignores_files_without_a_managed_block() {
        let tmp = std::env::temp_dir().join(format!("llmtrim-heal-none-{}", std::process::id()));
        let _ = std::fs::remove_dir_all(&tmp);
        std::fs::create_dir_all(&tmp).expect("mkdir");
        std::fs::write(tmp.join(".zshrc"), "export FOO=bar\n").expect("write");

        let healed =
            heal_profiles_in(&tmp, "http://127.0.0.1:43117", "/home/u/ca.pem", None).expect("heal");
        assert!(
            healed.is_empty(),
            "a file with no block is never created/touched"
        );
        assert_eq!(
            std::fs::read_to_string(tmp.join(".zshrc")).expect("read"),
            "export FOO=bar\n",
            "content untouched"
        );
        let _ = std::fs::remove_dir_all(&tmp);
    }

    #[test]
    fn choose_port_precedence() {
        // Explicit `--port` always wins, even over a running daemon and a configured env.
        assert_eq!(
            choose_port(Some(9000), Some(8800), Some(8700)),
            PortChoice::Use(9000)
        );
        // No explicit → reuse the running daemon's port (don't migrate a live proxy).
        assert_eq!(
            choose_port(None, Some(8800), Some(8700)),
            PortChoice::Use(8800)
        );
        // No daemon → reuse what the env already points at, so re-running converges.
        assert_eq!(choose_port(None, None, Some(8700)), PortChoice::Use(8700));
        // Nothing pinned → scan from the default (the only case that probes for a free port).
        assert_eq!(
            choose_port(None, None, None),
            PortChoice::ScanFrom(DEFAULT_PORT)
        );
    }

    /// The public boundary `llmtrim autostart` resolves through. A running daemon's port must
    /// win over any default — this is the regression `fix/autostart-port-resolve` locks in
    /// (bare `autostart` used to hardcode the default). Deterministic: `running` short-circuits
    /// `choose_port` before `configured_port()` reads any real env.
    #[test]
    fn resolve_port_prefers_running_over_default() {
        assert_eq!(resolve_port(None, Some(9001)).expect("running port"), 9001);
        assert_eq!(
            resolve_port(Some(7000), Some(9001)).expect("explicit"),
            7000
        );
    }

    #[test]
    fn parse_proxy_port_reads_the_wired_port() {
        assert_eq!(parse_proxy_port("http://127.0.0.1:8787"), Some(8787));
        // Embedded in a real profile/registry line, with trailing content after the digits.
        assert_eq!(
            parse_proxy_port("export HTTPS_PROXY=\"http://127.0.0.1:9001\"\nexport X=1\n"),
            Some(9001)
        );
        assert_eq!(parse_proxy_port("no proxy here"), None);
        assert_eq!(parse_proxy_port("127.0.0.1:"), None); // present but portless
    }

    #[test]
    fn strip_path_entry_removes_only_the_target() {
        let path = r"C:\Windows;C:\Users\u\AppData\Local\llmtrim\bin;C:\tools";
        let dir = r"C:\Users\u\AppData\Local\llmtrim\bin";
        assert_eq!(strip_path_entry(path, dir), r"C:\Windows;C:\tools");

        // Case- and trailing-slash-insensitive (Windows path semantics), order preserved.
        let messy = r"C:\a;c:\users\u\appdata\local\LLMTRIM\BIN\;C:\b";
        assert_eq!(strip_path_entry(messy, dir), r"C:\a;C:\b");

        // Absent → unchanged. Other entries (incl. pre-existing empties) are left as-is.
        assert_eq!(strip_path_entry(r"C:\a;C:\b", dir), r"C:\a;C:\b");
        // A leading-semicolon PATH (installer appended to an empty user PATH) collapses cleanly.
        assert_eq!(strip_path_entry(&format!(";{dir}"), dir), "");
    }

    #[test]
    fn strip_block_removes_managed_section_only() {
        let input = format!("keep1\n{BEGIN}\nexport X=1\n{END}\nkeep2\n");
        let out = strip_block(&input);
        assert_eq!(out, "keep1\nkeep2\n");
    }

    #[test]
    fn strip_block_is_noop_without_markers() {
        assert_eq!(strip_block("a\nb\n"), "a\nb\n");
    }

    #[test]
    fn strip_block_missing_end_marker_returns_original() {
        let input = "before\n# >>> llmtrim >>>\nexport HTTPS_PROXY=http://127.0.0.1:8787\n";
        // BEGIN present but END absent → return unchanged (don't erase everything after BEGIN)
        assert_eq!(strip_block(input), input);
    }

    #[test]
    fn strip_block_normal_removes_block() {
        let input = "before\n# >>> llmtrim >>>\nexport HTTPS_PROXY=http://127.0.0.1:8787\n# <<< llmtrim <<<\nafter\n";
        let result = strip_block(input);
        assert!(result.contains("before"));
        assert!(result.contains("after"));
        assert!(!result.contains("HTTPS_PROXY"));
    }

    #[test]
    fn env_block_posix_uses_export() {
        let b = env_block(
            "http://127.0.0.1:8787",
            "/home/u/ca.pem",
            None,
            Syntax::Posix,
        );
        assert!(b.contains("export HTTPS_PROXY=\"http://127.0.0.1:8787\""));
        assert!(b.contains("export NODE_EXTRA_CA_CERTS=\"/home/u/ca.pem\""));
        // LAN/local bypass travels with the proxy, in both casings tools read.
        assert!(b.contains(&format!("export NO_PROXY=\"{NO_PROXY}\"")));
        assert!(b.contains(&format!("export no_proxy=\"{NO_PROXY}\"")));
        assert!(b.starts_with(BEGIN) && b.trim_end().ends_with(END));
        // No bundle → native TLS vars are omitted (a CA-only file would break tunneled hosts).
        assert!(!b.contains("SSL_CERT_FILE"));
        assert!(!b.contains("CURL_CA_BUNDLE"));
    }

    #[test]
    fn env_block_posix_with_bundle_exports_native_tls_vars() {
        let b = env_block(
            "http://127.0.0.1:8787",
            "/home/u/.llmtrim/ca.pem",
            Some("/home/u/.llmtrim/ca-bundle.pem"),
            Syntax::Posix,
        );
        // Node keeps NODE_EXTRA_CA_CERTS; native clients (curl, rustls) get the full bundle.
        assert!(b.contains("export NODE_EXTRA_CA_CERTS=\"/home/u/.llmtrim/ca.pem\""));
        assert!(b.contains("export SSL_CERT_FILE=\"/home/u/.llmtrim/ca-bundle.pem\""));
        assert!(b.contains("export CURL_CA_BUNDLE=\"/home/u/.llmtrim/ca-bundle.pem\""));
        assert!(b.trim_end().ends_with(END));
    }

    #[cfg(not(windows))] // system_ca_bundle/ensure_ca_bundle are POSIX-only (Windows uses the cert store)
    #[test]
    fn ensure_ca_bundle_concats_roots_and_ca() {
        // Only meaningful where the OS ships a PEM root bundle (Linux CI, some macOS). Where it
        // doesn't (stock macOS), the builder correctly returns None and there is nothing to check.
        let Some(system) = system_ca_bundle() else {
            return;
        };
        let tmp = std::env::temp_dir().join(format!("llmtrim-catest-{}", std::process::id()));
        std::fs::create_dir_all(&tmp).expect("mkdir");
        let ca_path = tmp.join("ca.pem");
        std::fs::write(
            &ca_path,
            "-----BEGIN CERTIFICATE-----\nLLMTRIMFAKE\n-----END CERTIFICATE-----\n",
        )
        .expect("write ca");

        let bundle = ensure_ca_bundle(&ca_path)
            .expect("build")
            .expect("some bundle");
        assert_eq!(bundle, tmp.join("ca-bundle.pem"));
        let contents = std::fs::read_to_string(&bundle).expect("read bundle");
        // Carries both the OS roots (so tunneled hosts still verify) and our CA (so MITM'd ones do).
        let roots = std::fs::read_to_string(&system).expect("read system");
        assert!(contents.contains(roots.trim()));
        assert!(contents.contains("LLMTRIMFAKE"));

        std::fs::remove_dir_all(&tmp).ok();
    }

    // The block is sourced into every user's .bashrc/.zshrc, so a shell-syntax error breaks their
    // terminal startup — worse than a wrong var. `sh -n`/`bash -n` parse without executing, so
    // this catches an unbalanced if/fi or a bad quote that string-offset checks can't.
    #[cfg(unix)]
    #[test]
    fn env_block_posix_is_syntactically_valid_shell() {
        use std::io::Write;
        use std::process::{Command, Stdio};
        let block = env_block(
            "http://127.0.0.1:8787",
            "/home/u/ca.pem",
            None,
            Syntax::Posix,
        );
        for shell in ["sh", "bash"] {
            let mut child = match Command::new(shell)
                .arg("-n")
                .stdin(Stdio::piped())
                .stdout(Stdio::null())
                .stderr(Stdio::null())
                .spawn()
            {
                Ok(c) => c,
                Err(_) => continue, // shell not on this runner — skip
            };
            child
                .stdin
                .take()
                .unwrap()
                .write_all(block.as_bytes())
                .unwrap();
            let status = child.wait().unwrap();
            assert!(status.success(), "{shell} -n rejected the block:\n{block}");
        }
    }

    #[test]
    fn env_block_posix_gates_exports_on_daemon_liveness() {
        // The exports must be guarded by the `_alive` probe so a new shell opened after
        // `stop` doesn't wire itself at a dead proxy. Both the guard and a `fi` must be present,
        // and every export must sit inside the conditional (i.e. after the `if` line).
        let b = env_block(
            "http://127.0.0.1:8787",
            "/home/u/ca.pem",
            None,
            Syntax::Posix,
        );
        assert!(
            b.contains("llmtrim _alive"),
            "block must probe daemon liveness"
        );
        let if_at = b.find("if command -v llmtrim").expect("guard present");
        let fi_at = b.find("\nfi\n").expect("guard closed with fi");
        let export_at = b.find("export HTTPS_PROXY").expect("export present");
        assert!(
            if_at < export_at && export_at < fi_at,
            "exports must live inside the liveness guard"
        );

        // The native-TLS bundle exports (added for curl/git/rustls clients) must be gated too,
        // so a new shell after `stop` doesn't keep trusting the interceptor CA at a dead proxy.
        let with_bundle = env_block(
            "http://127.0.0.1:8787",
            "/home/u/ca.pem",
            Some("/home/u/.llmtrim/ca-bundle.pem"),
            Syntax::Posix,
        );
        let fi_at = with_bundle.find("\nfi\n").expect("guard closed with fi");
        let ssl_at = with_bundle
            .find("export SSL_CERT_FILE")
            .expect("bundle exports present");
        assert!(
            ssl_at < fi_at,
            "native-TLS exports must sit inside the guard"
        );
    }

    #[test]
    fn env_block_powershell_uses_env_assignment() {
        let b = env_block(
            "http://127.0.0.1:8787",
            "C:\\Users\\u\\ca.pem",
            None,
            Syntax::PowerShell,
        );
        assert!(b.contains("$env:HTTPS_PROXY = \"http://127.0.0.1:8787\""));
        assert!(b.contains("$env:NODE_EXTRA_CA_CERTS = \"C:\\Users\\u\\ca.pem\""));
        assert!(b.contains(&format!("$env:NO_PROXY = \"{NO_PROXY}\"")));
        assert!(!b.contains("export ")); // no posix syntax leaked in
    }

    #[test]
    fn strip_block_reverses_powershell_block() {
        let withblock = format!("keep\n{}", env_block("p", "c", None, Syntax::PowerShell));
        assert_eq!(strip_block(&withblock), "keep\n");
    }

    #[test]
    fn write_then_strip_is_idempotent() {
        // Writing a block then stripping it returns to the original content.
        let original = "export FOO=bar\n";
        let proxy = "http://127.0.0.1:8787";
        let ca = "/home/user/.llmtrim/ca.crt";
        let block = env_block(proxy, ca, None, Syntax::Posix);
        let written = format!("{original}{block}");
        let stripped = strip_block(&written);
        assert_eq!(
            stripped.trim_end(),
            original.trim_end(),
            "strip after write returns original"
        );
    }

    #[test]
    fn double_write_does_not_duplicate_block() {
        // Calling the write-then-strip flow twice should not duplicate the block.
        let proxy = "http://127.0.0.1:8787";
        let ca = "/home/user/.llmtrim/ca.crt";
        let original = "export FOO=bar\n";
        let block = env_block(proxy, ca, None, Syntax::Posix);
        let after_first = format!("{original}{block}");
        // Simulate a second write: strip then re-add (the real setup flow)
        let after_second = format!("{}{}", strip_block(&after_first), block);
        // The block should appear exactly once
        let begin_count = after_second.matches(">>> llmtrim >>>").count();
        assert_eq!(
            begin_count, 1,
            "block must appear exactly once after double write"
        );
    }

    // Exercise the registry set/has/clear cycle against a throwaway subkey under HKCU so
    // the real `HKCU\Environment` is never touched. The process's own PID keys the scratch
    // path so concurrent test runs don't collide.
    #[cfg(windows)]
    #[test]
    fn registry_env_set_has_clear_roundtrip() {
        use winreg::RegKey;
        use winreg::enums::{HKEY_CURRENT_USER, KEY_READ, KEY_WRITE};

        let hkcu = RegKey::predef(HKEY_CURRENT_USER);
        let scratch = format!("Software\\llmtrim-test-{}", std::process::id());
        let (env, _) = hkcu
            .create_subkey_with_flags(&scratch, KEY_READ | KEY_WRITE)
            .expect("create scratch key");

        assert!(!has_proxy_in(&env), "fresh key has no proxy");
        assert!(
            !clear_env_in(&env).expect("clear on empty key"),
            "nothing to clear yet"
        );

        set_env_in(&env, "http://127.0.0.1:18784", "C:\\Users\\u\\ca.pem").expect("set env");
        assert!(has_proxy_in(&env), "proxy set");
        assert_eq!(
            env.get_value::<String, _>("NODE_EXTRA_CA_CERTS")
                .expect("read CA value"),
            "C:\\Users\\u\\ca.pem"
        );

        assert!(
            clear_env_in(&env).expect("clear set values"),
            "values removed"
        );
        assert!(!has_proxy_in(&env), "proxy gone after clear");

        // Tidy up the scratch key.
        hkcu.delete_subkey_all(&scratch).ok();
    }

    #[test]
    fn first_free_port_rejects_occupied_accepts_free() {
        // Hold a real port open → occupied. Scanning just that port (span 0) finds nothing,
        // proving a bound port is rejected (this is the bug we hit: 8787 held by VS Code).
        let held = TcpListener::bind((Ipv4Addr::LOCALHOST, 0)).expect("bind ephemeral");
        let taken = held.local_addr().expect("local_addr").port();
        assert_eq!(
            first_free_port(taken, 0),
            None,
            "occupied port not rejected"
        );

        // With the port still held, a wider scan skips the occupied port and returns a free
        // one further along. (Re-probing the *same* port after drop is racy: under parallel
        // tests another thread can grab the freed ephemeral port before we rebind it.)
        let found = first_free_port(taken, 64).expect("a free port exists in the span");
        assert_ne!(found, taken, "scan returned the still-occupied port");
    }

    // ── Multi-profile sweep tests (POSIX only) ─────────────────────────────────────
    // All tests use a temp dir as the synthetic $HOME so real profile files are never
    // touched and tests are hermetic under parallel `cargo test` runs.
    //
    // `TempDir` is a drop guard: the directory (and its contents) is deleted when it goes
    // out of scope, with no external crate required.

    #[cfg(not(windows))]
    struct TempDir(PathBuf);

    #[cfg(not(windows))]
    impl TempDir {
        fn new(suffix: &str) -> Self {
            // Use PID + a suffix so concurrent test threads don't collide.
            let dir = std::env::temp_dir().join(format!(
                "llmtrim-test-{}-{}",
                std::process::id(),
                suffix
            ));
            std::fs::create_dir_all(&dir).expect("create temp dir");
            Self(dir)
        }
        fn path(&self) -> &std::path::Path {
            &self.0
        }
    }

    #[cfg(not(windows))]
    impl Drop for TempDir {
        fn drop(&mut self) {
            let _ = std::fs::remove_dir_all(&self.0);
        }
    }

    /// Helper: write a managed block into a candidate file under `base`.
    #[cfg(not(windows))]
    fn write_block_to(base: &std::path::Path, name: &str) {
        let block = format!("{BEGIN}\nexport HTTPS_PROXY=\"http://127.0.0.1:8787\"\n{END}\n");
        std::fs::write(base.join(name), block).expect("write test block");
    }

    /// Block present in two files → both cleaned; function returns both paths.
    #[cfg(not(windows))]
    #[test]
    fn remove_profile_block_in_cleans_all_files_that_contain_block() {
        let dir = TempDir::new("sweep-two");
        let base = dir.path();
        write_block_to(base, ".bashrc");
        write_block_to(base, ".zshrc");
        // .profile intentionally absent

        let cleaned = remove_profile_block_in(base).expect("remove_profile_block_in");
        let mut names: Vec<String> = cleaned
            .iter()
            .map(|p| p.file_name().unwrap().to_string_lossy().to_string())
            .collect();
        names.sort();
        assert_eq!(names, vec![".bashrc", ".zshrc"]);

        // Both files now contain no managed block.
        for name in [".bashrc", ".zshrc"] {
            let content = std::fs::read_to_string(base.join(name)).expect("read back");
            assert!(
                !content.contains(BEGIN),
                "{name} still contains BEGIN marker after sweep"
            );
        }
    }

    /// No files contain the block → Ok(empty vec), no error.
    #[cfg(not(windows))]
    #[test]
    fn remove_profile_block_in_noop_when_no_blocks() {
        let dir = TempDir::new("noop");
        let base = dir.path();
        // Write files with ordinary content, no managed block.
        std::fs::write(base.join(".bashrc"), "export FOO=bar\n").expect("write .bashrc");

        let cleaned = remove_profile_block_in(base).expect("remove_profile_block_in");
        assert!(cleaned.is_empty(), "expected no files cleaned");
        // Content unchanged.
        let content = std::fs::read_to_string(base.join(".bashrc")).expect("read");
        assert_eq!(content, "export FOO=bar\n");
    }

    /// One file is absent/unreadable, others are cleaned without error.
    #[cfg(not(windows))]
    #[test]
    fn remove_profile_block_in_skips_absent_files_cleans_rest() {
        let dir = TempDir::new("skip-absent");
        let base = dir.path();
        // Only .zshrc exists with a block; .bashrc and .profile are absent.
        write_block_to(base, ".zshrc");

        let cleaned = remove_profile_block_in(base).expect("remove_profile_block_in");
        assert_eq!(cleaned.len(), 1);
        assert_eq!(cleaned[0].file_name().unwrap().to_string_lossy(), ".zshrc");
    }

    /// profile_has_block_in returns true when ANY candidate file contains the block.
    #[cfg(not(windows))]
    #[test]
    fn profile_has_block_in_detects_block_in_any_candidate() {
        let dir = TempDir::new("has-block");
        let base = dir.path();

        // Nothing present → false.
        assert!(!profile_has_block_in(base));

        // Block only in .bashrc → still detected.
        write_block_to(base, ".bashrc");
        assert!(profile_has_block_in(base));

        // After sweep → false again.
        remove_profile_block_in(base).expect("sweep");
        assert!(!profile_has_block_in(base));

        // Block only in .zshrc → detected.
        write_block_to(base, ".zshrc");
        assert!(profile_has_block_in(base));
    }

    // ── write_profile_block_in tests ────────────────────────────────────────────────

    /// Round-trip: write then read back the block, confirm it contains valid export lines and
    /// BEGIN/END markers, and that unrelated content above it is preserved.
    #[cfg(not(windows))]
    #[test]
    fn write_profile_block_in_roundtrip_preserves_existing_content() {
        let dir = TempDir::new("wpb-roundtrip");
        let base = dir.path();
        let proxy = "http://127.0.0.1:8787";
        let ca = "/tmp/ca.crt";

        // Pre-populate .bashrc with unrelated content.
        std::fs::write(base.join(".bashrc"), "export FOO=bar\nexport BAZ=qux\n")
            .expect("write pre-existing .bashrc");

        let paths =
            write_profile_block_in(base, "bash", proxy, ca, None).expect("write_profile_block_in");
        // Only `.bashrc` exists and it is the bash default -> that's the sole target.
        assert_eq!(paths, vec![base.join(".bashrc")]);

        let content = std::fs::read_to_string(base.join(".bashrc")).expect("read back");
        // Unrelated content must be preserved above the block.
        assert!(
            content.contains("export FOO=bar"),
            "pre-existing content lost"
        );
        // Block delimiters must be present.
        assert!(content.contains(BEGIN), "BEGIN marker missing");
        assert!(content.contains(END), "END marker missing");
        // POSIX export syntax.
        assert!(
            content.contains(&format!("export HTTPS_PROXY=\"{proxy}\"")),
            "proxy export missing"
        );
        assert!(
            content.contains(&format!("export NODE_EXTRA_CA_CERTS=\"{ca}\"")),
            "CA export missing"
        );
    }

    /// Idempotency: calling write_profile_block_in twice on the same shell must leave exactly
    /// ONE managed block (BEGIN appears exactly once) and preserve unrelated content.
    #[cfg(not(windows))]
    #[test]
    fn write_profile_block_in_idempotent_second_call_does_not_duplicate_block() {
        let dir = TempDir::new("wpb-idempotent");
        let base = dir.path();
        let proxy = "http://127.0.0.1:8787";
        let ca = "/tmp/ca.crt";

        std::fs::write(base.join(".bashrc"), "# user config\n").expect("write .bashrc");

        write_profile_block_in(base, "bash", proxy, ca, None).expect("first write");
        write_profile_block_in(base, "bash", proxy, ca, None).expect("second write");

        let content = std::fs::read_to_string(base.join(".bashrc")).expect("read back");
        let begin_count = content.matches(BEGIN).count();
        assert_eq!(
            begin_count, 1,
            "BEGIN marker appears {begin_count} times after two writes — expected 1"
        );
        assert!(
            content.contains("# user config"),
            "pre-existing content lost after double write"
        );
    }

    /// Re-setup writes to every existing rc file AND the `$SHELL` default, refreshing (not
    /// orphaning) a prior block. Prior bash setup left a block (old port) in `.bashrc`;
    /// re-running under zsh with a new port must update `.bashrc` in place *and* create
    /// `.zshrc`, with the old port gone from both.
    #[cfg(not(windows))]
    #[test]
    fn write_profile_block_in_refreshes_all_existing_and_creates_shell_default() {
        let dir = TempDir::new("wpb-switch");
        let base = dir.path();
        let ca = "/tmp/ca.crt";

        // Prior bash setup: `.bashrc` carries a managed block pinned to the OLD port (8787).
        write_block_to(base, ".bashrc");

        // Re-setup under zsh, NEW port.
        let new_proxy = "http://127.0.0.1:9999";
        let paths =
            write_profile_block_in(base, "zsh", new_proxy, ca, None).expect("write for zsh");
        // Both the existing `.bashrc` and the freshly-created zsh default are written.
        assert!(
            paths.contains(&base.join(".bashrc")),
            "existing .bashrc written: {paths:?}"
        );
        assert!(
            paths.contains(&base.join(".zshrc")),
            "zsh default created: {paths:?}"
        );

        for name in [".bashrc", ".zshrc"] {
            let body = std::fs::read_to_string(base.join(name)).expect("read rc");
            assert_eq!(body.matches(BEGIN).count(), 1, "{name}: exactly one block");
            assert!(body.contains("9999"), "{name}: refreshed to the new port");
            assert!(
                !body.contains("8787"),
                "{name}: old port must be gone (no stale block)"
            );
        }
    }

    /// `$SHELL` default is created even when NO rc file exists yet, and ONLY that file
    /// (no littering `.bashrc`/`.profile` on a fresh zsh-only home).
    #[cfg(not(windows))]
    #[test]
    fn write_profile_block_in_creates_only_the_shell_default_when_none_exist() {
        let dir = TempDir::new("wpb-none");
        let base = dir.path();
        let paths =
            write_profile_block_in(base, "/bin/zsh", "http://127.0.0.1:8788", "/tmp/ca", None)
                .expect("write");
        assert_eq!(
            paths,
            vec![base.join(".zshrc")],
            "only the zsh default is created"
        );
        assert!(base.join(".zshrc").exists());
        for other in [".zprofile", ".bashrc", ".bash_profile", ".profile"] {
            assert!(!base.join(other).exists(), "{other} must not be created");
        }
    }

    /// Multiple shells installed (several rc files present): the block lands in every one,
    /// so whichever shell the terminal launches is covered.
    #[cfg(not(windows))]
    #[test]
    fn write_profile_block_in_writes_to_all_existing_rc_files() {
        let dir = TempDir::new("wpb-all");
        let base = dir.path();
        for f in [".zshrc", ".bashrc", ".profile"] {
            std::fs::write(base.join(f), "# pre-existing\n").expect("seed rc");
        }
        let paths =
            write_profile_block_in(base, "/bin/bash", "http://127.0.0.1:8788", "/tmp/ca", None)
                .expect("write");
        for f in [".zshrc", ".bashrc", ".profile"] {
            assert!(paths.contains(&base.join(f)), "{f} written: {paths:?}");
            let body = std::fs::read_to_string(base.join(f)).expect("read");
            assert_eq!(body.matches(BEGIN).count(), 1, "{f}: one block");
            assert!(body.contains("# pre-existing"), "{f}: kept user content");
        }
        // The two login rc files that did NOT exist stay absent (only existing + default).
        assert!(!base.join(".zprofile").exists());
        assert!(!base.join(".bash_profile").exists());
    }

    // ── strip_block adversarial cases ────────────────────────────────────────────────

    /// Block at the very start of the file (no content before BEGIN).
    #[test]
    fn strip_block_at_file_start() {
        let input = format!("{BEGIN}\nexport X=1\n{END}\nafter\n");
        let out = strip_block(&input);
        assert_eq!(out, "after\n");
    }

    /// Block at the very end of the file (no content after END).
    #[test]
    fn strip_block_at_file_end() {
        let input = format!("before\n{BEGIN}\nexport X=1\n{END}\n");
        let out = strip_block(&input);
        assert_eq!(out, "before\n");
    }

    /// Block in the middle of the file (content before and after).
    #[test]
    fn strip_block_in_the_middle() {
        let input = format!("top\n{BEGIN}\nexport X=1\n{END}\nbottom\n");
        let out = strip_block(&input);
        assert_eq!(out, "top\nbottom\n");
    }

    /// Two stacked (adjacent) managed blocks — both must be stripped.
    #[test]
    fn strip_block_two_stacked_blocks() {
        let input =
            format!("before\n{BEGIN}\nexport X=1\n{END}\n{BEGIN}\nexport Y=2\n{END}\nafter\n");
        let out = strip_block(&input);
        assert_eq!(out, "before\nafter\n");
    }

    /// File with no trailing newline — strip_block must not panic and must return
    /// a sane result (either the content before the block, or the original if no block).
    #[test]
    fn strip_block_no_trailing_newline() {
        // No block, no trailing newline — must return unchanged.
        let no_block = "just some text";
        let out = strip_block(no_block);
        assert_eq!(out, "just some text\n"); // strip_block always appends \n per line

        // With block and no trailing newline after END.
        let with_block = format!("before\n{BEGIN}\nexport X=1\n{END}");
        let out2 = strip_block(&with_block);
        assert_eq!(out2, "before\n");
    }

    // ── env_block syntax verification ───────────────────────────────────────────────

    /// POSIX block: the env lines are valid `export KEY="value"` lines, wrapped in the daemon
    /// liveness guard (`if … ; then` / `fi`). Every non-marker line is either the guard, the
    /// closing `fi`, or an export.
    #[test]
    fn env_block_posix_all_lines_are_valid_exports() {
        let proxy = "http://127.0.0.1:8787";
        let ca = "/home/user/.llmtrim/ca.crt";
        let block = env_block(proxy, ca, None, Syntax::Posix);

        // Collect non-marker, non-empty lines inside the block.
        let inner: Vec<&str> = block.lines().filter(|l| *l != BEGIN && *l != END).collect();
        assert!(!inner.is_empty(), "no inner lines in POSIX block");
        for line in &inner {
            let trimmed = line.trim_start();
            let is_guard = trimmed.starts_with("if ") || trimmed == "fi";
            let is_export = trimmed.starts_with("export ") && trimmed.contains("=\"");
            assert!(
                is_guard || is_export,
                "line is neither the liveness guard nor a valid export: {line:?}"
            );
        }
    }

    /// PowerShell block: every line between the markers uses `$env:` assignment syntax, never
    /// POSIX `export`.
    #[test]
    fn env_block_powershell_all_lines_use_dollar_env() {
        let proxy = "http://127.0.0.1:8787";
        let ca = "C:\\Users\\u\\ca.pem";
        let block = env_block(proxy, ca, None, Syntax::PowerShell);

        let inner: Vec<&str> = block.lines().filter(|l| *l != BEGIN && *l != END).collect();
        assert!(!inner.is_empty(), "no inner lines in PowerShell block");
        for line in &inner {
            assert!(
                line.starts_with("$env:") && line.contains(" = \""),
                "line is not a valid PowerShell env assignment: {line:?}"
            );
            assert!(
                !line.starts_with("export "),
                "POSIX `export` leaked into PowerShell block: {line:?}"
            );
        }
    }
}