/* 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/. */

// Tests sports suggestions and related code.

"use strict";

ChromeUtils.defineESModuleGetters(this, {
  SportsSuggestions:
    "moz-src:///browser/components/urlbar/private/SportsSuggestions.sys.mjs",
});

// 2025-11-01 - game status is "scheduled", without icon
const SUGGESTION_VALUE_SCHEDULED = {
  sport: "Sport 3",
  query: "query 3",
  date: "2025-11-01T17:00:00Z",
  home_team: {
    name: "Team 3 Home",
    score: null,
  },
  away_team: {
    name: "Team 3 Away",
    score: null,
  },
  status_type: "scheduled",
};

add_setup(async function init() {
  await Services.search.init();

  // Disable search suggestions so we don't hit the network.
  Services.prefs.setBoolPref("browser.search.suggest.enabled", false);

  // This test deals with `Intl` formating of dates and times, which depends on
  // the system locale, and assumes it's en-US. Make sure it's actually en-US.
  await QuickSuggestTestUtils.setRegionAndLocale({
    locale: "en-US",
    skipSuggestReset: true,
  });

  await QuickSuggestTestUtils.ensureQuickSuggestInit({
    merinoSuggestions: merinoSuggestions([SUGGESTION_VALUE_SCHEDULED]),
    prefs: [
      ["sports.featureGate", true],
      ["suggest.sports", true],
      ["suggest.quicksuggest.all", true],
    ],
  });
});

add_task(async function telemetryType() {
  Assert.equal(
    QuickSuggest.getFeature("SportsSuggestions").getSuggestionTelemetryType({}),
    "sports",
    "Telemetry type should be as expected"
  );
});

// The suggestions should be disabled when the relevant prefs are false.
add_task(async function disabledPrefs() {
  setNow("2025-10-31T14:00:00-04:00[-04:00]");

  let prefs = [
    "quicksuggest.enabled",
    "sports.featureGate",
    "suggest.sports",
    "suggest.quicksuggest.all",
  ];

  for (let pref of prefs) {
    info("Testing pref: " + pref);

    // First make sure the suggestion is added.
    await check_results({
      context: createContext("test", {
        providers: [UrlbarProviderQuickSuggest.name],
        isPrivate: false,
      }),
      matches: [
        expectedResult([
          {
            query: "query 3",
            sport: "Sport 3",
            status_type: "scheduled",
            date: "2025-11-01T17:00:00Z",
            home_team: {
              name: "Team 3 Home",
              score: null,
            },
            away_team: {
              name: "Team 3 Away",
              score: null,
            },
          },
        ]),
      ],
    });

    // Now disable them.
    UrlbarPrefs.set(pref, false);
    await check_results({
      context: createContext("test", {
        providers: [UrlbarProviderQuickSuggest.name],
        isPrivate: false,
      }),
      matches: [],
    });

    // Revert.
    UrlbarPrefs.set(pref, true);
    await QuickSuggestTestUtils.forceSync();
  }
});

