llmtrim 0.13.2

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
2611
2612
2613
2614
2615
2616
2617
2618
2619
2620
2621
2622
2623
2624
2625
2626
2627
2628
2629
2630
2631
2632
2633
2634
2635
2636
2637
2638
2639
2640
2641
2642
2643
2644
2645
2646
2647
2648
2649
2650
2651
2652
2653
2654
2655
2656
2657
2658
2659
2660
2661
2662
2663
2664
2665
2666
2667
2668
2669
2670
2671
2672
2673
2674
2675
2676
2677
2678
2679
2680
2681
2682
2683
2684
2685
2686
2687
2688
2689
2690
2691
2692
2693
2694
2695
2696
2697
2698
2699
2700
2701
2702
2703
2704
2705
2706
2707
2708
2709
2710
2711
2712
2713
2714
2715
2716
2717
2718
2719
2720
2721
2722
2723
2724
2725
2726
2727
2728
2729
2730
2731
2732
2733
2734
2735
2736
2737
2738
2739
2740
2741
2742
2743
2744
2745
2746
2747
2748
2749
2750
2751
2752
2753
2754
2755
2756
2757
2758
2759
2760
2761
2762
2763
2764
2765
2766
2767
2768
2769
2770
2771
2772
2773
2774
2775
2776
2777
2778
2779
2780
2781
2782
2783
2784
2785
2786
2787
2788
2789
2790
2791
2792
2793
2794
2795
2796
2797
2798
2799
2800
2801
2802
2803
2804
2805
2806
2807
2808
2809
2810
2811
2812
2813
2814
2815
2816
2817
2818
2819
2820
2821
2822
2823
2824
2825
2826
2827
2828
2829
2830
2831
2832
2833
2834
2835
2836
2837
2838
2839
2840
2841
2842
2843
2844
2845
2846
2847
2848
2849
2850
2851
2852
2853
2854
2855
2856
2857
2858
2859
2860
2861
2862
2863
2864
2865
2866
2867
2868
2869
2870
2871
2872
2873
2874
2875
2876
2877
2878
2879
2880
2881
2882
2883
2884
2885
2886
2887
2888
2889
2890
2891
2892
2893
2894
2895
2896
2897
2898
2899
2900
2901
2902
2903
2904
2905
2906
2907
2908
2909
2910
2911
2912
2913
2914
2915
2916
2917
2918
2919
2920
2921
2922
2923
2924
2925
2926
2927
2928
2929
2930
2931
2932
2933
2934
2935
2936
2937
2938
2939
2940
2941
2942
2943
2944
2945
2946
2947
2948
2949
2950
2951
2952
2953
2954
2955
2956
2957
2958
2959
2960
2961
2962
2963
2964
2965
2966
2967
2968
2969
2970
2971
2972
2973
2974
2975
2976
2977
2978
2979
2980
2981
2982
2983
2984
2985
2986
2987
2988
2989
2990
2991
2992
2993
2994
2995
2996
2997
2998
2999
3000
3001
3002
3003
3004
3005
3006
3007
3008
3009
3010
3011
3012
3013
3014
3015
3016
3017
3018
3019
3020
3021
3022
3023
3024
3025
3026
3027
3028
3029
3030
3031
3032
3033
3034
3035
3036
3037
3038
3039
3040
3041
3042
3043
3044
3045
3046
3047
3048
3049
3050
3051
3052
3053
3054
3055
3056
3057
3058
3059
3060
3061
3062
3063
3064
3065
3066
3067
3068
3069
3070
3071
3072
3073
3074
3075
3076
3077
3078
3079
3080
3081
3082
3083
3084
3085
3086
3087
3088
3089
3090
3091
3092
3093
3094
3095
3096
3097
3098
3099
3100
3101
3102
3103
3104
3105
3106
3107
3108
3109
3110
3111
3112
3113
3114
3115
3116
3117
3118
3119
3120
3121
3122
3123
3124
3125
3126
3127
3128
3129
3130
3131
3132
3133
3134
3135
3136
3137
3138
3139
3140
3141
3142
3143
3144
3145
3146
3147
3148
3149
3150
3151
3152
3153
3154
3155
3156
3157
3158
3159
3160
3161
3162
3163
3164
3165
3166
3167
3168
3169
3170
3171
3172
3173
3174
3175
3176
3177
3178
3179
3180
3181
3182
3183
3184
3185
3186
3187
3188
3189
3190
3191
3192
3193
3194
3195
3196
3197
3198
3199
3200
3201
3202
3203
3204
3205
3206
3207
3208
3209
3210
3211
3212
3213
3214
3215
3216
3217
3218
3219
3220
3221
3222
3223
3224
3225
3226
3227
3228
3229
3230
3231
3232
3233
3234
3235
3236
3237
3238
3239
3240
3241
3242
3243
3244
3245
3246
3247
3248
3249
3250
3251
3252
3253
3254
3255
3256
3257
3258
3259
3260
3261
3262
3263
3264
3265
3266
3267
3268
3269
3270
3271
3272
3273
3274
3275
3276
3277
3278
3279
3280
3281
3282
3283
3284
3285
3286
3287
3288
3289
3290
3291
3292
3293
3294
3295
3296
3297
3298
3299
3300
3301
3302
3303
3304
3305
3306
3307
3308
3309
3310
3311
3312
3313
3314
3315
3316
3317
3318
3319
3320
3321
3322
3323
3324
3325
3326
3327
3328
3329
3330
3331
3332
3333
3334
3335
3336
3337
3338
3339
3340
3341
3342
3343
3344
3345
3346
3347
3348
3349
3350
3351
3352
3353
3354
3355
3356
3357
3358
3359
3360
3361
3362
3363
3364
3365
3366
3367
3368
3369
3370
3371
3372
3373
3374
3375
3376
3377
3378
3379
3380
3381
3382
3383
3384
3385
3386
3387
3388
3389
3390
3391
3392
3393
3394
3395
3396
3397
3398
3399
3400
3401
3402
3403
3404
3405
3406
3407
3408
3409
3410
3411
3412
3413
3414
3415
3416
3417
3418
3419
3420
3421
3422
3423
3424
3425
3426
3427
3428
3429
3430
3431
3432
3433
3434
3435
3436
3437
3438
3439
3440
3441
3442
3443
3444
3445
3446
3447
3448
3449
3450
3451
3452
3453
3454
3455
3456
3457
3458
3459
3460
3461
3462
3463
3464
3465
3466
3467
3468
3469
3470
3471
3472
3473
3474
3475
3476
3477
3478
3479
3480
3481
3482
3483
3484
3485
3486
3487
3488
3489
3490
3491
3492
3493
3494
3495
3496
3497
3498
3499
3500
3501
3502
3503
3504
3505
3506
3507
3508
3509
3510
3511
3512
3513
3514
3515
3516
3517
3518
3519
3520
3521
3522
3523
3524
3525
3526
3527
3528
3529
3530
3531
3532
3533
3534
3535
3536
3537
3538
3539
3540
3541
3542
3543
3544
3545
3546
3547
3548
3549
3550
3551
3552
3553
3554
3555
3556
3557
3558
3559
3560
3561
3562
3563
3564
3565
3566
3567
//! `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 managed vars, in both `NO_PROXY` casings. POSIX form — see
/// [`unset_hint`] for the `$SHELL`-aware variant (fish uses `set -e`).
pub const UNSET_HINT: &str = "unset HTTPS_PROXY HTTP_PROXY NO_PROXY no_proxy NODE_EXTRA_CA_CERTS SSL_CERT_FILE CURL_CA_BUNDLE NODE_USE_ENV_PROXY";

