Source: lib/core/runtime/AvenxMock.js

import { AvenxPage } from './AvenxPage.js';
import { nextTick } from '../reactive/scheduler.js';
import { assertSnapshot } from '../testing/snapshot.js';

/**
 * Creates a deep proxy to track calls and state modifications.
 * @param {object} target - Target object to proxy.
 * @param {string[]} path - Key path of the object being proxied.
 * @param {object} options - State change and calls tracking options.
 * @returns {object} Proxied target.
 */
function createDeepMockProxy(target, path = [], options) {
  return new Proxy(target, {
    get(t, prop, receiver) {
      if (prop === '$calls') return options.calls;
      if (prop === '$stateChanges') return options.stateChanges;
      if (prop === '$onStateChange') {
        return (cb) => {
          options.stateChangeCallbacks.push(cb);
          return () => {
            const idx = options.stateChangeCallbacks.indexOf(cb);
            if (idx !== -1) options.stateChangeCallbacks.splice(idx, 1);
          };
        };
      }
      if (prop === '$onCall') {
        return (cb) => {
          options.callCallbacks.push(cb);
          return () => {
            const idx = options.callCallbacks.indexOf(cb);
            if (idx !== -1) options.callCallbacks.splice(idx, 1);
          };
        };
      }
      if (prop === '$reset') {
        return () => {
          options.calls.length = 0;
          options.stateChanges.length = 0;
        };
      }
      if (prop === '$isMock') return true;

      // Avoid proxying standard Symbols or constructor properties
      if (typeof prop === 'symbol' || prop === 'constructor' || prop === 'prototype') {
        return Reflect.get(t, prop, receiver);
      }

      const val = Reflect.get(t, prop, receiver);
      if (typeof val === 'function') {
        return function (...args) {
          if (path.length === 0) {
            options.calls.push({ method: prop, args });
            options.callCallbacks.forEach((cb) => cb(prop, args));
          }
          return val.apply(receiver, args);
        };
      }
      if (val && typeof val === 'object' && !(val instanceof Date) && !(val instanceof RegExp)) {
        return createDeepMockProxy(val, [...path, prop], options);
      }
      return val;
    },
    set(t, prop, value, receiver) {
      if (typeof prop === 'symbol') {
        return Reflect.set(t, prop, value, receiver);
      }
      const fullPath = [...path, prop];
      const pathStr = fullPath.join('.');
      options.stateChanges.push({ prop: pathStr, value });
      options.stateChangeCallbacks.forEach((cb) => cb(pathStr, value));
      return Reflect.set(t, prop, value, receiver);
    },
  });
}

/**
 * Recursively serializes a DOM element to HTML.
 * @param {Element|object} el - Element to serialize.
 * @returns {string} Serialized HTML string.
 */
function getHTML(el) {
  if (!el) return '';
  // If JSDOM/real DOM
  if (typeof Element !== 'undefined' && el instanceof Element) {
    return el.innerHTML;
  }
  // If it's a mock element with custom innerHTML getter
  const desc =
    Object.getOwnPropertyDescriptor(Object.getPrototypeOf(el) || {}, 'innerHTML') ||
    Object.getOwnPropertyDescriptor(el, 'innerHTML');
  if (desc && typeof desc.get === 'function') {
    return el.innerHTML;
  }
  // Fallback serializing children
  if (el.childNodes && el.childNodes.length > 0) {
    return el.childNodes
      .map((c) => {
        if (c.nodeType === 3) return c.textContent;
        if (c.nodeType === 1) {
          const attrsStr = (c.attributes || []).map((attr) => ` ${attr.name}="${attr.value}"`).join('');
          const tag = c.tagName.toLowerCase();
          return `<${tag}${attrsStr}>${getHTML(c)}</${tag}>`;
        }
        return '';
      })
      .join('');
  }
  return el.innerHTML || '';
}

/**
 * Main testing utility class for Avenx components.
 */
