"""
Comprehensive Unit Tests for Safe Teleportation & PoE2 Hideout Town Portal Engine.

Verifies:
1. Domain models, enums, backward-compatibility aliases, and immutability.
2. Authoritative 3.5s channeling state machine with movement/damage interrupts.
3. 5-Gate Destination Validation and spiral ray landing coordinate resolver.
4. Dynamic 2-way TownPortalLink, personal exclusivity, and party member traversal.
5. 6-portal Map Device charge tracking and permanent sealing at 0 charges.
6. Anti-exploit quarantine for active boss encounters and secret chambers.
7. Death penalty handling across wilderness and map instances.
"""

from __future__ import annotations

import pytest

from server.party.party_service import PartyService
from server.social.guild_roster_service import GuildRosterService
from server.social.social_service import SocialService
from server.social.social_types import GuildMemberPresenceDTO
from server.world.teleport_service import TeleportService
from server.world.town_portal_engine import TownPortalEngine
from server.world.town_portal_types import (
    InterruptReason,
    PortalQuarantineStatus,
    TargetType,
    TeleportChannelSession,
    TeleportChannelStatus,
    TownPortalLink,
)


@pytest.fixture
def services():
    party_svc = PartyService()
    social_svc = SocialService()
    guild_svc = GuildRosterService()
    teleport_svc = TeleportService(party_svc, social_svc, guild_svc)
    portal_engine = TownPortalEngine()
    return party_svc, social_svc, guild_svc, teleport_svc, portal_engine


class TestTownPortalTypesAndAliases:
    """Verifies dataclass immutability, slotted properties, and compatibility aliases."""

    def test_town_portal_link_properties_and_subscripting(self) -> None:
        portal = TownPortalLink(
            portal_id="tp_hero_1",
            owner_id="hero_1",
            wilderness_zone_id="zone_tang_kiem_nhai",
            wilderness_x=120.0,
            wilderness_y=340.0,
            hub_zone_id="zone_player_hideout",
            hub_x=0.0,
            hub_y=0.0,
            is_active=True,
        )
        assert portal.player_id == "hero_1"
        assert portal.origin_zone == "zone_tang_kiem_nhai"
        assert portal.origin_x == 120.0
        assert portal.origin_y == 340.0
        assert portal.hub_zone == "zone_player_hideout"
        # Dict-style subscripting
        assert portal["origin_zone"] == "zone_tang_kiem_nhai"
        assert portal["hub_zone"] == "zone_player_hideout"
        assert portal["is_active"] is True
        # Slotted dict-style mutability support
        portal["is_active"] = False
        assert portal.is_active is False

    def test_teleport_channel_session_properties_and_subscripting(self) -> None:
        session = TeleportChannelSession(
            session_id="chan_p1_100",
            player_id="p1",
            target_type=TargetType.PARTY_MEMBER,
            target_id="p2",
            start_time=1000.0,
            duration_seconds=3.5,
            target_zone_id="zone_wild",
        )
        assert session.channel_id == "chan_p1_100"
        assert session.caster_id == "p1"
        assert session.target_player_id == "p2"
        assert session.cast_time == 3.5
        assert session.duration_ms == 3500
        assert session["target_zone_id"] == "zone_wild"
        assert session["start_time"] == 1000.0


