'use strict';

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

const statusRoot = path.join(__dirname, '..');

function makeElement(attrs = {}, text = '', card = null) {
  return {
    attrs: { ...attrs }, textContent: text, placeholder: '', title: '',
    getAttribute(name) { return this.attrs[name] || null; },
    closest(selector) { return selector === '[data-status]' ? card : null; },
  };
}

function loadI18n(fetchImpl, elements = {}) {
  const source = fs.readFileSync(path.join(statusRoot, 'js', 'i18n.js'), 'utf8');
  const listeners = {};
  const document = {
    readyState: 'loading', documentElement: {},
    addEventListener(name, fn) { listeners[name] = fn; },
    querySelectorAll(selector) { return elements[selector] || []; },
    querySelector() { return null; }, getElementById() { return null; },
    createElement() { return { style: {}, appendChild() {}, addEventListener() {}, setAttribute() {} }; },
  };
  const context = {
    window: {}, document, console, fetch: fetchImpl,
    localStorage: { getItem() { return null; }, setItem() {} },
    setTimeout, clearTimeout,
  };
  vm.runInNewContext(source, context, { filename: 'i18n.js' });
  return { api: context.window.DCI18n, listeners };
}

test('English dictionary is fetched and semantic keys never replace English card copy', async () => {
  const card = makeElement({ 'data-i18n': 'card.internet' }, 'Internet');
  const calls = [];
  const { api } = loadI18n(async url => {
    calls.push(url);
    return { ok: true, async json() { return { translations: { 'card.internet': 'Internet' } }; } };
  }, {
    '[data-i18n]': [card], '[data-i18n-placeholder]': [],
    '[data-i18n-live-status]': [], '[data-i18n-title]': [],
  });
  await api.loadTranslations('en');
  api.applyTranslations();
  assert.deepEqual(calls, ['/api/v1/i18n/translations/en']);
  assert.equal(card.textContent, 'Internet');
  assert.equal(api.isLoaded(), true);
});


test('failed language switch preserves existing readable labels', async () => {
  const label = makeElement({ 'data-i18n': 'card.internet' }, 'Internet');
  const elements = {
    '[data-i18n]': [label], '[data-i18n-placeholder]': [],
    '[data-i18n-live-status]': [], '[data-i18n-title]': [],
  };
  const { api } = loadI18n(async () => ({ ok: false, async json() { return {}; } }), elements);
  api.setLanguage('es');
  await new Promise(resolve => setTimeout(resolve, 0));
  assert.equal(api.isLoaded(), false);
  assert.equal(label.textContent, 'Internet');
  assert.notEqual(label.textContent, 'card.internet');
});

test('language switch preserves ON runtime state and translates a dynamic DNS pill immediately', async () => {
  const onlineCard = { getAttribute(name) { return name === 'data-status' ? 'on' : null; } };
  const staticPill = makeElement({ 'data-i18n-live-status': 'binary' }, 'ON', onlineCard);
  const dynamicDnsPill = makeElement({ 'data-i18n-live-status': 'binary' }, 'ON', onlineCard);
  const elements = {
    '[data-i18n]': [], '[data-i18n-placeholder]': [],
    '[data-i18n-live-status]': [staticPill, dynamicDnsPill], '[data-i18n-title]': [],
  };
  const { api } = loadI18n(async () => ({
    ok: true,
    async json() { return { translations: { 'card.status.on': 'ENC', 'card.status.off': 'APAG' } }; },
  }), elements);
  await api.loadTranslations('es');
  api.applyTranslations();
  assert.equal(staticPill.textContent, 'ENC');
  assert.equal(dynamicDnsPill.textContent, 'ENC');
  assert.notEqual(staticPill.textContent, 'APAG', 'online state must not reset to OFF');
});

test('dynamic card template marks its pill and reapplies translations after insertion', () => {
  const source = fs.readFileSync(path.join(statusRoot, 'js', 'globals.js'), 'utf8');
  assert.match(source, /data-i18n-live-status="binary"/);
  assert.match(source, /DCI18n\.isLoaded\(\)/);
  assert.match(source, /DCI18n\.applyTranslations\(\)/);
});


