"""
Empirical Challenger Test Suite for Milestone 2 (R2: Waypoint Safe Radius & Healing).

Empirically challenges:
1. Exact boundary at distance 7.9 (inside) vs 8.1 (outside) across 8 radial angles (Server & Client).
2. Exact boundary at distance 8.0 (boundary included).
3. Target Dummy spawn and presence inside safe radius at (0, 0) (Server & Client).
4. Player health regeneration inside safe radius:
   - Low HP (50/100) -> 2% maxHp/sec regen.
   - Near Max HP (99/100) -> Clamped to maxHp.
   - At Max HP (100/100) -> No change / no overflow.
   - Dead player (0/100) -> No resurrection.
   - Outside safe radius (8.1, 0) -> No healing.
5. Monster leash state transition when player crosses safe radius boundary:
   - Aggro dropped (hasAggro -> False).
   - Leash engaged (isLeashing -> True, isInvulnerable -> True).
   - Attack execution blocked (0 damage inside safe radius).
   - Latch persistence: stays leashing even if player steps back outside until origin reached.
   - Origin arrival: reset isLeashing -> False, isInvulnerable -> False.
"""

from __future__ import annotations
import math
import subprocess
import sys
import unittest
from pathlib import Path

PROJECT_ROOT = Path(__file__).resolve().parent.parent.parent
SERVER_DIR = PROJECT_ROOT / "server"
if str(SERVER_DIR) not in sys.path:
    sys.path.insert(0, str(SERVER_DIR))
if str(PROJECT_ROOT) not in sys.path:
    sys.path.insert(0, str(PROJECT_ROOT))

from server.world.zone_engine import ZoneEngine
from server.world.zone_types import ZoneDefinition, ZoneType, ZoneEnvironment, Waypoint


class TestChallengerWaypointBoundaryEmpirical(unittest.TestCase):
    def setUp(self):
        self.engine = ZoneEngine()
        # Register a test OPEN_WORLD zone with waypoint at (0, 0) and safe_radius 8.0
        self.test_zone_id = "zone_empirical_wp_test"
        self.engine.add_zone(
            ZoneDefinition(
                zone_id=self.test_zone_id,
                name="Bản Đồ Thử Nghiệm Tọa Độ Gốc",
                zone_type=ZoneType.OPEN_WORLD,
                environment=ZoneEnvironment.NORMAL,
                min_level=1,
                max_players=100,
                bounds_width=2000.0,
                bounds_height=2000.0,
                default_spawn_x=0.0,
                default_spawn_y=0.0,
                respawn_zone_id=self.test_zone_id,
                respawn_x=0.0,
                respawn_y=0.0,
                waypoints={
                    "wp_origin": Waypoint(
                        waypoint_id="wp_origin",
                        name="Trụ Đá Gốc",
                        zone_id=self.test_zone_id,
                        x=0.0,
                        y=0.0,
                        safe_radius=8.0,
                    )
                },
            )
        )

    def test_server_boundary_7_9_inside_rejected(self):
        """Distance 7.9 from waypoint (0, 0) must be in safe radius and reject hostile spawn."""
        in_radius = self.engine.is_in_waypoint_safe_radius(self.test_zone_id, 7.9, 0.0)
        self.assertTrue(in_radius, "Distance 7.9 must be inside safe radius")

        allowed, reason = self.engine.validate_monster_spawn(self.test_zone_id, is_dummy=False, x=7.9, y=0.0)
        self.assertFalse(allowed, "Hostile mob at 7.9m must be REJECTED")
        self.assertIn("an toàn", reason)

    def test_server_boundary_8_1_outside_allowed(self):
        """Distance 8.1 from waypoint (0, 0) must be outside safe radius and allow hostile spawn."""
        in_radius = self.engine.is_in_waypoint_safe_radius(self.test_zone_id, 8.1, 0.0)
        self.assertFalse(in_radius, "Distance 8.1 must be outside safe radius")

        allowed, reason = self.engine.validate_monster_spawn(self.test_zone_id, is_dummy=False, x=8.1, y=0.0)
        self.assertTrue(allowed, "Hostile mob at 8.1m must be ALLOWED")
        self.assertIn("hợp lệ", reason)

    def test_server_boundary_8_0_exact_boundary(self):
        """Distance 8.0 (exact boundary) must be inside safe radius and reject hostile spawn."""
        in_radius = self.engine.is_in_waypoint_safe_radius(self.test_zone_id, 8.0, 0.0)
        self.assertTrue(in_radius, "Distance 8.0 must be inside safe radius (<= radius)")

        allowed, reason = self.engine.validate_monster_spawn(self.test_zone_id, is_dummy=False, x=8.0, y=0.0)
        self.assertFalse(allowed, "Hostile mob at exact 8.0m must be REJECTED")

    def test_server_radial_boundary_8_angles(self):
        """Stress-test 8 radial angles at 7.9m vs 8.1m."""
        for deg in [0, 45, 90, 135, 180, 225, 270, 315]:
            rad = math.radians(deg)
            x79, y79 = 7.9 * math.cos(rad), 7.9 * math.sin(rad)
            x81, y81 = 8.1 * math.cos(rad), 8.1 * math.sin(rad)

            self.assertTrue(
                self.engine.is_in_waypoint_safe_radius(self.test_zone_id, x79, y79),
                f"Angle {deg}° at 7.9m must be in safe radius",
            )
            ok79, _ = self.engine.validate_monster_spawn(self.test_zone_id, is_dummy=False, x=x79, y=y79)
            self.assertFalse(ok79, f"Angle {deg}° at 7.9m must reject hostile spawn")

            self.assertFalse(
                self.engine.is_in_waypoint_safe_radius(self.test_zone_id, x81, y81),
                f"Angle {deg}° at 8.1m must NOT be in safe radius",
            )
            ok81, _ = self.engine.validate_monster_spawn(self.test_zone_id, is_dummy=False, x=x81, y=y81)
            self.assertTrue(ok81, f"Angle {deg}° at 8.1m must allow hostile spawn")

    def test_server_target_dummy_at_origin_allowed(self):
        """Target dummy at (0, 0) must SUCCEED regardless of safe radius or sanctuary rules."""
        # Inside test open world at (0, 0)
        allowed, reason = self.engine.validate_monster_spawn(self.test_zone_id, is_dummy=True, x=0.0, y=0.0)
        self.assertTrue(allowed, "Target dummy at (0, 0) must SUCCEED")
        self.assertIn("Target Dummy", reason)

        # Inside canonical sanctuary at (0, 0)
        allowed_sanc, _ = self.engine.validate_monster_spawn("zone_boundless_sanctuary", is_dummy=True, x=0.0, y=0.0)
        self.assertTrue(allowed_sanc, "Target dummy in sanctuary at (0, 0) must SUCCEED")


