from datetime import date, timedelta
from decimal import Decimal
from unittest.mock import patch

import pytest
from pytest_factoryboy.fixture import LazyFixture
from wbcore.contrib.authentication.models import User

from wbaccounting.factories import EntryAccountingInformationFactory
from wbaccounting.generators import TestGenerator
from wbaccounting.models import BookingEntry, EntryAccountingInformation, Invoice


@pytest.mark.django_db
class TestInvoice:
    def test_str(self, invoice: Invoice):
        assert str(invoice) == invoice.title

    @pytest.mark.parametrize(
        "method,return_value",
        [
            ("get_endpoint_basename", "wbaccounting:invoice"),
            ("get_representation_value_key", "id"),
            ("get_representation_label_key", "{{title}} ({{invoice_date}})"),
            ("get_representation_endpoint", "wbaccounting:invoicerepresentation-list"),
        ],
    )
    def test_wbmodel_methods(self, method: str, return_value: str):
        assert getattr(Invoice, method)() == return_value

    @pytest.mark.parametrize("user__is_superuser", [False])
    def test_filter_for_user_no_superuser(self, invoice: Invoice, user: User):
        invoices = Invoice.objects.filter_for_user(user)  # type: ignore
        assert invoice not in invoices

    @pytest.mark.parametrize("user__is_superuser", [True])
    def test_filter_for_user_superuser(self, invoice: Invoice, user: User):
        invoices = Invoice.objects.filter_for_user(user)  # type: ignore
        assert invoice in invoices

    @pytest.mark.parametrize("user__is_superuser", [False])
    @pytest.mark.parametrize("user__user_permissions", [(["wbaccounting.view_invoice"])])
    @pytest.mark.parametrize("invoice__counterparty", [LazyFixture("entry")])
    @pytest.mark.parametrize("entry_accounting_information__entry", [LazyFixture("entry")])
    @pytest.mark.parametrize("entry_accounting_information__counterparty_is_private", [False])
    def test_filter_for_user_public_counterparty(
        self, invoice: Invoice, user: User, entry_accounting_information: EntryAccountingInformation
    ):
        invoices = Invoice.objects.filter_for_user(user)  # type: ignore
        assert invoice in invoices

    @pytest.mark.parametrize("user__is_superuser", [False])
    @pytest.mark.parametrize("user__user_permissions", [(["wbaccounting.view_entryaccountinginformation"])])
    @pytest.mark.parametrize("invoice__counterparty", [LazyFixture("entry")])
    @pytest.mark.parametrize("entry_accounting_information__entry", [LazyFixture("entry")])
    @pytest.mark.parametrize("entry_accounting_information__counterparty_is_private", [True])
    def test_filter_for_user_private_counterparty(
        self, user: User, invoice: Invoice, entry_accounting_information: EntryAccountingInformation
    ):
        invoices = Invoice.objects.filter_for_user(user)  # type: ignore
        assert invoice not in invoices

    @pytest.mark.parametrize("user__is_superuser", [False])
    @pytest.mark.parametrize("user__user_permissions", [(["wbaccounting.view_entryaccountinginformation"])])
    @pytest.mark.parametrize("invoice__counterparty", [LazyFixture("entry")])
    @pytest.mark.parametrize("entry_accounting_information__entry", [LazyFixture("entry")])
    @pytest.mark.parametrize("entry_accounting_information__counterparty_is_private", [True])
    def test_filter_for_user_private_counterparty_with_exempt(
        self, invoice: Invoice, user: User, entry_accounting_information: EntryAccountingInformation
    ):
        entry_accounting_information.exempt_users.add(user)
        entry_accounting_information_list = EntryAccountingInformation.objects.filter_for_user(user)  # type: ignore
        assert entry_accounting_information in entry_accounting_information_list

    @pytest.mark.parametrize("invoice__status", [Invoice.Status.DRAFT])
    @patch.object(EntryAccountingInformation, "get_booking_entry_generator")
    @patch.object(Invoice, "get_fx_rate")
    def test_refresh_booking_entry_draft(
        self, mock_get_fx_rate, mock_get_booking_entry_generator, invoice, booking_entry_factory
    ):
        mock_get_booking_entry_generator.return_value = TestGenerator
        mock_get_fx_rate.return_value = Decimal("1")

        EntryAccountingInformationFactory.create(entry=invoice.counterparty)
        already_assigned_booking_entry = booking_entry_factory.create(
            counterparty=invoice.counterparty,
            invoice=None,
            payment_date=date(2025, 1, 1),
            reference_date=invoice.reference_date,
        )
        unassigned_booking_entry = booking_entry_factory.create(
            counterparty=invoice.counterparty, invoice=None, payment_date=None, reference_date=invoice.reference_date
        )
        booking_entry_factory.create(
            counterparty=invoice.counterparty,
            invoice=None,
            payment_date=None,
            reference_date=invoice.reference_date + timedelta(days=1),
        )
        invoiced_booking_entry = booking_entry_factory.create(
            invoice=invoice,
            counterparty=invoice.counterparty,
            payment_date=None,
            reference_date=invoice.reference_date,
        )
        correcting_booking_entry = booking_entry_factory.create(
            initial_booking_entry=invoiced_booking_entry,
            invoice=None,
            counterparty=invoice.counterparty,
            payment_date=None,
            reference_date=invoice.reference_date,
        )

        expected_new_correcting_booking_entry = (
            invoiced_booking_entry.gross_value + correcting_booking_entry.gross_value
        )
        invoice.refresh_booking_entry()

        unassigned_booking_entry.refresh_from_db()
        correcting_booking_entry.refresh_from_db()
        already_assigned_booking_entry.refresh_from_db()

        # these entries should be assigned to the invoice now
        assert unassigned_booking_entry.invoice == invoice
        assert correcting_booking_entry.invoice == invoice

        # new gross value updated based on the existing invoiced booking entry and the invoiced entry deleted
        assert correcting_booking_entry.gross_value == expected_new_correcting_booking_entry
        with pytest.raises(BookingEntry.DoesNotExist):
            invoiced_booking_entry.refresh_from_db()

        # this entry should be kept not linked to the invoice as it's already paid
        assert already_assigned_booking_entry.invoice is None

        # check that a manually addded booking entry update automatically the max reference date
        booking_entry_factory.create(invoice=invoice, reference_date=invoice.reference_date + timedelta(days=1))
        initial_reference_date = invoice.reference_date
        invoice.refresh_booking_entry()
        assert invoice.reference_date == initial_reference_date + timedelta(days=1)
