"""Challenger Zero-Allocation & Hot-Path Performance Verification Suite.

Adversarially validates the zero-allocation claims and hot-path execution invariants
for `SpriteAtlasRenderer.ts` and `SkillVfxPlayer.ts` under 120 FPS ProMotion budgets.
"""

from __future__ import annotations

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

import pytest

REPO_ROOT = Path(__file__).resolve().parent.parent
TS_RENDERER_PATH = REPO_ROOT / "client" / "cocos" / "assets" / "scripts" / "animation" / "SpriteAtlasRenderer.ts"
TS_VFX_PLAYER_PATH = REPO_ROOT / "client" / "cocos" / "assets" / "scripts" / "combat" / "SkillVfxPlayer.ts"
NODE_HARNESS_PATH = REPO_ROOT / "tests" / "challenger_node_zero_alloc_harness.js"


def _run_node_ast_inspection(file_path: Path) -> Dict[str, Any]:
    """Inspects TypeScript AST methods using Node.js and TypeScript compiler API."""
    script = f"""
    const fs = require('fs');
    const path = require('path');
    const ts = require(path.resolve('./client/node_modules/typescript'));

    const code = fs.readFileSync({json.dumps(str(file_path))}, 'utf8');
    const sf = ts.createSourceFile({json.dumps(file_path.name)}, code, ts.ScriptTarget.ES2022, true);
    const results = {{}};

    function visit(node) {{
        if (ts.isMethodDeclaration(node) && node.name) {{
            const methodName = node.name.getText(sf);
            results[methodName] = {{
                newExpressions: [],
                arrayLiterals: 0,
                objectLiterals: 0,
                callExpressions: []
            }};

            node.body?.forEachChild(function checkChild(child) {{
                if (ts.isNewExpression(child)) {{
                    results[methodName].newExpressions.push(child.expression.getText(sf));
                }}
                if (ts.isArrayLiteralExpression(child)) {{
                    results[methodName].arrayLiterals++;
                }}
                if (ts.isObjectLiteralExpression(child)) {{
                    results[methodName].objectLiterals++;
                }}
                if (ts.isCallExpression(child)) {{
                    const callText = child.expression.getText(sf);
                    if (callText.includes('slice') || callText.includes('splice') || callText.includes('filter') || callText.includes('map')) {{
                        results[methodName].callExpressions.push(callText);
                    }}
                }}
                child.forEachChild(checkChild);
            }});
        }}
        node.forEachChild(visit);
    }}

    visit(sf);
    console.log(JSON.stringify(results));
    """
    proc = subprocess.run(
        ["node", "-e", script],
        cwd=str(REPO_ROOT),
        capture_output=True,
        text=True,
        check=True
    )
    return json.loads(proc.stdout)


def _run_node_runtime_harness() -> Dict[str, Any]:
    """Executes the instrumented Node.js runtime harness and captures metrics."""
    proc = subprocess.run(
        ["node", "--expose-gc", str(NODE_HARNESS_PATH)],
        cwd=str(REPO_ROOT),
        capture_output=True,
        text=True,
        check=True
    )
    return json.loads(proc.stdout)


@pytest.fixture(scope="module")
def renderer_ast() -> Dict[str, Any]:
    return _run_node_ast_inspection(TS_RENDERER_PATH)


@pytest.fixture(scope="module")
def vfx_player_ast() -> Dict[str, Any]:
    return _run_node_ast_inspection(TS_VFX_PLAYER_PATH)


@pytest.fixture(scope="module")
def runtime_metrics() -> Dict[str, Any]:
    return _run_node_runtime_harness()


class TestStaticAstZeroAllocationAudit:
    """Verifies AST syntax trees to guarantee zero heap instantiation in hot paths."""

    def test_ast_sprite_atlas_renderer_hot_path_no_new_expressions(self, renderer_ast: Dict[str, Any]) -> None:
        """SpriteAtlasRenderer hot-path methods must not allocate any new objects."""
        # getFrameSourceRect must never allocate new Rect
        assert "getFrameSourceRect" in renderer_ast
        assert len(renderer_ast["getFrameSourceRect"]["newExpressions"]) == 0, (
            f"Forbidden new expressions in getFrameSourceRect: {renderer_ast['getFrameSourceRect']['newExpressions']}"
        )

        # getFrameUVs must never allocate new objects
        assert "getFrameUVs" in renderer_ast
        assert len(renderer_ast["getFrameUVs"]["newExpressions"]) == 0, (
            f"Forbidden new expressions in getFrameUVs: {renderer_ast['getFrameUVs']['newExpressions']}"
        )

        # getPivotOffset must never allocate new objects
        assert "getPivotOffset" in renderer_ast
        assert len(renderer_ast["getPivotOffset"]["newExpressions"]) == 0
        assert renderer_ast["getPivotOffset"]["objectLiterals"] == 0

    def test_ast_skill_vfx_player_update_loop_zero_allocations(self, vfx_player_ast: Dict[str, Any]) -> None:
        """SkillVfxPlayer.update(dt) loop must have zero allocations."""
        assert "update" in vfx_player_ast
        update_info = vfx_player_ast["update"]
        assert len(update_info["newExpressions"]) == 0, (
            f"Forbidden new in SkillVfxPlayer.update: {update_info['newExpressions']}"
        )
        assert update_info["arrayLiterals"] == 0, "Forbidden array literals [] in update loop"
        assert update_info["objectLiterals"] == 0, "Forbidden object literals {} in update loop"
        assert len(update_info["callExpressions"]) == 0, (
            f"Forbidden array method calls in update: {update_info['callExpressions']}"
        )

    def test_ast_skill_vfx_player_play_skill_vfx_zero_allocations(self, vfx_player_ast: Dict[str, Any]) -> None:
        """SkillVfxPlayer.playSkillVfx must not allocate Node or Vectors in hot path."""
        assert "playSkillVfx" in vfx_player_ast
        play_info = vfx_player_ast["playSkillVfx"]
        assert len(play_info["newExpressions"]) == 0, (
            f"Forbidden new in playSkillVfx: {play_info['newExpressions']}"
        )
        assert play_info["arrayLiterals"] == 0, "Forbidden array literals [] in playSkillVfx"
        assert play_info["objectLiterals"] == 0, "Forbidden object literals {} in playSkillVfx"

    def test_ast_pool_prewarming_isolation(self, vfx_player_ast: Dict[str, Any]) -> None:
        """Node and Size constructors must only appear in ensurePoolInitialized."""
        assert "ensurePoolInitialized" in vfx_player_ast
        prewarm_info = vfx_player_ast["ensurePoolInitialized"]
        assert "Node" in prewarm_info["newExpressions"]
        assert "Size" in prewarm_info["newExpressions"]


