import assert from 'node:assert/strict';
import fs from 'node:fs';
import path from 'node:path';
import test from 'node:test';
import { fileURLToPath } from 'node:url';

const here = path.dirname(fileURLToPath(import.meta.url));

function readControlApp(): string {
  return fs.readFileSync(path.join(here, 'ControlApp.tsx'), 'utf8');
}

function readControlAppCss(): string {
  return fs.readFileSync(path.join(here, 'ControlApp.module.css'), 'utf8');
}

void test('ControlApp: звук молнии (public/molniya.mp3)', () => {
  const src = readControlApp();
  assert.ok(src.includes('molniya.mp3'));
  assert.ok(src.includes('playLightningEffectSound'));
  assert.ok(src.includes('LIGHTNING_EFFECT_MS'));
});

void test('ControlApp: звук заморозки (public/zamorozka.mp3)', () => {
  const src = readControlApp();
  assert.ok(src.includes('zamorozka.mp3'));
  assert.ok(src.includes('playFreezeEffectSound'));
});

void test('ControlApp: звук луча света (public/luch_sveta.mp3)', () => {
  const appSrc = readControlApp();
  const sfxSrc = fs.readFileSync(path.join(here, 'sunbeamSfx.ts'), 'utf8');
  assert.ok(appSrc.includes('playSunbeamEffectSound'));
  assert.ok(appSrc.includes('getSunbeamEffectLifeMs'));
  assert.ok(sfxSrc.includes('luch_sveta.mp3'));
  assert.ok(sfxSrc.includes('playbackRate'));
  assert.ok(sfxSrc.includes('SUNBEAM_PLAYBACK_RATE'));
});

void test('ControlApp: звук облака яда (public/oblako-yada.mp3)', () => {
  const appSrc = readControlApp();
  const sfxSrc = fs.readFileSync(path.join(here, 'poisonCloudSfx.ts'), 'utf8');
  assert.ok(appSrc.includes('getPoisonCloudEffectLifeMs'));
  assert.ok(appSrc.includes('playPoisonCloudEffectSound'));
  assert.ok(sfxSrc.includes('oblako-yada.mp3'));
  assert.ok(sfxSrc.includes('playbackRate'));
});

void test('ControlApp: активация ловушки «яд» спавнит poisonCloud как на пульте эффектов', () => {
  const src = readControlApp();
  assert.ok(src.includes("trap?.type === 'poison'"));
  assert.ok(src.includes("type: 'poisonCloud'"));
  assert.ok(src.includes('trap_pc_'));
  assert.ok(src.includes('playPoisonCloudEffectSound(poisonLifeMs)'));
});

void test('ControlApp: эффект «взрыв» + ловушка используют explosion webm/mp3', () => {
  const appSrc = readControlApp();
  const sfxSrc = fs.readFileSync(path.join(here, 'explosionSfx.ts'), 'utf8');
  assert.ok(appSrc.includes("title={t('control.explosion')}"));
  assert.ok(appSrc.includes("selectEffectTool('explosion')"));
  assert.ok(appSrc.includes("type: 'explosion'"));
  assert.ok(appSrc.includes('getExplosionEffectLifeMs'));
  assert.ok(appSrc.includes('playExplosionEffectSound'));
  assert.ok(appSrc.includes("trap?.type === 'explosion'"));
  assert.ok(appSrc.includes('trap_ex_'));
  assert.ok(sfxSrc.includes('explosion.mp3'));
  assert.ok(sfxSrc.includes('aerial-debris-smoke.webm'));
  assert.ok(appSrc.includes('ExplosionVideoOverlay'));
  const videoOverlaySrc = fs.readFileSync(
    path.join(here, '..', 'shared', 'effects', 'ExplosionVideoOverlay.tsx'),
    'utf8',
  );
  assert.ok(videoOverlaySrc.includes('explosionEffectVideoUrl'));
});

