import uuid
from unittest.mock import Mock

import pytest
from psycopg import Connection
from pytest_mock import MockFixture
from src.__types.errors import ClientError, ResourceDuplicatedError
from src.__types.index import Tenant
from src.domain.services.client_tenant_service import ClientTenantService
from src.domain.services.tenant_service import TenantService
from src.infrastructure.credential_manager import CredentialManager
from src.infrastructure.repositories.tenant_repository import TenantRepository


class TestTenantService:
    @pytest.fixture
    def mock_tenant_repository(self, mocker: MockFixture) -> Mock:
        return mocker.Mock(spec=TenantRepository)

    @pytest.fixture
    def mock_client_tenant_service(self, mocker: MockFixture) -> Mock:
        return mocker.Mock(spec=ClientTenantService)

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

    @pytest.fixture
    def mock_credential_manager(self, mocker: MockFixture) -> Mock:
        return mocker.Mock(spec=CredentialManager)

    @pytest.fixture
    def service(
        self,
        mock_tenant_repository: Mock,
        mock_client_tenant_service: Mock,
        mock_connection: Mock,
        mock_credential_manager: Mock,
    ) -> TenantService:
        return TenantService(
            tenant_repository=mock_tenant_repository,
            client_tenant_service=mock_client_tenant_service,
            connection=mock_connection,
            credential_manager=mock_credential_manager,
        )

    class TestIssue:
        @pytest.mark.parametrize(
            (
                "whi_customer_ids",
                "cydas_customer_ids",
                "expected_transfer_count",
            ),
            [
                (["whi#dummy_1"], [], 1),
                ([], ["cydas#dummy_1"], 1),
                (["whi#dummy_1", "whi#dummy_2"], ["cydas#dummy_1"], 3),
            ],
        )
        def test_should_create_and_return_tenant(
            self,
            service: TenantService,
            mocker: MockFixture,
            mock_tenant_repository: Mock,
            mock_client_tenant_service: Mock,
            mock_credential_manager: Mock,
            whi_customer_ids: list[str],
            cydas_customer_ids: list[str],
            expected_transfer_count: int,
        ) -> None:
            """Tenant発行時にApplication Clientを作成し、作成したTenantを返すことを確認する。"""
            # Arrange
            tenant_id = uuid.uuid4()
            mocker.patch("src.domain.services.tenant_service.uuid.uuid4", return_value=tenant_id)
            mock_credential_manager.issue_m2m.return_value = "application_client#dummy"
            expected_tenant = Tenant(
                id=tenant_id,
                legal_name="ダミー株式会社",
                name="dummy-name",
                m2m_application_client_id="application_client#dummy",
                whi_customer_id=whi_customer_ids,
                cydas_customer_id=cydas_customer_ids,
                allowed_m2m_metadata=[],
            )
            mock_tenant_repository.get.return_value = expected_tenant

            # Act
            actual = service.issue(
                legal_name="ダミー株式会社",
                name="dummy-name",
                whi_customer_ids=whi_customer_ids,
                cydas_customer_ids=cydas_customer_ids,
            )

            # Assert
            mock_credential_manager.issue_m2m.assert_called_once_with(tenant_id)
            mock_tenant_repository.add.assert_called_once_with(
                tenant_id=tenant_id,
                legal_name="ダミー株式会社",
                name="dummy-name",
                m2m_application_client_id="application_client#dummy",
                whi_customer_ids=whi_customer_ids,
                cydas_customer_ids=cydas_customer_ids,
            )
            assert mock_client_tenant_service.transfer.call_count == expected_transfer_count
            mock_tenant_repository.get.assert_called_once_with(tenant_id)
            assert actual == expected_tenant

        def test_should_raise_client_error_when_customer_ids_are_empty(
            self,
            service: TenantService,
            mock_tenant_repository: Mock,
            mock_client_tenant_service: Mock,
            mock_credential_manager: Mock,
        ) -> None:
            """取引先IDが空の場合にTenant発行がエラーになり移行が行われないことを確認する。"""
            # Act / Assert
            with pytest.raises(ClientError):
                service.issue(
                    legal_name="ダミー株式会社",
                    name="dummy-name",
                    whi_customer_ids=[],
                    cydas_customer_ids=[],
                )

            mock_credential_manager.issue_m2m.assert_not_called()
            mock_tenant_repository.add.assert_not_called()
            mock_client_tenant_service.transfer.assert_not_called()

        def test_should_cleanup_application_client_when_create_tenant_fails(
            self,
            service: TenantService,
            mocker: MockFixture,
            mock_credential_manager: Mock,
        ) -> None:
            """Tenant作成が失敗した場合に作成済みApplication Clientを削除することを確認する。"""
            # Arrange
            tenant_id = uuid.uuid4()
            mocker.patch("src.domain.services.tenant_service.uuid.uuid4", return_value=tenant_id)
            mock_credential_manager.issue_m2m.return_value = "application_client#dummy"
            mocker.patch.object(
                service,
                "_transfer",
                side_effect=ResourceDuplicatedError("duplicated"),
            )

            # Act / Assert
            with pytest.raises(ResourceDuplicatedError):
                service.issue(
                    legal_name="ダミー株式会社",
                    name="dummy-name",
                    whi_customer_ids=["whi#dummy_1"],
                    cydas_customer_ids=[],
                )

            mock_credential_manager.delete.assert_called_once_with(
                "application_client#dummy",
            )

        def test_should_reraise_original_error_when_cleanup_fails(
            self,
            service: TenantService,
            mocker: MockFixture,
            mock_credential_manager: Mock,
        ) -> None:
            """補償削除が失敗しても元の例外を再送出することを確認する。"""
            # Arrange
            tenant_id = uuid.uuid4()
            mocker.patch("src.domain.services.tenant_service.uuid.uuid4", return_value=tenant_id)
            mock_credential_manager.issue_m2m.return_value = "application_client#dummy"
            mocker.patch.object(
                service,
                "_transfer",
                side_effect=ResourceDuplicatedError("duplicated"),
            )
            mock_credential_manager.delete.side_effect = Exception(
                "cleanup failed",
            )

            # Act / Assert
            with pytest.raises(ResourceDuplicatedError):
                service.issue(
                    legal_name="ダミー株式会社",
                    name="dummy-name",
                    whi_customer_ids=["whi#dummy_1"],
                    cydas_customer_ids=[],
                )
