from datetime import date, timedelta
from unittest.mock import ANY, patch

import pytest
from dateutil.relativedelta import relativedelta
from django.core.exceptions import ValidationError
from django.utils import timezone

from ...core import TimePeriodType
from ...core.utils.promo_code import InvalidPromoCode
from ...order.models import OrderLine
from ...plugins.manager import get_plugins_manager
from ...site import GiftCardSettingsExpiryType
from ...warehouse.models import Allocation
from .. import GiftCardEvents, events
from ..models import GiftCardEvent
from ..utils import (
    add_gift_card_code_to_checkout,
    calculate_expiry_date,
    deactivate_order_gift_cards,
    fulfill_gift_card_lines,
    fulfill_non_shippable_gift_cards,
    get_gift_card_lines,
    get_non_shippable_gift_card_lines,
    gift_cards_create,
    order_has_gift_card_lines,
    remove_gift_card_code_from_checkout,
)


def test_add_gift_card_code_to_checkout(checkout, gift_card):
    # given
    assert checkout.gift_cards.count() == 0

    # when
    add_gift_card_code_to_checkout(
        checkout, "test@example.com", gift_card.code, gift_card.currency
    )

    # then
    assert checkout.gift_cards.count() == 1


def test_add_gift_card_code_to_checkout_inactive_card(checkout, gift_card):
    # given
    gift_card.is_active = False
    gift_card.save(update_fields=["is_active"])

    assert checkout.gift_cards.count() == 0

    # when
    # then
    with pytest.raises(InvalidPromoCode):
        add_gift_card_code_to_checkout(
            checkout, "test@example.com", gift_card.code, gift_card.currency
        )


def test_add_gift_card_code_to_checkout_expired_card(checkout, gift_card):
    # given
    gift_card.expiry_date = date.today() - timedelta(days=10)
    gift_card.save(update_fields=["expiry_date"])

    assert checkout.gift_cards.count() == 0

    # when
    # then
    with pytest.raises(InvalidPromoCode):
        add_gift_card_code_to_checkout(
            checkout, "test@example.com", gift_card.code, gift_card.currency
        )


def test_add_gift_card_code_to_checkout_invalid_currency(checkout, gift_card):
    # given
    currency = "EUR"

    assert gift_card.currency != currency
    assert checkout.gift_cards.count() == 0

    # when
    # then
    with pytest.raises(InvalidPromoCode):
        add_gift_card_code_to_checkout(
            checkout, "test@example.com", gift_card.code, currency
        )


def test_add_gift_card_code_to_checkout_used_gift_card(checkout, gift_card_used):
    # given
    assert gift_card_used.used_by_email
    assert checkout.gift_cards.count() == 0

    # when
    add_gift_card_code_to_checkout(
        checkout,
        gift_card_used.used_by_email,
        gift_card_used.code,
        gift_card_used.currency,
    )

    # then
    assert checkout.gift_cards.count() == 1


def test_add_gift_card_code_to_checkout_used_gift_card_invalid_user(
    checkout, gift_card_used
):
    # given
    email = "new_user@example.com"
    assert gift_card_used.used_by_email
    assert gift_card_used.used_by_email != email
    assert checkout.gift_cards.count() == 0

    # when
    # then
    with pytest.raises(InvalidPromoCode):
        add_gift_card_code_to_checkout(
            checkout, email, gift_card_used.code, gift_card_used.currency
        )


def test_remove_gift_card_code_from_checkout(checkout, gift_card):
    # given
    checkout.gift_cards.add(gift_card)
    assert checkout.gift_cards.count() == 1

    # when
    remove_gift_card_code_from_checkout(checkout, gift_card.code)

    # then
    assert checkout.gift_cards.count() == 0


def test_remove_gift_card_code_from_checkout_no_checkout_gift_cards(
    checkout, gift_card
):
    # given
    assert checkout.gift_cards.count() == 0

    # when
    remove_gift_card_code_from_checkout(checkout, gift_card.code)

    # then
    assert checkout.gift_cards.count() == 0


@pytest.mark.parametrize(
    "period_type, period", [("years", 5), ("weeks", 1), ("months", 13), ("days", 100)]
)
def test_calculate_expiry_settings(period_type, period, site_settings):
    # given
    site_settings.gift_card_expiry_type = GiftCardSettingsExpiryType.EXPIRY_PERIOD
    site_settings.gift_card_expiry_period_type = period_type.rstrip("s")
    site_settings.gift_card_expiry_period = period
    site_settings.save(
        update_fields=[
            "gift_card_expiry_type",
            "gift_card_expiry_period_type",
            "gift_card_expiry_period",
        ]
    )

    # when
    expiry_date = calculate_expiry_date(site_settings)

    # then
    assert expiry_date == timezone.now().date() + relativedelta(**{period_type: period})


