/* This Source Code Form is subject to the terms of the Mozilla Public
 * License, v. 2.0. If a copy of the MPL was not distributed with this
 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */

// This file is used to test the mime parser implemented in JS, mostly by means
// of creating custom emitters and verifying that the methods on that emitter
// are called in the correct order. This also tests that the various
// HeaderParser methods are run correctly.

const { MimeParser } = ChromeUtils.importESModule(
  "resource:///modules/mimeParser.sys.mjs"
);

// Utility method to compare objects
function compare_objects(real, expected) {
  // real is a Map; convert it into an object for uneval purposes
  if (typeof real == "object") {
    var newreal = {};
    for (const [k, v] of real) {
      newreal[k] = v;
    }
    real = newreal;
  }
  var a = uneval(real),
    b = uneval(expected);
  // Very long strings don't get printed out fully (unless they're wrong)
  if ((a.length > 100 || b.length > 100) && a == b) {
    Assert.equal(a, b);
  } else {
    Assert.equal(a, b);
  }
}

// Returns and deletes object[field] if present, or undefined if not.
function extract_field(object, field) {
  if (field in object) {
    var result = object[field];
    delete object[field];
    return result;
  }
  return undefined;
}

// A file cache for read_file.
var file_cache = {};

/**
 * Read a file into a string (all line endings become CRLF).
 */
async function read_file(file, start, end) {
  if (!(file in file_cache)) {
    var realFile = do_get_file("../../../data/" + file);
    file_cache[file] = (await IOUtils.readUTF8(realFile.path)).split(
      /\r\n|[\r\n]/
    );
  }
  var contents = file_cache[file];
  if (start !== undefined) {
    contents = contents.slice(start - 1, end - 1);
  }
  return contents.join("\r\n");
}

/**
 * Helper for body tests.
 *
 * Some extra options are listed too:
 * _split: The contents of the file will be passed in packets split by this
 *         regex. Be sure to include the split delimiter in a group so that they
 *         are included in the output packets!
 * _eol: The CRLFs in the input file will be replaced with the given line
 *       ending instead.
 *
 * @param {string} test - The name of test
 * @param {string} file - The name of the file to read (relative to mailnews/data)
 * @param {object} opts - Options for the mime parser, as well as a few extras
 *   detailed bove.
 * @param {object[][]} partspec - An array of [partnum, line start, line end]
 *   detailing the expected parts in the body. It will be expected that the
 *   accumulated body part data for partnum would be the contents
 *   of the file from [line start, line end) [1-based lines]
 */
async function make_body_test(test, file, opts, partspec) {
  const results = [];
  for (const p of partspec) {
    results.push([p[0], await read_file(file, p[1], p[2])]);
  }

  const msgcontents = await read_file(file);
  return [test, msgcontents, opts, results];
}

async function make_bodydecode_test(test, file, opts, expected) {
  const msgcontents = await read_file(file);
  return [test, msgcontents, opts, expected];
}

// This is the expected part specifier for the multipart-complex1 test file,
// specified here because it is used in several cases.
var mpart_complex1 = [
  ["1", 8, 10],
  ["2", 14, 16],
  ["3.1", 22, 24],
  ["4", 29, 31],
  ["5", 33, 35],
];

