// META: global=window,dedicatedworker,shadowrealm

/**
 * btoa() as defined by the HTML5 spec, which mostly just references RFC4648.
 */
function mybtoa(s) {
    // String conversion as required by WebIDL.
    s = String(s);

    // "The btoa() method must throw an INVALID_CHARACTER_ERR exception if the
    // method's first argument contains any character whose code point is
    // greater than U+00FF."
    for (var i = 0; i < s.length; i++) {
        if (s.charCodeAt(i) > 255) {
            return "INVALID_CHARACTER_ERR";
        }
    }

    var out = "";
    for (var i = 0; i < s.length; i += 3) {
        var groupsOfSix = [undefined, undefined, undefined, undefined];
        groupsOfSix[0] = s.charCodeAt(i) >> 2;
        groupsOfSix[1] = (s.charCodeAt(i) & 0x03) << 4;
        if (s.length > i + 1) {
            groupsOfSix[1] |= s.charCodeAt(i + 1) >> 4;
            groupsOfSix[2] = (s.charCodeAt(i + 1) & 0x0f) << 2;
        }
        if (s.length > i + 2) {
            groupsOfSix[2] |= s.charCodeAt(i + 2) >> 6;
            groupsOfSix[3] = s.charCodeAt(i + 2) & 0x3f;
        }
        for (var j = 0; j < groupsOfSix.length; j++) {
            if (typeof groupsOfSix[j] == "undefined") {
                out += "=";
            } else {
                out += btoaLookup(groupsOfSix[j]);
            }
        }
    }
    return out;
}

/**
 * Lookup table for mybtoa(), which converts a six-bit number into the
 * corresponding ASCII character.
 */
function btoaLookup(idx) {
    if (idx < 26) {
        return String.fromCharCode(idx + 'A'.charCodeAt(0));
    }
    if (idx < 52) {
        return String.fromCharCode(idx - 26 + 'a'.charCodeAt(0));
    }
    if (idx < 62) {
        return String.fromCharCode(idx - 52 + '0'.charCodeAt(0));
    }
    if (idx == 62) {
        return '+';
    }
    if (idx == 63) {
        return '/';
    }
    // Throw INVALID_CHARACTER_ERR exception here -- won't be hit in the tests.
}

function btoaException(input) {
    input = String(input);
    for (var i = 0; i < input.length; i++) {
        if (input.charCodeAt(i) > 255) {
            return true;
        }
    }
    return false;
}

function testBtoa(input) {
    // "The btoa() method must throw an INVALID_CHARACTER_ERR exception if the
    // method's first argument contains any character whose code point is
    // greater than U+00FF."
    var normalizedInput = String(input);
    for (var i = 0; i < normalizedInput.length; i++) {
        if (normalizedInput.charCodeAt(i) > 255) {
            assert_throws_dom("InvalidCharacterError", function() { btoa(input); },
                "Code unit " + i + " has value " + normalizedInput.charCodeAt(i) + ", which is greater than 255");
            return;
        }
    }
    assert_equals(btoa(input), mybtoa(input));
    assert_equals(atob(btoa(input)), String(input), "atob(btoa(input)) must be the same as String(input)");
}

var tests = ["עברית", "", "ab", "abc", "abcd", "abcde",
    // This one is thrown in because IE9 seems to fail atob(btoa()) on it.  Or
    // possibly to fail btoa().  I actually can't tell what's happening here,
    // but it doesn't hurt.
    "\xff\xff\xc0",
    // Is your DOM implementation binary-safe?
    "\0a", "a\0b",
    // WebIDL tests.
    undefined, null, 7, 12, 1.5, true, false, NaN, +Infinity, -Infinity, 0, -0,
    {toString: function() { return "foo" }},
];
for (var i = 0; i < 258; i++) {
    tests.push(String.fromCharCode(i));
}
tests.push(String.fromCharCode(10000));
tests.push(String.fromCharCode(65534));
tests.push(String.fromCharCode(65535));

// This is supposed to be U+10000.
tests.push(String.fromCharCode(0xd800, 0xdc00));
tests = tests.map(
    function(elem) {
        var expected = mybtoa(elem);
        if (expected === "INVALID_CHARACTER_ERR") {
            return ["btoa("  + format_value(elem) + ") must raise INVALID_CHARACTER_ERR", elem];
        }
        return ["btoa(" + format_value(elem) + ") == " + format_value(mybtoa(elem)), elem];
    }
);

var everything = "";
for (var i = 0; i < 256; i++) {
    everything += String.fromCharCode(i);
}
tests.push(["btoa(first 256 code points concatenated)", everything]);

generate_tests(testBtoa, tests);

promise_test(() => fetch_json("../../../fetch/data-urls/resources/base64.json").then(runAtobTests), "atob() setup.");

const idlTests = [
  [undefined, null],
  [null, [158, 233, 101]],
  [7, null],
  [12, [215]],
  [1.5, null],
  [true, [182, 187]],
  [false, null],
  [NaN, [53, 163]],
  [+Infinity, [34, 119, 226, 158, 43, 114]],
  [-Infinity, null],
  [0, null],
  [-0, null],
  [{toString: function() { return "foo" }}, [126, 138]],
  [{toString: function() { return "abcd" }}, [105, 183, 29]]
];

function runAtobTests(tests) {
  const allTests = tests.concat(idlTests);
  for(let i = 0; i < allTests.length; i++) {
    const input = allTests[i][0],
          output = allTests[i][1];
    test(() => {
      if(output === null) {
        assert_throws_dom("InvalidCharacterError", () => globalThis.atob(input));
      } else {
        const result = globalThis.atob(input);
        for(let ii = 0; ii < output.length; ii++) {
          assert_equals(result.charCodeAt(ii), output[ii]);
        }
      }
    }, "atob(" + format_value(input) + ")");
  }
}