// Main test for `SportsSuggestions._parseDate`.
add_task(async function datesAndTimes() {
  // For each test, we'll set `now`, call `_parseDate` with `date`, and check
  // the return value against `expected`.
  let tests = [
    // date is before this year
    {
      now: "2025-10-31T12:00:00-07:00[-07:00]",
      date: "2013-05-11T04:00:00-07:00",
      expected: {
        daysUntil: -Infinity,
        isFuture: false,
      },
    },

    // date is before yesterday
    {
      now: [
        "2025-10-31T00:00:00-07:00[-07:00]",
        "2025-10-31T23:59:59-07:00[-07:00]",
      ],
      date: ["2025-10-29T00:00:00-07:00", "2025-10-29T23:59:59-07:00"],
      expected: {
        daysUntil: -Infinity,
        isFuture: false,
      },
    },

    // date is yesterday
    {
      now: [
        "2025-10-31T00:00:00-07:00[-07:00]",
        "2025-10-31T23:59:59-07:00[-07:00]",
      ],
      date: ["2025-10-30T00:00:00-07:00", "2025-10-30T23:59:59-07:00"],
      expected: {
        daysUntil: -1,
        isFuture: false,
      },
    },

    // date is today (past)
    {
      now: [
        "2025-10-31T12:00:00-07:00[-07:00]",
        "2025-10-31T23:59:59-07:00[-07:00]",
      ],
      date: ["2025-10-31T00:00:00-07:00", "2025-10-31T11:59:59-07:00"],
      expected: {
        daysUntil: 0,
        isFuture: false,
      },
    },

    // date is today (now)
    {
      now: "2025-10-31T12:00:00-07:00[-07:00]",
      date: "2025-10-31T12:00:00-07:00",
      expected: {
        daysUntil: 0,
        isFuture: false,
      },
    },

    // date is today (future)
    {
      now: [
        "2025-10-31T00:00:00-07:00[-07:00]",
        "2025-10-31T12:00:00-07:00[-07:00]",
      ],
      date: ["2025-10-31T12:00:01-07:00", "2025-10-31T23:59:59-07:00"],
      expected: {
        daysUntil: 0,
        isFuture: true,
      },
    },

    // date is tomorrow
    {
      now: [
        "2025-10-31T00:00:00-07:00[-07:00]",
        "2025-10-31T23:59:59-07:00[-07:00]",
      ],
      date: ["2025-11-01T00:00:00-07:00", "2025-11-01T23:59:59-07:00"],
      expected: {
        daysUntil: 1,
        isFuture: true,
      },
    },

    // date is after tomorrow
    {
      now: [
        "2025-10-31T00:00:00-07:00[-07:00]",
        "2025-10-31T23:59:59-07:00[-07:00]",
      ],
      date: ["2025-11-02T00:00:00-07:00", "2025-11-02T23:59:59-07:00"],
      expected: {
        daysUntil: Infinity,
        isFuture: true,
      },
    },

    // date is after this year
    {
      now: "2025-10-31T00:00:00-07:00[-07:00]",
      date: "3013-05-11T04:00:00-07:00",
      expected: {
        daysUntil: Infinity,
        isFuture: true,
      },
    },
  ];

  for (let { now, date, expected } of tests) {
    let nows = typeof now == "string" ? [now] : now;
    let dates = typeof date == "string" ? [date] : date;
    for (let n of nows) {
      let zonedNow = setNow(n);
      for (let d of dates) {
        Assert.deepEqual(
          SportsSuggestions._parseDate(new Date(d)),
          {
            ...expected,
            zonedNow,
            zonedDate: new Date(d)
              .toTemporalInstant()
              .toZonedDateTimeISO(zonedNow),
          },
          "datesAndTimes test: " + JSON.stringify({ now: n, date: d })
        );
      }
    }
  }
});