class TestTeleportChannelStateMachine:
    """Verifies 3.5s channeling lifecycle, movement/damage interrupts, and spiral ray resolver."""

    def test_channel_full_lifecycle_success(self, services) -> None:
        party_svc, _, _, teleport_svc, _ = services
        p = party_svc.create_party("p1", "Caster", 30)
        party_svc.invite_player(p.party_id, "p1", "p2")
        party_svc.accept_invite(p.party_id, "p2", "Target", 30)

        ok, chan_id = teleport_svc.start_teleport_channel("p1", TargetType.PARTY_MEMBER, "p2", "zone_wild")
        assert ok is True and chan_id.startswith("chan_p1_")
        assert "p1" in teleport_svc.active_channels

        # Incomplete cast before 3.5s
        ok_premature, err = teleport_svc.complete_channel("p1", elapsed_time=3.4)
        assert ok_premature is False and "CAST_INCOMPLETE" in err

        # Completed cast at 3.5s
        ok_done, dest_zone = teleport_svc.complete_channel("p1", elapsed_time=3.5)
        assert ok_done is True and dest_zone == "zone_wild"
        assert "p1" not in teleport_svc.active_channels

    def test_movement_interrupt_triggers(self, services) -> None:
        party_svc, _, _, teleport_svc, _ = services
        p = party_svc.create_party("p1", "Caster", 30)
        party_svc.invite_player(p.party_id, "p1", "p2")
        party_svc.accept_invite(p.party_id, "p2", "Target", 30)

        teleport_svc.start_teleport_channel("p1", TargetType.PARTY_MEMBER, "p2", "zone_wild")
        # Sub-threshold movement tolerated (0.05 <= 0.05)
        assert teleport_svc.on_player_moved("p1", mag=0.04) is None
        assert "p1" in teleport_svc.active_channels

        # Movement > 0.05 cancels channel
        reason = teleport_svc.on_player_moved("p1", mag=0.06)
        assert reason == InterruptReason.INTERRUPT_MOVEMENT
        assert "p1" not in teleport_svc.active_channels

    def test_damage_interrupt_triggers(self, services) -> None:
        party_svc, _, _, teleport_svc, _ = services
        p = party_svc.create_party("p1", "Caster", 30)
        party_svc.invite_player(p.party_id, "p1", "p2")
        party_svc.accept_invite(p.party_id, "p2", "Target", 30)

        teleport_svc.start_teleport_channel("p1", TargetType.PARTY_MEMBER, "p2", "zone_wild")
        # 0 damage ignored
        assert teleport_svc.on_player_damaged("p1", damage=0.0) is None
        assert "p1" in teleport_svc.active_channels

        # Damage > 0 cancels channel immediately
        reason = teleport_svc.on_player_damaged("p1", damage=1.5)
        assert reason == InterruptReason.INTERRUPT_DAMAGE
        assert "p1" not in teleport_svc.active_channels

    def test_manual_cancellation(self, services) -> None:
        party_svc, _, _, teleport_svc, _ = services
        p = party_svc.create_party("p1", "Caster", 30)
        party_svc.invite_player(p.party_id, "p1", "p2")
        party_svc.accept_invite(p.party_id, "p2", "Target", 30)

        teleport_svc.start_teleport_channel("p1", TargetType.PARTY_MEMBER, "p2", "zone_wild")
        assert teleport_svc.cancel_channel("p1") is True
        assert teleport_svc.cancel_channel("p1") is False

    def test_spiral_ray_landing_resolver(self, services) -> None:
        _, _, _, teleport_svc, _ = services
        # Unblocked resolver picks first angle at 2.0m (angle 0.0 -> x + 2.0)
        lx, ly = teleport_svc.resolve_safe_landing_coords(10.0, 20.0)
        assert lx == 12.0 and ly == 20.0

        # If (12.0, 20.0) is blocked, picks second angle (45 degrees)
        def mock_blocked(x: float, y: float) -> bool:
            return x == 12.0 and y == 20.0

        lx2, ly2 = teleport_svc.resolve_safe_landing_coords(10.0, 20.0, mock_blocked)
        assert (lx2, ly2) != (12.0, 20.0)