// Format of tests:
// entry[0] = name of the test
// entry[1] = message (a string or an array of packets)
// entry[2] = options for the MIME parser
// entry[3] = A checker result:
//            either a {partnum: header object} (to check headers)
//            or a [[partnum body], [partnum body], ...] (to check bodies)
//            (the partnums refer to the expected part numbers of the MIME test)
// For body tests, unless you're testing decoding, use make_body_test.
// For decoding tests, use make_bodydecode_test
var parser_tests = [
  // Body tests from data
  // (Note: line numbers are 1-based. Also, to capture trailing EOF, add 2 to
  // the last line number of the file).
  make_body_test("Basic body", "basic1", {}, [["", 3, 5]]),
  make_body_test("Basic multipart", "multipart1", {}, [["1", 10, 12]]),
  make_body_test("Basic multipart", "multipart2", {}, [["1", 8, 11]]),
  make_body_test("Complex multipart", "multipart-complex1", {}, mpart_complex1),
  make_body_test("Truncated multipart", "multipart-complex2", {}, [
    ["1.1.1.1", 20, 24],
    ["2", 26, 56],
    ["3", 59, 61],
  ]),
  make_body_test("No LF multipart", "multipartmalt-detach", {}, [
    ["1", 20, 21],
    ["2.1", 27, 38],
    ["2.2", 42, 43],
    ["2.3", 47, 48],
  ]),
  make_body_test("Raw body", "multipart1", { bodyformat: "raw" }, [
    ["", 4, 14],
  ]),
  make_bodydecode_test(
    "Base64 decode 1",
    "base64-1",
    { bodyformat: "decode" },
    [
      [
        "",
        "\r\nHello, world! (Again...)\r\n\r\nLet's see how well base64 text" +
          " is handled.                            Yay, lots of spaces! There" +
          "'s even a CRLF at the end and one at the beginning, but the output" +
          " shouldn't have it.\r\n",
      ],
    ]
  ),
  make_bodydecode_test(
    "Base64 decode 2",
    "base64-2",
    { bodyformat: "decode" },
    [
      [
        "",
        "<html><body>This is base64 encoded HTML text, and the tags shouldn" +
          "'t be stripped.\r\n<b>Bold text is bold!</b></body></html>\r\n",
      ],
    ]
  ),
  make_body_test("Base64 nodecode", "base64-1", {}, [["", 4, 9]]),
  make_bodydecode_test(
    "QP decode",
    "bug505221",
    { pruneat: "1", bodyformat: "decode" },
    [
      [
        "1",
        '<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.0 Transitional//EN">\r' +
          '\n<HTML><HEAD>\r\n<META HTTP-EQUIV="Content-Type" CONTENT="text/h' +
          'tml; charset=us-ascii">\r\n\r\n\r\n<META content="MSHTML 6.00.600' +
          '0.16735" name=GENERATOR></HEAD>\r\n<BODY> bbb\r\n</BODY></HTML>',
      ],
    ]
  ),

  // Comprehensive tests from the torture test
  make_body_test("Torture regular body", "mime-torture", {}, [
    ["1", 17, 21],
    ["2$.1", 58, 75],
    ["2$.2.1", 83, 97],
    ["2$.3", 102, 130],
    ["3$", 155, 7742],
    ["4", 7747, 8213],
    ["5", 8218, 8242],
    ["6$.1.1", 8284, 8301],
    ["6$.1.2", 8306, 8733],
    ["6$.2.1", 8742, 9095],
    ["6$.2.2", 9100, 9354],
    ["6$.2.3", 9357, 11794],
    ["6$.2.4", 11797, 12155],
    ["6$.3", 12161, 12809],
    ["7$.1", 12844, 12845],
    ["7$.2", 12852, 13286],
    ["7$.3", 13288, 13297],
    ["8$.1", 13331, 13358],
    ["8$.2", 13364, 13734],
    ["9$", 13757, 20179],
    ["10", 20184, 21200],
    ["11$.1", 21223, 22031],
    ["11$.2", 22036, 22586],
    ["12$.1", 22607, 23469],
    ["12$.2", 23474, 23774],
    ["12$.3$.1", 23787, 23795],
    ["12$.3$.2.1", 23803, 23820],
    ["12$.3$.2.2", 23825, 24633],
    ["12$.3$.3", 24640, 24836],
    ["12$.3$.4$", 24848, 25872],
  ]),
  make_body_test("Torture pruneat", "mime-torture", { pruneat: "4" }, [
    ["4", 7747, 8213],
  ]),
];

