"""
Unit Test Suite for FreeExile Level Progression Service & Death Penalty Engine.
Verifies LevelProgressionService, Piecewise EXP Curve, Level Gap Decay,
and Tiered Death Penalty Safe Floor adhering to PoE2 2026 Standards.
"""

from __future__ import annotations
import math
from typing import Generator
import pytest

from server.world.combat_engine import CombatActor, CombatEngine, DamageEventResult
from server.world.level_progression_service import LevelProgressionService
from server.world.level_progression_types import DeathPenaltyResult
from server.world.martial_matrix import FiveElements


@pytest.fixture
def progression_service() -> Generator[LevelProgressionService, None, None]:
    """Provides a fresh LevelProgressionService."""
    service = LevelProgressionService()
    yield service


class TestLevelGapDecayFormula:
    """Verifies exponential decay formula: eta(Delta) = exp(-0.60 * (Delta - 5))."""

    def test_level_gap_exact_match_full_exp(
        self, progression_service: LevelProgressionService
    ) -> None:
        """Player level == Monster level yields 100% (1.0) multiplier."""
        assert progression_service.calculate_level_gap_multiplier(80, 80) == 1.0

    def test_level_gap_safe_range_within_5_levels(
        self, progression_service: LevelProgressionService
    ) -> None:
        """Level differences within [-5, +5] receive 100% full EXP."""
        for diff in range(-5, 6):
            mult = progression_service.calculate_level_gap_multiplier(80, 80 + diff)
            assert mult == 1.0

    def test_level_gap_decay_boundary_6_levels(
        self, progression_service: LevelProgressionService
    ) -> None:
        """Gap of 6 levels triggers exponential decay: exp(-0.60) ~= 0.5488."""
        mult = progression_service.calculate_level_gap_multiplier(86, 80)
        assert abs(mult - math.exp(-0.60)) < 1e-4

    def test_level_gap_decay_10_levels_lower_under_five_percent(
        self, progression_service: LevelProgressionService
    ) -> None:
        """Monster 10 levels lower yields <= 5% EXP (exp(-3.0) ~= 0.04978 <= 0.05)."""
        mult = progression_service.calculate_level_gap_multiplier(90, 80)
        assert mult <= 0.05
        assert abs(mult - math.exp(-3.0)) < 1e-4

    def test_level_gap_extreme_overlevel_clamp(
        self, progression_service: LevelProgressionService
    ) -> None:
        """Extreme level gap clamps to absolute floor 0.01 (1%)."""
        mult = progression_service.calculate_level_gap_multiplier(95, 20)
        assert mult == 0.01

    def test_level_gap_underleveled_anti_boosting(
        self, progression_service: LevelProgressionService
    ) -> None:
        """Underleveled player decay prevents power-leveling (clamped to 0.05)."""
        mult = progression_service.calculate_level_gap_multiplier(20, 80)
        assert 0.05 <= mult <= 0.10