class TestFiveGateDestinationValidation:
    """Verifies all 5 destination validation gates."""

    def test_gate_1_liveness(self, services) -> None:
        _, _, _, teleport_svc, _ = services
        ok, reason = teleport_svc.validate_destination_5_gates("p1", "p2", TargetType.PARTY_MEMBER, "zone_w", target_alive=False)
        assert ok is False and "GATE_1_TARGET_DEAD" in reason

    def test_gate_2_relationship_authorization(self, services) -> None:
        party_svc, social_svc, guild_svc, teleport_svc, _ = services
        # Party check
        ok, r1 = teleport_svc.validate_destination_5_gates("p1", "stranger", TargetType.PARTY_MEMBER, "zone_w")
        assert ok is False and "GATE_2_RELATIONSHIP" in r1

        # Friend check
        ok, r2 = teleport_svc.validate_destination_5_gates("p1", "stranger", TargetType.FRIEND, "zone_w")
        assert ok is False and "GATE_2_RELATIONSHIP" in r2

        # Guild check
        ok, r3 = teleport_svc.validate_destination_5_gates("p1", "stranger", TargetType.GUILDMATE, "zone_w")
        assert ok is False and "GATE_2_RELATIONSHIP" in r3

        # Guild valid mate
        m = GuildMemberPresenceDTO("g_mate", "Mate", 50, "Huynh Đệ", True, "zone_w")
        guild_svc.register_member("guild_1", m)
        ok_g, _ = teleport_svc.validate_destination_5_gates("p1", "g_mate", TargetType.GUILDMATE, "zone_w")
        assert ok_g is True

    def test_gate_3_and_4_capacity_and_lifecycle(self, services) -> None:
        party_svc, _, _, teleport_svc, _ = services
        p = party_svc.create_party("p1", "Caster", 30)
        party_svc.invite_player(p.party_id, "p1", "p2")
        party_svc.accept_invite(p.party_id, "p2", "Target", 30)

        # Gate 3 capacity exceeded
        teleport_svc.set_zone_state("zone_full", player_count=6, max_capacity=6)
        ok3, r3 = teleport_svc.validate_destination_5_gates("p1", "p2", TargetType.PARTY_MEMBER, "zone_full")
        assert ok3 is False and "GATE_3_CAPACITY" in r3

        # Gate 4 inactive lifecycle
        teleport_svc.set_zone_state("zone_dead", is_active=False)
        ok4, r4 = teleport_svc.validate_destination_5_gates("p1", "p2", TargetType.PARTY_MEMBER, "zone_dead")
        assert ok4 is False and "GATE_4_LIFECYCLE" in r4

    def test_gate_5_quarantine_and_isolation(self, services) -> None:
        party_svc, _, _, teleport_svc, _ = services
        p = party_svc.create_party("p1", "Caster", 30)
        party_svc.invite_player(p.party_id, "p1", "p2")
        party_svc.accept_invite(p.party_id, "p2", "Target", 30)

        # Boss quarantine
        teleport_svc.set_zone_state("zone_boss", is_boss_active=True)
        ok_b, rb = teleport_svc.validate_destination_5_gates("p1", "p2", TargetType.PARTY_MEMBER, "zone_boss")
        assert ok_b is False and "GATE_5_QUARANTINE_BOSS" in rb

        # Secret chamber quarantine
        teleport_svc.set_zone_state("zone_secret", is_secret_active=True)
        ok_s, rs = teleport_svc.validate_destination_5_gates("p1", "p2", TargetType.PARTY_MEMBER, "zone_secret")
        assert ok_s is False and "GATE_5_QUARANTINE_SECRET" in rs

        # Private hideout isolation
        ok_h, rh = teleport_svc.validate_destination_5_gates("p1", "p2", TargetType.PARTY_MEMBER, "zone_player_hideout_other")
        assert ok_h is False and "GATE_5_RESTRICTED_ISOLATION" in rh


