"use strict";

const { HttpServer } = ChromeUtils.importESModule(
  "resource://testing-common/httpd.sys.mjs"
);
const { setTimeout } = ChromeUtils.importESModule(
  "resource://gre/modules/Timer.sys.mjs"
);
const { sinon } = ChromeUtils.importESModule(
  "resource://testing-common/Sinon.sys.mjs"
);

ChromeUtils.defineESModuleGetters(this, {
  Region: "resource://gre/modules/Region.sys.mjs",
  RegionTestUtils: "resource://testing-common/RegionTestUtils.sys.mjs",
});

const INTERVAL_PREF = "browser.region.update.interval";

const RESPONSE_DELAY = 500;
const RESPONSE_TIMEOUT = 100;

const histogram = Services.telemetry.getHistogramById(
  "SEARCH_SERVICE_COUNTRY_FETCH_RESULT"
);

add_setup(async () => {
  Services.prefs.setBoolPref("browser.region.log", true);
  Services.prefs.setBoolPref("network.dns.disableIPv6", true);
  registerCleanupFunction(async () => {
    Services.prefs.clearUserPref("network.dns.disableIPv6");
  });
});

// Region.sys.mjs will call init() on being loaded and set a background
// task to fetch the region, ensure we have completed this before
// running the rest of the tests.
add_task(async function test_startup_with_no_pref() {
  Services.fog.testResetFOG();

  RegionTestUtils.setNetworkRegion("AT");

  // Region.sys.mjs is lazily loaded, so referencing `Region.` here will load
  // it and automatically call the `Region.init()` function.
  await checkTelemetry(Region.TELEMETRY.SUCCESS);
  Assert.equal(
    Glean.region.homeRegion.testGetValue(),
    "AT",
    "Should have correctly stored the region in home region after getting it"
  );

  await cleanup();
});

add_task(async function test_startup_with_pref() {
  Services.fog.testResetFOG();

  Services.prefs.setCharPref("browser.search.region", "GB");

  // If we failed to read the pref, we'd kick off a network connection. So set
  // up the network to return a different region here so that we'd be able to
  // detect that case.
  RegionTestUtils.setNetworkRegion("DE");

  // Use a different instance of region for testing this.
  let region = Region.newInstance();
  await region.init();

  Assert.equal(region.home, "GB", "Should have loaded the correct region");
  Assert.equal(
    Glean.region.homeRegion.testGetValue(),
    "GB",
    "Should have correctly stored the region in home region after getting it"
  );
});

add_task(async function test_basic() {
  Services.fog.testResetFOG();

  let srv = useHttpServer(RegionTestUtils.REGION_URL_PREF);
  srv.registerPathHandler("/", (req, res) => {
    res.setStatusLine("1.1", 200, "OK");
    send(res, { country_code: "UK" });
  });
  // start to listen the notification
  let updateRegion = TestUtils.topicObserved("browser-region-updated");
  await Region._fetchRegion();
  let [subject] = await updateRegion;

  Assert.ok(true, "Region fetch should succeed");
  Assert.equal(Region.home, "UK", "Region fetch should return correct result");
  Assert.equal(
    subject,
    Region.home,
    "Notification should be sent with the correct region"
  );

  assertStoredResultTelemetry({ restOfWorld: 1 });

  Assert.equal(
    Glean.region.homeRegion.testGetValue(),
    "UK",
    "Should have correctly set the region in home region after getting it"
  );

  await cleanup(srv);
});

add_task(async function test_invalid_url() {
  histogram.clear();
  Services.prefs.setIntPref("browser.region.retry-timeout", 0);
  Services.prefs.setCharPref(
    RegionTestUtils.REGION_URL_PREF,
    "http://localhost:0"
  );
  await Region._fetchRegion();
  await checkTelemetry(Region.TELEMETRY.NO_RESULT);
});

add_task(async function test_invalid_json() {
  histogram.clear();
  Services.prefs.setCharPref(
    RegionTestUtils.REGION_URL_PREF,
    'data:application/json,{"country_code"'
  );
  await Region._fetchRegion();
  await checkTelemetry(Region.TELEMETRY.NO_RESULT);
});

