"""
Unit Test & Benchmark Suite for FreeExile War-Fog Engine & Persistence (Milestone 4).
Verifies:
1. Line count constraint: war_fog.js <= 300 lines (Soft cap 350, Hard cap 500).
2. 3-State Fog Model (0=UNEXPLORED, 1=EXPLORED_FOGGED, 2=VISIBLE).
3. Dynamic Vision Reveal with radius 8 tiles and zero-heap decay/update.
4. Compact bit-packing persistence (< 2 KB for 120x90 grid).
5. Deterministic round-trip packing/unpacking fidelity (100% bit preservation).
6. LocalStorage key format: fe_fog_${zoneId}_${seed}.
7. Backward compatibility for modal simulator and HUD bindings.
"""

import subprocess
import json
import pytest
from pathlib import Path

MODULE_PATH = Path("c:/Projects/FreeExile/.agents/teamwork/explorer_m4_1/proposed_war_fog.js")


class TestWarFogEngineConstraints:
    def test_line_count_within_limits(self):
        """war_fog.js must remain strictly <= 300 lines."""
        assert MODULE_PATH.exists(), f"Proposed war_fog.js not found at {MODULE_PATH}"
        content = MODULE_PATH.read_text(encoding="utf-8")
        lines = len(content.splitlines())
        assert lines <= 300, f"proposed_war_fog.js exceeded 300 lines: {lines} lines"
        assert lines <= 350, "Soft cap 350 breached"
        assert lines <= 500, "Hard cap 500 breached"

    def test_es_module_exports_present(self):
        """Verifies canonical exports in war_fog.js."""
        content = MODULE_PATH.read_text(encoding="utf-8")
        required_symbols = [
            "FOG_STATE",
            "initFog",
            "getFogState",
            "setFogState",
            "updatePlayerVision",
            "packFogBits",
            "unpackFogBits",
            "saveFog",
            "loadFog",
            "getStorageKey",
            "openWarFogModal",
            "initMap",
            "updateFog",
        ]
        for sym in required_symbols:
            assert sym in content, f"Missing required export/function: {sym}"


class TestWarFogNodeRuntime:
    @classmethod
    def setup_class(cls):
        js_code = """
        const fs = require('fs');
        let code = fs.readFileSync('c:/Projects/FreeExile/.agents/teamwork/explorer_m4_1/proposed_war_fog.js', 'utf8');
        code = code.replace(/import\\s+[^;]+;\\s*/g, '');
        code = code.replace(/export\\s+const\\s+/g, 'var ');
        code = code.replace(/export\\s+let\\s+/g, 'var ');
        code = code.replace(/export\\s+function\\s+/g, 'function ');
        code = code.replace(/export\\s+\\{[^}]+\\};?\\s*/g, '');
        code = code.replace(/export\\s+default\\s+[^;]+;?\\s*/g, '');

        const mockStorage = {};
        global.localStorage = {
          getItem: (k) => mockStorage[k] || null,
          setItem: (k, v) => { mockStorage[k] = v; },
          removeItem: (k) => { delete mockStorage[k]; }
        };
        global.document = {
          getElementById: () => null,
          addEventListener: () => {}
        };
        global.window = {};

        const GRID_SIZE = 32, TILE_PX = 15, BIOMES = [{ id: 'TEST', name: 'Test' }];
        function generateMazeData() { return { biomeIdx: 0, mapTiles: [], fogMatrix: [], barricades: [], bossGate: null, bossRoom: null, playerPos: {x:0, y:0}, bossPos: {x:10, y:10}, simulatedPath: [] }; }
        function renderProceduralMap() {}

        eval(code);

        const results = {};

        // Test 1: 3-State Model & Initialization
        initFog(60, 45, 'zone_tang_kiem_nhai', 101);
        results.initState = {
          w: mapW, h: mapH,
          at00: getFogState(0, 0),
          atNeg: getFogState(-1, 0),
          atOob: getFogState(60, 45)
        };

        // Test 2: Dynamic Vision Reveal & Decay
        updatePlayerVision(10, 10, 8);
        const revealed10_10 = getFogState(10, 10);
        const revealedEdge = getFogState(10 + 8, 10);
        const outside = getFogState(10 + 9, 10);
        
        // Move to 25, 25: (10, 10) must decay from 2 (VISIBLE) to 1 (EXPLORED_FOGGED)
        updatePlayerVision(25, 25, 8);
        const decayed10_10 = getFogState(10, 10);
        const revealed25_25 = getFogState(25, 25);

        results.vision = {
          centerBeforeMove: revealed10_10,
          edgeBeforeMove: revealedEdge,
          outsideBeforeMove: outside,
          centerAfterMove: decayed10_10,
          newCenter: revealed25_25
        };

        // Test 3: Bit-packing and persistence on 120x90
        initFog(120, 90, 'zone_boundless_celestial_palace', 777);
        let seededExpCount = 0;
        for (let i = 0; i < 120 * 90; i++) {
          if ((i * 37 + 13) % 7 === 0) {
            fogGrid[i] = (i % 2 === 0) ? 1 : 2;
            seededExpCount++;
          }
        }
        const storageKey = getStorageKey('zone_boundless_celestial_palace', 777);
        const packed = packFogBits(fogGrid, 120, 90);
        saveFog('zone_boundless_celestial_palace', 777);
        const savedRaw = localStorage.getItem(storageKey);

        const freshGrid = new Uint8Array(120 * 90);
        const unpackOk = unpackFogBits(savedRaw, freshGrid, 120, 90);
        let bitMatches = 0;
        for (let i = 0; i < 120 * 90; i++) {
          const originalExp = fogGrid[i] > 0;
          const restoredExp = freshGrid[i] > 0;
          if (originalExp === restoredExp) bitMatches++;
        }

        results.persistence = {
          storageKey,
          packedLength: packed.length,
          packedBytesKb: (packed.length / 1024),
          unpackOk,
          totalTiles: 120 * 90,
          seededExpCount,
          bitMatches
        };

        console.log(JSON.stringify(results));
        """
        proc = subprocess.run(
            ["node", "-e", js_code],
            capture_output=True,
            text=True,
            cwd=str(MODULE_PATH.parent),
            check=True
        )
        cls.node_results = json.loads(proc.stdout)

    def test_init_and_bounds(self):
        st = self.node_results["initState"]
        assert st["w"] == 60 and st["h"] == 45
        assert st["at00"] == 0  # UNEXPLORED initially
        assert st["atNeg"] == 0  # Out-of-bounds safe fallback
        assert st["atOob"] == 0  # Out-of-bounds safe fallback

    def test_3_state_vision_reveal_and_decay(self):
        vis = self.node_results["vision"]
        assert vis["centerBeforeMove"] == 2
        assert vis["edgeBeforeMove"] == 2
        assert vis["outsideBeforeMove"] == 0
        assert vis["centerAfterMove"] == 1
        assert vis["newCenter"] == 2

    def test_compact_bitpacking_under_2kb(self):
        pers = self.node_results["persistence"]
        assert pers["storageKey"] == "fe_fog_zone_boundless_celestial_palace_777"
        assert pers["unpackOk"] is True
        assert pers["packedBytesKb"] < 2.0, f"Packed string size {pers['packedBytesKb']} KB exceeds 2.0 KB"
        assert pers["bitMatches"] == pers["totalTiles"]