class TestTownPortalEngineAndQuarantine:
    """Verifies 2-way TownPortalLink, 6 map charges, encounter quarantines, and death penalty."""

    def test_2_way_portal_creation_and_exclusivity(self, services) -> None:
        _, _, _, _, portal_engine = services
        ok1, _, p1_id = portal_engine.request_town_portal("hero", "zone_wild", "inst_1", 10.0, 20.0)
        assert ok1 is True and p1_id is not None
        assert portal_engine.portals[p1_id].is_active is True

        # Recasting closes previous portal
        ok2, _, p2_id = portal_engine.request_town_portal("hero", "zone_wild", "inst_1", 50.0, 60.0)
        assert ok2 is True and p2_id != p1_id
        assert portal_engine.portals[p1_id].is_active is False
        assert portal_engine.portals[p2_id].is_active is True

    def test_party_member_portal_traversal(self, services) -> None:
        party_svc, _, _, _, portal_engine = services
        p = party_svc.create_party("leader", "Leader", 40)
        party_svc.invite_player(p.party_id, "leader", "mate")
        party_svc.accept_invite(p.party_id, "mate", "Mate", 40)

        _, _, p_id = portal_engine.request_town_portal("leader", "zone_wild", "inst_1", 15.0, 25.0)

        # Leader (owner) can traverse
        ok_own, _, dest1 = portal_engine.traverse_town_portal("leader", p_id, party_svc, is_return=False)
        assert ok_own is True and dest1[0] == "zone_player_hideout"

        # Party member can traverse
        ok_mate, _, dest2 = portal_engine.traverse_town_portal("mate", p_id, party_svc, is_return=True)
        assert ok_mate is True and dest2 == ("zone_wild", 15.0, 25.0)

        # Non-party stranger rejected
        ok_stranger, err, _ = portal_engine.traverse_town_portal("stranger", p_id, party_svc)
        assert ok_stranger is False and "PORTAL_UNAUTHORIZED" in err

    def test_map_device_charge_tracking_and_permanent_seal(self, services) -> None:
        _, _, _, _, portal_engine = services
        portal_engine.register_astral_map_device("map_t16", initial_charges=2)
        assert portal_engine.map_charges["map_t16"] == 2

        ok1, _, rem1 = portal_engine.enter_map_portal("map_t16")
        assert ok1 is True and rem1 == 1

        ok2, _, rem2 = portal_engine.enter_map_portal("map_t16")
        assert ok2 is True and rem2 == 0

        # Attempt 3: 0 charges remain -> MAP_SEALED
        ok3, msg3, rem3 = portal_engine.enter_map_portal("map_t16")
        assert ok3 is False and rem3 == 0 and "MAP_SEALED" in msg3

    def test_anti_exploit_boss_and_secret_quarantine(self, services) -> None:
        _, _, _, _, portal_engine = services
        portal_engine.set_encounter_state("inst_boss", boss_active=True)

        # Blocked in boss fight
        ok_b, msg_b, p_b = portal_engine.request_town_portal("hero", "zone_boss", "inst_boss", 0.0, 0.0)
        assert ok_b is False and p_b is None
        assert "QUARANTINE_BOSS_ENGAGED" in msg_b

        # Boss defeated -> allowed
        portal_engine.set_encounter_state("inst_boss", boss_active=False)
        ok_b2, _, p_b2 = portal_engine.request_town_portal("hero", "zone_boss", "inst_boss", 0.0, 0.0)
        assert ok_b2 is True and p_b2 is not None

        # Blocked in secret chamber
        portal_engine.set_encounter_state("inst_secret", secret_active=True)
        ok_s, msg_s, p_s = portal_engine.request_town_portal("hero", "zone_secret", "inst_secret", 0.0, 0.0)
        assert ok_s is False and p_s is None
        assert "QUARANTINE_SECRET_CHAMBER" in msg_s

    def test_death_penalty_wilderness_vs_map_instance(self, services) -> None:
        _, _, _, _, portal_engine = services
        # Wilderness death closes personal portal, keeps other portals
        _, _, p1 = portal_engine.request_town_portal("hero_1", "zone_w", "inst_1", 0.0, 0.0)
        _, _, p2 = portal_engine.request_town_portal("hero_2", "zone_w", "inst_1", 0.0, 0.0)
        ok_w, msg_w = portal_engine.on_player_death("hero_1", "zone_w", "inst_1", is_map_instance=False)
        assert ok_w is True and "WILDERNESS_DEATH" in msg_w
        assert portal_engine.portals[p1].is_active is False
        assert portal_engine.portals[p2].is_active is True

        # Map instance death consumes charge
        portal_engine.register_astral_map_device("map_inst", initial_charges=4)
        ok_m, msg_m = portal_engine.on_player_death("hero_1", "zone_map", "map_inst", is_map_instance=True)
        assert ok_m is True and "MAP_DEATH_CONSUMED_PORTAL" in msg_m
        assert portal_engine.map_charges["map_inst"] == 3