add_task(async function test_timeout() {
  histogram.clear();
  Services.prefs.setIntPref("browser.region.retry-timeout", 0);
  Services.prefs.setIntPref("browser.region.timeout", RESPONSE_TIMEOUT);
  let srv = useHttpServer(RegionTestUtils.REGION_URL_PREF);
  srv.registerPathHandler("/", (req, res) => {
    res.processAsync();
    do_timeout(RESPONSE_DELAY, () => {
      send(res, { country_code: "UK" });
      res.finish();
    });
  });

  await Region._fetchRegion();
  await checkTelemetry(Region.TELEMETRY.TIMEOUT);
  await cleanup(srv);
});

add_task(async function test_location() {
  let location = { location: { lat: -1, lng: 1 }, accuracy: 100 };
  let srv = useHttpServer("geo.provider.network.url");
  srv.registerPathHandler("/", (req, res) => {
    res.setStatusLine("1.1", 200, "OK");
    send(res, location);
  });

  let result = await Region._getLocation();
  Assert.ok(true, "Region fetch should succeed");
  Assert.deepEqual(result, location, "Location is returned");

  await cleanup(srv);
});

add_task(async function test_update() {
  RegionTestUtils.setNetworkRegion("FR");
  await Region._fetchRegion();
  Assert.equal(Region.home, "FR", "Should have correct region");
  Assert.equal(
    Glean.region.homeRegion.testGetValue(),
    "FR",
    "Should have correctly set the region in home region after getting it"
  );

  RegionTestUtils.setNetworkRegion("DE");
  await Region._fetchRegion();
  Assert.equal(Region.home, "FR", "Shouldnt have changed yet");
  Assert.equal(
    Glean.region.homeRegion.testGetValue(),
    "FR",
    "Should not have updated the home region telemetry yet"
  );

  // The first fetchRegion will set the prefs to determine when
  // to update the home region, we need to do 2 fetchRegions to test
  // it isnt updating when it shouldnt.
  await Region._fetchRegion();
  Assert.equal(Region.home, "FR", "Shouldnt have changed yet again");
  Assert.equal(
    Glean.region.homeRegion.testGetValue(),
    "FR",
    "Should not have updated the home region telemetry yet (again)"
  );

  Services.prefs.setIntPref(INTERVAL_PREF, 1);
  /* eslint-disable mozilla/no-arbitrary-setTimeout */
  await new Promise(resolve => setTimeout(resolve, 1100));
  await Region._fetchRegion();
  Assert.equal(Region.home, "DE", "Should have updated now");
  Assert.equal(
    Glean.region.homeRegion.testGetValue(),
    "DE",
    "Should have correctly set the home region telemetry"
  );

  await cleanup();
});

add_task(async function test_update_us() {
  Services.fog.testResetFOG();

  // Setting the region to US whilst within a US timezone should work.
  let stub = sinon.stub(Region, "_isUSTimezone").returns(true);
  Region._setHomeRegion(null, false);
  RegionTestUtils.setNetworkRegion("US");
  await Region._fetchRegion();

  assertStoredResultTelemetry({ unitedStates: 1 });
  Assert.equal(Region.home, "US", "Should have correct region");
  Assert.equal(
    Glean.region.homeRegion.testGetValue(),
    "US",
    "Should have correctly set the home region telemetry"
  );

  Services.fog.testResetFOG();

  // Setting the region to US whilst not within a US timezone should not work.
  stub.returns(false);
  Region._setHomeRegion(null, false);
  RegionTestUtils.setNetworkRegion("US");
  await Region._fetchRegion();

  assertStoredResultTelemetry({ ignoredUnitedStates: 1 });
  Assert.equal(Region.home, null, "Should have not set the region");

  sinon.restore();
});

add_task(async function test_max_retry() {
  Region._setHomeRegion(null, false);
  let requestsSeen = 0;
  Services.prefs.setIntPref("browser.region.retry-timeout", RESPONSE_TIMEOUT);
  Services.prefs.setIntPref("browser.region.timeout", RESPONSE_TIMEOUT);
  let srv = useHttpServer(RegionTestUtils.REGION_URL_PREF);
  srv.registerPathHandler("/", (req, res) => {
    requestsSeen++;
    res.setStatusLine("1.1", 200, "OK");
    res.processAsync();
    do_timeout(RESPONSE_DELAY, res.finish.bind(res));
  });

  Region._fetchRegion();
  await TestUtils.waitForCondition(() => requestsSeen === 3);
  /* eslint-disable mozilla/no-arbitrary-setTimeout */
  await new Promise(resolve => setTimeout(resolve, RESPONSE_DELAY));

  Assert.equal(Region.home, null, "failed to fetch region");
  Assert.equal(requestsSeen, 3, "Retried 4 times");

  Region._retryCount = 0;
  await cleanup(srv);
});

