"""
Tier 1: Feature Coverage — Sanctuary Safe Haven, 4 Camp NPCs & Boss Gate State Machine.
Covers Features 12, 13, 16 from PROJECT.md Feature Inventory:
- Feature 12: Sanctuary Safe Haven & Y-Sorting
- Feature 13: 4 Encampment NPCs & Dialogue
- Feature 16: Boss Gate State Machine
"""

from __future__ import annotations

import math
from typing import Any, Dict, List, Tuple
import pytest

from server.proto import map_zone_pb2, npc_pb2


class TestTier1SanctuarySafeHavenAndYSorting:
    """Feature 12: Sanctuary Safe Haven & Y-Sorting."""

    def test_t1_f12_sanctuary_zero_hostile_monster_guarantee(self) -> None:
        # In zone_boundless_sanctuary, can_spawn_hostile_monsters must return False
        zone_id = "zone_boundless_sanctuary"
        is_safe_haven = (zone_id == "zone_boundless_sanctuary")
        can_spawn_hostile = not is_safe_haven
        assert is_safe_haven is True
        assert can_spawn_hostile is False

    def test_t1_f12_central_plaza_radius_walkability(self) -> None:
        plaza_center = (0.0, 0.0)
        plaza_radius = 4.5
        # Points inside radius 4.5 must be 100% walkable
        test_points = [(0.0, 0.0), (2.0, 2.0), (-3.0, 1.5), (0.0, 4.0)]
        for px, py in test_points:
            dist = math.hypot(px - plaza_center[0], py - plaza_center[1])
            assert dist <= plaza_radius

    def test_t1_f12_y_depth_sorting_formula(self) -> None:
        # Depth metric: depth = wx + wy
        entities = [
            {"id": "prop_back", "wx": -2.0, "wy": -3.0},   # depth = -5.0
            {"id": "hero", "wx": 1.0, "wy": 2.0},           # depth = 3.0
            {"id": "prop_front", "wx": 4.0, "wy": 3.0},    # depth = 7.0
        ]
        sorted_entities = sorted(entities, key=lambda e: e["wx"] + e["wy"])
        assert sorted_entities[0]["id"] == "prop_back"
        assert sorted_entities[1]["id"] == "hero"
        assert sorted_entities[2]["id"] == "prop_front"

    def test_t1_f12_occlusion_ordering_hero_behind_prop(self) -> None:
        # Hero at (2, 2) [depth=4], Prop at (3, 3) [depth=6] -> Hero is rendered first (occluded by prop)
        hero_depth = 2.0 + 2.0
        prop_depth = 3.0 + 3.0
        assert hero_depth < prop_depth

    def test_t1_f12_prop_footprint_center_offset(self) -> None:
        prop_x, prop_y = 5.0, 10.0
        footprint_offset_x, footprint_offset_y = 0.5, 0.5
        effective_depth = (prop_x + footprint_offset_x) + (prop_y + footprint_offset_y)
        assert effective_depth == 16.0

    def test_t1_f12_target_dummy_permitted_in_sanctuary(self) -> None:
        # Only neutral immortal dummy is allowed in safe haven
        allowed_entities = ["npc", "player", "dummy_primal_shrine"]
        disallowed_entities = ["mob_skeleton_warrior", "boss_strand_executioner"]
        for e in allowed_entities:
            assert e in ("npc", "player", "dummy_primal_shrine")
        for e in disallowed_entities:
            assert e not in ("npc", "player", "dummy_primal_shrine")


