"""Tests for combat encounter and state machine."""

import time

import pytest

from mudlib.combat.encounter import CombatEncounter, CombatState, ResolveResult
from mudlib.combat.moves import CombatMove
from mudlib.entity import Entity


@pytest.fixture
def attacker():
    return Entity(name="Goku", x=0, y=0, pl=100.0, stamina=50.0)


@pytest.fixture
def defender():
    return Entity(name="Vegeta", x=0, y=0, pl=100.0, stamina=50.0)


@pytest.fixture
def punch():
    return CombatMove(
        name="punch right",
        move_type="attack",
        stamina_cost=5.0,
        hit_time_ms=800,
        damage_pct=0.15,
        countered_by=["dodge left", "parry high"],
    )


@pytest.fixture
def dodge():
    return CombatMove(
        name="dodge left",
        move_type="defense",
        stamina_cost=3.0,
        active_ms=800,
        recovery_ms=2700,
    )


@pytest.fixture
def wrong_dodge():
    return CombatMove(
        name="dodge right",
        move_type="defense",
        stamina_cost=3.0,
        active_ms=800,
        recovery_ms=2700,
    )


@pytest.fixture
def sweep():
    return CombatMove(
        name="sweep",
        move_type="attack",
        stamina_cost=8.0,
        hit_time_ms=600,
        damage_pct=0.20,
        countered_by=["jump"],
    )


def test_combat_encounter_initial_state(attacker, defender):
    """Test encounter starts in IDLE state."""
    encounter = CombatEncounter(attacker=attacker, defender=defender)
    assert encounter.state == CombatState.IDLE
    assert encounter.current_move is None
    assert encounter.pending_defense is None
    assert encounter.move_started_at == 0.0


def test_attack_transitions_to_pending(attacker, defender, punch):
    """Test attacking transitions to PENDING state."""
    encounter = CombatEncounter(attacker=attacker, defender=defender)
    encounter.attack(punch)

    assert encounter.state == CombatState.PENDING
    assert encounter.current_move is punch
    assert encounter.move_started_at > 0.0


def test_attack_applies_stamina_cost(attacker, defender, punch):
    """Test attacking costs stamina."""
    encounter = CombatEncounter(attacker=attacker, defender=defender)
    initial_stamina = attacker.stamina
    encounter.attack(punch)

    assert attacker.stamina == initial_stamina - punch.stamina_cost


def test_defend_records_pending_defense(attacker, defender, punch, dodge):
    """Test defend records the defense move and activates it."""
    encounter = CombatEncounter(attacker=attacker, defender=defender)
    encounter.attack(punch)
    encounter.defend(dodge)

    assert encounter.pending_defense is dodge
    assert encounter.defense_activated_at is not None


def test_defend_does_not_deduct_stamina(attacker, defender, punch, dodge):
    """Test encounter.defend() does not deduct stamina (command layer does)."""
    encounter = CombatEncounter(attacker=attacker, defender=defender)
    encounter.attack(punch)

    initial_stamina = defender.stamina
    encounter.defend(dodge)

    assert defender.stamina == initial_stamina


def test_tick_pending_to_resolve(attacker, defender, punch):
    """Test tick advances from PENDING to RESOLVE after hit time."""
    encounter = CombatEncounter(attacker=attacker, defender=defender)
    encounter.attack(punch)

    # Wait for hit_time_ms (800ms)
    time.sleep(0.85)
    now = time.monotonic()
    encounter.tick(now)

    assert encounter.state == CombatState.RESOLVE


def test_resolve_successful_counter(attacker, defender, punch, dodge):
    """Test resolve with successful counter does no damage."""
    encounter = CombatEncounter(attacker=attacker, defender=defender)
    encounter.attack(punch)
    encounter.defend(dodge)

    initial_pl = defender.pl
    result = encounter.resolve()

    assert isinstance(result, ResolveResult)
    assert defender.pl == initial_pl
    assert result.damage == 0.0
    assert result.countered is True
    assert "counter" in result.resolve_template.lower()
    assert encounter.state == CombatState.IDLE
    assert result.combat_ended is False


