from src.gameplay import death as deathModule
from src.gameplay.death import (
    deathConfig, recordDeath, watchDeathRecovery, watchPlayersSeen,
)


class _Orch:
    def __init__(self):
        self.rootTask = None

    def setRootTask(self, context, task):
        self.rootTask = task


def _context(hp=100, coordinate=(100, 100, 7), players=None, death=None):
    context = {
        'ng_pause': False,
        'ng_screenshot': None,
        'ng_statusBar': {'hpPercentage': hp},
        'ng_radar': {'coordinate': list(coordinate) if coordinate else None},
        'ng_tasksOrchestrator': _Orch(),
        'ng_vitals': {'coordinate': [100, 100, 7]},
        'ng_cave': {'waypoints': {'items': [], 'currentIndex': 0}},
        'ng_supplies': {'rules': []},
        'gameWindow': {'players': players or []},
        'allies': [],
        'enemies': [],
    }
    if death is not None:
        context['ng_death'] = death
    return context


def _patch(mocker):
    logSpy = mocker.patch.object(deathModule, 'logEvent')
    mocker.patch.object(deathModule, 'saveEventScreenshot')
    resumeSpy = mocker.patch.object(
        deathModule, 'resumeAtNearestWaypoint', side_effect=lambda c: c)
    return logSpy, resumeSpy


def test_players_seen_marks_timestamp(mocker):
    _patch(mocker)
    context = _context(players=[{'name': 'Pk Killa'}])
    watchPlayersSeen(context, now=1000)
    assert context['ng_deathWatch']['playersSeenAt'] == 1000


def test_allies_do_not_mark_timestamp(mocker):
    _patch(mocker)
    context = _context(players=[{'name': 'Buddy'}])
    context['allies'] = ['Buddy']
    watchPlayersSeen(context, now=1000)
    assert 'ng_deathWatch' not in context


def test_monster_death_does_not_pause(mocker):
    logSpy, _ = _patch(mocker)
    context = _context(hp=0)

    recordDeath(context, now=2000)

    assert context['ng_pause'] is False
    assert context['ng_death']['killer'] == 'monster'
    assert context['ng_death']['coordinate'] == [100, 100, 7]
    assert logSpy.call_args[0][0] == 'deathByMonster'


def test_recent_player_sighting_means_pk(mocker):
    logSpy, _ = _patch(mocker)
    context = _context(hp=0)
    context['ng_deathWatch'] = {'playersSeenAt': 1990}

    recordDeath(context, now=2000)

    assert context['ng_pause'] is True
    assert context['ng_death']['killer'] == 'player'
    assert logSpy.call_args[0][0] == 'deathByPlayer'


def test_old_player_sighting_still_monster(mocker):
    logSpy, _ = _patch(mocker)
    context = _context(hp=0)
    context['ng_deathWatch'] = {'playersSeenAt': 1900}

    recordDeath(context, now=2000)

    assert logSpy.call_args[0][0] == 'deathByMonster'


def test_record_death_is_idempotent(mocker):
    logSpy, _ = _patch(mocker)
    context = _context(hp=0)
    recordDeath(context, now=2000)
    recordDeath(context, now=2001)

    assert logSpy.call_count == 1


def test_recovery_waits_without_coordinate(mocker):
    logSpy, _ = _patch(mocker)
    context = _context(coordinate=None, death={
        'killer': 'monster', 'coordinate': [100, 100, 7], 'at': 1000,
    })

    watchDeathRecovery(context, now=1010)

    assert context['ng_tasksOrchestrator'].rootTask is None
    assert logSpy.call_count == 0


def test_recovery_waits_while_still_dead(mocker):
    logSpy, _ = _patch(mocker)
    context = _context(hp=0, death={
        'killer': 'monster', 'coordinate': [100, 100, 7], 'at': 1000,
    })

    watchDeathRecovery(context, now=1010)

    assert context['ng_tasksOrchestrator'].rootTask is None


def test_recovery_starts_corpse_run(mocker):
    logSpy, _ = _patch(mocker)
    context = _context(death={
        'killer': 'monster', 'coordinate': [100, 100, 7], 'at': 1000,
    })

    watchDeathRecovery(context, now=1010)

    root = context['ng_tasksOrchestrator'].rootTask
    assert root is not None and root.name == 'lootCorpse'
    assert context['ng_death']['recoveryStarted'] is True
    assert logSpy.call_args[0][0] == 'deathRecoveryStarted'


def test_recovery_finishes_after_corpse_task(mocker):
    logSpy, resumeSpy = _patch(mocker)
    death = {
        'killer': 'monster', 'coordinate': [100, 100, 7], 'at': 1000,
        'recoveryStarted': True,
    }
    context = _context(death=death)
    context['ng_tasksOrchestrator'].rootTask = type(
        'Task', (), {'status': 'completed'})()

    watchDeathRecovery(context, now=1010)

    assert 'ng_death' not in context
    assert resumeSpy.call_count == 1
    kinds = [call[0][0] for call in logSpy.call_args_list]
    assert 'deathRecovered' in kinds


