// src/_node-compat.ts
//
// DOM stubs for Node.js environments.
// When headless tests import the barrel (src/index.ts), browser-targeting classes
// like MatrixActorHtmlElement (extends HTMLElement) and MxInterface (customElements.define)
// fail because the DOM API doesn't exist. This shim provides enough surface
// for class declarations to evaluate AND for headless component tests to run.
//
// In the browser this file is a silent no-op — all globals already exist.

export {};

interface NodeCompatShadowRoot {
  mode: string;
  innerHTML: string;
  getElementById: (id?: string) => null;
  querySelector: (selector?: string) => null;
  querySelectorAll: (selector?: string) => [];
  appendChild: <T>(node: T) => T;
  removeChild: <T>(node: T) => T;
  replaceChildren: () => void;
  addEventListener: (type: string, handler: EventListenerOrEventListenerObject) => void;
  removeEventListener: (type: string, handler: EventListenerOrEventListenerObject) => void;
  dispatchEvent: (event: Event) => boolean;
  contains: (node: unknown) => boolean;
  childNodes: unknown[];
  children: unknown[];
  firstChild: null;
}

interface NodeCompatTemplateElement {
  innerHTML: string;
  style?: {
    setProperty: (name: string, value: string) => void;
    getPropertyValue: (name: string) => string;
    removeProperty: (name: string) => void;
  };
  content: {
    childNodes: unknown[];
    querySelectorAll: (selector?: string) => [];
  };
  tagName: string;
  getAttribute: (name?: string) => null;
  setAttribute: (name?: string, value?: string) => void;
}

interface NodeCompatDocument {
  createElement: (tag: string) => NodeCompatTemplateElement;
  createDocumentFragment: () => { appendChild: <T>(node: T) => T; childNodes: unknown[] };
  querySelector: (selector?: string) => null;
  querySelectorAll: (selector?: string) => [];
  body: {
    appendChild: <T>(node: T) => T;
    removeChild: <T>(node: T) => T;
  };
}

interface NodeCompatCustomElementsRegistry {
  define: (name: string, ctor?: CustomElementConstructor) => void;
  get: (name: string) => CustomElementConstructor | undefined;
  getName?: (ctor: CustomElementConstructor) => string | null;
  upgrade?: (root: Node) => void;
  whenDefined?: (name: string) => Promise<CustomElementConstructor>;
}

type NodeCompatGlobal = typeof globalThis & {
  HTMLElement?: typeof HTMLElement;
  document?: NodeCompatDocument;
  customElements?: NodeCompatCustomElementsRegistry;
  requestAnimationFrame?: (callback: FrameRequestCallback) => number;
  cancelAnimationFrame?: (handle: number) => void;
};

const g = globalThis as NodeCompatGlobal;

if (typeof g.HTMLElement === 'undefined') {
  const NodeCompatHTMLElement = class {
    private _attrs: Map<string, string> = new Map();
    private _children: unknown[] = [];
    private _style: Map<string, string> = new Map();
    shadowRoot: NodeCompatShadowRoot | null = null;
    localName = '';
    tagName = '';
    id = '';
    innerHTML = '';
    textContent = '';
    className = '';
    parentElement: Element | null = null;
    style = {
      setProperty: (name: string, value: string) => {
        this._style.set(name, value);
      },
      getPropertyValue: (name: string) => this._style.get(name) ?? '',
      removeProperty: (name: string) => {
        this._style.delete(name);
      },
    };

    // NamedNodeMap-like iterable for _bridgeAttributes
    get attributes(): Iterable<{ name: string; value: string }> {
      return Array.from(this._attrs.entries()).map(([name, value]) => ({ name, value }));
    }

    attachShadow(_opts?: { mode: string }): NodeCompatShadowRoot {
      const childNodes: unknown[] = [];
      const root: NodeCompatShadowRoot = {
        mode: 'open',
        innerHTML: '',
        getElementById: () => null,
        querySelector: () => null,
        querySelectorAll: () => [],
        appendChild: <T>(n: T) => {
          childNodes.push(n);
          return n;
        },
        removeChild: <T>(n: T) => {
          const index = childNodes.indexOf(n);
          if (index >= 0) childNodes.splice(index, 1);
          return n;
        },
        replaceChildren: () => {},
        addEventListener: () => {},
        removeEventListener: () => {},
        dispatchEvent: () => true,
        contains: (node: unknown) => childNodes.includes(node),
        childNodes,
        children: childNodes,
        firstChild: null,
      };
      this.shadowRoot = root;
      return root;
    }

    getAttribute(name: string): string | null {
      return this._attrs.get(name) ?? null;
    }

    setAttribute(name: string, value: string): void {
      this._attrs.set(name, value);
      if (name === 'id') this.id = value;
    }

    hasAttribute(name: string): boolean {
      return this._attrs.has(name);
    }

    removeAttribute(name: string): void {
      this._attrs.delete(name);
    }

    appendChild<T>(node: T): T {
      this._children.push(node);
      return node;
    }

    removeChild<T>(node: T): T {
      const idx = this._children.indexOf(node);
      if (idx >= 0) this._children.splice(idx, 1);
      return node;
    }

    get children(): unknown[] { return this._children; }
    get childNodes(): unknown[] { return this._children; }
    closest(_selector: string): Element | null { return null; }
    querySelector(_selector: string): Element | null { return null; }
    querySelectorAll(_selector: string): Element[] { return []; }
    addEventListener(_type: string, _handler: EventListenerOrEventListenerObject): void {}
    removeEventListener(_type: string, _handler: EventListenerOrEventListenerObject): void {}
    dispatchEvent(_event: Event): boolean { return true; }
  };
  g.HTMLElement = NodeCompatHTMLElement as unknown as typeof HTMLElement;
}