export class AvenxMock {
  /**
   * Creates a mock bridge proxy.
   * @param {Function|object} bridgeClassOrObject - The bridge class or object to mock.
   * @param {object} [initialData] - Initial state override.
   * @returns {object} The mock bridge proxy.
   */
  static createMockBridge(bridgeClassOrObject, initialData = {}) {
    let instance;
    if (typeof bridgeClassOrObject === 'function') {
      instance = new bridgeClassOrObject();
    } else if (bridgeClassOrObject && typeof bridgeClassOrObject === 'object') {
      instance = Object.create(Object.getPrototypeOf(bridgeClassOrObject));
      Object.defineProperties(instance, Object.getOwnPropertyDescriptors(bridgeClassOrObject));
    } else {
      instance = {};
    }

    if (initialData) {
      Object.assign(instance, initialData);
    }

    const calls = [];
    const stateChanges = [];
    const stateChangeCallbacks = [];
    const callCallbacks = [];

    const options = {
      calls,
      stateChanges,
      stateChangeCallbacks,
      callCallbacks,
    };

    return createDeepMockProxy(instance, [], options);
  }

  /**
   * Creates a new testing sandbox.
   * @returns {AvenxSandbox}
   */
  static createSandbox() {
    return new AvenxSandbox();
  }

  /**
   * Creates a lightweight mock router for isolated page / route-dependent tests.
   * Attaches itself to `window.__avenx_routers` so component `$route` works.
   * @param {object} [options]
   * @param {object} [options.currentRoute] - Full route object override.
   * @param {string} [options.hash] - Initial hash (default `#/`).
   * @param {string} [options.page] - Active page name.
   * @param {object} [options.params] - Path params.
   * @param {object} [options.queryParams] - Query params (merged into `params.query`).
   * @param {Array<Function|object>} [options.guards] - Optional guards run on push/replace.
   * @returns {object} Mock router with `push`, `replace`, `getParams`, and `currentRoute`.
   */
  static createMockRouter(options = {}) {
    const initialHash =
      (options.currentRoute && options.currentRoute.hash) ||
      options.hash ||
      '#/';
    const [pathPart, queryPart] = initialHash.split('?');
    const parsedQuery = {};
    if (queryPart) {
      const qp = new URLSearchParams(queryPart);
      for (const [k, v] of qp.entries()) {
        if (v === 'true') parsedQuery[k] = true;
        else if (v === 'false') parsedQuery[k] = false;
        else if (/^\d+$/.test(v)) parsedQuery[k] = Number(v);
        else parsedQuery[k] = v;
      }
    }
    const queryFromOptions =
      options.queryParams ||
      (options.currentRoute && options.currentRoute.query) ||
      (options.currentRoute && options.currentRoute.params && options.currentRoute.params.query) ||
      parsedQuery;
    const pathParams = {
      ...(options.params || {}),
      ...(options.currentRoute && options.currentRoute.params ? { ...options.currentRoute.params } : {}),
    };
    delete pathParams.query;
    if (queryFromOptions && Object.keys(queryFromOptions).length > 0) {
      pathParams.query = { ...queryFromOptions };
    }

    const currentRoute = {
      hash: initialHash,
      path: (options.currentRoute && options.currentRoute.path) || pathPart || '',
      page: (options.currentRoute && options.currentRoute.page) || options.page || '',
      params: pathParams,
      query: { ...queryFromOptions },
    };

    const guards = Array.isArray(options.guards) ? options.guards.slice() : [];
    const calls = [];

    /**
     * @param {string} path
     * @param {'push'|'replace'} method
     */
    const navigate = (path, method) => {
      const hash = path.startsWith('#') ? path : `#${path.startsWith('/') ? path : `/${path}`}`;
      const [navPathPart, navQueryPart] = hash.split('?');
      const navQuery = {};
      if (navQueryPart) {
        const qp = new URLSearchParams(navQueryPart);
        for (const [k, v] of qp.entries()) {
          if (v === 'true') navQuery[k] = true;
          else if (v === 'false') navQuery[k] = false;
          else if (/^\d+$/.test(v)) navQuery[k] = Number(v);
          else navQuery[k] = v;
        }
      }
      const nextParams = { ...currentRoute.params };
      if (Object.keys(navQuery).length > 0) {
        nextParams.query = { ...navQuery };
      } else {
        delete nextParams.query;
      }
      const nextRoute = {
        hash,
        path: navPathPart,
        page: currentRoute.page,
        params: nextParams,
        query: navQuery,
      };

      for (const guard of guards) {
        const result =
          typeof guard === 'function'
            ? guard(nextRoute, { ...currentRoute })
            : guard && typeof guard.canActivate === 'function'
              ? guard.canActivate(nextRoute, { ...currentRoute })
              : true;
        if (result === false) {
          calls.push({ method, args: [path], blocked: true });
          return false;
        }
        if (typeof result === 'string') {
          return navigate(result, method);
        }
      }

      currentRoute.hash = nextRoute.hash;
      currentRoute.path = nextRoute.path;
      currentRoute.params = nextRoute.params;
      currentRoute.query = nextRoute.query;
      calls.push({ method, args: [path], blocked: false });
      return true;
    };

    const mockRouter = {
      currentRoute,
      guards,
      $calls: calls,
      $isMock: true,
      push(path) {
        return navigate(path, 'push');
      },
      replace(path) {
        return navigate(path, 'replace');
      },
      getParams() {
        return { ...currentRoute.params };
      },
      $reset() {
        calls.length = 0;
      },
    };

    if (typeof window === 'undefined') {
      global.window = global.window || {};
    }
    window.__avenx_routers = [mockRouter];

    return mockRouter;
  }