add_task(async function test_retry() {
  Region._setHomeRegion(null, false);
  let requestsSeen = 0;
  Services.prefs.setIntPref("browser.region.retry-timeout", RESPONSE_TIMEOUT);
  Services.prefs.setIntPref("browser.region.timeout", RESPONSE_TIMEOUT);
  let srv = useHttpServer(RegionTestUtils.REGION_URL_PREF);
  srv.registerPathHandler("/", (req, res) => {
    res.setStatusLine("1.1", 200, "OK");
    if (++requestsSeen == 2) {
      res.setStatusLine("1.1", 200, "OK");
      send(res, { country_code: "UK" });
    } else {
      res.processAsync();
      do_timeout(RESPONSE_DELAY, res.finish.bind(res));
    }
  });

  Region._fetchRegion();
  await TestUtils.waitForCondition(() => requestsSeen === 2);
  /* eslint-disable mozilla/no-arbitrary-setTimeout */
  await new Promise(resolve => setTimeout(resolve, RESPONSE_DELAY));

  Assert.equal(Region.home, "UK", "failed to fetch region");
  Assert.equal(requestsSeen, 2, "Retried 2 times");

  await cleanup(srv);
});

add_task(async function test_timerManager() {
  RegionTestUtils.setNetworkRegion("FR");

  // Ensure the home region updates immediately, but the update
  // check will only happen once per second.
  Services.prefs.setIntPref("browser.region.update.interval", 0);
  Services.prefs.setIntPref("browser.region.update.debounce", 1);

  let region = Region.newInstance();
  await region.init();
  Assert.equal(region.home, "FR", "Should have correct initial region");

  // Updates are being debounced, these should be ignored.
  RegionTestUtils.setNetworkRegion("DE");
  await region._updateTimer();
  await region._updateTimer();
  Assert.equal(region.home, "FR", "Ignored updates to region");

  // Set the debounce interval to 0 so these updates are used.
  Services.prefs.setIntPref("browser.region.update.debounce", 0);
  RegionTestUtils.setNetworkRegion("AU");
  await region._updateTimer();
  await region._updateTimer();
  Assert.equal(region.home, "AU", "region has been updated");
  await cleanup();
});

function useHttpServer(pref) {
  let server = new HttpServer();
  server.start(-1);
  Services.prefs.setCharPref(
    pref,
    `http://localhost:${server.identity.primaryPort}/`
  );
  return server;
}

function send(res, json) {
  res.setStatusLine("1.1", 200, "OK");
  res.setHeader("content-type", "application/json", true);
  res.write(JSON.stringify(json));
}

async function cleanup(srv = null) {
  Services.prefs.clearUserPref("browser.search.region");
  Region._setHomeRegion(null, false);
  if (srv) {
    await new Promise(r => srv.stop(r));
  }
}

async function checkTelemetry(aExpectedValue) {
  // Wait until there is 1 result.
  await TestUtils.waitForCondition(() => {
    let snapshot = histogram.snapshot();
    return Object.values(snapshot.values).reduce((a, b) => a + b, 0) == 1;
  });
  let snapshot = histogram.snapshot();
  Assert.equal(snapshot.values[aExpectedValue], 1);
}

function assertStoredResultTelemetry({
  restOfWorld = null,
  unitedStates = null,
  ignoredUnitedStates = null,
}) {
  Assert.equal(
    Glean.region.storeRegionResult.ignoredUnitedStatesIncorrectTimezone.testGetValue(),
    ignoredUnitedStates,
    "Should have the expected count for ignoring setting the region when US but not in the correct timezone"
  );
  Assert.equal(
    Glean.region.storeRegionResult.setForUnitedStates.testGetValue(),
    unitedStates,
    "Should have the expected count for setting the region for the US"
  );
  Assert.equal(
    Glean.region.storeRegionResult.setForRestOfWorld.testGetValue(),
    restOfWorld,
    "Should have the expected count for setting the region for the rest of the world"
  );
}
