import uuid
from datetime import date
from decimal import Decimal

import psycopg
import pytest
from src.__types.errors import ResourceDuplicatedError, ResourceNotFoundError
from src.__types.index import Tenant
from src.domain.entities.quota_allocation import QuotaAllocation, QuotaAllocationStatus, QuotaAllocationType
from src.infrastructure.repositories.quota_repository import QuotaRepository


class TestQuotaRepository:
    # fixtureに存在するテナント
    EXISTING_TENANT_ID = uuid.UUID("00000001-0000-0000-0000-000000000000")

    # fixtureに存在するcontract_id
    EXISTING_CONTRACT_ID = "contract#alpha#03"

    @pytest.fixture
    def tenant(self) -> Tenant:
        return Tenant(
            id=TestQuotaRepository.EXISTING_TENANT_ID,
            m2m_application_client_id="application_client#alpha",
            legal_name="アルファ株式会社",
            whi_customer_id=["whi#alpha_1", "whi#alpha_2"],
            cydas_customer_id=["cydas#alpha"],
            allowed_m2m_metadata=[],
        )

    @pytest.fixture
    def repository(self, tenant: Tenant, db_connection: psycopg.Connection) -> QuotaRepository:
        return QuotaRepository(tenant=tenant, connection=db_connection)

    class TestGet:
        def test_returns_non_expired_allocations(
            self,
            repository: QuotaRepository,
        ) -> None:
            """EXPIREDを除くクォータ割り当てが取得できることを確認する。"""
            # Act
            quota = repository.get()

            # Assert: フィクスチャデータのアルファテナントはexpired×2、active×2、scheduled×1
            assert len(quota.allocations) == 3
            statuses = {a.status for a in quota.allocations}
            assert QuotaAllocationStatus.EXPIRED not in statuses

        def test_returns_empty_quota_when_no_allocations(
            self,
            db_connection: psycopg.Connection,
        ) -> None:
            """クォータ割り当てが存在しないテナントの場合、空のQuotaが返ることを確認する。"""
            # Arrange　フィクスチャデータに存在しない新規テナント
            no_allocation_tenant = Tenant(
                id=uuid.uuid4(),
                m2m_application_client_id="application_client#no_alloc",
                legal_name="割り当てなしテナント",
                whi_customer_id=[],
                cydas_customer_id=None,
                allowed_m2m_metadata=[],
            )
            repo = QuotaRepository(tenant=no_allocation_tenant, connection=db_connection)

            # Act
            quota = repo.get()

            # Assert
            assert quota.allocations == []

    class TestLockQuotaAllocation:
        def test_returns_allocation(
            self,
            repository: QuotaRepository,
        ) -> None:
            """存在するクォータ割り当てIDを指定してロック取得できることを確認する。"""
            # Arrange: フィクスチャデータに存在するallocation_id (contract#alpha#03 / active)
            existing_allocation_id = uuid.UUID("00000003-0000-0000-0000-000000000000")

            # Act
            allocation = repository.lock_quota_allocation(existing_allocation_id)

            # Assert
            assert allocation.id == existing_allocation_id
            assert allocation.status == QuotaAllocationStatus.ACTIVE

        def test_raises_when_not_found(
            self,
            repository: QuotaRepository,
        ) -> None:
            """存在しないIDを指定した場合はResourceNotFoundErrorを送出することを確認する。"""
            # Arrange
            non_existing_id = uuid.uuid4()

            # Act / Assert
            with pytest.raises(ResourceNotFoundError):
                repository.lock_quota_allocation(non_existing_id)

        def test_raises_when_allocation_belongs_to_other_tenant(
            self,
            db_connection: psycopg.Connection,
        ) -> None:
            """他テナントのクォータ割り当てIDを指定した場合はResourceNotFoundErrorを送出することを確認する。"""
            # Arrange　ベータテナントのallocation_idをアルファテナントのリポジトリで取得しようとする
            beta_allocation_id = uuid.UUID("00000006-0000-0000-0000-000000000000")
            alpha_repo = QuotaRepository(
                tenant=Tenant(
                    id=TestQuotaRepository.EXISTING_TENANT_ID,
                    m2m_application_client_id="application_client#alpha",
                    legal_name="アルファ株式会社",
                    whi_customer_id=["whi#alpha_1"],
                    cydas_customer_id=None,
                    allowed_m2m_metadata=[],
                ),
                connection=db_connection,
            )

            # Act / Assert
            with pytest.raises(ResourceNotFoundError):
                alpha_repo.lock_quota_allocation(beta_allocation_id)

    class TestAddQuotaAllocation:
        def test_success(
            self,
            repository: QuotaRepository,
            db_connection: psycopg.Connection,
        ) -> None:
            """新規のcontract_idでクォータ割り当てを追加できることを確認する。"""
            # Arrange
            allocation, contract_id = TestQuotaRepository._new_allocation("contract#new#unique")

            # Act
            repository.add_quota_allocation(allocation, contract_id=contract_id)

            # Assert
            with db_connection.cursor() as cur:
                cur.execute(
                    """--sql
                    SELECT id, tenant_id, contract_id
                    FROM quota_allocations
                    WHERE id = %(id)s
                    """,
                    {"id": allocation.id},
                )
                actual = cur.fetchone()
                assert actual is not None
                assert actual[0] == allocation.id
                assert actual[1] == TestQuotaRepository.EXISTING_TENANT_ID
                assert actual[2] == contract_id

        def test_raises_when_same_tenant_id_contract_id_and_dates(
            self,
            repository: QuotaRepository,
        ) -> None:
            """同一tenant_id・contract_id・有効期間の組み合わせでの重複追加はResourceDuplicatedErrorを送出することを確認する。"""
            # Arrange フィクスチャデータに既存のcontract_idおよびその有効期間(2025-07-01〜2026-06-30)を使用
            allocation, _ = TestQuotaRepository._new_allocation(
                valid_from_date=date(2025, 7, 1),
                valid_to_date=date(2026, 6, 30),
            )

            # Act / Assert
            with pytest.raises(ResourceDuplicatedError):
                repository.add_quota_allocation(allocation, contract_id=TestQuotaRepository.EXISTING_CONTRACT_ID)

        def test_success_with_same_contract_id_but_different_dates(
            self,
            repository: QuotaRepository,
        ) -> None:
            """同一tenant_idとcontract_idでも有効期間が異なる場合は追加できることを確認する。"""
            # Arrange フィクスチャデータに既存のcontract_idかつ異なる有効期間を使用
            allocation, _ = TestQuotaRepository._new_allocation(
                valid_from_date=date(2026, 7, 1),
                valid_to_date=date(2027, 6, 30),
            )

            # Act (例外が上がらないことを確認)
            repository.add_quota_allocation(allocation, contract_id=TestQuotaRepository.EXISTING_CONTRACT_ID)

        def test_success_with_same_contract_id_different_tenant(
            self,
            db_connection: psycopg.Connection,
        ) -> None:
            """同一contract_idでもtenant_idが異なる場合は追加できることを確認する。"""
            # Arrange フィクスチャデータとは別のテナントを用意
            other_tenant = Tenant(
                id=uuid.UUID("00000002-0000-0000-0000-000000000000"),
                m2m_application_client_id="application_client#beta",
                legal_name="ベータ株式会社",
                whi_customer_id=["whi#beta"],
                cydas_customer_id=None,
                allowed_m2m_metadata=[],
            )
            other_repo = QuotaRepository(tenant=other_tenant, connection=db_connection)
            allocation, _ = TestQuotaRepository._new_allocation()

            # Act　(例外が上がらないことを確認)
            other_repo.add_quota_allocation(allocation, contract_id=TestQuotaRepository.EXISTING_CONTRACT_ID)

    @classmethod
    def _new_allocation(
        cls,
        contract_id: str = "contract#new#01",
        valid_from_date: date = date(2026, 1, 1),
        valid_to_date: date = date(2026, 12, 31),
    ) -> tuple[QuotaAllocation, str]:
        allocation = QuotaAllocation(
            id=uuid.uuid4(),
            type=QuotaAllocationType.BASE,
            status=QuotaAllocationStatus.ACTIVE,
            amount=Decimal(10000),
            used=Decimal(0),
            valid_from_date=valid_from_date,
            valid_to_date=valid_to_date,
        )
        return allocation, contract_id