def test_resolve_failed_counter(attacker, defender, punch, wrong_dodge):
    """Test resolve with wrong defense move deals damage."""
    encounter = CombatEncounter(attacker=attacker, defender=defender)
    encounter.attack(punch)
    encounter.defend(wrong_dodge)

    initial_pl = defender.pl
    result = encounter.resolve()

    expected_damage = attacker.pl * punch.damage_pct
    assert defender.pl == initial_pl - expected_damage
    assert result.damage == expected_damage
    assert result.countered is False
    assert "hit" in result.resolve_template.lower()
    assert encounter.state == CombatState.IDLE
    assert result.combat_ended is False


def test_resolve_no_defense(attacker, defender, punch):
    """Test resolve with no defense deals increased damage."""
    encounter = CombatEncounter(attacker=attacker, defender=defender)
    encounter.attack(punch)

    initial_pl = defender.pl
    result = encounter.resolve()

    # No defense = 1.5x damage
    expected_damage = attacker.pl * punch.damage_pct * 1.5
    assert defender.pl == initial_pl - expected_damage
    assert result.damage == expected_damage
    assert result.countered is False
    # Template should indicate a hit
    assert result.resolve_template != ""
    assert encounter.state == CombatState.IDLE
    assert result.combat_ended is False


def test_resolve_clears_pending_defense(attacker, defender, punch, dodge):
    """Test resolve clears pending defense."""
    encounter = CombatEncounter(attacker=attacker, defender=defender)
    encounter.attack(punch)
    encounter.defend(dodge)

    encounter.resolve()

    assert encounter.pending_defense is None
    assert encounter.current_move is None


def test_full_state_machine_cycle(attacker, defender, punch):
    """Test complete state machine cycle from IDLE to IDLE."""
    encounter = CombatEncounter(attacker=attacker, defender=defender)

    # IDLE → PENDING
    encounter.attack(punch)
    assert encounter.state == CombatState.PENDING

    # PENDING → RESOLVE (after hit_time_ms)
    time.sleep(0.85)
    encounter.tick(time.monotonic())
    assert encounter.state == CombatState.RESOLVE

    # RESOLVE → IDLE
    encounter.resolve()
    assert encounter.state == CombatState.IDLE


def test_combat_state_enum():
    """Test CombatState enum values."""
    assert CombatState.IDLE.value == "idle"
    assert CombatState.PENDING.value == "pending"
    assert CombatState.RESOLVE.value == "resolve"


def test_resolve_knockout_does_not_end_combat(attacker, defender, punch):
    """KO should not end combat by itself."""
    encounter = CombatEncounter(attacker=attacker, defender=defender)

    # Set defender to low PL so attack will knock them out
    defender.pl = 10.0

    encounter.attack(punch)
    result = encounter.resolve()

    assert defender.pl <= 0
    assert result.combat_ended is False
    assert result.damage > 0


def test_resolve_exhaustion_does_not_end_combat(attacker, defender, punch):
    """Exhaustion should not end combat by itself."""
    encounter = CombatEncounter(attacker=attacker, defender=defender)

    # Set attacker stamina to exactly the cost so attack depletes it
    attacker.stamina = punch.stamina_cost

    encounter.attack(punch)
    result = encounter.resolve()

    assert attacker.stamina <= 0
    assert result.combat_ended is False


def test_resolve_exhausted_defender_does_not_end_combat(attacker, defender, punch):
    """Exhausted defender still does not auto-end encounter."""
    encounter = CombatEncounter(attacker=attacker, defender=defender)
    defender.stamina = 0.0

    encounter.attack(punch)
    result = encounter.resolve()

    assert defender.stamina <= 0
    assert result.combat_ended is False


