"""VerificationCode 单元测试."""

from __future__ import annotations

from datetime import datetime, timedelta, timezone
from unittest.mock import patch

import pytest
from domain.verification_code import VerificationCode


class TestVerificationCodeCreate:
    """create() 工厂方法测试."""

    def test_create_basic(self):
        vc = VerificationCode.create("test@example.com", "email_login")
        assert vc.id is not None
        assert len(vc.id) == 32
        assert vc.recipient == "test@example.com"
        assert vc.code_type == "email_login"
        assert len(vc.code) == 6
        assert vc.code.isdigit()
        assert vc.used_at is None
        assert vc.attempts == 0
        assert vc.created_at is not None
        assert vc.expires_at > vc.created_at

    def test_create_recipient_stripped(self):
        vc = VerificationCode.create("  test@example.com  ", "email_login")
        assert vc.recipient == "test@example.com"

    def test_create_custom_code(self):
        vc = VerificationCode.create("test@example.com", "email_login", custom_code="123456")
        assert vc.code == "123456"

    def test_create_custom_ttl(self):
        fixed_now = datetime(2026, 1, 1, 12, 0, 0, tzinfo=timezone.utc)
        with patch("domain.verification_code.datetime") as mock_dt:
            mock_dt.now.return_value = fixed_now
            mock_dt.side_effect = lambda *a, **kw: datetime(*a, **kw)
            vc = VerificationCode.create("test@example.com", "email_login", ttl_seconds=60)
        assert vc.expires_at == fixed_now + timedelta(seconds=60)

    def test_create_default_ttl_300(self):
        fixed_now = datetime(2026, 1, 1, 12, 0, 0, tzinfo=timezone.utc)
        with patch("domain.verification_code.datetime") as mock_dt:
            mock_dt.now.return_value = fixed_now
            mock_dt.side_effect = lambda *a, **kw: datetime(*a, **kw)
            vc = VerificationCode.create("test@example.com", "email_login")
        assert vc.expires_at == fixed_now + timedelta(seconds=300)

    def test_create_unique_ids(self):
        vc1 = VerificationCode.create("a@b.com", "email_login")
        vc2 = VerificationCode.create("a@b.com", "email_login")
        assert vc1.id != vc2.id

    def test_create_unique_codes(self):
        codes = set()
        for _ in range(20):
            vc = VerificationCode.create("a@b.com", "email_login")
            codes.add(vc.code)
        # 20个随机6位码几乎肯定不都一样
        assert len(codes) > 1

    def test_create_phone_recipient(self):
        vc = VerificationCode.create("13800138000", "phone_login")
        assert vc.recipient == "13800138000"
        assert vc.code_type == "phone_login"

    def test_create_all_code_types(self):
        for ct in ["email_bind", "phone_bind", "email_login", "phone_login", "reset_password"]:
            vc = VerificationCode.create("test@example.com", ct)
            assert vc.code_type == ct


class TestVerificationCodeIsExpired:
    """is_expired 属性测试."""

    def test_not_expired_future(self):
        future = datetime.now(timezone.utc) + timedelta(hours=1)
        vc = VerificationCode(
            id="1",
            recipient="a@b.com",
            code="123456",
            code_type="email_login",
            expires_at=future,
        )
        assert vc.is_expired is False

    def test_expired_past(self):
        past = datetime.now(timezone.utc) - timedelta(hours=1)
        vc = VerificationCode(
            id="1",
            recipient="a@b.com",
            code="123456",
            code_type="email_login",
            expires_at=past,
        )
        assert vc.is_expired is True

    def test_expired_boundary_exact(self):
        # 用mock固定时间，expires_at等于当前时间不算过期
        fixed_now = datetime(2026, 1, 1, 12, 0, 0, tzinfo=timezone.utc)
        with patch("domain.verification_code.datetime") as mock_dt:
            mock_dt.now.return_value = fixed_now
            mock_dt.side_effect = lambda *a, **kw: datetime(*a, **kw)
            vc = VerificationCode(
                id="1",
                recipient="a@b.com",
                code="123456",
                code_type="email_login",
                expires_at=fixed_now,
            )
            assert vc.is_expired is False


class TestVerificationCodeIsUsed:
    """is_used 属性测试."""

    def test_not_used_default(self):
        vc = VerificationCode.create("a@b.com", "email_login")
        assert vc.is_used is False

    def test_is_used_after_mark(self):
        vc = VerificationCode.create("a@b.com", "email_login")
        vc.mark_used()
        assert vc.is_used is True


