import test from "node:test";
import assert from "node:assert/strict";
import { createBackNavigation } from "./back-navigation.js";

function createFakeBrowser({ asyncBack = false, initialState = null } = {}) {
  const listeners = new Set();
  const entries = [{ state: initialState }];
  let index = 0;
  const pendingBacks = [];
  const eventTarget = {
    addEventListener(type, listener) { if (type === "popstate") listeners.add(listener); },
    removeEventListener(type, listener) { if (type === "popstate") listeners.delete(listener); }
  };
  const history = {
    get state() { return entries[index].state; },
    pushState(state) {
      entries.splice(index + 1, entries.length, { state });
      index += 1;
    },
    replaceState(state) {
      entries[index].state = state;
    },
    back() {
      if (index === 0) return;
      const navigate = () => {
        index -= 1;
        for (const listener of listeners) listener({ state: entries[index].state });
      };
      if (asyncBack) pendingBacks.push(navigate);
      else navigate();
    }
  };
  return { eventTarget, history, flushBack() { pendingBacks.shift()?.(); }, get index() { return index; } };
}

test("system back dismisses registered layers in last-opened order", () => {
  const browser = createFakeBrowser();
  const navigation = createBackNavigation(browser);
  const dismissed = [];

  navigation.register("screen", () => dismissed.push("screen"));
  navigation.register("menu", () => dismissed.push("menu"));
  browser.history.back();
  browser.history.back();

  assert.deepEqual(dismissed, ["menu", "screen"]);
  assert.equal(browser.index, 0);
});

test("programmatic dismiss closes a layer and consumes its history entry once", () => {
  const browser = createFakeBrowser();
  const navigation = createBackNavigation(browser);
  let dismissals = 0;
  navigation.register("menu", () => { dismissals += 1; });

  navigation.dismiss("menu");

  assert.equal(dismissals, 1);
  assert.equal(browser.index, 0);
  browser.history.back();
  assert.equal(dismissals, 1);
});

test("native back requests traverse only when an app layer is open", () => {
  const browser = createFakeBrowser();
  const navigation = createBackNavigation(browser);
  const dismissed = [];

  assert.equal(navigation.back(), false);
  navigation.register("settings", () => dismissed.push("settings"));
  assert.equal(navigation.back(), true);
  assert.deepEqual(dismissed, ["settings"]);
  assert.equal(browser.index, 0);
});

test("a stale popup close does not dismiss its parent screen", () => {
  const browser = createFakeBrowser();
  const navigation = createBackNavigation(browser);
  const dismissed = [];
  navigation.register("settings", () => dismissed.push("settings"));

  const handled = navigation.dismiss("settings-language-menu");

  assert.equal(handled, false);
  assert.deepEqual(dismissed, []);
  assert.equal(browser.index, 1);
});

test("unregister removes an owned entry without invoking its callback", () => {
  const browser = createFakeBrowser();
  const navigation = createBackNavigation(browser);
  let dismissals = 0;
  navigation.register("menu", () => { dismissals += 1; });

  navigation.unregister("menu");

  assert.equal(dismissals, 0);
  assert.equal(browser.index, 1);
});

test("creating the coordinator does not add history at the root", () => {
  const browser = createFakeBrowser();
  const navigation = createBackNavigation(browser);

  assert.equal(browser.index, 0);
  navigation.dispose();
});

test("creating the coordinator clears a stale layer left by reload", () => {
  const browser = createFakeBrowser({
    initialState: { keep: "safe", __codexRemoteBackLayer: "stale-settings" }
  });
  const navigation = createBackNavigation(browser);

  assert.deepEqual(browser.history.state, { keep: "safe" });
  assert.equal(browser.index, 0);
  navigation.dispose();
});

test("layer back returns to the root without asking to exit", () => {
  const browser = createFakeBrowser();
  const navigation = createBackNavigation(browser);
  const dismissed = [];

  navigation.register("screen", () => dismissed.push("screen"));
  browser.history.back();

  assert.deepEqual(dismissed, ["screen"]);
  assert.equal(browser.index, 0);
  navigation.dispose();
});

test("programmatic dismiss waits for history traversal before continuing", () => {
  const browser = createFakeBrowser({ asyncBack: true });
  const navigation = createBackNavigation(browser);
  const events = [];
  navigation.register("menu", () => events.push("closed"));

  navigation.dismiss("menu", () => events.push("continued"));
  assert.deepEqual(events, ["closed"]);

  browser.flushBack();
  assert.deepEqual(events, ["closed", "continued"]);
});