def test_resolve_returns_combat_continues_normally(attacker, defender, punch):
    """Test resolve returns combat_ended=False when both have resources."""
    encounter = CombatEncounter(attacker=attacker, defender=defender)
    encounter.attack(punch)

    result = encounter.resolve()

    assert attacker.stamina > 0
    assert defender.pl > 0
    assert result.combat_ended is False


def test_resolve_template_not_empty(attacker, defender, punch):
    """Test resolve returns a template."""
    encounter = CombatEncounter(attacker=attacker, defender=defender)
    encounter.attack(punch)
    result = encounter.resolve()

    assert result.resolve_template != ""


def test_resolve_template_uses_pov_tags(attacker, defender, punch):
    """Test resolve template uses POV tags."""
    encounter = CombatEncounter(attacker=attacker, defender=defender)
    encounter.attack(punch)
    result = encounter.resolve()

    # Template should contain POV tags like {attacker} or {defender}
    assert "{" in result.resolve_template


def test_resolve_counter_template_indicates_counter(attacker, defender, punch, dodge):
    """Test counter template indicates a successful counter."""
    encounter = CombatEncounter(attacker=attacker, defender=defender)
    encounter.attack(punch)
    encounter.defend(dodge)
    result = encounter.resolve()

    assert "counter" in result.resolve_template.lower()


# --- Attack switching (feint) tests ---


def test_switch_attack_during_pending(attacker, defender, punch, sweep):
    """Test attack during PENDING replaces move and restarts timer."""
    encounter = CombatEncounter(attacker=attacker, defender=defender)
    encounter.attack(punch)
    original_start = encounter.move_started_at

    assert encounter.state == CombatState.PENDING

    # Switch to sweep during pending
    time.sleep(0.1)  # Small delay to ensure timer would differ
    encounter.attack(sweep)

    assert encounter.current_move is sweep
    assert encounter.state == CombatState.PENDING
    # Timer should restart on switch
    assert encounter.move_started_at > original_start


def test_switch_refunds_old_stamina(attacker, defender, punch, sweep):
    """Test switching refunds old move's cost and charges new."""
    encounter = CombatEncounter(attacker=attacker, defender=defender)
    initial_stamina = attacker.stamina

    encounter.attack(punch)
    assert attacker.stamina == initial_stamina - punch.stamina_cost

    # Switch to sweep — should refund punch, charge sweep
    encounter.attack(sweep)
    assert attacker.stamina == initial_stamina - sweep.stamina_cost


def test_switch_stamina_clamped_to_max(attacker, defender, punch, sweep):
    """Test stamina refund clamped to max_stamina."""
    encounter = CombatEncounter(attacker=attacker, defender=defender)

    # Set stamina so refund would exceed max
    attacker.stamina = attacker.max_stamina
    encounter.attack(punch)

    # Manually set stamina high so refund would exceed max
    attacker.stamina = attacker.max_stamina - 1
    encounter.attack(sweep)

    # Refund of punch (5) would push past max, should clamp
    assert attacker.stamina <= attacker.max_stamina


def test_resolve_uses_final_move(attacker, defender, punch, sweep):
    """Test resolve uses the switched-to move, not the original."""
    encounter = CombatEncounter(attacker=attacker, defender=defender)
    encounter.attack(punch)
    encounter.attack(sweep)  # Switch

    initial_pl = defender.pl
    result = encounter.resolve()

    # Sweep does 0.20 * 1.5 = 0.30 of PL (no defense)
    expected_damage = attacker.pl * sweep.damage_pct * 1.5
    assert defender.pl == initial_pl - expected_damage
    assert result.resolve_template != ""


# --- last_action_at (last landed damage) tracking tests ---


def test_last_action_at_not_updated_on_attack(attacker, defender, punch):
    """Attack startup should not reset timeout until damage lands."""
    encounter = CombatEncounter(attacker=attacker, defender=defender)
    assert encounter.last_action_at == 0.0

    encounter.attack(punch)

    assert encounter.last_action_at == 0.0


