"""Test the 'choose an account to attack' view."""

from datetime import timedelta

import pytest
from asgiref.sync import sync_to_async
from django.contrib.auth.models import User
from django.test.client import AsyncClient  # type: ignore
from django.utils import timezone

from promptgame.gameui.matchmaking import get_or_update_targets
from promptgame.gameui.models import Account
from promptgame.gameui.tiers import valid_tier
from promptgame.gameui.user_account_tools import AccountDefaults
from promptgame.tests.helpers import (
    async_cooldown_active_nowarn,
    async_login,
    async_make_account,
    dummy_attack,
    get_content,
)


@pytest.fixture
def default_balance() -> int:
    return AccountDefaults.generate().balance


@pytest.fixture()
def narrow_matchmaking_settings(settings, default_balance: int) -> None:
    # we can return accounts at any rank
    settings.MATCHMAKING_RANK_FRACTION = 1.0
    settings.MATCHMAKING_MAX_ACCOUNTS = 5

    # enable tiers at < 500, >= 500 with protection down one tier
    cutoff = default_balance // 2
    settings.TIERS = [
        {
            "title": "Low",
            "emoji": "⭐",
            "range": (-float("inf"), cutoff),
            "description": "≥$0",
            "neighbors": (0, 1),
        },
        {
            "title": "High",
            "emoji": "⭐⭐",
            "range": (cutoff + 1, float("inf")),
            "description": ">$default_balance // 2",
            "neighbors": (1, 1),
        },
    ]


@pytest.mark.django_db
@pytest.mark.asyncio
async def test_get_targets(
    narrow_matchmaking_settings: None,
    async_client: AsyncClient,
    mock_streaming_llm_output_granted: None,
    default_balance: int,
) -> None:
    """Make sure that get_target_accounts() obeys unlocking logic correctly."""
    # delete all existing accounts
    await Account.objects.all().adelete()

    my_account = await async_make_account()
    my_account.unlock_at = timezone.now() - timedelta(days=2)
    await my_account.asave()
    await my_account.arefresh_from_db()
    assert not my_account.locked
    assert timezone.is_aware(my_account.unlock_at)
    assert my_account.balance == default_balance  # sanity check

    # create some other accounts
    recently_unlocked = await async_make_account()
    recently_unlocked.unlock_at = timezone.now() - timedelta(minutes=5)
    recently_unlocked.balance = my_account.balance
    await recently_unlocked.asave()
    await recently_unlocked.arefresh_from_db()
    assert not recently_unlocked.locked

    old_unlocked = await async_make_account()
    old_unlocked.unlock_at = timezone.now() - timedelta(days=2)
    old_unlocked.balance = my_account.balance
    await old_unlocked.asave()
    await old_unlocked.arefresh_from_db()
    assert not old_unlocked.locked

    soon_to_be_unlocked = await async_make_account()
    soon_to_be_unlocked.unlock_at = timezone.now() + timedelta(minutes=5)
    soon_to_be_unlocked.balance = my_account.balance
    await soon_to_be_unlocked.asave()
    await soon_to_be_unlocked.arefresh_from_db()
    assert soon_to_be_unlocked.locked

    eventually_unlocked = await async_make_account()
    eventually_unlocked.unlock_at = timezone.now() + timedelta(days=2)
    eventually_unlocked.balance = my_account.balance
    await eventually_unlocked.asave()
    await eventually_unlocked.arefresh_from_db()
    assert eventually_unlocked.locked

    # make an account in cooldown
    in_cooldown = await async_make_account()
    in_cooldown.unlock_at = timezone.now() - timedelta(days=2)
    await in_cooldown.asave()
    my_account_owner = await User.objects.aget(id=my_account.owner_id)
    await async_login(my_account_owner, async_client)
    content = await get_content(await dummy_attack(async_client, in_cooldown))
    assert "You stole" in content  # attack actually succeeded
    # make sure it's unlocked
    in_cooldown.unlock_at = timezone.now() - timedelta(days=2)
    await in_cooldown.asave()
    await in_cooldown.arefresh_from_db()
    assert not in_cooldown.locked
    assert await async_cooldown_active_nowarn(attacker=my_account, defender=in_cooldown)

    # make an account which is protected
    protected = await async_make_account()
    protected.unlock_at = timezone.now() - timedelta(days=2)
    protected.balance = default_balance // 4
    await protected.asave()
    await protected.arefresh_from_db()
    assert not protected.locked
    assert not valid_tier(
        attacker_balance=my_account.balance, defender_balance=protected.balance
    )

    # make sure this returns the right unlocked accounts
    # (we repeat a few times to reduce flakiness because get_target_accounts()
    # uses randomness from SQLite)
    for _ in range(5):
        targets = await sync_to_async(get_or_update_targets)(my_account)
        target_account_ids = sorted([t.account_id for t in targets])
        assert target_account_ids == sorted(
            [recently_unlocked.account_id, old_unlocked.account_id]
        )

        for account in targets:
            assert account.unlock_at is not None
            assert timezone.is_aware(account.unlock_at)
