// 音が実際に鳴るかを確かめる。
//
// 「lowpass が『ローパス』と訳されて音が出ない」を静的検査だけに任せると、
// 次に別の綴りが同じ壊れ方をしたとき、また出荷してしまう。
// ここでは Web Audio のノード生成と start() を数えて、
//   ・鳴らそうとしたか（beep が途中で return していないか）
//   ・生成中に例外が出ていないか
// を見る。自動再生の制限は --autoplay-policy で外す。
import fs from 'fs';
import { spawn } from 'child_process';

const CHROME = 'C:/Program Files/Google/Chrome/Application/chrome.exe';
const PORT = 9523, profile = 'C:/Users/ADMINI~1/AppData/Local/Temp/lm-audio';
const PAGE = process.argv[2] || 'build/index-test.html';
fs.rmSync(profile, { recursive: true, force: true });
const chrome = spawn(CHROME, ['--headless=new', '--disable-gpu', '--no-sandbox', '--no-first-run',
  '--autoplay-policy=no-user-gesture-required',
  '--remote-debugging-port=' + PORT, '--user-data-dir=' + profile,
  '--window-size=1400,900', 'about:blank'], { stdio: 'ignore' });
const sleep = ms => new Promise(r => setTimeout(r, ms));

let list = null;
for (let i = 0; i < 40; i++) {
  try { list = await (await fetch(`http://127.0.0.1:${PORT}/json/list`)).json(); if (list.length) break; } catch { }
  await sleep(300);
}
const ws = new WebSocket(list.find(t => t.type === 'page').webSocketDebuggerUrl);
await new Promise(r => ws.addEventListener('open', r));
let id = 0; const pend = new Map(); const errors = [];
ws.addEventListener('message', e => {
  const m = JSON.parse(e.data);
  if (m.id && pend.has(m.id)) { pend.get(m.id)(m.result); pend.delete(m.id); }
  if (m.method === 'Runtime.exceptionThrown')
    errors.push('EXCEPTION: ' + (m.params.exceptionDetails.exception?.description || m.params.exceptionDetails.text));
  if (m.method === 'Runtime.consoleAPICalled' && m.params.type === 'error')
    errors.push('console.error: ' + m.params.args.map(a => a.value ?? a.description).join(' '));
});
const send = (m, p = {}) => new Promise(res => { const i = ++id; pend.set(i, res); ws.send(JSON.stringify({ id: i, method: m, params: p })); });
const ev = async x => (await send('Runtime.evaluate', { expression: x, returnByValue: true, awaitPromise: true })).result?.value;

await send('Page.enable'); await send('Runtime.enable');

// ページが動き出す前に Web Audio を計測できるよう仕込む
await send('Page.addScriptToEvaluateOnNewDocument', {
  source: `
  window.__audio = {nodes:0, starts:0, throws:[], ctxs:0};
  // createOscillator などは AudioContext ではなく BaseAudioContext の側にある。
  // Cls.prototype だけを見ると一つも捕まらない。鎖をたどる。
  const wrap = (Cls) => {
    if (!Cls) return;
    for (let P = Cls.prototype; P && P !== Object.prototype; P = Object.getPrototypeOf(P))
    for (const m of Object.getOwnPropertyNames(P)) {
      if (!/^create/.test(m)) continue;
      const f = P[m];
      if (typeof f !== 'function') continue;
      P[m] = function(...a){
        try {
          const n = f.apply(this, a);
          window.__audio.nodes++;
          if (n && typeof n.start === 'function') {
            const s = n.start.bind(n);
            n.start = (...b) => { window.__audio.starts++; try { return s(...b); }
                                  catch(e){ window.__audio.throws.push('start: '+e.message); } };
          }
          return n;
        } catch(e){ window.__audio.throws.push(m+': '+e.message); throw e; }
      };
    }
  };
  const OrigAC = window.AudioContext || window.webkitAudioContext;
  if (OrigAC) {
    wrap(OrigAC);
    const Wrapped = function(...a){ window.__audio.ctxs++; return new OrigAC(...a); };
    Wrapped.prototype = OrigAC.prototype;
    window.AudioContext = Wrapped;
    if (window.webkitAudioContext) window.webkitAudioContext = Wrapped;
  }
  // 列挙値を入れた瞬間に投げる例外を、握りつぶさず記録する
  for (const [Cls, prop] of [[window.BiquadFilterNode,'type'], [window.OscillatorNode,'type'],
                             [window.WaveShaperNode,'oversample']]) {
    if (!Cls) continue;
    const d = Object.getOwnPropertyDescriptor(Cls.prototype, prop);
    if (!d || !d.set) continue;
    Object.defineProperty(Cls.prototype, prop, Object.assign({}, d, {
      set(v){ try { d.set.call(this, v); }
              catch(e){ window.__audio.throws.push(Cls.name+'.'+prop+' = '+JSON.stringify(v)+' : '+e.message); throw e; } }
    }));
  }
  `,
});