/// Fish equivalent of [`UNSET_HINT`] (`set -e` unsets one or more variables).
/// Unix-only: Windows `unset_hint` always returns the POSIX constant (registry env path).
#[cfg(not(windows))]
const UNSET_HINT_FISH: &str = "set -e HTTPS_PROXY HTTP_PROXY NO_PROXY no_proxy NODE_EXTRA_CA_CERTS SSL_CERT_FILE CURL_CA_BUNDLE NODE_USE_ENV_PROXY";

/// Shell-aware unset snippet for the *current* session. Fish gets `set -e …`; everything else
/// gets the POSIX [`UNSET_HINT`]. Prefers the live-session signal (`FISH_VERSION`, which fish
/// always exports) over `$SHELL`, so an interactive fish started from a bash login shell still
/// gets a pasteable fish command.
pub fn unset_hint() -> &'static str {
    #[cfg(not(windows))]
    {
        if session_is_fish() {
            return UNSET_HINT_FISH;
        }
    }
    UNSET_HINT
}

/// 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.
/// Shared with skip-login's Claude `settings.json` env package so shell profile and settings
/// agree on the bypass list (see `statusline::SubAuthProxyEnv`).
pub(crate) 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: any candidate shell-profile block; Windows:
/// `HKCU\Environment`). Public so `status`/`doctor` can compare the wired port against the
/// daemon's. On POSIX we scan **all** candidate profiles (not only `$SHELL`'s default) so a
/// fish-only install still reports the port when the login shell is bash, and vice versa.
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))]
    {
        let Ok(home) = std::env::var("HOME") else {
            return None;
        };
        configured_port_in(std::path::Path::new(&home))
    }
}

/// Inner seam for [`configured_port`]: first `127.0.0.1:<port>` found in any candidate
/// profile under `base`. Tests pass a temp dir so real `$HOME` is never read.
#[cfg(not(windows))]
fn configured_port_in(base: &std::path::Path) -> Option<u16> {
    for path in candidate_profiles(base) {
        if let Ok(text) = std::fs::read_to_string(path)
            && let Some(port) = parse_proxy_port(&text)
        {
            return Some(port);
        }
    }
    None
}

/// 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 the `NO_PROXY` bypass, the
/// daemon-liveness gate (`llmtrim _alive`), or `NODE_USE_ENV_PROXY` (Node undici proxy for Claude
/// Code). Only a well-formed block qualifies: a malformed BEGIN-without-END is skipped (rewriting
/// it would stack a duplicate), an already-current block is skipped, and a marker the user placed
/// *outside* the block we manage does not count. 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,
        mut node_proxy_in_block,
    ) = (false, 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,
            l if in_block && l.contains("NODE_USE_ENV_PROXY") => node_proxy_in_block = true,
            _ => {}
        }
    }
    saw_begin && saw_end && (!bypass_in_block || !gate_in_block || !node_proxy_in_block)
}

#[cfg(not(windows))]
fn heal_profiles_in(
    base: &std::path::Path,
    proxy: &str,
    ca: &str,
    bundle: Option<&str>,
) -> Result<Vec<PathBuf>> {
    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;
        }
        // Per-path dialect: a stale block in config.fish must be rewritten as fish, not POSIX.
        let block = env_block(proxy, ca, bundle, syntax_for_profile(&path));
        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()
                .map(|p| profile_label(p))
                .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. Claude Code integrations + tray: one ensure pass (statusline, guard, /sub, compact,
    //     tray autostart). Idempotent; honors remembered opt-outs. `--force` / non-TTY take
    //     recommended defaults without asking.
    let ensure_report = crate::ensure::apply(crate::ensure::Options {
        interactive: !force && std::io::IsTerminal::is_terminal(&std::io::stdin()),
        quiet: true,
        restart_daemon: false, // setup reconciles the interceptor below
        download_tray: false,
        install_missing: true,
    })
    .unwrap_or_else(|e| {
        rows.push((
            ui::WARN,
            "Ensure".into(),
            format!("integrations skipped: {e:#}"),
        ));
        crate::ensure::Report::default()
    });
    let compact_changed = ensure_report.applied.contains(&"compact");
    rows.extend(ensure_report.rows);

    // 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 && !compact_changed => {
            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:");
        for line in manual_env_lines(&proxy, &ca, bundle.as_deref()) {
            println!("    {line}");
        }
    }

    // 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 integrations (statusline, guard, /sub, compact) are applied by ensure above —
    // no separate install homework.
    #[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(())
}

