"""
Tier 1 E2E Tests: Party Lifecycle, Nearby Radius Math, EXP Scaling,
Loot Scaling, 3 Allocation Modes, and Dynamic Monster Scaling (R1).
"""

from __future__ import annotations
import math
import pytest
from tests.e2e_social_party.test_harness import (
    GroundLootDrop,
    LootAllocationMode,
    PartyMemberState,
    PartyScalingCalculator,
    PartyService,
)


@pytest.fixture
def party_svc() -> PartyService:
    return PartyService()


class TestTier1PartyLifecycle:
    """Feature 1: 6-Player Party Lifecycle (Create, Invite, Accept, Decline, Promote, Leave, Kick, Disband)."""

    def test_party_create_initializes_leader(self, party_svc: PartyService) -> None:
        p = party_svc.create_party("p1", "LeaderOne", 25)
        assert p.leader_id == "p1"
        assert len(p.members) == 1
        assert "p1" in p.members
        assert p.members["p1"].level == 25

    def test_party_invite_and_accept_flow(self, party_svc: PartyService) -> None:
        p = party_svc.create_party("p1", "LeaderOne", 25)
        invited = party_svc.invite_player(p.party_id, "p1", "p2")
        assert invited is True
        accepted = party_svc.accept_invite(p.party_id, "p2", "MemberTwo", 24)
        assert accepted is True
        assert len(p.members) == 2
        assert "p2" in p.members

    def test_party_invite_and_decline_flow(self, party_svc: PartyService) -> None:
        p = party_svc.create_party("p1", "LeaderOne", 25)
        party_svc.invite_player(p.party_id, "p1", "p2")
        declined = party_svc.decline_invite(p.party_id, "p2")
        assert declined is True
        assert len(p.members) == 1
        # Cannot accept after decline
        assert party_svc.accept_invite(p.party_id, "p2", "MemberTwo", 24) is False

    def test_party_promote_leader(self, party_svc: PartyService) -> None:
        p = party_svc.create_party("p1", "LeaderOne", 25)
        party_svc.invite_player(p.party_id, "p1", "p2")
        party_svc.accept_invite(p.party_id, "p2", "MemberTwo", 24)
        promoted = party_svc.promote_leader(p.party_id, "p1", "p2")
        assert promoted is True
        assert p.leader_id == "p2"

    def test_party_leave_and_auto_promote(self, party_svc: PartyService) -> None:
        p = party_svc.create_party("p1", "LeaderOne", 25)
        party_svc.invite_player(p.party_id, "p1", "p2")
        party_svc.accept_invite(p.party_id, "p2", "MemberTwo", 24)
        remaining = party_svc.leave_party("p1")
        assert remaining is not None
        assert remaining.leader_id == "p2"
        assert len(remaining.members) == 1

    def test_party_kick_and_disband(self, party_svc: PartyService) -> None:
        p = party_svc.create_party("p1", "LeaderOne", 25)
        party_svc.invite_player(p.party_id, "p1", "p2")
        party_svc.accept_invite(p.party_id, "p2", "MemberTwo", 24)
        kicked = party_svc.kick_member(p.party_id, "p1", "p2")
        assert kicked is True
        assert "p2" not in p.members
        disbanded = party_svc.disband_party(p.party_id, "p1")
        assert disbanded is True
        assert p.party_id not in party_svc.parties