class TestVerificationCodeIsValid:
    """is_valid 属性测试."""

    def test_valid_fresh(self):
        future = datetime.now(timezone.utc) + timedelta(hours=1)
        vc = VerificationCode(
            id="1",
            recipient="a@b.com",
            code="123456",
            code_type="email_login",
            expires_at=future,
        )
        assert vc.is_valid is True

    def test_invalid_expired(self):
        past = datetime.now(timezone.utc) - timedelta(hours=1)
        vc = VerificationCode(
            id="1",
            recipient="a@b.com",
            code="123456",
            code_type="email_login",
            expires_at=past,
        )
        assert vc.is_valid is False

    def test_invalid_used(self):
        future = datetime.now(timezone.utc) + timedelta(hours=1)
        vc = VerificationCode(
            id="1",
            recipient="a@b.com",
            code="123456",
            code_type="email_login",
            expires_at=future,
        )
        vc.mark_used()
        assert vc.is_valid is False

    def test_invalid_expired_and_used(self):
        past = datetime.now(timezone.utc) - timedelta(hours=1)
        vc = VerificationCode(
            id="1",
            recipient="a@b.com",
            code="123456",
            code_type="email_login",
            expires_at=past,
        )
        vc.mark_used()
        assert vc.is_valid is False


class TestVerificationCodeMarkUsed:
    """mark_used 方法测试."""

    def test_mark_used_sets_timestamp(self):
        vc = VerificationCode.create("a@b.com", "email_login")
        assert vc.used_at is None
        before = datetime.now(timezone.utc)
        vc.mark_used()
        after = datetime.now(timezone.utc)
        assert vc.used_at is not None
        assert before <= vc.used_at <= after

    def test_mark_used_twice_overwrites(self):
        vc = VerificationCode.create("a@b.com", "email_login")
        vc.mark_used()
        first = vc.used_at
        # 时间足够短，一般不会不同，但确保可以重复调用
        vc.mark_used()
        assert vc.used_at is not None


class TestVerificationCodeIncrementAttempts:
    """increment_attempts 方法测试."""

    def test_default_zero(self):
        vc = VerificationCode.create("a@b.com", "email_login")
        assert vc.attempts == 0

    def test_increment_once(self):
        vc = VerificationCode.create("a@b.com", "email_login")
        vc.increment_attempts()
        assert vc.attempts == 1

    def test_increment_multiple(self):
        vc = VerificationCode.create("a@b.com", "email_login")
        for _i in range(5):
            vc.increment_attempts()
        assert vc.attempts == 5


class TestVerificationCodeBasics:
    """基础构造和 slots 测试."""

    def test_direct_construction(self):
        now = datetime.now(timezone.utc)
        vc = VerificationCode(
            id="abc123",
            recipient="test@test.com",
            code="000000",
            code_type="email_bind",
            expires_at=now + timedelta(minutes=5),
            used_at=None,
            attempts=0,
            created_at=now,
        )
        assert vc.id == "abc123"
        assert vc.recipient == "test@test.com"
        assert vc.code == "000000"

    def test_slots_no_extra_attrs(self):
        vc = VerificationCode.create("a@b.com", "email_login")
        with pytest.raises((AttributeError, TypeError)):
            vc.new_field = "value"  # type: ignore[attr-defined]

    def test_equality_same_id(self):
        now = datetime(2026, 1, 1, 12, 0, 0, tzinfo=timezone.utc)
        vc1 = VerificationCode(
            id="same",
            recipient="a@b.com",
            code="111",
            code_type="email_login",
            expires_at=now,
            created_at=now,
        )
        vc2 = VerificationCode(
            id="same",
            recipient="a@b.com",
            code="111",
            code_type="email_login",
            expires_at=now,
            created_at=now,
        )
        assert vc1 == vc2

    def test_equality_different_id(self):
        now = datetime.now(timezone.utc)
        vc1 = VerificationCode(
            id="id1",
            recipient="a@b.com",
            code="111",
            code_type="email_login",
            expires_at=now,
        )
        vc2 = VerificationCode(
            id="id2",
            recipient="a@b.com",
            code="111",
            code_type="email_login",
            expires_at=now,
        )
        assert vc1 != vc2