/// 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}"),
        )),
    }

    // 2c. Remove only llmtrim-owned Claude window /sub hooks and skill.
    match crate::window_sub::uninstall() {
        Ok(()) => rows.push((
            ui::OK,
            "Window /sub".into(),
            "removed owned Claude Code integration".into(),
        )),
        Err(e) => rows.push((ui::WARN, "Window /sub".into(), format!("not removed: {e}"))),
    }

    // 2c2. Drop the dummy Anthropic auth env we inject while always-sub is on.
    match crate::statusline::clear_sub_auth_env() {
        Ok(crate::statusline::SubAuthEnvChange::Removed) => rows.push((
            ui::OK,
            "Claude auth".into(),
            "removed dummy ANTHROPIC_AUTH_TOKEN from settings".into(),
        )),
        Ok(_) => {}
        Err(e) => rows.push((ui::WARN, "Claude auth".into(), format!("not cleaned: {e}"))),
    }

    // 2d. Unwire the Claude Code guard hook, leaving the user's other hooks in place.
    match crate::guard::unwire() {
        Ok(true) => rows.push((
            ui::OK,
            "Guard".into(),
            "removed from Claude Code settings".into(),
        )),
        Ok(false) => rows.push((ui::NOTE, "Guard".into(), "no hook to remove".into())),
        Err(e) => rows.push((ui::WARN, "Guard".into(), format!("not removed: {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 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 POSIX shells, the universal `.profile`, and
/// fish's config:
/// zsh interactive `.zshrc` / login `.zprofile`; bash interactive `.bashrc` / login
/// `.bash_profile`; `.profile` (sh, and bash-login when no `.bash_profile`); and
/// `.config/fish/config.fish` (fish's only user config — there is no separate login file).
/// Paths are relative to `base` (the home directory); tests pass a temp dir so no real
/// `$HOME` is ever mutated.
///
/// When `base` is the real `$HOME` and `XDG_CONFIG_HOME` points somewhere other than
/// `base/.config`, the fish config under that tree is also a candidate (fish reads
/// `$XDG_CONFIG_HOME/fish/config.fish`). Tests pass a temp `base`, so ambient XDG is never
/// consulted — hermetic suites must not touch the developer's real fish config.
#[cfg(not(windows))]
fn candidate_profiles(base: &std::path::Path) -> Vec<PathBuf> {
    let mut paths: Vec<PathBuf> = [
        ".zshrc",
        ".zprofile",
        ".bashrc",
        ".bash_profile",
        ".profile",
        // Multi-component: Path::join keeps the separators, yielding base/.config/fish/config.fish.
        ".config/fish/config.fish",
    ]
    .iter()
    .map(|f| base.join(f))
    .collect();

    // Custom XDG only when operating on the real home. Comparing against `$HOME` keeps temp-dir
    // tests from ever writing into the ambient XDG tree.
    if let (Ok(home), Ok(xdg)) = (std::env::var("HOME"), std::env::var("XDG_CONFIG_HOME"))
        && std::path::Path::new(&home) == base
    {
        let xdg_fish = PathBuf::from(xdg).join("fish/config.fish");
        if !paths.contains(&xdg_fish) {
            paths.push(xdg_fish);
        }
    }
    paths
}

/// Home-relative path to the default fish config (`~/.config/fish/config.fish`).
#[cfg(not(windows))]
fn fish_config_rel() -> &'static str {
    ".config/fish/config.fish"
}

/// Absolute path to the fish config fish will actually read for this `base` home.
/// When `base` is the real `$HOME` and `XDG_CONFIG_HOME` is set, that is
/// `$XDG_CONFIG_HOME/fish/config.fish` (fish ignores `~/.config` in that case).
/// Otherwise `base/.config/fish/config.fish`. Hermetic tests pass a temp `base` ≠ `$HOME`,
/// so ambient XDG never redirects them.
#[cfg(not(windows))]
fn fish_config_path(base: &std::path::Path) -> PathBuf {
    if let (Ok(home), Ok(xdg)) = (std::env::var("HOME"), std::env::var("XDG_CONFIG_HOME"))
        && std::path::Path::new(&home) == base
        && !xdg.is_empty()
    {
        return PathBuf::from(xdg).join("fish/config.fish");
    }
    base.join(fish_config_rel())
}

/// Strip the llmtrim managed block from **every** candidate shell profile that contains it
/// (bash/zsh/profile **and** fish), 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; 5] = [
    "HTTPS_PROXY",
    "HTTP_PROXY",
    "NO_PROXY",
    "NODE_EXTRA_CA_CERTS",
    "NODE_USE_ENV_PROXY",
];

/// 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")?;
    env.set_value("NODE_USE_ENV_PROXY", &"1".to_string())
        .context("failed to set NODE_USE_ENV_PROXY")?;
    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.
/// `Posix` and `Fish` are written on Unix (picked per target file — a multi-shell home can
/// hold both); `PowerShell` on Windows. Every arm of `env_block` is 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, Debug, PartialEq, Eq)]
enum Syntax {
    Posix,
    Fish,
    PowerShell,
}

/// True when a `$SHELL` (or any path) names fish — basename equality, so `/usr/bin/fish`
/// and `fish` match but `/usr/bin/notfish` does not.
#[cfg_attr(windows, allow(dead_code))]
fn shell_is_fish(shell: &str) -> bool {
    let name = shell.rsplit(['/', '\\']).next().unwrap_or(shell);
    name == "fish"
}

/// True when the *current process* is running under fish: either fish exported
/// `FISH_VERSION` into this environment (interactive fish, or anything launched from one),
/// or `$SHELL` itself is fish. `FISH_VERSION` wins so `stop`/`setup --env` print fish syntax
/// even when the login shell is still bash.
#[cfg_attr(windows, allow(dead_code))]
fn session_is_fish() -> bool {
    session_is_fish_from(
        std::env::var_os("FISH_VERSION").is_some(),
        std::env::var("SHELL").ok().as_deref(),
    )
}

/// Pure core of [`session_is_fish`] — `has_fish_version` mirrors `FISH_VERSION` being set;
/// `shell` is the `$SHELL` value. Split out so unit tests don't mutate process env.
#[cfg_attr(windows, allow(dead_code))]
fn session_is_fish_from(has_fish_version: bool, shell: Option<&str>) -> bool {
    if has_fish_version {
        return true;
    }
    shell.map(shell_is_fish).unwrap_or(false)
}

/// 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`]. Fish returns a multi-component relative path
/// (`.config/fish/config.fish`); `Path::join` handles that.
#[cfg(not(windows))]
fn shell_profile_file(shell: &str) -> &'static str {
    if shell_is_fish(shell) {
        fish_config_rel()
    } else if shell.ends_with("zsh") {
        ".zshrc"
    } else if shell.ends_with("bash") {
        ".bashrc"
    } else {
        ".profile"
    }
}

/// Dialect for a profile path: fish's `config.fish` gets [`Syntax::Fish`]; everything else
/// under the POSIX candidate list gets [`Syntax::Posix`]. Used so a multi-shell home gets the
/// right block shape in each file (never `export` inside fish, never `set -gx` inside bash).
#[cfg(not(windows))]
fn syntax_for_profile(path: &std::path::Path) -> Syntax {
    // Match on the filename so a custom XDG layout ending in `config.fish` still counts,
    // and so tests that seed `base/config.fish` directly also resolve correctly.
    match path.file_name().and_then(|n| n.to_str()) {
        Some("config.fish") => Syntax::Fish,
        _ => Syntax::Posix,
    }
}

/// Short label for setup/uninstall output: basenames for flat rc files, the home-relative
/// `.config/fish/config.fish` for fish (plain `config.fish` would be ambiguous).
#[cfg_attr(windows, allow(dead_code))]
fn profile_label(path: &std::path::Path) -> String {
    let s = path.to_string_lossy();
    if s.ends_with(".config/fish/config.fish") {
        ".config/fish/config.fish".into()
    } else if path.file_name().and_then(|n| n.to_str()) == Some("config.fish") {
        // Non-default layout (e.g. $XDG_CONFIG_HOME/fish/config.fish) — show parent/name.
        match path
            .parent()
            .and_then(|p| p.file_name())
            .and_then(|n| n.to_str())
        {
            Some(parent) => format!("{parent}/config.fish"),
            None => "config.fish".into(),
        }
    } else {
        path.file_name()
            .map(|n| n.to_string_lossy().into_owned())
            .unwrap_or_else(|| path.display().to_string())
    }
}

/// The profile file for the current `$SHELL` (Unix) or PowerShell profile (Windows), and the
/// syntax it uses. On Unix the live write/heal/port paths scan **all** [`candidate_profiles`]
/// instead; this helper remains for the Windows profile arm and as the `$SHELL`-default
/// mapping used by tests of [`shell_profile_file`].
#[cfg_attr(not(windows), allow(dead_code))]
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);
        let path = PathBuf::from(home).join(file);
        let syntax = syntax_for_profile(&path);
        Some((path, syntax))
    }
    #[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 POSIX and fish (lowercase too: 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| {
                    let b = posix_quote(b);
                    format!(
                        "\x20   export SSL_CERT_FILE={b}\n\
                         \x20   export CURL_CA_BUNDLE={b}\n"
                    )
                })
                .unwrap_or_default();
            let (proxy, ca, no_proxy) =
                (posix_quote(proxy), posix_quote(ca), posix_quote(NO_PROXY));
            // NODE_USE_ENV_PROXY: Node 20.18+/22+ undici fetch ignores HTTPS_PROXY unless set
            // (Claude Code's Anthropic client). Without it, skip-login's dummy token hits
            // real Anthropic → 401 Invalid bearer token despite a healthy MITM.
            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\
                 \x20   export NODE_USE_ENV_PROXY='1'\n\
                 {native}fi\n\
                 {END}\n"
            )
        }
        // fish: `set -gx` for exported globals; `command -q` is the quiet existence check;
        // `and` chains the liveness probe; the block closes with `end` (no `then`/`fi`).
        // Same daemon-gated contract as the POSIX arm — a new fish shell after `stop` must
        // not inherit a dead proxy.
        Syntax::Fish => {
            let native = bundle
                .map(|b| {
                    let b = fish_quote(b);
                    format!(
                        "\x20   set -gx SSL_CERT_FILE {b}\n\
                         \x20   set -gx CURL_CA_BUNDLE {b}\n"
                    )
                })
                .unwrap_or_default();
            let (proxy, ca, no_proxy) = (fish_quote(proxy), fish_quote(ca), fish_quote(NO_PROXY));
            format!(
                "{BEGIN}\n\
                 if command -q llmtrim; and llmtrim _alive 2>/dev/null\n\
                 \x20   set -gx HTTPS_PROXY {proxy}\n\
                 \x20   set -gx HTTP_PROXY {proxy}\n\
                 \x20   set -gx NO_PROXY {no_proxy}\n\
                 \x20   set -gx no_proxy {no_proxy}\n\
                 \x20   set -gx NODE_EXTRA_CA_CERTS {ca}\n\
                 \x20   set -gx NODE_USE_ENV_PROXY '1'\n\
                 {native}end\n\
                 {END}\n"
            )
        }
        Syntax::PowerShell => {
            let (proxy, ca, no_proxy) = (
                powershell_quote(proxy),
                powershell_quote(ca),
                powershell_quote(NO_PROXY),
            );
            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\
                 $env:NODE_USE_ENV_PROXY = '1'\n\
                 {END}\n"
            )
        }
    }
}