if (typeof g.document === 'undefined') {
  // Minimal DOM shim for Node.js — intentionally incomplete, only what the framework uses.
  // These casts are necessary because TypeScript's DOM types are far richer than this shim.
  g.document = {
    createElement(tag: string): NodeCompatTemplateElement {
      const lowerTag = tag.toLowerCase();
      const definedCtor = g.customElements?.get?.(lowerTag);
      if (definedCtor) {
        const element = new definedCtor() as HTMLElement & { localName?: string; tagName?: string };
        element.localName = lowerTag;
        element.tagName = lowerTag.toUpperCase();
        return element as unknown as NodeCompatTemplateElement;
      }
      const content = {
        childNodes: [] as unknown[],
        querySelectorAll: (_selector?: string) => [] as unknown as [],
      };
      const element = new g.HTMLElement() as HTMLElement & NodeCompatTemplateElement & {
        localName?: string;
        tagName?: string;
        content?: typeof content;
      };
      element.localName = lowerTag;
      element.tagName = lowerTag.toUpperCase();
      if (lowerTag === 'template') {
        element.content = content;
      }
      return Object.assign(element, {
        innerHTML: '',
        getAttribute: () => null,
        setAttribute: () => {},
      });
    },
    createDocumentFragment() {
      return { appendChild: <T>(n: T) => n, childNodes: [] as unknown[] };
    },
    querySelector() {
      return null;
    },
    querySelectorAll() {
      return [] as unknown as [];
    },
    body: { appendChild: <T>(n: T) => n, removeChild: <T>(n: T) => n },
  } as NodeCompatDocument as typeof g.document;
}

if (typeof g.customElements === 'undefined') {
  const registry = new Map<string, CustomElementConstructor>();
  const pending = new Map<string, Array<(ctor: CustomElementConstructor) => void>>();
  g.customElements = {
    define(name: string, ctor?: CustomElementConstructor) {
      if (!ctor) {
        throw new TypeError(`Custom element '${name}' requires a constructor`);
      }
      if (registry.has(name)) return;
      registry.set(name, ctor);
      const waiters = pending.get(name);
      if (waiters) {
        pending.delete(name);
        for (const resolve of waiters) {
          resolve(ctor);
        }
      }
    },
    get(name: string) {
      return registry.get(name);
    },
    getName(ctor: CustomElementConstructor) {
      for (const [name, current] of registry.entries()) {
        if (current === ctor) return name;
      }
      return null;
    },
    upgrade() {},
    whenDefined(name: string) {
      const existing = registry.get(name);
      if (existing) return Promise.resolve(existing);
      return new Promise<CustomElementConstructor>((resolve) => {
        const waiters = pending.get(name);
        if (waiters) {
          waiters.push(resolve);
          return;
        }
        pending.set(name, [resolve]);
      });
    },
  } as NodeCompatCustomElementsRegistry as typeof g.customElements;
}

if (typeof g.requestAnimationFrame === 'undefined') {
  g.requestAnimationFrame = ((callback: FrameRequestCallback) => {
    const handle = setTimeout(() => callback(Date.now()), 16);
    return Number(handle);
  }) as typeof g.requestAnimationFrame;
}

if (typeof g.cancelAnimationFrame === 'undefined') {
  g.cancelAnimationFrame = ((handle: number) => {
    clearTimeout(handle);
  }) as typeof g.cancelAnimationFrame;
}
