from types import SimpleNamespace

import pytest

from src.gameplay import failsafe as failsafe_module
from src.gameplay.failsafe import PlayerFailsafe

NOW = 1_800_000_000


@pytest.fixture
def watchdog(tmp_path, monkeypatch):
    monkeypatch.delenv(failsafe_module.FAILSAFE_ENV, raising=False)
    monkeypatch.delenv(failsafe_module.TRIGGER_SECONDS_ENV, raising=False)
    monkeypatch.setenv(
        'TIBIAPILOT_EVENTS_FILE', str(tmp_path / 'events.jsonl'),
    )
    monkeypatch.setenv('TIBIAPILOT_EVENT_SHOT_DIR', str(tmp_path / 'shots'))
    monkeypatch.setattr(failsafe_module, 'hotkey', lambda *a: None)
    monkeypatch.setattr(failsafe_module, 'press', lambda *a: None)
    return PlayerFailsafe()


def ctx(players=(), paused=False, task=None, allies=(), enemies=()):
    calls = []

    def get_current_task(context):
        return task

    def set_root_task(context, value):
        calls.append(value)
        context['_task'] = value

    normalized = [
        player if isinstance(player, dict) else {'name': player}
        for player in players
    ]
    context = {
        'ng_pause': paused,
        'gameWindow': {'players': normalized},
        'allies': list(allies),
        'enemies': list(enemies),
        'ng_tasksOrchestrator': SimpleNamespace(
            getCurrentTask=get_current_task,
            setRootTask=set_root_task,
        ),
        '_task': task,
        '_calls': calls,
    }
    return context


def test_disabled_passthrough(tmp_path, monkeypatch):
    monkeypatch.setenv(failsafe_module.FAILSAFE_ENV, '0')
    watchdog = PlayerFailsafe()
    context = ctx(players=['GM Adam'])
    for step in range(10):
        context = watchdog.tick(context, NOW + step * 5)
    assert context['ng_pause'] is False


def test_players_must_stay_visible(watchdog):
    context = ctx(players=['Some Knight'])
    context = watchdog.tick(context, NOW)
    assert context['ng_pause'] is False
    context = watchdog.tick(context, NOW + 1)
    assert context['ng_pause'] is False


def test_sustained_players_pause_and_cancel_task(watchdog):
    context = ctx(players=['GM Adam', 'Hunter'], task=object())
    context = watchdog.tick(context, NOW)
    context = watchdog.tick(context, NOW + 3)
    assert context['ng_pause'] is True
    assert context['_calls'] == [None]
    assert context['ng_failsafe']['reason'] == 'players'
    assert context['ng_failsafe']['count'] == 2


def test_failsafe_without_active_task_keeps_orchestrator_untouched(watchdog):
    context = ctx(players=['GM Adam'], task=None)
    context = watchdog.tick(context, NOW)
    context = watchdog.tick(context, NOW + 3)
    assert context['ng_pause'] is True
    assert context['_calls'] == []


def test_timer_resets_when_players_leave(watchdog):
    context = ctx(players=['Some Knight'])
    context = watchdog.tick(context, NOW)
    context['gameWindow']['players'] = []
    context = watchdog.tick(context, NOW + 1)
    context['gameWindow']['players'] = [{'name': 'Some Knight'}]
    context = watchdog.tick(context, NOW + 2)
    context = watchdog.tick(context, NOW + 4)
    # 2 s seit Rueckkehr < 2.5 s Schwelle: noch kein Alarm.
    assert context['ng_pause'] is False


def test_paused_context_never_triggers(watchdog):
    context = ctx(players=['GM Adam'], paused=True)
    for step in range(10):
        context = watchdog.tick(context, NOW + step * 5)
    assert 'ng_failsafe' not in context


def test_env_trigger_seconds(monkeypatch):
    monkeypatch.setenv(failsafe_module.TRIGGER_SECONDS_ENV, '10')
    watchdog = PlayerFailsafe()
    context = ctx(players=['GM Adam'])
    context = watchdog.tick(context, NOW)
    context = watchdog.tick(context, NOW + 5)
    assert context['ng_pause'] is False


