import uuid
from unittest.mock import Mock

import pytest
from pytest_mock import MockFixture
from src.__types.errors import BulkDuplicatedError, IdentityProviderError, ResourceDuplicatedError
from src.__types.index import User, UserWriteAttribute
from src.infrastructure.repositories.user_identity_repository import UserIdentityRepository
from src.infrastructure.repositories.user_repository import UserRepository
from src.usecases.user_command_usecase import UserCommandUseCase


def _userpool_error() -> IdentityProviderError:
    return IdentityProviderError("Identity provider error")


class TestUserCommandUseCase:
    @pytest.fixture
    def mock_user_repository(self, mocker: MockFixture) -> Mock:
        return mocker.Mock(spec=UserRepository)

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

    @pytest.fixture
    def usecase(
        self,
        mock_user_repository: Mock,
        mock_user_identity_repository: Mock,
    ) -> UserCommandUseCase:
        return UserCommandUseCase(
            user_repository=mock_user_repository,
            user_identity_repository=mock_user_identity_repository,
        )

    class TestAdd:
        def test_creates_user_and_identity(
            self,
            usecase: UserCommandUseCase,
            mock_user_repository: Mock,
            mock_user_identity_repository: Mock,
        ) -> None:
            """ユーザーをリポジトリに作成し、外部ID基盤にも作成されることを確認する。"""
            # Arrange
            created_user = User(
                id=uuid.uuid4(),
                display_id="0001",
                display_name="テストユーザー",
                email="test@example.com",
            )
            mock_user_repository.create.return_value = created_user

            # Act
            usecase.add(
                display_id="0001",
                display_name="テストユーザー",
                email="test@example.com",
            )

            # Assert
            mock_user_repository.create.assert_called_once_with(
                display_id="0001",
                display_name="テストユーザー",
                email="test@example.com",
            )
            mock_user_identity_repository.create_user.assert_called_once_with(user=created_user)

        def test_compensates_on_identity_failure(
            self,
            usecase: UserCommandUseCase,
            mock_user_repository: Mock,
            mock_user_identity_repository: Mock,
        ) -> None:
            """外部ID基盤の作成失敗時、DBレコードを削除して補償する。"""
            # Arrange
            created_user = User(
                id=uuid.uuid4(),
                display_id="0001",
                display_name="テストユーザー",
                email="test@example.com",
            )
            mock_user_repository.create.return_value = created_user
            mock_user_identity_repository.create_user.side_effect = _userpool_error()

            # Act & Assert
            with pytest.raises(IdentityProviderError):
                usecase.add(
                    display_id="0001",
                    display_name="テストユーザー",
                    email="test@example.com",
                )

            mock_user_repository.bulk_delete.assert_called_once_with(user_ids=[created_user.id])

        def test_compensates_on_userpool_duplicate_error(
            self,
            usecase: UserCommandUseCase,
            mock_user_repository: Mock,
            mock_user_identity_repository: Mock,
        ) -> None:
            """UserPoolでゴーストユーザー重複を検出した場合、DBレコードを削除して補償する。"""
            # Arrange
            created_user = User(
                id=uuid.uuid4(),
                display_id="0001",
                display_name="テストユーザー",
                email="test@example.com",
            )
            mock_user_repository.create.return_value = created_user
            mock_user_identity_repository.create_user.side_effect = ResourceDuplicatedError(
                "User already exists in UserPool.",
                duplicated_field="display_id",
            )

            # Act & Assert
            with pytest.raises(ResourceDuplicatedError):
                usecase.add(
                    display_id="0001",
                    display_name="テストユーザー",
                    email="test@example.com",
                )

            mock_user_repository.bulk_delete.assert_called_once_with(user_ids=[created_user.id])

        def test_raises_original_error_when_cleanup_fails(
            self,
            usecase: UserCommandUseCase,
            mock_user_repository: Mock,
            mock_user_identity_repository: Mock,
        ) -> None:
            """UserPool失敗後のDB削除が失敗しても、元のIdentityProviderErrorが再送出される。"""
            # Arrange
            created_user = User(
                id=uuid.uuid4(),
                display_id="0001",
                display_name="テストユーザー",
                email="test@example.com",
            )
            mock_user_repository.create.return_value = created_user
            mock_user_identity_repository.create_user.side_effect = _userpool_error()
            mock_user_repository.bulk_delete.side_effect = RuntimeError("DB cleanup failed")

            # Act & Assert
            with pytest.raises(IdentityProviderError):
                usecase.add(
                    display_id="0001",
                    display_name="テストユーザー",
                    email="test@example.com",
                )

    class TestUpdate:
        def test_updates_user_and_identity(
            self,
            usecase: UserCommandUseCase,
            mock_user_repository: Mock,
            mock_user_identity_repository: Mock,
        ) -> None:
            """ユーザーをリポジトリで更新し、外部ID基盤も更新されることを確認する。"""
            # Arrange
            user_id = uuid.uuid4()
            old_user = User(
                id=user_id,
                display_id="0001",
                display_name="テストユーザー",
                email="test@example.com",
            )
            updated_user = User(
                id=user_id,
                display_id="0001-updated",
                display_name="更新済みユーザー",
                email="updated@example.com",
            )
            mock_user_repository.get.return_value = old_user
            mock_user_repository.update.return_value = updated_user

            # Act
            usecase.update(
                user_id=user_id,
                display_id="0001-updated",
                display_name="更新済みユーザー",
                email="updated@example.com",
            )

            # Assert
            mock_user_repository.update.assert_called_once_with(
                user_id=user_id,
                display_id="0001-updated",
                display_name="更新済みユーザー",
                email="updated@example.com",
            )
            mock_user_identity_repository.update_user.assert_called_once_with(user=updated_user)

        def test_skips_identity_update_when_attributes_unchanged(
            self,
            usecase: UserCommandUseCase,
            mock_user_repository: Mock,
            mock_user_identity_repository: Mock,
        ) -> None:
            """display_idとemailが変わっていなければ外部ID基盤の更新をスキップする。"""
            # Arrange
            user_id = uuid.uuid4()
            old_user = User(
                id=user_id,
                display_id="0001",
                display_name="テストユーザー",
                email="test@example.com",
            )
            updated_user = User(
                id=user_id,
                display_id="0001",
                display_name="表示名だけ変更",
                email="test@example.com",
            )
            mock_user_repository.get.return_value = old_user
            mock_user_repository.update.return_value = updated_user

            # Act
            usecase.update(
                user_id=user_id,
                display_id="0001",
                display_name="表示名だけ変更",
                email="test@example.com",
            )

            # Assert
            mock_user_identity_repository.update_user.assert_not_called()

        def test_raises_without_db_update_on_identity_failure(
            self,
            usecase: UserCommandUseCase,
            mock_user_repository: Mock,
            mock_user_identity_repository: Mock,
        ) -> None:
            """外部ID基盤の更新失敗時、DB更新は実行されずエラーが再送出される。"""
            # Arrange
            user_id = uuid.uuid4()
            old_user = User(
                id=user_id,
                display_id="0001",
                display_name="テストユーザー",
                email="test@example.com",
            )
            mock_user_repository.get.return_value = old_user
            mock_user_identity_repository.update_user.side_effect = _userpool_error()

            # Act & Assert
            with pytest.raises(IdentityProviderError):
                usecase.update(
                    user_id=user_id,
                    display_id="0001-updated",
                    display_name="更新済みユーザー",
                    email="updated@example.com",
                )

            # UserPool → DB の順であるため、UserPool失敗時にDB更新は呼ばれない
            mock_user_repository.update.assert_not_called()

        def test_rollbacks_userpool_on_db_failure(
            self,
            usecase: UserCommandUseCase,
            mock_user_repository: Mock,
            mock_user_identity_repository: Mock,
        ) -> None:
            """DB更新失敗時、外部ID基盤を旧データにロールバックする。"""
            # Arrange
            user_id = uuid.uuid4()
            old_user = User(
                id=user_id,
                display_id="0001",
                display_name="テストユーザー",
                email="test@example.com",
            )
            mock_user_repository.get.return_value = old_user
            mock_user_repository.update.side_effect = RuntimeError("DB error")

            # Act & Assert
            with pytest.raises(RuntimeError, match="DB error"):
                usecase.update(
                    user_id=user_id,
                    display_id="0001-updated",
                    display_name="更新済みユーザー",
                    email="updated@example.com",
                )

            # UserPool更新成功後にDB失敗 → UserPoolを旧データで復元
            assert mock_user_identity_repository.update_user.call_count == 2
            mock_user_identity_repository.update_user.assert_called_with(user=old_user)

        def test_raises_db_error_even_when_userpool_rollback_fails(
            self,
            usecase: UserCommandUseCase,
            mock_user_repository: Mock,
            mock_user_identity_repository: Mock,
        ) -> None:
            """DB失敗後のUserPoolロールバックが失敗しても、元のDB例外が再送出される。"""
            # Arrange
            user_id = uuid.uuid4()
            old_user = User(
                id=user_id,
                display_id="0001",
                display_name="テストユーザー",
                email="test@example.com",
            )
            mock_user_repository.get.return_value = old_user
            mock_user_repository.update.side_effect = RuntimeError("DB error")
            # 1回目 (正常更新) は成功、2回目 (ロールバック) は失敗
            mock_user_identity_repository.update_user.side_effect = [
                None,
                _userpool_error(),
            ]

            # Act & Assert
            with pytest.raises(RuntimeError, match="DB error"):
                usecase.update(
                    user_id=user_id,
                    display_id="0001-updated",
                    display_name="更新済みユーザー",
                    email="updated@example.com",
                )

    class TestBulkAddValidate:
        @pytest.fixture
        def users(self) -> list[UserWriteAttribute]:
            return [
                UserWriteAttribute(display_id="0001", display_name="ユーザー1", email="user1@example.com"),
                UserWriteAttribute(display_id="0002", display_name="ユーザー2", email="user2@example.com"),
            ]

        def test_passes_when_no_duplicates(
            self,
            usecase: UserCommandUseCase,
            mock_user_repository: Mock,
            users: list[UserWriteAttribute],
        ) -> None:
            """重複なしの場合、例外が発生しないことを確認する。"""
            mock_user_repository.find_duplicates.return_value = []

            usecase.bulk_add_validate(users=users)

            mock_user_repository.find_duplicates.assert_called_once_with(
                display_ids=["0001", "0002"],
                emails=["user1@example.com", "user2@example.com"],
            )

        def test_raises_on_display_id_duplicate(
            self,
            usecase: UserCommandUseCase,
            mock_user_repository: Mock,
            users: list[UserWriteAttribute],
        ) -> None:
            """display_idが既存データと重複した場合、BulkDuplicatedErrorが発生する。"""
            mock_user_repository.find_duplicates.return_value = [
                User(id=uuid.uuid4(), display_id="0001", display_name="既存", email="existing@example.com"),
            ]

            with pytest.raises(BulkDuplicatedError) as exc_info:
                usecase.bulk_add_validate(users=users)

            assert "users[0].display_id" in exc_info.value.duplicated_fields

        def test_raises_on_email_duplicate(
            self,
            usecase: UserCommandUseCase,
            mock_user_repository: Mock,
            users: list[UserWriteAttribute],
        ) -> None:
            """emailが既存データと重複した場合、BulkDuplicatedErrorが発生する。"""
            mock_user_repository.find_duplicates.return_value = [
                User(id=uuid.uuid4(), display_id="9999", display_name="既存", email="user2@example.com"),
            ]

            with pytest.raises(BulkDuplicatedError) as exc_info:
                usecase.bulk_add_validate(users=users)

            assert "users[1].email" in exc_info.value.duplicated_fields

        def test_raises_on_multiple_duplicates(
            self,
            usecase: UserCommandUseCase,
            mock_user_repository: Mock,
            users: list[UserWriteAttribute],
        ) -> None:
            """複数のフィールドが重複した場合、全件分のエラー情報を返す。"""
            mock_user_repository.find_duplicates.return_value = [
                User(id=uuid.uuid4(), display_id="0001", display_name="既存1", email="user2@example.com"),
            ]

            with pytest.raises(BulkDuplicatedError) as exc_info:
                usecase.bulk_add_validate(users=users)

            assert "users[0].display_id" in exc_info.value.duplicated_fields
            assert "users[1].email" in exc_info.value.duplicated_fields

    class TestBulkAdd:
        @pytest.fixture
        def users(self) -> list[UserWriteAttribute]:
            return [
                UserWriteAttribute(display_id="0001", display_name="ユーザー1", email="user1@example.com"),
                UserWriteAttribute(display_id="0002", display_name="ユーザー2", email="user2@example.com"),
            ]

        @pytest.mark.usefixtures("mock_user_repository", "mock_user_identity_repository")
        def test_delegates_to_user_service(
            self,
            usecase: UserCommandUseCase,
            users: list[UserWriteAttribute],
            mocker: MockFixture,
        ) -> None:
            """UserServiceのbulk_addに委譲されることを確認する。"""
            mock_bulk_add = mocker.patch(
                "src.usecases.user_command_usecase.UserService.bulk_add",
            )

            usecase.bulk_add(users=users)

            mock_bulk_add.assert_called_once_with(users=users)

    class TestBulkDelete:
        def test_deletes_users_from_db_and_userpool(
            self,
            usecase: UserCommandUseCase,
            mock_user_repository: Mock,
            mock_user_identity_repository: Mock,
        ) -> None:
            """DB一括削除後、UserPoolから各ユーザーを個別削除する。"""
            # Arrange
            user_ids = [uuid.uuid4(), uuid.uuid4(), uuid.uuid4()]

            # Act
            usecase.bulk_delete(user_ids=user_ids)

            # Assert
            mock_user_repository.bulk_delete.assert_called_once_with(user_ids=user_ids)
            assert mock_user_identity_repository.delete_user.call_count == 3
            for user_id in user_ids:
                mock_user_identity_repository.delete_user.assert_any_call(user_id=user_id)

        @pytest.mark.usefixtures("mock_user_repository")
        def test_continues_on_userpool_partial_failure(
            self,
            usecase: UserCommandUseCase,
            mock_user_identity_repository: Mock,
        ) -> None:
            """UserPool削除が一部失敗しても、残りのユーザー削除を続行する。"""
            # Arrange
            user_ids = [uuid.uuid4(), uuid.uuid4(), uuid.uuid4()]
            mock_user_identity_repository.delete_user.side_effect = [
                None,
                _userpool_error(),
                None,
            ]

            # Act & Assert
            with pytest.raises(IdentityProviderError, match="Failed to delete users in UserPool"):
                usecase.bulk_delete(user_ids=user_ids)

            # Assert — 全ユーザーに対して削除が試行される
            assert mock_user_identity_repository.delete_user.call_count == 3

        def test_does_not_rollback_db_on_userpool_failure(
            self,
            usecase: UserCommandUseCase,
            mock_user_repository: Mock,
            mock_user_identity_repository: Mock,
        ) -> None:
            """UserPool削除失敗時にDBの復元は行わない。"""
            # Arrange
            user_ids = [uuid.uuid4(), uuid.uuid4()]
            mock_user_identity_repository.delete_user.side_effect = _userpool_error()

            # Act & Assert
            with pytest.raises(IdentityProviderError, match="Failed to delete users in UserPool"):
                usecase.bulk_delete(user_ids=user_ids)

            # Assert — bulk_deleteは1回だけ (最初の削除のみ)、復元呼び出しなし
            mock_user_repository.bulk_delete.assert_called_once_with(user_ids=user_ids)

        def test_raises_on_db_failure(
            self,
            usecase: UserCommandUseCase,
            mock_user_repository: Mock,
            mock_user_identity_repository: Mock,
        ) -> None:
            """DB一括削除が失敗した場合、UserPool削除は実行されずエラーが再送出される。"""
            # Arrange
            user_ids = [uuid.uuid4(), uuid.uuid4()]
            mock_user_repository.bulk_delete.side_effect = RuntimeError("DB error")

            # Act & Assert
            with pytest.raises(RuntimeError, match="DB error"):
                usecase.bulk_delete(user_ids=user_ids)

            mock_user_identity_repository.delete_user.assert_not_called()
