"use strict";

AddonTestUtils.init(this);
AddonTestUtils.createAppInfo(
  "xpcshell@tests.mozilla.org",
  "XPCShell",
  "1",
  "42"
);

add_task(async function setup() {
  await AddonTestUtils.promiseStartupManager();
});

add_task(async function test_ext_context_does_have_webidl_bindings() {
  await runExtensionAPITest("should have a browser global object", {
    backgroundScript() {
      const { browser, chrome } = self;

      return {
        hasExtensionAPI: !!browser,
        hasExtensionMockAPI: !!browser?.mockExtensionAPI,
        hasChromeCompatGlobal: !!chrome,
        hasChromeMockAPI: !!chrome?.mockExtensionAPI,
      };
    },
    assertResults({ testResult, testError }) {
      Assert.deepEqual(testError, undefined);
      Assert.deepEqual(
        testResult,
        {
          hasExtensionAPI: true,
          hasExtensionMockAPI: true,
          hasChromeCompatGlobal: true,
          hasChromeMockAPI: true,
        },
        "browser and browser.test WebIDL API bindings found"
      );
    },
  });
});

add_task(async function test_propagated_extension_error() {
  await runExtensionAPITest(
    "should throw an extension error on ResultType::EXTENSION_ERROR",
    {
      backgroundScript({ testAsserts }) {
        try {
          const api = self.browser.mockExtensionAPI;
          api.methodSyncWithReturn("arg0", 1, { value: "arg2" });
        } catch (err) {
          testAsserts.isErrorInstance(err);
          throw err;
        }
      },
      mockAPIRequestHandler() {
        return {
          type: Ci.mozIExtensionAPIRequestResult.EXTENSION_ERROR,
          value: new Error("Fake Extension Error"),
        };
      },
      assertResults({ testError }) {
        Assert.deepEqual(testError?.message, "Fake Extension Error");
      },
    }
  );
});

add_task(async function test_system_errors_donot_leak() {
  function assertResults({ testError }) {
    ok(
      testError?.message?.match(/An unexpected error occurred/),
      `Got the general unexpected error as expected: ${testError?.message}`
    );
  }

  function mockAPIRequestHandler() {
    throw new Error("Fake handleAPIRequest exception");
  }

  const msg =
    "should throw an unexpected error occurred if handleAPIRequest throws";

  await runExtensionAPITest(`sync method ${msg}`, {
    backgroundScript({ testAsserts }) {
      try {
        self.browser.mockExtensionAPI.methodSyncWithReturn("arg0");
      } catch (err) {
        testAsserts.isErrorInstance(err);
        throw err;
      }
    },
    mockAPIRequestHandler,
    assertResults,
  });

  await runExtensionAPITest(`async method ${msg}`, {
    backgroundScript({ testAsserts }) {
      try {
        self.browser.mockExtensionAPI.methodAsync("arg0");
      } catch (err) {
        testAsserts.isErrorInstance(err);
        throw err;
      }
    },
    mockAPIRequestHandler,
    assertResults,
  });

  await runExtensionAPITest(`no return method ${msg}`, {
    backgroundScript({ testAsserts }) {
      try {
        self.browser.mockExtensionAPI.methodNoReturn("arg0");
      } catch (err) {
        testAsserts.isErrorInstance(err);
        throw err;
      }
    },
    mockAPIRequestHandler,
    assertResults,
  });
});

add_task(async function test_call_sync_function_result() {
  await runExtensionAPITest(
    "sync API methods should support structured clonable return values",
    {
      backgroundScript({ testAsserts }) {
        const api = self.browser.mockExtensionAPI;
        const results = {
          string: api.methodSyncWithReturn("string-result"),
          nested_prop: api.methodSyncWithReturn({
            string: "123",
            number: 123,
            date: new Date("2020-09-20"),
            map: new Map([
              ["a", 1],
              ["b", 2],
            ]),
          }),
        };

        testAsserts.isInstanceOf(results.nested_prop.date, "Date");
        testAsserts.isInstanceOf(results.nested_prop.map, "Map");
        return results;
      },
      mockAPIRequestHandler(policy, request) {
        if (request.apiName === "methodSyncWithReturn") {
          // Return the first argument unmodified, which will be checked in the
          // resultAssertFn above.
          return {
            type: Ci.mozIExtensionAPIRequestResult.RETURN_VALUE,
            value: request.args[0],
          };
        }
        throw new Error("Unexpected API method");
      },
      assertResults({ testResult, testError }) {
        Assert.deepEqual(testError, null, "Got no error as expected");
        Assert.deepEqual(testResult, {
          string: "string-result",
          nested_prop: {
            string: "123",
            number: 123,
            date: new Date("2020-09-20"),
            map: new Map([
              ["a", 1],
              ["b", 2],
            ]),
          },
        });
      },
    }
  );
});