class TestTieredDeathPenalty:
    """Verifies tiered percentage penalties and absolute safe floor (never de-level)."""

    @pytest.mark.parametrize("level", [1, 20, 45, 60])
    def test_death_penalty_grace_period_level_1_to_60(
        self, progression_service: LevelProgressionService, level: int
    ) -> None:
        """Levels 1 to 60 have 0% death penalty (grace period)."""
        assert progression_service.get_death_penalty_ratio(level) == 0.0

    @pytest.mark.parametrize("level", [61, 70, 80])
    def test_death_penalty_mid_tier_61_to_80(
        self, progression_service: LevelProgressionService, level: int
    ) -> None:
        """Levels 61 to 80 lose 5% of current level EXP bar."""
        assert progression_service.get_death_penalty_ratio(level) == 0.05

    @pytest.mark.parametrize("level", [81, 85, 89])
    def test_death_penalty_high_tier_81_to_89(
        self, progression_service: LevelProgressionService, level: int
    ) -> None:
        """Levels 81 to 89 lose 10% of current level EXP bar."""
        assert progression_service.get_death_penalty_ratio(level) == 0.10

    @pytest.mark.parametrize("level", [90, 95, 98])
    def test_death_penalty_endgame_tier_90_to_98(
        self, progression_service: LevelProgressionService, level: int
    ) -> None:
        """Levels 90 to 98 lose 15% of current level EXP bar."""
        assert progression_service.get_death_penalty_ratio(level) == 0.15

    def test_death_penalty_pinnacle_tier_99(
        self, progression_service: LevelProgressionService
    ) -> None:
        """Level 99 loses 25% of current level EXP bar."""
        assert progression_service.get_death_penalty_ratio(99) == 0.25

    def test_death_penalty_godhood_tier_100(
        self, progression_service: LevelProgressionService
    ) -> None:
        """Level 100 has 0% death penalty (immortal cap)."""
        assert progression_service.get_death_penalty_ratio(100) == 0.0

    def test_death_penalty_safe_floor_at_zero_exp(
        self, progression_service: LevelProgressionService
    ) -> None:
        """Dying at 0% EXP results in 0 lost EXP; player never drops below level."""
        pid = "player_zero_exp"
        progression_service.set_player_state(pid, level=95, current_exp=0)
        res = progression_service.apply_death_penalty(pid)
        assert res.exp_lost == 0
        assert res.new_exp == 0
        assert res.de_leveled is False
        assert progression_service.get_level_info(pid).level == 95

    def test_death_penalty_safe_floor_clamping(
        self, progression_service: LevelProgressionService
    ) -> None:
        """Dying at 10% EXP at Level 99 (25% penalty) clamps to 0% without de-leveling."""
        pid = "player_clamp_test"
        d99 = progression_service.get_delta_exp(99)
        initial_exp = int(d99 * 0.10)
        progression_service.set_player_state(pid, level=99, current_exp=initial_exp)
        res = progression_service.apply_death_penalty(pid)
        assert res.new_exp == 0
        assert res.exp_lost == initial_exp
        assert res.de_leveled is False
        assert progression_service.get_level_info(pid).level == 99

    def test_death_penalty_consecutive_death_streak(
        self, progression_service: LevelProgressionService
    ) -> None:
        """5 consecutive deaths at Level 99 clamp at 0% and track deaths_count."""
        pid = "player_streak"
        d99 = progression_service.get_delta_exp(99)
        progression_service.set_player_state(pid, level=99, current_exp=int(d99 * 0.50))
        for _ in range(5):
            progression_service.apply_death_penalty(pid)
        state = progression_service.get_level_info(pid)
        assert state.level == 99
        assert state.current_exp == 0
        assert state.deaths_count == 5

    def test_level_100_death_handling_telemetry_and_listener(
        self, progression_service: LevelProgressionService
    ) -> None:
        """Level 100 death increments deaths_count, loses 0 EXP, and notifies listeners."""
        pid = "player_lv100_death_telemetry"
        b100 = progression_service.get_canonical_benchmark(100)
        progression_service.set_player_state(pid, level=100, current_exp=0, deaths_count=0)
        events: list[DeathPenaltyResult] = []
        progression_service.add_death_penalty_listener(lambda r: events.append(r))

        res = progression_service.apply_death_penalty(pid)
        assert res.player_id == pid and res.level == 100
        assert res.exp_lost == 0 and res.penalty_ratio == 0.0
        assert res.current_exp_before == 0 and res.current_exp_after == 0 and not res.de_leveled

        state = progression_service.get_level_info(pid)
        assert state.deaths_count == 1 and state.level == 100
        assert state.current_exp == 0 and state.cumulative_exp == b100.cumulative_exp
        assert len(events) == 1 and events[0] == res

    def test_level_100_consecutive_death_streak_increments_deaths_count(
        self, progression_service: LevelProgressionService
    ) -> None:
        """Consecutive deaths at Level 100 increment deaths_count and dispatch every event."""
        pid = "player_lv100_streak"
        progression_service.set_player_state(pid, level=100, current_exp=0, deaths_count=0)
        events: list[DeathPenaltyResult] = []
        progression_service.add_death_penalty_listener(lambda r: events.append(r))

        for i in range(1, 4):
            res = progression_service.apply_death_penalty(pid)
            assert res.exp_lost == 0
            assert progression_service.get_level_info(pid).deaths_count == i
        assert len(events) == 3