def test_recovery_without_death_point_skips_corpse(mocker):
    logSpy, _ = _patch(mocker)
    context = _context(death={
        'killer': 'monster', 'coordinate': None, 'at': 1000,
    })

    watchDeathRecovery(context, now=1010)

    assert 'ng_death' not in context
    kinds = [call[0][0] for call in logSpy.call_args_list]
    assert 'deathRecoverySkipped' in kinds


def test_recovery_timeout_gives_up(mocker):
    logSpy, _ = _patch(mocker)
    context = _context(death={
        'killer': 'monster', 'coordinate': [100, 100, 7], 'at': 1000,
        'recoveryStarted': True,
    })

    watchDeathRecovery(context, now=1000 + 9999)

    assert 'ng_death' not in context
    kinds = [call[0][0] for call in logSpy.call_args_list]
    assert 'deathRecoveryFailed' in kinds


def test_pk_death_waits_for_human(mocker):
    logSpy, _ = _patch(mocker)
    context = _context(death={
        'killer': 'player', 'coordinate': [100, 100, 7], 'at': 1000,
    })
    context['ng_pause'] = True

    watchDeathRecovery(context, now=1010)

    assert 'ng_death' in context
    assert logSpy.call_count == 0


def test_pk_death_acknowledged_after_manual_resume(mocker):
    logSpy, _ = _patch(mocker)
    context = _context(death={
        'killer': 'player', 'coordinate': [100, 100, 7], 'at': 1000,
    })

    watchDeathRecovery(context, now=1010)

    assert 'ng_death' not in context
    kinds = [call[0][0] for call in logSpy.call_args_list]
    assert 'deathAcknowledged' in kinds


def _withHealing(context):
    context['healing'] = {'potions': {
        'firstHealthPotion': {'slot': 1},
        'firstManaPotion': {'slot': 2},
    }}
    return context


def test_low_potions_trigger_emergency_refill(mocker):
    logSpy, _ = _patch(mocker)
    mocker.patch.object(deathModule, 'getSlotCount', return_value=0)
    context = _withHealing(_context(death={
        'killer': 'monster', 'coordinate': [100, 100, 7], 'at': 1000,
    }))
    context['ng_supplies'] = {'rules': [{'enabled': True, 'label': 'shop'}]}
    context['ng_cave']['waypoints']['items'] = [
        {'label': 'hunt'}, {'label': 'shop'},
    ]

    watchDeathRecovery(context, now=1010)

    death = context['ng_death']
    assert death['refillStarted'] is True
    assert death['refillCap'] == deathModule.EMERGENCY_REFILL_CAP
    assert context['ng_cave']['waypoints']['currentIndex'] == 1
    assert context['ng_tasksOrchestrator'].rootTask is None
    assert logSpy.call_args[0][0] == 'deathRefillStarted'


def test_enough_potions_skip_refill(mocker):
    logSpy, _ = _patch(mocker)
    mocker.patch.object(deathModule, 'getSlotCount', return_value=80)
    context = _withHealing(_context(death={
        'killer': 'monster', 'coordinate': [100, 100, 7], 'at': 1000,
    }))

    watchDeathRecovery(context, now=1010)

    # Kein Nachkauf noetig — Bergung startet sofort.
    assert context['ng_death']['refillDone'] is True
    assert context['ng_tasksOrchestrator'].rootTask is not None
    assert logSpy.call_args[0][0] == 'deathRecoveryStarted'


def test_missing_refill_label_skips_to_corpse(mocker):
    logSpy, _ = _patch(mocker)
    mocker.patch.object(deathModule, 'getSlotCount', return_value=0)
    context = _withHealing(_context(death={
        'killer': 'monster', 'coordinate': [100, 100, 7], 'at': 1000,
    }))

    watchDeathRecovery(context, now=1010)

    assert context['ng_death']['refillDone'] is True
    assert 'refillStarted' not in context['ng_death']
    assert context['ng_tasksOrchestrator'].rootTask is not None


def test_refill_completes_and_corpse_run_starts(mocker):
    logSpy, _ = _patch(mocker)
    mocker.patch.object(deathModule, 'getSlotCount', return_value=30)
    context = _withHealing(_context(death={
        'killer': 'monster', 'coordinate': [100, 100, 7], 'at': 1000,
        'refillStarted': True, 'refillAt': 1005, 'refillCap': 30,
    }))

    watchDeathRecovery(context, now=1010)

    death = context['ng_death']
    assert death['refillDone'] is True
    assert 'refillCap' not in death
    assert context['ng_tasksOrchestrator'].rootTask is not None
    kinds = [call[0][0] for call in logSpy.call_args_list]
    assert 'deathRefillDone' in kinds
    assert 'deathRecoveryStarted' in kinds