add_task(async function test_call_sync_fn_missing_return() {
  await runExtensionAPITest(
    "should throw an unexpected error occurred on missing return value",
    {
      backgroundScript() {
        self.browser.mockExtensionAPI.methodSyncWithReturn("arg0");
      },
      mockAPIRequestHandler() {
        return undefined;
      },
      assertResults({ testError }) {
        ok(
          testError?.message?.match(/An unexpected error occurred/),
          `Got the general unexpected error as expected: ${testError?.message}`
        );
      },
    }
  );
});

add_task(async function test_call_async_throw_extension_error() {
  await runExtensionAPITest(
    "an async function can throw an error occurred for param validation errors",
    {
      backgroundScript({ testAsserts }) {
        try {
          self.browser.mockExtensionAPI.methodAsync("arg0");
        } catch (err) {
          testAsserts.isErrorInstance(err);
          throw err;
        }
      },
      mockAPIRequestHandler() {
        return {
          type: Ci.mozIExtensionAPIRequestResult.EXTENSION_ERROR,
          value: new Error("Fake Param Validation Error"),
        };
      },
      assertResults({ testError }) {
        Assert.deepEqual(testError?.message, "Fake Param Validation Error");
      },
    }
  );
});

add_task(async function test_call_async_reject_error() {
  await runExtensionAPITest(
    "an async function rejected promise should propagate extension errors",
    {
      async backgroundScript({ testAsserts }) {
        try {
          await self.browser.mockExtensionAPI.methodAsync("arg0");
        } catch (err) {
          testAsserts.isErrorInstance(err);
          throw err;
        }
      },
      mockAPIRequestHandler() {
        return {
          type: Ci.mozIExtensionAPIRequestResult.RETURN_VALUE,
          value: Promise.reject(new Error("Fake API rejected error object")),
        };
      },
      assertResults({ testError }) {
        Assert.deepEqual(testError?.message, "Fake API rejected error object");
      },
    }
  );
});

add_task(async function test_call_async_function_result() {
  await runExtensionAPITest(
    "async API methods should support structured clonable resolved values",
    {
      async backgroundScript({ testAsserts }) {
        const api = self.browser.mockExtensionAPI;
        const results = {
          string: await api.methodAsync("string-result"),
          nested_prop: await api.methodAsync({
            string: "123",
            number: 123,
            date: new Date("2020-09-20"),
            map: new Map([
              ["a", 1],
              ["b", 2],
            ]),
          }),
        };

        testAsserts.isInstanceOf(results.nested_prop.date, "Date");
        testAsserts.isInstanceOf(results.nested_prop.map, "Map");
        return results;
      },
      mockAPIRequestHandler(policy, request) {
        if (request.apiName === "methodAsync") {
          // Return the first argument unmodified, which will be checked in the
          // resultAssertFn above.
          return {
            type: Ci.mozIExtensionAPIRequestResult.RETURN_VALUE,
            value: Promise.resolve(request.args[0]),
          };
        }
        throw new Error("Unexpected API method");
      },
      assertResults({ testResult, testError }) {
        Assert.deepEqual(testError, null, "Got no error as expected");
        Assert.deepEqual(testResult, {
          string: "string-result",
          nested_prop: {
            string: "123",
            number: 123,
            date: new Date("2020-09-20"),
            map: new Map([
              ["a", 1],
              ["b", 2],
            ]),
          },
        });
      },
    }
  );
});

add_task(async function test_call_no_return_throw_extension_error() {
  await runExtensionAPITest(
    "no return function call throw an error occurred for param validation errors",
    {
      backgroundScript({ testAsserts }) {
        try {
          self.browser.mockExtensionAPI.methodNoReturn("arg0");
        } catch (err) {
          testAsserts.isErrorInstance(err);
          throw err;
        }
      },
      mockAPIRequestHandler() {
        return {
          type: Ci.mozIExtensionAPIRequestResult.EXTENSION_ERROR,
          value: new Error("Fake Param Validation Error"),
        };
      },
      assertResults({ testError }) {
        Assert.deepEqual(testError?.message, "Fake Param Validation Error");
      },
    }
  );
});

add_task(async function test_call_no_return_without_errors() {
  await runExtensionAPITest(
    "handleAPIHandler can return undefined on api calls to methods with no return",
    {
      backgroundScript() {
        self.browser.mockExtensionAPI.methodNoReturn("arg0");
      },
      mockAPIRequestHandler() {
        return undefined;
      },
      assertResults({ testError }) {
        Assert.deepEqual(testError, null, "Got no error as expected");
      },
    }
  );
});

