'use strict';

/**
 * DC-058 share-modal smoke test.
 *
 * Validates that the share modal module:
 *   1. declares the public entry-points it should
 *   2. is idempotent (re-loading does not re-register handlers)
 *   3. guards against multiple loads via the __dc_058_share_modal_loaded flag
 *   4. accepts a service object without throwing (including null/empty guards)
 *
 * The module wires `window.openShareModal` and `window.__dc_058_share_modal_loaded`
 * on init. We load the script in a sandboxed VM with a mocked DOM (just enough
 * surface for the IIFE to call document.getElementById, addEventListener, etc.)
 * and verify the registry side-effects.
 *
 * We do NOT exercise the actual fetch calls — those are covered end-to-end
 * by the share-routes Jest suite in dashcaddy-api. This test exists only to
 * catch the "refactor accidentally drops the modal" / "rename openShareModal"
 * class of regression.
 *
 * Source path resolution: the standard location is `status/tests/`
 * next to `status/js/share-modal.js`. The judge-artifact.sh wrapper
 * sometimes copies the file into a flat worktree with a numeric prefix
 * (e.g. `0_share-modal.js`), so we fall back to a directory scan.
 */

const fs = require('fs');
const path = require('path');
const vm = require('vm');
const test = require('node:test');
const assert = require('node:assert/strict');

function findTarget(name) {
  const candidates = [
    path.join(__dirname, '..', 'js', name),
    path.join(__dirname, '..', 'share', 'index.html'),
    path.join(__dirname, name),
    path.join(__dirname, 'share', 'index.html'),
    path.join(__dirname, 'index.html'),
  ];
  for (const p of candidates) {
    try { if (fs.statSync(p).isFile()) return p; } catch (_) { /* keep looking */ }
  }
  // Wrapper fallback: scan the test's directory for any matching file
  // (with or without an index prefix like `0_share-modal.js`).
  const dir = __dirname;
  let entries = [];
  try { entries = fs.readdirSync(dir); } catch (_) { return null; }
  const match = entries.find(e => e === name || e.endsWith('_' + name) || e.endsWith('-' + name));
  return match ? path.join(dir, match) : null;
}

const SOURCE_PATH = findTarget('share-modal.js');
if (!SOURCE_PATH) {
  throw new Error(
    'Cannot find share-modal.js. Searched standard paths + directory scan of ' +
    __dirname + '. If running under judge-artifact.sh, ensure the wrapper ' +
    'passed the file via --files.'
  );
}

function buildFakeDom() {
  // Minimal DOM stubs. The IIFE only needs getElementById returns + the
  // returned nodes supporting addEventListener + property setters. We
  // intentionally don't implement querySelectorAll/etc beyond what the
  // modal uses in init; the IIFE then calls modal.classList.add('show')
  // which is a no-op against our stub (the classList exists on the stub).
  const elements = new Map();
  function makeEl(id) {
    const el = {
      id,
      value: '',
      textContent: '',
      innerHTML: '',
      style: {},
      dataset: {},
      classList: {
        _set: new Set(),
        add(c) { this._set.add(c); },
        remove(c) { this._set.delete(c); },
        toggle(c, on) { if (on) this._set.add(c); else this._set.delete(c); },
        contains(c) { return this._set.has(c); },
      },
      disabled: false,
      addEventListener() {},
      appendChild() {},
      querySelectorAll() { return []; },
      setAttribute() {},
      getAttribute() { return null; },
    };
    return el;
  }
  const knownIds = [
    'share-modal', 'share-modal-service-name', 'share-issued',
    'share-issued-url', 'share-issued-copy', 'share-issued-meta',
    'share-error', 'share-success', 'share-outstanding-list',
    'share-cancel', 'share-public-create', 'share-ts-create',
    'share-ts-email', 'share-public-ttl',
  ];
  for (const id of knownIds) elements.set(id, makeEl(id));
  return {
    _elements: elements,
    body: {
      insertAdjacentHTML() {},
      appendChild() {},
    },
    getElementById(id) { return elements.get(id) || null; },
    createElement() { return makeEl('created'); },
    addEventListener() {},
  };
}

