"""微信同步登录 UseCase 单元测试."""

from __future__ import annotations

from unittest.mock import MagicMock

import pytest

from packages.application.auth.wechat_sync_use_case import (
    WechatSyncRequest,
    WechatSyncResponse,
    WechatSyncUseCase,
)
from packages.domain.entities import User

JWT_KEY = "test-secret-key-for-jwt-12345"


@pytest.fixture
def mock_user_repo():
    repo = MagicMock()
    # 默认全部查不到，具体用例再覆盖
    repo.find_by_wechat_openid.return_value = None
    repo.find_by_wechat_unionid.return_value = None
    repo.find_by_username.return_value = None
    return repo


@pytest.fixture
def mock_session_store():
    return MagicMock()


@pytest.fixture
def sample_user():
    user = User(
        id="user_001",
        email="test@wechat.local",
        username="wx_test123",
        display_name="微信用户",
        password_hash="hashed",
        email_verified=True,
    )
    user.wechat_openid = "openid_123"
    user.wechat_unionid = "unionid_456"
    user.last_login_at = None
    user.last_login_ip = None
    return user


def make_use_case(repo, store):
    return WechatSyncUseCase(repo, session_store=store, jwt_secret_key=JWT_KEY)


class TestWechatSyncRequest:
    def test_openid_stripped(self):
        assert WechatSyncRequest(openid="  openid_123  ").openid == "openid_123"

    def test_unionid_stripped(self):
        assert WechatSyncRequest(openid="o1", unionid="  unionid_456  ").unionid == "unionid_456"

    def test_default_nickname(self):
        # 微信新规拿不到真实昵称，新用户统一默认"小虾同学"
        assert WechatSyncRequest(openid="o1").nickname == "小虾同学"

    def test_default_source(self):
        assert WechatSyncRequest(openid="o1").source == "miniapp"

    def test_empty_unionid(self):
        assert WechatSyncRequest(openid="o1").unionid == ""


class TestWechatSyncResponse:
    def test_to_dict_contains_fields(self):
        resp = WechatSyncResponse(
            access_token="access_123",
            refresh_token="refresh_456",
            user_id="user_001",
            nickname="测试用户",
            avatar_url="https://example.com/avatar.jpg",
            is_new_user=False,
            expires_in=1800,
        )
        data = resp.to_dict()
        assert data["access_token"] == "access_123"
        assert data["token"] == "access_123"
        assert data["refresh_token"] == "refresh_456"
        assert data["user_id"] == "user_001"
        assert data["is_new_user"] is False
        assert data["expires_in"] == 1800
        assert data["user"]["id"] == "user_001"
        assert data["user"]["display_name"] == "测试用户"