// Tests `SportsSuggestions._parseDate` with dates across time zone changes.
add_task(function timeZoneTransition() {
  // This task is based around 2025-11-02, when Daylight Saving Time ends in the
  // U.S. On 2025-11-02 at 2:00 am, the time changes to 1:00 am Standard Time.

  let tests = [
    // `now` and `date` both in PDT (daylight saving)
    {
      now: "2025-10-02T12:00:00-07:00[America/Los_Angeles]",
      date: "2025-10-01T00:00:00-07:00",
      expected: {
        daysUntil: -1,
        isFuture: false,
      },
    },

    // `now` in PST, `date` in PDT
    {
      now: "2025-11-03T00:00:00-08:00[America/Los_Angeles]",
      date: "2025-11-01T00:00:00-07:00",
      expected: {
        daysUntil: -Infinity,
        isFuture: false,
      },
    },
    {
      now: "2025-11-02T12:00:00-08:00[America/Los_Angeles]",
      date: "2025-11-01T00:00:00-07:00",
      expected: {
        daysUntil: -1,
        isFuture: false,
      },
    },
    {
      now: "2025-11-02T01:00:00-08:00[America/Los_Angeles]",
      date: "2025-11-01T00:00:00-07:00",
      expected: {
        daysUntil: -1,
        isFuture: false,
      },
    },
    {
      now: "2025-11-02T23:59:59-08:00[America/Los_Angeles]",
      date: "2025-11-01T00:00:00-07:00",
      expected: {
        daysUntil: -1,
        isFuture: false,
      },
    },
    {
      now: "2025-11-02T01:00:00-08:00[America/Los_Angeles]",
      date: "2025-11-02T00:00:00-07:00",
      expected: {
        daysUntil: 0,
        isFuture: false,
      },
    },
    {
      now: "2025-11-02T01:00:00-08:00[America/Los_Angeles]",
      date: "2025-11-02T01:00:00-07:00",
      expected: {
        daysUntil: 0,
        isFuture: false,
      },
    },

    // `now` in PDT, `date` in PST
    {
      now: "2025-11-02T01:00:00-07:00[America/Los_Angeles]",
      date: "2025-11-02T01:00:00-08:00",
      expected: {
        daysUntil: 0,
        isFuture: true,
      },
    },
    {
      now: "2025-11-02T00:00:00-07:00[America/Los_Angeles]",
      date: "2025-11-02T01:00:00-08:00",
      expected: {
        daysUntil: 0,
        isFuture: true,
      },
    },
    {
      now: "2025-11-01T00:00:00-07:00[America/Los_Angeles]",
      date: "2025-11-02T23:59:59-08:00",
      expected: {
        daysUntil: 1,
        isFuture: true,
      },
    },
    {
      now: "2025-11-01T00:00:00-07:00[America/Los_Angeles]",
      date: "2025-11-02T01:00:00-08:00",
      expected: {
        daysUntil: 1,
        isFuture: true,
      },
    },
    {
      now: "2025-11-01T00:00:00-07:00[America/Los_Angeles]",
      date: "2025-11-02T12:00:00-08:00",
      expected: {
        daysUntil: 1,
        isFuture: true,
      },
    },
    {
      now: "2025-11-01T00:00:00-07:00[America/Los_Angeles]",
      date: "2025-11-03T00:00:00-08:00",
      expected: {
        daysUntil: Infinity,
        isFuture: true,
      },
    },

    // `now` and `date` both in PST (standard time)
    {
      now: "2025-11-11T12:00:00-08:00[America/Los_Angeles]",
      date: "2025-11-10T00:00:00-08:00",
      expected: {
        daysUntil: -1,
        isFuture: false,
      },
    },
  ];

  for (let { now, date, expected } of tests) {
    let zonedNow = setNow(now);
    Assert.deepEqual(
      SportsSuggestions._parseDate(new Date(date)),
      {
        ...expected,
        zonedNow,
        zonedDate: new Date(date)
          .toTemporalInstant()
          .toZonedDateTimeISO(zonedNow),
      },
      "timeZoneTransition test: " + JSON.stringify({ now, date })
    );
  }
});

add_task(async function command_notInterested() {
  setNow("2025-10-31T14:00:00-04:00[-04:00]");

  await doDismissAllTest({
    result: expectedResult([
      {
        query: "query 3",
        sport: "Sport 3",
        status_type: "scheduled",
        date: "2025-11-01T17:00:00Z",
        home_team: {
          name: "Team 3 Home",
          score: null,
        },
        away_team: {
          name: "Team 3 Away",
          score: null,
        },
      },
    ]),
    command: "not_interested",
    feature: QuickSuggest.getFeature("SportsSuggestions"),
    pref: "suggest.sports",
    queries: [{ query: "test" }],
  });
});

