"""Auth bind_contact + wechat_sync use cases unit tests.

Covers BindContactUseCase, SendVerificationCodeUseCase, WechatSyncUseCase.
"""

from __future__ import annotations

from dataclasses import dataclass, field
from datetime import datetime, timedelta, timezone
from unittest.mock import MagicMock

from packages.application.auth.bind_contact_use_case import (
    BindContactRequest,
    BindContactResponse,
    BindContactUseCase,
    SendVerificationCodeRequest,
    SendVerificationCodeResponse,
    SendVerificationCodeUseCase,
)
from packages.application.auth.wechat_sync_use_case import (
    WechatSyncRequest,
    WechatSyncResponse,
    WechatSyncUseCase,
)

# ── Test helpers ─────────────────────────────────────────


@dataclass
class FakeUser:
    id: str = "user-123"
    email: str = "test@example.com"
    display_name: str = "Test User"
    username: str = "testuser"
    password_hash: str = ""
    email_verified: bool = False
    phone: str = ""
    phone_verified: bool = False
    binding_completed_at: datetime | None = None
    last_login_at: datetime | None = None
    last_login_ip: str | None = None
    wechat_openid: str | None = None
    wechat_unionid: str | None = None
    email_verification_token: str | None = None
    password_reset_token: str | None = None
    password_reset_expires_at: datetime | None = None
    created_at: datetime = field(default_factory=lambda: datetime.now(timezone.utc))


class FakeUserRepository:
    def __init__(self, user=None):
        self._user = user
        self.saved_user = None
        self.save_called = 0

    def find_by_id(self, user_id):
        if self._user and self._user.id == user_id:
            return self._user
        return None

    def find_by_email(self, email):
        if self._user and self._user.email == email:
            return self._user
        return None

    def find_by_phone(self, phone):
        if self._user and self._user.phone == phone:
            return self._user
        return None

    def find_by_username(self, username):
        if self._user and self._user.username == username:
            return self._user
        return None

    def find_by_wechat_openid(self, openid):
        if self._user and self._user.wechat_openid == openid:
            return self._user
        return None

    def find_by_wechat_unionid(self, unionid):
        if self._user and self._user.wechat_unionid == unionid:
            return self._user
        return None

    def save(self, user):
        self.saved_user = user
        self.save_called += 1
        self._user = user
        return user


class FakeVerificationCode:
    def __init__(self, code="123456", created_at=None, expires_at=None):
        self.code = code
        self.created_at = created_at or datetime.now(timezone.utc)
        self.expires_at = expires_at or (datetime.now(timezone.utc) + timedelta(minutes=5))


class FakeVerificationCodeService:
    def __init__(self, verify_success=True, verify_error=None, generate_code="123456"):
        self._verify_success = verify_success
        self._verify_error = verify_error
        self._generate_code = generate_code
        self.verified = []
        self.generated = []

    def verify(self, recipient, code_type, code_value):
        self.verified.append(
            {
                "recipient": recipient,
                "code_type": code_type,
                "code_value": code_value,
            }
        )
        return self._verify_success, self._verify_error

    def generate(self, recipient, code_type):
        self.generated.append({"recipient": recipient, "code_type": code_type})
        return FakeVerificationCode(code=self._generate_code), None


class FakeSessionStore:
    def __init__(self):
        self.saved_sessions = []

    def save_session(self, **kwargs):
        self.saved_sessions.append(kwargs)
        return True


class FakeSmsService:
    def __init__(self):
        self.sent = []

    def send_verification_code(self, phone, code):
        self.sent.append({"phone": phone, "code": code})


class FakeEmailService:
    def __init__(self):
        self.sent = []

    def send_email(self, to, subject, body):
        self.sent.append({"to": to, "subject": subject, "body": body})


# ── BindContactUseCase tests ────────────────────────────