class TestLevelUpTransitions:
    """Verifies level advancement, rollover EXP, and passive/stat grants."""

    def test_level_up_1_to_2_exact_exp(
        self, progression_service: LevelProgressionService
    ) -> None:
        """Awarding exactly 600 EXP advances Level 1 to Level 2 with 0 rollover."""
        pid = "player_exact"
        res = progression_service.award_monster_exp(pid, 1, 1, 600)
        assert res.level_up_occurred is True
        assert res.new_level == 2
        assert res.current_exp == 0

    def test_level_up_1_to_2_with_rollover(
        self, progression_service: LevelProgressionService
    ) -> None:
        """Awarding 650 EXP advances Level 1 to Level 2 with 50 rollover EXP."""
        pid = "player_rollover"
        res = progression_service.award_monster_exp(pid, 1, 1, 650)
        assert res.level_up_occurred is True
        assert res.new_level == 2
        assert res.current_exp == 50

    def test_level_up_stats_and_talent_points(
        self, progression_service: LevelProgressionService
    ) -> None:
        """Level up grants +5 stat points and +1 unspent talent point."""
        pid = "player_stats"
        progression_service.award_monster_exp(pid, 1, 1, 600)
        state = progression_service.get_level_info(pid)
        assert state.unspent_talent_points == 1
        assert state.total_talent_points == 1

    def test_multi_level_jump(
        self, progression_service: LevelProgressionService
    ) -> None:
        """Large EXP award cascades through multiple levels accurately."""
        pid = "player_multi"
        d1 = progression_service.get_delta_exp(1)  # 600
        d2 = progression_service.get_delta_exp(2)  # 2662
        res = progression_service.award_monster_exp(pid, 1, 1, d1 + d2 + 100)
        assert res.new_level == 3
        assert res.current_exp == 100
        assert res.levels_gained == 2


class TestLevelCapAndTerminalState:
    """Verifies strict Level 100 cap and zero overflow."""

    def test_level_100_cap_no_further_exp_gain(
        self, progression_service: LevelProgressionService
    ) -> None:
        """Kills at Level 100 award 0 effective EXP."""
        pid = "player_lv100"
        progression_service.set_player_state(pid, level=100, current_exp=0)
        res = progression_service.award_monster_exp(pid, 100, 100, 100000)
        assert res.effective_exp == 0
        assert res.level_up_occurred is False
        assert res.new_level == 100

    def test_level_100_death_penalty_immunity(
        self, progression_service: LevelProgressionService
    ) -> None:
        """Level 100 player suffers 0 penalty upon death."""
        pid = "player_lv100_death"
        progression_service.set_player_state(pid, level=100, current_exp=0)
        res = progression_service.apply_death_penalty(pid)
        assert res.exp_lost == 0
        assert res.new_exp == 0
        assert progression_service.get_level_info(pid).level == 100

    def test_level_100_cumulative_exp_clamping_on_level_up(
        self, progression_service: LevelProgressionService
    ) -> None:
        """Level 99 with surplus EXP clamps exactly to 24,285,477,315 with zero overflow."""
        pid = "player_lv99_clamp_test"
        expected_cap = progression_service.get_canonical_benchmark(100).cumulative_exp
        d99 = progression_service.get_delta_exp(99)

        progression_service.set_player_state(pid, level=99, current_exp=d99 - 1)
        assert progression_service.get_level_info(pid).cumulative_exp == expected_cap - 1

        res = progression_service.award_monster_exp(pid, monster_level=100, base_exp=1_000_000)
        assert res.new_level == 100 and res.leveled_up and res.current_exp == 0

        info = progression_service.get_level_info(pid)
        assert info.level == 100 and info.current_exp == 0
        assert info.cumulative_exp == expected_cap and info.lifetime_exp == expected_cap

    def test_level_100_set_player_state_cumulative_exp_clamping(
        self, progression_service: LevelProgressionService
    ) -> None:
        """Direct set_player_state at Level 100 clamps cumulative_exp to benchmark cap."""
        pid = "player_direct_100"
        expected_cap = progression_service.get_canonical_benchmark(100).cumulative_exp
        progression_service.set_player_state(pid, level=100, current_exp=5000)
        info = progression_service.get_level_info(pid)
        assert info.level == 100 and info.current_exp == 0
        assert info.cumulative_exp == expected_cap

    def test_level_100_further_monster_kills_maintain_cumulative_exp_cap(
        self, progression_service: LevelProgressionService
    ) -> None:
        """Subsequent monster kills at Level 100 award 0 EXP and maintain cumulative cap."""
        pid = "player_100_grind"
        expected_cap = progression_service.get_canonical_benchmark(100).cumulative_exp
        progression_service.set_player_state(pid, level=100, current_exp=0)
        for _ in range(5):
            res = progression_service.award_monster_exp(pid, monster_level=100, base_exp=500_000)
            assert res.effective_exp == 0 and not res.leveled_up
        assert progression_service.get_level_info(pid).cumulative_exp == expected_cap