test('ordinary service card built after i18n load is translated immediately', () => {
  class Node {
    constructor(tag = 'div') {
      this.tag = tag; this.children = []; this.attrs = {}; this.textContent = '';
      this.className = ''; this.id = ''; this.style = {};
      this.classList = { add() {}, toggle() {} };
    }
    appendChild(child) { this.children.push(child); child.parentNode = this; return child; }
    setAttribute(name, value) { this.attrs[name] = String(value); }
    getAttribute(name) { return this.attrs[name] || null; }
    closest(selector) {
      if (selector === '[data-status]' && this.attrs['data-status']) return this;
      return this.parentNode ? this.parentNode.closest(selector) : null;
    }
    addEventListener() {}
    querySelectorAll(selector) {
      const found = [];
      const visit = node => {
        const attr = selector.match(/^\[([^\]]+)\]$/);
        if (attr && Object.prototype.hasOwnProperty.call(node.attrs, attr[1])) found.push(node);
        if (selector === '.card' && node.className.split(/\s+/).includes('card')) found.push(node);
        node.children.forEach(visit);
      };
      this.children.forEach(visit);
      return found;
    }
  }
  const cards = new Node('section');
  const document = {
    createElement(tag) { return new Node(tag); },
    getElementById(id) { return id === 'cards' ? cards : null; },
    querySelector() { return null; },
  };
  const translations = { 'card.status.off': 'APAG', 'action.open': 'Abrir' };
  const window = {
    APPS: [{ id: 'demo', name: 'Demo', logo: '/demo.png' }],
    DCI18n: {
      isLoaded() { return true; },
      t(key) { return translations[key] || key; },
      applyTranslations() {
        cards.querySelectorAll('[data-i18n]').forEach(el => {
          el.textContent = this.t(el.getAttribute('data-i18n'));
        });
        cards.querySelectorAll('[data-i18n-live-status]').forEach(el => {
          const card = el.closest('[data-status]');
          const key = card.getAttribute('data-status') === 'on' ? 'card.status.on' : 'card.status.off';
          el.textContent = this.t(key);
        });
      },
    },
    open() {},
  };
  const context = {
    window, document, console, SITE: { dnsServers: {} },
    buildServiceUrl(id) { return 'https://' + id + '.sami'; },
    requestAnimationFrame(fn) { fn(); }, fetch: async () => ({ ok: true }),
    setTimeout, clearTimeout, performance: { now() { return 0; } },
  };
  const source = fs.readFileSync(path.join(statusRoot, 'js', 'core', 'grid.js'), 'utf8');
  vm.runInNewContext(source, context, { filename: 'grid.js' });
  // grid.js initializes APPS itself; emulate service loading after module init.
  window.APPS = [{ id: 'demo', name: 'Demo', logo: '/demo.png' }];
  window.buildGrid();
  const livePills = cards.querySelectorAll('[data-i18n-live-status]');
  const openButtons = cards.querySelectorAll('[data-i18n]');
  assert.equal(livePills.length, 1);
  assert.equal(livePills[0].textContent, 'APAG');
  assert.equal(openButtons.length, 1);
  assert.equal(openButtons[0].textContent, 'Abrir');
});

test('live health polling uses translated ON and OFF labels', () => {
  const source = fs.readFileSync(path.join(statusRoot, 'js', 'core', 'grid.js'), 'utf8');
  assert.match(source, /card\.status\.on/);
  assert.match(source, /card\.status\.off/);
  assert.doesNotMatch(source, /pill\.textContent\s*=\s*up\s*\?\s*['"]ON['"]\s*:\s*['"]OFF['"]/);
});


test('version modal escapes malicious API metadata before innerHTML rendering', () => {
  const html = fs.readFileSync(path.join(statusRoot, 'index.html'), 'utf8');
  const script = html.match(/<script>\s*\(function\(\) \{[\s\S]*?function escapeHtml[\s\S]*?window\.applyVersionUpdate = applyVersionUpdate;[\s\S]*?<\/script>/);
  assert.ok(script, 'expected inline version modal script');
  const escapeSource = script[0].match(/function escapeHtml\(value\) \{[\s\S]*?\n      \}/)[0];
  const formatSource = script[0].match(/function formatValue\(value\) \{[\s\S]*?\n      \}/)[0];
  const rowSource = script[0].match(/function renderInfoRow\(label, value\) \{[\s\S]*?\n      \}/)[0];
  const context = {};
  vm.runInNewContext(escapeSource + '\n' + formatSource + '\n' + rowSource, context);
  const payload = '<img src=x onerror="globalThis.pwned=1">';
  const row = context.renderInfoRow(payload, payload);
  assert.doesNotMatch(row, /<img\b/i);
  assert.doesNotMatch(row, /onerror="/i);
  assert.match(row, /&lt;img/);
  assert.match(row, /&quot;globalThis\.pwned=1&quot;/);
});

test('index loads the generated core bundle containing live-status translation logic', () => {
  const html = fs.readFileSync(path.join(statusRoot, 'index.html'), 'utf8');
  const bundle = fs.readFileSync(path.join(statusRoot, 'dist', 'core.js'), 'utf8');
  assert.match(html, /<script src="\/dist\/core\.js" defer><\/script>/);
  assert.match(bundle, /data-i18n-live-status/);
  assert.match(bundle, /card\.status\.on/);
  assert.match(bundle, /applyTranslations/);
});

test('translated controls preserve their visual glyph prefixes', () => {
  const html = fs.readFileSync(path.join(statusRoot, 'index.html'), 'utf8');
  assert.match(html, /data-i18n="filter\.online" data-i18n-prefix="🟢 "/);
  assert.match(html, /data-i18n="filter\.offline" data-i18n-prefix="🔴 "/);
  assert.match(html, /data-i18n="filter\.batch_operations" data-i18n-prefix="☰ "/);
  assert.match(html, /data-i18n-placeholder="filter\.services_placeholder" data-i18n-prefix="🔍 "/);
});