def test_refill_timeout_still_recovers(mocker):
    logSpy, _ = _patch(mocker)
    mocker.patch.object(deathModule, 'getSlotCount', return_value=0)
    context = _withHealing(_context(death={
        'killer': 'monster', 'coordinate': [100, 100, 7], 'at': 1000,
        'refillStarted': True, 'refillAt': 1005, 'refillCap': 30,
    }))

    watchDeathRecovery(context, now=1005 + 400)

    death = context['ng_death']
    assert death['refillDone'] is True
    assert 'refillCap' not in death
    assert context['ng_tasksOrchestrator'].rootTask is not None
    kinds = [call[0][0] for call in logSpy.call_args_list]
    assert 'deathRefillFailed' in kinds


def test_refill_waits_while_shopping(mocker):
    logSpy, _ = _patch(mocker)
    mocker.patch.object(deathModule, 'getSlotCount', return_value=0)
    context = _withHealing(_context(death={
        'killer': 'monster', 'coordinate': [100, 100, 7], 'at': 1000,
        'refillStarted': True, 'refillAt': 1005, 'refillCap': 30,
    }))

    watchDeathRecovery(context, now=1060)

    assert 'refillDone' not in context['ng_death']
    assert context['ng_tasksOrchestrator'].rootTask is None
    assert logSpy.call_count == 0


# ------------------------------------------------------------- Konfiguration
def test_death_config_defaults_and_override():
    assert deathConfig({})['refillCap'] == 30
    merged = deathConfig({'ng_deathConfig': {'refillCap': 50, 'quatsch': 1}})
    assert merged['refillCap'] == 50
    assert merged['minPotions'] == 10
    assert 'quatsch' not in merged


def test_disabled_recovery_pauses_on_monster_death(mocker):
    _patch(mocker)
    context = _context(hp=0)
    context['ng_deathConfig'] = {'enabled': False}

    recordDeath(context, now=2000)

    assert context['ng_pause'] is True


def test_no_pause_on_pk_when_configured(mocker):
    logSpy, _ = _patch(mocker)
    context = _context(hp=0)
    context['ng_deathWatch'] = {'playersSeenAt': 1995}
    context['ng_deathConfig'] = {'pauseOnPlayerKill': False}

    recordDeath(context, now=2000)

    assert context['ng_pause'] is False
    assert logSpy.call_args[0][0] == 'deathByPlayer'


def test_disabled_recovery_acknowledged_on_manual_resume(mocker):
    logSpy, _ = _patch(mocker)
    context = _context(death={
        'killer': 'monster', 'coordinate': [100, 100, 7], 'at': 1000,
    })
    context['ng_deathConfig'] = {'enabled': False}

    watchDeathRecovery(context, now=1010)

    assert 'ng_death' not in context
    kinds = [call[0][0] for call in logSpy.call_args_list]
    assert 'deathAcknowledged' in kinds


def test_custom_killer_window(mocker):
    logSpy, _ = _patch(mocker)
    context = _context(hp=0)
    context['ng_deathWatch'] = {'playersSeenAt': 1950}
    context['ng_deathConfig'] = {'killerWindowSeconds': 10}

    recordDeath(context, now=2000)

    # 50s alt, Fenster nur 10s — zaehlt als Monster-Kill.
    assert logSpy.call_args[0][0] == 'deathByMonster'


def test_custom_refill_values(mocker):
    logSpy, _ = _patch(mocker)
    mocker.patch.object(deathModule, 'getSlotCount', return_value=15)
    context = _withHealing(_context(death={
        'killer': 'monster', 'coordinate': [100, 100, 7], 'at': 1000,
    }))
    context['ng_deathConfig'] = {'minPotions': 20, 'refillCap': 5}
    context['ng_supplies'] = {'rules': [{'enabled': True, 'label': 'shop'}]}
    context['ng_cave']['waypoints']['items'] = [{'label': 'shop'}]

    watchDeathRecovery(context, now=1010)

    # 15 < 20 → Nachkauf mit Kappung 5 statt 30.
    assert context['ng_death']['refillCap'] == 5
    assert logSpy.call_args[0][1]['cap'] == 5


def test_custom_recovery_timeout(mocker):
    logSpy, _ = _patch(mocker)
    context = _context(death={
        'killer': 'monster', 'coordinate': [100, 100, 7], 'at': 1000,
        'recoveryStarted': True, 'refillDone': True,
    })
    context['ng_deathConfig'] = {'recoveryTimeoutMinutes': 1}

    watchDeathRecovery(context, now=1000 + 90)

    assert 'ng_death' not in context
    kinds = [call[0][0] for call in logSpy.call_args_list]
    assert 'deathRecoveryFailed' in kinds