class TestServiceStateAndPersistence:
    """Verifies DTO properties, aliases, and polymorphic call signatures."""

    def test_interface_polymorphism_and_aliases(
        self, progression_service: LevelProgressionService
    ) -> None:
        """Verifies compatibility with E2E signatures and alias properties."""
        pid = "player_alias"
        res = progression_service.award_monster_exp(pid, 80, 80, 1000)
        assert res.effective_exp == res.exp_awarded == 1000
        assert res.level_up_occurred == res.leveled_up is False

        death_res = progression_service.apply_death_penalty(pid)
        assert death_res.new_exp == death_res.current_exp_after
        assert death_res.penalty_exp_lost == death_res.exp_lost
        assert death_res.penalty_percentage == death_res.penalty_ratio

    def test_award_monster_exp_negative_base_exp_sanitized_to_zero(
        self, progression_service: LevelProgressionService
    ) -> None:
        """Negative base_exp argument is clamped to 0, preventing unintended EXP loss."""
        pid = "player_neg_exp"
        progression_service.set_player_state(pid, level=10, current_exp=100)
        res = progression_service.award_monster_exp(pid, monster_level=10, base_exp=-100)
        assert res.effective_exp == 0 and res.exp_awarded == 0
        assert progression_service.get_level_info(pid).current_exp == 100


