import unittest
from datetime import datetime
from unittest.mock import MagicMock, AsyncMock
from decimal import Decimal

from src.ezgg_lan_manager.services.AccountingService import AccountingService, InsufficientFundsError
from src.ezgg_lan_manager.types.Transaction import Transaction


class AccountingServiceTests(unittest.IsolatedAsyncioTestCase):
    def setUp(self) -> None:
        self.mock_database_service = MagicMock()
        self.mock_database_service.add_transaction = AsyncMock()
        self.accounting_service = AccountingService(self.mock_database_service)

    def test_importing_unit_under_test_works(self) -> None:
        """
        This test asserts that the object produced in setUp is AccountingService object,
        if that fails, there is no reason to execute any other test.
        """
        self.assertIsInstance(self.accounting_service, AccountingService)

    def test_making_string_from_euro_value_works_correctly(self) -> None:
        test_value = Decimal("134.66")
        expected_result = "134.66 €"
        self.assertEqual(expected_result, AccountingService.make_euro_string_from_decimal(test_value))

    def test_making_euro_string_from_negative_value_works_correctly(self) -> None:
        test_value = Decimal("-997.41")
        expected_result = "-997.41 €"
        self.assertEqual(expected_result, AccountingService.make_euro_string_from_decimal(test_value))

    def test_making_euro_string_from_less_than_ten_cents_works_correctly(self) -> None:
        test_value = Decimal("0.04")
        expected_result = "0.04 €"
        self.assertEqual(expected_result, AccountingService.make_euro_string_from_decimal(test_value))

    async def test_get_balance_correctly_adds_up_transactions(self) -> None:
        self.mock_database_service.get_all_transactions_for_user = AsyncMock(return_value=[
            Transaction(
                user_id=0,
                value=Decimal("0.05"),
                is_debit=True,
                reference="",
                transaction_date=datetime.now()
            ),
            Transaction(
                user_id=0,
                value=Decimal("0.99"),
                is_debit=False,
                reference="",
                transaction_date=datetime.now()
            ),
            Transaction(
                user_id=0,
                value=Decimal("1.01"),
                is_debit=False,
                reference="",
                transaction_date=datetime.now()
            ),
            Transaction(
                user_id=0,
                value=Decimal("0.77"),
                is_debit=True,
                reference="",
                transaction_date=datetime.now()
            ),
        ])
        expected_result = Decimal("1.18")
        actual_result = await self.accounting_service.get_balance(0)

        self.assertEqual(expected_result, actual_result)

    async def test_trying_to_remove_more_than_is_on_account_balance_raises_exception(self) -> None:
        user_balance = Decimal("1.00")
        balance_to_remove = Decimal("1.01")
        self.mock_database_service.get_all_transactions_for_user = AsyncMock(return_value=[
            Transaction(
                user_id=0,
                value=user_balance,
                is_debit=False,
                reference="",
                transaction_date=datetime.now()
            )
        ])

        with self.assertRaises(InsufficientFundsError):
            await self.accounting_service.remove_balance(0, balance_to_remove, "TestRef")

    async def test_trying_to_remove_less_than_is_on_account_balance_spawns_correct_transaction(self) -> None:
        user_balance = Decimal("1.01")
        balance_to_remove = Decimal("1.00")
        reference = "Yey, a reference"

        self.mock_database_service.get_all_transactions_for_user = AsyncMock(return_value=[
            Transaction(
                user_id=123,
                value=user_balance,
                is_debit=False,
                reference="",
                transaction_date=datetime.now()
            )
        ])

        await self.accounting_service.remove_balance(123, balance_to_remove, reference)

        # Async unittest API is still not perfect for retrieving a call value. So we force it to hand it over.
        last_transaction: Transaction = self.mock_database_service.add_transaction.call_args_list[-1].args[-1]

        self.assertEqual(123, last_transaction.user_id)
        self.assertEqual(balance_to_remove, last_transaction.value)
        self.assertEqual(True, last_transaction.is_debit)
        self.assertEqual(reference, last_transaction.reference)