void test('ControlApp: эффекты в пульте, иконки с тултипами и подписью для a11y', () => {
  const src = readControlApp();
  assert.ok(src.includes("t('control.instruments')"));
  assert.ok(src.includes("t('control.descriptionTool')"));
  assert.ok(src.includes("t('control.descriptionMissing')"));
  assert.ok(src.includes('openSceneDescription'));
  assert.ok(!src.includes('SceneDescriptionViewModal'));
  assert.ok(src.includes("t('control.effects')"));
  assert.ok(src.includes("t('control.tools')"));
  assert.ok(src.includes("t('control.fieldEffects')"));
  assert.ok(src.includes("t('control.actionEffects')"));
  assert.ok(src.includes("t('control.darknessControl')"));
  assert.ok(src.includes("t('control.explorerBrush')"));
  assert.ok(src.includes("t('control.closerBrush')"));
  assert.ok(src.includes("tool: 'closeBrush'") || src.includes("selectEffectTool('closeBrush')"));
  assert.ok(src.includes("mode: b.tool === 'closeBrush' ? 'cover' : 'reveal'") || src.includes("'cover'"));
  assert.ok(src.includes('SceneDarknessOverlay'));
  assert.ok(src.includes('useSceneDarknessState'));
  assert.ok(src.includes("t('control.sunbeam')"));
  assert.ok(src.includes("t('control.lightning')"));
  assert.ok(src.includes("title={t('control.water')}"));
  assert.ok(src.includes("title={t('control.fire')}"));
  assert.ok(src.includes("title={t('control.darkness')}"));
  assert.ok(src.includes("title={t('control.poisonCloud')}"));
  assert.ok(src.includes("title={t('control.fog')}"));
  assert.ok(src.includes("ariaLabel={t('control.fog')}"));
  assert.ok(src.includes('selectEffectTool'), 'повторный клик снимает инструмент');
  assert.ok(src.includes("tool.tool === next ? 'none' : next"));
  assert.ok(src.includes('iconOnly'));
  assert.ok(src.includes("title={t('control.clearEffects')}"));
  assert.ok(src.includes("ariaLabel={t('control.clearEffects')}"));
  assert.ok(src.includes('#e5484d'));
  const instruments = src.indexOf("t('control.instruments')");
  const fx = src.indexOf("t('control.effects')");
  const story = src.indexOf("t('control.storyLine')");
  assert.ok(
    instruments !== -1 && fx !== -1 && instruments < fx,
    'Блок инструментов должен быть выше эффектов',
  );
  assert.ok(fx !== -1 && story !== -1 && fx < story, 'Блок эффектов должен быть выше сюжетной линии');
});

void test('ControlApp: чекбокс привязки токенов к сетке', () => {
  const src = readControlApp();
  const css = readControlAppCss();
  assert.ok(src.includes('useTokenGridSnapSession'));
  assert.ok(src.includes('snapNormToGridCell'));
  assert.ok(src.includes('data-testid="token-grid-snap"'));
  assert.ok(src.includes("t('control.snapTokensToGrid')"));
  assert.ok(src.includes('snapAllTokensToGrid'));
  assert.ok(src.includes('snapNorm={snapNormActive}'));
  assert.ok(css.includes('.snapToGrid'));
});

void test('ControlApp: сюжетная линия — колонка сверху вниз и фон как у карточек ветвления', () => {
  const src = readControlApp();
  const css = readControlAppCss();
  const story = src.indexOf("t('control.storyLine')");
  assert.ok(story !== -1);
  assert.ok(src.includes('className={styles.storyScroll}'));
  assert.match(css, /\.storyScroll[\s\S]*?justify-content:\s*flex-start/);
  assert.match(css, /\.storyScroll[\s\S]*?background:\s*var\(--color-overlay-dark-2\)/);
  assert.match(css, /\.branchCard[\s\S]*?background:\s*var\(--color-overlay-dark-2\)/);
});

void test('ControlApp: клик по истории добавляет новый шаг, не подавляет запись', () => {
  const src = readControlApp();
  assert.ok(!src.includes('suppressNextHistoryPushRef'));
  assert.ok(!src.includes('arr.includes(cur)'));
  assert.ok(src.includes('historyRef.current = [...arr, cur]'));
});

void test('ControlApp: текущая сцена в истории — последнее вхождение', () => {
  const src = readControlApp();
  assert.ok(src.includes('history.lastIndexOf(currentGraphNodeId)'));
  assert.ok(src.includes('idx === currentHistoryIdx'));
});

void test('ControlApp: сюжетная линия не меняет граф и сцены проекта', () => {
  const src = readControlApp();
  const story = src.indexOf("t('control.storyLine')");
  assert.ok(story !== -1);
  const tail = src.slice(story);
  assert.ok(!tail.includes('addSceneGraphNode'));
  assert.ok(!tail.includes('removeSceneGraphNode'));
  assert.ok(!tail.includes('addSceneGraphEdge'));
  assert.ok(!tail.includes('removeSceneGraphEdge'));
  assert.ok(!tail.includes('updateProject'));
  assert.ok(tail.includes('ipcChannels.project.setCurrentGraphNode'));
});

void test('ControlApp: слой кисти не использует курсор not-allowed (ластик тоже crosshair)', () => {
  const src = readControlApp();
  const css = readControlAppCss();
  assert.ok(!src.includes("tool.tool === 'eraser' ? 'not-allowed'"));
  assert.ok(src.includes('className={styles.brushLayer}'));
  assert.match(css, /\.brushLayer[\s\S]*?cursor:\s*crosshair/);
});

void test('ControlApp: радиус кисти не в блоке предпросмотра', () => {
  const src = readControlApp();
  const previewLabel = src.indexOf("t('control.screenPreview')");
  const radius = src.indexOf("t('control.brushRadius')");
  assert.ok(previewLabel !== -1 && radius !== -1);
  assert.ok(
    radius < previewLabel,
    'Слайдер радиуса должен быть в пульте (файл: выше заголовка предпросмотра)',
  );
});

