const { ClientEnvironment } = ChromeUtils.importESModule(
  "resource://normandy/lib/ClientEnvironment.sys.mjs"
);
const { TargetingContext } = ChromeUtils.importESModule(
  "resource://messaging-system/targeting/Targeting.sys.mjs"
);
const { TelemetryTestUtils } = ChromeUtils.importESModule(
  "resource://testing-common/TelemetryTestUtils.sys.mjs"
);
const { TestUtils } = ChromeUtils.importESModule(
  "resource://testing-common/TestUtils.sys.mjs"
);

add_task(async function instance_with_default() {
  let targeting = new TargetingContext();

  let res = await targeting.eval(
    `ctx.locale == '${Services.locale.appLocaleAsBCP47}'`
  );

  Assert.ok(res, "Has local context");
});

add_task(async function instance_with_context() {
  let targeting = new TargetingContext({ bar: 42 });

  let res = await targeting.eval("ctx.bar == 42");

  Assert.ok(res, "Merge provided context with default");
});

add_task(async function eval_1_context() {
  let targeting = new TargetingContext();

  let res = await targeting.eval("custom1.bar == 42", { custom1: { bar: 42 } });

  Assert.ok(res, "Eval uses provided context");
});

add_task(async function eval_2_context() {
  let targeting = new TargetingContext();

  let res = await targeting.eval("custom1.bar == 42 && custom2.foo == 42", {
    custom1: { bar: 42 },
    custom2: { foo: 42 },
  });

  Assert.ok(res, "Eval uses provided context");
});

add_task(async function eval_multiple_context() {
  let targeting = new TargetingContext();

  let res = await targeting.eval(
    "custom1.bar == 42 && custom2.foo == 42 && custom3.baz == 42",
    { custom1: { bar: 42 }, custom2: { foo: 42 } },
    { custom3: { baz: 42 } }
  );

  Assert.ok(res, "Eval uses provided context");
});

add_task(async function eval_multiple_context_precedence() {
  let targeting = new TargetingContext();

  let res = await targeting.eval(
    "custom1.bar == 42 && custom2.foo == 42",
    { custom1: { bar: 24 }, custom2: { foo: 24 } },
    { custom1: { bar: 42 }, custom2: { foo: 42 } }
  );

  Assert.ok(res, "Last provided context overrides previously defined ones.");
});

add_task(async function eval_evalWithDefault() {
  let targeting = new TargetingContext({ foo: 42 });

  let res = await targeting.evalWithDefault("foo == 42");

  Assert.ok(res, "Eval uses provided context");
});

add_task(async function log_targeting_error_events() {
  let ctx = {
    get foo() {
      throw new Error("unit test");
    },
  };
  let targeting = new TargetingContext(ctx);
  let stub = sinon.stub(targeting, "_sendUndesiredEvent");

  await Assert.rejects(
    targeting.evalWithDefault("foo == 42", ctx),
    /unit test/,
    "Getter should throw"
  );

  Assert.equal(stub.callCount, 1, "Error event was logged");
  let {
    args: [{ event, value }],
  } = stub.firstCall;
  Assert.equal(event, "AttributeError", "Correct error message");
  Assert.equal(value, "foo", "Correct attribute name");
});

add_task(async function eval_evalWithDefault_precedence() {
  let targeting = new TargetingContext({ region: "space" });
  let res = await targeting.evalWithDefault("region != 'space'");

  Assert.ok(res, "Custom context does not override TargetingEnvironment");
});

add_task(async function eval_evalWithDefault_combineContexts() {
  let combinedCtxs = TargetingContext.combineContexts({ foo: 1 }, { foo: 2 });
  let targeting = new TargetingContext(combinedCtxs);
  let res = await targeting.evalWithDefault("foo == 1");

  Assert.ok(res, "First match is returned for combineContexts");
});

add_task(async function log_targeting_error_events_in_namespace() {
  let ctx = {
    get foo() {
      throw new Error("unit test");
    },
  };
  let targeting = new TargetingContext(ctx);
  let stub = sinon.stub(targeting, "_sendUndesiredEvent");
  let catchStub = sinon.stub();

  try {
    await targeting.eval("ctx.foo == 42");
  } catch (e) {
    catchStub();
  }

  Assert.equal(stub.callCount, 1, "Error event was logged");
  let {
    args: [{ event, value }],
  } = stub.firstCall;
  Assert.equal(event, "AttributeError", "Correct error message");
  Assert.equal(value, "ctx.foo", "Correct attribute name");
  Assert.ok(catchStub.calledOnce, "eval throws errors");
});

