import uuid

import psycopg
import pytest
from src.__types.errors import ResourceDuplicatedError, ResourceNotFoundError
from src.__types.index import Tenant, User
from src.infrastructure.repositories.user_repository import UserRepository


class TestUserRepository:
    @pytest.fixture
    def tenant_alpha(self) -> Tenant:
        return Tenant(
            id=uuid.UUID("00000001-0000-0000-0000-000000000000"),
            m2m_application_client_id="application_client#alpha",
            legal_name="アルファ株式会社",
            name="alpha",
            whi_customer_id=["whi#alpha_1"],
            cydas_customer_id=["cydas#alpha"],
            allowed_m2m_metadata=[],
        )

    @pytest.fixture
    def tenant_beta(self) -> Tenant:
        return Tenant(
            id=uuid.UUID("00000002-0000-0000-0000-000000000000"),
            m2m_application_client_id="application_client#beta",
            legal_name="ベータ株式会社",
            name="beta",
            whi_customer_id=["whi#beta"],
            cydas_customer_id=[],
            allowed_m2m_metadata=[],
        )

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

    class TestCount:
        def test_returns_count_for_tenant(
            self,
            repository: UserRepository,
        ) -> None:
            """テナントに紐づくユーザーの総数を取得できることを確認する。"""
            actual = repository.count()

            assert actual == 3

        def test_returns_zero_for_empty_tenant(
            self,
            db_connection: psycopg.Connection,
        ) -> None:
            """ユーザーが存在しないテナントで0が返ることを確認する。"""
            empty_tenant = Tenant(
                id=uuid.uuid4(),
                m2m_application_client_id="application_client#empty",
                legal_name="空テナント",
                name="empty",
                whi_customer_id=[],
                cydas_customer_id=[],
                allowed_m2m_metadata=[],
            )
            repository = UserRepository(tenant=empty_tenant, connection=db_connection)

            actual = repository.count()

            assert actual == 0

    class TestFind:
        def test_returns_first_page(
            self,
            repository: UserRepository,
        ) -> None:
            """1ページ目のユーザーリストを取得できることを確認する。"""
            actual = repository.find(limit=2, offset=0)

            assert len(actual) == 2
            assert all(isinstance(user, User) for user in actual)

        def test_returns_second_page(
            self,
            repository: UserRepository,
        ) -> None:
            """2ページ目のユーザーリストを取得できることを確認する。"""
            actual = repository.find(limit=2, offset=2)

            assert len(actual) == 1

        def test_returns_empty_for_out_of_range_page(
            self,
            repository: UserRepository,
        ) -> None:
            """範囲外のページで空リストが返ることを確認する。"""
            actual = repository.find(limit=10, offset=1000)

            assert actual == []

        def test_orders_by_updated_at_desc_then_display_id_asc(
            self,
            repository: UserRepository,
        ) -> None:
            """updated_at降順、同一updated_atの場合はdisplay_id昇順で返ることを確認する。"""
            actual = repository.find(limit=100, offset=0)

            display_ids = [user.display_id for user in actual]
            assert display_ids == ["0001", "0002", "admin"]

        def test_isolates_by_tenant(
            self,
            db_connection: psycopg.Connection,
            tenant_beta: Tenant,
        ) -> None:
            """他テナントのユーザーが含まれないことを確認する。"""
            repository = UserRepository(tenant=tenant_beta, connection=db_connection)

            actual = repository.find(limit=100, offset=0)

            assert len(actual) == 1
            assert actual[0].display_id == "0001"
            assert actual[0].display_name == "社員001#ベータ株式会社"

    class TestFindAll:
        def test_returns_all_users_for_tenant(
            self,
            repository: UserRepository,
        ) -> None:
            """テナントに紐づく全ユーザーを取得できることを確認する。"""
            actual = repository.find_all()

            assert len(actual) == 3
            assert all(isinstance(user, User) for user in actual)

        def test_orders_by_updated_at_desc_then_display_id_asc(
            self,
            repository: UserRepository,
        ) -> None:
            """updated_at降順、同一updated_atの場合はdisplay_id昇順で返ることを確認する。"""
            actual = repository.find_all()

            display_ids = [user.display_id for user in actual]
            assert display_ids == ["0001", "0002", "admin"]

        def test_isolates_by_tenant(
            self,
            db_connection: psycopg.Connection,
            tenant_beta: Tenant,
        ) -> None:
            """他テナントのユーザーが含まれないことを確認する。"""
            repository = UserRepository(tenant=tenant_beta, connection=db_connection)

            actual = repository.find_all()

            assert len(actual) == 1
            assert actual[0].display_id == "0001"

        def test_returns_empty_for_empty_tenant(
            self,
            db_connection: psycopg.Connection,
        ) -> None:
            """ユーザーが存在しないテナントで空リストが返ることを確認する。"""
            empty_tenant = Tenant(
                id=uuid.uuid4(),
                m2m_application_client_id="application_client#empty",
                legal_name="空テナント",
                name="empty",
                whi_customer_id=[],
                cydas_customer_id=[],
                allowed_m2m_metadata=[],
            )
            repository = UserRepository(tenant=empty_tenant, connection=db_connection)

            actual = repository.find_all()

            assert actual == []

    class TestFindDuplicates:
        def test_returns_matching_users_by_display_id(
            self,
            repository: UserRepository,
        ) -> None:
            """display_idが一致するユーザーを返すことを確認する。"""
            actual = repository.find_duplicates(display_ids=["0001"], emails=[])

            assert len(actual) == 1
            assert actual[0].display_id == "0001"

        def test_returns_matching_users_by_email(
            self,
            repository: UserRepository,
        ) -> None:
            """emailが一致するユーザーを返すことを確認する。"""
            actual = repository.find_duplicates(display_ids=[], emails=["test001@test.com"])

            assert len(actual) == 1
            assert actual[0].email == "test001@test.com"

        def test_returns_union_of_display_id_and_email_matches(
            self,
            repository: UserRepository,
        ) -> None:
            """display_idとemailの両方で一致するユーザーをOR条件で返すことを確認する。"""
            actual = repository.find_duplicates(
                display_ids=["0001"],
                emails=["test002@test.com"],
            )

            assert len(actual) == 2

        def test_returns_empty_for_no_matches(
            self,
            repository: UserRepository,
        ) -> None:
            """一致するユーザーが存在しない場合に空リストを返すことを確認する。"""
            actual = repository.find_duplicates(
                display_ids=["nonexistent"],
                emails=["nonexistent@test.com"],
            )

            assert actual == []

        def test_isolates_by_tenant(
            self,
            db_connection: psycopg.Connection,
            tenant_beta: Tenant,
        ) -> None:
            """他テナントのユーザーが含まれないことを確認する。"""
            repository = UserRepository(tenant=tenant_beta, connection=db_connection)

            actual = repository.find_duplicates(display_ids=["0001"], emails=["test001@test.com"])

            assert len(actual) == 1
            assert actual[0].display_name == "社員001#ベータ株式会社"

    class TestGet:
        def test_returns_user(
            self,
            repository: UserRepository,
        ) -> None:
            """指定IDのユーザーを取得できることを確認する。"""
            user_id = uuid.UUID("00000001-0000-0000-0000-000000000002")

            actual = repository.get(user_id)

            assert actual == User(
                id=user_id,
                display_id="0002",
                display_name="社員002#アルファ株式会社",
                email="test002@test.com",
            )

        def test_raises_not_found_for_nonexistent_id(
            self,
            repository: UserRepository,
        ) -> None:
            """存在しないIDでResourceNotFoundErrorが発生することを確認する。"""
            with pytest.raises(ResourceNotFoundError):
                repository.get(uuid.uuid4())

        def test_raises_not_found_for_other_tenant_user(
            self,
            repository: UserRepository,
        ) -> None:
            """他テナントのユーザーIDでResourceNotFoundErrorが発生することを確認する。"""
            other_tenant_user_id = uuid.UUID("00000002-0000-0000-0000-000000000000")

            with pytest.raises(ResourceNotFoundError):
                repository.get(other_tenant_user_id)

    class TestCreate:
        def test_creates_user(
            self,
            repository: UserRepository,
        ) -> None:
            """新規ユーザーを作成できることを確認する。"""
            repository.create(
                display_id="0003",
                display_name="社員003#アルファ株式会社",
                email="test003@test.com",
            )

            created = repository.find(limit=10, offset=0)
            assert any(user.display_id == "0003" and user.email == "test003@test.com" for user in created)

        def test_returns_created_user(
            self,
            repository: UserRepository,
        ) -> None:
            """作成したユーザーがUser型で正しいフィールドを持つことを確認する。"""
            actual = repository.create(
                display_id="0004",
                display_name="社員004#アルファ株式会社",
                email="test004@test.com",
            )

            assert isinstance(actual, User)
            assert actual.display_id == "0004"
            assert actual.display_name == "社員004#アルファ株式会社"
            assert actual.email == "test004@test.com"
            assert actual.id is not None

        def test_raises_duplicated_error_for_display_id(
            self,
            repository: UserRepository,
        ) -> None:
            """display_id重複時にdisplay_id重複として扱われることを確認する。"""
            with pytest.raises(ResourceDuplicatedError, match=r"User with display_id for tenant ID"):
                repository.create(
                    display_id="0001",
                    display_name="重複display_idユーザー",
                    email="unique-display-id@test.com",
                )

        def test_raises_duplicated_error_for_email(
            self,
            repository: UserRepository,
        ) -> None:
            """email重複時にemail重複として扱われることを確認する。"""
            with pytest.raises(ResourceDuplicatedError, match=r"User with email for tenant ID"):
                repository.create(
                    display_id="0099",
                    display_name="重複emailユーザー",
                    email="test001@test.com",
                )

    class TestUpdate:
        def test_updates_user(
            self,
            repository: UserRepository,
        ) -> None:
            """ユーザー情報を更新できることを確認する。"""
            user_id = uuid.UUID("00000001-0000-0000-0000-000000000002")

            actual = repository.update(
                user_id=user_id,
                display_id="0002-updated",
                display_name="更新済み社員002",
                email="updated002@test.com",
            )

            assert actual == User(
                id=user_id,
                display_id="0002-updated",
                display_name="更新済み社員002",
                email="updated002@test.com",
            )

        def test_raises_not_found_for_nonexistent_id(
            self,
            repository: UserRepository,
        ) -> None:
            """存在しないIDでResourceNotFoundErrorが発生することを確認する。"""
            with pytest.raises(ResourceNotFoundError):
                repository.update(
                    user_id=uuid.uuid4(),
                    display_id="0099",
                    display_name="存在しないユーザー",
                    email="nonexistent@test.com",
                )

        def test_raises_not_found_for_other_tenant_user(
            self,
            repository: UserRepository,
        ) -> None:
            """他テナントのユーザーIDでResourceNotFoundErrorが発生することを確認する。"""
            other_tenant_user_id = uuid.UUID("00000002-0000-0000-0000-000000000000")

            with pytest.raises(ResourceNotFoundError):
                repository.update(
                    user_id=other_tenant_user_id,
                    display_id="0099",
                    display_name="他テナントユーザー",
                    email="other-tenant@test.com",
                )

        def test_raises_duplicated_error_for_display_id(
            self,
            repository: UserRepository,
        ) -> None:
            """display_id重複時にdisplay_id重複として扱われることを確認する。"""
            user_id = uuid.UUID("00000001-0000-0000-0000-000000000002")

            with pytest.raises(ResourceDuplicatedError, match=r"User with display_id for tenant ID"):
                repository.update(
                    user_id=user_id,
                    display_id="0001",
                    display_name="重複display_idユーザー",
                    email="unique-update@test.com",
                )

        def test_raises_duplicated_error_for_email(
            self,
            repository: UserRepository,
        ) -> None:
            """email重複時にemail重複として扱われることを確認する。"""
            user_id = uuid.UUID("00000001-0000-0000-0000-000000000002")

            with pytest.raises(ResourceDuplicatedError, match=r"User with email for tenant ID"):
                repository.update(
                    user_id=user_id,
                    display_id="0002",
                    display_name="重複emailユーザー",
                    email="test001@test.com",
                )

    class TestBulkDelete:
        def test_bulk_delete_users(
            self,
            repository: UserRepository,
        ) -> None:
            """複数のユーザーを一括削除できることを確認する。"""
            user_ids = [
                uuid.UUID("00000001-0000-0000-0000-000000000001"),
                uuid.UUID("00000001-0000-0000-0000-000000000002"),
            ]

            repository.bulk_delete(user_ids=user_ids)

            for user_id in user_ids:
                with pytest.raises(ResourceNotFoundError):
                    repository.get(user_id)

        def test_does_not_delete_other_tenant_users(
            self,
            db_connection: psycopg.Connection,
            repository: UserRepository,
            tenant_beta: Tenant,
        ) -> None:
            """他テナントのユーザーIDを含めても他テナントのユーザーは削除されない。"""
            other_tenant_user_id = uuid.UUID("00000002-0000-0000-0000-000000000000")

            repository.bulk_delete(user_ids=[other_tenant_user_id])

            beta_repository = UserRepository(tenant=tenant_beta, connection=db_connection)
            beta_repository.get(other_tenant_user_id)

        def test_does_not_raise_for_empty_list(
            self,
            repository: UserRepository,
        ) -> None:
            """空リストを指定してもエラーにならないことを確認する。"""
            repository.bulk_delete(user_ids=[])

        def test_does_not_raise_for_nonexistent_ids(
            self,
            repository: UserRepository,
        ) -> None:
            """存在しないIDを含んでもエラーにならないことを確認する。"""
            repository.bulk_delete(user_ids=[uuid.uuid4(), uuid.uuid4()])