class TestTier1NearbyRadiusMath:
    """Feature 2: Nearby Radius Math (15.0m threshold, same zone, instance_id)."""

    def test_nearby_radius_under_fifteen_meters(self, party_svc: PartyService) -> None:
        p = party_svc.create_party("p1", "Leader", 30, "zone_wild", "inst_1")
        party_svc.invite_player(p.party_id, "p1", "p2")
        party_svc.accept_invite(p.party_id, "p2", "Buddy", 30, "zone_wild", "inst_1")
        p.members["p1"].x, p.members["p1"].y = 0.0, 0.0
        p.members["p2"].x, p.members["p2"].y = 10.0, 5.0  # hypot = ~11.18m <= 15.0m
        nearby = party_svc.get_nearby_members("p1", radius=15.0)
        assert len(nearby) == 2

    def test_nearby_radius_exact_fifteen_meters(self, party_svc: PartyService) -> None:
        p = party_svc.create_party("p1", "Leader", 30, "zone_wild", "inst_1")
        party_svc.invite_player(p.party_id, "p1", "p2")
        party_svc.accept_invite(p.party_id, "p2", "Buddy", 30, "zone_wild", "inst_1")
        p.members["p1"].x, p.members["p1"].y = 0.0, 0.0
        p.members["p2"].x, p.members["p2"].y = 9.0, 12.0  # 9^2 + 12^2 = 81 + 144 = 225 = 15.0^2
        nearby = party_svc.get_nearby_members("p1", radius=15.0)
        assert len(nearby) == 2

    def test_nearby_radius_beyond_fifteen_meters(self, party_svc: PartyService) -> None:
        p = party_svc.create_party("p1", "Leader", 30, "zone_wild", "inst_1")
        party_svc.invite_player(p.party_id, "p1", "p2")
        party_svc.accept_invite(p.party_id, "p2", "Buddy", 30, "zone_wild", "inst_1")
        p.members["p1"].x, p.members["p1"].y = 0.0, 0.0
        p.members["p2"].x, p.members["p2"].y = 15.01, 0.0  # 15.01m > 15.0m
        nearby = party_svc.get_nearby_members("p1", radius=15.0)
        assert len(nearby) == 1
        assert nearby[0].player_id == "p1"

    def test_nearby_radius_different_zones_excluded(self, party_svc: PartyService) -> None:
        p = party_svc.create_party("p1", "Leader", 30, "zone_wild_1", "inst_1")
        party_svc.invite_player(p.party_id, "p1", "p2")
        party_svc.accept_invite(p.party_id, "p2", "Buddy", 30, "zone_wild_2", "inst_1")
        p.members["p1"].x, p.members["p1"].y = 0.0, 0.0
        p.members["p2"].x, p.members["p2"].y = 0.0, 0.0
        nearby = party_svc.get_nearby_members("p1", radius=15.0)
        assert len(nearby) == 1

    def test_nearby_radius_different_instances_excluded(self, party_svc: PartyService) -> None:
        p = party_svc.create_party("p1", "Leader", 30, "zone_wild", "inst_1")
        party_svc.invite_player(p.party_id, "p1", "p2")
        party_svc.accept_invite(p.party_id, "p2", "Buddy", 30, "zone_wild", "inst_2")
        p.members["p1"].x, p.members["p1"].y = 0.0, 0.0
        p.members["p2"].x, p.members["p2"].y = 0.0, 0.0
        nearby = party_svc.get_nearby_members("p1", radius=15.0)
        assert len(nearby) == 1


class TestTier1ExpPartyScaling:
    """Feature 3: EXP Party Scaling (+30% total pool per extra member, (Lvl+10)^2.71 weighting)."""

    def test_exp_scaling_solo_baseline(self) -> None:
        m = [PartyMemberState("p1", "Solo", 50, "z1", "i1", 0.0, 0.0)]
        exp_res = PartyScalingCalculator.calculate_party_exp(1000, m)
        assert exp_res["p1"] == 1000

    def test_exp_scaling_two_equal_members(self) -> None:
        m = [
            PartyMemberState("p1", "A", 50, "z1", "i1", 0.0, 0.0),
            PartyMemberState("p2", "B", 50, "z1", "i1", 0.0, 0.0),
        ]
        # Pool = 1000 * (1.0 + 0.30) = 1300. Split 50/50 = 650 each.
        exp_res = PartyScalingCalculator.calculate_party_exp(1000, m)
        assert exp_res["p1"] == 650
        assert exp_res["p2"] == 650

    def test_exp_scaling_six_equal_members(self) -> None:
        m = [PartyMemberState(f"p{i}", f"M{i}", 60, "z1", "i1", 0.0, 0.0) for i in range(1, 7)]
        # Pool = 1000 * (1.0 + 0.30 * 5) = 2500. Split 6 ways = floor(2500/6) = 416 each.
        exp_res = PartyScalingCalculator.calculate_party_exp(1000, m)
        for i in range(1, 7):
            assert exp_res[f"p{i}"] == 416

    def test_exp_scaling_level_weighting(self) -> None:
        m = [
            PartyMemberState("low", "LowLevel", 20, "z1", "i1", 0.0, 0.0),
            PartyMemberState("high", "HighLevel", 80, "z1", "i1", 0.0, 0.0),
        ]
        # Weight(low) = (20+10)^2.71 = 30^2.71 = ~10323
        # Weight(high) = (80+10)^2.71 = 90^2.71 = 30^2.71 * 3^2.71 = ~10323 * 19.68 = ~203176
        # High receives vastly larger share of pool.
        exp_res = PartyScalingCalculator.calculate_party_exp(1000, m)
        assert exp_res["high"] > exp_res["low"] * 10

    def test_exp_scaling_zero_members_handled(self) -> None:
        assert PartyScalingCalculator.calculate_party_exp(1000, []) == {}


