"""
Unit and Integration Test Suite for FreeExile Milestone 2 (R2: Waypoint Safe Radius & Healing):
- Waypoint dataclass safe_radius field default 8.0
- Server ZoneEngine.is_in_waypoint_safe_radius() logic
- Server ZoneEngine.validate_monster_spawn() coordinate-aware safe radius rejection
- Client wilderness_zone_packs.js ZONE_WAYPOINTS registry and isInWaypointSafeRadius()
- Client monster_system.js safe zone leashing, 2% maxHp/sec healing, and attack suppression
- Client initial monster placement outside safe radius
- Client world_renderer.js safe zone indicator
- Line count constraints: monster_system.js <= 490, world_renderer.js <= 500, zone_engine.py <= 500
"""

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 world.zone_types import Waypoint
from world.zone_engine import ZoneEngine

WEBAPP_DIR = PROJECT_ROOT / "client" / "webapp"
WILDERNESS_PACKS_JS = WEBAPP_DIR / "js" / "data" / "wilderness_zone_packs.js"
MONSTER_SYSTEM_JS = WEBAPP_DIR / "js" / "engine" / "monster_system.js"
WORLD_RENDERER_JS = WEBAPP_DIR / "js" / "engine" / "world_renderer.js"
ZONE_ENGINE_PY = SERVER_DIR / "world" / "zone_engine.py"


class TestWaypointSafeRadiusServer(unittest.TestCase):
    def setUp(self):
        self.engine = ZoneEngine()

    def test_waypoint_dataclass_safe_radius_default(self):
        """Verifies Waypoint dataclass has safe_radius default = 8.0."""
        wp = Waypoint(
            waypoint_id="wp_test",
            name="Test Waypoint",
            zone_id="zone_test",
            x=10.0,
            y=20.0,
        )
        self.assertEqual(wp.safe_radius, 8.0)

    def test_waypoint_dataclass_custom_safe_radius(self):
        """Verifies Waypoint dataclass accepts custom safe_radius."""
        wp = Waypoint(
            waypoint_id="wp_custom",
            name="Custom Waypoint",
            zone_id="zone_test",
            x=10.0,
            y=20.0,
            safe_radius=15.0,
        )
        self.assertEqual(wp.safe_radius, 15.0)

    def test_canonical_waypoints_have_safe_radius(self):
        """Verifies all registered canonical waypoints in zones have safe_radius."""
        zone = self.engine.get_zone("zone_tang_kiem_nhai")
        self.assertTrue(len(zone.waypoints) > 0)
        for wp in zone.waypoints.values():
            self.assertEqual(wp.safe_radius, 8.0)

    def test_is_in_waypoint_safe_radius_detection(self):
        """Verifies is_in_waypoint_safe_radius detects coordinates inside and outside."""
        zone_id = "zone_tang_kiem_nhai"
        zone = self.engine.get_zone(zone_id)
        wp = list(zone.waypoints.values())[0]

        # Exact waypoint location -> True
        self.assertTrue(self.engine.is_in_waypoint_safe_radius(zone_id, wp.x, wp.y))

        # 5.0m from waypoint (inside 8.0m radius) -> True
        self.assertTrue(self.engine.is_in_waypoint_safe_radius(zone_id, wp.x + 3.0, wp.y + 4.0))

        # 8.0m exactly (boundary) -> True
        self.assertTrue(self.engine.is_in_waypoint_safe_radius(zone_id, wp.x + 8.0, wp.y))

        # 8.5m from waypoint (outside 8.0m radius) -> False
        self.assertFalse(self.engine.is_in_waypoint_safe_radius(zone_id, wp.x + 6.0, wp.y + 6.0))

        # 30.0m from waypoint (far outside) -> False
        self.assertFalse(self.engine.is_in_waypoint_safe_radius(zone_id, wp.x + 25.0, wp.y + 25.0))

        # Non-existent zone -> False
        self.assertFalse(self.engine.is_in_waypoint_safe_radius("zone_non_existent", 0.0, 0.0))

    def test_validate_monster_spawn_rejects_inside_safe_radius(self):
        """Verifies validate_monster_spawn rejects hostile mob spawn inside waypoint safe radius."""
        zone_id = "zone_tang_kiem_nhai"
        zone = self.engine.get_zone(zone_id)
        wp = list(zone.waypoints.values())[0]

        # Hostile mob right at waypoint -> Rejected
        allowed, reason = self.engine.validate_monster_spawn(zone_id, is_dummy=False, x=wp.x, y=wp.y)
        self.assertFalse(allowed)
        self.assertIn("an toàn", reason)
        self.assertIn(wp.name, reason)

        # Hostile mob at 4.0m from waypoint -> Rejected
        allowed, reason = self.engine.validate_monster_spawn(zone_id, is_dummy=False, x=wp.x + 2.0, y=wp.y + 2.0)
        self.assertFalse(allowed)
        self.assertIn("an toàn", reason)

    def test_validate_monster_spawn_allows_outside_safe_radius(self):
        """Verifies validate_monster_spawn allows hostile mob spawn outside waypoint safe radius."""
        zone_id = "zone_tang_kiem_nhai"
        zone = self.engine.get_zone(zone_id)
        wp = list(zone.waypoints.values())[0]

        # Hostile mob at 20.0m from waypoint -> Allowed
        allowed, reason = self.engine.validate_monster_spawn(zone_id, is_dummy=False, x=wp.x + 20.0, y=wp.y + 20.0)
        self.assertTrue(allowed)
        self.assertIn("hợp lệ", reason)

    def test_validate_monster_spawn_allows_target_dummy_inside_safe_radius(self):
        """Verifies Target Dummy is allowed to spawn even at waypoint coordinates."""
        zone_id = "zone_tang_kiem_nhai"
        zone = self.engine.get_zone(zone_id)
        wp = list(zone.waypoints.values())[0]

        allowed, reason = self.engine.validate_monster_spawn(zone_id, is_dummy=True, x=wp.x, y=wp.y)
        self.assertTrue(allowed)
        self.assertIn("Target Dummy", reason)

    def test_validate_monster_spawn_backward_compatibility(self):
        """Verifies callers passing only (zone_id, is_dummy) continue to function identically."""
        allowed, _ = self.engine.validate_monster_spawn("zone_tang_kiem_nhai")
        self.assertTrue(allowed)

        allowed_sanctuary, _ = self.engine.validate_monster_spawn("zone_boundless_sanctuary")
        self.assertFalse(allowed_sanctuary)


