import datetime
import uuid
from decimal import Decimal
from unittest.mock import ANY, Mock, call
from zoneinfo import ZoneInfo

import pytest
from psycopg import Connection
from psycopg.errors import UniqueViolation
from pytest_mock import MockFixture
from src.__types.errors import PriceCodeNotFoundError
from src.__types.index import Client, ClientTenant, Price, Tenant, UsageEventRequestMessage, UsageEventRequestPayload
from src.domain.entities.quota import Quota
from src.domain.entities.quota_allocation import QuotaAllocation, QuotaAllocationStatus, QuotaAllocationType
from src.domain.entities.usage_event import UsageEvent
from src.infrastructure.repositories.price_repository import PriceRepository
from src.infrastructure.repositories.quota_repository import QuotaRepository
from src.infrastructure.repositories.usage_event_repository import UsageEventRepository
from src.infrastructure.usage_event_request_queue import UsageEventRequestQueue
from src.infrastructure.usage_event_request_storage import UsageEventRequestStorage
from src.usecases.usage_event_usecase import UsageEventUseCase


class TestUsageEventUsecase:
    @pytest.fixture
    def mock_usage_event_repository(self, mocker: MockFixture):
        return mocker.Mock(spec=UsageEventRepository)

    @pytest.fixture
    def mock_quota_repository(self, mocker: MockFixture):
        return mocker.Mock(spec=QuotaRepository)

    @pytest.fixture
    def mock_price_repository(self, mocker: MockFixture):
        mock = mocker.Mock(spec=PriceRepository)
        mock.get.return_value = Price(
            code="dummy_code",
            price=Decimal("0.01"),
        )
        return mock

    @pytest.fixture
    def mock_connection(self, mocker: MockFixture):
        mock = mocker.Mock(spec=Connection)
        mock.transaction.return_value = mocker.MagicMock()
        return mock

    @pytest.fixture
    def usecase(
        self,
        mock_usage_event_repository: UsageEventRepository,
        mock_quota_repository: QuotaRepository,
        mock_price_repository: PriceRepository,
        mock_connection: Connection,
    ):
        return UsageEventUseCase(
            repository=mock_usage_event_repository,
            quota_repository=mock_quota_repository,
            price_repository=mock_price_repository,
            connection=mock_connection,
        )

    @pytest.fixture
    def mock_storage(self, mocker: MockFixture):
        return mocker.Mock(spec=UsageEventRequestStorage)

    @pytest.fixture
    def mock_queue(self, mocker: MockFixture):
        return mocker.Mock(spec=UsageEventRequestQueue)

    timestamp = datetime.datetime(2024, 6, 1, 12, 0, 0, 0, tzinfo=ZoneInfo("Asia/Tokyo"))

    class TestValidate:
        """UsageEventUseCase#validateのテスト"""

        def test_success(self, usecase: UsageEventUseCase):
            """検証成功"""
            usecase.validate(code="dummy_code")

        def test_unknown_code(self, usecase: UsageEventUseCase, mock_price_repository: Mock):
            """使用区分が登録されていない"""
            mock_price_repository.get.side_effect = PriceCodeNotFoundError("Not found code: unknown_code")
            with pytest.raises(PriceCodeNotFoundError):
                usecase.validate(code="unknown_code")

    class TestConsumeUsageEventRequest:
        """UsageEventUseCase#consumeにおけるUsageEventRequestに関するテスト"""

        def test_unique(
            self,
            usecase: UsageEventUseCase,
            mock_usage_event_repository: Mock,
            mock_storage: Mock,
            mock_tenant: Tenant,
            mock_client: Client,
            mock_client_tenant: ClientTenant,
        ):
            """冪等キーが重複していない"""
            # Arrange
            usage_events = [
                UsageEvent(
                    code="dummy_code",
                    quantity=Decimal(100),
                    service="dummy_service",
                    feature="dummy_feature",
                ),
            ]
            mock_storage.get.return_value = UsageEventRequestPayload(
                tenant=mock_tenant,
                client=mock_client,
                client_tenant=mock_client_tenant,
                idempotency_key=uuid.uuid4(),
                request_events=[usage_events.model_dump(mode="json") for usage_events in usage_events],
                timestamp=TestUsageEventUsecase.timestamp,
            )

            # Act
            usecase.consume(
                message=UsageEventRequestMessage(
                    tenant=mock_tenant,
                    object_key="dummy/object/key.json",
                ),
                storage=mock_storage,
            )
            # Assert
            mock_usage_event_repository.put_usage_event_request.assert_called_once()

        def test_not_found_payload(
            self,
            usecase: UsageEventUseCase,
            mock_usage_event_repository: Mock,
            mock_storage: Mock,
            mock_tenant: Tenant,
        ):
            """使用イベントリクエストペイロードが見つからない時に処理が終了する"""
            # Arrange
            mock_storage.get.return_value = None
            # Act
            usecase.consume(
                message=UsageEventRequestMessage(
                    tenant=mock_tenant,
                    object_key="dummy/object/key.json",
                ),
                storage=mock_storage,
            )
            # Assert
            mock_storage.get.assert_called_once()
            mock_usage_event_repository.put_usage_event_request.assert_not_called()

        def test_duplicate_idempotency_key(
            self,
            usecase: UsageEventUseCase,
            mock_usage_event_repository: Mock,
            mock_storage: Mock,
            mock_tenant: Tenant,
            mock_client: Client,
            mock_client_tenant: ClientTenant,
        ):
            """冪等キーが重複している"""
            # Arrange
            usage_events = [
                UsageEvent(
                    code="dummy_code",
                    quantity=Decimal(100),
                    service="dummy_service",
                    feature="dummy_feature",
                ),
            ]
            mock_storage.get.return_value = UsageEventRequestPayload(
                tenant=mock_tenant,
                client=mock_client,
                client_tenant=mock_client_tenant,
                idempotency_key=uuid.uuid4(),
                request_events=[usage_events.model_dump(mode="json") for usage_events in usage_events],
                timestamp=TestUsageEventUsecase.timestamp,
            )
            mock_usage_event_repository.put_usage_event_request.side_effect = UniqueViolation("Duplicate")
            # Act
            usecase.consume(
                message=UsageEventRequestMessage(
                    tenant=mock_tenant,
                    object_key="dummy/object/key.json",
                ),
                storage=mock_storage,
            )
            # Assert
            mock_usage_event_repository.put_usage_event_request.assert_called_once()
            mock_usage_event_repository.put.assert_not_called()

    class TestConsumeQuotaUsage:
        """UsageEventUseCase#consumeにおけるQuotaUsageに関するテスト"""

        def test_single(
            self,
            usecase: UsageEventUseCase,
            mock_usage_event_repository: Mock,
            mock_quota_repository: Mock,
            mock_storage: Mock,
            mock_tenant: Tenant,
            mock_client: Client,
            mock_client_tenant: ClientTenant,
        ):
            """単一の使用イベントを記録する"""
            # Arrange
            quota_allocation = QuotaAllocation(
                id=uuid.uuid4(),
                type=QuotaAllocationType.BASE,
                amount=Decimal(100000),
                used=Decimal(20000),
                status=QuotaAllocationStatus.ACTIVE,
                valid_from_date=datetime.date(2024, 1, 1),
                valid_to_date=datetime.date(2024, 12, 31),
            )
            mock_quota_repository.get.return_value = Quota(
                allocations=[
                    quota_allocation,
                ],
            )
            mock_quota_repository.lock_quota_allocation.return_value = quota_allocation
            usage_events = [
                UsageEvent(
                    code="dummy_code",
                    quantity=Decimal(10),
                    service="dummy_service",
                    feature="dummy_feature",
                ),
            ]
            mock_storage.get.return_value = UsageEventRequestPayload(
                tenant=mock_tenant,
                client=mock_client,
                client_tenant=mock_client_tenant,
                idempotency_key=uuid.uuid4(),
                request_events=[usage_events.model_dump(mode="json") for usage_events in usage_events],
                timestamp=TestUsageEventUsecase.timestamp,
            )
            # Act
            usecase.consume(
                message=UsageEventRequestMessage(
                    tenant=mock_tenant,
                    object_key="dummy/object/key.json",
                ),
                storage=mock_storage,
            )
            # Assert
            mock_usage_event_repository.put_usage_event_request.assert_called_once()
            mock_usage_event_repository.put.assert_called_once()
            mock_usage_event_repository.put_quota_usage.assert_called_once_with(
                usage_event_id=ANY,
                quota_allocation=mock_quota_repository.lock_quota_allocation.return_value,
                unit=Decimal(10),
            )

        def test_multiple(
            self,
            usecase: UsageEventUseCase,
            mock_usage_event_repository: Mock,
            mock_quota_repository: Mock,
            mock_storage: Mock,
            mock_tenant: Tenant,
            mock_client: Client,
            mock_client_tenant: ClientTenant,
        ):
            """複数の使用イベントを記録する"""
            # Arrange
            quota_allocation = QuotaAllocation(
                id=uuid.uuid4(),
                type=QuotaAllocationType.BASE,
                amount=Decimal(100000),
                used=Decimal(20000),
                status=QuotaAllocationStatus.ACTIVE,
                valid_from_date=datetime.date(2024, 1, 1),
                valid_to_date=datetime.date(2024, 12, 31),
            )
            mock_quota_repository.get.return_value = Quota(
                allocations=[
                    quota_allocation,
                ],
            )
            mock_quota_repository.lock_quota_allocation.return_value = quota_allocation
            usage_events = [
                UsageEvent(
                    code="dummy_code",
                    quantity=Decimal(10),
                    service="dummy_service",
                    feature="dummy_feature",
                ),
                UsageEvent(
                    code="dummy_code",
                    quantity=Decimal(20),
                    service="dummy_service",
                    feature="dummy_feature",
                ),
            ]
            mock_storage.get.return_value = UsageEventRequestPayload(
                tenant=mock_tenant,
                client=mock_client,
                client_tenant=mock_client_tenant,
                idempotency_key=uuid.uuid4(),
                request_events=[usage_events.model_dump(mode="json") for usage_events in usage_events],
                timestamp=TestUsageEventUsecase.timestamp,
            )
            # Act
            usecase.consume(
                message=UsageEventRequestMessage(
                    tenant=mock_tenant,
                    object_key="dummy/object/key.json",
                ),
                storage=mock_storage,
            )
            # Assert
            assert mock_usage_event_repository.put_usage_event_request.call_count == 1
            assert mock_usage_event_repository.put.call_count == 2
            assert mock_usage_event_repository.put_quota_usage.call_count == 2
            mock_usage_event_repository.put_quota_usage.assert_has_calls(
                [
                    call(
                        usage_event_id=ANY,
                        quota_allocation=mock_quota_repository.lock_quota_allocation.return_value,
                        unit=Decimal(10),
                    ),
                    call(
                        usage_event_id=ANY,
                        quota_allocation=mock_quota_repository.lock_quota_allocation.return_value,
                        unit=Decimal(20),
                    ),
                ],
                any_order=True,
            )

        def test_large_with_no_quota_allocation(
            self,
            usecase: UsageEventUseCase,
            mock_usage_event_repository: Mock,
            mock_quota_repository: Mock,
            mock_storage: Mock,
            mock_tenant: Tenant,
            mock_client: Client,
            mock_client_tenant: ClientTenant,
        ):
            """クォータ割り当てが枯渇するような大きな使用イベントを記録する"""
            # Arrange
            first_quota = Quota(
                allocations=[
                    QuotaAllocation(
                        id=uuid.uuid4(),
                        type=QuotaAllocationType.BASE,
                        amount=Decimal(100000),
                        used=Decimal(20000),
                        status=QuotaAllocationStatus.ACTIVE,
                        valid_from_date=datetime.date(2023, 1, 1),
                        valid_to_date=datetime.date(2023, 12, 31),
                    ),
                ],
            )
            second_quota = Quota(
                allocations=[
                    QuotaAllocation(
                        id=uuid.uuid4(),
                        type=QuotaAllocationType.BASE,
                        amount=Decimal(100000),
                        used=Decimal(100000),
                        status=QuotaAllocationStatus.EXCEEDED,
                        valid_from_date=datetime.date(2023, 1, 1),
                        valid_to_date=datetime.date(2023, 12, 31),
                    ),
                ],
            )
            mock_quota_repository.get.side_effect = [
                first_quota,
                second_quota,
            ]
            mock_quota_repository.lock_quota_allocation.return_value = first_quota.allocations[0]
            usage_events = [
                UsageEvent(
                    code="dummy_code",
                    quantity=Decimal(100000),
                    service="dummy_service",
                    feature="dummy_feature",
                ),
            ]
            mock_storage.get.return_value = UsageEventRequestPayload(
                tenant=mock_tenant,
                client=mock_client,
                client_tenant=mock_client_tenant,
                idempotency_key=uuid.uuid4(),
                request_events=[usage_events.model_dump(mode="json") for usage_events in usage_events],
                timestamp=TestUsageEventUsecase.timestamp,
            )
            # Act
            usecase.consume(
                message=UsageEventRequestMessage(
                    tenant=mock_tenant,
                    object_key="dummy/object/key.json",
                ),
                storage=mock_storage,
            )
            # Assert
            mock_usage_event_repository.put_usage_event_request.assert_called_once()
            mock_usage_event_repository.put.assert_called_once()
            assert mock_usage_event_repository.put_quota_usage.call_count == 2
            mock_usage_event_repository.put_quota_usage.assert_has_calls(
                [
                    call(
                        usage_event_id=ANY,
                        quota_allocation=mock_quota_repository.lock_quota_allocation.return_value,
                        unit=Decimal(80000),
                    ),
                    call(
                        usage_event_id=ANY,
                        quota_allocation=None,
                        unit=Decimal(20000),
                    ),
                ],
                any_order=True,
            )

        def test_large_with_dual_quota_allocation(
            self,
            usecase: UsageEventUseCase,
            mock_usage_event_repository: Mock,
            mock_quota_repository: Mock,
            mock_storage: Mock,
            mock_tenant: Tenant,
            mock_client: Client,
            mock_client_tenant: ClientTenant,
        ):
            """複数のクォータ割り当てが存在し、両方から使用量を差し引くような大きな使用イベントを記録する"""
            # Arrange
            mock_quota_repository.get.side_effect = [
                Quota(
                    allocations=[
                        QuotaAllocation(
                            id=uuid.uuid4(),
                            type=QuotaAllocationType.BASE,
                            amount=Decimal(100000),
                            used=Decimal(20000),
                            status=QuotaAllocationStatus.ACTIVE,
                            valid_from_date=datetime.date(2024, 1, 1),
                            valid_to_date=datetime.date(2024, 12, 31),
                        ),
                        QuotaAllocation(
                            id=uuid.uuid4(),
                            type=QuotaAllocationType.TOP_UP,
                            amount=Decimal(100000),
                            used=Decimal(0),
                            status=QuotaAllocationStatus.ACTIVE,
                            valid_from_date=datetime.date(2024, 1, 1),
                            valid_to_date=datetime.date(2024, 12, 31),
                        ),
                    ],
                ),
                Quota(
                    allocations=[
                        QuotaAllocation(
                            id=uuid.uuid4(),
                            type=QuotaAllocationType.BASE,
                            amount=Decimal(100000),
                            used=Decimal(100000),
                            status=QuotaAllocationStatus.EXCEEDED,
                            valid_from_date=datetime.date(2024, 1, 1),
                            valid_to_date=datetime.date(2024, 12, 31),
                        ),
                        QuotaAllocation(
                            id=uuid.uuid4(),
                            type=QuotaAllocationType.TOP_UP,
                            amount=Decimal(100000),
                            used=Decimal(0),
                            status=QuotaAllocationStatus.ACTIVE,
                            valid_from_date=datetime.date(2024, 1, 1),
                            valid_to_date=datetime.date(2024, 12, 31),
                        ),
                    ],
                ),
            ]
            lock_quota_allocations = [
                QuotaAllocation(
                    id=uuid.uuid4(),
                    type=QuotaAllocationType.BASE,
                    amount=Decimal(100000),
                    used=Decimal(20000),
                    status=QuotaAllocationStatus.ACTIVE,
                    valid_from_date=datetime.date(2024, 1, 1),
                    valid_to_date=datetime.date(2024, 12, 31),
                ),
                QuotaAllocation(
                    id=uuid.uuid4(),
                    type=QuotaAllocationType.TOP_UP,
                    amount=Decimal(100000),
                    used=Decimal(0),
                    status=QuotaAllocationStatus.ACTIVE,
                    valid_from_date=datetime.date(2024, 1, 1),
                    valid_to_date=datetime.date(2024, 12, 31),
                ),
            ]
            mock_quota_repository.lock_quota_allocation.side_effect = lock_quota_allocations
            usage_events = [
                UsageEvent(
                    code="dummy_code",
                    quantity=Decimal(100000),
                    service="dummy_service",
                    feature="dummy_feature",
                ),
            ]
            mock_storage.get.return_value = UsageEventRequestPayload(
                tenant=mock_tenant,
                client=mock_client,
                client_tenant=mock_client_tenant,
                idempotency_key=uuid.uuid4(),
                request_events=[usage_events.model_dump(mode="json") for usage_events in usage_events],
                timestamp=TestUsageEventUsecase.timestamp,
            )
            # Act
            usecase.consume(
                message=UsageEventRequestMessage(
                    tenant=mock_tenant,
                    object_key="dummy/object/key.json",
                ),
                storage=mock_storage,
            )
            # Assert
            mock_usage_event_repository.put_usage_event_request.assert_called_once()
            mock_usage_event_repository.put.assert_called_once()
            assert mock_usage_event_repository.put_quota_usage.call_count == 2
            mock_usage_event_repository.put_quota_usage.assert_has_calls(
                [
                    call(
                        usage_event_id=ANY,
                        quota_allocation=lock_quota_allocations[0],
                        unit=Decimal(80000),
                    ),
                    call(
                        usage_event_id=ANY,
                        quota_allocation=lock_quota_allocations[1],
                        unit=Decimal(20000),
                    ),
                ],
                any_order=True,
            )