def test_last_action_at_not_updated_on_defend(attacker, defender, punch, dodge):
    """Defense input should not reset timeout without landed damage."""
    encounter = CombatEncounter(attacker=attacker, defender=defender)
    encounter.attack(punch)
    assert encounter.last_action_at == 0.0
    encounter.defend(dodge)

    assert encounter.last_action_at == 0.0


def test_last_action_at_updates_when_damage_lands(attacker, defender, punch):
    """Landed damage should refresh timeout timestamp."""
    encounter = CombatEncounter(attacker=attacker, defender=defender)
    assert encounter.last_action_at == 0.0
    encounter.attack(punch)
    before = time.monotonic()
    encounter.resolve()
    assert encounter.last_action_at >= before


def test_last_action_at_unchanged_when_attack_is_countered(
    attacker, defender, punch, dodge
):
    """No damage (successful counter) should not refresh timeout timestamp."""
    encounter = CombatEncounter(
        attacker=attacker, defender=defender, last_action_at=10.0
    )
    encounter.attack(punch)
    encounter.defend(dodge)
    encounter.resolve()
    assert encounter.last_action_at == 10.0


# --- Defense active/recovery window tests ---


def test_defense_expired_before_resolve(attacker, defender):
    """Defense activates, active_ms passes, attack resolves.

    Defense should NOT counter (hit lands).
    """
    # Create attack with 800ms hit time
    quick_attack = CombatMove(
        name="quick punch",
        move_type="attack",
        stamina_cost=5.0,
        hit_time_ms=800,
        damage_pct=0.15,
        countered_by=["quick dodge"],
    )

    # Create defense with short active window (200ms)
    quick_dodge = CombatMove(
        name="quick dodge",
        move_type="defense",
        stamina_cost=3.0,
        active_ms=200,
        recovery_ms=300,
    )

    encounter = CombatEncounter(attacker=attacker, defender=defender)
    encounter.attack(quick_attack)
    encounter.defend(quick_dodge)

    # Wait for defense to expire (200ms) but before attack lands (800ms)
    time.sleep(0.31)
    encounter.tick(time.monotonic())

    # Now resolve — defense should be expired
    initial_pl = defender.pl
    result = encounter.resolve()

    # Defense expired, so attack should hit for full damage
    expected_damage = attacker.pl * quick_attack.damage_pct * 1.5
    assert defender.pl == initial_pl - expected_damage
    assert result.damage == expected_damage
    assert result.countered is False


def test_defense_active_during_resolve(attacker, defender):
    """Defense activates within active_ms of resolve — defense SHOULD counter."""
    # Create attack with 800ms hit time
    quick_attack = CombatMove(
        name="quick punch",
        move_type="attack",
        stamina_cost=5.0,
        hit_time_ms=800,
        damage_pct=0.15,
        countered_by=["quick dodge"],
    )

    # Create defense with long active window (1000ms)
    quick_dodge = CombatMove(
        name="quick dodge",
        move_type="defense",
        stamina_cost=3.0,
        active_ms=1000,
        recovery_ms=300,
    )

    encounter = CombatEncounter(attacker=attacker, defender=defender)
    encounter.attack(quick_attack)
    encounter.defend(quick_dodge)

    # Immediately resolve — defense is still active
    initial_pl = defender.pl
    result = encounter.resolve()

    # Defense is active, should counter successfully
    assert defender.pl == initial_pl
    assert result.damage == 0.0
    assert result.countered is True