def test_allies_do_not_trigger(watchdog):
    context = ctx(players=['Best Buddy'], allies=['best buddy'])
    for step in range(5):
        context = watchdog.tick(context, NOW + step * 3)
    assert context['ng_pause'] is False


def test_enemies_escalate_immediately(watchdog):
    context = ctx(
        players=['Best Buddy', 'Known Killer'],
        allies=['Best Buddy'],
        enemies=['Known Killer'],
        task=object(),
    )
    context = watchdog.tick(context, NOW)
    assert context['ng_pause'] is True
    assert context['ng_failsafe']['reason'] == 'enemy'
    assert context['ng_failsafe']['count'] == 1
    assert context['_calls'] == [None]


def test_mixed_unknown_and_ally_uses_timer(watchdog):
    context = ctx(
        players=['Best Buddy', 'Stranger'], allies=['Best Buddy'])
    context = watchdog.tick(context, NOW)
    assert context['ng_pause'] is False
    context = watchdog.tick(context, NOW + 3)
    assert context['ng_pause'] is True
    assert context['ng_failsafe']['reason'] == 'players'


def test_classify_players_split():
    players = [
        {'name': 'Ally One'}, {'name': 'Enemy One'}, {'name': 'Stranger'},
    ]
    hostile, friendly, unknown = failsafe_module.classifyPlayers(
        players, ['ally one'], ['ENEMY one'])
    assert [p['name'] for p in hostile] == ['Enemy One']
    assert [p['name'] for p in friendly] == ['Ally One']
    assert [p['name'] for p in unknown] == ['Stranger']


def _combatCtx(**kwargs):
    context = ctx(**kwargs)
    context['ng_cave'] = {
        'isAttackingSomeCreature': True,
        'waypoints': {
            'items': [{'label': 'Hunt'}, {'label': 'Depot'}],
            'currentIndex': 0,
            'state': None,
        },
    }
    context['ng_radar'] = {'coordinate': [100, 100, 7]}
    return context


def test_enemy_without_combat_logs_out_immediately(watchdog, mocker):
    hotkeySpy = mocker.spy(failsafe_module, 'hotkey')
    context = ctx(players=['Pker'], enemies=['Pker'])

    watchdog.tick(context, NOW)

    hotkeySpy.assert_called_once_with('ctrl', 'q')
    assert context['ng_pause'] is True


def test_enemy_in_combat_escapes_instead_of_logout(watchdog, mocker):
    hotkeySpy = mocker.spy(failsafe_module, 'hotkey')
    pressSpy = mocker.spy(failsafe_module, 'press')
    context = _combatCtx(players=['Pker'], enemies=['Pker'])

    watchdog.tick(context, NOW)

    hotkeySpy.assert_not_called()
    pressSpy.assert_called_with('esc')
    assert context['ng_pause'] is False
    assert context['ng_cave']['waypoints']['currentIndex'] == 1
    assert context['ng_logoutRetry']['attempts'] == 0


def test_logout_retry_retries_and_pauses_after_max_attempts(watchdog, mocker):
    hotkeySpy = mocker.spy(failsafe_module, 'hotkey')
    context = _combatCtx(players=['Pker'], enemies=['Pker'])
    watchdog.tick(context, NOW)
    assert hotkeySpy.call_count == 0

    for attempt in range(1, 13):
        context['ng_cave']['isAttackingSomeCreature'] = False
        watchdog.tick(context, NOW + attempt * 5)

    assert hotkeySpy.call_count == 12
    assert context['ng_pause'] is True
    assert 'ng_logoutRetry' not in context


def test_logout_retry_detects_successful_logout(watchdog, mocker):
    mocker.spy(failsafe_module, 'hotkey')
    context = _combatCtx(players=['Pker'], enemies=['Pker'])
    watchdog.tick(context, NOW)
    watchdog.tick(context, NOW + 5)
    context['ng_radar']['coordinate'] = None

    watchdog.tick(context, NOW + 10)

    assert context['ng_pause'] is True
    assert 'ng_logoutRetry' not in context