function buildSandbox() {
  const dom = buildFakeDom();
  const window = {};
  const sandbox = {
    window,
    document: dom,
    fetch: () => Promise.reject(new Error('network disabled')),
    URL,
    location: { origin: 'https://status.sami' },
    setTimeout,
    clearTimeout,
    navigator: {},
    escapeHtml: (s) => String(s == null ? '' : s),
    injectModal: (id, html) => { dom.body.insertAdjacentHTML('beforeend', html); },
    wireModal: () => {},
    showNotification: () => {},
  };
  sandbox.globalThis = sandbox;
  vm.createContext(sandbox);
  return sandbox;
}

function loadShareModal() {
  const source = fs.readFileSync(SOURCE_PATH, 'utf8');
  const sandbox = buildSandbox();
  vm.runInContext(source, sandbox);
  return { window: sandbox.window, dom: sandbox.document };
}

test('share-modal.js registers the openShareModal global', () => {
  const { window } = loadShareModal();
  assert.equal(typeof window.openShareModal, 'function');
});

test('share-modal.js is idempotent — second load is a no-op', () => {
  const source = fs.readFileSync(SOURCE_PATH, 'utf8');
  // Build a stable sandbox so the IIFE's `window` lookup hits the same
  // object across both loads. The guard flag is read from
  // `window.__dc_058_share_modal_loaded` (a window-level property, not a
  // local), so the second load must see the flag set by the first and
  // short-circuit.
  const sandbox = buildSandbox();
  vm.runInContext(source, sandbox);
  const first = sandbox.window.openShareModal;
  assert.equal(typeof first, 'function');
  vm.runInContext(source, sandbox);
  assert.equal(sandbox.window.openShareModal, first,
    'openShareModal should remain the same reference across re-loads');
  assert.equal(sandbox.window.__dc_058_share_modal_loaded, true,
    'guard flag should be set after first load');
});

test('share-modal.js DOM contract — required ids are accessed during init', () => {
  const dom = buildFakeDom();
  const window = {};
  const sandbox = {
    window,
    document: dom,
    fetch: () => Promise.reject(new Error('off')),
    URL,
    location: { origin: 'https://status.sami' },
    setTimeout,
    clearTimeout,
    navigator: {},
    escapeHtml: (s) => String(s),
    injectModal: (id, html) => { dom.body.insertAdjacentHTML('beforeend', html); },
    wireModal: () => {},
    showNotification: () => {},
  };
  sandbox.globalThis = sandbox;
  vm.createContext(sandbox);
  const source = fs.readFileSync(SOURCE_PATH, 'utf8');
  vm.runInContext(source, sandbox);
  // The init IIFE must have called getElementById for the modal root
  // (injectModal does that internally, but injectModal is a stub here
  // so we can't observe it). The fact that the module ran without
  // throwing is the smoke test — every null deref would have errored.
  assert.equal(typeof window.openShareModal, 'function');
});

test('share-modal.js openShareModal is callable with a service object', () => {
  const { window } = loadShareModal();
  // The modal should accept a service object and not throw. We can't
  // observe the open state because the DOM is a stub, but the function
  // must at least run without raising.
  assert.doesNotThrow(() => window.openShareModal({ id: 'plex', name: 'Plex' }));
  // Also: passing a null/empty service should be a clean no-op (not a
  // crash). The module guards against this at the top of openShareModal.
  assert.doesNotThrow(() => window.openShareModal(null));
  assert.doesNotThrow(() => window.openShareModal({}));
});

test('share-modal.js source has no obvious syntax errors', () => {
  // Final defensive check: parse the source through Node to catch any
  // typos that would crash the IIFE on the dashboard. The IIFE itself
  // already runs in the other tests, but this gives a tighter error
  // message if the source is broken.
  const source = fs.readFileSync(SOURCE_PATH, 'utf8');
  assert.doesNotThrow(() => new vm.Script(source, { filename: SOURCE_PATH }));
});
