/* 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/. */
const { TelemetryTestUtils } = ChromeUtils.importESModule(
  "resource://testing-common/TelemetryTestUtils.sys.mjs"
);
const TEST_URLS = [
  "http://mochi.test:8888/browser/",
  "https://www.example.com/",
  "https://example.net/",
  "https://example.org/",
  "about:logo",
  "about:robots",
];
const TOPIC_CLOSED_OBJECTS_CHANGED = "sessionstore-closed-objects-changed";
var preparedClosedIds;

add_setup(async function testSetup() {
  registerCleanupFunction(async () => {
    await promiseAllButPrimaryWindowClosed();
    Services.obs.notifyObservers(null, "browser:purge-session-history");
  });
  preparedClosedIds = await prepareClosedData();
  SimpleTest.promiseFocus(window);
});

async function prepareClosedData() {
  // prepare some closed state
  let closedIds = {};
  await openAndCloseTab(window, TEST_URLS[0]);
  closedIds.tab0 = SessionStore.getClosedTabDataForWindow(window)[0].closedId;

  const testWindow1 = await BrowserTestUtils.openNewBrowserWindow();
  // open a non-transitory, worth-keeping tab to ensure window data is saved on close
  await BrowserTestUtils.openNewForegroundTab(
    testWindow1.gBrowser,
    "about:mozilla"
  );
  await openAndCloseTab(testWindow1, TEST_URLS[1]);
  closedIds.tab1 =
    SessionStore.getClosedTabDataForWindow(testWindow1)[0].closedId;

  const testWindow2 = await BrowserTestUtils.openNewBrowserWindow();
  // open a non-transitory, worth-keeping tab to ensure window data is saved on close
  await BrowserTestUtils.openNewForegroundTab(
    testWindow2.gBrowser,
    "about:mozilla"
  );
  await openAndCloseTab(testWindow2, TEST_URLS[2]);
  closedIds.tab2 =
    SessionStore.getClosedTabDataForWindow(testWindow2)[0].closedId;
  await openAndCloseTab(testWindow2, TEST_URLS[3]);
  closedIds.tab3 =
    SessionStore.getClosedTabDataForWindow(testWindow2)[0].closedId;

  const testWindow3 = await BrowserTestUtils.openNewBrowserWindow();
  // open a non-transitory, worth-keeping tab to ensure window data is saved on close
  await BrowserTestUtils.openNewForegroundTab(
    testWindow3.gBrowser,
    "about:mozilla"
  );
  await openAndCloseTab(testWindow3, TEST_URLS[4]);
  closedIds.tab4 =
    SessionStore.getClosedTabDataForWindow(testWindow3)[0].closedId;

  // open a private window
  const privateWin = await BrowserTestUtils.openNewBrowserWindow({
    private: true,
  });
  // open a non-transitory, worth-keeping tab to ensure window data is saved on close
  await BrowserTestUtils.openNewForegroundTab(
    privateWin.gBrowser,
    "about:mozilla"
  );
  await openAndCloseTab(privateWin, TEST_URLS[5]);
  closedIds.tab5 =
    SessionStore.getClosedTabDataForWindow(privateWin)[0].closedId;

  const testWindow6 = await BrowserTestUtils.openNewBrowserWindow();

  const testWindow7 = await BrowserTestUtils.openNewBrowserWindow();
  await openAndCloseTab(testWindow7, TEST_URLS[4]);

  await BrowserTestUtils.closeWindow(testWindow1);
  closedIds.testWindow1 = SessionStore.getClosedWindowData()[0].closedId;
  await BrowserTestUtils.closeWindow(testWindow2);

  closedIds.testWindow2 = SessionStore.getClosedWindowData()[0].closedId;
  await BrowserTestUtils.closeWindow(testWindow3);

  closedIds.testWindow3 = SessionStore.getClosedWindowData()[0].closedId;
  await BrowserTestUtils.closeWindow(privateWin);
  Assert.ok(
    closedIds.testWindow2 > closedIds.testWindow1,
    "We got the closedIds in the expected order"
  );

  await BrowserTestUtils.closeWindow(testWindow6);
  await BrowserTestUtils.closeWindow(testWindow7);
  return closedIds;
}

// The tasks need to be run sequentially. They are broken up so the different functions
// under test and any stack traces are easier to find

add_task(async function get_closed_tabs_from_closed_windows() {
  Assert.equal(
    SessionStore.getClosedTabCount({ closedTabsFromClosedWindows: false }),
    1,
    "Expected closed tab count (in currently-open windows only)"
  );
  Assert.equal(
    SessionStore.getClosedTabCount(),
    5,
    "Expected closed tab count (in currently-open and closed windows)"
  );
  // We dont keep the closed tab from the private window
  Assert.equal(
    SessionStore.getClosedTabCountFromClosedWindows(),
    4,
    "Expected closed tab count (from closed windows)"
  );

  // Closed tabs from closed windows aren't included in the list from getClosedTabData()
  Assert.deepEqual(
    SessionStore.getClosedTabData().map(td => td.state.entries[0].url),
    ["http://mochi.test:8888/browser/"],
    "Matched closed tab data from currently open windows"
  );

  // Closed tabs from open windows or closed private windows aren't included in the list
  // from getClosedTabDataFromClosedWindows()
  Assert.deepEqual(
    SessionStore.getClosedTabDataFromClosedWindows().map(
      td => td.state.entries[0].url
    ),
    [
      "about:logo",
      "https://example.org/",
      "https://example.net/",
      "https://www.example.com/",
    ],
    "Matched closed tab data from currently closed windows"
  );
});