add_task(async function log_timeout_errors() {
  let ctx = {
    timeout: 1,
    get foo() {
      return new Promise(() => {});
    },
  };

  let targeting = new TargetingContext(ctx);
  let stub = sinon.stub(targeting, "_sendUndesiredEvent");
  let catchStub = sinon.stub();

  try {
    await targeting.eval("ctx.foo");
  } catch (e) {
    catchStub();
  }

  Assert.equal(catchStub.callCount, 1, "Timeout error throws");
  Assert.equal(stub.callCount, 1, "Timeout event was logged");
  let {
    args: [{ event, value }],
  } = stub.firstCall;
  Assert.equal(event, "AttributeTimeout", "Correct error message");
  Assert.equal(value, "ctx.foo", "Correct attribute name");
});

add_task(async function test_telemetry_event_timeout() {
  Services.telemetry.clearEvents();
  let ctx = {
    timeout: 1,
    get foo() {
      return new Promise(() => {});
    },
  };
  let expectedEvents = [
    ["messaging_experiments", "targeting", "attribute_timeout", "ctx.foo"],
  ];
  let targeting = new TargetingContext(ctx);

  try {
    await targeting.eval("ctx.foo");
  } catch (e) {}

  TelemetryTestUtils.assertEvents(expectedEvents);
  Services.telemetry.clearEvents();
});

add_task(async function test_telemetry_event_error() {
  Services.telemetry.clearEvents();
  let ctx = {
    get bar() {
      throw new Error("unit test");
    },
  };
  let expectedEvents = [
    ["messaging_experiments", "targeting", "attribute_error", "ctx.bar"],
  ];
  let targeting = new TargetingContext(ctx);

  try {
    await targeting.eval("ctx.bar");
  } catch (e) {}

  TelemetryTestUtils.assertEvents(expectedEvents);
  Services.telemetry.clearEvents();
});

// Make sure that when using the Normandy-style ClientEnvironment context,
// `liveTelemetry` works. `liveTelemetry` is a particularly tricky object to
// proxy, so it's useful to check specifically.
add_task(async function test_live_telemetry() {
  let ctx = { env: ClientEnvironment };
  let targeting = new TargetingContext();
  // This shouldn't throw.
  await targeting.eval("env.liveTelemetry.main", ctx);
});

add_task(async function test_default_targeting() {
  const targeting = new TargetingContext();
  const expected_attributes = [
    "locale",
    "localeLanguageCode",
    // "region", // Not available in test, requires network access to determine
    "userId",
    "version",
    "channel",
    "platform",
  ];

  for (let attribute of expected_attributes) {
    let res = await targeting.eval(`ctx.${attribute}`);
    Assert.ok(res, `[eval] result for ${attribute} should not be null`);
  }

  for (let attribute of expected_attributes) {
    let res = await targeting.evalWithDefault(attribute);
    Assert.ok(
      res,
      `[evalWithDefault] result for ${attribute} should not be null`
    );
  }
});

add_task(async function test_targeting_os() {
  const targeting = new TargetingContext();
  await TestUtils.waitForCondition(() =>
    targeting.eval("ctx.os.isWindows || ctx.os.isMac || ctx.os.isLinux")
  );
  let res = await targeting.eval(
    `(ctx.os.isWindows && ctx.os.windowsVersion && ctx.os.windowsBuildNumber) ||
     (ctx.os.isMac && ctx.os.macVersion && ctx.os.darwinVersion) ||
     (ctx.os.isLinux && os.darwinVersion == null)
    `
  );
  Assert.ok(res, `Should detect platform version got: ${res}`);
});

add_task(async function test_targeting_source_constructor() {
  Services.telemetry.clearEvents();
  const targeting = new TargetingContext(
    {
      foo: true,
      get bar() {
        throw new Error("bar");
      },
    },
    { source: "unit_testing" }
  );

  let res = await targeting.eval("ctx.foo");
  Assert.ok(res, "Should eval to true");

  let expectedEvents = [
    [
      "messaging_experiments",
      "targeting",
      "attribute_error",
      "ctx.bar",
      { source: "unit_testing" },
    ],
  ];
  try {
    await targeting.eval("ctx.bar");
  } catch (e) {}

  TelemetryTestUtils.assertEvents(expectedEvents);
  Services.telemetry.clearEvents();
});

add_task(async function test_targeting_source_override() {
  Services.telemetry.clearEvents();
  const targeting = new TargetingContext(
    {
      foo: true,
      get bar() {
        throw new Error("bar");
      },
    },
    { source: "unit_testing" }
  );

  let res = await targeting.eval("ctx.foo");
  Assert.ok(res, "Should eval to true");

  let expectedEvents = [
    [
      "messaging_experiments",
      "targeting",
      "attribute_error",
      "bar",
      { source: "override" },
    ],
  ];
  try {
    targeting.setTelemetrySource("override");
    await targeting.evalWithDefault("bar");
  } catch (e) {}

  TelemetryTestUtils.assertEvents(expectedEvents);
  Services.telemetry.clearEvents();
});