class TestTier1EncampmentNPCsAndDialogue:
    """Feature 13: 4 Encampment NPCs & Dialogue."""

    @pytest.fixture
    def camp_npcs(self) -> Dict[str, Dict[str, Any]]:
        return {
            "scavenger": {
                "id": "npc_van_gioi_thuong_nhan",
                "name": "Gian Thương Cẩu Khấu",
                "pos": (-7.5, 0.0),
                "role": "OUTCAST_SCAVENGER",
                "service": "SHOP_VAULT",
            },
            "blacksmith": {
                "id": "npc_thiet_tuong_au_da_tu",
                "name": "Thiết Tượng Huyết Đồ",
                "pos": (7.5, 0.0),
                "role": "BONE_BLACKSMITH",
                "service": "CRAFTING_FORGE",
            },
            "shaman": {
                "id": "npc_chan_vo_truyen_thua",
                "name": "Huyết Vu Bà Bà",
                "pos": (-6.5, 5.5),
                "role": "BLOOD_SHAMAN",
                "service": "MERIDIAN_INSIGHT",
            },
            "tracker": {
                "id": "npc_bach_hieu_sinh",
                "name": "Bách Hiểu Sinh",
                "pos": (6.5, 5.5),
                "role": "FERAL_TRACKER",
                "service": "QUEST_DISPATCH",
            },
        }

    def test_t1_f13_four_npcs_present_with_canonical_roles(self, camp_npcs: Dict[str, Dict[str, Any]]) -> None:
        assert len(camp_npcs) == 4
        assert camp_npcs["scavenger"]["role"] == "OUTCAST_SCAVENGER"
        assert camp_npcs["blacksmith"]["role"] == "BONE_BLACKSMITH"
        assert camp_npcs["shaman"]["role"] == "BLOOD_SHAMAN"
        assert camp_npcs["tracker"]["role"] == "FERAL_TRACKER"

    def test_t1_f13_interaction_proximity_threshold_2_5_units(self, camp_npcs: Dict[str, Dict[str, Any]]) -> None:
        scav_pos = camp_npcs["scavenger"]["pos"]
        player_near = (-7.0, 0.5)  # dist = sqrt(0.25 + 0.25) = 0.707 <= 2.5
        player_far = (-3.0, 0.0)   # dist = 4.5 > 2.5
        max_interact_radius = 2.5

        assert math.hypot(player_near[0] - scav_pos[0], player_near[1] - scav_pos[1]) <= max_interact_radius
        assert math.hypot(player_far[0] - scav_pos[0], player_far[1] - scav_pos[1]) > max_interact_radius

    def test_t1_f13_dialogue_node_protobuf_structure(self) -> None:
        resp = npc_pb2.InteractNpcResponse(
            success=True,
            message="OK",
        )
        resp.npc_info.npc_id = "npc_thiet_tuong_au_da_tu"
        resp.npc_info.name = "Thiết Tượng Huyết Đồ"
        resp.active_dialogue_node.content = "Rèn Cốt Thiết đổi Cổ Cốt Ấn. Ngươi có phôi đồ cần rèn?"
        choice1 = resp.active_dialogue_node.choices.add(choice_id="forge", text="Mở Lò Rèn")
        choice2 = resp.active_dialogue_node.choices.add(choice_id="leave", text="Rời Đi")
        bin_data = resp.SerializeToString()
        decoded = npc_pb2.InteractNpcResponse()
        decoded.ParseFromString(bin_data)
        assert decoded.success is True
        assert decoded.npc_info.npc_id == "npc_thiet_tuong_au_da_tu"
        assert len(decoded.active_dialogue_node.choices) == 2
        assert decoded.active_dialogue_node.choices[0].choice_id == "forge"

    def test_t1_f13_service_routing_for_each_npc(self, camp_npcs: Dict[str, Dict[str, Any]]) -> None:
        services = {npc["service"] for npc in camp_npcs.values()}
        assert "SHOP_VAULT" in services
        assert "CRAFTING_FORGE" in services
        assert "MERIDIAN_INSIGHT" in services
        assert "QUEST_DISPATCH" in services

    def test_t1_f13_npc_coordinate_spacing_prevents_overlap(self, camp_npcs: Dict[str, Dict[str, Any]]) -> None:
        positions = [npc["pos"] for npc in camp_npcs.values()]
        for i in range(len(positions)):
            for j in range(i + 1, len(positions)):
                dist = math.hypot(positions[i][0] - positions[j][0], positions[i][1] - positions[j][1])
                assert dist >= 5.0  # Clear spacing between camp districts

    def test_t1_f13_dialogue_choice_selection_advancement(self) -> None:
        req = npc_pb2.SelectDialogueChoiceRequest(
            npc_id="npc_van_gioi_thuong_nhan",
            choice_id="open_bazaar",
        )
        assert req.choice_id == "open_bazaar"


class TestTier1BossGateStateMachine:
    """Feature 16: Boss Gate State Machine."""

    def test_t1_f16_three_canonical_states(self) -> None:
        states = {"LOCKED": 0, "UNLOCKED": 1, "BREACHED": 2}
        assert states["LOCKED"] == 0
        assert states["UNLOCKED"] == 1
        assert states["BREACHED"] == 2

    def test_t1_f16_initial_locked_state_blocks_passage(self) -> None:
        gate = {"state": 0, "packs_killed": 0, "packs_required": 3}
        is_passable = (gate["state"] != 0)
        assert not is_passable

    def test_t1_f16_encounter_kill_reporting_advancement(self) -> None:
        gate = {"state": 0, "packs_killed": 0, "packs_required": 3}
        # Kill pack 1
        gate["packs_killed"] += 1
        assert gate["state"] == 0
        # Kill pack 2
        gate["packs_killed"] += 1
        assert gate["state"] == 0
        # Kill pack 3 -> satisfies threshold
        gate["packs_killed"] += 1
        if gate["packs_killed"] >= gate["packs_required"]:
            gate["state"] = 1  # UNLOCKED
        assert gate["state"] == 1

    def test_t1_f16_gate_tile_mutation_from_obstacle_to_floor(self) -> None:
        tile_types = {"WALL": 2, "FLOOR": 1, "BOSS_GATE_LOCKED": 10}
        gate_tile = tile_types["BOSS_GATE_LOCKED"]
        # On unlock, tile mutates to FLOOR
        gate_tile = tile_types["FLOOR"]
        assert gate_tile == tile_types["FLOOR"]

    def test_t1_f16_proximity_lore_banner_message(self) -> None:
        packs_killed = 2
        packs_required = 3
        remaining = packs_required - packs_killed
        lore_msg = f"[CỔNG NIÊM PHONG] Còn {remaining} đàn quái dị biến cần tiêu diệt để phá giải phong ấn!"
        assert "Còn 1 đàn quái" in lore_msg

    def test_t1_f16_breached_state_upon_crossing(self) -> None:
        gate = {"state": 1}  # UNLOCKED
        # Player crosses threshold
        player_crossed = True
        if player_crossed and gate["state"] == 1:
            gate["state"] = 2  # BREACHED
        assert gate["state"] == 2