add_task(async function forget_closed_tabs_from_closed_windows() {
  // forget the most recent tab we closed in testWindow1
  let closedObjectsChanged = TestUtils.topicObserved(
    TOPIC_CLOSED_OBJECTS_CHANGED
  );
  info(
    "Calling forgetClosedTab with window sourceClosedId: " +
      preparedClosedIds.testWindow1
  );
  SessionStore.forgetClosedTab(
    { sourceClosedId: preparedClosedIds.testWindow1 },
    0
  );
  await closedObjectsChanged;
  Assert.deepEqual(
    SessionStore.getClosedTabDataFromClosedWindows().map(
      td => td.state.entries[0].url
    ),
    [
      "about:logo",
      "https://example.org/",
      "https://example.net/",
      // "https://www.example.com/" from testWindow1 was forgotten and should be removed
    ],
    "Closed tab forgotten"
  );

  // forget a closed window that had closed tabs
  let closedTabCount = SessionStore.getClosedTabCountFromClosedWindows();
  closedObjectsChanged = TestUtils.topicObserved(TOPIC_CLOSED_OBJECTS_CHANGED);
  SessionStore.forgetClosedWindowById(preparedClosedIds.testWindow3);
  await closedObjectsChanged;
  Assert.equal(
    SessionStore.getClosedTabCountFromClosedWindows(),
    closedTabCount - 1,
    "The closed tab is gone when we forget the closed window it was in"
  );
});

add_task(async function expected_exceptions_closed_tabs_from_closed_windows() {
  // testWindow3 was forgotten, attempting to undoClose its tabs should throw
  Assert.throws(
    () =>
      SessionStore.undoClosedTabFromClosedWindow(
        { sourceClosedId: preparedClosedIds.testWindow3 },
        preparedClosedIds.tab4
      ),
    /NS_ERROR_ILLEGAL_VALUE/,
    "Invalid window or source for undoClosedTabFromClosedWindow throws"
  );
  // tab1 was forgotten, attempting to undoClose it should throw
  Assert.throws(
    () =>
      SessionStore.undoClosedTabFromClosedWindow(
        { sourceClosedId: preparedClosedIds.testWindow1 },
        preparedClosedIds.tab1
      ),
    /NS_ERROR_ILLEGAL_VALUE/,
    "Invalid tab closedId for undoClosedTabFromClosedWindow throws"
  );

  // testWindow3 was forgotten, attempting to forget its tabs should throw
  Assert.throws(
    () =>
      SessionStore.forgetClosedTab(
        { sourceClosedId: preparedClosedIds.testWindow3 },
        preparedClosedIds.tab4
      ),
    /NS_ERROR_ILLEGAL_VALUE/,
    "Invalid window or source for forgetClosedTab throws"
  );

  // tab1 was forgotten, attempting to forget it again should throw
  Assert.throws(
    () =>
      SessionStore.undoClosedTabFromClosedWindow(
        { sourceClosedId: preparedClosedIds.testWindow3 },
        preparedClosedIds.tab4
      ),
    /NS_ERROR_ILLEGAL_VALUE/,
    "Invalid window for undoClosedTabFromClosedWindow throws"
  );
});

add_task(async function undo_closed_tabs_from_closed_windows() {
  const openTabsCount = gBrowser.tabs.length;

  // undo most recently closed tab from a closed window using a window closedId
  let closedObjectsChanged = TestUtils.topicObserved(
    TOPIC_CLOSED_OBJECTS_CHANGED
  );
  let restoredTab1 = SessionStore.undoCloseTab(
    { sourceClosedId: preparedClosedIds.testWindow2 },
    0,
    window
  );
  await BrowserTestUtils.waitForEvent(
    window.gBrowser.tabContainer,
    "SSTabRestored"
  );
  await closedObjectsChanged;
  Assert.equal(
    window.gBrowser.tabs.length,
    openTabsCount + 1,
    "The closed tab was restored"
  );
  Assert.equal(
    restoredTab1.linkedBrowser.currentURI.spec,
    "https://example.org/",
    "The re-opened tab had the expected URL"
  );
  Assert.deepEqual(
    SessionStore.getClosedTabDataFromClosedWindows().map(
      td => td.state.entries[0].url
    ),
    [
      "https://example.net/",
      // "https://example.org/" from testWindow2 got reopened and should be removed
    ],
    "Re-opened tabs not listed in closed tabs"
  );

  // use undoClosedTabFromClosedWindows to reference the closed tab by closedId
  closedObjectsChanged = TestUtils.topicObserved(TOPIC_CLOSED_OBJECTS_CHANGED);
  let restoredTab2 = SessionStore.undoClosedTabFromClosedWindow(
    { sourceClosedId: preparedClosedIds.testWindow2 },
    preparedClosedIds.tab2,
    window
  );
  await BrowserTestUtils.waitForEvent(
    window.gBrowser.tabContainer,
    "SSTabRestored"
  );
  await closedObjectsChanged;
  Assert.equal(
    window.gBrowser.tabs.length,
    openTabsCount + 2,
    "The closed tab was restored"
  );
  Assert.equal(
    restoredTab2.linkedBrowser.currentURI.spec,
    "https://example.net/",
    "The re-opened tab had the expected URL"
  );

  Assert.deepEqual(
    SessionStore.getClosedTabDataFromClosedWindows().map(
      td => td.state.entries[0].url
    ),
    [
      // "https://example.org/" from testWindow2 got reopened and should be removed
    ],
    "Re-opened tabs not listed in closed tabs"
  );

  // clean up the re-opened tabs
  for (let tab of [restoredTab1, restoredTab2]) {
    await TabStateFlusher.flush(tab.linkedBrowser);
    let sessionUpdatePromise = BrowserTestUtils.waitForSessionStoreUpdate(tab);
    BrowserTestUtils.removeTab(tab);
    await sessionUpdatePromise;
  }
});