add_task(async function test_async_method_chrome_compatible_callback() {
  function mockAPIRequestHandler(policy, request) {
    if (request.args[0] === "fake-async-method-failure") {
      return {
        type: Ci.mozIExtensionAPIRequestResult.RETURN_VALUE,
        value: Promise.reject("this-should-not-be-passed-to-cb-as-parameter"),
      };
    }

    return {
      type: Ci.mozIExtensionAPIRequestResult.RETURN_VALUE,
      value: Promise.resolve(request.args),
    };
  }

  await runExtensionAPITest(
    "async method should support an optional chrome-compatible callback",
    {
      mockAPIRequestHandler,
      async backgroundScript({ testAsserts }) {
        const api = self.browser.mockExtensionAPI;
        const success_cb_params = await new Promise(resolve => {
          const res = api.methodAsync(
            { prop: "fake-async-method-success" },
            (...results) => {
              resolve(results);
            }
          );
          testAsserts.equal(res, undefined, "no promise should be returned");
        });
        const error_cb_params = await new Promise(resolve => {
          const res = api.methodAsync(
            "fake-async-method-failure",
            (...results) => {
              resolve(results);
            }
          );
          testAsserts.equal(res, undefined, "no promise should be returned");
        });
        return { success_cb_params, error_cb_params };
      },
      assertResults({ testError, testResult }) {
        Assert.deepEqual(testError, null, "Got no error as expected");
        Assert.deepEqual(
          testResult,
          {
            success_cb_params: [[{ prop: "fake-async-method-success" }]],
            error_cb_params: [],
          },
          "Got the expected results from the chrome compatible callbacks"
        );
      },
    }
  );

  await runExtensionAPITest(
    "async method with ambiguous args called with a chrome-compatible callback",
    {
      mockAPIRequestHandler,
      async backgroundScript({ testAsserts }) {
        const api = self.browser.mockExtensionAPI;
        const success_cb_params = await new Promise(resolve => {
          const res = api.methodAmbiguousArgsAsync(
            "arg0",
            { prop: "arg1" },
            3,
            (...results) => {
              resolve(results);
            }
          );
          testAsserts.equal(res, undefined, "no promise should be returned");
        });
        const error_cb_params = await new Promise(resolve => {
          const res = api.methodAmbiguousArgsAsync(
            "fake-async-method-failure",
            (...results) => {
              resolve(results);
            }
          );
          testAsserts.equal(res, undefined, "no promise should be returned");
        });
        return { success_cb_params, error_cb_params };
      },
      assertResults({ testError, testResult }) {
        Assert.deepEqual(testError, null, "Got no error as expected");
        Assert.deepEqual(
          testResult,
          {
            success_cb_params: [["arg0", { prop: "arg1" }, 3]],
            error_cb_params: [],
          },
          "Got the expected results from the chrome compatible callbacks"
        );
      },
    }
  );
});

add_task(async function test_get_property() {
  await runExtensionAPITest(
    "getProperty API request does return a value synchrously",
    {
      backgroundScript() {
        return self.browser.mockExtensionAPI.propertyAsString;
      },
      mockAPIRequestHandler() {
        return {
          type: Ci.mozIExtensionAPIRequestResult.RETURN_VALUE,
          value: "property-value",
        };
      },
      assertResults({ testError, testResult }) {
        Assert.deepEqual(testError, null, "Got no error as expected");
        Assert.deepEqual(
          testResult,
          "property-value",
          "Got the expected result"
        );
      },
    }
  );

  await runExtensionAPITest(
    "getProperty API request can return an error object",
    {
      backgroundScript({ testAsserts }) {
        const errObj = self.browser.mockExtensionAPI.propertyAsErrorObject;
        testAsserts.isErrorInstance(errObj);
        testAsserts.equal(errObj.message, "fake extension error");
      },
      mockAPIRequestHandler(policy, request) {
        let savedFrame = request.calledSavedFrame;
        return {
          type: Ci.mozIExtensionAPIRequestResult.RETURN_VALUE,
          value: ChromeUtils.createError("fake extension error", savedFrame),
        };
      },
      assertResults({ testError }) {
        Assert.deepEqual(testError, null, "Got no error as expected");
      },
    }
  );
});

add_task(async function test_get_property_return_long_string() {
  await runExtensionAPITest("getProperty with long string value", {
    backgroundScript() {
      return self.browser.mockExtensionAPI.propertyAsString;
    },
    mockAPIRequestHandler() {
      const longString = new Array(200).fill("x").join("");
      return {
        type: Ci.mozIExtensionAPIRequestResult.RETURN_VALUE,
        value: longString,
      };
    },
    assertResults({ testError, testResult }) {
      Assert.deepEqual(testError, null, "Got no error as expected");
      Assert.deepEqual(
        testResult,
        new Array(200).fill("x").join(""),
        "Got the expected result"
      );
    },
  });
});