class TestChallengerClientRuntimeEmpirical(unittest.TestCase):
    def test_run_client_runtime_harness_via_node(self):
        """
        Executes a dedicated Node.js harness that tests:
        1. Client isInWaypointSafeRadius at 7.9, 8.1, 8.0, and 8 radial angles.
        2. Preservation of target dummy at (0, 0).
        3. Player health regen: low HP, clamped maxHp, at maxHp, dead, and outside radius.
        4. Monster leashing across boundary: aggro drop, invulnerability, attack blocking, and persistent latch.
        """
        script = """
import assert from 'node:assert';
import fs from 'node:fs';
import { isInWaypointSafeRadius, ZONE_WAYPOINTS, isSafeHavenZone } from './client/webapp/js/data/wilderness_zone_packs.js';

globalThis.window = globalThis;
globalThis.isInWaypointSafeRadius = isInWaypointSafeRadius;
globalThis.ZONE_WAYPOINTS = ZONE_WAYPOINTS;
globalThis.isSafeHavenZone = isSafeHavenZone;
globalThis.currentZoneId = 'zone_tang_kiem_nhai';

globalThis.document = {
  getElementById: (id) => ({
    style: {},
    innerText: '',
    textContent: '',
    setAttribute() {},
    removeAttribute() {},
    classList: { add() {}, remove() {} }
  }),
  querySelector: () => null,
  querySelectorAll: () => []
};

globalThis.player = {
  wx: 0, wy: 0, hp: 100, maxHp: 100, level: 1, facing: 1,
  godMode: false, potionCooldown: 0, destination: { wx: 0, wy: 0, active: false }
};

globalThis.updatePlayerHpUI = () => {};
globalThis.cancelTelegraph = () => {};

const monsterCode = fs.readFileSync('./client/webapp/js/engine/monster_system.js', 'utf8');
new Function(monsterCode)();

// 1. Boundary tests in JS
assert.strictEqual(isInWaypointSafeRadius(7.9, 0, 'zone_tang_kiem_nhai'), true, '7.9 must be true');
assert.strictEqual(isInWaypointSafeRadius(8.1, 0, 'zone_tang_kiem_nhai'), false, '8.1 must be false');
assert.strictEqual(isInWaypointSafeRadius(8.0, 0, 'zone_tang_kiem_nhai'), true, '8.0 must be true');
for (const deg of [0, 45, 90, 135, 180, 225, 270, 315]) {
  const rad = (deg * Math.PI) / 180;
  assert.strictEqual(isInWaypointSafeRadius(7.9 * Math.cos(rad), 7.9 * Math.sin(rad), 'zone_tang_kiem_nhai'), true);
  assert.strictEqual(isInWaypointSafeRadius(8.1 * Math.cos(rad), 8.1 * Math.sin(rad), 'zone_tang_kiem_nhai'), false);
}

// 2. Target dummy preserved at (0, 0)
const mobs = window.getActiveMonsters();
mobs.push({ id: 'dummy_origin', isDummy: true, wx: 0, wy: 0 });
for (let i = mobs.length - 1; i >= 0; i--) {
  if (!mobs[i].isDummy && window.isInWaypointSafeRadius(mobs[i].wx, mobs[i].wy, 'zone_tang_kiem_nhai')) {
    mobs.splice(i, 1);
  }
}
assert.ok(mobs.find(m => m.id === 'dummy_origin'), 'Dummy at (0,0) must not be filtered');

// 3. Player health regen
player.wx = 0; player.wy = 0;
player.hp = 50; player.maxHp = 100;
window.updateMonstersTick(1.0);
assert.ok(Math.abs(player.hp - 52.0) < 0.001, '50 HP + 2% of 100 * 1s = 52 HP');

player.hp = 99; player.maxHp = 100;
window.updateMonstersTick(1.0);
assert.strictEqual(player.hp, 100, '99 HP clamped to 100 HP');

player.hp = 100; player.maxHp = 100;
window.updateMonstersTick(1.0);
assert.strictEqual(player.hp, 100, 'Max HP remains 100');

player.hp = 0; player.maxHp = 100;
window.updateMonstersTick(1.0);
assert.strictEqual(player.hp, 0, 'Dead player must not heal');

player.wx = 8.1; player.wy = 0;
player.hp = 50; player.maxHp = 100;
window.updateMonstersTick(1.0);
assert.strictEqual(player.hp, 50, 'Outside safe radius must not heal');

// 4. Leash state when crossing boundary
mobs.length = 0;
const wolf = {
  id: 'wolf_1', wx: 10.0, wy: 0.0, originWx: 12.0, originWy: 0.0,
  maxHp: 1000, hp: 500, hasAggro: true, isChasing: true, isNeutral: false,
  isDummy: false, isInvulnerable: false, isLeashing: false, attackCooldown: 0, attackDmg: 50
};
mobs.push(wolf);

player.wx = 9.0; player.wy = 0.0; player.hp = 100;
window.updateMonstersTick(0.1);
assert.strictEqual(wolf.hasAggro, true);
assert.strictEqual(wolf.isLeashing, false);

// Player enters safe zone (7.9)
player.wx = 7.9; player.wy = 0.0;
window.updateMonstersTick(0.1);
assert.strictEqual(wolf.hasAggro, false, 'hasAggro dropped');
assert.strictEqual(wolf.isLeashing, true, 'isLeashing engaged');
assert.strictEqual(wolf.isInvulnerable, true, 'isInvulnerable engaged');

// Player steps outside while mob returns
player.wx = 8.1; player.wy = 0.0;
window.updateMonstersTick(0.1);
assert.strictEqual(wolf.isLeashing, true, 'isLeashing latched');
assert.strictEqual(wolf.hasAggro, false, 'hasAggro stays false');

// Mob reaches origin
wolf.wx = wolf.originWx; wolf.wy = wolf.originWy;
window.updateMonstersTick(0.1);
assert.strictEqual(wolf.isLeashing, false, 'isLeashing cleared at origin');
assert.strictEqual(wolf.isInvulnerable, false, 'isInvulnerable cleared at origin');

console.log('HARNESS_ALL_PASSED');
"""
        proc = subprocess.run(
            ["node", "--input-type=module", "-e", script],
            capture_output=True,
            text=True,
            cwd=str(PROJECT_ROOT),
        )
        self.assertEqual(
            proc.returncode,
            0,
            f"Node.js empirical harness failed:\nSTDOUT:\n{proc.stdout}\nSTDERR:\n{proc.stderr}",
        )
        self.assertIn("HARNESS_ALL_PASSED", proc.stdout)


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