class TestBindContactUseCase:
    def _make_use_case(self, user_repo=None, verify_svc=None):
        return BindContactUseCase(
            user_repository=user_repo or FakeUserRepository(),
            verification_code_service=verify_svc or FakeVerificationCodeService(),
        )

    def test_bind_phone_success(self):
        user = FakeUser(id="user-1", phone="", phone_verified=False)
        repo = FakeUserRepository(user=user)
        verify_svc = FakeVerificationCodeService(verify_success=True)
        use_case = self._make_use_case(user_repo=repo, verify_svc=verify_svc)

        req = BindContactRequest(
            user_id="user-1",
            phone="13800138000",
            phone_code="123456",
        )
        response, error = use_case.execute(req)

        assert error is None
        assert response is not None
        assert isinstance(response, BindContactResponse)
        assert response.user.phone == "13800138000"
        assert response.user.phone_verified is True

        # verification was called
        assert len(verify_svc.verified) == 1
        assert verify_svc.verified[0]["recipient"] == "13800138000"

    def test_bind_email_success(self):
        user = FakeUser(id="user-1", email="old@example.com", email_verified=False)
        repo = FakeUserRepository(user=user)
        verify_svc = FakeVerificationCodeService(verify_success=True)
        use_case = self._make_use_case(user_repo=repo, verify_svc=verify_svc)

        req = BindContactRequest(
            user_id="user-1",
            email="new@example.com",
            email_code="123456",
        )
        response, error = use_case.execute(req)

        assert error is None
        assert response is not None
        assert response.user.email == "new@example.com"
        assert response.user.email_verified is True
        assert len(verify_svc.verified) == 1

    def test_bind_both_phone_and_email(self):
        user = FakeUser(id="user-1", phone="", phone_verified=False, email_verified=False)
        repo = FakeUserRepository(user=user)
        verify_svc = FakeVerificationCodeService(verify_success=True)
        use_case = self._make_use_case(user_repo=repo, verify_svc=verify_svc)

        req = BindContactRequest(
            user_id="user-1",
            phone="13800138000",
            phone_code="123456",
            email="new@example.com",
            email_code="123456",
        )
        response, error = use_case.execute(req)

        assert error is None
        assert response is not None
        assert response.user.phone == "13800138000"
        assert response.user.phone_verified is True
        assert response.user.email == "new@example.com"
        assert response.user.email_verified is True
        assert response.user.binding_completed_at is not None
        assert len(verify_svc.verified) == 2

    def test_binding_complete_with_real_email(self):
        """Both phone and email verified, real email (not wechat.local) → binding completed."""
        user = FakeUser(
            id="user-1",
            email="",
            phone="",
            phone_verified=False,
            email_verified=False,
        )
        repo = FakeUserRepository(user=user)
        verify_svc = FakeVerificationCodeService(verify_success=True)
        use_case = self._make_use_case(user_repo=repo, verify_svc=verify_svc)

        req = BindContactRequest(
            user_id="user-1",
            phone="13800138000",
            phone_code="123456",
            email="user@real.com",
            email_code="123456",
        )
        response, error = use_case.execute(req)

        assert error is None
        assert response.user.binding_completed_at is not None
        assert response.to_dict()["user"]["binding_complete"] is True

    def test_binding_not_complete_with_wechat_email(self):
        """WeChat placeholder email doesn't count for binding completion."""
        user = FakeUser(
            id="user-1",
            email="abc@wechat.local",
            email_verified=True,
            phone="",
            phone_verified=False,
        )
        repo = FakeUserRepository(user=user)
        verify_svc = FakeVerificationCodeService(verify_success=True)
        use_case = self._make_use_case(user_repo=repo, verify_svc=verify_svc)

        req = BindContactRequest(
            user_id="user-1",
            phone="13800138000",
            phone_code="123456",
        )
        response, error = use_case.execute(req)

        assert error is None
        # wechat.local email doesn't count
        assert response.user.binding_completed_at is None
        assert response.to_dict()["user"]["binding_complete"] is False

    def test_no_phone_no_email_returns_error(self):
        repo = FakeUserRepository()
        use_case = self._make_use_case(user_repo=repo)

        req = BindContactRequest(user_id="user-1")
        response, error = use_case.execute(req)

        assert response is None
        assert "至少填写手机号或邮箱" in error

    def test_user_not_found(self):
        repo = FakeUserRepository()  # no user
        use_case = self._make_use_case(user_repo=repo)

        req = BindContactRequest(user_id="nonexistent", phone="13800138000", phone_code="123")
        response, error = use_case.execute(req)

        assert response is None
        assert "用户不存在" in error

    def test_invalid_phone_format(self):
        user = FakeUser(id="user-1")
        repo = FakeUserRepository(user=user)
        use_case = self._make_use_case(user_repo=repo)

        req = BindContactRequest(user_id="user-1", phone="123", phone_code="123456")
        response, error = use_case.execute(req)

        assert response is None
        assert error is not None

    def test_invalid_email_format(self):
        user = FakeUser(id="user-1")
        repo = FakeUserRepository(user=user)
        use_case = self._make_use_case(user_repo=repo)

        req = BindContactRequest(user_id="user-1", email="not-an-email", email_code="123456")
        response, error = use_case.execute(req)

        assert response is None
        assert error is not None

    def test_phone_missing_code(self):
        user = FakeUser(id="user-1")
        repo = FakeUserRepository(user=user)
        use_case = self._make_use_case(user_repo=repo)

        req = BindContactRequest(user_id="user-1", phone="13800138000")
        response, error = use_case.execute(req)

        assert response is None
        assert "请输入手机验证码" in error

    def test_email_missing_code(self):
        user = FakeUser(id="user-1")
        repo = FakeUserRepository(user=user)
        use_case = self._make_use_case(user_repo=repo)

        req = BindContactRequest(user_id="user-1", email="a@b.com")
        response, error = use_case.execute(req)

        assert response is None
        assert "请输入邮箱验证码" in error

    def test_phone_already_bound_to_other_user(self):
        other_user = FakeUser(id="user-2", phone="13800138000")
        current_user = FakeUser(id="user-1", phone="")
        # repo only finds "other" user for phone lookup
        repo = FakeUserRepository(user=other_user)
        # But we also need find_by_id to find the current user
        # Our simple repo can only hold one user. Let's use MagicMock instead.
        repo = MagicMock()
        repo.find_by_id.return_value = current_user
        repo.find_by_phone.return_value = other_user

        verify_svc = FakeVerificationCodeService()
        use_case = BindContactUseCase(user_repository=repo, verification_code_service=verify_svc)

        req = BindContactRequest(user_id="user-1", phone="13800138000", phone_code="123456")
        response, error = use_case.execute(req)

        assert response is None
        assert "已被其他账号绑定" in error

    def test_email_already_bound_to_other_user(self):
        current_user = FakeUser(id="user-1", email="old@example.com")
        other_user = FakeUser(id="user-2", email="new@example.com")

        repo = MagicMock()
        repo.find_by_id.return_value = current_user
        repo.find_by_email.return_value = other_user

        verify_svc = FakeVerificationCodeService()
        use_case = BindContactUseCase(user_repository=repo, verification_code_service=verify_svc)

        req = BindContactRequest(user_id="user-1", email="new@example.com", email_code="123456")
        response, error = use_case.execute(req)

        assert response is None
        assert "已被其他账号绑定" in error

    def test_phone_verification_failed(self):
        user = FakeUser(id="user-1", phone="")
        repo = FakeUserRepository(user=user)
        verify_svc = FakeVerificationCodeService(verify_success=False, verify_error="验证码错误或已过期")
        use_case = self._make_use_case(user_repo=repo, verify_svc=verify_svc)

        req = BindContactRequest(user_id="user-1", phone="13800138000", phone_code="wrong")
        response, error = use_case.execute(req)

        assert response is None
        assert "手机验证码错误" in error

    def test_email_verification_failed(self):
        user = FakeUser(id="user-1")
        repo = FakeUserRepository(user=user)
        verify_svc = FakeVerificationCodeService(verify_success=False, verify_error="验证码错误")
        use_case = self._make_use_case(user_repo=repo, verify_svc=verify_svc)

        req = BindContactRequest(user_id="user-1", email="a@b.com", email_code="wrong")
        response, error = use_case.execute(req)

        assert response is None
        assert "邮箱验证码错误" in error

    def test_bind_same_phone_to_self_ok(self):
        """Binding the same phone to the same user should work (no conflict)."""
        user = FakeUser(id="user-1", phone="13800138000", phone_verified=False)
        repo = FakeUserRepository(user=user)
        verify_svc = FakeVerificationCodeService(verify_success=True)
        use_case = self._make_use_case(user_repo=repo, verify_svc=verify_svc)

        req = BindContactRequest(user_id="user-1", phone="13800138000", phone_code="123456")
        response, error = use_case.execute(req)

        assert error is None
        assert response is not None

    def test_exception_handling(self):
        repo = MagicMock()
        repo.find_by_id.side_effect = RuntimeError("DB down")
        verify_svc = FakeVerificationCodeService()
        use_case = BindContactUseCase(user_repository=repo, verification_code_service=verify_svc)

        req = BindContactRequest(user_id="user-1", phone="13800138000", phone_code="123")
        response, error = use_case.execute(req)

        assert response is None
        assert "绑定失败" in error