  /**
   * Triggers a DOM or custom event on an element.
   * Uses native event constructors for common DOM events and CustomEvent
   * for custom component events.
   * @param {Element|object} target - Target element.
   * @param {string} eventName - Event name.
   * @param {object} [detailOrOptions] - Event detail or event options.
   * @returns {Event|object}
   */
  static triggerEvent(target, eventName, detailOrOptions = {}) {
    if (!target) {
      throw new Error('triggerEvent requires a target element.');
    }

    const options =
      detailOrOptions && typeof detailOrOptions === 'object'
        ? detailOrOptions
        : {};

    // Real DOM element
    if (typeof target.dispatchEvent === 'function') {
      if ('value' in options && 'value' in target) {
        target.value = options.value;
      }

      let event;

      switch (eventName) {
        case 'click':
          if (typeof window !== 'undefined' && typeof window.MouseEvent !== 'undefined') {
            event = new window.MouseEvent(eventName, {
              bubbles: true,
              cancelable: true,
              ...options,
            });
          }
          break;

        case 'input':
          if (typeof window !== 'undefined' && typeof window.InputEvent !== 'undefined') {
            event = new window.InputEvent(eventName, {
              bubbles: true,
              cancelable: true,
              ...options,
            });
          }
          break;

        case 'keyup':
          if (typeof window !== 'undefined' && typeof window.KeyboardEvent !== 'undefined') {
            event = new window.KeyboardEvent(eventName, {
              bubbles: true,
              cancelable: true,
              ...options,
            });
          }
          break;

        default:
          break;
      }

      // Fallback for unsupported/missing native event constructors
      if (!event) {
        if (
          typeof window !== 'undefined' &&
          typeof window.CustomEvent !== 'undefined'
        ) {
          event = new window.CustomEvent(eventName, {
            bubbles: true,
            cancelable: true,
            detail: detailOrOptions,
          });
        } else if (
          typeof window !== 'undefined' &&
          typeof window.Event !== 'undefined'
        ) {
          event = new window.Event(eventName, {
            bubbles: true,
            cancelable: true,
          });
        }
      }

      if (!event) {
        throw new Error('No supported Event constructor available.');
      }

      target.dispatchEvent(event);
      return event;
    }

    // Mock element
    AvenxMock.trigger(target, eventName, detailOrOptions);

    return {
      type: eventName,
      target,
      ...options,
    };
  }