def test_calculate_expiry_settings_for_never_expire_settings(site_settings):
    # given
    site_settings.gift_card_expiry_type = GiftCardSettingsExpiryType.NEVER_EXPIRE

    # when
    expiry_date = calculate_expiry_date(site_settings)

    # then
    assert expiry_date is None


@patch("saleor.giftcard.utils.send_gift_card_notification")
def test_gift_cards_create(
    send_notification_mock,
    order,
    gift_card_shippable_order_line,
    gift_card_non_shippable_order_line,
    site_settings,
    staff_user,
):
    # given
    manager = get_plugins_manager()
    order_lines = [gift_card_shippable_order_line, gift_card_non_shippable_order_line]
    quantities = {
        gift_card_shippable_order_line.pk: 1,
        gift_card_non_shippable_order_line.pk: 1,
    }
    user_email = order.user_email

    # when
    gift_cards = gift_cards_create(
        order, order_lines, quantities, site_settings, staff_user, None, manager
    )

    # then
    assert len(gift_cards) == len(order_lines)

    shippable_gift_card = gift_cards[0]
    shippable_price = gift_card_shippable_order_line.unit_price_gross
    assert shippable_gift_card.initial_balance == shippable_price
    assert shippable_gift_card.current_balance == shippable_price
    assert shippable_gift_card.created_by == order.user
    assert shippable_gift_card.created_by_email == user_email
    assert shippable_gift_card.expiry_date is None

    bought_event_for_shippable_card = GiftCardEvent.objects.get(
        gift_card=shippable_gift_card
    )
    assert bought_event_for_shippable_card.user == staff_user
    assert bought_event_for_shippable_card.app is None
    assert bought_event_for_shippable_card.type == GiftCardEvents.BOUGHT
    assert bought_event_for_shippable_card.parameters == {
        "order_id": order.id,
        "expiry_date": None,
    }

    non_shippable_gift_card = gift_cards[1]
    non_shippable_price = gift_card_non_shippable_order_line.total_price_gross
    assert non_shippable_gift_card.initial_balance == non_shippable_price
    assert non_shippable_gift_card.current_balance == non_shippable_price
    assert non_shippable_gift_card.created_by == order.user
    assert non_shippable_gift_card.created_by_email == user_email
    assert non_shippable_gift_card.expiry_date is None

    shippable_event = GiftCardEvent.objects.get(
        gift_card=non_shippable_gift_card, type=GiftCardEvents.BOUGHT
    )
    assert shippable_event.user == staff_user
    assert shippable_event.app is None
    assert shippable_event.parameters == {"order_id": order.id, "expiry_date": None}

    send_notification_mock.assert_called_once_with(
        staff_user,
        None,
        order.user,
        user_email,
        non_shippable_gift_card,
        manager,
        order.channel.slug,
        resending=False,
    )


@patch("saleor.giftcard.utils.send_gift_card_notification")
def test_gift_cards_create_expiry_date_set(
    send_notification_mock,
    order,
    gift_card_shippable_order_line,
    gift_card_non_shippable_order_line,
    site_settings,
    staff_user,
):
    # given
    manager = get_plugins_manager()
    site_settings.gift_card_expiry_type = GiftCardSettingsExpiryType.EXPIRY_PERIOD
    site_settings.gift_card_expiry_period_type = TimePeriodType.WEEK
    site_settings.gift_card_expiry_period = 20
    site_settings.save(
        update_fields=[
            "gift_card_expiry_type",
            "gift_card_expiry_period_type",
            "gift_card_expiry_period",
        ]
    )
    order_lines = [gift_card_non_shippable_order_line]
    quantities = {
        gift_card_shippable_order_line.pk: 1,
        gift_card_non_shippable_order_line.pk: 1,
    }
    user_email = order.user_email

    # when
    gift_cards = gift_cards_create(
        order, order_lines, quantities, site_settings, staff_user, None, manager
    )

    # then
    assert len(gift_cards) == len(order_lines)

    non_shippable_gift_card = gift_cards[0]
    non_shippable_price = gift_card_non_shippable_order_line.total_price_gross
    assert non_shippable_gift_card.initial_balance == non_shippable_price
    assert non_shippable_gift_card.current_balance == non_shippable_price
    assert non_shippable_gift_card.created_by == order.user
    assert non_shippable_gift_card.created_by_email == user_email
    assert non_shippable_gift_card.expiry_date

    shippable_event = GiftCardEvent.objects.get(
        gift_card=non_shippable_gift_card, type=GiftCardEvents.BOUGHT
    )
    assert shippable_event.user == staff_user
    assert shippable_event.app is None
    assert shippable_event.parameters == {
        "order_id": order.id,
        "expiry_date": non_shippable_gift_card.expiry_date.isoformat(),
    }

    send_notification_mock.assert_called_once_with(
        staff_user,
        None,
        order.user,
        user_email,
        non_shippable_gift_card,
        manager,
        order.channel.slug,
        resending=False,
    )