class TestWaypointSafeRadiusClient(unittest.TestCase):
    @classmethod
    def setUpClass(cls):
        cls.packs_js_content = WILDERNESS_PACKS_JS.read_text(encoding="utf-8")
        cls.monster_js_content = MONSTER_SYSTEM_JS.read_text(encoding="utf-8")
        cls.renderer_js_content = WORLD_RENDERER_JS.read_text(encoding="utf-8")

    def test_wilderness_packs_exports_registry_and_helper(self):
        """Verifies wilderness_zone_packs.js exports ZONE_WAYPOINTS and isInWaypointSafeRadius."""
        self.assertIn("export const ZONE_WAYPOINTS", self.packs_js_content)
        self.assertIn("export function isInWaypointSafeRadius", self.packs_js_content)
        self.assertIn("window.ZONE_WAYPOINTS = ZONE_WAYPOINTS", self.packs_js_content)
        self.assertIn("window.isInWaypointSafeRadius = isInWaypointSafeRadius", self.packs_js_content)

    def test_client_is_in_waypoint_safe_radius_via_node(self):
        """Verifies isInWaypointSafeRadius(0, 0) is true and (20, 20) is false via Node.js."""
        script = (
            "import { isInWaypointSafeRadius } from './client/webapp/js/data/wilderness_zone_packs.js';"
            "const atOrigin = isInWaypointSafeRadius(0, 0);"
            "const farAway = isInWaypointSafeRadius(20, 20);"
            "console.log(JSON.stringify({ atOrigin, farAway }));"
        )
        proc = subprocess.run(
            ["node", "--input-type=module", "-e", script],
            capture_output=True,
            text=True,
            cwd=str(PROJECT_ROOT),
            check=True,
        )
        # Parse last non-empty line as JSON
        output_lines = [ln.strip() for ln in proc.stdout.splitlines() if ln.strip()]
        last_line = output_lines[-1]
        import json
        data = json.loads(last_line)
        self.assertTrue(data["atOrigin"], "isInWaypointSafeRadius(0, 0) must be true for spawn point")
        self.assertFalse(data["farAway"], "isInWaypointSafeRadius(20, 20) must be false outside radius")

    def test_wilderness_packs_tang_kiem_outside_safe_radius(self):
        """Verifies initial pack coordinates in zone_tang_kiem_nhai are outside 8.0 unit safe radius."""
        import re
        pack_alpha_match = re.search(r"packId:\s*'pack_tang_kiem_alpha'.*?leader:\s*\{[^}]+wx:\s*([-\d.]+),\s*wy:\s*([-\d.]+)", self.packs_js_content, re.DOTALL)
        self.assertIsNotNone(pack_alpha_match)
        wx_alpha = float(pack_alpha_match.group(1))
        wy_alpha = float(pack_alpha_match.group(2))
        dist_alpha = math.hypot(wx_alpha, wy_alpha)
        self.assertGreater(dist_alpha, 8.0, f"pack_tang_kiem_alpha at ({wx_alpha}, {wy_alpha}) dist {dist_alpha} must be > 8.0")

        pack_beta_match = re.search(r"packId:\s*'pack_tang_kiem_beta'.*?leader:\s*\{[^}]+wx:\s*([-\d.]+),\s*wy:\s*([-\d.]+)", self.packs_js_content, re.DOTALL)
        self.assertIsNotNone(pack_beta_match)
        wx_beta = float(pack_beta_match.group(1))
        wy_beta = float(pack_beta_match.group(2))
        dist_beta = math.hypot(wx_beta, wy_beta)
        self.assertGreater(dist_beta, 8.0, f"pack_tang_kiem_beta at ({wx_beta}, {wy_beta}) dist {dist_beta} must be > 8.0")

    def test_monster_system_safe_zone_healing_and_leashing(self):
        """Verifies monster_system.js implements 2% maxHp/sec healing and force-leashing on safe entry."""
        self.assertIn("playerInSafeZone", self.monster_js_content)
        self.assertIn("0.02 * dt", self.monster_js_content)
        self.assertIn("isInWaypointSafeRadius", self.monster_js_content)
        self.assertIn("m.isLeashing = true", self.monster_js_content)

    def test_monster_system_attack_guard_immunity(self):
        """Verifies executeMonsterAttackOnPlayer guards against hitting player in safe zone."""
        self.assertIn("isInWaypointSafeRadius", self.monster_js_content)
        self.assertIn("if (player.godMode || isSafeHavenZone() || (window.isInWaypointSafeRadius && window.isInWaypointSafeRadius(player.wx, player.wy))) return;", self.monster_js_content)

    def test_world_renderer_safe_zone_indicator(self):
        """Verifies world_renderer.js renders waypoint and gentle visual safe zone indicator with distance fade."""
        self.assertIn("ZONE_WAYPOINTS", self.renderer_js_content)
        self.assertIn("362, 181", self.renderer_js_content)
        self.assertIn("14.0", self.renderer_js_content)
        self.assertIn("setLineDash", self.renderer_js_content)


class TestCodeHygieneAndLineLimits(unittest.TestCase):
    def test_monster_system_line_limit(self):
        """Verifies monster_system.js is <= 490 lines (target <= 485)."""
        lines = MONSTER_SYSTEM_JS.read_text(encoding="utf-8").splitlines()
        self.assertLessEqual(len(lines), 490, f"monster_system.js has {len(lines)} lines (exceeds 490 target)")
        self.assertLessEqual(len(lines), 500, f"monster_system.js has {len(lines)} lines (exceeds 500 hard cap)")

    def test_world_renderer_line_limit(self):
        """Verifies world_renderer.js is <= 500 lines."""
        lines = WORLD_RENDERER_JS.read_text(encoding="utf-8").splitlines()
        self.assertLessEqual(len(lines), 500, f"world_renderer.js has {len(lines)} lines (exceeds 500 hard cap)")

    def test_zone_engine_line_limit(self):
        """Verifies zone_engine.py is <= 500 lines."""
        lines = ZONE_ENGINE_PY.read_text(encoding="utf-8").splitlines()
        self.assertLessEqual(len(lines), 500, f"zone_engine.py has {len(lines)} lines (exceeds 500 hard cap)")


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