function test_parser(message, opts, results) {
  var checkingHeaders = !(results instanceof Array);
  var calls = 0,
    dataCalls = 0;
  var fusingParts = extract_field(opts, "_nofuseparts") === undefined;
  var emitter = {
    stack: [],
    startMessage: function emitter_startMsg() {
      Assert.equal(this.stack.length, 0, "no stack at start");
      calls++;
      this.partData = "";
    },
    endMessage: function emitter_endMsg() {
      Assert.equal(this.stack.length, 0, "no stack at end");
      calls++;
    },
    startPart: function emitter_startPart(partNum, headers) {
      this.stack.push(partNum);
      if (checkingHeaders) {
        Assert.ok(partNum in results);
        compare_objects(headers, results[partNum]);
        if (fusingParts) {
          Assert.equal(this.partData, "");
        }
      }
    },
    deliverPartData: function emitter_partData(partNum, data) {
      Assert.equal(this.stack[this.stack.length - 1], partNum);
      try {
        if (!checkingHeaders) {
          if (fusingParts) {
            this.partData += data;
          } else {
            Assert.equal(partNum, results[dataCalls][0]);
            compare_objects(data, results[dataCalls][1]);
          }
        }
      } finally {
        if (!fusingParts) {
          dataCalls++;
        }
      }
    },
    endPart: function emitter_endPart(partNum) {
      if (this.partData != "") {
        Assert.equal(partNum, results[dataCalls][0]);
        compare_objects(this.partData, results[dataCalls][1]);
        dataCalls++;
        this.partData = "";
      }
      Assert.equal(this.stack.pop(), partNum);
    },
  };
  opts.onerror = function (e) {
    throw e;
  };
  MimeParser.parseSync(message, emitter, opts);
  Assert.equal(calls, 2);
  if (!checkingHeaders) {
    Assert.equal(dataCalls, results.length);
  }
}

// Format of tests:
// entry[0] = header
// entry[1] = flags
// entry[2] = result to match
var header_tests = [
  // Parameter passing
  ["multipart/related", MimeParser.HEADER_PARAMETER, ["multipart/related", {}]],
  ["a ; b=v", MimeParser.HEADER_PARAMETER, ["a", { b: "v" }]],
  ["a ; b='v'", MimeParser.HEADER_PARAMETER, ["a", { b: "'v'" }]],
  ['a; b = "v"', MimeParser.HEADER_PARAMETER, ["a", { b: "v" }]],
  ["a;b=1;b=2", MimeParser.HEADER_PARAMETER, ["a", { b: "1" }]],
  ["a;b=2;b=1", MimeParser.HEADER_PARAMETER, ["a", { b: "2" }]],
  ['a;b="a;b"', MimeParser.HEADER_PARAMETER, ["a", { b: "a;b" }]],
  ['a;b="\\\\"', MimeParser.HEADER_PARAMETER, ["a", { b: "\\" }]],
  ['a;b="a\\b\\c"', MimeParser.HEADER_PARAMETER, ["a", { b: "abc" }]],
  ["a;b=1;c=2", MimeParser.HEADER_PARAMETER, ["a", { b: "1", c: "2" }]],
  ['a;b="a\\', MimeParser.HEADER_PARAMETER, ["a", { b: "a" }]],
  ["a;b", MimeParser.HEADER_PARAMETER, ["a", {}]],
  ['a;b=";";c=d', MimeParser.HEADER_PARAMETER, ["a", { b: ";", c: "d" }]],
];

function test_header(headerValue, flags, expected) {
  const result = MimeParser.parseHeaderField(headerValue, flags);
  Assert.equal(result.preSemi, expected[0]);
  compare_objects(result, expected[1]);
}

add_task(async function testit() {
  for (let test of parser_tests) {
    test = await test;
    dump("Testing message " + test[0]);
    if (test[1] instanceof Array) {
      dump(" using " + test[1].length + " packets");
    }
    dump("\n");
    test_parser(test[1], test[2], test[3]);
  }
  for (const test of header_tests) {
    dump("Testing value ->" + test[0] + "<- with flags " + test[1] + "\n");
    test_header(test[0], test[1], test[2]);
  }
});