@patch("saleor.giftcard.utils.send_gift_card_notification")
def test_gift_cards_create_multiple_quantity(
    send_notification_mock,
    order,
    gift_card_non_shippable_order_line,
    site_settings,
    staff_user,
):
    # given
    manager = get_plugins_manager()
    quantity = 3
    gift_card_non_shippable_order_line.quantity = quantity
    gift_card_non_shippable_order_line.save(update_fields=["quantity"])
    order_lines = [gift_card_non_shippable_order_line]
    quantities = {gift_card_non_shippable_order_line.pk: quantity}

    # when
    gift_cards = gift_cards_create(
        order, order_lines, quantities, site_settings, staff_user, None, manager
    )

    # then
    assert len(gift_cards) == quantity
    price = gift_card_non_shippable_order_line.unit_price_gross
    for gift_card in gift_cards:
        assert gift_card.initial_balance == price
        assert gift_card.current_balance == price

    assert GiftCardEvent.objects.filter(type=GiftCardEvents.BOUGHT).count() == quantity
    assert send_notification_mock.call_count == quantity


def test_get_gift_card_lines(
    gift_card_non_shippable_order_line, gift_card_shippable_order_line, order_line
):
    # given
    lines = [
        gift_card_non_shippable_order_line,
        gift_card_shippable_order_line,
        order_line,
    ]

    # when
    gift_card_lines = get_gift_card_lines(lines)

    # then
    assert set(gift_card_lines) == {
        gift_card_non_shippable_order_line,
        gift_card_shippable_order_line,
    }


def test_get_gift_card_lines_no_gift_card_lines(
    order_line_with_one_allocation, order_line
):
    # given
    lines = [order_line_with_one_allocation, order_line]

    # when
    gift_card_lines = get_gift_card_lines(lines)

    # then
    assert not gift_card_lines


def test_get_non_shippable_gift_card_lines(
    gift_card_non_shippable_order_line, gift_card_shippable_order_line, order_line
):
    # given
    lines = [
        gift_card_non_shippable_order_line,
        gift_card_shippable_order_line,
        order_line,
    ]

    # when
    gift_card_lines = get_non_shippable_gift_card_lines(lines)

    # then
    assert set(gift_card_lines) == {gift_card_non_shippable_order_line}


def test_get_non_shippable_gift_card_lines_no_gift_card_lines(
    order_line_with_one_allocation, order_line
):
    # given
    lines = [order_line_with_one_allocation, order_line]

    # when
    gift_card_lines = get_gift_card_lines(lines)

    # then
    assert not gift_card_lines


@patch("saleor.giftcard.utils.create_fulfillments")
@patch("saleor.giftcard.utils.gift_cards_create")
def test_fulfill_non_shippable_gift_cards(
    gift_cards_create_mock,
    create_fulfillments_mock,
    order,
    gift_card_shippable_order_line,
    gift_card_non_shippable_order_line,
    site_settings,
    staff_user,
    warehouse,
):
    # given
    manager = get_plugins_manager()
    order_lines = [gift_card_shippable_order_line, gift_card_non_shippable_order_line]
    quantities = {
        gift_card_non_shippable_order_line.pk: 1,
    }

    # when
    fulfill_non_shippable_gift_cards(
        order, order_lines, site_settings, staff_user, None, manager
    )

    # then
    gift_cards_create_mock.assert_called_once()
    args, _kwargs = gift_cards_create_mock.call_args
    assert args[0] == order
    assert {line.pk for line in args[1]} == {gift_card_non_shippable_order_line.pk}
    assert args[2] == quantities
    assert args[3] == site_settings
    assert args[4] == staff_user
    assert args[5] is None
    assert args[6] == manager

    fulfillment_lines_for_warehouses = {
        str(warehouse.pk): [
            {
                "order_line": gift_card_non_shippable_order_line,
                "quantity": gift_card_non_shippable_order_line.quantity,
            },
        ]
    }

    create_fulfillments_mock.assert_called_once_with(
        staff_user,
        None,
        order,
        fulfillment_lines_for_warehouses,
        ANY,
        notify_customer=True,
    )


