import warnings
from collections import abc
from dataclasses import dataclass
from typing import List, Optional

from asgiref.sync import sync_to_async
from django.conf import settings
from django.contrib.auth.models import User
from django.http import HttpResponse, StreamingHttpResponse
from django.test.client import AsyncClient  # type: ignore
from django.test.client import Client
from django.urls import reverse
from django.utils import timezone

import promptgame.gameui.redis_utils as ru
from promptgame.gameui.cooldown import cooldown_active
from promptgame.gameui.models import Account
from promptgame.gameui.user_account_tools import (
    async_create_user_and_account,
    create_user_and_account,
)


def sync_clear_redis_test_db() -> None:
    """Clear the Redis test database."""
    prefix = settings.REDIS_KEY_PREFIX
    assert prefix == "test-pg", f"Expected prefix to be 'test-pg', got {prefix=}"
    client = ru.get_sync_client()
    keys = client.keys(f"{prefix}:*")
    # delete block_size keys at a time
    # (keeps us well under limits: https://stackoverflow.com/a/42913294)
    block_size = 1024
    for i in range(0, len(keys), block_size):
        client.delete(*keys[i : i + block_size])

    # delete all old cache clients
    ru._global_cached_clients.clear()


async def async_make_account() -> Account:
    """Create a new account."""
    _, account = await async_create_user_and_account()
    return account


def make_account() -> Account:
    """Create a new account."""
    _, account = create_user_and_account()
    return account


def unlock_account(account: Account) -> None:
    """Make an account unlocked."""
    account.unlock_at = timezone.now() - timezone.timedelta(days=1)
    account.save()


async def async_unlock_account(account: Account) -> None:
    """Make an account unlocked."""
    return await sync_to_async(unlock_account)(account)


async def get_content(resp: HttpResponse | StreamingHttpResponse) -> str:
    """Convert an async streaming response to a string.

    Takes either HttpResponse or StreamingHttpResponse for consistency with the
    dummy_attack() return type, but will error out if the response is not a
    streaming response (since that indicates an error; e.g. due to a disallowed
    HTTP verb)."""
    assert isinstance(resp, StreamingHttpResponse)
    # assert that resp.streaming_content is an asynchronous iterable of bytes
    assert isinstance(resp.streaming_content, abc.AsyncIterable)
    bytes = b"".join([b async for b in resp.streaming_content])
    return bytes.decode("utf-8")


def login(user: User, client: Client) -> None:
    """Log in to an account."""
    client.force_login(user)


async def async_login(user: User, client: Client) -> None:
    """Log in to an account."""
    await sync_to_async(login)(user, client)


def attack_url(account: Account) -> str:
    """URL to attack an account."""
    return reverse("subaccount_login_submit", args=[account.account_id])


async def dummy_attack(
    async_client: AsyncClient,
    victim_account: Account,
    attack_input: str = "foo",
) -> HttpResponse | StreamingHttpResponse:
    """Perform an attack on the given victim account using given input."""
    return await async_client.post(
        attack_url(victim_account),
        {
            "attacker_input": attack_input,
        },
    )


def cooldown_active_nowarn(*, attacker: Account, defender: Account) -> bool:
    """cooldown_active() with no warnings, for use in tests (that function is
    meant not to be used outside of test cases because runs a query each time,
    and its use may be symptomatic of an N+1 query problem)"""
    with warnings.catch_warnings():
        warnings.filterwarnings("ignore", message=".*cooldown_active.*")
        return cooldown_active(defender=defender, attacker=attacker)


async_cooldown_active_nowarn = sync_to_async(cooldown_active_nowarn)


@dataclass
class SSEMessage:
    event: Optional[str] = None
    data: Optional[str] = None


def parse_sse_messages(sse_stream: str) -> List[SSEMessage]:
    """Parse a HTTP response containing a stream of SSE messages."""
    # This whole thing was written by GPT4. I'm inclined not to touch it because
    # it works okay.
    # Initialize an empty list to store parsed messages
    parsed_messages = []

    # Initialize variables to hold the current event and data
    current_event = None
    current_data = []

    # Split the stream into lines
    lines = sse_stream.splitlines()

    # Iterate through each line to parse fields
    for line in lines:
        # Handle comment lines (starting with a colon)
        if line.startswith(":"):
            continue

        # Handle field lines
        if ":" in line:
            field_name, field_value = line.split(":", 1)

            # Remove leading space from the field value
            field_value = field_value.lstrip()

            # Handle "event" field
            if field_name == "event":
                current_event = field_value

            # Handle "data" field
            if field_name == "data":
                current_data.append(field_value)

        # Handle end-of-message (empty line)
        if line == "":
            if current_data:
                # Create a new SSEMessage and append it to the list
                parsed_messages.append(
                    SSEMessage(event=current_event, data="\n".join(current_data))
                )

                # Reset current event and data for the next message
                current_event = None
                current_data = []

    return parsed_messages