# ── SendVerificationCodeUseCase tests ───────────────────


class TestSendVerificationCodeUseCase:
    def test_send_phone_code_success(self):
        verify_svc = FakeVerificationCodeService(generate_code="654321")
        sms_svc = FakeSmsService()
        use_case = SendVerificationCodeUseCase(
            verification_code_service=verify_svc,
            sms_service=sms_svc,
        )

        req = SendVerificationCodeRequest(target="phone", value="13800138000", purpose="bind")
        response, error = use_case.execute(req)

        assert error is None
        assert response is not None
        assert isinstance(response, SendVerificationCodeResponse)
        assert response.expires_in > 0
        assert response.resend_after == 60

        assert len(verify_svc.generated) == 1
        assert verify_svc.generated[0]["code_type"] == "phone_bind"

        # SMS was sent
        assert len(sms_svc.sent) == 1
        assert sms_svc.sent[0]["phone"] == "13800138000"
        assert sms_svc.sent[0]["code"] == "654321"

    def test_send_email_code_success(self):
        verify_svc = FakeVerificationCodeService(generate_code="111222")
        email_svc = FakeEmailService()
        use_case = SendVerificationCodeUseCase(
            verification_code_service=verify_svc,
            email_service=email_svc,
        )

        req = SendVerificationCodeRequest(target="email", value="test@example.com", purpose="bind")
        response, error = use_case.execute(req)

        assert error is None
        assert response is not None

        assert len(verify_svc.generated) == 1
        assert verify_svc.generated[0]["code_type"] == "email_bind"

        assert len(email_svc.sent) == 1
        assert email_svc.sent[0]["to"] == "test@example.com"
        assert "111222" in email_svc.sent[0]["body"]

    def test_invalid_phone_format(self):
        verify_svc = FakeVerificationCodeService()
        use_case = SendVerificationCodeUseCase(verification_code_service=verify_svc)

        req = SendVerificationCodeRequest(target="phone", value="123", purpose="bind")
        response, error = use_case.execute(req)

        assert response is None
        assert error is not None

    def test_invalid_email_format(self):
        verify_svc = FakeVerificationCodeService()
        use_case = SendVerificationCodeUseCase(verification_code_service=verify_svc)

        req = SendVerificationCodeRequest(target="email", value="not-email", purpose="bind")
        response, error = use_case.execute(req)

        assert response is None
        assert error is not None

    def test_unsupported_target_type(self):
        verify_svc = FakeVerificationCodeService()
        use_case = SendVerificationCodeUseCase(verification_code_service=verify_svc)

        req = SendVerificationCodeRequest(target="carrier_pigeon", value="hello", purpose="bind")
        response, error = use_case.execute(req)

        assert response is None
        assert "不支持的目标类型" in error

    def test_email_recipient_lowercased(self):
        verify_svc = FakeVerificationCodeService()
        email_svc = FakeEmailService()
        use_case = SendVerificationCodeUseCase(
            verification_code_service=verify_svc,
            email_service=email_svc,
        )

        req = SendVerificationCodeRequest(target="email", value="TEST@Example.COM", purpose="login")
        response, error = use_case.execute(req)

        assert error is None
        # recipient should be lowercased
        assert verify_svc.generated[0]["recipient"] == "test@example.com"

    def test_no_sms_service_phone_still_returns_success(self):
        """If no SMS service is configured, code is generated but not sent."""
        verify_svc = FakeVerificationCodeService()
        use_case = SendVerificationCodeUseCase(verification_code_service=verify_svc)

        req = SendVerificationCodeRequest(target="phone", value="13800138000", purpose="login")
        response, error = use_case.execute(req)

        assert error is None
        assert response is not None
        # code was generated
        assert len(verify_svc.generated) == 1

    def test_exception_handling(self):
        verify_svc = MagicMock()
        verify_svc.generate.side_effect = RuntimeError("Redis down")
        use_case = SendVerificationCodeUseCase(verification_code_service=verify_svc)

        req = SendVerificationCodeRequest(target="email", value="a@b.com", purpose="bind")
        response, error = use_case.execute(req)

        assert response is None
        assert "发送失败" in error

    def test_to_dict_returns_correct_fields(self):
        resp = SendVerificationCodeResponse(expires_in=300, resend_after=60)
        d = resp.to_dict()
        assert d["expires_in"] == 300
        assert d["resend_after"] == 60