  /**
   * Triggers an event on an element.
   * Supports standard DOM Event/CustomEvent and custom MockNode trigger method.
   * @param {Element} element
   * @param {string} eventName
   * @param {object} [eventData]
   */
  static trigger(element, eventName, eventData = {}) {
    let event;
    if (typeof Event !== 'undefined' && typeof element.dispatchEvent === 'function') {
      if (typeof CustomEvent !== 'undefined') {
        event = new CustomEvent(eventName, { bubbles: true, cancelable: true, detail: eventData });
      } else {
        event = new Event(eventName, { bubbles: true, cancelable: true });
      }
      Object.assign(event, eventData);
    } else {
      event = {
        target: element,
        type: eventName,
        preventDefault() {
          this.defaultPrevented = true;
        },
        stopPropagation() {
          this.cancelBubble = true;
        },
        ...eventData,
      };
    }

    if (!event.target) {
      try {
        Object.defineProperty(event, 'target', { value: element, writable: true, configurable: true });
      } catch {
        event.target = element;
      }
    }

    let isRealDOM = false;
    if (typeof Element !== 'undefined' && element instanceof Element && typeof element.dispatchEvent === 'function') {
      isRealDOM = true;
      element.dispatchEvent(event);
    } else if (typeof element.dispatchEvent === 'function') {
      element.dispatchEvent(event);
    }

    if (!isRealDOM) {
      let current = element;
      while (current) {
        if (current.listeners && typeof current.listeners[eventName] === 'function') {
          current.listeners[eventName](event);
        } else if (current.addEventListener && typeof current.listeners === 'object' && current.listeners[eventName]) {
          current.listeners[eventName](event);
        }

        let hasRun = false;
        if (current.__avenxDelegatedListeners && current.__avenxDelegatedListeners.has(eventName)) {
          const entry = current.__avenxDelegatedListeners.get(eventName);
          const handler = typeof entry === 'function' ? entry : entry.handler;
          if (typeof handler === 'function') {
            handler(event);
            hasRun = true;
          }
        }

        if (current.__avenxDirectListeners && current.__avenxDirectListeners.has(eventName)) {
          const entry = current.__avenxDirectListeners.get(eventName);
          const handler = typeof entry === 'function' ? entry : entry.handler;
          if (typeof handler === 'function') {
            handler(event);
            hasRun = true;
          }
        }

        if (!hasRun && Array.isArray(current.recordedCalls)) {
          current.recordedCalls.forEach((call) => {
            if (call.method === 'addEventListener' && call.event === eventName && typeof call.callback === 'function') {
              call.callback(event);
            }
          });
        }

        if (event.cancelBubble) break;
        current = current.parentNode;
      }
    }
  }

  /**
   * Instantiates, mounts, and returns component instance with mocked props, slots, and state.
   * @param {Function} ComponentClass
   * @param {object} [options]
   * @returns {Promise<object>}
   */
  static mountTestComponent(ComponentClass, options) {
    return mountTestComponent(ComponentClass, options);
  }

  /**
   * Helper to dispatch DOM events synchronously and flush scheduler microtasks.
   * @param {Element} element
   * @param {string} eventType
   * @param {object} [detail]
   * @returns {Promise<void>}
   */
  static fireEvent(element, eventType, detail) {
    return fireEvent(element, eventType, detail);
  }

  /**
   * Waits for pending Promise microtasks / macrotasks to settle.
   * @returns {Promise<void>}
   */
  static flushPromises() {
    return flushPromises();
  }
}

/**
 * Settles the event loop so pending Promises and timers can complete.
 * Prefer this over ad-hoc `setTimeout` chains in unit tests.
 * @returns {Promise<void>}
 */
export function flushPromises() {
  return new Promise((resolve) => {
    if (typeof globalThis.setImmediate === 'function') {
      globalThis.setImmediate(resolve);
    } else {
      setTimeout(resolve, 0);
    }
  });
}

/**
 * Instantiates, mounts, and returns the component instance and target DOM element for testing.
 * @param {Function} ComponentClass - Component class to mount.
 * @param {object} [options] - Mounting options.
 * @param {object} [options.props] - Props to pass to component.
 * @param {object|string} [options.slots] - Slot content string, DOM node, or map of slot name to content.
 * @param {object} [options.state] - State overrides.
 * @param {object} [options.initialState] - Alias for state overrides.
 * @param {object} [options.bridges] - Mock or real bridges to pass.
 * @param {object} [options.components] - Child component map.
 * @param {Element} [options.container] - Target DOM container.
 * @param {object} [options.route] - Mock route options.
 * @returns {Promise<{instance: object, element: Element, container: Element, unmount: Function, html: string}>}
 */
