"""
Unit tests for FreeExile Visceral Procedural Audio & Sound FX Engine (PoE2 spirit).
Verifies sfx_engine.js pure Web Audio API synthesis, multi-bus architecture,
visceral sound generation routines, gesture unlock, settings persistence,
native haptic synchronization, and complete integration in index.html, main.js, and combat_skills.js.
"""

import subprocess
import unittest
from pathlib import Path

PROJECT_ROOT = Path(__file__).resolve().parent.parent.parent
CLIENT_WEBAPP = PROJECT_ROOT / "client" / "webapp"
SFX_ENGINE_JS = CLIENT_WEBAPP / "js" / "audio" / "sfx_engine.js"
AUDIO_HAPTICS_JS = CLIENT_WEBAPP / "js" / "engine" / "audio_haptics.js"
COMBAT_SKILLS_JS = CLIENT_WEBAPP / "js" / "engine" / "combat_skills.js"
MAIN_JS = CLIENT_WEBAPP / "js" / "main.js"
INDEX_HTML = CLIENT_WEBAPP / "index.html"


class TestVisceralAudioEngine(unittest.TestCase):
    @classmethod
    def setUpClass(cls):
        with open(SFX_ENGINE_JS, "r", encoding="utf-8") as f:
            cls.sfx_code = f.read()
        with open(INDEX_HTML, "r", encoding="utf-8") as f:
            cls.html_code = f.read()
        templates_dir = CLIENT_WEBAPP / "templates"
        if templates_dir.is_dir():
            for tpl in templates_dir.glob("*.html"):
                cls.html_code += "\n" + tpl.read_text(encoding="utf-8")
        with open(MAIN_JS, "r", encoding="utf-8") as f:
            cls.main_code = f.read()
        with open(COMBAT_SKILLS_JS, "r", encoding="utf-8") as f:
            cls.combat_code = f.read()
        with open(AUDIO_HAPTICS_JS, "r", encoding="utf-8") as f:
            cls.haptics_code = f.read()

    def test_sfx_engine_file_exists_and_line_count(self):
        """sfx_engine.js must exist and adhere to file length standards (<= 500 lines)."""
        self.assertTrue(SFX_ENGINE_JS.exists(), "sfx_engine.js must exist in client/webapp/js/audio/")
        line_count = len(self.sfx_code.splitlines())
        self.assertLessEqual(line_count, 500, f"sfx_engine.js line count ({line_count}) must be <= 500 lines")

    def test_javascript_syntax_validity_with_node(self):
        """All modified engine JS files must pass Node.js syntax parsing check."""
        files_to_check = [SFX_ENGINE_JS, MAIN_JS, COMBAT_SKILLS_JS, AUDIO_HAPTICS_JS]
        for js_file in files_to_check:
            res = subprocess.run(
                ["node", "-c", str(js_file)],
                capture_output=True,
                text=True,
                check=False,
            )
            self.assertEqual(res.returncode, 0, f"Node syntax error in {js_file.name}: {res.stderr}")

    def test_es_module_and_window_exports(self):
        """SFXEngine must export as ES Module and expose window globals for browser compatibility."""
        self.assertIn("export class SFXEngine", self.sfx_code)
        self.assertIn("export const sfxEngine", self.sfx_code)
        self.assertIn("export default sfxEngine", self.sfx_code)
        self.assertIn("window.SFXEngine = SFXEngine", self.sfx_code)
        self.assertIn("window.sfxEngine = sfxEngine", self.sfx_code)
        self.assertIn("window.FreeExileAudio = {", self.sfx_code)

    def test_multi_bus_architecture_defined(self):
        """Audio engine must define multi-bus routing: master, sfx, music, ambient, ui gain nodes."""
        bus_nodes = ["masterGain", "sfxGain", "musicGain", "ambientGain", "uiGain"]
        for bus in bus_nodes:
            self.assertIn(f"this.{bus}", self.sfx_code, f"Bus {bus} must be declared in SFXEngine")
        # Master connects to destination, sub-buses connect to master
        self.assertIn("this.masterGain.connect(this.audioCtx.destination)", self.sfx_code)
        self.assertIn("this.sfxGain.connect(this.masterGain)", self.sfx_code)
        self.assertIn("this.musicGain.connect(this.masterGain)", self.sfx_code)
        self.assertIn("this.ambientGain.connect(this.masterGain)", self.sfx_code)
        self.assertIn("this.uiGain.connect(this.masterGain)", self.sfx_code)

    def test_procedural_noise_buffers_generation(self):
        """Engine must procedurally pre-compute white and brown noise buffers without network requests."""
        self.assertIn("generateNoiseBuffers()", self.sfx_code)
        self.assertIn("this.whiteNoiseBuffer", self.sfx_code)
        self.assertIn("this.brownNoiseBuffer", self.sfx_code)

    def test_all_thirteen_visceral_sound_routines_implemented(self):
        """All 13 required savage primal ARPG sound generation routines must exist."""
        required_methods = [
            "playBladeSlash",
            "playBoneCrunch",
            "playFleshHit",
            "playDodgeWhoosh",
            "playFlameBurst",
            "playLightningCrack",
            "playFrostShatter",
            "playBossHeavySlam",
            "playBossStaggerGong",
            "playLootDropChime",
            "playLevelUp",
            "startAmbientWilderness",
            "stopAmbientWilderness",
            "playUIButton",
        ]
        for method in required_methods:
            self.assertIn(f"{method}(", self.sfx_code, f"Method {method}() must be implemented in SFXEngine")

    def test_visceral_synthesis_acoustics(self):
        """Verify visceral synthesis techniques: sub-bass sine, filter sweeps, gong partials, loot tiers."""
        # Blade slash: bandpass filter sweep
        self.assertIn("'bandpass'", self.sfx_code)
        # Bone crunch: sub-bass sine (70 -> 30Hz)
        self.assertIn("playBoneCrunch", self.sfx_code)
        self.assertIn("70, 30", self.sfx_code)
        # Flesh hit: dual formant filters
        self.assertIn("playFleshHit", self.sfx_code)
        # Boss heavy slam: binaural shockwave & screen shake
        self.assertIn("playBossHeavySlam", self.sfx_code)
        self.assertIn("triggerScreenShake", self.sfx_code)
        # Boss stagger gong: 4.0s poise break
        self.assertIn("playBossStaggerGong", self.sfx_code)
        # Loot drop chime: multiple tiers (common, magic, rare, unique, divine)
        self.assertIn("playLootDropChime", self.sfx_code)
        for tier in ["common", "magic", "rare", "unique", "divine"]:
            self.assertIn(tier, self.sfx_code)
        # War horn level up chord
        self.assertIn("playLevelUp", self.sfx_code)
        # Ambient wilderness drone
        self.assertIn("startAmbientWilderness", self.sfx_code)

    def test_audio_context_gesture_unlock(self):
        """Engine must handle browser AudioContext unlocking on user interaction gesture."""
        self.assertIn("unlockAudio()", self.sfx_code)
        self.assertIn("resume()", self.sfx_code)
        # main.js also attaches gesture unlock
        self.assertIn("unlockAudio", self.main_code)
        self.assertIn("startAmbientWilderness", self.main_code)

    def test_volume_and_settings_persistence(self):
        """Settings must persist in localStorage under 'freeexile_audio_settings'."""
        self.assertIn("freeexile_audio_settings", self.sfx_code)
        self.assertIn("loadSettings", self.sfx_code)
        self.assertIn("saveSettings", self.sfx_code)
        self.assertIn("setMasterVolume", self.sfx_code)
        self.assertIn("setSFXVolume", self.sfx_code)
        self.assertIn("setAmbientVolume", self.sfx_code)
        self.assertIn("setMusicVolume", self.sfx_code)
        self.assertIn("setUIVolume", self.sfx_code)
        self.assertIn("toggleMute", self.sfx_code)

    def test_native_haptics_synchronization(self):
        """Engine must support iOS WebKit messageHandlers, AndroidHaptics, and navigator.vibrate."""
        self.assertIn("triggerHaptic", self.sfx_code)
        self.assertIn("webkit.messageHandlers.haptic", self.sfx_code)
        self.assertIn("AndroidHaptics", self.sfx_code)
        self.assertIn("navigator.vibrate", self.sfx_code)

    def test_index_html_imports_sfx_engine(self):
        """index.html must import sfx_engine.js before combat_skills.js and main.js."""
        self.assertIn("js/audio/sfx_engine.js", self.html_code)
        sfx_idx = self.html_code.index("js/audio/sfx_engine.js")
        combat_idx = self.html_code.index("js/engine/combat_skills.js")
        main_idx = self.html_code.index("js/main.js")
        self.assertLess(sfx_idx, combat_idx, "sfx_engine.js must be imported before combat_skills.js")
        self.assertLess(sfx_idx, main_idx, "sfx_engine.js must be imported before main.js")

    def test_game_settings_modal_audio_controls(self):
        """Game Settings modal must include sliders for Master, SFX, Ambient, Mute button, and Haptics."""
        self.assertIn('id="audio-master-slider"', self.html_code)
        self.assertIn('id="audio-sfx-slider"', self.html_code)
        self.assertIn('id="audio-ambient-slider"', self.html_code)
        self.assertIn('id="audio-mute-btn"', self.html_code)
        self.assertIn('id="audio-haptics-toggle"', self.html_code)
        self.assertIn('id="btn-preview-slash"', self.html_code)
        self.assertIn('id="btn-preview-slam"', self.html_code)
        self.assertIn('id="btn-preview-loot"', self.html_code)

    def test_main_js_wires_audio_settings_and_ui_clicks(self):
        """main.js must bind slider input events, mute toggle, preview buttons, and UI click SFX."""
        self.assertIn("initAudioSettingsUI", self.main_code)
        self.assertIn("audio-master-slider", self.main_code)
        self.assertIn("audio-sfx-slider", self.main_code)
        self.assertIn("audio-ambient-slider", self.main_code)
        self.assertIn("audio-mute-btn", self.main_code)
        self.assertIn("playUIButton", self.main_code)

    def test_combat_skills_integration(self):
        """combat_skills.js must invoke SFXEngine methods for skills, impacts, dodge, and boss events."""
        # Dodge
        self.assertIn("sfxEngine.playDodgeWhoosh()", self.combat_code)
        # Skills
        self.assertIn("sfxEngine.playFlameBurst()", self.combat_code)
        self.assertIn("sfxEngine.playLightningCrack()", self.combat_code)
        self.assertIn("sfxEngine.playFrostShatter()", self.combat_code)
        # Hits
        self.assertIn("sfxEngine.playBladeSlash()", self.combat_code)
        self.assertIn("sfxEngine.playBoneCrunch()", self.combat_code)
        self.assertIn("sfxEngine.playFleshHit()", self.combat_code)
        # Boss events & Loot
        self.assertIn("sfxEngine.playBossHeavySlam()", self.combat_code)
        self.assertIn("sfxEngine.playBossStaggerGong()", self.combat_code)
        self.assertIn("sfxEngine.playLootDropChime", self.combat_code)
        # Boss functions exported to window
        self.assertIn("window.bossHeavySlam = bossHeavySlam", self.combat_code)
        self.assertIn("window.bossStagger = bossStagger", self.combat_code)

    def test_headless_node_sfx_engine_execution(self):
        """Execute SFXEngine in Node.js with Web Audio API mock to verify runtime function execution."""
        mock_script = """
        class AudioParamMock {
          constructor() { this.value = 1.0; }
          setValueAtTime(v, t) { this.value = v; }
          linearRampToValueAtTime(v, t) { this.value = v; }
          exponentialRampToValueAtTime(v, t) { this.value = v; }
        }
        class AudioNodeMock {
          constructor() {
            this.gain = new AudioParamMock();
            this.frequency = new AudioParamMock();
            this.Q = new AudioParamMock();
            this.pan = new AudioParamMock();
          }
          connect() {}
          start() {}
          stop() {}
        }
        class AudioBufferMock {
          constructor(channels, length, sampleRate) {
            this.data = new Float32Array(length);
          }
          getChannelData() { return this.data; }
        }
        global.window = {
          AudioContext: class {
            constructor() {
              this.state = 'running';
              this.currentTime = 0.05;
              this.sampleRate = 44100;
              this.destination = new AudioNodeMock();
            }
            createGain() { return new AudioNodeMock(); }
            createOscillator() { return new AudioNodeMock(); }
            createBiquadFilter() { return new AudioNodeMock(); }
            createBufferSource() { return new AudioNodeMock(); }
            createStereoPanner() { return new AudioNodeMock(); }
            createBuffer(ch, len, sr) { return new AudioBufferMock(ch, len, sr); }
            resume() { return Promise.resolve(); }
          }
        };
        global.document = {
          addEventListener() {},
          removeEventListener() {},
        };
        global.localStorage = {
          _store: {},
          getItem(k) { return this._store[k] || null; },
          setItem(k, v) { this._store[k] = String(v); }
        };
        try {
          Object.defineProperty(globalThis, 'navigator', {
            value: { vibrate() {} },
            configurable: true,
            writable: true
          });
        } catch (e) {
          if (globalThis.navigator) globalThis.navigator.vibrate = () => {};
        }

        import('./client/webapp/js/audio/sfx_engine.js').then(({ sfxEngine }) => {
          sfxEngine.init();
          sfxEngine.setMasterVolume(0.8);
          sfxEngine.setSFXVolume(0.7);
          sfxEngine.setAmbientVolume(0.5);

          // Test all visceral sound routines execute without error
          sfxEngine.playBladeSlash();
          sfxEngine.playBoneCrunch();
          sfxEngine.playFleshHit();
          sfxEngine.playDodgeWhoosh();
          sfxEngine.playFlameBurst();
          sfxEngine.playLightningCrack();
          sfxEngine.playFrostShatter();
          sfxEngine.playBossHeavySlam();
          sfxEngine.playBossStaggerGong();
          sfxEngine.playLootDropChime('rare');
          sfxEngine.playLootDropChime('divine');
          sfxEngine.playLevelUp();
          sfxEngine.startAmbientWilderness();
          sfxEngine.stopAmbientWilderness();
          sfxEngine.playUIButton();

          console.log('SFX_ENGINE_EXECUTION_SUCCESS');
        }).catch(err => {
          console.error(err);
          process.exit(1);
        });
        """
        res = subprocess.run(
            ["node", "--input-type=module", "-e", mock_script],
            cwd=str(PROJECT_ROOT),
            capture_output=True,
            text=True,
            check=False,
        )
        self.assertEqual(res.returncode, 0, f"Headless execution failed: {res.stderr}")
        self.assertIn("SFX_ENGINE_EXECUTION_SUCCESS", res.stdout)


if __name__ == "__main__":
    unittest.main()