def test_defense_queuing_during_recovery(attacker, defender):
    """defend() while in recovery should set queued_defense, not pending_defense."""
    quick_dodge = CombatMove(
        name="quick dodge",
        move_type="defense",
        stamina_cost=3.0,
        active_ms=200,
        recovery_ms=300,
    )

    encounter = CombatEncounter(attacker=attacker, defender=defender)

    # First defend — activates immediately
    encounter.defend(quick_dodge)
    assert encounter.pending_defense is quick_dodge
    assert encounter.defense_activated_at is not None

    # Wait for defense to expire and enter recovery
    time.sleep(0.31)
    encounter.tick(time.monotonic())

    assert encounter.pending_defense is None
    assert encounter.defense_recovery_until is not None

    # Try to defend during recovery — should queue
    second_dodge = CombatMove(
        name="second dodge",
        move_type="defense",
        stamina_cost=3.0,
        active_ms=200,
        recovery_ms=300,
    )
    encounter.defend(second_dodge)

    # Should be queued, not pending
    assert encounter.pending_defense is None
    assert encounter.queued_defense is second_dodge


def test_queued_defense_activates_after_recovery(attacker, defender):
    """After recovery_ms passes, tick() should activate the queued defense."""
    quick_dodge = CombatMove(
        name="quick dodge",
        move_type="defense",
        stamina_cost=3.0,
        active_ms=200,
        recovery_ms=300,
    )

    encounter = CombatEncounter(attacker=attacker, defender=defender)

    # First defend
    encounter.defend(quick_dodge)

    # Wait for defense to expire
    time.sleep(0.31)
    encounter.tick(time.monotonic())

    # Queue second defense during recovery
    second_dodge = CombatMove(
        name="second dodge",
        move_type="defense",
        stamina_cost=3.0,
        active_ms=200,
        recovery_ms=300,
    )
    encounter.defend(second_dodge)

    assert encounter.queued_defense is second_dodge
    assert encounter.pending_defense is None

    # Wait for recovery to finish
    time.sleep(0.31)
    encounter.tick(time.monotonic())

    # Queued defense should now be active
    assert encounter.pending_defense is second_dodge
    assert encounter.queued_defense is None
    assert encounter.defense_activated_at is not None


def test_recovery_persists_after_resolve(attacker, defender):
    """resolve() should NOT clear defense_recovery_until.

    Recovery carries across attacks.
    """
    quick_attack = CombatMove(
        name="quick punch",
        move_type="attack",
        stamina_cost=5.0,
        hit_time_ms=800,
        damage_pct=0.15,
        countered_by=["quick dodge"],
    )

    quick_dodge = CombatMove(
        name="quick dodge",
        move_type="defense",
        stamina_cost=3.0,
        active_ms=200,
        recovery_ms=300,
    )

    encounter = CombatEncounter(attacker=attacker, defender=defender)

    # Defend, then wait for defense to expire and enter recovery
    encounter.defend(quick_dodge)
    time.sleep(0.31)
    encounter.tick(time.monotonic())

    recovery_until = encounter.defense_recovery_until
    assert recovery_until is not None

    # Attack and resolve during recovery period
    encounter.attack(quick_attack)
    encounter.resolve()

    # Recovery should persist after resolve
    assert encounter.defense_recovery_until == recovery_until


def test_attack_switch_restarts_timer(attacker, defender):
    """attack(), then attack() again with different move.

    move_started_at should reset.
    """
    first_attack = CombatMove(
        name="first punch",
        move_type="attack",
        stamina_cost=5.0,
        hit_time_ms=800,
        damage_pct=0.15,
        countered_by=[],
    )

    second_attack = CombatMove(
        name="second punch",
        move_type="attack",
        stamina_cost=5.0,
        hit_time_ms=800,
        damage_pct=0.15,
        countered_by=[],
    )

    encounter = CombatEncounter(attacker=attacker, defender=defender)
    encounter.attack(first_attack)
    first_start = encounter.move_started_at

    # Small delay to ensure time difference
    time.sleep(0.1)

    # Switch to second attack
    encounter.attack(second_attack)
    second_start = encounter.move_started_at

    # Timer should have restarted
    assert second_start > first_start
    assert encounter.current_move is second_attack
