"use strict";

const test = require("node:test");
const assert = require("node:assert/strict");
const reorder = require("../../Web/market-nav-reorder.js");

class FakeClassList {
  constructor(values = []) { this.values = new Set(values); }
  add(value) { this.values.add(value); }
  remove(value) { this.values.delete(value); }
  contains(value) { return this.values.has(value); }
}

class FakeButton {
  constructor(market, active = false) {
    this.dataset = { market };
    this.parentElement = null;
    this.classList = new FakeClassList(active ? ["active"] : []);
    this.attributes = new Map();
    this.draggable = false;
    this.focusCount = 0;
  }

  closest(selector) { return selector === "button[data-market]" ? this : null; }
  setAttribute(name, value) { this.attributes.set(name, String(value)); }
  focus() { this.focusCount += 1; }
}

class FakeNav {
  constructor(markets, activeMarket = "compare") {
    this.children = markets.map(market => new FakeButton(market, market === activeMarket));
    for (const child of this.children) child.parentElement = this;
    this.listeners = new Map();
  }

  addEventListener(type, callback) {
    const callbacks = this.listeners.get(type) || [];
    callbacks.push(callback);
    this.listeners.set(type, callbacks);
  }

  removeEventListener(type, callback) {
    this.listeners.set(type, (this.listeners.get(type) || []).filter(value => value !== callback));
  }

  append(child) {
    this.children = this.children.filter(value => value !== child);
    this.children.push(child);
    child.parentElement = this;
  }

  prepend(child) {
    this.children = this.children.filter(value => value !== child);
    this.children.unshift(child);
    child.parentElement = this;
  }

  dispatch(type, event) {
    event.currentTarget = this;
    for (const callback of this.listeners.get(type) || []) callback(event);
  }

  button(market) { return this.children.find(button => button.dataset.market === market); }
}

class FakeDataTransfer {
  constructor() {
    this.values = new Map();
    this.effectAllowed = "none";
    this.dropEffect = "none";
  }
  setData(type, value) { this.values.set(type, value); }
  getData(type) { return this.values.get(type) || ""; }
}

function eventFor(target, values = {}) {
  return {
    target,
    altKey: false,
    ctrlKey: false,
    metaKey: false,
    shiftKey: false,
    repeat: false,
    defaultPrevented: false,
    propagationStopped: false,
    preventDefault() { this.defaultPrevented = true; },
    stopPropagation() { this.propagationStopped = true; },
    ...values
  };
}

function createNav(activeMarket = "compare") {
  return new FakeNav([reorder.fixedMarket, ...reorder.originalMarkets], activeMarket);
}

test("dashboard is fixed while original market identities swap by ID", () => {
  const nav = createNav();
  const result = reorder.swapOrder(
    reorder.readOrder(nav),
    "exchange",
    "expert");

  assert.equal(result.changed, true);
  assert.equal(reorder.applyOrder(nav, result.order), true);
  assert.equal(nav.children[0].dataset.market, "dashboard");
  assert.equal(nav.children[2].dataset.market, "expert");
  assert.equal(nav.children[9].dataset.market, "exchange");
  assert.equal(nav.button("compare").classList.contains("active"), true);
});

test("Alt+Arrow provides adjacent reorder and honors first/last boundaries", () => {
  const nav = createNav("overseas");
  const controller = reorder.createController({ nav });
  assert.equal(controller.mount(), true);

  const first = nav.button("overseas");
  const firstBoundary = eventFor(first, { key: "ArrowUp", altKey: true });
  nav.dispatch("keydown", firstBoundary);
  assert.deepEqual(controller.readOrder(), reorder.originalMarkets);
  assert.equal(firstBoundary.defaultPrevented, true);

  const last = nav.button("halted");
  const lastBoundary = eventFor(last, { key: "ArrowDown", altKey: true });
  nav.dispatch("keydown", lastBoundary);
  assert.deepEqual(controller.readOrder(), reorder.originalMarkets);

  const move = eventFor(first, { key: "ArrowDown", altKey: true });
  nav.dispatch("keydown", move);
  assert.deepEqual(controller.readOrder().slice(0, 3), ["exchange", "overseas", "index"]);
  assert.equal(nav.button("overseas").classList.contains("active"), true);
  assert.equal(first.focusCount, 1);
});