export async function mountTestComponent(ComponentClass, options = {}) {
  const {
    props = {},
    slots = null,
    state = null,
    initialState = null,
    bridges = {},
    components = {},
    container = null,
    route = null,
  } = options;

  if (route) {
    AvenxMock.createMockRouter(typeof route === 'object' ? route : {});
  }

  let target = container;
  if (!target) {
    if (typeof document !== 'undefined' && typeof document.createElement === 'function') {
      target = document.createElement('div');
    } else {
      target = new AvenxSandbox()._createMockElement('div');
    }
  }

  if (slots) {
    if (typeof slots === 'string') {
      target.innerHTML = slots;
    } else if (typeof slots === 'object') {
      if (slots.nodeType) {
        target.appendChild(slots);
      } else {
        Object.entries(slots).forEach(([name, content]) => {
          if (name === 'default') {
            if (typeof content === 'string') {
              if (typeof target.insertAdjacentHTML === 'function') {
                target.insertAdjacentHTML('beforeend', content);
              } else {
                target.innerHTML += content;
              }
            } else if (content && content.nodeType) {
              target.appendChild(content);
            }
          } else {
            if (typeof content === 'string') {
              if (typeof document !== 'undefined' && typeof document.createElement === 'function') {
                const div = document.createElement('div');
                div.setAttribute('slot', name);
                div.innerHTML = content;
                target.appendChild(div);
              }
            } else if (content && content.nodeType) {
              if (typeof content.setAttribute === 'function') {
                content.setAttribute('slot', name);
              }
              target.appendChild(content);
            }
          }
        });
      }
    }
  }

  let instance;
  if (ComponentClass.prototype && ComponentClass.prototype instanceof AvenxPage) {
    instance = new ComponentClass(bridges, components, props);
  } else {
    try {
      instance = new ComponentClass(bridges, props);
    } catch {
      instance = new ComponentClass(props);
    }
  }

  const stateOverrides = state || initialState;
  if (stateOverrides && instance.state) {
    Object.assign(instance.state, stateOverrides);
  }

  instance.mount(target);
  await nextTick();

  const element =
    (typeof instance._getElement === 'function' && instance._getElement()) ||
    (typeof instance.getElement === 'function' && instance.getElement()) ||
    target.firstElementChild ||
    target;

  const matchesSelector = (node, selector) => {
    if (!node || node.nodeType !== 1 || !selector) return false;
    if (selector === '*') return true;
    if (/^[a-zA-Z][\w-]*$/.test(selector)) {
      return node.tagName && node.tagName.toLowerCase() === selector.toLowerCase();
    }
    if (selector.startsWith('.')) {
      const cls = selector.slice(1);
      const classAttr = (typeof node.getAttribute === 'function' && node.getAttribute('class')) || '';
      return (
        (node.classList && typeof node.classList.contains === 'function' && node.classList.contains(cls)) ||
        classAttr.split(/\s+/).includes(cls)
      );
    }
    const tagClass = selector.match(/^([a-zA-Z][\w-]*)\.([\w-]+)$/);
    if (tagClass) {
      const classAttr = (typeof node.getAttribute === 'function' && node.getAttribute('class')) || '';
      return (
        node.tagName &&
        node.tagName.toLowerCase() === tagClass[1].toLowerCase() &&
        ((node.classList && node.classList.contains(tagClass[2])) || classAttr.split(/\s+/).includes(tagClass[2]))
      );
    }
    if (typeof node.matches === 'function') {
      try {
        return node.matches(selector);
      } catch {
        return false;
      }
    }
    return false;
  };

  const collectElements = (root) => {
    const out = [];
    const visit = (node) => {
      if (!node) return;
      if (node.nodeType === 1) out.push(node);
      const kids = node.childNodes || node.children || [];
      for (let i = 0; i < kids.length; i++) {
        visit(kids[i]);
      }
    };
    visit(root);
    return out;
  };

  return {
    instance,
    component: instance,
    element,
    container: target,
    update: () => {
      if (typeof instance.update === 'function') {
        instance.update();
      }
    },
    unmount: () => {
      if (typeof instance.unmount === 'function') {
        instance.unmount();
      }
    },
    toMatchSnapshot(name = 'default', snapshotOptions = {}) {
      return assertSnapshot(element || target, name, snapshotOptions);
    },
    get html() {
      return getHTML(target);
    },
    find(selector) {
      if (!selector) return null;
      const roots = [element, target].filter(Boolean);
      for (const root of roots) {
        if (matchesSelector(root, selector)) return root;
        if (typeof root.querySelector === 'function') {
          const found = root.querySelector(selector);
          if (found) return found;
        }
        const nodes = collectElements(root);
        for (const node of nodes) {
          if (node !== root && matchesSelector(node, selector)) {
            return node;
          }
        }
      }
      return null;
    },
    findAll(selector) {
      if (!selector) return [];
      const seen = new Set();
      const out = [];
      const roots = [element, target].filter(Boolean);
      for (const root of roots) {
        if (typeof root.querySelectorAll === 'function') {
          const list = root.querySelectorAll(selector);
          for (let i = 0; i < list.length; i++) {
            if (!seen.has(list[i])) {
              seen.add(list[i]);
              out.push(list[i]);
            }
          }
        }
        for (const node of collectElements(root)) {
          if (matchesSelector(node, selector) && !seen.has(node)) {
            seen.add(node);
            out.push(node);
          }
        }
      }
      return out;
    },
    findComponent(ComponentClassOrName) {
      const matches = [];
      const roots = [element, target].filter(Boolean);
      for (const root of roots) {
        for (const node of collectElements(root)) {
          const comp = node.__avenx_comp_instance;
          if (!comp) continue;
          let ok;
          if (typeof ComponentClassOrName === 'function') {
            ok = comp instanceof ComponentClassOrName || comp.constructor === ComponentClassOrName;
          } else if (typeof ComponentClassOrName === 'string') {
            const name = ComponentClassOrName;
            ok =
              (comp.constructor && comp.constructor.name === name) ||
              (comp.name && comp.name === name) ||
              (typeof node.getAttribute === 'function' &&
                (node.getAttribute('data-avenx-comp') === name ||
                  node.getAttribute('data-avenx-comp-dynamic') === name));
          } else {
            ok = true;
          }
          if (ok) matches.push(comp);
        }
      }
      return matches.find((c) => c !== instance) || matches[0] || null;
    },
    async trigger(selectorOrEl, eventName, detail = {}) {
      let el = selectorOrEl;
      if (typeof selectorOrEl === 'string') {
        el = this.find(selectorOrEl);
      }
      if (!el) {
        throw new Error(`trigger: element not found for ${String(selectorOrEl)}`);
      }
      return fireEvent(el, eventName, detail);
    },
  };
}