add_task(async function command_showLessFrequently() {
  setNow("2025-10-31T14:00:00-04:00[-04:00]");

  UrlbarPrefs.clear("sports.showLessFrequentlyCount");
  UrlbarPrefs.clear("sports.minKeywordLength");

  let cleanUpNimbus = await UrlbarTestUtils.initNimbusFeature({
    realtimeMinKeywordLength: 0,
    realtimeShowLessFrequentlyCap: 3,
  });

  let result = expectedResult([
    {
      query: "query 3",
      sport: "Sport 3",
      status_type: "scheduled",
      date: "2025-11-01T17:00:00Z",
      home_team: {
        name: "Team 3 Home",
        score: null,
      },
      away_team: {
        name: "Team 3 Away",
        score: null,
      },
    },
  ]);

  const testData = [
    {
      input: "spo",
      before: {
        canShowLessFrequently: true,
        showLessFrequentlyCount: 0,
        minKeywordLength: 0,
      },
      after: {
        canShowLessFrequently: true,
        showLessFrequentlyCount: 1,
        minKeywordLength: 4,
      },
    },
    {
      input: "sport",
      before: {
        canShowLessFrequently: true,
        showLessFrequentlyCount: 1,
        minKeywordLength: 4,
      },
      after: {
        canShowLessFrequently: true,
        showLessFrequentlyCount: 2,
        minKeywordLength: 6,
      },
    },
    {
      input: "sports",
      before: {
        canShowLessFrequently: true,
        showLessFrequentlyCount: 2,
        minKeywordLength: 6,
      },
      after: {
        canShowLessFrequently: false,
        showLessFrequentlyCount: 3,
        minKeywordLength: 7,
      },
    },
  ];

  for (let { input, before, after } of testData) {
    let feature = QuickSuggest.getFeature("SportsSuggestions");

    await check_results({
      context: createContext(input, {
        providers: [UrlbarProviderQuickSuggest.name],
        isPrivate: false,
      }),
      matches: [result],
    });

    Assert.equal(
      UrlbarPrefs.get("sports.minKeywordLength"),
      before.minKeywordLength
    );
    Assert.equal(feature.canShowLessFrequently, before.canShowLessFrequently);
    Assert.equal(
      feature.showLessFrequentlyCount,
      before.showLessFrequentlyCount
    );

    triggerCommand({
      result,
      feature,
      command: "show_less_frequently",
      searchString: input,
    });

    Assert.equal(
      UrlbarPrefs.get("sports.minKeywordLength"),
      after.minKeywordLength
    );
    Assert.equal(feature.canShowLessFrequently, after.canShowLessFrequently);
    Assert.equal(
      feature.showLessFrequentlyCount,
      after.showLessFrequentlyCount
    );

    await check_results({
      context: createContext(input, {
        providers: [UrlbarProviderQuickSuggest.name],
        isPrivate: false,
      }),
      matches: [],
    });
  }

  await cleanUpNimbus();
  UrlbarPrefs.clear("sports.showLessFrequentlyCount");
  UrlbarPrefs.clear("sports.minKeywordLength");
});

let gSandbox;
let gDateStub;

function setNow(dateStr) {
  if (!dateStr) {
    gSandbox?.restore();
    return null;
  }

  let global = Cu.getGlobalForObject(SportsSuggestions);
  if (!gSandbox) {
    gSandbox = sinon.createSandbox();
    gDateStub = gSandbox.stub(SportsSuggestions, "_zonedDateTimeISO");
  }

  let zonedNow = global.Temporal.ZonedDateTime.from(dateStr);
  gDateStub.returns(zonedNow);

  return zonedNow;
}

function merinoSuggestions(values) {
  return [
    {
      provider: "sports",
      is_sponsored: false,
      score: 0.2,
      title: "",
      custom_details: {
        sports: {
          values,
        },
      },
    },
  ];
}

function expectedResult(expectedItems) {
  return {
    type: UrlbarUtils.RESULT_TYPE.DYNAMIC,
    source: UrlbarUtils.RESULT_SOURCE.SEARCH,
    isBestMatch: true,
    hideRowLabel: true,
    rowIndex: -1,
    heuristic: false,
    exposureTelemetry: 0,
    payload: {
      items: expectedItems,
      source: "merino",
      provider: "sports",
      telemetryType: "sports",
      isSponsored: false,
      engine: Services.search.defaultEngine.name,
      dynamicType: "realtime-sports",
    },
  };
}