class TestWechatSyncLoginExisting:
    def test_login_by_openid(self, mock_user_repo, mock_session_store, sample_user):
        """openid 命中、unionid 一致，正常登录"""
        mock_user_repo.find_by_wechat_openid.return_value = sample_user
        mock_user_repo.find_by_wechat_unionid.return_value = sample_user
        resp, err = make_use_case(mock_user_repo, mock_session_store).execute(
            WechatSyncRequest(openid="openid_123", unionid="unionid_456")
        )
        assert err is None
        assert resp.user_id == "user_001"
        assert resp.is_new_user is False

    def test_backfill_unionid_for_legacy_openid_user(self, mock_user_repo, mock_session_store):
        """核心修复：openid 命中的老账号没有 unionid，请求带 unionid 时补写"""
        legacy = User(
            id="legacy_001",
            email="legacy@wechat.local",
            username="wx_legacy",
            display_name="微信用户",
            password_hash="h",
            email_verified=True,
            wechat_openid="oGjxK3_old",
            wechat_unionid=None,
        )
        mock_user_repo.find_by_wechat_openid.return_value = legacy
        # unionid 查找：补写前确认无其他账号占用
        mock_user_repo.find_by_wechat_unionid.return_value = None

        resp, err = make_use_case(mock_user_repo, mock_session_store).execute(
            WechatSyncRequest(openid="oGjxK3_old", unionid="o5nVk_union")
        )
        assert err is None
        assert resp.user_id == "legacy_001"
        assert resp.is_new_user is False
        assert legacy.wechat_unionid == "o5nVk_union"
        # 至少保存过一次（补写 + 最后登录更新）
        mock_user_repo.save.assert_called()

    def test_login_by_unionid_binds_new_openid(self, mock_user_repo, mock_session_store, sample_user):
        """unionid 命中（跨端老用户），openid 未绑定过 → 绑定新 openid"""
        sample_user.wechat_openid = None
        mock_user_repo.find_by_wechat_openid.return_value = None
        mock_user_repo.find_by_wechat_unionid.return_value = sample_user

        resp, err = make_use_case(mock_user_repo, mock_session_store).execute(
            WechatSyncRequest(openid="new_openid", unionid="unionid_456", nickname="测试")
        )
        assert err is None
        assert resp.is_new_user is False
        assert sample_user.wechat_openid == "new_openid"

    def test_unionid_user_already_has_same_openid_no_extra_write(self, mock_user_repo, mock_session_store, sample_user):
        """unionid 命中且 openid 已经是当前 openid，不额外改写"""
        mock_user_repo.find_by_wechat_openid.return_value = sample_user
        mock_user_repo.find_by_wechat_unionid.return_value = sample_user
        saved = []
        mock_user_repo.save.side_effect = lambda u: saved.append(u)
        make_use_case(mock_user_repo, mock_session_store).execute(
            WechatSyncRequest(openid="openid_123", unionid="unionid_456")
        )
        # 只有最后登录信息那一次 save，没有绑定/补写导致的额外 save
        assert len(saved) == 1

    def test_updates_last_login(self, mock_user_repo, mock_session_store, sample_user):
        mock_user_repo.find_by_wechat_openid.return_value = sample_user
        mock_user_repo.find_by_wechat_unionid.return_value = sample_user
        make_use_case(mock_user_repo, mock_session_store).execute(WechatSyncRequest(openid="openid_123"))
        assert sample_user.last_login_at is not None
        assert sample_user.last_login_ip == "bff_gateway"

    def test_returns_tokens(self, mock_user_repo, mock_session_store, sample_user):
        mock_user_repo.find_by_wechat_openid.return_value = sample_user
        mock_user_repo.find_by_wechat_unionid.return_value = sample_user
        resp, _ = make_use_case(mock_user_repo, mock_session_store).execute(WechatSyncRequest(openid="openid_123"))
        assert resp.access_token and resp.refresh_token and resp.expires_in > 0