void test('ControlApp: музыка разделена на сцену и кампанию', () => {
  const src = readControlApp();
  assert.ok(src.includes("t('control.sceneMusic')"));
  assert.ok(src.includes("t('control.gameMusic')"));
  // при музыке сцены — кампанию ставим на паузу
  assert.ok(src.includes('allowCampaignAudio'));
  assert.ok(
    src.includes('campaignAudioSpecKey'),
    'кампания: перезагрузка аудио привязана к списку треков, не к смене сцены',
  );
  assert.match(src, /pause campaign\./i);
});

void test('ControlApp: у каждой аудиозаписи есть регулятор громкости под транспортом', () => {
  const src = readControlApp();
  const card = fs.readFileSync(path.join(here, 'ControlAudioCard.tsx'), 'utf8');
  const css = readControlAppCss();
  assert.ok(src.includes('ControlAudioCard'));
  assert.ok(src.includes("t('control.volume')"));
  assert.ok(src.includes('sceneAudioGainRef'));
  assert.ok(src.includes('campaignAudioGainRef'));
  assert.ok(src.includes('applyAudioGain'));
  assert.ok(card.includes('VolumeSpeakerIcon'));
  assert.ok(card.includes('styles.audioVolume'));
  assert.ok(card.includes('styles.audioVolumeRow'));
  assert.match(css, /\.audioControls[\s\S]*?flex-direction:\s*column/);
  assert.match(css, /\.audioVolumeRow\b/);
  assert.match(css, /\.audioVolumeIcon\b/);
  assert.match(css, /\.audioVolume\b/);
});

void test('ControlApp: весь контент скроллится в окне, отступы сверху и снизу равны', () => {
  const css = readControlAppCss();
  assert.match(css, /\.page\s*\{[^}]*padding:\s*16px/s);
  assert.match(css, /\.page\s*\{[^}]*overflow:\s*auto/s);
  assert.doesNotMatch(css, /\.rightStack\s*\{[^}]*overflow-y:\s*auto/s);
});

void test('ControlApp: вода/дождь гасят огонь; ambient огня/дождя + слайдер «Звук эффектов»', () => {
  const src = readControlApp();
  const fireSfx = fs.readFileSync(path.join(here, 'fireAmbientSfx.ts'), 'utf8');
  const rainSfx = fs.readFileSync(path.join(here, 'rainAmbientSfx.ts'), 'utf8');
  const i18n = fs.readFileSync(path.join(here, '../editor/i18n/editorMessages.ts'), 'utf8');

  assert.ok(src.includes('extinguishFireAlong'));
  assert.ok(src.includes("types: ['fire']"));
  assert.ok(src.includes("hitMode: 'brush'"));
  assert.ok(src.includes('effectsSoundRow'));
  assert.ok(src.includes('setFireAmbientActive'));
  assert.ok(src.includes('setRainAmbientActive'));
  assert.ok(src.includes('hasFireOnScene'));
  assert.ok(src.includes('hasRainOnScene'));
  assert.ok(src.includes("t('control.effectsSound')"));
  assert.ok(fireSfx.includes('fire-ambient.mp3'));
  assert.ok(rainSfx.includes('rain-ambient.mp3'));
  assert.ok(i18n.includes("'control.effectsSound': 'Звук эффектов'"));

  const radius = src.indexOf("t('control.brushRadius')");
  const effectsSound = src.indexOf("t('control.effectsSound')");
  assert.ok(radius !== -1 && effectsSound !== -1 && radius < effectsSound);
});

void test('ControlApp: загрузка камп. аудио — useEffect зависит только от api и campaignAudioSpecKey', () => {
  const src = readControlApp();
  const re = /\/\/ Campaign elements:[\s\S]*?useEffect\(\(\) => \{[\s\S]*?\}\s*,\s*\[([^\]]*)\]\s*\)\s*;/;
  const m = re.exec(src);
  assert.ok(m, 'ожидается useEffect загрузки кампании после комментария Campaign elements');
  const depList = m[1];
  assert.ok(depList !== undefined);
  const deps = depList
    .split(',')
    .map((s) => s.trim())
    .filter(Boolean);
  assert.deepEqual(
    deps,
    ['api', 'campaignAudioSpecKey'],
    'смена сцены / allowCampaignAudio / campaignAudioRefs не должны перезапускать загрузку кампании',
  );
  assert.ok(!/\ballowCampaignAudio\b/.test(depList));
  assert.ok(!/\bcurrentScene\b/.test(depList));
  assert.ok(!/\bproject\b/.test(depList));
  assert.ok(!/\bcampaignAudioRefs\b/.test(depList));
});
