import assert from "node:assert/strict";
import { describe, it } from "node:test";
import {
  ACCENTS,
  BODY_SHAPES,
  HAIR_COLORS,
  OUTFITS,
  SKIN_TONES,
  createDefaultLocalProfile,
  createProfileEditor,
  type LocalProfile,
} from "../profile/index.ts";

type Listener = (event: FakeEvent) => void;

class FakeEvent {
  defaultPrevented = false;
  readonly type: string;
  readonly target: FakeElement;
  readonly key: string;
  readonly ctrlKey: boolean;
  readonly metaKey: boolean;
  readonly shiftKey: boolean;
  constructor(
    type: string,
    target: FakeElement,
    key = "",
    ctrlKey = false,
    metaKey = false,
    shiftKey = false,
  ) {
    this.type = type;
    this.target = target;
    this.key = key;
    this.ctrlKey = ctrlKey;
    this.metaKey = metaKey;
    this.shiftKey = shiftKey;
  }
  preventDefault(): void { this.defaultPrevented = true; }
}

class FakeElement {
  readonly children: FakeElement[] = [];
  readonly attributes = new Map<string, string>();
  readonly listeners = new Map<string, Listener[]>();
  parentElement: FakeElement | null = null;
  className = "";
  textContent = "";
  hidden = false;
  value = "";
  id = "";
  type = "";
  maxLength = 0;
  autocomplete = "";
  disabled = false;
  readonly ownerDocument: FakeDocument;
  readonly tagName: string;

  constructor(ownerDocument: FakeDocument, tagName: string) {
    this.ownerDocument = ownerDocument;
    this.tagName = tagName;
  }

  append(...nodes: FakeElement[]): void {
    for (const node of nodes) {
      node.parentElement = this;
      this.children.push(node);
    }
  }
  setAttribute(name: string, value: string): void { this.attributes.set(name, value); }
  getAttribute(name: string): string | null { return this.attributes.get(name) ?? null; }
  addEventListener(type: string, listener: Listener): void {
    const found = this.listeners.get(type);
    if (found) found.push(listener);
    else this.listeners.set(type, [listener]);
  }
  dispatch(type: string, init: Partial<Pick<FakeEvent, "key" | "ctrlKey" | "metaKey" | "shiftKey">> = {}): FakeEvent {
    const event = new FakeEvent(type, this, init.key, init.ctrlKey, init.metaKey, init.shiftKey);
    for (const listener of this.listeners.get(type) ?? []) listener(event);
    return event;
  }
  focus(): void { this.ownerDocument.activeElement = this; }
  remove(): void {
    if (!this.parentElement) return;
    const index = this.parentElement.children.indexOf(this);
    if (index >= 0) this.parentElement.children.splice(index, 1);
    this.parentElement = null;
  }
  find(attribute: string, value: string): FakeElement {
    if (this.attributes.get(attribute) === value) return this;
    for (const child of this.children) {
      try { return child.find(attribute, value); } catch { /* continue */ }
    }
    throw new Error(`missing [${attribute}=${value}]`);
  }
  descendants(tagName: string): FakeElement[] {
    const result: FakeElement[] = [];
    for (const child of this.children) {
      if (child.tagName === tagName.toUpperCase()) result.push(child);
      result.push(...child.descendants(tagName));
    }
    return result;
  }
}

class FakeDocument {
  activeElement: FakeElement | null = null;
  createElement(tagName: string): FakeElement { return new FakeElement(this, tagName.toUpperCase()); }
}

function setup(profile = createDefaultLocalProfile("member-22", "Avery")) {
  const document = new FakeDocument();
  const container = document.createElement("div");
  const previews: LocalProfile[] = [];
  const saves: LocalProfile[] = [];
  let cancels = 0;
  const editor = createProfileEditor({
    container: container as unknown as HTMLElement,
    profile,
    identityId: "member-22",
    onPreview: (next) => previews.push(next),
    onSave: (next) => saves.push(next),
    onCancel: () => cancels++,
  });
  return { document, container, editor, previews, saves, cancels: () => cancels };
}