class TestWechatSyncConflicts:
    def test_unionid_and_openid_bound_to_different_users(self, mock_user_repo, mock_session_store):
        """unionid 与 openid 分属两个账号 → 冲突报错，不写库"""
        ua = User(
            id="ua",
            email="a@wechat.local",
            username="wxa",
            display_name="A",
            password_hash="h",
            wechat_openid="o1",
            wechat_unionid=None,
        )
        ub = User(
            id="ub",
            email="b@wechat.local",
            username="wxb",
            display_name="B",
            password_hash="h",
            wechat_openid="oX",
            wechat_unionid="un1",
        )
        mock_user_repo.find_by_wechat_openid.return_value = ua
        mock_user_repo.find_by_wechat_unionid.return_value = ub

        resp, err = make_use_case(mock_user_repo, mock_session_store).execute(
            WechatSyncRequest(openid="o1", unionid="un1")
        )
        assert resp is None
        assert "conflict" in err
        # 补写不得发生
        assert ua.wechat_unionid is None

    def test_backfill_unionid_already_used_by_other(self, mock_user_repo, mock_session_store):
        """给 openid 老账号补 unionid 时发现 unionid 已被他人占用 → 冲突"""
        ua = User(
            id="ua",
            email="a@wechat.local",
            username="wxa",
            display_name="A",
            password_hash="h",
            wechat_openid="o1",
            wechat_unionid=None,
        )
        ub = User(
            id="ub",
            email="b@wechat.local",
            username="wxb",
            display_name="B",
            password_hash="h",
            wechat_openid="o2",
            wechat_unionid="un1",
        )
        # openid 命中 ua；unionid 首次查找（优先级查询）命中 ub
        mock_user_repo.find_by_wechat_openid.return_value = ua
        mock_user_repo.find_by_wechat_unionid.return_value = ub

        resp, err = make_use_case(mock_user_repo, mock_session_store).execute(
            WechatSyncRequest(openid="o1", unionid="un1")
        )
        assert resp is None
        assert "conflict" in err
        assert ua.wechat_unionid is None

    def test_unionid_user_openid_belongs_to_other(self, mock_user_repo, mock_session_store):
        """unionid 命中 ua，但请求的 openid 属于另一个账号 ub → 冲突，不抢占 openid"""
        ua = User(
            id="ua",
            email="a@wechat.local",
            username="wxa",
            display_name="A",
            password_hash="h",
            wechat_openid="oA",
            wechat_unionid="un1",
        )
        ub = User(
            id="ub",
            email="b@wechat.local",
            username="wxb",
            display_name="B",
            password_hash="h",
            wechat_openid="oB",
            wechat_unionid=None,
        )
        mock_user_repo.find_by_wechat_openid.return_value = ub
        mock_user_repo.find_by_wechat_unionid.return_value = ua

        resp, err = make_use_case(mock_user_repo, mock_session_store).execute(
            WechatSyncRequest(openid="oB", unionid="un1")
        )
        assert resp is None
        assert "conflict" in err
        assert ua.wechat_openid == "oA"  # 未被改写


class TestWechatSyncNewUser:
    def test_create_new_user(self, mock_user_repo, mock_session_store):
        saved = {}
        mock_user_repo.save.side_effect = lambda u: saved.update({u.id: u})
        resp, err = make_use_case(mock_user_repo, mock_session_store).execute(
            WechatSyncRequest(openid="new_openid_789", unionid="new_union_789", nickname="新用户")
        )
        assert err is None
        assert resp.is_new_user is True
        u = saved[resp.user_id]
        assert u.wechat_openid == "new_openid_789"
        assert u.wechat_unionid == "new_union_789"
        assert u.email.endswith("@wechat.local")
        assert u.username.startswith("wx_")
        assert u.email_verified is True
        assert u.password_hash

    def test_username_conflict_adds_suffix(self, mock_user_repo, mock_session_store):
        call_count = [0]

        def find_by_username(username):
            call_count[0] += 1
            return MagicMock() if call_count[0] <= 2 else None

        mock_user_repo.find_by_username.side_effect = find_by_username
        resp, err = make_use_case(mock_user_repo, mock_session_store).execute(WechatSyncRequest(openid="test_openid"))
        assert err is None
        assert resp.is_new_user is True
        assert call_count[0] >= 2


class TestWechatSyncErrors:
    def test_empty_openid(self, mock_user_repo, mock_session_store):
        resp, err = make_use_case(mock_user_repo, mock_session_store).execute(WechatSyncRequest(openid=""))
        assert resp is None
        assert "openid is required" in err

    def test_exception_returns_error(self, mock_user_repo, mock_session_store):
        mock_user_repo.find_by_wechat_openid.side_effect = Exception("DB error")
        mock_user_repo.find_by_wechat_unionid.side_effect = Exception("DB error")
        resp, err = make_use_case(mock_user_repo, mock_session_store).execute(WechatSyncRequest(openid="openid_123"))
        assert resp is None
        assert "Internal error" in err


