from http import HTTPStatus
from typing import List, Optional

import pytest
from bs4 import BeautifulSoup
from django.test import Client
from django.test.client import AsyncClient  # type: ignore

from promptgame.gameui import llm_wrappers
from promptgame.gameui.attack_lists import AttackListForm, render_paginated_table
from promptgame.gameui.models import Account, AuthAttempt
from promptgame.tests.helpers import login, make_account


def create_attack(
    *,
    attacker: Account,
    defender: Account,
    success: bool = True,
    attacker_input: str = "",
    net_defender_gain: float = 0.0,
    net_attacker_gain: float = 0.0,
) -> AuthAttempt:
    """Create an attack with the given parameters. Similar to
    AuthAttempt.objects.create, but has sensible defaults."""
    assert defender.active_defense is not None
    auth_attempt = AuthAttempt.objects.create(
        attacking_account=attacker,
        defending_account=defender,
        was_successful=success,
        attacker_input=attacker_input,
        defense=defender.active_defense,
        defense_pre_prompt=defender.active_defense.pre_prompt,
        defense_post_prompt=defender.active_defense.post_prompt,
        access_code=defender.active_defense.access_code,
        attacking_transfer_amount=net_defender_gain,
        defending_transfer_amount=net_attacker_gain,
        attacking_balance=attacker.balance,
        defending_balance=defender.balance,
        auto_moderated=True,
        llm_choice=llm_wrappers.LLMChoice.GPT_35_TURBO_0613,
    )
    return auth_attempt


@pytest.fixture
def two_attacks_per_page(settings) -> None:
    settings.ATTACKS_PER_PAGE = 2


@pytest.fixture
def my_account() -> Account:
    return make_account()


NAUGHTY_STRING = "SHOULD NEVER SHOW UP AT ALL. "
NAUGHTY_STRING_SELF_ATTACKS = "SHOULD NOT SHOW UP IN SELF-ATTACKS. "
NAUGHTY_STRING_ATTACKS_ON_ME = "SHOULD NOT SHOW UP IN ATTACKS ON ME. "
NAUGHTY_STRING_ATTACKS_BY_ME = "SHOULD NOT SHOW UP IN ATTACKS BY ME. "


def make_other():
    acct = make_account()
    defense = acct.active_defense
    assert defense is not None
    # these should NEVER show up anywhere in attack tables
    defense.pre_prompt = f"Other defense pre-prompt. {NAUGHTY_STRING}"
    defense.pre_prompt = f"Other defense post-prompt. {NAUGHTY_STRING}"
    defense.access_code = f"Other defense post-prompt. {NAUGHTY_STRING}"
    defense.save()
    return acct


@pytest.fixture
def other_1() -> Account:
    return make_other()


@pytest.fixture
def other_2() -> Account:
    return make_other()


@pytest.fixture
def common_db_setup(my_account, other_1, other_2):
    # create some attacks that should never show up
    create_attack(
        attacker=other_1,
        defender=other_2,
        attacker_input=f"Other 1 -> Other 2. {NAUGHTY_STRING}",
    )
    create_attack(
        attacker=other_1,
        defender=other_1,
        attacker_input=f"Other 1 -> Other 1. {NAUGHTY_STRING}",
    )

    # create some attacks that should show up in self-attacks
    for i in range(1, 3 + 1):
        create_attack(
            attacker=my_account,
            defender=my_account,
            attacker_input=f"Self-attack {i}. {NAUGHTY_STRING_ATTACKS_BY_ME} {NAUGHTY_STRING_ATTACKS_ON_ME} {NAUGHTY_STRING_ATTACKS_ON_ME}.",
        )

    # create some attacks that should show up in attacks on me
    for i in range(1, 2 + 1):
        create_attack(
            attacker=other_1,
            defender=my_account,
            attacker_input=f"Other 1 -> Me #{i}. {NAUGHTY_STRING_SELF_ATTACKS} {NAUGHTY_STRING_ATTACKS_BY_ME}",
        )
    create_attack(
        attacker=other_2,
        defender=my_account,
        attacker_input=f"Other 2 -> Me. {NAUGHTY_STRING_SELF_ATTACKS} {NAUGHTY_STRING_ATTACKS_BY_ME}",
    )

    # create some attacks that should show up in attacks by me
    for i in range(1, 2 + 1):
        create_attack(
            attacker=my_account,
            defender=other_1,
            attacker_input=f"Me -> Other 1 #{i}. {NAUGHTY_STRING_SELF_ATTACKS} {NAUGHTY_STRING_ATTACKS_ON_ME}",
        )
    create_attack(
        attacker=my_account,
        defender=other_2,
        attacker_input=f"Me -> Other 2. {NAUGHTY_STRING_SELF_ATTACKS} {NAUGHTY_STRING_ATTACKS_ON_ME}",
    )