/**
 * Helper to dispatch DOM events synchronously and flush the microtask scheduler.
 * @param {Element} element - Target element to fire event on.
 * @param {string} eventType - Type of event (e.g. 'click', 'input', 'change').
 * @param {object} [detail] - Event detail or options (e.g. { value: 'foo' }).
 * @returns {Promise<void>}
 */
export async function fireEvent(element, eventType, detail = {}) {
  if (!element) {
    throw new Error('fireEvent requires a target Element.');
  }

  if (detail && typeof detail === 'object') {
    if ('value' in detail && 'value' in element) {
      element.value = detail.value;
    }
    if ('checked' in detail && 'checked' in element) {
      element.checked = detail.checked;
    }
  }

  AvenxMock.trigger(element, eventType, detail);
  await nextTick();
}

/**
 * Sandbox container for isolating and registering components under test.
 */
export class AvenxSandbox {
  /**
   * Initializes the AvenxSandbox instance.
   */
  constructor() {
    /** @type {Map<string, Function>} */
    this.components = new Map();
    /** @type {object} */
    this.bridges = {};
  }

  /**
   * Registers a component class with the sandbox.
   * @param {string} name
   * @param {Function} compClass
   * @returns {AvenxSandbox}
   */
  register(name, compClass) {
    this.components.set(name, compClass);
    return this;
  }

  /**
   * Registers a bridge with the sandbox.
   * @param {string} name
   * @param {object} bridgeInstance
   * @returns {AvenxSandbox}
   */
  registerBridge(name, bridgeInstance) {
    this.bridges[name] = bridgeInstance;
    return this;
  }

  /**
   * Mocks the router state.
   * @param {object} route
   * @returns {AvenxSandbox}
   */
  setRoute(route) {
    AvenxMock.createMockRouter({
      currentRoute: route,
      hash: route && route.hash,
      page: route && route.page,
      params: route && route.params,
      queryParams: route && route.params && route.params.query,
    });
    return this;
  }

  /**
   * Waits for any pending scheduled updates to complete.
   * @returns {Promise<void>}
   */
  async waitForUpdate() {
    await new Promise((resolve) => setTimeout(resolve, 0));
  }

  /**
   * Mounts a component in isolation.
   * @param {Function} compClass
   * @param {object} [props]
   * @param {Element} [container]
   * @returns {object} Sandbox mount helper instance.
   */
  mount(compClass, props = {}, container = null) {
    let target = container;
    if (!target) {
      if (typeof document !== 'undefined' && typeof document.createElement === 'function') {
        target = document.createElement('div');
      } else {
        target = this._createMockElement('div');
      }
    }

    let instance;
    if (compClass.prototype instanceof AvenxPage) {
      instance = new compClass(this.bridges, this.components, props);
    } else {
      instance = new compClass(this.bridges, props);
    }

    instance.mount(target);

    return {
      instance,
      container: target,
      get html() {
        return getHTML(target);
      },
      update: () => {
        instance.update();
      },
      trigger: (selectorOrElement, eventName, eventData = {}) => {
        let el = selectorOrElement;
        if (typeof selectorOrElement === 'string') {
          if (typeof target.querySelector === 'function') {
            el = target.querySelector(selectorOrElement);
          } else {
            el = this._findMockElementBySelector(target, selectorOrElement);
          }
        }
        if (!el) {
          throw new Error(`Element not found: ${selectorOrElement}`);
        }
        AvenxMock.trigger(el, eventName, eventData);
      },
    };
  }