class TestDynamicRuntimeZeroAllocationAudit:
    """Verifies empirical runtime metrics across 12,000 frames at 120 FPS."""

    def test_runtime_120fps_update_loop_zero_heap_allocations(self, runtime_metrics: Dict[str, Any]) -> None:
        """Update loop over 12,000 frames must produce exactly 0 constructor allocations."""
        update = runtime_metrics["runtimeUpdate"]
        assert update["ticksExecuted"] >= 10000
        assert update["nodeConstructors"] == 0
        assert update["rectConstructors"] == 0
        assert update["vec2Constructors"] == 0
        assert update["vec3Constructors"] == 0
        assert update["sizeConstructors"] == 0
        assert update["colorConstructors"] == 0
        assert update["arraySlices"] == 0

    def test_runtime_continuous_skill_casting_zero_heap_allocations(self, runtime_metrics: Dict[str, Any]) -> None:
        """1,400 skill activations must produce exactly 0 constructor allocations."""
        cast = runtime_metrics["runtimeSkillCast"]
        assert cast["castsExecuted"] >= 1000
        assert cast["nodeConstructors"] == 0
        assert cast["rectConstructors"] == 0
        assert cast["vec2Constructors"] == 0
        assert cast["vec3Constructors"] == 0
        assert cast["sizeConstructors"] == 0
        assert cast["colorConstructors"] == 0
        assert cast["arraySlices"] == 0

    def test_runtime_steady_state_uv_cache_hit_rate_100_percent(self, runtime_metrics: Dict[str, Any]) -> None:
        """Steady-state combat UV Map lookups must achieve strictly 100.0% cache hit rate."""
        steady = runtime_metrics["steadyStateCacheMetrics"]
        assert steady["uvLookups"] > 2000
        assert steady["uvMisses"] == 0, f"Detected {steady['uvMisses']} UV cache misses during steady state!"
        assert steady["hitRatePercent"] == 100.0

    def test_runtime_total_uv_cache_hit_rate_above_99_percent(self, runtime_metrics: Dict[str, Any]) -> None:
        """Overall UV Map lookup efficiency across all operations must exceed 99.5%."""
        total = runtime_metrics["totalCacheMetrics"]
        assert total["uvLookups"] > 2000
        assert total["overallHitRatePercent"] >= 99.5

    def test_runtime_pool_saturation_and_lru_recycling(self, runtime_metrics: Dict[str, Any]) -> None:
        """Pool saturation must recycle oldest slots without allocating new nodes."""
        sat = runtime_metrics["saturation"]
        assert sat["totalCasts"] == 50
        assert sat["poolSize"] == 16
        assert sat["maxActiveObserved"] <= 16
        assert sat["newNodesAllocated"] == 0

    def test_runtime_heap_stability_under_sustained_stress(self, runtime_metrics: Dict[str, Any]) -> None:
        """Steady-state heap growth over 12,000 ticks must remain stable (< 500 KB)."""
        heap = runtime_metrics["heapStability"]
        delta = heap["steadyStateDeltaBytes"]
        assert delta < 500 * 1024, f"Heap expanded by {delta} bytes under steady state!"


class TestAdversarialEdgeCases:
    """Stress-tests corner cases: multi-direction rotation, invalid IDs, and reentrancy."""

    def test_adversarial_multi_direction_rotation_lazy_memoization(self, runtime_metrics: Dict[str, Any]) -> None:
        """Casting skills in all 8 directions must memoize UVs and hit 100% on subsequent cycles."""
        multi_dir = runtime_metrics["adversarial"]["multiDirection"]
        assert multi_dir["testedDirections"] == 8
        assert multi_dir["initialColdMisses"] == 7  # 7 non-'S' directions cold on first access
        assert multi_dir["warmLookups"] == multi_dir["expectedLookups"] == 400
        assert multi_dir["warmHitRatePercent"] == 100.0
        assert multi_dir["success"] is True

    def test_adversarial_unknown_skill_id_fallback(self, runtime_metrics: Dict[str, Any]) -> None:
        """Unknown skill ID must fall back to sequential grid without throwing."""
        unknown = runtime_metrics["adversarial"]["unknownSkill"]
        assert unknown["uvValid"] is True
        assert unknown["uvLength"] == 4
        assert unknown["success"] is True

    def test_adversarial_idempotent_pool_initialization(self, runtime_metrics: Dict[str, Any]) -> None:
        """Redundant calls to ensurePoolInitialized must not spawn additional nodes."""
        idemp = runtime_metrics["adversarial"]["idempotentPrewarm"]
        assert idemp["subsequentNodesCreated"] == 0
        assert idemp["success"] is True