class TestWechatSyncSession:
    def test_session_saved(self, mock_user_repo, mock_session_store, sample_user):
        mock_user_repo.find_by_wechat_openid.return_value = sample_user
        mock_user_repo.find_by_wechat_unionid.return_value = sample_user
        make_use_case(mock_user_repo, mock_session_store).execute(
            WechatSyncRequest(openid="openid_123", source="miniapp")
        )
        mock_session_store.save_session.assert_called_once()
        kw = mock_session_store.save_session.call_args[1]
        assert kw["user_id"] == "user_001"
        assert "wechat_miniapp" in kw["device_info"]
        assert kw["expires_in_seconds"] == 30 * 24 * 3600


class TestWechatSyncNicknameRules:
    """昵称规则：默认昵称、登录不覆盖存量昵称、中文昵称全链路无损。"""

    def test_existing_user_display_name_never_overwritten_on_login(self, mock_user_repo, mock_session_store):
        """守护规则：已存在用户登录时，即使请求带了别的 nickname，
        其 display_name 也绝不能被覆盖（只有建号时写入）。"""
        user = User(
            id="user_custom",
            email="c@wechat.local",
            username="wx_custom",
            display_name="我的专属昵称",
            password_hash="h",
            email_verified=True,
            wechat_openid="openid_123",
            wechat_unionid="unionid_456",
        )
        mock_user_repo.find_by_wechat_openid.return_value = user
        mock_user_repo.find_by_wechat_unionid.return_value = user

        resp, err = make_use_case(mock_user_repo, mock_session_store).execute(
            WechatSyncRequest(
                openid="openid_123",
                unionid="unionid_456",
                nickname="小虾同学",  # 请求里带的任何昵称都不准覆盖
            )
        )
        assert err is None
        assert resp.is_new_user is False
        # 数据层用户昵称不变
        assert user.display_name == "我的专属昵称"
        # 返回给调用方的昵称也是存量昵称
        assert resp.nickname == "我的专属昵称"
        # save 的都是同一个用户对象，且 display_name 未被改写
        for call in mock_user_repo.save.call_args_list:
            assert call.args[0].display_name == "我的专属昵称"

    def test_new_user_gets_default_nickname_when_nickname_empty(self, mock_user_repo, mock_session_store):
        """新用户且请求未带昵称（微信新规场景）→ 默认昵称小虾同学。"""
        saved = {}
        mock_user_repo.save.side_effect = lambda u: saved.update({u.id: u})
        resp, err = make_use_case(mock_user_repo, mock_session_store).execute(
            WechatSyncRequest(openid="new_oid_default", unionid="new_uid_default", nickname="")
        )
        assert err is None
        assert resp.is_new_user is True
        u = saved[resp.user_id]
        assert u.display_name == "小虾同学"
        assert resp.nickname == "小虾同学"

    def test_chinese_nickname_full_chain_no_mojibake(self, mock_user_repo, mock_session_store):
        """中文昵称全链路无损：建号写入 → 仓储对象 → 响应返回，
        不允许出现 latin-1/utf-8 双重编码乱码（如 \u00e8\u0080\u0081 一类）。"""
        chinese_name = "老字号🏮肩腰腿🏮御众堂永康店"
        saved = {}
        mock_user_repo.save.side_effect = lambda u: saved.update({u.id: u})

        resp, err = make_use_case(mock_user_repo, mock_session_store).execute(
            WechatSyncRequest(
                openid="new_oid_cn",
                unionid="new_uid_cn",
                nickname=chinese_name,
            )
        )
        assert err is None
        u = saved[resp.user_id]
        # 写入仓储对象的昵称必须与原文完全一致
        assert u.display_name == chinese_name
        assert resp.nickname == chinese_name
        assert resp.to_dict()["user"]["display_name"] == chinese_name
        assert resp.to_dict()["user_info"]["display_name"] == chinese_name
        # 乱码特征：双重编码后必然出现Ã/\u00c2 拼音字母或控制字符
        assert "\u00c3" not in u.display_name and "\u00c2" not in u.display_name
        assert "\u00e8" not in u.display_name