class TestTier1LootQuantityRarityScaling:
    """Feature 4: Loot Quantity & Rarity Scaling (+50% Item/Curr Qty, +30% Rarity per extra member)."""

    def test_loot_scaling_solo_multipliers(self) -> None:
        item_qty, curr_qty, rarity = PartyScalingCalculator.calculate_loot_multipliers(1)
        assert math.isclose(item_qty, 1.0)
        assert math.isclose(curr_qty, 1.0)
        assert math.isclose(rarity, 1.0)

    def test_loot_scaling_two_players(self) -> None:
        item_qty, curr_qty, rarity = PartyScalingCalculator.calculate_loot_multipliers(2)
        assert math.isclose(item_qty, 1.50)
        assert math.isclose(curr_qty, 1.50)
        assert math.isclose(rarity, 1.30)

    def test_loot_scaling_six_players(self) -> None:
        item_qty, curr_qty, rarity = PartyScalingCalculator.calculate_loot_multipliers(6)
        # 1.0 + 0.50 * 5 = 3.50; 1.0 + 0.30 * 5 = 2.50
        assert math.isclose(item_qty, 3.50)
        assert math.isclose(curr_qty, 3.50)
        assert math.isclose(rarity, 2.50)

    def test_loot_scaling_linear_progression(self) -> None:
        for n in range(1, 6):
            i1, c1, r1 = PartyScalingCalculator.calculate_loot_multipliers(n)
            i2, c2, r2 = PartyScalingCalculator.calculate_loot_multipliers(n + 1)
            assert math.isclose(i2 - i1, 0.50)
            assert math.isclose(c2 - c1, 0.50)
            assert math.isclose(r2 - r1, 0.30)

    def test_loot_scaling_zero_nearby_fallback(self) -> None:
        i, c, r = PartyScalingCalculator.calculate_loot_multipliers(0)
        assert i == 1.0 and c == 1.0 and r == 1.0


class TestTier1LootAllocationModes:
    """Feature 5: 3 Loot Allocation Modes (Permanent, Short 5.0s, Free-For-All)."""

    def test_loot_mode_permanent_allocated_can_pickup(self) -> None:
        drop = GroundLootDrop("d1", "sword_rare", "p1", drop_timestamp=100.0, allocation_mode=LootAllocationMode.PERMANENT)
        assert drop.can_player_pickup("p1", current_time=102.0) is True

    def test_loot_mode_permanent_non_allocated_rejected(self) -> None:
        drop = GroundLootDrop("d1", "sword_rare", "p1", drop_timestamp=100.0, allocation_mode=LootAllocationMode.PERMANENT)
        assert drop.can_player_pickup("p2", current_time=200.0) is False

    def test_loot_mode_short_allocated_can_pickup_immediate(self) -> None:
        drop = GroundLootDrop("d2", "orb_exalt", "p1", drop_timestamp=100.0, allocation_mode=LootAllocationMode.SHORT)
        assert drop.can_player_pickup("p1", current_time=101.5) is True

    def test_loot_mode_short_other_blocked_before_five_seconds(self) -> None:
        drop = GroundLootDrop("d2", "orb_exalt", "p1", drop_timestamp=100.0, allocation_mode=LootAllocationMode.SHORT)
        assert drop.can_player_pickup("p2", current_time=104.99) is False

    def test_loot_mode_short_other_allowed_after_five_seconds(self) -> None:
        drop = GroundLootDrop("d2", "orb_exalt", "p1", drop_timestamp=100.0, allocation_mode=LootAllocationMode.SHORT)
        assert drop.can_player_pickup("p2", current_time=105.0) is True

    def test_loot_mode_free_for_all_instant_pickup(self) -> None:
        drop = GroundLootDrop("d3", "currency_gold", "p1", drop_timestamp=100.0, allocation_mode=LootAllocationMode.FREE_FOR_ALL)
        assert drop.can_player_pickup("p2", current_time=100.0) is True


class TestTier1MonsterScaling:
    """Feature 6: Dynamic Monster Scaling (+50% Common, +70% Rare, +100% Boss HP; +10% Armor, +2% Res)."""

    def test_monster_scaling_common_magic_hp(self) -> None:
        s = PartyScalingCalculator.calculate_monster_scaling("COMMON", party_size=6)
        assert math.isclose(s["hp_mult"], 3.50)  # 1.0 + 0.50 * 5

    def test_monster_scaling_rare_hp(self) -> None:
        s = PartyScalingCalculator.calculate_monster_scaling("RARE", party_size=6)
        assert math.isclose(s["hp_mult"], 4.50)  # 1.0 + 0.70 * 5

    def test_monster_scaling_boss_hp(self) -> None:
        s = PartyScalingCalculator.calculate_monster_scaling("BOSS", party_size=6)
        assert math.isclose(s["hp_mult"], 6.00)  # 1.0 + 1.00 * 5

    def test_monster_scaling_armor_and_resistances(self) -> None:
        s = PartyScalingCalculator.calculate_monster_scaling("BOSS", party_size=6)
        assert math.isclose(s["armor_mult"], 1.50)  # 1.0 + 0.10 * 5
        assert math.isclose(s["res_bonus"], 10.0)   # 2.0 * 5

    def test_monster_scaling_poise_ratio(self) -> None:
        s = PartyScalingCalculator.calculate_monster_scaling("RARE", party_size=4)
        assert math.isclose(s["poise_ratio"], 0.15)