@patch("saleor.giftcard.utils.create_fulfillments")
@patch("saleor.giftcard.utils.gift_cards_create")
def test_fulfill_non_shippable_gift_cards_line_with_allocation(
    gift_cards_create_mock,
    create_fulfillments_mock,
    order,
    gift_card_shippable_order_line,
    gift_card_non_shippable_order_line,
    site_settings,
    staff_user,
    warehouse,
):
    # given
    manager = get_plugins_manager()
    order_lines = [gift_card_shippable_order_line, gift_card_non_shippable_order_line]
    quantities = {
        gift_card_non_shippable_order_line.pk: 1,
    }

    order = gift_card_non_shippable_order_line.order
    non_shippable_variant = gift_card_non_shippable_order_line.variant
    non_shippable_variant.track_inventory = True
    non_shippable_variant.save(update_fields=["track_inventory"])

    stock = non_shippable_variant.stocks.first()
    Allocation.objects.create(
        order_line=gift_card_non_shippable_order_line,
        stock=stock,
        quantity_allocated=gift_card_non_shippable_order_line.quantity,
    )

    # when
    fulfill_non_shippable_gift_cards(
        order, order_lines, site_settings, staff_user, None, manager
    )

    # then
    gift_cards_create_mock.assert_called_once()
    args, kwargs = gift_cards_create_mock.call_args
    assert args[0] == order
    assert {line.pk for line in args[1]} == {gift_card_non_shippable_order_line.pk}
    assert args[2] == quantities
    assert args[3] == site_settings
    assert args[4] == staff_user
    assert args[5] is None
    assert args[6] == manager

    fulfillment_lines_for_warehouses = {
        str(stock.warehouse.pk): [
            {
                "order_line": gift_card_non_shippable_order_line,
                "quantity": gift_card_non_shippable_order_line.quantity,
            },
        ]
    }

    create_fulfillments_mock.assert_called_once_with(
        staff_user,
        None,
        order,
        fulfillment_lines_for_warehouses,
        ANY,
        notify_customer=True,
    )


def test_fulfill_gift_card_lines(
    staff_user, gift_card_non_shippable_order_line, gift_card_shippable_order_line
):
    # given
    manager = get_plugins_manager()
    order = gift_card_non_shippable_order_line.order
    non_shippable_variant = gift_card_non_shippable_order_line.variant
    non_shippable_variant.track_inventory = True
    non_shippable_variant.save(update_fields=["track_inventory"])

    Allocation.objects.create(
        order_line=gift_card_non_shippable_order_line,
        stock=non_shippable_variant.stocks.first(),
        quantity_allocated=gift_card_non_shippable_order_line.quantity,
    )

    lines = OrderLine.objects.filter(
        pk__in=[
            gift_card_non_shippable_order_line.pk,
            gift_card_shippable_order_line.pk,
        ]
    )

    # when
    fulfillments = fulfill_gift_card_lines(lines, staff_user, None, order, manager)

    # then
    assert len(fulfillments) == 1
    assert fulfillments[0].lines.count() == len(lines)


def test_fulfill_gift_card_lines_lack_of_stock(
    staff_user, gift_card_non_shippable_order_line, gift_card_shippable_order_line
):
    # given
    manager = get_plugins_manager()
    order = gift_card_non_shippable_order_line.order
    gift_card_non_shippable_order_line.variant.stocks.all().delete()

    lines = OrderLine.objects.filter(
        pk__in=[
            gift_card_non_shippable_order_line.pk,
            gift_card_shippable_order_line.pk,
        ]
    )

    # when
    with pytest.raises(ValidationError):
        fulfill_gift_card_lines(lines, staff_user, None, order, manager)


def test_deactivate_order_gift_cards(
    gift_card, gift_card_expiry_date, gift_card_created_by_staff, order, staff_user
):
    # given
    bought_cards = [gift_card, gift_card_expiry_date]
    events.gift_cards_bought_event(bought_cards, order.id, staff_user, None)

    for card in [gift_card, gift_card_expiry_date, gift_card_created_by_staff]:
        assert card.is_active

    # when
    deactivate_order_gift_cards(order.id, staff_user, None)

    # then
    for card in bought_cards:
        card.refresh_from_db()
        assert not card.is_active
        assert card.events.filter(type=GiftCardEvents.DEACTIVATED)

    assert gift_card_created_by_staff.is_active
    assert not gift_card_created_by_staff.events.filter(type=GiftCardEvents.DEACTIVATED)


def test_deactivate_order_gift_cards_no_order_gift_cards(
    gift_card, gift_card_expiry_date, gift_card_created_by_staff, order, staff_user
):
    # given
    cards = [gift_card, gift_card_expiry_date, gift_card_created_by_staff]
    for card in cards:
        assert card.is_active

    # when
    deactivate_order_gift_cards(order.id, staff_user, None)

    # then
    for card in cards:
        card.refresh_from_db()
        assert card.is_active


def test_order_has_gift_card_lines_true(gift_card_shippable_order_line):
    order = gift_card_shippable_order_line.order
    assert order_has_gift_card_lines(order) is True


def test_order_has_gift_card_lines_false(order):
    assert order_has_gift_card_lines(order) is False
