import assert from 'node:assert/strict';
import fs from 'node:fs/promises';
import path from 'node:path';
import test from 'node:test';
import vm from 'node:vm';

const root = path.resolve(import.meta.dirname, '..');

async function read(relativePath) {
  return fs.readFile(path.join(root, relativePath), 'utf8');
}

async function clientSources() {
  const directory = path.join(root, 'public/js');
  const names = await fs.readdir(directory);
  return Promise.all(names.filter(name => name.endsWith('.js')).map(name => read(`public/js/${name}`)));
}

test('every static HTML id is referenced by authored client JavaScript', async () => {
  const html = await read('public/index.html');
  const scripts = (await clientSources()).join('\n');
  const ids = [...html.matchAll(/\bid="([^"]+)"/g)].map(match => match[1]);

  assert.ok(ids.length > 0);
  for (const id of ids) {
    assert.match(scripts, new RegExp(id.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')), `unused id: ${id}`);
  }
});

test('every static HTML class is used by CSS or authored JavaScript', async () => {
  const html = await read('public/index.html');
  const css = `${await read('public/css/bootstrap.css')}\n${await read('public/css/app.css')}`;
  const scripts = (await clientSources()).join('\n');
  const classes = [...html.matchAll(/\bclass="([^"]+)"/g)]
    .flatMap(match => match[1].split(/\s+/))
    .filter(Boolean);

  for (const className of new Set(classes)) {
    assert.ok(css.includes(className) || scripts.includes(className), `unused class: ${className}`);
  }
});

test('application actions use pointer events instead of click handlers', async () => {
  const scripts = (await Promise.all([
    read('public/js/app.js'),
    read('public/js/slideshow.js'),
    read('public/js/bootstrap-loader.js')
  ])).join('\n');

  assert.match(scripts, /pointerup/);
  assert.match(scripts, /keydown/);
  assert.doesNotMatch(scripts, /(?:\.on|addEventListener)\(\s*['"]click['"]/);
});

test('mobile navigation footer contains a rotating SVG arrow and exact metre output', async () => {
  const html = await read('public/index.html');
  const app = await read('public/js/app.js');
  const css = await read('public/css/app.css');

  assert.match(html, /<footer id="navigation-footer"/);
  assert.match(html, /<svg id="navigation-arrow"/);
  assert.match(app, /Distance\.bearing/);
  assert.match(app, /Math\.round\((?:metric\.distance|progress\.distance|distance)\)/);
  assert.match(app, /`\$\{distance\} m`/);
  assert.match(css, /#navigation-arrow[\s\S]*transform[\s\S]*transition/);
  assert.match(css, /touch-action:\s*manipulation/);
});

test('client bearing calculation points north and east correctly', async () => {
  const source = await read('public/js/distance.js');
  const context = { Wegwichtel: {} };
  vm.runInNewContext(source, context);

  const distance = context.Wegwichtel.Distance;
  assert.ok(Math.abs(distance.bearing({ lat: 0, lon: 0 }, { lat: 1, lon: 0 })) < 0.001);
  assert.ok(Math.abs(distance.bearing({ lat: 0, lon: 0 }, { lat: 0, lon: 1 }) - 90) < 0.001);
});



test('authored browser modules use the portable global object', async () => {
  const scripts = (await clientSources()).join('\n');

  assert.match(scripts, /globalThis\.Wegwichtel/);
  assert.doesNotMatch(scripts, /\bwindow\./);
});

test('route controls use accessible icon buttons with start, pause and stop states', async () => {
  const html = await read('public/index.html');
  const app = await read('public/js/app.js');
  const geo = await read('public/js/geolocation.js');

  assert.match(html, /id="start-route"[\s\S]*images\/icons\/play\.svg/);
  assert.match(html, /id="pause-route"[\s\S]*images\/icons\/pause\.svg/);
  assert.match(html, /id="stop-route"[\s\S]*images\/icons\/stop\.svg/);
  assert.match(html, /aria-label="Route starten"/);
  assert.match(html, /aria-label="Route pausieren"/);
  assert.match(html, /aria-label="Route beenden"/);
  assert.match(html, /<fieldset>[\s\S]*<legend>Routensteuerung<\/legend>/);
  assert.doesNotMatch(html, /role="group"/);
  assert.doesNotMatch(html, /aria-type=/);
  assert.match(app, /routeState:\s*'idle'/);
  assert.match(app, /state\.routeState = 'paused'/);
  assert.match(app, /state\.routeState = 'running'/);
  assert.match(app, /function endRoute/);
  assert.match(geo, /watchPosition/);
  assert.match(geo, /clearWatch/);
});

test('route control SVG files are present', async () => {
  for (const name of ['play.svg', 'pause.svg', 'stop.svg']) {
    const svg = await read(`public/images/icons/${name}`);
    assert.match(svg, /<svg/);
    assert.match(svg, /viewBox="0 0 48 48"/);
  }
});


test('pause and stop control audio and stop returns to route selection', async () => {
  const app = await read('public/js/app.js');
  const audio = await read('public/js/audio-player.js');

  assert.match(app, /function pauseRoute\(\)[\s\S]*ns\.AudioPlayer\.pause\(\)/);
  assert.match(app, /function endRoute[\s\S]*ns\.AudioPlayer\.unload\(\)/);
  assert.match(app, /endRoute\(\{ returnToRoutes: true \}\)/);
  assert.match(app, /navigate\('#routes-page', \{ replace: true \}\)/);
  assert.match(audio, /audio\.removeAttribute\('src'\)/);
  assert.match(audio, /audio\.load\(\)/);
});

test('the selected or triggered station is highlighted semantically', async () => {
  const app = await read('public/js/app.js');
  const css = await read('public/css/app.css');

  assert.match(app, /setActivePoi\(poi\.id\)/);
  assert.match(app, /attr\('aria-current', 'step'\)/);
  assert.match(css, /#poi-list button\[aria-current="step"\]/);
  assert.match(css, /aktuelle Station/);
});

test('POIs have live direction and metre indicators and take navigation priority', async () => {
  const app = await read('public/js/app.js');
  const css = await read('public/css/app.css');
  const html = await read('public/index.html');

  assert.match(app, /class="poi-proximity"/);
  assert.match(app, /function updatePoiIndicators\(position\)/);
  assert.match(app, /function nearestPendingPoi\(metrics\)/);
  assert.match(app, /if \(nextPoi\) showPoiNavigation\(position, nextPoi\)/);
  assert.match(app, /Nächster POI:/);
  assert.match(app, /find\('small'\)\.text\(`\$\{distance\} m`\)/);
  assert.match(app, /find\('svg'\)\.css\('transform'/);
  assert.match(css, /\.poi-proximity[\s\S]*grid-column:\s*2/);
  assert.match(css, /\.poi-proximity svg[\s\S]*transition:\s*transform/);
  assert.match(html, /Richtung zum nächsten POI/);
});


test('reaching a POI preloads audio without autoplay and uses the vibration wrapper', async () => {
  const app = await read('public/js/app.js');
  const audio = await read('public/js/audio-player.js');
  const html = await read('public/index.html');
  const loader = await read('public/js/bootstrap-loader.js');

  assert.match(app, /ns\.AudioPlayer\.load\(poi\.audioUrl\)/);
  assert.doesNotMatch(app, /AudioPlayer\.play\(/);
  assert.match(app, /ns\.Vibration\.start\(ns\.Vibration\.Patterns\.ACTIVE_POI\)/);
  assert.doesNotMatch(app, /navigator\.vibrate/);
  assert.match(app, /function stopRouteSensors\(\)[\s\S]*ns\.Vibration\.stop\(\)/);
  assert.match(audio, /audio\.load\(\)/);
  assert.match(html, /<audio id="poi-audio" controls preload="metadata" aria-label="Audiowiedergabe zur Station" hidden>/);
  assert.match(audio, /removeAttr\('hidden'\)/);
  assert.match(audio, /Date\.now\(\)/);
  assert.match(loader, /'vibration'/);
});


test('POI page uses icon-based back and slideshow controls in a single responsive row', async () => {
  const html = await read('public/index.html');
  const css = await read('public/css/app.css');
  const loader = await read('public/js/bootstrap-loader.js');

  assert.match(html, /class="back-button"[\s\S]*images\/icons\/back\.svg/);
  assert.match(html, /id="slide-prev"[\s\S]*images\/icons\/chevron-left\.svg/);
  assert.match(html, /id="slide-next"[\s\S]*images\/icons\/chevron-right\.svg/);
  assert.match(html, /<fieldset>[\s\S]*<legend>Bildnavigation<\/legend>/);
  assert.match(css, /#slideshow > fieldset[\s\S]*display:\s*grid/);
  assert.match(css, /grid-template-columns:\s*var\(--icon-button-size\) minmax\(0, 1fr\) var\(--icon-button-size\)/);
  assert.match(css, /#slideshow > fieldset[\s\S]*align-items:\s*center/);
  assert.match(css, /#slideshow > fieldset > button\.ui-button[\s\S]*width:\s*var\(--icon-button-size\)/);
  assert.match(css, /#slideshow > fieldset > div[\s\S]*min-width:\s*0/);
  assert.match(css, /#slideshow > fieldset > div[\s\S]*justify-content:\s*center/);
  assert.match(css, /#slide-caption[\s\S]*overflow-wrap:\s*anywhere/);
  assert.match(css, /#slide-image[\s\S]*object-fit:\s*contain/);
  assert.doesNotMatch(css, /#slideshow img\s*\{/);
  assert.match(html, /images\/icons\/play\.svg" alt="Route starten"/);
  assert.match(html, /images\/icons\/chevron-left\.svg" alt="Vorheriges Bild"/);
  assert.doesNotMatch(loader, /controlgroup/);
});
