// 日本語化ビルドの動作検証: Chrome を CDP で叩き、
// 例外・console.error を集めながら各画面を開いてスクリーンショットを撮る。
import fs from 'fs';
import { spawn } from 'child_process';

const CHROME = 'C:/Program Files/Google/Chrome/Application/chrome.exe';
// 既定はそのビルドで作り直される index-test.html。以前は index-auto.html を
// 見ており、それが前バージョンの置き土産だったせいで、直したはずの不具合が
// テストにだけ出続けるということが起きた。作り直されないファイルは見ない。
const PAGE = process.argv[2] || 'build/index-test.html';
const TAG = process.argv[3] || 'ja';
const PORT = 9333;
const profile = 'C:/Users/ADMINI~1/AppData/Local/Temp/lm-cdp-' + TAG;

fs.rmSync(profile, { recursive: true, force: true });
const url = 'file:///' + process.cwd().replace(/\\/g, '/') + '/' + PAGE;

const chrome = spawn(CHROME, [
  '--headless=new', '--disable-gpu', '--no-sandbox', '--no-first-run',
  '--remote-debugging-port=' + PORT, '--user-data-dir=' + profile,
  '--window-size=1400,950', 'about:blank'
], { stdio: 'ignore' });

const sleep = ms => new Promise(r => setTimeout(r, ms));

async function targets() {
  const r = await fetch(`http://127.0.0.1:${PORT}/json/list`);
  return r.json();
}

let ws, id = 0;
const pending = new Map();
const errors = [];
const consoleMsgs = [];

function send(method, params = {}, sessionId) {
  const msgId = ++id;
  return new Promise((res, rej) => {
    pending.set(msgId, { res, rej });
    ws.send(JSON.stringify({ id: msgId, method, params, sessionId }));
  });
}