def crawl_tables(client: Client, rendered_html: str) -> List[BeautifulSoup]:
    """Crawl the page numbers on a rendered table to get HTML for each page."""
    first_page_parsed = BeautifulSoup(rendered_html, "html.parser")
    page_links = first_page_parsed.find_all("a", class_="page-active")
    first_page_number = int(page_links[0].text)
    last_page_number = int(page_links[-1].text)
    first_page_href = page_links[0].get("hx-get")
    parsed_pages = []
    for page_number in range(first_page_number, last_page_number + 1):
        page_url = first_page_href.replace(
            f"page={first_page_number}", f"page={page_number}"
        )
        get_result = client.get(page_url)
        assert get_result.status_code == HTTPStatus.OK
        page_html = get_result.content.decode("utf8")
        parsed_pages.append(BeautifulSoup(page_html, "html.parser"))
    return parsed_pages


def extract_attacks_from_pages(parsed_pages: List[BeautifulSoup]) -> List[str]:
    attacks = []
    for parsed_page in parsed_pages:
        for details in parsed_page.find_all("details"):
            attacks.append(details.text.strip())
    return attacks


def make_attack_form(
    current_account: Account, attacker: Optional[Account], defender: Optional[Account]
) -> AttackListForm:
    form = AttackListForm(
        {
            "attacker": attacker.account_id if attacker is not None else None,
            "defender": defender.account_id if defender is not None else None,
        },
        current_account=current_account,
    )
    assert form.is_valid()
    return form


@pytest.mark.django_db
def test_self_attack_view(
    my_account, common_db_setup, two_attacks_per_page, client: Client
):
    # Make sure that we have three attacks across two pages, that NAUGHTY_STRING
    # and NAUGHTY_STRING_SELF_ATTACKS do not show up, and that "Self attack 1",
    # "Self attack 2" and "Self attack 3" all show up.
    login(my_account.owner, client)
    attack_form = make_attack_form(
        attacker=my_account, defender=my_account, current_account=my_account
    )
    rendered_html = render_paginated_table(
        my_account=my_account, attack_form=attack_form
    )
    parsed_pages = crawl_tables(client, rendered_html)

    for page in parsed_pages:
        raw_html = str(page)
        assert NAUGHTY_STRING not in raw_html
        assert NAUGHTY_STRING_SELF_ATTACKS not in raw_html

    attacks = extract_attacks_from_pages(parsed_pages)
    assert len(attacks) == 3
    assert len(parsed_pages) == 2
    assert any("Self-attack 1" in attack for attack in attacks)
    assert any("Self-attack 2" in attack for attack in attacks)
    assert any("Self-attack 3" in attack for attack in attacks)


@pytest.mark.django_db
def test_attacks_on_me_view(
    my_account, common_db_setup, two_attacks_per_page, client: Client
):
    # Make sure that we have three attacks across two pages, that NAUGHTY_STRING
    # and NAUGHTY_STRING_ATTACKS_ON_ME do not show up, and that "Other 1 -> Me #1",
    # "Other 1 -> Me #2" and "Other 2 -> Me" all show up. Also make sure that
    # access codes and defenses do not show up.
    login(my_account.owner, client)
    attack_form = make_attack_form(
        attacker=None, defender=my_account, current_account=my_account
    )
    rendered_html = render_paginated_table(
        my_account=my_account, attack_form=attack_form
    )
    parsed_pages = crawl_tables(client, rendered_html)

    for page in parsed_pages:
        raw_html = str(page)
        assert NAUGHTY_STRING not in raw_html
        assert NAUGHTY_STRING_ATTACKS_ON_ME not in raw_html

    attacks = extract_attacks_from_pages(parsed_pages)
    assert len(attacks) == 3
    assert len(parsed_pages) == 2
    assert any("Other 1 -> Me #1" in attack for attack in attacks)
    assert any("Other 1 -> Me #2" in attack for attack in attacks)
    assert any("Other 2 -> Me" in attack for attack in attacks)


@pytest.mark.django_db
def test_attacks_by_me_view(
    my_account, common_db_setup, two_attacks_per_page, client: Client
):
    # Make sure that we have three attacks across two pages, that NAUGHTY_STRING
    # and NAUGHTY_STRING_ATTACKS_BY_ME do not show up, and that "Me -> Other 1 #1",
    # "Me -> Other 1 #2" and "Me -> Other 2" all show up.
    login(my_account.owner, client)
    attack_form = make_attack_form(
        attacker=my_account, defender=None, current_account=my_account
    )
    rendered_html = render_paginated_table(
        my_account=my_account, attack_form=attack_form
    )
    parsed_pages = crawl_tables(client, rendered_html)

    for page in parsed_pages:
        raw_html = str(page)
        assert NAUGHTY_STRING not in raw_html
        assert NAUGHTY_STRING_ATTACKS_BY_ME not in raw_html

    attacks = extract_attacks_from_pages(parsed_pages)
    assert len(attacks) == 3
    assert len(parsed_pages) == 2
    assert any("Me -> Other 1 #1" in attack for attack in attacks)
    assert any("Me -> Other 1 #2" in attack for attack in attacks)
    assert any("Me -> Other 2" in attack for attack in attacks)
