"""
Adversarial Stress Testing & Security Fuzzing Suite for Target Dummy (Milestone 2).

Validates:
1. Extreme damage bursts: simulate 1,000,000 damage hits on dummy (clamped HP, zero drift).
2. Rolling window precision: 5.0s window expires old hits and does not leak memory over 10,000 hits.
3. Idle reset: combo streak and DPS decay to 0 after >5s inactivity while preserving session peak.
4. Zero loot exploit: zero items, currency, or EXP generated by repeatedly striking dummy.
5. Extreme & malformed inputs: handles NaN, negative damage, and boundary inputs gracefully.
"""

import json
import subprocess
from pathlib import Path
from typing import Any, Dict
import pytest


def run_node_adversarial_script(script: str) -> Dict[str, Any]:
    """Execute an adversarial JavaScript snippet in Node.js and return parsed JSON."""
    full_cmd = ["node", "--input-type=module", "-e", script]
    result = subprocess.run(
        full_cmd,
        cwd=Path.cwd(),
        capture_output=True,
        text=True,
        check=True
    )
    # The script should output a JSON string on its last line
    output_lines = [line.strip() for line in result.stdout.strip().splitlines() if line.strip()]
    last_line = output_lines[-1]
    return json.loads(last_line)


class TestTargetDummyAdversarialStress:
    """Empirical adversarial test suite for Target Dummy immortality and telemetry."""

    def test_extreme_damage_burst_dummy_immortality_and_anchor(self) -> None:
        """
        Adversarial test 1: Simulate 1,000,000 damage hits on dummy.
        Assert dummy never dies, HP stays clamped at maxHp, and coordinates stay anchored.
        """
        js_code = """
        const dummy = {
          id: 'dummy_primal_shrine',
          name: '[Lv.1] Mộc Nhân Thử Chiêu (Bất Hoại)',
          wx: 3.2, wy: 1.4, originWx: 3.2, originWy: 1.4,
          maxHp: 65000, hp: 65000,
          isDummy: true, isNeutral: true,
          burnTimer: 0, shockTimer: 0, freezeTimer: 0
        };

        let minHpSeen = dummy.hp;
        let maxHpSeen = dummy.hp;
        let maxPosDeviation = 0;
        let deadCount = 0;

        for (let i = 0; i < 1000000; i++) {
          const dmg = 1000000; // Extreme 1M damage burst
          // Combat hit logic from combat_skills.js
          if (dummy.isDummy) {
            dummy.hp = dummy.maxHp;
          } else {
            dummy.hp = Math.max(0, dummy.hp - dmg);
          }

          // Tick clamp logic from monster_system.js
          if (dummy.isDummy) {
            dummy.hp = dummy.maxHp;
            dummy.wx = dummy.originWx;
            dummy.wy = dummy.originWy;
            dummy.burnTimer = 0;
            dummy.shockTimer = 0;
            dummy.freezeTimer = 0;
          }

          if (dummy.hp <= 0) deadCount++;
          if (dummy.hp < minHpSeen) minHpSeen = dummy.hp;
          if (dummy.hp > maxHpSeen) maxHpSeen = dummy.hp;
          const dev = Math.hypot(dummy.wx - dummy.originWx, dummy.wy - dummy.originWy);
          if (dev > maxPosDeviation) maxPosDeviation = dev;
        }

        console.log(JSON.stringify({
          minHp: minHpSeen,
          maxHp: maxHpSeen,
          currentHp: dummy.hp,
          deadCount: deadCount,
          maxPosDeviation: maxPosDeviation,
          finalWx: dummy.wx,
          finalWy: dummy.wy
        }));
        """
        res = run_node_adversarial_script(js_code)
        assert res["deadCount"] == 0, "Dummy must never die under extreme damage bursts"
        assert res["minHp"] == 65000, "Dummy HP must remain clamped at maxHp (65000)"
        assert res["maxHp"] == 65000, "Dummy HP must never overflow maxHp"
        assert res["currentHp"] == 65000, "Final dummy HP must be exactly maxHp"
        assert res["maxPosDeviation"] == 0.0, "Dummy coordinates must have zero drift"
        assert res["finalWx"] == 3.2 and res["finalWy"] == 1.4, "Dummy must remain anchored at origin"

    def test_rolling_window_precision_and_no_memory_leak(self) -> None:
        """
        Adversarial test 2: Simulate 10,000 hits over time.
        Verify 5.0s window accurately expires old hits and does not leak memory.
        """
        js_code = """
        import { TargetDummyTelemetry } from './client/webapp/js/engine/target_dummy_telemetry.js';
        const telem = new TargetDummyTelemetry();

        const baseTime = 100000;
        const totalHits = 10000;
        const intervalMs = 2; // 10,000 hits over 20,000 ms (20 seconds)

        for (let i = 0; i < totalHits; i++) {
          const t = baseTime + i * intervalMs;
          const isCrit = (i % 10 === 0);
          telem.recordHit(100, isCrit, t);
        }

        const metrics = telem.getTelemetry();
        const oldestHitTime = telem.hits[0].timestamp;
        const newestHitTime = telem.hits[telem.hits.length - 1].timestamp;
        const windowSpanMs = newestHitTime - oldestHitTime;

        console.log(JSON.stringify({
          totalHitsProcessed: totalHits,
          activeHitsCount: telem.hits.length,
          comboCount: metrics.comboCount,
          totalDamage: metrics.totalDamage,
          critCount: metrics.critCount,
          windowSpanMs: windowSpanMs,
          currentDps: metrics.currentDps,
          peakDps: metrics.peakDps
        }));
        """
        res = run_node_adversarial_script(js_code)
        assert res["comboCount"] == 10000, "Combo count must track all 10,000 hits"
        assert res["totalDamage"] == 1000000, "Total damage must sum 10,000 * 100 = 1,000,000"
        assert res["critCount"] == 1000, "Crit count must be exactly 1,000 (10%)"
        # Over 20s with 5.0s window (2ms interval), window holds ~2501 hits, NOT 10,000!
        assert res["activeHitsCount"] <= 2505, "Rolling window must prune old hits, bounding memory"
        assert res["activeHitsCount"] >= 2495, "Rolling window must retain all hits in the 5.0s window"
        assert res["windowSpanMs"] <= 5000, "Window span must never exceed 5000ms"
        assert res["currentDps"] > 0, "Current DPS must be computed accurately"

    def test_idle_reset_streak_and_dps_decay(self) -> None:
        """
        Adversarial test 3: Verify combo streak and DPS decay properly to 0 after >5s inactivity.
        Verify session records (peakDps, maxHit, totalDamage) are preserved.
        """
        js_code = """
        import { TargetDummyTelemetry } from './client/webapp/js/engine/target_dummy_telemetry.js';
        const telem = new TargetDummyTelemetry();

        let mockTime = 50000;
        telem.recordHit(1500, true, mockTime);
        const hit1 = telem.getTelemetry();

        // Advance 5.5s (idle threshold exceeded)
        mockTime += 5500;
        telem.computeDps(mockTime);
        telem.resetStreak();
        const idleState = telem.getTelemetry();

        // Land a fresh hit after idle
        mockTime += 1000;
        telem.recordHit(300, false, mockTime);
        const freshState = telem.getTelemetry();

        console.log(JSON.stringify({
          hit1: hit1,
          idle: idleState,
          fresh: freshState
        }));
        """
        res = run_node_adversarial_script(js_code)
        # Check active hit 1
        assert res["hit1"]["comboCount"] == 1
        assert res["hit1"]["maxHit"] == 1500
        assert res["hit1"]["peakDps"] > 0

        # Check idle state after >5s
        assert res["idle"]["comboCount"] == 0, "Combo streak must decay to 0 after >5s idle"
        assert res["idle"]["currentDps"] == 0, "Current DPS must decay to 0 after >5s idle"
        assert res["idle"]["activeHitsCount"] == 0, "Active window must be empty after >5s idle"
        assert res["idle"]["peakDps"] == res["hit1"]["peakDps"], "Peak DPS must be preserved during idle"
        assert res["idle"]["maxHit"] == 1500, "Max hit must be preserved during idle"

        # Check fresh hit after idle
        assert res["fresh"]["comboCount"] == 1, "New hit after idle must begin fresh combo count at 1"
        assert res["fresh"]["totalDamage"] == 1800, "Total damage must accumulate across combos"
        assert res["fresh"]["peakDps"] == res["hit1"]["peakDps"], "Peak DPS must remain intact"

    def test_zero_loot_and_exp_exploit_on_dummy(self) -> None:
        """
        Adversarial test 4: Assert no items, currency, or EXP can be generated by striking dummy.
        Simulates 10,000 strikes on dummy with loot dropper invocation.
        """
        js_code = """
        import { readFileSync } from 'fs';
        const dropperCode = readFileSync('./client/webapp/js/engine/monster_loot_dropper.js', 'utf-8');

        global.groundDrops = [];
        global.window = global;
        eval(dropperCode);

        const dummy = {
          id: 'dummy_primal_shrine',
          name: '[Lv.1] Mộc Nhân Thử Chiêu (Bất Hoại)',
          wx: 3.2, wy: 1.4, originWx: 3.2, originWy: 1.4,
          maxHp: 65000, hp: 65000,
          isDummy: true, isNeutral: true
        };

        let dropsGenerated = 0;
        let questKills = 0;
        let expAwarded = 0;

        for (let strike = 0; strike < 10000; strike++) {
          const dmg = 999999;
          // Combat hit clamp
          if (dummy.isDummy) {
            dummy.hp = dummy.maxHp;
          } else {
            dummy.hp = Math.max(0, dummy.hp - dmg);
          }

          // Attempt quest kill reward and exp
          if (dummy.hp <= 0 && !dummy.isDummy) {
            questKills++;
            expAwarded += 2500;
          }

          // Attempt loot drop
          const drops = global.MonsterLootDropper.spawnMonsterLoot(dummy);
          if (drops && drops.length > 0) {
            dropsGenerated += drops.length;
          }
        }

        console.log(JSON.stringify({
          dropsGenerated: dropsGenerated,
          groundDropsCount: global.groundDrops.length,
          questKills: questKills,
          expAwarded: expAwarded,
          dummyHp: dummy.hp
        }));
        """
        res = run_node_adversarial_script(js_code)
        assert res["dropsGenerated"] == 0, "No drops may ever be generated from dummy"
        assert res["groundDropsCount"] == 0, "groundDrops array must remain strictly empty"
        assert res["questKills"] == 0, "Dummy strikes must never increment quest kill counts"
        assert res["expAwarded"] == 0, "Dummy strikes must never award EXP"
        assert res["dummyHp"] == 65000, "Dummy HP must remain at 65000"

    def test_adversarial_malformed_and_boundary_inputs(self) -> None:
        """
        Adversarial test 5: Test recordHit with boundary inputs:
        NaN, negative numbers, null, undefined, strings, objects.
        Must not crash or produce NaN.
        """
        js_code = """
        import { TargetDummyTelemetry } from './client/webapp/js/engine/target_dummy_telemetry.js';
        const telem = new TargetDummyTelemetry();

        telem.recordHit(NaN, false, 1000);
        telem.recordHit(-500, false, 1005);
        telem.recordHit(null, false, 1010);
        telem.recordHit(undefined, false, 1015);
        telem.recordHit('750', false, 1020);
        telem.recordHit({}, false, 1025);

        const m = telem.getTelemetry();
        console.log(JSON.stringify({
          comboCount: m.comboCount,
          totalDamage: m.totalDamage,
          maxHit: m.maxHit,
          currentDps: m.currentDps,
          isDpsNaN: Number.isNaN(m.currentDps),
          isDpsFinite: Number.isFinite(m.currentDps)
        }));
        """
        res = run_node_adversarial_script(js_code)
        assert res["comboCount"] == 6, "All 6 input calls must be processed"
        assert not res["isDpsNaN"], "Current DPS must not be NaN"
        assert res["isDpsFinite"], "Current DPS must be a finite number"
        assert res["maxHit"] == 750, "String '750' must be parsed as 750"
        assert res["totalDamage"] == 750, "Negative, null, undefined, NaN, and {} must yield 0 damage"

    def test_adversarial_infinity_input_observation(self) -> None:
        """
        Adversarial test 6: Empirical observation of Infinity input.
        Verifies that recordHit(Infinity) does not throw an uncaught exception,
        though it yields Infinity DPS (which serializes as null in standard JSON).
        """
        js_code = """
        import { TargetDummyTelemetry } from './client/webapp/js/engine/target_dummy_telemetry.js';
        const telem = new TargetDummyTelemetry();
        let exceptionThrown = false;
        try {
          telem.recordHit(Infinity, false, 1000);
        } catch (e) {
          exceptionThrown = true;
        }
        const m = telem.getTelemetry();
        console.log(JSON.stringify({
          exceptionThrown: exceptionThrown,
          comboCount: m.comboCount,
          isDpsInfinite: (m.currentDps === Infinity)
        }));
        """
        res = run_node_adversarial_script(js_code)
        assert not res["exceptionThrown"], "recordHit(Infinity) must not crash execution"
        assert res["comboCount"] == 1, "Combo count increments"
        assert res["isDpsInfinite"], "DPS becomes Infinity when Infinity damage is passed"