# ── WechatSyncUseCase tests ─────────────────────────────


class TestWechatSyncUseCase:
    def _make_use_case(self, user_repo=None, session_store=None, secret_key="test-secret-key-for-jwt"):
        return WechatSyncUseCase(
            user_repository=user_repo or FakeUserRepository(),
            session_store=session_store or FakeSessionStore(),
            jwt_secret_key=secret_key,
        )

    def test_existing_user_login_by_openid(self):
        user = FakeUser(
            id="user-1",
            wechat_openid="openid-abc",
            display_name="WeChat User",
        )
        repo = FakeUserRepository(user=user)
        store = FakeSessionStore()
        use_case = self._make_use_case(user_repo=repo, session_store=store)

        req = WechatSyncRequest(openid="openid-abc")
        response, error = use_case.execute(req)

        assert error is None
        assert response is not None
        assert isinstance(response, WechatSyncResponse)
        assert response.user_id == "user-1"
        assert response.is_new_user is False
        assert response.access_token
        assert response.refresh_token

        # session created
        assert len(store.saved_sessions) == 1
        session = store.saved_sessions[0]
        assert session["user_id"] == "user-1"
        assert "wechat_" in session["device_info"]

        # last login updated
        assert repo.saved_user.last_login_at is not None
        assert repo.saved_user.last_login_ip == "bff_gateway"

    def test_existing_user_by_unionid_binds_openid(self):
        user = FakeUser(
            id="user-1",
            wechat_openid=None,  # no openid
            wechat_unionid="unionid-xyz",
            display_name="Existing User",
        )
        repo = FakeUserRepository(user=user)
        use_case = self._make_use_case(user_repo=repo)

        req = WechatSyncRequest(openid="openid-new", unionid="unionid-xyz")
        response, error = use_case.execute(req)

        assert error is None
        assert response is not None
        assert response.user_id == "user-1"
        assert response.is_new_user is False

        # openid was bound
        assert repo.saved_user.wechat_openid == "openid-new"

    def test_new_user_creation(self):
        repo = FakeUserRepository()  # no existing user
        store = FakeSessionStore()
        use_case = self._make_use_case(user_repo=repo, session_store=store)

        req = WechatSyncRequest(
            openid="openid-new123",
            nickname="微信昵称",
            avatar_url="https://example.com/avatar.png",
        )
        response, error = use_case.execute(req)

        assert error is None
        assert response is not None
        assert response.is_new_user is True
        assert response.nickname == "微信昵称"
        assert response.user_id
        assert len(response.user_id) == 32  # uuid4 hex

        # user was saved
        assert repo.saved_user is not None
        assert repo.saved_user.wechat_openid == "openid-new123"
        assert repo.saved_user.email_verified is True
        assert "wechat.local" in repo.saved_user.email
        assert repo.saved_user.username.startswith("wx_")
        assert repo.saved_user.display_name == "微信昵称"

    def test_empty_openid_returns_error(self):
        use_case = self._make_use_case()
        req = WechatSyncRequest(openid="")
        response, error = use_case.execute(req)

        assert response is None
        assert "openid is required" in error

    def test_default_nickname_when_empty(self):
        repo = FakeUserRepository()
        use_case = self._make_use_case(user_repo=repo)

        req = WechatSyncRequest(openid="openid-1", nickname="")
        response, error = use_case.execute(req)

        assert error is None
        assert response.is_new_user is True
        assert response.nickname == "小虾同学"

    def test_username_uniqueness_suffix(self):
        """When username already exists, a numeric suffix is added."""
        # First user with same openid prefix
        existing = FakeUser(username="wx_openidnew123_")
        repo = FakeUserRepository(user=existing)

        # Our simple FakeUserRepository only holds one user.
        # Use MagicMock for more control.
        repo = MagicMock()
        repo.find_by_wechat_openid.return_value = None
        repo.find_by_wechat_unionid.return_value = None
        # first find_by_username returns a user (conflict), second time None (unique)
        call_count = {"n": 0}

        def mock_find_by_username(username):
            call_count["n"] += 1
            if call_count["n"] == 1:
                return FakeUser(username=username)  # conflict
            return None  # unique on second try

        repo.find_by_username.side_effect = mock_find_by_username
        repo.save = MagicMock(side_effect=lambda u: u)

        use_case = WechatSyncUseCase(
            user_repository=repo,
            session_store=FakeSessionStore(),
            jwt_secret_key="test-secret",
        )

        req = WechatSyncRequest(openid="openid-new123", nickname="Test")
        response, error = use_case.execute(req)

        assert error is None
        assert response.is_new_user is True
        # username should have _1 suffix
        saved_user = repo.save.call_args[0][0]
        assert saved_user.username.endswith("_1")

    def test_to_dict_has_token_alias_for_compat(self):
        user = FakeUser(id="u-1", wechat_openid="oid-1", display_name="Name")
        repo = FakeUserRepository(user=user)
        use_case = self._make_use_case(user_repo=repo)

        req = WechatSyncRequest(openid="oid-1")
        response, _ = use_case.execute(req)

        d = response.to_dict()
        assert d["access_token"] == d["token"]  # compat alias
        assert d["is_new_user"] is False
        assert d["user"]["id"] == "u-1"
        assert d["user_info"]["display_name"] == "Name"

    def test_access_token_has_correct_claims(self):
        import jwt as pyjwt

        user = FakeUser(id="user-99", wechat_openid="oid-99")
        repo = FakeUserRepository(user=user)
        use_case = self._make_use_case(user_repo=repo)

        req = WechatSyncRequest(openid="oid-99")
        response, _ = use_case.execute(req)

        payload = pyjwt.decode(response.access_token, "test-secret-key-for-jwt", algorithms=["HS256"])
        assert payload["sub"] == "user-99"
        assert payload["type"] == "user_auth"
        assert "sid" in payload
        assert "exp" in payload

    def test_exception_handling(self):
        repo = MagicMock()
        repo.find_by_wechat_openid.side_effect = RuntimeError("DB down")
        use_case = self._make_use_case(user_repo=repo)

        req = WechatSyncRequest(openid="abc")
        response, error = use_case.execute(req)

        assert response is None
        assert "Internal error" in error