describe("profile editor DOM adapter", () => {
  it("builds a closed labelled dialog with every enumerated appearance choice", () => {
    const { container, editor } = setup();
    const root = editor.root as unknown as FakeElement;
    assert.equal(container.children.length, 1);
    assert.equal(root.hidden, true);
    assert.equal(root.getAttribute("role"), "dialog");
    assert.equal(root.getAttribute("aria-modal"), "true");
    assert.ok(root.getAttribute("aria-labelledby"));
    assert.equal(root.find("data-field", "displayName").getAttribute("aria-describedby"), root.descendants("p")[1]?.id);
    assert.equal(root.find("data-field", "skinTone").children.length, SKIN_TONES.length);
    assert.equal(root.find("data-field", "outfit").children.length, OUTFITS.length);
    assert.equal(root.find("data-field", "accent").children.length, ACCENTS.length);
    assert.equal(root.find("data-field", "hair").children.length, HAIR_COLORS.length);
    assert.equal(root.find("data-field", "bodyShape").children.length, BODY_SHAPES.length);
    assert.equal(editor.state().open, false);
  });

  it("opens with focus, edits preview data, saves, and restores invoker focus", () => {
    const { document, editor, previews, saves } = setup();
    const trigger = document.createElement("button");
    trigger.focus();
    editor.open();
    const root = editor.root as unknown as FakeElement;
    const name = root.find("data-field", "displayName");
    assert.equal(document.activeElement, name);
    assert.equal(root.getAttribute("aria-hidden"), "false");

    name.value = "Avery Chen";
    name.dispatch("input");
    const outfit = root.find("data-field", "outfit");
    outfit.value = "sage";
    outfit.dispatch("change");
    assert.equal(editor.state().draft.displayName, "Avery Chen");
    assert.equal(editor.state().draft.appearance.outfit, "sage");
    assert.equal(editor.state().dirty, true);
    assert.equal(previews.at(-1)?.appearance.outfit, "sage");

    root.find("data-action", "save").dispatch("click");
    assert.equal(saves.length, 1);
    assert.equal(saves[0]?.displayName, "Avery Chen");
    assert.equal(editor.state().open, false);
    assert.equal(document.activeElement, trigger);
  });

  it("blocks invalid names and exposes accessible validation", () => {
    const { document, editor, saves } = setup();
    editor.open();
    const root = editor.root as unknown as FakeElement;
    const name = root.find("data-field", "displayName");
    const save = root.find("data-action", "save");
    name.value = " ";
    name.dispatch("input");
    assert.equal(editor.state().valid, false);
    assert.equal(name.getAttribute("aria-invalid"), "true");
    assert.equal(save.disabled, true);
    // Form submit covers Enter/assistive submit even though a disabled click cannot fire in a browser.
    root.descendants("form")[0]?.dispatch("submit");
    assert.equal(saves.length, 0);
    assert.equal(document.activeElement, name);
  });

  it("resets appearance deterministically and Escape cancels the draft", () => {
    const profile = createDefaultLocalProfile("some-other-id", "River");
    const { editor, previews, cancels } = setup(profile);
    editor.open();
    const root = editor.root as unknown as FakeElement;
    const outfit = root.find("data-field", "outfit");
    outfit.value = outfit.value === "ink" ? "clay" : "ink";
    outfit.dispatch("change");
    const name = root.find("data-field", "displayName");
    name.value = "River Two";
    name.dispatch("input");
    root.find("data-action", "reset").dispatch("click");
    assert.deepEqual(editor.state().draft.appearance, createDefaultLocalProfile("member-22", "River").appearance);
    assert.equal(editor.state().draft.displayName, "River Two");

    const event = root.dispatch("keydown", { key: "Escape" });
    assert.equal(event.defaultPrevented, true);
    assert.equal(cancels(), 1);
    assert.deepEqual(editor.state().draft, profile);
    assert.deepEqual(previews.at(-1), profile);
  });

  it("traps keyboard focus and supports command-enter save", () => {
    const { document, editor, saves } = setup();
    editor.open();
    const root = editor.root as unknown as FakeElement;
    const first = root.find("data-field", "displayName");
    const last = root.find("data-action", "save");
    last.focus();
    assert.equal(root.dispatch("keydown", { key: "Tab" }).defaultPrevented, true);
    assert.equal(document.activeElement, first);
    first.focus();
    assert.equal(root.dispatch("keydown", { key: "Tab", shiftKey: true }).defaultPrevented, true);
    assert.equal(document.activeElement, last);
    assert.equal(root.dispatch("keydown", { key: "Enter", ctrlKey: true }).defaultPrevented, true);
    assert.equal(saves.length, 1);
  });

  it("includes visible contextual controls in its focus trap and skips hidden ones", () => {
    const { document, editor } = setup();
    const cameraStart = document.createElement("button");
    const cameraStop = document.createElement("button");
    cameraStop.hidden = true;
    editor.registerFocusables([
      cameraStart as unknown as HTMLElement,
      cameraStop as unknown as HTMLElement,
    ]);
    editor.open();
    const root = editor.root as unknown as FakeElement;
    const first = root.find("data-field", "displayName");
    cameraStart.focus();
    assert.equal(root.dispatch("keydown", { key: "Tab" }).defaultPrevented, true);
    assert.equal(document.activeElement, first);
    cameraStart.hidden = true;
    const save = root.find("data-action", "save");
    save.focus();
    assert.equal(root.dispatch("keydown", { key: "Tab" }).defaultPrevented, true);
    assert.equal(document.activeElement, first);
  });

  it("strictly updates caller data, then disposes without leaving DOM", () => {
    const { container, editor, previews } = setup();
    const next = createDefaultLocalProfile("new-person", "Morgan");
    const updated = editor.update(next);
    assert.deepEqual(updated.profile, next);
    assert.deepEqual(updated.draft, next);
    assert.deepEqual(previews.at(-1), next);
    assert.throws(() => editor.update({ ...next, displayName: "" }), RangeError);
    editor.open();
    editor.dispose();
    editor.dispose();
    assert.equal(container.children.length, 0);
  });
});