async function main() {
  // chrome の起動待ち
  let list = null;
  for (let i = 0; i < 40; i++) {
    try { list = await targets(); if (list.length) break; } catch (e) { }
    await sleep(300);
  }
  if (!list || !list.length) throw new Error('chrome did not come up');

  const target = list.find(t => t.type === 'page');
  ws = new WebSocket(target.webSocketDebuggerUrl);
  await new Promise(r => ws.addEventListener('open', r));

  ws.addEventListener('message', ev => {
    const m = JSON.parse(ev.data);
    if (m.id && pending.has(m.id)) {
      const p = pending.get(m.id); pending.delete(m.id);
      m.error ? p.rej(new Error(JSON.stringify(m.error))) : p.res(m.result);
      return;
    }
    if (m.method === 'Runtime.exceptionThrown') {
      const d = m.params.exceptionDetails;
      errors.push('EXCEPTION: ' + (d.exception?.description || d.text) +
        ' @' + (d.url || '') + ':' + d.lineNumber);
    }
    if (m.method === 'Runtime.consoleAPICalled' && ['error', 'warning'].includes(m.params.type)) {
      consoleMsgs.push(m.params.type + ': ' +
        m.params.args.map(a => a.value ?? a.description ?? a.type).join(' '));
    }
  });

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

  await send('Page.navigate', { url });
  await sleep(4000);

  const evalJs = async expr => {
    const r = await send('Runtime.evaluate', { expression: expr, returnByValue: true, awaitPromise: true });
    if (r.exceptionDetails) errors.push('EVAL: ' + (r.exceptionDetails.exception?.description || r.exceptionDetails.text));
    return r.result?.value;
  };

  const shot = async name => {
    const r = await send('Page.captureScreenshot', { format: 'png' });
    fs.writeFileSync(`shots/${TAG}-${name}.png`, Buffer.from(r.data, 'base64'));
  };

  const out = {};

  // --- 入会画面 -------------------------------------------------------
  out.enrolTitle = await evalJs('document.querySelector("#creator .tname").textContent');
  await shot('01-enrol');

  // 署名して開始
  await evalJs('document.getElementById("soldbtn").click()');
  await sleep(6000);
  await shot('02-play');
  out.docTitle = await evalJs('document.title');

  // --- 早送り: エンジンそのものを回して数時間ぶんの下降を進める -------
  out.ff = [];
  for (let k = 0; k < 6; k++) {
    const r = await evalJs('(function(){try{ fastForward(3600); autosave(); return {ok:true, act:G.act, lvl:G.level, gold:Math.round(G.gold), kills:G.counters.kills}; }catch(e){ return {ok:false, err:String(e)}; }})()');
    out.ff.push(r);
    await sleep(600);
  }
  await sleep(3000);
  await shot('03-running');
  out.logLines = await evalJs('document.querySelectorAll("#log > div").length');
  out.logSample = await evalJs(
    '[...document.querySelectorAll("#log > div")].slice(-6).map(d=>d.textContent.trim())');

  // --- 各ウィンドウを開く ---------------------------------------------
  const opens = [
    ['m-options', '04-options'],
    ['m-bestiary', '05-bestiary'],
    ['m-trophies', '06-achievements'],
    ['m-stats', '07-stats'],
    ['m-honour', '08-prestige'],
    ['m-charter', '09-skills'],
    ['m-counter', '10-shop'],
    ['m-guild', '11-guildhouse'],
  ];
  out.opened = [];
  for (const [btn, name] of opens) {
    const ok = await evalJs(`(()=>{const b=document.getElementById(${JSON.stringify(btn)});
      if(!b) return "missing"; if(b.classList.contains("hidden")) return "hidden";
      b.click(); return "clicked";})()`);
    out.opened.push(btn + '=' + ok);
    if (ok === 'clicked') { await sleep(1200); await shot(name);
      await evalJs('[...document.querySelectorAll(".win:not(.hidden) [data-win=close]")].forEach(b=>b.click())');
      await sleep(400); }
  }

  // --- ゲージが実際に塗られているか -------------------------------------
  // clip-path に渡す "inset(0 " を訳してしまい、塗りが出なくなったことがある。
  // 例外も出ず lint も通る壊れ方なので、描画結果を直に測る。
  out.bars = await evalJs(`(()=>{
    const rows=[...document.querySelectorAll('.pbar')].map(b=>{
      const f=b.querySelector('.fill'); if(!f) return null;
      const cs=getComputedStyle(f);
      const vis=b.offsetParent!==null && b.getBoundingClientRect().width>0;
      return {id:b.id||b.className, clip:cs.clipPath, w:Math.round(f.getBoundingClientRect().width), vis};
    }).filter(Boolean);
    const shown=rows.filter(r=>r.vis);
    return {
      total: rows.length, visible: shown.length,
      badClip: shown.filter(r=>!/^inset\\(/.test(r.clip)).map(r=>r.id+':'+r.clip),
      zeroWidth: shown.filter(r=>r.w===0).map(r=>r.id),
    };
  })()`);

  // --- 文字化け / 未訳の検出 -------------------------------------------
  out.mojibake = await evalJs(`(()=>{const t=document.body.innerText;
    return {tofu:(t.match(/\\uFFFD/g)||[]).length,
            dollarBrace:(t.match(/\\$\\{/g)||[]).length,
            quotEnt:(t.match(/&quot;|&amp;lt;/g)||[]).length};})()`);
  out.jaRatio = await evalJs(`(()=>{const t=document.body.innerText;
    const ja=(t.match(/[ぁ-んァ-ヶ一-龯]/g)||[]).length;
    const en=(t.match(/[A-Za-z]/g)||[]).length;
    return {ja, en, pct: Math.round(ja/(ja+en)*100)};})()`);

  // --- セーブの往復 ------------------------------------------------------
  // セーブコードの往復。1.6.3 でコードが難読化され chronicleCode という名前は
  // 消えたので、内部を呼ばずに **書き出しの窓**（id は HTML 側なので潰れない）を
  // 開いて中身を読む。名前に頼らない分、次の版でも生き残る。
  await evalJs(`(()=>{const b=document.getElementById("m-save"); if(b) b.click();})()`);
  await sleep(700);
  out.saveRoundTrip = await evalJs(`(()=>{try{
     const ta = document.getElementById("savetext");
     if(!ta) return {ok:false, err:"書き出しの窓が見つかりません"};
     const code = (ta.value||"").trim();
     if(!code) return {ok:false, err:"セーブコードが空です"};
     // decodeURIComponent(escape(atob(...))) は多バイト文字で壊れる。
     // 実際これで日本語の冒険者名が孤立サロゲートに化け、セーブが壊れたように
     // 見えた（コード自体は正しかった）。バイト列から UTF-8 として読む。
     const bin = atob(code);
     const bytes = Uint8Array.from(bin, c => c.charCodeAt(0));
     const j = JSON.parse(new TextDecoder("utf-8").decode(bytes));
     return {len: code.length, v: j.v, name: j.g && j.g.name, act: j.g && j.g.act, ok:true};
   }catch(e){ return {ok:false, err:String(e)}; }})()`);
  await evalJs(`(()=>{const x=document.querySelector('[data-close="savedlg"]'); if(x) x.click();})()`);

  out.errors = errors;
  out.console = consoleMsgs.slice(0, 30);
  console.log(JSON.stringify(out, null, 1));

  ws.close();
  chrome.kill();
}

main().catch(e => { console.error('TEST FAILED:', e.message); chrome.kill(); process.exit(1); });
