import random
import uuid
from http import HTTPStatus

import pytest
from django.contrib import auth
from django.contrib.auth.models import User
from django.db import IntegrityError
from django.db.models.query import sync_to_async
from django.test import Client
from django.test.client import AsyncClient  # type: ignore
from django.urls import reverse

from promptgame.gameui.models import Account, Defense
from promptgame.gameui.user_account_tools import (
    AccountDefaults,
    create_user_and_account,
)

# For an explanation of what hte helpers in this file do, see the django-pytest
# docs (especially the Client/AsyncClient docs):
# https://pytest-django.readthedocs.io/en/latest/helpers.html#id2


# the "client" argument means that our function receives a Django Client object
# from pytest, which we can use to make requests (see the pytest-django docs for
# a list of other fixtures available)
def test_landing_page_anonymous(client: Client) -> None:
    """Check that we get a 'create account' page if we're not logged in."""
    resp = client.get(reverse("create_account"))
    assert resp.status_code == HTTPStatus.OK
    # check for presence of the "create account" button text
    assert "Hack their AI. Defend your own" in resp.content.decode("utf-8")


# pytest-django requires you to mark any test that touches the database with
# the @pytest.mark.django_db decorator.
@pytest.mark.django_db
def test_landing_page_logged_in(client: Client) -> None:
    """Check that we get redirected to the overview page if we're logged in."""
    # log in as a user
    user, _ = create_user_and_account()
    client.force_login(user)
    resp = client.get(reverse("create_account"))
    assert resp.status_code == HTTPStatus.FOUND
    overview_url = reverse("overview")
    assert overview_url in resp.url


@pytest.mark.django_db
@pytest.mark.asyncio
async def test_landing_page_create(
    async_client: AsyncClient, mock_requests_post_success: None
) -> None:
    """Test that POST-ing to the create account page works.

    It should log us in with a new user account and redirect us to the overview
    page."""
    # we're not logged in
    session = await sync_to_async(get_session)(async_client)
    assert "_auth_user_id" not in session

    # Create an account.  The only thing the POST view needs is a CSRF token.
    resp = await async_client.post(reverse("captcha_post"))

    # make sure we got redirected
    assert resp.status_code == HTTPStatus.FOUND
    overview_url = reverse("subaccounts")
    assert overview_url == resp.url

    # make sure we're logged in as a user
    user = await sync_to_async(auth.get_user)(async_client)
    assert user.is_authenticated


def get_session(client):
    return client.session


@pytest.mark.django_db
@pytest.mark.asyncio
async def test_captcha_reject(
    async_client: AsyncClient, mock_requests_post_failure: None
) -> None:
    """Test that POST-ing to the create account page works when captcha fails.

    It should return us to the login page."""
    # we're not logged in
    session = await sync_to_async(get_session)(async_client)
    assert "_auth_user_id" not in session

    # Create an account.  The only thing the POST view needs is a CSRF token.
    resp = await async_client.post(reverse("captcha_post"))

    # make sure we got redirected
    assert resp.status_code == HTTPStatus.FOUND
    overview_url = reverse("create_account")
    assert overview_url == resp.url


@pytest.mark.django_db
def test_correct_account_creation() -> None:
    """Test that an account is correctly made when a user is created."""
    user, account = create_user_and_account()
    assert user.account == account
    assert account.owner == user

    # Create a new user, confirm we can link an account with it
    user = User.objects.create(username="testUser")
    user2, account = create_user_and_account(user)
    assert user.account == account
    assert account.owner == user
    assert user == user2

    # If you pass in a user who already has an account, throw an error
    user, account = create_user_and_account()
    with pytest.raises(ValueError):
        create_user_and_account(user)


@pytest.mark.django_db
def test_account_id_duplication() -> None:
    """Test that we can't create two accounts with the same account ID."""
    # create an account
    _, account = create_user_and_account()
    account_id = account.account_id
    with pytest.raises(IntegrityError):
        # try to create another account with the same account ID
        user = User.objects.create(username="auto-" + uuid.uuid1().hex)
        account = Account.objects.create(
            owner=user,
            account_id=account_id,
            balance=1,
        )


@pytest.mark.django_db
def test_account_id_retry() -> None:
    """Test that if we generate a duplicate ID we try again."""

    # create an account
    random.seed(0)
    first_random = AccountDefaults.generate().account_id
    second_random = AccountDefaults.generate().account_id

    # Reset the random seed so that we get the same results
    random.seed(0)
    _, account1 = create_user_and_account()

    # Reset the random seed. It should try to generate a duplicate
    # ID, then try again.
    random.seed(0)
    _, account2 = create_user_and_account()
    assert account1.account_id == first_random
    assert account2.account_id == second_random
    assert account1.account_id != account2.account_id


@pytest.mark.django_db
def test_random_defenses() -> None:
    n_to_generate = 20
    random.seed(0)
    generated = set()
    for _ in range(n_to_generate):
        _, account = create_user_and_account()
        defense: Defense = account.active_defense
        generated.add(
            (
                defense.pre_prompt,
                defense.post_prompt,
                defense.access_code,
            )
        )
        assert len(defense.pre_prompt) > 0
        assert len(defense.post_prompt) > 0
        assert len(defense.access_code) > 0
        assert "access granted" in defense.access_code_output.lower()
    # make sure that randomization works for all fields
    assert len(generated) > 1
    # also make sure that we don't get the same value for any one field twice
    # (we get more than one type of pre-prompt, more than one type of post-prompt, etc)
    field_sets = list(set(l) for l in zip(*generated))
    for field_set in field_sets:
        assert len(field_set) > 1