/// Escape `s` for a single-quoted PowerShell literal: `'…'`, doubling any embedded `'`.
/// Deliberately *not* `#[cfg]`-gated: [`env_block`]'s PowerShell arm is compiled and unit-tested
/// on every platform, so this must exist in a POSIX build too.
fn powershell_quote(s: &str) -> String {
    format!("'{}'", s.replace('\'', "''"))
}

/// Escape `s` for a single-quoted POSIX shell literal: `'…'`, closing/reopening around any
/// embedded `'`. Inside single quotes nothing else is special — `$`, backtick, `\`, `"`, `;`
/// are all literal — so this is safe for arbitrary paths.
///
/// Distinct from [`crate::statusline::shell_quote_path`], which is `#[cfg]`-split and emits
/// *double* quotes on Windows: [`env_block`] emits POSIX syntax on every platform, so it needs
/// a quoter that doesn't change shape with the build target.
fn posix_quote(s: &str) -> String {
    format!("'{}'", s.replace('\'', "'\"'\"'"))
}

/// Escape `s` for a single-quoted fish literal: `'…'`. Inside fish single quotes the only
/// special sequences are `\'` and `\\`, so backslashes are doubled first, then each `'` is
/// turned into `\'`. Same safety contract as [`posix_quote`] — hostile paths stay inert.
fn fish_quote(s: &str) -> String {
    format!("'{}'", s.replace('\\', "\\\\").replace('\'', "\\'"))
}

/// The interceptor env as standalone, ungated shell lines — one assignment per variable, no
/// `BEGIN`/`END` markers and no `llmtrim _alive` liveness gate (unlike [`env_block`], this
/// isn't a persisted block that must self-disable when the daemon later stops; it's a
/// one-shot snippet the caller evals or copies immediately). Shared by `run`'s "no profile
/// found" fallback and `print_env`, so both always report the same variables. Values are
/// single-quoted (as in `env_block`) since this snippet exists specifically to be `eval`'d /
/// `source`'d. Dialect follows the platform (and, on Unix, [`session_is_fish`] —
/// `FISH_VERSION` first, then `$SHELL`): fish sessions get `set -gx`, everyone else on
/// Unix gets `export`, Windows gets `$env:`.
fn manual_env_lines(proxy: &str, ca: &str, bundle: Option<&str>) -> Vec<String> {
    #[cfg(not(windows))]
    {
        manual_env_lines_for(proxy, ca, bundle, session_is_fish())
    }
    #[cfg(windows)]
    {
        let _ = bundle; // no combined bundle on Windows — see env_block's PowerShell arm
        vec![
            format!("$env:HTTPS_PROXY = {}", powershell_quote(proxy)),
            format!("$env:HTTP_PROXY = {}", powershell_quote(proxy)),
            format!("$env:NO_PROXY = {}", powershell_quote(NO_PROXY)),
            format!("$env:NODE_EXTRA_CA_CERTS = {}", powershell_quote(ca)),
            "$env:NODE_USE_ENV_PROXY = '1'".to_string(),
        ]
    }
}

/// Testable core of [`manual_env_lines`] for Unix: `fish = true` emits `set -gx`, otherwise
/// POSIX `export`. Kept separate so unit tests don't depend on the ambient `$SHELL`.
#[cfg(not(windows))]
fn manual_env_lines_for(proxy: &str, ca: &str, bundle: Option<&str>, fish: bool) -> Vec<String> {
    if fish {
        let q = fish_quote;
        let mut lines = vec![
            format!("set -gx HTTPS_PROXY {}", q(proxy)),
            format!("set -gx HTTP_PROXY {}", q(proxy)),
            format!("set -gx NO_PROXY {}", q(NO_PROXY)),
            format!("set -gx no_proxy {}", q(NO_PROXY)),
            format!("set -gx NODE_EXTRA_CA_CERTS {}", q(ca)),
            "set -gx NODE_USE_ENV_PROXY '1'".to_string(),
        ];
        if let Some(b) = bundle {
            lines.push(format!("set -gx SSL_CERT_FILE {}", q(b)));
            lines.push(format!("set -gx CURL_CA_BUNDLE {}", q(b)));
        }
        lines
    } else {
        let q = posix_quote;
        let mut lines = vec![
            format!("export HTTPS_PROXY={}", q(proxy)),
            format!("export HTTP_PROXY={}", q(proxy)),
            format!("export NO_PROXY={}", q(NO_PROXY)),
            format!("export no_proxy={}", q(NO_PROXY)),
            format!("export NODE_EXTRA_CA_CERTS={}", q(ca)),
            "export NODE_USE_ENV_PROXY='1'".to_string(),
        ];
        if let Some(b) = bundle {
            lines.push(format!("export SSL_CERT_FILE={}", q(b)));
            lines.push(format!("export CURL_CA_BUNDLE={}", q(b)));
        }
        lines
    }
}

