/**
 * Phase 0 read-isolation fault test (docs/clean-architecture.md §9).
 *
 * Proves two things before the read-worker ships:
 *   A. CORRECTNESS — the worker returns byte-identical results to the
 *      in-process store for getUnifiedInbox / getMessages / searchMessages.
 *   C. ISOLATION — running reads through the worker keeps the MAIN event loop
 *      free (low event-loop lag), whereas in-process reads block it for the
 *      full duration of each SQLite scan. Main-loop lag is exactly what froze
 *      the IPC pump and produced the "nothing in the summary" timeouts.
 *
 * Runs against a COPY of the live DB so it can't collide with a running
 * daemon (two writers on one WAL file = corruption). List reads need only the
 * .db file, not the .eml store.
 *
 *   node test/phase0-readworker.mjs
 */
import fs from "node:fs";
import path from "node:path";
import os from "node:os";
import { fileURLToPath } from "node:url";
import { MailxDB, Store, FileMessageStore } from "../packages/mailx-store/index.js";
import { spawnDbWorker } from "../packages/mailx-service/db-worker-client.js";
import { getStorePath } from "../packages/mailx-settings/index.js";

const here = path.dirname(fileURLToPath(import.meta.url));
const liveDir = process.env.RMFMAIL_DIR || path.join(os.homedir(), ".rmfmail");
const liveDb = path.join(liveDir, "mailx.db");

function log(...a) { console.log(...a); }
function fail(msg) { console.error("  ✗ FAIL:", msg); process.exitCode = 1; }
function ok(msg) { console.log("  ✓", msg); }

// ── set up a temp copy of the live DB ──────────────────────────────────────
const tmp = fs.mkdtempSync(path.join(os.tmpdir(), "rmf-phase0-"));
// Body reads resolve body_path relative to the REAL store. Reading .eml files
// is read-only and safe alongside a running daemon; we never write here.
const tmpStore = (() => { try { return getStorePath(); } catch { return path.join(tmp, "store"); } })();
fs.mkdirSync(tmpStore, { recursive: true });
log(`\nPhase 0 read-worker fault test`);
log(`  live DB : ${liveDb}`);
log(`  temp dir: ${tmp}`);
if (!fs.existsSync(liveDb)) { fail(`no live DB at ${liveDb}`); process.exit(1); }
for (const suffix of ["", "-wal", "-shm"]) {
    const src = liveDb + suffix;
    if (fs.existsSync(src)) {
        try { fs.copyFileSync(src, path.join(tmp, "mailx.db" + suffix)); }
        catch (e) { log(`  (skip ${suffix || "main"}: ${e.message})`); }
    }
}
const sizeMB = (fs.statSync(path.join(tmp, "mailx.db")).size / 1048576).toFixed(0);
log(`  copied ${sizeMB} MB\n`);

// ── construct main (writer) store + spawn the read worker ──────────────────
let mainDb, store, worker;
try {
    mainDb = new MailxDB(tmp);                 // runs migrations on the COPY (safe)
    store = new Store(mainDb, new FileMessageStore(tmpStore));
} catch (e) {
    fail(`could not open copied DB (likely a torn copy of a live WAL): ${e.message}`);
    process.exit(1);
}
try {
    worker = await spawnDbWorker({ dbPath: tmp, storePath: tmpStore });
    ok("read-worker spawned + init handshake completed");
} catch (e) {
    fail(`worker spawn failed: ${e.message}`);
    process.exit(1);
}

// Pick a real folder for getMessages: first account's first folder with rows.
const accounts = store.getAccounts();
let probeAccount = null, probeFolderId = null;
for (const a of accounts) {
    const folders = store.getFolders(a.id);
    for (const f of folders) {
        const r = store.getMessages({ accountId: a.id, folderId: f.id, page: 1, pageSize: 5 });
        if (r.items.length > 0) { probeAccount = a.id; probeFolderId = f.id; break; }
    }
    if (probeFolderId != null) break;
}
log(`  probe folder: account=${probeAccount} folderId=${probeFolderId}, accounts=${accounts.length}\n`);

// ── TEST A: correctness ────────────────────────────────────────────────────
log("TEST A — worker results match in-process");
function summarize(r) {
    return {
        total: r.total,
        n: r.items.length,
        firstUid: r.items[0]?.uid ?? null,
        firstSubj: (r.items[0]?.subject ?? "").slice(0, 40),
        lastUid: r.items[r.items.length - 1]?.uid ?? null,
    };
}
function eq(label, a, b) {
    const sa = JSON.stringify(summarize(a)), sb = JSON.stringify(summarize(b));
    if (sa === sb) ok(`${label}: identical  ${sa}`);
    else fail(`${label}: MISMATCH\n      in-proc: ${sa}\n      worker : ${sb}`);
}