  /**
   * Internal helper to resolve mock DOM selector fallback.
   * @param {object} root - Root node to start traversal.
   * @param {string} selector - CSS selector string.
   * @returns {object|null} Matched element or null.
   * @private
   */
  _findMockElementBySelector(root, selector) {
    if (!root) return null;
    if (selector.startsWith('#')) {
      const id = selector.substring(1);
      const traverse = (node) => {
        if (node.getAttribute && node.getAttribute('id') === id) return node;
        if (node.attrs && node.attrs.id === id) return node;
        const children = node.childNodes || node.children || [];
        for (const child of children) {
          const res = traverse(child);
          if (res) return res;
        }
        return null;
      };
      return traverse(root);
    } else if (selector.startsWith('.')) {
      const className = selector.substring(1);
      const traverse = (node) => {
        if (node.getAttribute && node.getAttribute('class') === className) return node;
        if (node.attrs && node.attrs.class === className) return node;
        const children = node.childNodes || node.children || [];
        for (const child of children) {
          const res = traverse(child);
          if (res) return res;
        }
        return null;
      };
      return traverse(root);
    } else {
      const tag = selector.toUpperCase();
      const traverse = (node) => {
        if (node.tagName === tag) return node;
        const children = node.childNodes || node.children || [];
        for (const child of children) {
          const res = traverse(child);
          if (res) return res;
        }
        return null;
      };
      return traverse(root);
    }
  }

  /**
   * Helper to create a fallback mock element.
   * @param {string} tagName - Tag name of the element.
   * @returns {object} Fallback element object.
   * @private
   */
  _createMockElement(tagName) {
    const listeners = {};
    const element = {
      nodeType: 1,
      tagName: tagName.toUpperCase(),
      attrs: {},
      attributes: [],
      childNodes: [],
      children: [],
      listeners,
      hasAttribute(name) {
        return name in this.attrs;
      },
      getAttribute(name) {
        return this.attrs[name] !== undefined ? this.attrs[name] : null;
      },
      setAttribute(name, val) {
        this.attrs[name] = val;
      },
      removeAttribute(name) {
        delete this.attrs[name];
      },
      appendChild(child) {
        if (child.parentNode) {
          child.parentNode.removeChild(child);
        }
        child.parentNode = this;
        this.childNodes.push(child);
        if (child.nodeType === 1) {
          this.children.push(child);
        }
        return child;
      },
      removeChild(child) {
        const idx = this.childNodes.indexOf(child);
        if (idx !== -1) {
          this.childNodes.splice(idx, 1);
          child.parentNode = null;
        }
        const cIdx = this.children.indexOf(child);
        if (cIdx !== -1) {
          this.children.splice(cIdx, 1);
        }
        return child;
      },
      addEventListener(event, callback) {
        listeners[event] = callback;
      },
      removeEventListener(event, callback) {
        if (listeners[event] === callback) {
          delete listeners[event];
        }
      },
      querySelectorAll(selector) {
        if (selector === '*') {
          const result = [];
          const traverse = (node) => {
            const children = node.childNodes || node.children || [];
            children.forEach((child) => {
              if (child.nodeType === 1) {
                result.push(child);
              }
              traverse(child);
            });
          };
          traverse(this);
          return result;
        }
        return [];
      },
      get innerHTML() {
        return this.childNodes
          .map((c) => {
            if (c.nodeType === 3) return c.textContent;
            if (c.nodeType === 1) return c.outerHTML || `<${c.tagName.toLowerCase()}></${c.tagName.toLowerCase()}>`;
            return '';
          })
          .join('');
      },
      set innerHTML(val) {
        this.childNodes.forEach((c) => {
          c.parentNode = null;
        });
        this.childNodes = [];
        this.children = [];
      },
    };
    return element;
  }
}

export { assertSnapshot };