/// `llmtrim setup --env` — print the interceptor environment variables `setup` would wire,
/// then exit. No shell profile, autostart, or daemon changes; the CA (and, on POSIX, the
/// combined CA bundle) are generated if missing so the printed paths are valid immediately.
/// Port resolution mirrors `run`'s real precedence (`--port` > running daemon > already-
/// configured env > scan from default) via [`resolve_port`], but skips the orphaned-port
/// reclaim dance in `run` — that's tied to actually taking over a port to start a daemon,
/// not to printing.
pub fn print_env(requested: Option<u16>) -> Result<()> {
    let running = crate::daemon::running();
    let port = resolve_port(requested, running.as_ref().map(|s| s.port))?;
    if running.map(|s| s.port) != Some(port) {
        eprintln!(
            "{}",
            ui::warn(
                ui::color_stderr(),
                &format!(
                    "no llmtrim daemon is running on port {port} — start one with \
                     `llmtrim start --port {port}` before eval'ing this, or HTTPS_PROXY will \
                     point at a dead port"
                )
            )
        );
    }
    crate::serve::ensure_ca()?;
    let ca_path = crate::serve::ca_cert_path()?;
    let ca = ca_path.to_string_lossy().into_owned();
    let proxy = format!("http://127.0.0.1:{port}");
    #[cfg(not(windows))]
    let bundle = match ensure_ca_bundle(&ca_path) {
        Ok(b) => b.map(|p| p.to_string_lossy().into_owned()),
        Err(e) => {
            eprintln!(
                "{}",
                ui::warn(ui::color_stderr(), &format!("CA bundle not built: {e}"))
            );
            None
        }
    };
    #[cfg(windows)]
    let bundle: Option<String> = None;
    for line in manual_env_lines(&proxy, &ca, bundle.as_deref()) {
        println!("{line}");
    }
    Ok(())
}