{
    const ip = store.getUnifiedInbox(1, 50, false);
    const wk = await worker.bus.request("db:getUnifiedInbox", { page: 1, pageSize: 50, flaggedOnly: false });
    eq("getUnifiedInbox(1,50)", ip, wk);
}
if (probeFolderId != null) {
    const q = { accountId: probeAccount, folderId: probeFolderId, page: 1, pageSize: 50, sort: "date", sortDir: "desc" };
    const ip = store.getMessages(q);
    const wk = await worker.bus.request("db:getMessages", q);
    eq("getMessages(folder)", ip, wk);
}
{
    const ip = store.searchMessages("the", 1, 50, undefined, undefined, false);
    const wk = await worker.bus.request("db:searchMessages", { query: "the", page: 1, pageSize: 50, includeTrashSpam: false });
    eq('searchMessages("the")', ip, wk);
}
// getMessage (envelope + cached body parse) — needs the real .eml store.
if (probeFolderId != null) {
    const list = store.getMessages({ accountId: probeAccount, folderId: probeFolderId, page: 1, pageSize: 20 });
    const withBody = list.items.find(m => m.bodyPath) || list.items[0];
    if (withBody) {
        const ip = await store.getMessage(probeAccount, withBody.uid, false, probeFolderId);
        const wk = await worker.bus.request("db:getMessage", { accountId: probeAccount, uid: withBody.uid, allowRemote: false, folderId: probeFolderId });
        const shape = (m) => m ? { cached: m.cached, htmlLen: (m.bodyHtml || "").length, textLen: (m.bodyText || "").length, atts: (m.attachments || []).length, subj: (m.subject || "").slice(0, 30) } : null;
        const sa = JSON.stringify(shape(ip)), sb = JSON.stringify(shape(wk));
        if (sa === sb) ok(`getMessage(uid=${withBody.uid}): identical  ${sa}`);
        else fail(`getMessage: MISMATCH\n      in-proc: ${sa}\n      worker : ${sb}`);
    } else {
        log("  (getMessage: no probe message available, skipped)");
    }
}

// ── TEST B: raw cost of one in-process read on this DB ──────────────────────
log("\nTEST B — single-read cost (in-process, on the main thread)");
{
    const t0 = Date.now();
    store.getUnifiedInbox(1, 50, false);
    const ms = Date.now() - t0;
    log(`  one in-process getUnifiedInbox = ${ms} ms (this much main-thread block PER read under the old design)`);
}

// ── TEST C: event-loop lag — in-process vs worker ──────────────────────────
// A 25ms heartbeat measures how late the event loop fires. Big lag = the
// main thread (hence the IPC pump, hence the UI) is frozen.
function lagMonitor() {
    let maxLag = 0, sum = 0, n = 0, last = Date.now();
    const timer = setInterval(() => {
        const now = Date.now();
        const lag = Math.max(0, now - last - 25);
        maxLag = Math.max(maxLag, lag); sum += lag; n++; last = now;
    }, 25);
    timer.unref?.();
    return { stop() { clearInterval(timer); return { maxLag, avgLag: n ? Math.round(sum / n) : 0 }; } };
}
const READS = 40;
async function yield_() { return new Promise(r => setImmediate(r)); }

log(`\nTEST C — main event-loop lag during ${READS} reads`);
// In-process: each read is a synchronous scan on main → main freezes per read.
{
    const m = lagMonitor();
    for (let i = 0; i < READS; i++) { store.getUnifiedInbox(1, 50, false); await yield_(); }
    const { maxLag, avgLag } = m.stop();
    log(`  in-process : maxLag=${maxLag}ms  avgLag=${avgLag}ms  ← main thread blocked this long`);
    globalThis.__inproc = { maxLag, avgLag };
}
// Worker: each read computes on the worker thread → main only relays messages.
{
    const m = lagMonitor();
    for (let i = 0; i < READS; i++) { await worker.bus.request("db:getUnifiedInbox", { page: 1, pageSize: 50, flaggedOnly: false }); }
    const { maxLag, avgLag } = m.stop();
    log(`  worker     : maxLag=${maxLag}ms  avgLag=${avgLag}ms  ← main thread stayed free`);
    globalThis.__worker = { maxLag, avgLag };
}
{
    const ip = globalThis.__inproc, wk = globalThis.__worker;
    if (wk.maxLag < ip.maxLag) ok(`worker keeps main loop freer: maxLag ${wk.maxLag}ms vs ${ip.maxLag}ms (${(ip.maxLag / Math.max(1, wk.maxLag)).toFixed(1)}× better)`);
    else fail(`worker did NOT reduce main-loop lag (worker ${wk.maxLag}ms vs in-proc ${ip.maxLag}ms)`);
}

// ── teardown ───────────────────────────────────────────────────────────────
await worker.close();
try { fs.rmSync(tmp, { recursive: true, force: true }); } catch { /* */ }
log(`\n${process.exitCode ? "RESULT: FAIL" : "RESULT: PASS"}\n`);
process.exit(process.exitCode || 0);
