"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
const node_fs_1 = require("node:fs");
const node_os_1 = require("node:os");
const node_path_1 = require("node:path");
const _chai_spec_1 = require("./_chai.spec");
const Array_1 = require("./Array");
const CoordinateParser_1 = require("./CoordinateParser");
const ExifDate_1 = require("./ExifDate");
const ExifDateTime_1 = require("./ExifDateTime");
const ExifTool_1 = require("./ExifTool");
const ExifToolVendoredTags_1 = require("./ExifToolVendoredTags");
const File_1 = require("./File");
const Object_1 = require("./Object");
const Sidecars_1 = require("./Sidecars");
const String_1 = require("./String");
const Tags_1 = require("./Tags");
describe("WriteTask", function () {
    this.slow(1); // always show timings
    const ImageExifData = {
        UserComment: "This is a user comment added by exiftool.",
        Artist: "Arturo DeImage",
        Copyright: "© Chuckles McSnortypants, Inc.",
        Credit: "photo by Jenny Snapsalot",
    };
    for (const opts of [
        { maxProcs: 1, maxRetries: 0, useMWG: true },
        { maxProcs: 3, maxRetries: 3, useMWG: false },
    ]) {
        describe(`new ExifTool(${JSON.stringify(opts)})`, () => {
            const exiftool = new ExifTool_1.ExifTool(opts);
            after(() => (0, _chai_spec_1.end)(exiftool));
            describe("with empty WriteTags", () => {
                it("resolves with no options", async () => {
                    const f = await (0, _chai_spec_1.testImg)();
                    const result = await exiftool.write(f, {});
                    (0, _chai_spec_1.expect)((0, Array_1.toArray)(result.warnings)).to.eql([]);
                    (0, _chai_spec_1.expect)(result.unchanged).to.eql(1);
                });
                it("resolves writeArgs: -overwrite_original", async () => {
                    const f = await (0, _chai_spec_1.testImg)();
                    const result = await exiftool.write(f, {}, { writeArgs: ["-overwrite_original"] });
                    (0, _chai_spec_1.expect)((0, Array_1.toArray)(result.warnings)).to.eql([]);
                    (0, _chai_spec_1.expect)(result.unchanged).to.eql(1);
                });
            });
            describe("MWG composite tags", () => {
                it("round-trips Creator", async () => {
                    const f = await (0, _chai_spec_1.testImg)();
                    const Creator = "Ms " + (0, _chai_spec_1.randomChars)(5) + " " + (0, _chai_spec_1.randomChars)(5);
                    await exiftool.write(f, { Creator });
                    const t = await exiftool.read(f);
                    if (opts.useMWG) {
                        (0, _chai_spec_1.expect)(t.Creator).to.eql(Creator, ".Creator");
                        (0, _chai_spec_1.expect)(t.Artist).to.eql(Creator, ".Artist");
                    }
                    else {
                        (0, _chai_spec_1.expect)(t.Creator).to.eql([Creator], ".Creator");
                        (0, _chai_spec_1.expect)(t.Artist).to.eql(undefined, ".Artist");
                    }
                });
                it("round-trips Description", async () => {
                    const f = await (0, _chai_spec_1.testImg)();
                    const Description = "new description " + (0, _chai_spec_1.randomChars)(8);
                    await exiftool.write(f, { Description });
                    const t = await exiftool.read(f);
                    if (opts.useMWG) {
                        (0, _chai_spec_1.expect)(t.Description).to.eql(Description, ".Description");
                        (0, _chai_spec_1.expect)(t.ImageDescription).to.eql(Description, ".ImageDescription");
                        (0, _chai_spec_1.expect)(t["Caption-Abstract"]).to.eql(Description, ".Caption-Abstract");
                    }
                    else {
                        (0, _chai_spec_1.expect)(t.Description).to.eql(Description, ".Description");
                        (0, _chai_spec_1.expect)(t.ImageDescription).to.eql("Prior Title", ".ImageDescription");
                        (0, _chai_spec_1.expect)(t["Caption-Abstract"]).to.eql("Prior Title", ".Caption-Abstract");
                    }
                });
            });
            async function assertRoundTrip({ dest, tagName, inputValue, expectedValue, writeArgs, cmp, }) {
                const fileExists = (0, node_fs_1.existsSync)(dest);
                const wt = {};
                wt[tagName] = inputValue;
                const writeResult = await exiftool.write(dest, wt, {
                    writeArgs,
                });
                (0, _chai_spec_1.expect)(writeResult.warnings).to.eql(undefined, JSON.stringify({
                    writeTags: wt,
                    warnings: writeResult.warnings,
                    writeArgs,
                }));
                if (fileExists) {
                    (0, _chai_spec_1.expect)(writeResult).to.containSubset({ created: 0, updated: 1 });
                }
                else {
                    (0, _chai_spec_1.expect)(writeResult).to.containSubset({ created: 1, updated: 0 });
                }
                const result = (await exiftool.read(dest));
                const expected = expectedValue ?? inputValue;
                const cleanTagName = (0, String_1.stripSuffix)(tagName, "#");
                const actual = result[cleanTagName];
                if (cmp != null) {
                    cmp(actual, result);
                }
                else {
                    (0, _chai_spec_1.expect)(actual).to.eql(expected, JSON.stringify({ src: dest, tagName, expected, actual }));
                }
                return writeResult;
            }
            // Well-supported text tag name:
            const textTagName = "Description";
            // Well-supported multi-value string tag:
            const multiTagName = "TagsList";
            function runRoundTripTests({ withTZ, dest, }) {
                const tzo = withTZ ? "+08:00" : "";
                it("round-trips a comment", async () => {
                    return assertRoundTrip({
                        dest: await dest(),
                        tagName: textTagName,
                        inputValue: "new comment from " + new Date(),
                    });
                });
                it("round-trips a comment with many whitespace flavors", async () => {
                    return assertRoundTrip({
                        dest: await dest(),
                        tagName: textTagName,
                        inputValue: "a\rnew\ncomment\n\r\tfrom\r\n" + new Date(),
                    });
                });
                it("round-trips a non-latin comment", async () => {
                    return assertRoundTrip({
                        dest: await dest(),
                        tagName: textTagName,
                        inputValue: "早安晨之美" + new Date(),
                    });
                });
                it("round-trips a comment with simple and compound codepoint emoji", async () => {
                    return assertRoundTrip({
                        dest: await dest(),
                        tagName: textTagName,
                        inputValue: "⌚✨💑🏽👰🏽🦏🌈🦍🦄🧑‍🤝‍🧑🚵‍♀️ " + new Date(),
                    });
                });
                it("round-trips a comment with non-latin filename", async () => {
                    return assertRoundTrip({
                        dest: await dest("中文.jpg"),
                        tagName: textTagName,
                        inputValue: "new comment from " + new Date(),
                    });
                });
                it("round-trips a non-latin comment with non-latin filename", async () => {
                    return assertRoundTrip({
                        dest: await dest("中文.jpg"),
                        tagName: textTagName,
                        inputValue: "早安晨之美" + new Date(),
                    });
                });
                it("round-trips a rtl comment", async () => {
                    return assertRoundTrip({
                        dest: await dest(),
                        tagName: textTagName,
                        inputValue: "مرحبا بالعالم " + new Date(),
                    });
                });
                it("round-trips a numeric Orientation", async () => {
                    return assertRoundTrip({
                        dest: await dest(),
                        tagName: "Orientation#",
                        inputValue: 1,
                    });
                });
                it("round-trips a string Orientation 90 CW", async () => {
                    return assertRoundTrip({
                        dest: await dest(),
                        tagName: "Orientation#",
                        inputValue: 6,
                    });
                });
                it("round-trips a string Orientation 180 CW", async () => {
                    return assertRoundTrip({
                        dest: await dest(),
                        tagName: "Orientation#",
                        inputValue: 3,
                    });
                });
                it("updates ExposureTime to a specific time", async () => {
                    return assertRoundTrip({
                        dest: await dest(),
                        tagName: "ExposureTime",
                        inputValue: "1/300",
                    });
                });
                it("updates DateTimeOriginal to a specific time", async () => {
                    return assertRoundTrip({
                        dest: await dest(),
                        tagName: "DateTimeOriginal",
                        inputValue: "2017-11-15T12:34:56" + tzo,
                        cmp: (actual) => {
                            (0, _chai_spec_1.expect)(actual.toISOString()).to.eql(`2017-11-15T12:34:56${tzo}`);
                        },
                    });
                });
                it("round-trips list tag array input", async () => {
                    return assertRoundTrip({
                        dest: await dest(),
                        tagName: multiTagName,
                        inputValue: [
                            "one",
                            "two",
                            "three",
                            "commas, and { evil [\t|\r] characters \n }",
                        ],
                    });
                });
                it("updates DateTimeDigitized with TimeZoneOffset", async () => {
                    const src = await dest();
                    const wt = {
                        DateTimeDigitized: new ExifDateTime_1.ExifDateTime(2010, 7, 13, 14, 15, 16, 123),
                        TimeZoneOffset: +8,
                    };
                    await exiftool.write(src, wt);
                    const newTags = await exiftool.read(src);
                    const d = newTags.DateTimeDigitized;
                    (0, _chai_spec_1.expect)(d.toISOString()).to.eql("2010-07-13T14:15:16.123" + tzo, JSON.stringify(d));
                    return;
                });
                it("updates CreateDate to a time with zeroes and OffsetTimeDigitized", async () => {
                    const src = await dest();
                    const wt = {
                        CreateDate: new ExifDateTime_1.ExifDateTime(2019, 1, 2, 0, 0, 0),
                        // We have to clear the GPS info to make the OffsetTime be respected:
                        GPSLatitude: null,
                        GPSLongitude: null,
                        OffsetTimeDigitized: "-05:00",
                    };
                    await exiftool.write(src, wt);
                    const t = await exiftool.read(src);
                    (0, _chai_spec_1.expect)(t.CreateDate?.toString()).to.eql("2019-01-02T00:00:00" + (withTZ ? "-05:00" : ""));
                    return;
                });
                it("updates ReleaseDate to a specific date", async () => {
                    const f = await dest();
                    const wt = {
                        ReleaseDate: ExifDate_1.ExifDate.fromISO("2019-01-02"),
                    };
                    await exiftool.write(f, wt);
                    const newTags = await exiftool.read(f);
                    (0, _chai_spec_1.expect)(newTags.ReleaseDate.toISOString()).to.eql("2019-01-02");
                    return;
                });
                it("writes XMP CreateDate as a year-only partial date", async () => {
                    const f = await dest();
                    const yearOnlyDate = ExifDate_1.ExifDate.fromYear(1980);
                    (0, _chai_spec_1.expect)(yearOnlyDate.isYearOnly()).to.be.true;
                    const wt = {
                        "XMP:CreateDate": yearOnlyDate,
                    };
                    const writeResult = await exiftool.write(f, wt);
                    (0, _chai_spec_1.expect)(writeResult.created + writeResult.updated).to.be.greaterThan(0);
                    // Note: ExifTool may convert partial dates to different formats when reading back
                    // The important thing is that we can write them without errors
                    const newTags = await exiftool.read(f);
                    // Check that some date-related field was written
                    (0, _chai_spec_1.expect)(newTags.CreateDate ??
                        newTags.DateTimeOriginal ??
                        newTags.ModifyDate).to.not.be.undefined;
                    return;
                });
                it("writes XMP CreateDate as a year-month partial date", async () => {
                    const f = await dest();
                    const yearMonthDate = ExifDate_1.ExifDate.fromYearMonth("1980:08");
                    (0, _chai_spec_1.expect)(yearMonthDate.isYearMonth()).to.be.true;
                    const wt = {
                        "XMP:CreateDate": yearMonthDate,
                    };
                    const writeResult = await exiftool.write(f, wt);
                    (0, _chai_spec_1.expect)(writeResult.created + writeResult.updated).to.be.greaterThan(0);
                    // Note: ExifTool may convert partial dates to different formats when reading back
                    // The important thing is that we can write them without errors
                    const newTags = await exiftool.read(f);
                    // Check that some date-related field was written
                    (0, _chai_spec_1.expect)(newTags.CreateDate ??
                        newTags.DateTimeOriginal ??
                        newTags.ModifyDate).to.not.be.undefined;
                    return;
                });
                it("writes XMP CreateDate with a numeric year", async () => {
                    const f = await dest();
                    const wt = {
                        "XMP:CreateDate": 1980,
                    };
                    const writeResult = await exiftool.write(f, wt);
                    (0, _chai_spec_1.expect)(writeResult.created + writeResult.updated).to.be.greaterThan(0);
                    // Note: ExifTool may convert numeric years to different formats when reading back
                    // The important thing is that we can write them without errors
                    const newTags = await exiftool.read(f);
                    // Check that some date-related field was written
                    (0, _chai_spec_1.expect)(newTags.CreateDate ??
                        newTags.DateTimeOriginal ??
                        newTags.ModifyDate).to.not.be.undefined;
                    return;
                });
                it("round-trips a boolean true value", async () => {
                    const file = await dest();
                    // Using a native boolean tag
                    return assertRoundTrip({
                        dest: file,
                        tagName: "AlreadyApplied",
                        inputValue: true,
                        expectedValue: true,
                    });
                });
                it("round-trips a boolean false value", async () => {
                    const file = await dest();
                    // Using a native boolean tag
                    return assertRoundTrip({
                        dest: file,
                        tagName: "AlreadyApplied",
                        inputValue: false,
                        expectedValue: false,
                    });
                });
                function randomFloat(min, max) {
                    return Math.random() * (max - min) + min;
                }
                for (const ignoreZeroZeroLatLon of [false, true]) {
                    describe("round-trips GPS values (attempt to reproduce #131): " +
                        JSON.stringify({ ignoreZeroZeroLatLon }), () => {
                        // Verify there's no shenanigans with negative, zero, or positive
                        // lat/lon combinations:
                        for (const GPSLatitude of [
                            randomFloat(-89, -1),
                            0,
                            39.1132577,
                            randomFloat(1, 89),
                        ]) {
                            for (const GPSLongitude of [
                                randomFloat(-179, -1),
                                -84.6907715,
                                0,
                                randomFloat(1, 179),
                            ]) {
                                it(JSON.stringify({ GPSLatitude, GPSLongitude }), async () => {
                                    exiftool.options.ignoreZeroZeroLatLon =
                                        ignoreZeroZeroLatLon;
                                    const f = await dest();
                                    const result = await exiftool.write(f, {
                                        GPSLatitude,
                                        GPSLongitude,
                                        Title: "title",
                                    });
                                    (0, _chai_spec_1.expect)(result.created + result.updated).to.eql(1);
                                    const tags = await exiftool.read(f, {
                                        ignoreZeroZeroLatLon,
                                    });
                                    if (ignoreZeroZeroLatLon &&
                                        GPSLatitude === 0 &&
                                        GPSLongitude === 0) {
                                        (0, _chai_spec_1.expect)(tags.GPSLatitude).to.eql(undefined);
                                        (0, _chai_spec_1.expect)(tags.GPSLongitude).to.eql(undefined);
                                    }
                                    else {
                                        (0, _chai_spec_1.expect)(tags.GPSLatitude).to.be.closeTo(GPSLatitude, 0.001);
                                        (0, _chai_spec_1.expect)(tags.GPSLongitude).to.be.closeTo(GPSLongitude, 0.001);
                                    }
                                });
                            }
                        }
                    });
                }
                it("round-trips a struct tag", async () => {
                    const struct = [
                        { RegItemId: "item 1", RegOrgId: "org 1" },
                        { RegEntryRole: "role 2", RegOrgId: "org 2" },
                    ];
                    const f = await dest();
                    await exiftool.write(f, { RegistryID: struct });
                    const tags = await exiftool.read(f);
                    (0, _chai_spec_1.expect)(tags.RegistryID).to.eql(struct);
                });
                it("rejects setting to a non-time value", async () => {
                    const src = await dest();
                    (0, _chai_spec_1.expect)((await exiftool.write(src, {
                        DateTimeOriginal: "this is not a time",
                    })).warnings?.join("\n")).to.match(/Invalid date\/time/);
                });
                it("rejects an invalid numeric Orientation", async () => {
                    const src = await dest();
                    (0, _chai_spec_1.expect)((await exiftool.write(src, { "Orientation#": -1 })).warnings?.join("\n")).to.match(/Value below int16u minimum/i);
                });
                it("rejects an invalid string Orientation", async () => {
                    const src = await dest();
                    (0, _chai_spec_1.expect)((await exiftool.write(src, {
                        Orientation: "this isn't a valid orientation",
                    })).warnings?.join("\n")).to.be.match(/Can't convert IFD0:Orientation/i);
                });
                it("tags case-insensitively", async () => {
                    const src = await dest();
                    await exiftool.write(src, { rating: 12 }, [
                        "-overwrite_original",
                    ]);
                    const t = (await exiftool.read(src));
                    // this should compile...
                    (0, _chai_spec_1.expect)(t.rating).to.eql(undefined);
                    // but ExifTool will have done the conversion to "Rating":
                    (0, _chai_spec_1.expect)(t.Rating).to.eql(12);
                });
                it("rejects un-writable tags", async () => {
                    const src = await dest();
                    (0, _chai_spec_1.expect)((await exiftool.write(src, {
                        ImageOffset: 12345,
                    })).warnings?.join("\n")).to.match(/ImageOffset is not writable/i);
                });
                it("handles deleting tags from empty files", async () => {
                    const src = await dest();
                    const isSidecar = (0, Sidecars_1.isSidecarExt)(src);
                    // if sidecar, should be empty:
                    (0, _chai_spec_1.expect)(await (0, File_1.isFileEmpty)(src)).to.eql(isSidecar);
                    await exiftool.write(src, { Orientation: null });
                    // still should be empty:
                    (0, _chai_spec_1.expect)(await (0, File_1.isFileEmpty)(src)).to.eql(isSidecar);
                    if (!isSidecar) {
                        const t = await exiftool.read(src);
                        (0, _chai_spec_1.expect)(t.Orientation).to.eql(undefined);
                    }
                });
                it("removes null values", async () => {
                    const src = await dest();
                    const ExposureTime = "1/4567";
                    // NOTE: Neither XPComment nor Comment are supported by .XMP
                    const UserComment = [
                        "Buenos días",
                        "Schönen Tag",
                        "Добрый день",
                        "良い一日",
                        "יום טוב",
                    ].join(",");
                    await exiftool.write(src, {
                        Title: _chai_spec_1.UnicodeTestMessage,
                        "Orientation#": 3,
                        ExposureTime,
                        UserComment,
                    });
                    {
                        (0, _chai_spec_1.expect)(await (0, File_1.isFileEmpty)(src)).to.eql(false);
                        const t = await exiftool.read(src);
                        (0, _chai_spec_1.expect)(t).to.containSubset({
                            Orientation: 3,
                            ExposureTime,
                            UserComment,
                        });
                    }
                    await exiftool.write(src, { Orientation: null });
                    {
                        (0, _chai_spec_1.expect)(await (0, File_1.isFileEmpty)(src)).to.eql(false);
                        const t = await exiftool.read(src);
                        (0, _chai_spec_1.expect)(t.Orientation).to.eql(undefined);
                        (0, _chai_spec_1.expect)(t).to.containSubset({
                            Title: _chai_spec_1.UnicodeTestMessage,
                            ExposureTime,
                            UserComment,
                        });
                    }
                    await exiftool.write(src, { ExposureTime: null, UserComment: null });
                    {
                        (0, _chai_spec_1.expect)(await (0, File_1.isFileEmpty)(src)).to.eql(false);
                        const t = await exiftool.read(src);
                        (0, _chai_spec_1.expect)(t.Orientation).to.eql(undefined);
                        (0, _chai_spec_1.expect)(t.ExposureTime).to.eql(undefined);
                        (0, _chai_spec_1.expect)(t.UserComment).to.eql(undefined);
                        (0, _chai_spec_1.expect)(t.Title).to.eql(_chai_spec_1.UnicodeTestMessage);
                    }
                });
                it("Accepts a shortcut tag", async () => {
                    // AllDates doesn't accept millisecond precision:
                    const date = "2018-04-17T12:34:56+08:00";
                    const src = await dest();
                    await exiftool.write(src, { AllDates: date });
                    const tags = await exiftool.read(src);
                    (0, _chai_spec_1.expect)(String(tags.DateTimeOriginal)).to.eql(date);
                    (0, _chai_spec_1.expect)(String(tags.CreateDate)).to.eql(date);
                    (0, _chai_spec_1.expect)(String(tags.ModifyDate)).to.eql(date);
                    return;
                });
                it("rejects unknown files", () => {
                    return (0, _chai_spec_1.expect)(exiftool.write((0, node_path_1.join)((0, node_os_1.tmpdir)(), ".nonexistant-" + Date.now()), {
                        Comment: "boom",
                    })).to.be.rejectedWith(/ENOENT|File not found/i);
                });
                it("rejects unknown tags", async () => {
                    const src = await dest();
                    return (0, _chai_spec_1.expect)((await exiftool.write(src, { RandomTag: 123 })).warnings?.join("\n")).to.match(/Tag 'RandomTag' is not defined/);
                });
                // Security regression tests for argument-injection via tag names.
                // These assert the injection is refused BEFORE exiftool is invoked;
                // if this ever regresses, the reporter's PoCs would (1) write
                // ../exploit on the host filesystem and (2) exfiltrate /etc/passwd
                // into the working directory.
                it("rejects argument-injection via tag name (PoC #1: -o redirection)", async () => {
                    const src = await dest();
                    return (0, _chai_spec_1.expect)(exiftool.write(src, {
                        "XMP-dc:Title\n-o\n../exploit\n-XMP-dc:Title": "HELLOWORLD",
                    })).to.be.rejectedWith(/Invalid tag name|control character/);
                });
                it("rejects argument-injection via tag name (PoC #2: -p /etc/passwd)", async () => {
                    const src = await dest();
                    return (0, _chai_spec_1.expect)(exiftool.write(src, {
                        "execute\n-p\n/etc/passwd\n-w!\nleak.txt\ninput.jpg\n-execute\n-Comment": "HELLOWORLD",
                    })).to.be.rejectedWith(/Invalid tag name|control character/);
                });
                it("rejects tag names containing = (value delimiter)", async () => {
                    const src = await dest();
                    return (0, _chai_spec_1.expect)(exiftool.write(src, { "Artist=evil": "x" })).to.be.rejectedWith(/Invalid tag name|control character/);
                });
                // Filename arguments are not covered by validateTagName; they rely
                // on the defense-in-depth control-character check in
                // ExifToolTask.renderCommand. This test pins that safety net for the
                // write path.
                it("rejects filename containing a newline (defense-in-depth)", async () => {
                    return (0, _chai_spec_1.expect)(exiftool.write("input.jpg\n-o\n../exploit", { Artist: "x" })).to.be.rejectedWith(/control character/);
                });
                it("round-trips a struct tag with a ResourceEvent with primitive values", async () => {
                    const inputValue = [
                        {
                            Action: "testing",
                            Changed: "🤷🏿‍♀️",
                        },
                    ];
                    return assertRoundTrip({
                        dest: await dest(),
                        tagName: "History",
                        inputValue,
                    });
                });
                it("round-trips a struct tag with a stringified value", async () => {
                    const inputValue = [
                        {
                            Action: "testing",
                            Changed: "🤷🏿‍♀️",
                            Parameters: JSON.stringify({
                                numeric: 123,
                                string: "hello",
                                meanString: "\n|\r}\t{][(), ",
                            }),
                        },
                    ];
                    return assertRoundTrip({
                        dest: await dest(),
                        tagName: "History",
                        inputValue,
                    });
                });
            }
            describe("round-trip with an image", () => runRoundTripTests({
                withTZ: true,
                dest: (name) => (0, _chai_spec_1.testImg)({
                    srcBasename: name,
                    destBasename: "ĩmägë.jpg",
                }), // non-latin characters
            }));
            describe("round-trip with an XMP sidecar", () => runRoundTripTests({
                withTZ: false, // BOO XMP DOESN'T LIKE TIMEZONES WTH
                dest: (ea) => (0, _chai_spec_1.testFile)((ea ?? "ïmg") + ".xmp"), // < i with diaeresis/umlaut
            }));
            describe("round-trip with an MIE sidecar", () => runRoundTripTests({
                withTZ: true,
                dest: (ea) => (0, _chai_spec_1.testFile)((ea ?? "îmg") + ".mie"), // i with circumflex
            }));
            function mkResourceEvent(o) {
                return {
                    Action: "test",
                    Changed: "rating",
                    InstanceID: "instance-id-" + (0, _chai_spec_1.randomChars)(),
                    Parameters: "value-" + (0, _chai_spec_1.randomChars)(),
                    SoftwareAgent: "PhotoStructure",
                    When: ExifDateTime_1.ExifDateTime.now(),
                    ...o,
                };
            }
            function assertEqlResourceEvents(a, b) {
                if (a != null || b != null) {
                    for (let idx = 0; idx < a.length; idx++) {
                        (0, _chai_spec_1.expect)((0, Object_1.omit)(a[idx], "When")).to.eql((0, Object_1.omit)(b[idx], "When"));
                        (0, _chai_spec_1.assertEqlDateish)(a[idx].When, b[idx].When);
                    }
                }
            }
            async function mkXMP(nativePath, t) {
                const priorContents = {
                    Copyright: "PhotoStructure, Inc. " + (0, _chai_spec_1.randomChars)(),
                    ...t,
                };
                await exiftool.write(nativePath, priorContents);
                (0, _chai_spec_1.expect)(await exiftool.read(nativePath)).to.containSubset((0, Object_1.omit)(priorContents, "History", "Versions"));
            }
            describe("appends History structs", () => {
                it("from no XMP", async () => {
                    const f = await (0, _chai_spec_1.testFile)("image.xmp");
                    const re = mkResourceEvent();
                    await exiftool.write(f, { "History+": re }); // < NOT AN ARRAY
                    // NOTE: This tests ReadTask handles History records properly:
                    const t = (await exiftool.read(f));
                    assertEqlResourceEvents(t.History, [re]);
                });
                it("from empty XMP", async () => {
                    const f = await (0, _chai_spec_1.testFile)("image.xmp");
                    const re = mkResourceEvent();
                    await mkXMP(f);
                    await exiftool.write(f, { "History+": [re] });
                    const t = (await exiftool.read(f));
                    assertEqlResourceEvents(t.History[0], [re]);
                });
                it("from XMP with existing History", async () => {
                    const f = await (0, _chai_spec_1.testFile)("image.xmp");
                    const re1 = mkResourceEvent({ Action: "test-1" });
                    const re2 = mkResourceEvent({ Action: "test-2" });
                    await mkXMP(f, { History: [re1] });
                    await exiftool.write(f, { "History+": [re2] });
                    const t = (await exiftool.read(f));
                    assertEqlResourceEvents(t.History, [re1, re2]);
                });
            });
            describe("replaces History structs", () => {
                it("from empty XMP", async () => {
                    const f = await (0, _chai_spec_1.testFile)("image.xmp");
                    await mkXMP(f);
                    const re = mkResourceEvent();
                    await exiftool.write(f, { History: [re] });
                    const t = (await exiftool.read(f));
                    assertEqlResourceEvents(t.History, [re]);
                });
                it("from XMP with existing History", async () => {
                    const f = await (0, _chai_spec_1.testFile)("image.xmp");
                    const re1 = mkResourceEvent({ Action: "test-1" });
                    const re2 = mkResourceEvent({ Action: "test-2" });
                    await mkXMP(f, { History: [re1] });
                    await exiftool.write(f, { History: [re2] });
                    const t = (await exiftool.read(f));
                    assertEqlResourceEvents(t.History, [re2]);
                });
            });
            function mkVersion(v) {
                return {
                    Comments: "comment " + (0, _chai_spec_1.randomChars)(),
                    Event: mkResourceEvent(),
                    Modifier: "modifier " + (0, _chai_spec_1.randomChars)(),
                    ModifyDate: ExifDateTime_1.ExifDateTime.now(),
                    Version: "version " + (0, _chai_spec_1.randomChars)(),
                    ...v,
                };
            }
            function assertEqlVersions(a, b) {
                for (let idx = 0; idx < a.length; idx++) {
                    const av = a[idx];
                    const bv = b[idx];
                    (0, _chai_spec_1.expect)((0, Object_1.omit)(av, "ModifyDate", "Event")).to.eql((0, Object_1.omit)(bv, "ModifyDate", "Event"));
                    if (av.Event != null || bv.Event != null)
                        assertEqlResourceEvents([av.Event], [bv.Event]);
                    (0, _chai_spec_1.assertEqlDateish)(a[idx].ModifyDate, b[idx].ModifyDate);
                }
            }
            describe("appends Versions structs", () => {
                it("from no XMP", async () => {
                    const f = await (0, _chai_spec_1.testFile)("image.xmp");
                    const v = mkVersion();
                    await exiftool.write(f, { "Versions+": v }); // < NOT AN ARRAY
                    const t = (await exiftool.read(f));
                    assertEqlVersions(t.Versions, [v]);
                });
                it("from empty XMP", async () => {
                    const f = await (0, _chai_spec_1.testFile)("image.xmp");
                    await mkXMP(f);
                    const v = mkVersion();
                    await exiftool.write(f, { "Versions+": v }); // < NOT AN ARRAY
                    const t = (await exiftool.read(f));
                    assertEqlVersions(t.Versions, [v]);
                });
                it("from XMP with existing History", async () => {
                    const f = await (0, _chai_spec_1.testFile)("image.xmp");
                    const v1 = mkVersion({ Modifier: "event-1" });
                    const v2 = mkVersion({ Modifier: "event-2" });
                    await mkXMP(f, { Versions: [v1] });
                    await exiftool.write(f, { "Versions+": [v2] });
                    const t = (await exiftool.read(f));
                    assertEqlVersions(t.Versions, [v1, v2]);
                });
            });
            describe("replaces Versions structs", () => {
                it("from XMP with existing History", async () => {
                    const f = await (0, _chai_spec_1.testFile)("image.xmp");
                    const v1 = mkVersion({ Modifier: "event-1" });
                    const v2 = mkVersion({ Modifier: "event-2" });
                    await mkXMP(f, { Versions: [v1] });
                    await exiftool.write(f, { Versions: v2 }); // < OH SNAP NOT AN ARRAY BUT IT STILL WORKS
                    const t = (await exiftool.read(f));
                    assertEqlVersions(t.Versions, [v2]);
                });
            });
        });
    }
    /**
     * @see https://github.com/photostructure/exiftool-vendored.js/issues/178
     */
    describe("deleteAllTags()", () => {
        const exiftool = new ExifTool_1.ExifTool();
        after(() => (0, _chai_spec_1.end)(exiftool));
        const expectedDefinedTags = [
            "Make",
            "Model",
            "Software",
            "ExposureTime",
            "FNumber",
            "ISO",
            "CreateDate",
            "DateTimeOriginal",
            "LightSource",
            "Flash",
            "FocalLength",
            "SerialNumber",
            "DateTimeUTC",
        ];
        function assertMissingGeneralTags(t) {
            for (const ea of expectedDefinedTags) {
                (0, _chai_spec_1.expect)(t).to.not.haveOwnProperty(ea);
            }
        }
        function assertDefinedGeneralTags(t) {
            for (const ea of expectedDefinedTags) {
                (0, _chai_spec_1.expect)(t).to.haveOwnProperty(ea);
            }
        }
        function isIntrinsticTag(k) {
            return (Tags_1.FileTagsNames.includes(k) ||
                Tags_1.ExifToolTagsNames.includes(k) ||
                ExifToolVendoredTags_1.ExifToolVendoredTagNames.includes(k) ||
                ["ImageSize", "Megapixels"].includes(k));
        }
        function expectedChangedTag(k) {
            return [
                "CurrentIPTCDigest",
                "ExifByteOrder",
                ...Tags_1.FileTagsNames.values,
                ...ExifToolVendoredTags_1.ExifToolVendoredTagNames.values,
            ].includes(k);
        }
        it("deletes all tags by default", async () => {
            const img = await (0, _chai_spec_1.testImg)({ srcBasename: "oly.jpg" });
            const before = await exiftool.read(img);
            (0, _chai_spec_1.expect)(before).to.containSubset(ImageExifData);
            assertDefinedGeneralTags(before);
            await exiftool.deleteAllTags(img);
            const after = await exiftool.read(img);
            assertMissingGeneralTags(after);
            (0, _chai_spec_1.expect)(after).to.not.containSubset(ImageExifData);
            for (const k in ImageExifData) {
                (0, _chai_spec_1.expect)(after).to.not.haveOwnProperty(k);
            }
            // And make sure everything else is gone:
            for (const k in before) {
                if (expectedChangedTag(k))
                    continue;
                if (isIntrinsticTag(k)) {
                    (0, _chai_spec_1.expect)(after[k]).to.eql(before[k], "intrinsic tag " + k);
                }
                else {
                    (0, _chai_spec_1.expect)(after).to.not.haveOwnProperty(k);
                }
            }
        });
        for (const key in ImageExifData) {
            it(`deletes all tags except ${key}`, async () => {
                const img = await (0, _chai_spec_1.testImg)({ srcBasename: "oly.jpg" });
                const before = await exiftool.read(img);
                (0, _chai_spec_1.expect)(before).to.containSubset(ImageExifData);
                assertDefinedGeneralTags(before);
                await exiftool.deleteAllTags(img, { retain: [key] });
                const after = await exiftool.read(img);
                assertMissingGeneralTags(after);
                (0, _chai_spec_1.expect)(after).to.haveOwnProperty(key);
                for (const k in Object.keys(ImageExifData)) {
                    if (k !== key) {
                        (0, _chai_spec_1.expect)(after).to.not.haveOwnProperty(k);
                    }
                }
            });
        }
        it("supports deleting everything-except (issue #178)", async () => {
            const img = await (0, _chai_spec_1.testImg)({ srcBasename: "oly.jpg" });
            const before = await exiftool.read(img);
            (0, _chai_spec_1.expect)(before).to.containSubset(ImageExifData);
            assertDefinedGeneralTags(before);
            await exiftool.deleteAllTags(img, { retain: Object.keys(ImageExifData) });
            const after = await exiftool.read(img);
            assertMissingGeneralTags(after);
            (0, _chai_spec_1.expect)(after).to.containSubset(ImageExifData);
        });
        it("rejects argument-injection via retain tag name", async () => {
            const img = await (0, _chai_spec_1.testImg)({ srcBasename: "oly.jpg" });
            return (0, _chai_spec_1.expect)(exiftool.deleteAllTags(img, {
                retain: ["Artist\n-o\n../retain-exploit"],
            })).to.be.rejectedWith(/Invalid retain tag name|control character/);
        });
        it("supports creating arrays in structs", async () => {
            const f = await (0, _chai_spec_1.testImg)();
            const regionData = {
                RegionInfo: {
                    AppliedToDimensions: { W: 10, H: 10, Unit: "pixel" },
                    RegionList: [
                        {
                            Area: { X: 0.5, Y: 0.5, W: 0.1, H: 0.1, Unit: "normalized" },
                            Rotation: 0,
                            Type: "Face",
                            Name: "randomName1",
                        },
                        {
                            Area: { X: 0.5, Y: 0.5, W: 0.1, H: 0.1, Unit: "normalized" },
                            Rotation: 0,
                            Type: "Face",
                            Name: "randomName2",
                        },
                    ],
                },
            };
            await exiftool.write(f, regionData);
            const after = await exiftool.read(f);
            (0, _chai_spec_1.expect)(after).to.containSubset(regionData);
        });
    });
});
describe("WriteTask (simpler)", () => {
    const exiftool = new ExifTool_1.ExifTool();
    after(() => (0, _chai_spec_1.end)(exiftool));
    describe("GPS coordinate writing", () => {
        // Test coordinates covering all hemispheres:
        const testCoordinates = [
            { lat: 37.7749, lon: -122.4194, desc: "NW - San Francisco" },
            { lat: 40.7128, lon: -74.006, desc: "NW - New York" },
            { lat: -33.8688, lon: 151.2093, desc: "SE - Sydney" },
            { lat: -22.9068, lon: -43.1729, desc: "SW - Rio de Janeiro" },
            { lat: 35.6762, lon: 139.6503, desc: "NE - Tokyo" },
            { lat: 1.3521, lon: 103.8198, desc: "NE near equator - Singapore" },
            { lat: -0.1855, lon: -78.4352, desc: "SW near equator - Quito" },
        ];
        const fileFormats = [
            { type: "JPEG", getPath: () => (0, _chai_spec_1.testImg)({ srcBasename: "noexif.jpg" }) },
            { type: "MIE", getPath: () => (0, _chai_spec_1.testFile)("test.mie") },
            { type: "XMP", getPath: () => (0, _chai_spec_1.testFile)("test.xmp") },
        ];
        for (const format of fileFormats) {
            describe(`${format.type} files`, () => {
                for (const coords of testCoordinates) {
                    const { lat, lon, desc } = coords;
                    const assertTags = (tags) => {
                        // Verify latitude
                        (0, _chai_spec_1.expect)(tags.GPSLatitude).to.be.closeTo(lat, 0.0001);
                        if (format.type !== "MIE") {
                            // .mie files don't get a Ref tag (!?!)
                            (0, _chai_spec_1.expect)(tags.GPSLatitudeRef).to.equal(lat >= 0 ? "N" : "S");
                        }
                        // Verify longitude
                        (0, _chai_spec_1.expect)(tags.GPSLongitude).to.be.closeTo(lon, 0.0001);
                        if (format.type !== "MIE") {
                            // .mie files don't get a Ref tag (!?!)
                            (0, _chai_spec_1.expect)(tags.GPSLongitudeRef).to.equal(lon >= 0 ? "E" : "W");
                        }
                        // Verify GPSPosition format
                        const actual = (0, CoordinateParser_1.parseCoordinates)(tags.GPSPosition);
                        (0, _chai_spec_1.expect)(actual.latitude).to.be.closeTo(lat, 0.0001);
                        (0, _chai_spec_1.expect)(actual.longitude).to.be.closeTo(lon, 0.0001);
                    };
                    it(`writes and reads back coordinates for ${desc}`, async () => {
                        const file = await format.getPath();
                        await exiftool.write(file, {
                            GPSLatitude: lat,
                            GPSLongitude: lon,
                        });
                        assertTags(await exiftool.read(file));
                    });
                    it(`writes and reads back GPSPosition for ${desc}`, async () => {
                        const file = await format.getPath();
                        await exiftool.write(file, {
                            GPSPosition: lat + "," + lon,
                        });
                        assertTags(await exiftool.read(file));
                    });
                    it(`handles clearing GPS data for ${desc}`, async () => {
                        const file = await format.getPath();
                        // First write coordinates
                        await exiftool.write(file, {
                            Title: `Something to avoid "Can't delete all meta information" error`,
                            GPSLatitude: lat,
                            GPSLongitude: lon,
                        });
                        // Then clear them
                        await exiftool.write(file, {
                            GPSLatitude: null,
                            GPSLongitude: null,
                        });
                        // Verify they're gone
                        const tags = await exiftool.read(file);
                        (0, _chai_spec_1.expect)(tags.GPSLatitude).to.eql(undefined);
                        (0, _chai_spec_1.expect)(tags.GPSLongitude).to.eql(undefined);
                        (0, _chai_spec_1.expect)(tags.GPSLatitudeRef).to.eql(undefined);
                        (0, _chai_spec_1.expect)(tags.GPSLongitudeRef).to.eql(undefined);
                        (0, _chai_spec_1.expect)(tags.GPSPosition).to.eql(undefined);
                    });
                }
                it("doesn't reject invalid latitude values", async () => {
                    const file = await format.getPath();
                    await exiftool.write(file, { GPSLatitude: 91 }); // > 90 degrees
                    const tags = await exiftool.readRaw(file);
                    (0, _chai_spec_1.expect)(tags.GPSLatitude).to.eql(`91 deg 0' 0.00" N`);
                });
                it("doesn't reject invalid longitude values", async () => {
                    const file = await format.getPath();
                    await exiftool.write(file, { GPSLongitude: 181 }); // > 180 degrees
                    const tags = await exiftool.readRaw(file);
                    (0, _chai_spec_1.expect)(tags.GPSLongitude).to.eql(`181 deg 0' 0.00" E`);
                });
            });
        }
    });
});
//# sourceMappingURL=WriteTask.spec.js.map