class TestCombatEngineIntegration:
    """Verifies CombatEngine fatal damage callback and progression hook."""

    def test_combat_fatal_damage_triggers_death_penalty(
        self, progression_service: LevelProgressionService
    ) -> None:
        """When player suffers lethal damage, attached progression service penalizes player."""
        engine = CombatEngine()
        engine.attach_progression_service(progression_service)

        d85 = progression_service.get_delta_exp(85)
        progression_service.set_player_state("p_combat_die", level=85, current_exp=int(d85 * 0.50))

        attacker = CombatActor(10, "Boss Thần Ma", FiveElements.HOA, base_attack=9999.0)
        defender = CombatActor(
            11, "Player In Peril", FiveElements.HOA, current_hp=500.0,
            max_hp=500.0, is_player=True, player_id="p_combat_die"
        )
        engine.register_actor(attacker)
        engine.register_actor(defender)

        dmg_res = engine.calculate_damage(10, 11, 9999.0, FiveElements.HOA, current_timestamp_ms=1000)
        assert dmg_res.is_fatal is True
        assert defender.current_hp == 0.0

        state = progression_service.get_level_info("p_combat_die")
        assert state.deaths_count == 1
        assert state.current_exp == int(d85 * 0.50) - int(d85 * 0.10)

    def test_combat_monster_defeat_awards_player_exp(
        self, progression_service: LevelProgressionService
    ) -> None:
        """When monster is defeated by player, attached progression service awards EXP."""
        engine = CombatEngine()
        engine.attach_progression_service(progression_service)

        progression_service.set_player_state("p_combat_slayer", level=1, current_exp=0)

        attacker = CombatActor(
            20, "Slayer Player", FiveElements.KIM, base_attack=100.0,
            is_player=True, player_id="p_combat_slayer"
        )
        defender = CombatActor(
            21, "Wild Skeleton", FiveElements.MOC, current_hp=50.0,
            max_hp=50.0, is_player=False, level=1
        )
        engine.register_actor(attacker)
        engine.register_actor(defender)

        dmg_res = engine.calculate_damage(20, 21, 100.0, FiveElements.KIM, current_timestamp_ms=2000)
        assert dmg_res.is_fatal is True
        assert defender.current_hp == 0.0
        assert progression_service.get_level_info("p_combat_slayer").current_exp > 0

    def test_combat_corpse_overkill_produces_not_fatal_and_no_callback(self) -> None:
        """Striking dead defender (HP=0.0) produces is_fatal=False and skips callback."""
        engine = CombatEngine()
        fatal_events: list[DamageEventResult] = []
        engine.set_fatal_damage_hook(lambda res, atk, dfn: fatal_events.append(res))

        attacker = CombatActor(1, "Attacker", FiveElements.KIM, base_attack=100.0)
        defender = CombatActor(2, "Defender", FiveElements.MOC, current_hp=50.0, max_hp=50.0)
        engine.register_actor(attacker)
        engine.register_actor(defender)

        # Strike 1: Lethal blow (50 -> 0)
        res1 = engine.calculate_damage(1, 2, 100.0, FiveElements.KIM, current_timestamp_ms=1000)
        assert res1.is_fatal is True and defender.current_hp == 0.0 and len(fatal_events) == 1

        # Strike 2: Corpse overkill
        res2 = engine.calculate_damage(1, 2, 100.0, FiveElements.KIM, current_timestamp_ms=1010)
        assert res2.is_fatal is False and defender.current_hp == 0.0 and len(fatal_events) == 1

        # Strike 3: Heavy overkill on corpse
        res3 = engine.calculate_damage(1, 2, 9999.0, FiveElements.KIM, current_timestamp_ms=1020)
        assert res3.is_fatal is False and len(fatal_events) == 1

    def test_combat_corpse_overkill_does_not_duplicate_monster_exp(
        self, progression_service: LevelProgressionService
    ) -> None:
        """Striking a dead monster corpse does not award duplicate progression EXP."""
        engine = CombatEngine()
        engine.attach_progression_service(progression_service)

        pid = "p_corpse_slayer"
        progression_service.set_player_state(pid, level=50, current_exp=0)
        hero = CombatActor(10, "Hero", FiveElements.KIM, base_attack=200.0, is_player=True, player_id=pid)
        goblin = CombatActor(20, "Goblin", FiveElements.MOC, current_hp=20.0, max_hp=20.0, is_player=False, level=50)
        engine.register_actor(hero)
        engine.register_actor(goblin)

        res1 = engine.calculate_damage(10, 20, 200.0, FiveElements.KIM, current_timestamp_ms=1000)
        assert res1.is_fatal is True
        exp_after_kill = progression_service.get_level_info(pid).current_exp
        assert exp_after_kill > 0

        res2 = engine.calculate_damage(10, 20, 200.0, FiveElements.KIM, current_timestamp_ms=1010)
        assert res2.is_fatal is False
        assert progression_service.get_level_info(pid).current_exp == exp_after_kill

    def test_combat_channeled_barrage_on_dead_player_does_not_multiply_death_penalty(
        self, progression_service: LevelProgressionService
    ) -> None:
        """Multi-hit barrage on player applies death penalty exactly once, not per projectile."""
        engine = CombatEngine()
        engine.attach_progression_service(progression_service)

        pid = "p_victim_barrage"
        d95 = progression_service.get_delta_exp(95)
        progression_service.set_player_state(pid, level=95, current_exp=d95, deaths_count=0)

        boss = CombatActor(30, "Boss", FiveElements.HOA, base_attack=9999.0, is_player=False, level=95)
        player = CombatActor(31, "Victim", FiveElements.KIM, current_hp=100.0, max_hp=100.0, is_player=True, player_id=pid)
        engine.register_actor(boss)
        engine.register_actor(player)

        r1 = engine.calculate_damage(30, 31, 9999.0, FiveElements.HOA, current_timestamp_ms=2000)
        r2 = engine.calculate_damage(30, 31, 9999.0, FiveElements.HOA, current_timestamp_ms=2010)
        r3 = engine.calculate_damage(30, 31, 9999.0, FiveElements.HOA, current_timestamp_ms=2020)

        assert r1.is_fatal is True and not r2.is_fatal and not r3.is_fatal
        state = progression_service.get_level_info(pid)
        assert state.deaths_count == 1
        expected_penalty = int(math.floor(d95 * 0.15))
        assert state.current_exp == d95 - expected_penalty