test("an event passed by a click handler is not invoked after history traversal", () => {
  const browser = createFakeBrowser({ asyncBack: true });
  const navigation = createBackNavigation(browser);
  navigation.register("menu", () => {});

  navigation.dismiss("menu", { type: "click" });
  browser.flushBack();

  assert.equal(browser.index, 0);
});

test("an event passed while dismissing a non-current layer is not invoked", () => {
  const browser = createFakeBrowser();
  const navigation = createBackNavigation(browser);
  const dismissed = [];
  navigation.register("project", () => dismissed.push("project"));
  navigation.register("screen", () => dismissed.push("screen"));

  assert.doesNotThrow(() => navigation.dismiss("project", { type: "click" }));
  assert.deepEqual(dismissed, ["project"]);
});

test("unregistering a popup does not race the screen that replaces it", () => {
  const browser = createFakeBrowser({ asyncBack: true });
  const navigation = createBackNavigation(browser);
  const dismissed = [];
  navigation.register("picker", () => dismissed.push("picker"));

  navigation.unregister("picker");
  navigation.register("screen", () => dismissed.push("screen"));
  browser.flushBack();
  browser.flushBack();

  assert.deepEqual(dismissed, []);
  assert.equal(browser.index, 2);
});

test("closing settings returns to its source screen without dismissing it", () => {
  const browser = createFakeBrowser();
  const navigation = createBackNavigation(browser);
  const dismissed = [];
  navigation.register("screen", () => dismissed.push("screen"));
  navigation.register("settings", () => dismissed.push("settings"));

  navigation.dismiss("settings");

  assert.deepEqual(dismissed, ["settings"]);
  assert.equal(browser.index, 1);
});

test("closing nested details together neutralizes their history without dismissing the screen", () => {
  const browser = createFakeBrowser({ asyncBack: true });
  const navigation = createBackNavigation(browser);
  const dismissed = [];
  navigation.register("screen", () => dismissed.push("screen"));
  navigation.register("menu", () => dismissed.push("menu"));
  navigation.register("picker", () => dismissed.push("picker"));

  navigation.unregister("menu");
  navigation.unregister("picker");
  browser.flushBack();
  browser.flushBack();

  assert.deepEqual(dismissed, []);
  assert.equal(browser.index, 3);
});

test("a lower screen back request dismisses the top popup first", () => {
  const browser = createFakeBrowser();
  const navigation = createBackNavigation(browser);
  const dismissed = [];
  navigation.register("screen", () => dismissed.push("screen"));
  navigation.register("menu", () => dismissed.push("menu"));

  navigation.dismiss("screen", undefined, { preferTop: true });

  assert.deepEqual(dismissed, ["menu"]);
  assert.equal(browser.index, 1);
});

test("rapid popup close reopen close does not dismiss its parent screen", () => {
  const browser = createFakeBrowser({ asyncBack: true });
  const navigation = createBackNavigation(browser);
  const dismissed = [];
  navigation.register("screen", () => dismissed.push("screen"));
  navigation.register("notifications", () => dismissed.push("notifications"));

  navigation.dismiss("notifications");
  navigation.register("notifications", () => dismissed.push("notifications"));
  navigation.dismiss("notifications");

  browser.flushBack();
  browser.flushBack();
  browser.flushBack();

  assert.deepEqual(dismissed, ["notifications", "notifications"]);
  assert.equal(browser.index, 1);
});

test("reopening a popup after effect cleanup does not dismiss its parent screen", () => {
  const browser = createFakeBrowser({ asyncBack: true });
  const navigation = createBackNavigation(browser);
  const dismissed = [];
  navigation.register("screen", () => dismissed.push("screen"));
  navigation.register("notifications", () => dismissed.push("notifications"));

  navigation.unregister("notifications");
  navigation.register("notifications", () => dismissed.push("notifications"));
  navigation.dismiss("notifications");
  browser.flushBack();
  browser.flushBack();

  assert.deepEqual(dismissed, ["notifications"]);
  assert.equal(browser.index, 1);
});

test("a legacy unmarked popup entry is skipped without dismissing its parent screen", () => {
  const browser = createFakeBrowser({ asyncBack: true });
  const navigation = createBackNavigation(browser);
  const dismissed = [];
  navigation.register("screen", () => dismissed.push("screen"));
  navigation.register("notifications", () => dismissed.push("notifications"));

  browser.history.replaceState(null);
  navigation.unregister("notifications");
  navigation.register("notifications", () => dismissed.push("notifications"));
  navigation.dismiss("notifications");
  browser.flushBack();
  browser.flushBack();

  assert.deepEqual(dismissed, ["notifications"]);
  assert.equal(browser.index, 1);
});