test("drag swap preserves active state and existing click behavior", () => {
  const nav = createNav("compare");
  let clicked = null;
  nav.addEventListener("click", event => {
    clicked = event.target.closest("button[data-market]")?.dataset.market ?? null;
  });
  const controller = reorder.createController({ nav });
  assert.equal(controller.mount(), true);

  const source = nav.button("compare");
  const target = nav.button("overseas");
  const dataTransfer = new FakeDataTransfer();
  nav.dispatch("dragstart", eventFor(source, { dataTransfer }));
  const over = eventFor(target, { dataTransfer });
  nav.dispatch("dragover", over);
  assert.equal(over.defaultPrevented, true);
  nav.dispatch("drop", eventFor(target, { dataTransfer }));

  assert.equal(controller.readOrder()[0], "compare");
  assert.equal(controller.readOrder()[5], "overseas");
  assert.equal(nav.children[0].dataset.market, "dashboard");
  assert.equal(source.classList.contains("active"), true);

  const click = eventFor(source);
  nav.dispatch("click", click);
  assert.equal(clicked, "compare");
  assert.equal(click.defaultPrevented, false);
});

test("malformed, external, dashboard and mismatched drags fail closed", () => {
  const nav = createNav();
  const controller = reorder.createController({ nav });
  controller.mount();
  const initial = [...controller.readOrder()];

  const dashboardStart = eventFor(nav.button("dashboard"), {
    dataTransfer: new FakeDataTransfer()
  });
  nav.dispatch("dragstart", dashboardStart);
  assert.equal(dashboardStart.defaultPrevented, true);

  const source = nav.button("exchange");
  const target = nav.button("expert");
  const malformed = new FakeDataTransfer();
  nav.dispatch("dragstart", eventFor(source, { dataTransfer: malformed }));
  malformed.setData(reorder.dragMime, "mbn-market-nav:v1:dashboard");
  nav.dispatch("drop", eventFor(target, { dataTransfer: malformed }));
  assert.deepEqual(controller.readOrder(), initial);

  const dashboardTarget = new FakeDataTransfer();
  nav.dispatch("dragstart", eventFor(source, { dataTransfer: dashboardTarget }));
  nav.dispatch("drop", eventFor(nav.button("dashboard"), { dataTransfer: dashboardTarget }));
  assert.deepEqual(controller.readOrder(), initial);

  const external = new FakeDataTransfer();
  external.setData(reorder.dragMime, reorder.createDragToken("exchange"));
  nav.dispatch("drop", eventFor(target, { dataTransfer: external }));
  assert.deepEqual(controller.readOrder(), initial);
  assert.equal(reorder.parseDragToken("mbn-market-nav:v1:unknown"), null);
  assert.equal(reorder.parseDragToken(" mbn-market-nav:v1:exchange"), null);
});

test("input, modal/playout lock and malformed DOM cannot reorder", () => {
  const nav = createNav();
  let blocked = true;
  const controller = reorder.createController({ nav, isBlocked: () => blocked });
  assert.equal(controller.mount(), true);
  const initial = [...controller.readOrder()];
  const locked = eventFor(nav.button("exchange"), {
    key: "ArrowDown",
    altKey: true
  });
  nav.dispatch("keydown", locked);
  assert.deepEqual(controller.readOrder(), initial);

  blocked = false;
  const input = { closest: () => null };
  const typing = eventFor(input, { key: "ArrowDown", altKey: true });
  nav.dispatch("keydown", typing);
  assert.equal(typing.defaultPrevented, false);
  assert.deepEqual(controller.readOrder(), initial);

  const malformedNav = createNav();
  malformedNav.button("expert").dataset.market = "compare";
  const malformedController = reorder.createController({ nav: malformedNav });
  assert.equal(malformedController.mount(), false);
});

test("reorder state is session-only and has no persistence surface", () => {
  assert.equal("storageKey" in reorder, false);
  assert.equal("save" in reorder, false);
  assert.deepEqual(reorder.originalMarkets, [
    "overseas", "exchange", "index", "kospi", "kosdaq",
    "compare", "theme", "foreign-stock", "expert", "halted"
  ]);
});
