/**
 * buildMimeMessage must be a pure EXTRACTION of the send/draft assembly that
 * lived in mailx-service and mailx-store-web — same bytes out, not "close
 * enough". This compares it against a verbatim copy of the pre-2026-08-09
 * desktop code (below, `legacyBuild`) across the shapes the app actually
 * sends. Volatile parts (boundary strings, Message-ID, Date) are injected or
 * normalised so a diff means a real structural difference.
 *
 * Run: node packages/mailx-types/test/mime-build.test.mjs
 */
import assert from "node:assert";
import {
    buildMimeMessage, encodeQuotedPrintable, encodeHeaderWord,
    encodeAddressHeader, htmlToPlainText, extractInlineImages,
} from "../index.js";

// ── the code as it stood in mailx-service/index.ts before the unification ──
function legacyBuild(o) {
    const hasHtml = !!o.bodyHtml;
    const domain = o.domain;
    const messageId = o.messageId;
    const extracted = hasHtml ? extractInlineImages(o.bodyHtml, domain) : { html: "", images: [] };
    const htmlBody = extracted.html;
    const inlineImages = extracted.images;
    const textBody = o.bodyText || (hasHtml ? htmlToPlainText(htmlBody) : "");
    const htmlEncoded = hasHtml ? encodeQuotedPrintable(htmlBody) : "";
    const textEncoded = encodeQuotedPrintable(textBody);
    const hasAttachments = Array.isArray(o.attachments) && o.attachments.length > 0;
    const commonHeaders = [
        `From: ${encodeAddressHeader(o.from)}`, `To: ${encodeAddressHeader(o.to)}`,
        o.cc ? `Cc: ${encodeAddressHeader(o.cc)}` : null,
        o.bcc ? `Bcc: ${encodeAddressHeader(o.bcc)}` : null,
        `Subject: ${encodeHeaderWord(o.subject)}`, `Date: ${o.now.toUTCString()}`,
        `Message-ID: ${messageId}`,
        o.inReplyTo ? `In-Reply-To: ${o.inReplyTo}` : null,
        o.references?.length ? `References: ${o.references.join(" ")}` : null,
        `MIME-Version: 1.0`,
    ].filter(h => h !== null);

    let rawMessage;
    let n = 0;
    const newBoundary = () => `mailx_B${n++}`;
    const wrap76 = (s) => s.match(/.{1,76}/g)?.join("\r\n") || s;
    const makeInner = () => {
        if (hasHtml) {
            const altBoundary = newBoundary();
            const body =
                `--${altBoundary}\r\n` +
                `Content-Type: text/plain; charset=UTF-8\r\n` +
                `Content-Transfer-Encoding: quoted-printable\r\n\r\n` +
                `${textEncoded}\r\n` +
                `--${altBoundary}\r\n` +
                `Content-Type: text/html; charset=UTF-8\r\n` +
                `Content-Transfer-Encoding: quoted-printable\r\n\r\n` +
                `${htmlEncoded}\r\n` +
                `--${altBoundary}--\r\n`;
            const alt = { headers: [`Content-Type: multipart/alternative; boundary="${altBoundary}"`], body };
            if (inlineImages.length === 0) return alt;
            const relBoundary = newBoundary();
            const relParts = [
                `--${relBoundary}\r\n` + alt.headers.join("\r\n") + `\r\n\r\n` + alt.body,
                ...inlineImages.map(im =>
                    `--${relBoundary}\r\n` +
                    `Content-Type: ${im.mime}\r\n` +
                    `Content-ID: <${im.contentId}>\r\n` +
                    `Content-Disposition: inline\r\n` +
                    `Content-Transfer-Encoding: base64\r\n\r\n` +
                    `${wrap76(im.base64)}\r\n`),
            ];
            return {
                headers: [`Content-Type: multipart/related; boundary="${relBoundary}"; type="multipart/alternative"`],
                body: relParts.join("") + `--${relBoundary}--\r\n`,
            };
        }
        return {
            headers: [`Content-Type: text/plain; charset=UTF-8`, `Content-Transfer-Encoding: quoted-printable`],
            body: textEncoded,
        };
    };
    if (hasAttachments) {
        const mixedBoundary = newBoundary();
        const inner = makeInner();
        const parts = [`--${mixedBoundary}\r\n` + inner.headers.join("\r\n") + `\r\n\r\n` + inner.body];
        for (const att of o.attachments) {
            const filename = (att.filename || "attachment").replace(/[\r\n"]/g, "_");
            const mime = att.mimeType || "application/octet-stream";
            parts.push(
                `--${mixedBoundary}\r\n` +
                `Content-Type: ${mime}; name="${filename}"\r\n` +
                `Content-Disposition: attachment; filename="${filename}"\r\n` +
                `Content-Transfer-Encoding: base64\r\n\r\n` +
                `${wrap76(att.dataBase64 || "")}\r\n`
            );
        }
        rawMessage = [...commonHeaders, `Content-Type: multipart/mixed; boundary="${mixedBoundary}"`].join("\r\n")
            + `\r\n\r\n` + parts.join("") + `--${mixedBoundary}--\r\n`;
    } else {
        const inner = makeInner();
        rawMessage = [...commonHeaders, ...inner.headers].join("\r\n") + `\r\n\r\n` + inner.body;
    }
    return rawMessage;
}

// ── the legacy DRAFT assembly, verbatim ──
function legacyDraft(o) {
    const body = o.bodyHtml || o.bodyText || "";
    return [
        `From: ${encodeAddressHeader(o.from)}`,
        o.to ? `To: ${encodeAddressHeader(o.to)}` : null,
        o.cc ? `Cc: ${encodeAddressHeader(o.cc)}` : null,
        o.bcc ? `Bcc: ${encodeAddressHeader(o.bcc)}` : null,
        `Subject: ${encodeHeaderWord(o.subject)}`, `Date: ${o.now.toUTCString()}`,
        `X-Mailx-Draft-ID: ${o.draftId}`,
        `MIME-Version: 1.0`, `Content-Type: text/html; charset=UTF-8`,
        `Content-Transfer-Encoding: quoted-printable`,
    ].filter(h => h !== null).join("\r\n") + `\r\n\r\n` + encodeQuotedPrintable(body);
}

const PNG = "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mP8z8BQDwAEhQGAhKmMIQAAAABJRU5ErkJggg==";
const now = new Date("Sat, 09 Aug 2026 13:30:00 GMT");
const messageId = "<fixed.id@bob.ma>";
const base = { from: "Bob <bob@bob.ma>", to: "A <a@x.com>", subject: "hello", domain: "bob.ma", now, messageId };

const cases = [
    ["plain text only", { ...base, bodyText: "just text\nwith a line" }],
    ["html (alternative)", { ...base, bodyHtml: "<p>hi <b>there</b></p>" }],
    ["html + explicit text", { ...base, bodyHtml: "<p>hi</p>", bodyText: "hi" }],
    ["html + inline image (related)", { ...base, bodyHtml: `<p>see</p><img src="data:image/png;base64,${PNG}">` }],
    ["text + attachment (mixed)", { ...base, bodyText: "see file", attachments: [{ filename: "a.txt", mimeType: "text/plain", dataBase64: "aGVsbG8=" }] }],
    ["html + inline image + 2 attachments", {
        ...base, bodyHtml: `<p>x</p><img src="data:image/png;base64,${PNG}">`,
        attachments: [
            { filename: 'we"ird\nname.pdf', mimeType: "application/pdf", dataBase64: PNG },
            { dataBase64: "AAAA" },
        ],
    }],
    ["cc + bcc + threading headers", {
        ...base, bodyText: "t", cc: "C <c@x.com>", bcc: "D <d@x.com>",
        inReplyTo: "<prev@x.com>", references: ["<a@x.com>", "<b@x.com>"],
    }],
    ["non-ASCII display name and subject", {
        ...base, from: "Bøb Frankstøn <bob@bob.ma>", to: "Renée <r@x.com>",
        subject: "Canada’s fastest — really", bodyText: "t",
    }],
    ["long base64 folding", { ...base, bodyText: "t", attachments: [{ filename: "big.bin", dataBase64: "Q".repeat(500) }] }],
];

/** extractInlineImages mints a random cid: id per call, so two runs over the
 *  same input differ there by design. Normalise it for the comparison — what
 *  must hold is that the SAME id appears in the <img src> and in that part's
 *  Content-ID, which is asserted separately below. */
const normCid = (s) => s.replace(/img\d+\.[a-z0-9]+@/g, "img.CID@");

let pass = 0;
for (const [name, o] of cases) {
    let n = 0;
    const got = buildMimeMessage({ ...o, unique: () => `B${n++}` }).raw;
    const want = legacyBuild(o);
    assert.strictEqual(normCid(got), normCid(want),
        `MISMATCH in "${name}"\n--- new ---\n${got}\n--- old ---\n${want}`);
    // cid self-consistency: every id occurs exactly twice (src + Content-ID)
    const ids = [...got.matchAll(/img\d+\.[a-z0-9]+@[a-z0-9.]+/g)].map(m => m[0]);
    for (const id of new Set(ids)) {
        assert.strictEqual(ids.filter(x => x === id).length, 2,
            `cid ${id} should appear in both the <img src> and its Content-ID in "${name}"`);
    }
    pass++;
}

// draft path
for (const [name, o] of [
    ["draft html", { ...base, bodyHtml: "<p>draft</p>", draftId: "mailx-draft-1" }],
    ["draft with bcc + alias From", { ...base, from: "Alias <alias@bob.ma>", bodyHtml: "<p>d</p>", cc: "c@x.com", bcc: "b@x.com", draftId: "mailx-draft-2" }],
    ["draft non-ASCII recipient", { ...base, to: "Renée <r@x.com>", bodyHtml: "<p>d</p>", draftId: "mailx-draft-3" }],
]) {
    const got = buildMimeMessage({
        ...o, simpleHtml: true, omitMessageId: true,
        extraHeaders: [`X-Mailx-Draft-ID: ${o.draftId}`],
    }).raw;
    const want = legacyDraft(o);
    assert.strictEqual(got, want, `MISMATCH in "${name}"\n--- new ---\n${got}\n--- old ---\n${want}`);
    pass++;
}

// Structural guarantees that must hold regardless of the legacy comparison.
const withImg = buildMimeMessage({ ...base, bodyHtml: `<img src="data:image/png;base64,${PNG}">`, unique: (() => { let i = 0; return () => `B${i++}`; })() });
assert.match(withImg.raw, /multipart\/related/, "inline image must produce multipart/related");
assert.match(withImg.raw, /Content-ID: </, "inline image must carry a Content-ID");
assert.strictEqual(withImg.inlineImageCount, 1);
assert.ok(!/data:image\/png;base64/.test(withImg.raw), "no data: URI may survive into the wire format");
pass++;

const eai = buildMimeMessage({ ...base, from: "Bøb <bob@bob.ma>", to: "Renée <r@x.com>", subject: "Canada’s", bodyText: "t" });
for (const line of eai.raw.split("\r\n")) {
    if (!line) break;                                  // headers end at the blank line
    assert.ok(!/[-]/.test(line), `8-bit byte survived in header: ${line}`);
}
pass++;

console.log(`mime-build: ${pass} checks passed`);