/// Inner seam for [`write_profile_block`]: write into profile files under `base` as the home
/// directory (`$HOME` in production; a temp dir in tests). Sweeps stale blocks from all
/// candidates under `base` first, then writes the new block. Returns the paths written.
#[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 — including fish
    // (via [`fish_config_path`], which honors `$XDG_CONFIG_HOME` when `base` is real `$HOME`).
    // The managed BEGIN/END markers make a block in several files idempotent and cleanly
    // removable. Each file gets its own dialect (POSIX vs fish) via [`syntax_for_profile`].
    //
    // Fish special case: if the fish config's parent dir already exists but `config.fish`
    // does not, still create the config. Users who run fish interactively with a bash/zsh
    // login shell often have the directory (conf.d, functions) without ever creating
    // config.fish — without this, setup would only wire bash and leave fish unwired.
    let mut targets: Vec<PathBuf> = candidate_profiles(base)
        .into_iter()
        .filter(|p| p.exists())
        .collect();
    // Fish shell-default uses [`fish_config_path`] so custom XDG is the create-if-missing
    // target (not a dead `~/.config/fish` fish will never source).
    let shell_default = if shell_is_fish(shell) {
        fish_config_path(base)
    } else {
        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
    }
    // Interactive-fish-with-bash-login: fish config dir often exists without config.fish.
    // Use the same path fish will read (XDG-aware) so we don't invent a dead ~/.config copy
    // when XDG_CONFIG_HOME redirects away from base/.config.
    let fish_cfg = fish_config_path(base);
    if !targets.contains(&fish_cfg) && fish_cfg.parent().is_some_and(|p| p.is_dir()) {
        targets.push(fish_cfg);
    }

    let mut written = Vec::with_capacity(targets.len());
    for path in targets {
        // Fish lives under `.config/fish/` or `$XDG_CONFIG_HOME/fish/`; create parents when new.
        if let Some(parent) = path.parent() {
            std::fs::create_dir_all(parent)
                .with_context(|| format!("failed to create {}", parent.display()))?;
        }
        // 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');
        }
        let block = env_block(proxy, ca, bundle, syntax_for_profile(&path));
        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'"));
        // Node undici needs this to honor HTTPS_PROXY (Claude Code).
        assert!(b.contains("export NODE_USE_ENV_PROXY='1'"));
        // 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))]
    #[test]
    fn manual_env_lines_posix_omits_bundle_vars_when_none() {
        // Use the pure helper — `manual_env_lines` follows the ambient session (FISH_VERSION /
        // $SHELL) and would flip to fish syntax under a fish developer shell.
        let lines = manual_env_lines_for("http://127.0.0.1:8787", "/home/u/ca.pem", None, false);
        assert!(lines.contains(&"export HTTPS_PROXY='http://127.0.0.1:8787'".to_string()));
        assert!(lines.contains(&"export HTTP_PROXY='http://127.0.0.1:8787'".to_string()));
        assert!(lines.contains(&format!("export NO_PROXY='{NO_PROXY}'")));
        assert!(lines.contains(&format!("export no_proxy='{NO_PROXY}'")));
        assert!(lines.contains(&"export NODE_EXTRA_CA_CERTS='/home/u/ca.pem'".to_string()));
        assert!(lines.contains(&"export NODE_USE_ENV_PROXY='1'".to_string()));
        assert!(!lines.iter().any(|l| l.contains("SSL_CERT_FILE")));
        assert!(!lines.iter().any(|l| l.contains("CURL_CA_BUNDLE")));
        // Unlike env_block, this is a standalone eval-able snippet: no BEGIN/END markers,
        // no `llmtrim _alive` liveness gate.
        assert!(
            !lines
                .iter()
                .any(|l| l.contains(BEGIN) || l.contains("_alive"))
        );
    }

    #[cfg(not(windows))]
    #[test]
    fn manual_env_lines_posix_includes_bundle_vars_when_present() {
        let lines = manual_env_lines_for(
            "http://127.0.0.1:8787",
            "/home/u/.llmtrim/ca.pem",
            Some("/home/u/.llmtrim/ca-bundle.pem"),
            false,
        );
        assert!(
            lines.contains(&"export SSL_CERT_FILE='/home/u/.llmtrim/ca-bundle.pem'".to_string())
        );
        assert!(
            lines.contains(&"export CURL_CA_BUNDLE='/home/u/.llmtrim/ca-bundle.pem'".to_string())
        );
    }

    #[cfg(windows)]
    #[test]
    fn manual_env_lines_windows_uses_env_assignment_and_ignores_bundle() {
        let lines = manual_env_lines(
            "http://127.0.0.1:8787",
            "C:\\Users\\u\\.llmtrim\\ca.pem",
            Some("C:\\Users\\u\\.llmtrim\\ca-bundle.pem"),
        );
        assert!(lines.contains(&"$env:HTTPS_PROXY = 'http://127.0.0.1:8787'".to_string()));
        assert!(!lines.iter().any(|l| l.contains("SSL_CERT_FILE")));
        assert!(!lines.iter().any(|l| l.contains("CURL_CA_BUNDLE")));
    }

    #[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}");
        }
    }

    // Same contract for fish: a syntax error in config.fish breaks every new fish session.
    // `fish -n <file>` parses without running (more portable than stdin across fish builds);
    // skip when fish isn't installed.
    #[cfg(unix)]
    #[test]
    fn env_block_fish_is_syntactically_valid_shell() {
        use std::process::Command;
        let block = env_block(
            "http://127.0.0.1:8787",
            "/home/u/ca.pem",
            Some("/home/u/.llmtrim/ca-bundle.pem"),
            Syntax::Fish,
        );
        // Probe first so we don't leave a temp file when fish is absent.
        if Command::new("fish").arg("--version").output().is_err() {
            return;
        }
        let path = std::env::temp_dir().join(format!(
            "llmtrim-fish-syntax-{}-{}.fish",
            std::process::id(),
            std::time::SystemTime::now()
                .duration_since(std::time::UNIX_EPOCH)
                .map(|d| d.as_nanos())
                .unwrap_or(0)
        ));
        std::fs::write(&path, &block).expect("write temp fish block");
        let out = Command::new("fish")
            .arg("-n")
            .arg(&path)
            .output()
            .expect("spawn fish -n");
        let _ = std::fs::remove_file(&path);
        assert!(
            out.status.success(),
            "fish -n rejected the block:\n{block}\nstderr: {}",
            String::from_utf8_lossy(&out.stderr)
        );
    }

    #[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"
        );
    }

    /// A CA path or home dir carrying shell metacharacters must land in the profile as an inert
    /// literal — no command substitution, no variable expansion, no statement break.
    #[test]
    fn env_block_posix_quotes_hostile_values() {
        let hostile = "/home/$(whoami)/`id`/a;b/it's/\"q\"/back\\slash/ca.pem";
        let b = env_block(
            "http://127.0.0.1:8787",
            hostile,
            Some(hostile),
            Syntax::Posix,
        );
        // Every occurrence sits inside single quotes, with embedded `'` broken out and back in.
        let quoted = "'/home/$(whoami)/`id`/a;b/it'\"'\"'s/\"q\"/back\\slash/ca.pem'";
        assert!(
            b.contains(&format!("export NODE_EXTRA_CA_CERTS={quoted}")),
            "hostile CA path not safely quoted: {b}"
        );
        assert!(b.contains(&format!("export SSL_CERT_FILE={quoted}")));
        // No bare interpolation survived anywhere in the block.
        assert!(
            !b.contains("=\"/home/"),
            "value emitted in double quotes: {b}"
        );
        // And a real shell agrees: the block parses, and the value comes back byte-identical
        // (no substitution, no expansion, no statement break).
        let line = b
            .lines()
            .find(|l| l.trim_start().starts_with("export NODE_EXTRA_CA_CERTS="))
            .expect("CA export present");
        if let Ok(out) = std::process::Command::new("sh")
            .arg("-c")
            .arg(format!("{line}\nprintf %s \"$NODE_EXTRA_CA_CERTS\""))
            .output()
        {
            assert!(out.status.success(), "sh rejected {line:?}");
            assert_eq!(String::from_utf8_lossy(&out.stdout), hostile);
        }
    }

    /// The same hostile values through the PowerShell arm: single-quoted literals with `'`
    /// doubled, so `$(...)`, backticks and `;` stay inert.
    #[test]
    fn env_block_powershell_quotes_hostile_values() {
        let hostile = "C:\\Users\\$(whoami)\\`id`\\a;b\\it's\\\"q\"\\ca.pem";
        let b = env_block("http://127.0.0.1:8787", hostile, None, Syntax::PowerShell);
        let quoted = "'C:\\Users\\$(whoami)\\`id`\\a;b\\it''s\\\"q\"\\ca.pem'";
        assert!(
            b.contains(&format!("$env:NODE_EXTRA_CA_CERTS = {quoted}")),
            "hostile CA path not safely quoted: {b}"
        );
        assert!(
            !b.contains(" = \"C:"),
            "value emitted in double quotes: {b}"
        );
    }

    /// The port must still be readable back out of a single-quoted block — `parse_proxy_port`
    /// keys off `127.0.0.1:`, which quoting doesn't touch.
    #[test]
    fn parse_proxy_port_reads_single_quoted_block() {
        for syntax in [Syntax::Posix, Syntax::Fish, Syntax::PowerShell] {
            let b = env_block("http://127.0.0.1:8787", "/home/u/ca.pem", None, syntax);
            assert_eq!(parse_proxy_port(&b), Some(8787));
        }
    }

    #[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 env_block_fish_uses_set_gx_and_gates_on_alive() {
        let b = env_block(
            "http://127.0.0.1:8787",
            "/home/u/ca.pem",
            None,
            Syntax::Fish,
        );
        assert!(
            b.contains("set -gx HTTPS_PROXY 'http://127.0.0.1:8787'"),
            "fish set -gx missing: {b}"
        );
        assert!(b.contains("set -gx NODE_EXTRA_CA_CERTS '/home/u/ca.pem'"));
        assert!(b.contains(&format!("set -gx NO_PROXY '{NO_PROXY}'")));
        assert!(
            b.contains("llmtrim _alive"),
            "fish block must probe daemon liveness"
        );
        // Guard shape: `if …; and …` … `end` — no POSIX `then`/`fi`, no `export`.
        assert!(b.contains("if command -q llmtrim; and llmtrim _alive"));
        assert!(b.contains("\nend\n"), "fish block must close with end");
        assert!(
            !b.contains("export "),
            "POSIX export leaked into fish block"
        );
        assert!(!b.contains("\nfi\n"), "POSIX fi leaked into fish block");
        assert!(!b.contains("$env:"), "PowerShell leaked into fish block");

        // Exports sit inside the guard.
        let if_at = b.find("if command -q llmtrim").expect("guard present");
        let end_at = b.find("\nend\n").expect("guard closed with end");
        let set_at = b.find("set -gx HTTPS_PROXY").expect("set present");
        assert!(
            if_at < set_at && set_at < end_at,
            "set -gx must live inside the liveness guard"
        );

        // Bundle exports gated too.
        let with_bundle = env_block(
            "http://127.0.0.1:8787",
            "/home/u/ca.pem",
            Some("/home/u/.llmtrim/ca-bundle.pem"),
            Syntax::Fish,
        );
        let end_at = with_bundle.find("\nend\n").expect("end");
        let ssl_at = with_bundle
            .find("set -gx SSL_CERT_FILE")
            .expect("bundle set present");
        assert!(ssl_at < end_at, "native-TLS sets must sit inside the guard");
    }

    /// Hostile path through the fish arm: single-quoted with `\'` / `\\` escapes so
    /// `$(…)`, backticks and `;` stay inert.
    #[test]
    fn env_block_fish_quotes_hostile_values() {
        let hostile = "/home/$(whoami)/`id`/a;b/it's/\"q\"/back\\slash/ca.pem";
        let b = env_block(
            "http://127.0.0.1:8787",
            hostile,
            Some(hostile),
            Syntax::Fish,
        );
        // fish_quote: `\` → `\\`, `'` → `\'`.
        let quoted = "'/home/$(whoami)/`id`/a;b/it\\'s/\"q\"/back\\\\slash/ca.pem'";
        assert!(
            b.contains(&format!("set -gx NODE_EXTRA_CA_CERTS {quoted}")),
            "hostile CA path not safely quoted: {b}"
        );
        assert!(b.contains(&format!("set -gx SSL_CERT_FILE {quoted}")));
        assert!(
            !b.contains("set -gx NODE_EXTRA_CA_CERTS \"/home/"),
            "value emitted in double quotes: {b}"
        );
    }

    #[test]
    fn shell_is_fish_matches_path_and_basename() {
        assert!(shell_is_fish("/usr/bin/fish"));
        assert!(shell_is_fish("fish"));
        assert!(shell_is_fish("/usr/local/bin/fish"));
        assert!(!shell_is_fish("/bin/bash"));
        assert!(!shell_is_fish("/bin/zsh"));
        assert!(!shell_is_fish("/usr/bin/notfish"));
        assert!(!shell_is_fish(""));
    }

    #[test]
    fn session_is_fish_from_prefers_fish_version_over_shell() {
        // Interactive fish always exports FISH_VERSION, even when login $SHELL is bash.
        assert!(session_is_fish_from(true, Some("/bin/bash")));
        assert!(session_is_fish_from(true, None));
        assert!(session_is_fish_from(false, Some("/usr/bin/fish")));
        assert!(!session_is_fish_from(false, Some("/bin/bash")));
        assert!(!session_is_fish_from(false, None));
        assert!(!session_is_fish_from(false, Some("/usr/bin/notfish")));
    }

    #[test]
    fn shell_profile_file_maps_fish_to_config_fish() {
        #[cfg(not(windows))]
        {
            assert_eq!(
                shell_profile_file("/usr/bin/fish"),
                ".config/fish/config.fish"
            );
            assert_eq!(shell_profile_file("fish"), ".config/fish/config.fish");
            assert_eq!(shell_profile_file("/bin/zsh"), ".zshrc");
            assert_eq!(shell_profile_file("/bin/bash"), ".bashrc");
            assert_eq!(shell_profile_file("/bin/sh"), ".profile");
        }
    }

    /// Temp-dir `base` must never follow ambient XDG — only real `$HOME` does.
    #[cfg(not(windows))]
    #[test]
    fn fish_config_path_ignores_xdg_when_base_is_not_home() {
        let dir = TempDir::new("fish-xdg-hermetic");
        let base = dir.path();
        let path = fish_config_path(base);
        assert_eq!(path, base.join(".config/fish/config.fish"));
    }

    #[test]
    fn syntax_for_profile_distinguishes_fish() {
        #[cfg(not(windows))]
        {
            assert_eq!(
                syntax_for_profile(std::path::Path::new("/home/u/.config/fish/config.fish")),
                Syntax::Fish
            );
            assert_eq!(
                syntax_for_profile(std::path::Path::new("/home/u/.bashrc")),
                Syntax::Posix
            );
            assert_eq!(
                syntax_for_profile(std::path::Path::new("/home/u/.zshrc")),
                Syntax::Posix
            );
        }
    }

    #[cfg(not(windows))]
    #[test]
    fn managed_block_needs_heal_recognises_current_fish_block() {
        let current = env_block(
            "http://127.0.0.1:8787",
            "/home/u/ca.pem",
            None,
            Syntax::Fish,
        );
        assert!(
            !managed_block_needs_heal(&current),
            "current fish block must not need heal"
        );
        // Old fish-shaped block missing the gate still heals (markers + no _alive).
        let old = format!("{BEGIN}\nset -gx HTTPS_PROXY 'x'\nset -gx NO_PROXY 'y'\n{END}\n");
        assert!(
            managed_block_needs_heal(&old),
            "fish block missing _alive gate needs heal"
        );
    }

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

    #[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",
            ".config/fish/config.fish",
        ] {
            assert!(!base.join(other).exists(), "{other} must not be created");
        }
    }

    /// Fish `$SHELL`: create `~/.config/fish/config.fish` (and its parents) with fish syntax,
    /// and do not litter bash/zsh rc files on a fish-only home.
    #[cfg(not(windows))]
    #[test]
    fn write_profile_block_in_creates_fish_config_with_fish_syntax() {
        let dir = TempDir::new("wpb-fish");
        let base = dir.path();
        let proxy = "http://127.0.0.1:8788";
        let ca = "/tmp/ca.crt";
        let paths = write_profile_block_in(base, "/usr/bin/fish", proxy, ca, None).expect("write");
        let fish_cfg = base.join(".config/fish/config.fish");
        assert_eq!(
            paths,
            vec![fish_cfg.clone()],
            "only fish config created: {paths:?}"
        );
        assert!(fish_cfg.exists(), "fish config file must exist");
        let body = std::fs::read_to_string(&fish_cfg).expect("read fish config");
        assert!(body.contains(BEGIN) && body.contains(END));
        assert!(
            body.contains(&format!("set -gx HTTPS_PROXY '{proxy}'")),
            "fish syntax missing: {body}"
        );
        assert!(
            body.contains("if command -q llmtrim; and llmtrim _alive"),
            "fish liveness guard missing: {body}"
        );
        assert!(
            !body.contains("export "),
            "POSIX must not land in fish config"
        );
        for other in [
            ".zshrc",
            ".bashrc",
            ".profile",
            ".bash_profile",
            ".zprofile",
        ] {
            assert!(
                !base.join(other).exists(),
                "{other} must not be created for fish"
            );
        }
    }

    /// `$SHELL=bash` but `~/.config/fish/` already exists (interactive fish user who never
    /// made config.fish): still create config.fish with fish syntax so fish sessions get the env.
    #[cfg(not(windows))]
    #[test]
    fn write_profile_block_in_creates_fish_config_when_fish_dir_exists_under_bash_shell() {
        let dir = TempDir::new("wpb-fish-dir");
        let base = dir.path();
        let fish_dir = base.join(".config/fish");
        std::fs::create_dir_all(&fish_dir).expect("mkdir fish dir");
        // conf.d-only install — no config.fish yet.
        std::fs::create_dir_all(fish_dir.join("conf.d")).expect("mkdir conf.d");
        std::fs::write(base.join(".bashrc"), "# bash\n").expect("seed bashrc");

        let paths =
            write_profile_block_in(base, "/bin/bash", "http://127.0.0.1:8788", "/tmp/ca", None)
                .expect("write");
        let fish_cfg = base.join(".config/fish/config.fish");
        assert!(
            paths.contains(&fish_cfg),
            "fish config created because fish dir exists: {paths:?}"
        );
        assert!(paths.contains(&base.join(".bashrc")));
        let fish = std::fs::read_to_string(&fish_cfg).expect("read fish");
        assert!(
            fish.contains("set -gx HTTPS_PROXY 'http://127.0.0.1:8788'"),
            "fish dialect required: {fish}"
        );
        assert!(
            !fish.contains("export "),
            "must not write POSIX into fish config"
        );
        let bash = std::fs::read_to_string(base.join(".bashrc")).expect("read bash");
        assert!(bash.contains("export HTTPS_PROXY="));
        assert!(!bash.contains("set -gx"));
    }

    /// Bare home with no fish dir and `$SHELL=bash` must NOT invent a fish config.
    #[cfg(not(windows))]
    #[test]
    fn write_profile_block_in_does_not_invent_fish_config_without_fish_dir() {
        let dir = TempDir::new("wpb-no-fish-dir");
        let base = dir.path();
        let paths =
            write_profile_block_in(base, "/bin/bash", "http://127.0.0.1:8788", "/tmp/ca", None)
                .expect("write");
        assert_eq!(paths, vec![base.join(".bashrc")]);
        assert!(!base.join(".config/fish/config.fish").exists());
        // And we must not have created the fish directory tree either.
        assert!(!base.join(".config/fish").exists());
    }

    /// `configured_port` must find a port wired only into fish config even when `$SHELL` is bash
    /// (profile_target alone would miss it).
    #[cfg(not(windows))]
    #[test]
    fn configured_port_in_finds_port_in_any_candidate_including_fish() {
        let dir = TempDir::new("cfg-port-fish");
        let base = dir.path();
        let fish_cfg = base.join(".config/fish/config.fish");
        std::fs::create_dir_all(fish_cfg.parent().unwrap()).expect("mkdir");
        let block = env_block("http://127.0.0.1:43117", "/tmp/ca", None, Syntax::Fish);
        std::fs::write(&fish_cfg, block).expect("write fish");
        // No bashrc — only fish carries the port.
        assert_eq!(configured_port_in(base), Some(43117));

        // Empty home → none.
        let empty = TempDir::new("cfg-port-empty");
        assert_eq!(configured_port_in(empty.path()), None);
    }

    /// Multi-shell home that already has both `.bashrc` and fish config: each gets its own
    /// dialect (POSIX vs fish), and re-setup under fish still refreshes the bashrc block.
    #[cfg(not(windows))]
    #[test]
    fn write_profile_block_in_writes_fish_and_posix_dialects_side_by_side() {
        let dir = TempDir::new("wpb-multi-fish");
        let base = dir.path();
        let fish_cfg = base.join(".config/fish/config.fish");
        std::fs::create_dir_all(fish_cfg.parent().unwrap()).expect("mkdir fish");
        std::fs::write(base.join(".bashrc"), "# bash pre\n").expect("seed bashrc");
        std::fs::write(&fish_cfg, "# fish pre\n").expect("seed fish");

        let paths = write_profile_block_in(
            base,
            "/usr/bin/fish",
            "http://127.0.0.1:9999",
            "/tmp/ca",
            None,
        )
        .expect("write");
        assert!(
            paths.contains(&base.join(".bashrc")),
            "bashrc written: {paths:?}"
        );
        assert!(paths.contains(&fish_cfg), "fish config written: {paths:?}");

        let bash = std::fs::read_to_string(base.join(".bashrc")).expect("read bash");
        assert!(bash.contains("# bash pre"));
        assert!(bash.contains("export HTTPS_PROXY='http://127.0.0.1:9999'"));
        assert!(
            !bash.contains("set -gx"),
            "fish syntax must not land in bashrc"
        );

        let fish = std::fs::read_to_string(&fish_cfg).expect("read fish");
        assert!(fish.contains("# fish pre"));
        assert!(fish.contains("set -gx HTTPS_PROXY 'http://127.0.0.1:9999'"));
        assert!(
            !fish.contains("export "),
            "POSIX must not land in fish config"
        );
    }

    /// Uninstall sweeps a fish config block just like the POSIX ones.
    #[cfg(not(windows))]
    #[test]
    fn remove_profile_block_in_cleans_fish_config() {
        let dir = TempDir::new("sweep-fish");
        let base = dir.path();
        let fish_cfg = base.join(".config/fish/config.fish");
        std::fs::create_dir_all(fish_cfg.parent().unwrap()).expect("mkdir");
        let block = env_block("http://127.0.0.1:8787", "/tmp/ca", None, Syntax::Fish);
        std::fs::write(&fish_cfg, format!("# keep\n{block}")).expect("write fish");

        let cleaned = remove_profile_block_in(base).expect("remove");
        assert_eq!(cleaned, vec![fish_cfg.clone()]);
        let after = std::fs::read_to_string(&fish_cfg).expect("read");
        assert_eq!(after, "# keep\n");
        assert!(!after.contains(BEGIN));
    }

    /// Heal rewrites a stale fish block into the current fish dialect (not POSIX).
    #[cfg(not(windows))]
    #[test]
    fn heal_rewrites_stale_fish_block_as_fish() {
        let dir = TempDir::new("heal-fish");
        let base = dir.path();
        let fish_cfg = base.join(".config/fish/config.fish");
        std::fs::create_dir_all(fish_cfg.parent().unwrap()).expect("mkdir");
        // Old block: markers + proxy, no NO_PROXY / no _alive gate.
        let old = format!("{BEGIN}\nset -gx HTTPS_PROXY 'http://127.0.0.1:43117'\n{END}\n");
        std::fs::write(&fish_cfg, &old).expect("write");

        let healed =
            heal_profiles_in(base, "http://127.0.0.1:43117", "/home/u/ca.pem", None).expect("heal");
        assert_eq!(healed, vec![fish_cfg.clone()]);
        let after = std::fs::read_to_string(&fish_cfg).expect("read");
        assert!(after.contains("set -gx NO_PROXY"), "bypass added in fish");
        assert!(after.contains("llmtrim _alive"), "gate added in fish");
        assert!(after.contains("set -gx HTTPS_PROXY"), "still fish syntax");
        assert!(
            !after.contains("export "),
            "heal must not rewrite fish as POSIX"
        );
    }

    #[cfg(not(windows))]
    #[test]
    fn manual_env_lines_for_fish_uses_set_gx() {
        let lines = manual_env_lines_for("http://127.0.0.1:8787", "/home/u/ca.pem", None, true);
        assert!(lines.iter().any(|l| l.starts_with("set -gx HTTPS_PROXY ")));
        assert!(lines.iter().all(|l| !l.starts_with("export ")));
        let posix = manual_env_lines_for("http://127.0.0.1:8787", "/home/u/ca.pem", None, false);
        assert!(posix.iter().any(|l| l.starts_with("export HTTPS_PROXY=")));
        assert!(posix.iter().all(|l| !l.starts_with("set -gx ")));
    }

    #[test]
    fn profile_label_shows_fish_path() {
        assert_eq!(
            profile_label(std::path::Path::new("/home/u/.config/fish/config.fish")),
            ".config/fish/config.fish"
        );
        assert_eq!(
            profile_label(std::path::Path::new("/home/u/.bashrc")),
            ".bashrc"
        );
    }

    /// 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. Values are single-quoted so hostile paths can't expand.
    #[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:?}"
            );
        }
    }

    /// Fish block: every non-marker line is the liveness guard, `end`, or a `set -gx` assignment.
    #[test]
    fn env_block_fish_all_lines_are_valid_sets() {
        let proxy = "http://127.0.0.1:8787";
        let ca = "/home/user/.llmtrim/ca.crt";
        let block = env_block(proxy, ca, None, Syntax::Fish);

        let inner: Vec<&str> = block.lines().filter(|l| *l != BEGIN && *l != END).collect();
        assert!(!inner.is_empty(), "no inner lines in fish block");
        for line in &inner {
            let trimmed = line.trim_start();
            let is_guard = trimmed.starts_with("if ") || trimmed == "end";
            let is_set = trimmed.starts_with("set -gx ") && trimmed.contains(" '");
            assert!(
                is_guard || is_set,
                "line is neither the liveness guard nor a valid set -gx: {line:?}"
            );
            assert!(
                !trimmed.starts_with("export "),
                "POSIX export leaked into fish block: {line:?}"
            );
        }
    }
}