from unittest.mock import Mock

import pytest
from fastapi.security import HTTPAuthorizationCredentials
from jose.exceptions import JWTError
from pytest_mock import MockFixture
from src.__types.errors import ClientError
from src.__types.index import Tenant, User
from src.routers.management.authorizer import user_authorizer

USER_PATH = "src.routers.management.authorizer.user_authorizer."


@pytest.fixture
def mock_connect(mocker: MockFixture) -> Mock:
    mock_connection = mocker.Mock()
    connect_mock = mocker.patch(f"{USER_PATH}psycopg.connect")
    connect_mock.return_value.__enter__.return_value = mock_connection
    return connect_mock


class TestUserAuthorizer:
    def test_get_user_tenant_success(
        self,
        monkeypatch: pytest.MonkeyPatch,
        mocker: MockFixture,
        credential: HTTPAuthorizationCredentials,
        mock_tenant: Tenant,
        mock_user: User,
        mock_connect: Mock,
    ) -> None:
        """ユーザーのIDトークンからテナント情報を取得できること"""
        monkeypatch.setattr(user_authorizer.settings, "management_user_pool_id", "ap-northeast-1_testpool")
        mocker.patch(f"{USER_PATH}get_kid", return_value="kid-123")
        mocker.patch(f"{USER_PATH}get_key", return_value=mocker.Mock())
        mocker.patch(
            f"{USER_PATH}jwt.decode",
            return_value={
                "custom:tenant_id": str(mock_tenant.id),
                "custom:user_id": str(mock_user.id),
                "token_use": "id",
            },
        )
        repository = mocker.patch(f"{USER_PATH}TenantRepository").return_value
        repository.get.return_value = mock_tenant

        tenant = user_authorizer.get_user_tenant(
            credential=credential,
            header_tenant_name=mock_tenant.name,
        )

        assert tenant == mock_tenant
        repository.get.assert_called_once_with(str(mock_tenant.id))
        mock_connect.assert_called_once()

    def test_get_user_tenant_raises_when_tenant_id_missing(
        self,
        monkeypatch: pytest.MonkeyPatch,
        mocker: MockFixture,
        credential: HTTPAuthorizationCredentials,
    ) -> None:
        """ユーザーのIDトークンにテナントIDがない場合、JWTErrorが発生すること"""
        monkeypatch.setattr(user_authorizer.settings, "management_user_pool_id", "ap-northeast-1_testpool")
        mocker.patch(f"{USER_PATH}get_kid", return_value="kid-123")
        mocker.patch(f"{USER_PATH}get_key", return_value=mocker.Mock())
        mocker.patch(
            f"{USER_PATH}jwt.decode",
            return_value={"token_use": "id"},
        )

        with pytest.raises(JWTError, match="Missing required claim: 'custom:tenant_id'"):
            user_authorizer.get_user_tenant(credential=credential, header_tenant_name="tenant-name")

    def test_get_user_tenant_raises_when_user_id_missing(
        self,
        monkeypatch: pytest.MonkeyPatch,
        mocker: MockFixture,
        credential: HTTPAuthorizationCredentials,
        mock_tenant: Tenant,
    ) -> None:
        """ユーザーのIDトークンにユーザーIDがない場合、JWTErrorが発生すること"""
        monkeypatch.setattr(user_authorizer.settings, "management_user_pool_id", "ap-northeast-1_testpool")
        mocker.patch(f"{USER_PATH}get_kid", return_value="kid-123")
        mocker.patch(f"{USER_PATH}get_key", return_value=mocker.Mock())
        mocker.patch(
            f"{USER_PATH}jwt.decode",
            return_value={"custom:tenant_id": str(mock_tenant.id), "token_use": "id"},
        )

        with pytest.raises(JWTError, match="Missing required claim: 'custom:user_id'"):
            user_authorizer.get_user_tenant(credential=credential, header_tenant_name="tenant-name")

    def test_get_user_tenant_raises_when_header_missing(
        self,
        credential: HTTPAuthorizationCredentials,
    ) -> None:
        """ヘッダーにテナント名がない場合、ClientErrorが発生すること"""
        with pytest.raises(ClientError):
            user_authorizer.get_user_tenant(credential=credential, header_tenant_name=None)

    def test_get_user_tenant_raises_when_issuer_not_configured(
        self,
        monkeypatch: pytest.MonkeyPatch,
        credential: HTTPAuthorizationCredentials,
    ) -> None:
        """ユーザープールのIssuerが設定されていない場合、ValueErrorが発生すること"""
        monkeypatch.setattr(user_authorizer.settings, "management_user_pool_id", None)

        with pytest.raises(ValueError, match=r"Management user issuer/JWKS URL is not configured"):
            user_authorizer.get_user_tenant(credential=credential, header_tenant_name="tenant-name")

    def test_get_user_tenant_raises_when_tenant_name_mismatch(
        self,
        monkeypatch: pytest.MonkeyPatch,
        mocker: MockFixture,
        credential: HTTPAuthorizationCredentials,
        mock_tenant: Tenant,
        mock_user: User,
        mock_connect: Mock,
    ) -> None:
        """IDトークンのテナントIDと、ヘッダーのテナント名が一致しない場合、JWTErrorが発生すること"""
        monkeypatch.setattr(user_authorizer.settings, "management_user_pool_id", "ap-northeast-1_testpool")
        mocker.patch(f"{USER_PATH}get_kid", return_value="kid-123")
        mocker.patch(f"{USER_PATH}get_key", return_value=mocker.Mock())
        mocker.patch(
            f"{USER_PATH}jwt.decode",
            return_value={
                "custom:tenant_id": str(mock_tenant.id),
                "custom:user_id": str(mock_user.id),
                "token_use": "id",
            },
        )
        repository = mocker.patch(f"{USER_PATH}TenantRepository").return_value
        repository.get.return_value = mock_tenant

        with pytest.raises(JWTError, match="does not match"):
            user_authorizer.get_user_tenant(credential=credential, header_tenant_name="wrong-name")

        mock_connect.assert_called_once()