await send('Page.navigate', { url: 'file:///' + process.cwd().replace(/\\/g, '/') + '/' + PAGE });
await sleep(4500);

// 入会の問答を抜ける
for (let i = 0; i < 14; i++) {
  const done = await ev(`(()=>{
    const dlg=[...document.querySelectorAll('.win,.dialog,[role=dialog]')].find(d=>d.offsetParent);
    const bs=[...(dlg||document).querySelectorAll('button')].filter(b=>b.offsetParent && !b.disabled);
    const pick = bs.find(b=>/次|始|署名|決定|OK/.test(b.textContent)) || bs[bs.length-1];
    if(!pick) return true; pick.click(); return false; })()`);
  await sleep(400);
  if (done) break;
}
await sleep(1200);

// 音を入れる。1.6.3 でコードが難読化され setSound / soundOn / beep という
// 名前は消えたので、内部ではなく **設定画面のチェックボックス**（#opt-sound は
// HTML 側なので潰れない）を押す。名前に依存しない分、次の版でも生き残る。
const setup = await ev(`(()=>{
  const r = {};
  const m = document.getElementById("m-options"); if (m) m.click();
  const cb = document.getElementById("opt-sound");
  r.found = !!cb;
  if (cb && !cb.checked) { cb.click(); r.clicked = true; }
  r.checked = cb ? cb.checked : null;
  // 効果音は出来事に合わせてしか鳴らないので、短い観察では 0 のこともある。
  // 音楽は鳴り続けるので、こちらも入れて「鳴る道が生きているか」を見る。
  const mu = document.getElementById("opt-music");
  r.musicFound = !!mu;
  if (mu && !mu.checked) { mu.click(); r.musicClicked = true; }
  r.musicChecked = mu ? mu.checked : null;
  return r;
})()`);
await sleep(800);
// 設定窓を閉じて、ふつうに遊ばせる
await ev(`(()=>{const x=document.querySelector('[data-close]'); if(x) x.click();})()`);

// 音は出来事に合わせて鳴る。しばらく回して、鳴りに行った回数を数える。
const before = await ev('JSON.parse(JSON.stringify(window.__audio))');
await sleep(12000);
const played = await ev(`(()=>({
  before: null, guards: null,
  after: JSON.parse(JSON.stringify(window.__audio)),
  ctxState: null, soundOn: null, audioLive: null
}))()`);
played.before = before;
played.guards = setup;
played.ctxState = await ev('(()=>{try{return window.__audio.ctxs?"created":"none"}catch(e){return null}})()');
played.soundOn = setup.checked;
played.audioLive = setup.found;

const startsMade = played.after.starts - played.before.starts;
const nodesMade = played.after.nodes - played.before.nodes;
console.log(JSON.stringify({
  ページ: PAGE,
  '音の設定(#opt-sound)': played.soundOn,
  '音声文脈': played.ctxState,
  '設定項目の有無': played.audioLive,
  作られた音源ノード: nodesMade,
  'start() 回数': startsMade,
  '設定操作': played.guards,
  'Web Audio の例外': played.after.throws,
  ページの例外: errors.slice(0, 5),
}, null, 1));

const ok = played.soundOn === true && startsMade > 0 && played.after.throws.length === 0;
console.log(ok ? '\n音の判定: OK（鳴らしに行っている）'
  : '\n音の判定: 失敗 — 上の値を確認してください');
ws.close(); chrome.kill();
process.exit(ok ? 0 : 1);
