"""视频分享领域模型单元测试 — wave215"""

from __future__ import annotations

import re
from datetime import datetime, timedelta, timezone

import pytest

from packages.domain.video_share import (
    VideoShare,
    _hash_password,
    generate_share_token,
)

# ── 密码哈希 ─────────────────────────────────────────────────────────────────


class TestHashPassword:
    def test_empty_password_returns_empty(self):
        assert _hash_password("") == ""

    def test_same_password_same_hash(self):
        h1 = _hash_password("secret123")
        h2 = _hash_password("secret123")
        assert h1 == h2
        assert h1 != ""

    def test_different_password_different_hash(self):
        h1 = _hash_password("pass1")
        h2 = _hash_password("pass2")
        assert h1 != h2

    def test_hash_is_sha256_hex(self):
        h = _hash_password("test")
        assert len(h) == 64
        assert re.match(r"^[0-9a-f]{64}$", h)

    def test_hash_contains_salt(self):
        # 直接SHA-256("test") vs 加盐后的结果应该不同
        import hashlib

        direct = hashlib.sha256(b"test").hexdigest()
        salted = _hash_password("test")
        assert direct != salted


# ── Token 生成 ──────────────────────────────────────────────────────────────


class TestGenerateShareToken:
    def test_default_length_12(self):
        token = generate_share_token()
        assert len(token) == 12

    def test_custom_length(self):
        token = generate_share_token(20)
        assert len(token) == 20

    def test_url_friendly_no_ambiguous_chars(self):
        # 不应包含容易混淆的字符：i, l, o, I, L, O, 0, 1
        token = generate_share_token(100)
        for ch in "ilO01":
            assert ch not in token

    def test_alphanumeric_only(self):
        token = generate_share_token(50)
        assert token.isalnum()

    def test_two_tokens_different(self):
        # 随机生成的两个token应该不同
        t1 = generate_share_token()
        t2 = generate_share_token()
        assert t1 != t2


# ── VideoShare.create ───────────────────────────────────────────────────────


class TestVideoShareCreate:
    def test_basic_create(self):
        share = VideoShare.create(video_id="v1", user_id="u1")
        assert share.id is not None
        assert share.video_id == "v1"
        assert share.user_id == "u1"
        assert share.share_token is not None
        assert len(share.share_token) == 12
        assert share.password_hash is None
        assert share.expires_at is None
        assert share.view_count == 0
        assert share.download_count == 0
        assert share.is_active is True
        assert share.created_at is not None
        assert share.updated_at is not None

    def test_create_with_password(self):
        share = VideoShare.create(video_id="v1", user_id="u1", password="secret")
        assert share.password_hash is not None
        assert share.password_hash != "secret"
        assert len(share.password_hash) == 64

    def test_create_with_empty_password_no_hash(self):
        share = VideoShare.create(video_id="v1", user_id="u1", password="")
        assert share.password_hash is None

    def test_create_with_expires_at(self):
        future = datetime.now(timezone.utc) + timedelta(days=7)
        share = VideoShare.create(video_id="v1", user_id="u1", expires_at=future)
        assert share.expires_at == future

    def test_create_past_expires_at_raises(self):
        past = datetime.now(timezone.utc) - timedelta(days=1)
        with pytest.raises(ValueError, match="expires_at cannot be in the past"):
            VideoShare.create(video_id="v1", user_id="u1", expires_at=past)

    def test_create_empty_video_id_raises(self):
        with pytest.raises(ValueError, match="video_id cannot be empty"):
            VideoShare.create(video_id="", user_id="u1")

    def test_create_whitespace_video_id_raises(self):
        with pytest.raises(ValueError, match="video_id cannot be empty"):
            VideoShare.create(video_id="   ", user_id="u1")

    def test_create_empty_user_id_raises(self):
        with pytest.raises(ValueError, match="user_id cannot be empty"):
            VideoShare.create(video_id="v1", user_id="")

    def test_create_strips_whitespace(self):
        share = VideoShare.create(video_id="  v1  ", user_id="  u1  ")
        assert share.video_id == "v1"
        assert share.user_id == "u1"

    def test_create_unique_id_each_time(self):
        s1 = VideoShare.create(video_id="v1", user_id="u1")
        s2 = VideoShare.create(video_id="v1", user_id="u1")
        assert s1.id != s2.id

    def test_create_unique_token_each_time(self):
        s1 = VideoShare.create(video_id="v1", user_id="u1")
        s2 = VideoShare.create(video_id="v1", user_id="u1")
        assert s1.share_token != s2.share_token


# ── has_password ────────────────────────────────────────────────────────────


class TestVideoShareHasPassword:
    def test_no_password(self):
        share = VideoShare.create(video_id="v1", user_id="u1")
        assert share.has_password is False

    def test_with_password(self):
        share = VideoShare.create(video_id="v1", user_id="u1", password="pass")
        assert share.has_password is True

    def test_empty_password_none(self):
        share = VideoShare.create(video_id="v1", user_id="u1", password="")
        assert share.has_password is False


# ── is_expired ──────────────────────────────────────────────────────────────


class TestVideoShareIsExpired:
    def test_no_expiry_never_expired(self):
        share = VideoShare.create(video_id="v1", user_id="u1")
        assert share.is_expired is False

    def test_future_expiry_not_expired(self):
        future = datetime.now(timezone.utc) + timedelta(hours=1)
        share = VideoShare.create(video_id="v1", user_id="u1", expires_at=future)
        assert share.is_expired is False

    def test_past_expiry_is_expired(self):
        share = VideoShare.create(video_id="v1", user_id="u1")
        share.expires_at = datetime.now(timezone.utc) - timedelta(seconds=1)
        assert share.is_expired is True


# ── is_accessible ───────────────────────────────────────────────────────────


class TestVideoShareIsAccessible:
    def test_active_no_expiry_accessible(self):
        share = VideoShare.create(video_id="v1", user_id="u1")
        assert share.is_accessible is True

    def test_revoked_not_accessible(self):
        share = VideoShare.create(video_id="v1", user_id="u1")
        share.is_active = False
        assert share.is_accessible is False

    def test_expired_not_accessible(self):
        share = VideoShare.create(video_id="v1", user_id="u1")
        share.expires_at = datetime.now(timezone.utc) - timedelta(days=1)
        assert share.is_accessible is False

    def test_revoked_and_expired_not_accessible(self):
        share = VideoShare.create(video_id="v1", user_id="u1")
        share.is_active = False
        share.expires_at = datetime.now(timezone.utc) - timedelta(days=1)
        assert share.is_accessible is False


# ── verify_password ─────────────────────────────────────────────────────────


class TestVideoShareVerifyPassword:
    def test_no_password_any_pass_ok(self):
        share = VideoShare.create(video_id="v1", user_id="u1")
        assert share.verify_password("anything") is True
        assert share.verify_password("") is True

    def test_no_password_none_ok(self):
        share = VideoShare.create(video_id="v1", user_id="u1")
        assert share.verify_password("") is True

    def test_correct_password(self):
        share = VideoShare.create(video_id="v1", user_id="u1", password="mysecret")
        assert share.verify_password("mysecret") is True

    def test_wrong_password(self):
        share = VideoShare.create(video_id="v1", user_id="u1", password="mysecret")
        assert share.verify_password("wrong") is False

    def test_empty_password_with_protection(self):
        share = VideoShare.create(video_id="v1", user_id="u1", password="mysecret")
        assert share.verify_password("") is False

    def test_password_case_sensitive(self):
        share = VideoShare.create(video_id="v1", user_id="u1", password="Secret")
        assert share.verify_password("secret") is False
        assert share.verify_password("Secret") is True


# ── 计数方法 ────────────────────────────────────────────────────────────────


class TestVideoShareCounters:
    def test_increment_view(self):
        share = VideoShare.create(video_id="v1", user_id="u1")
        assert share.view_count == 0
        share.increment_view_count()
        assert share.view_count == 1
        share.increment_view_count()
        assert share.view_count == 2

    def test_increment_download(self):
        share = VideoShare.create(video_id="v1", user_id="u1")
        assert share.download_count == 0
        share.increment_download_count()
        assert share.download_count == 1
        share.increment_download_count()
        assert share.download_count == 2

    def test_counters_independent(self):
        share = VideoShare.create(video_id="v1", user_id="u1")
        share.increment_view_count()
        share.increment_view_count()
        share.increment_download_count()
        assert share.view_count == 2
        assert share.download_count == 1


# ── revoke ──────────────────────────────────────────────────────────────────


class TestVideoShareRevoke:
    def test_revoke_sets_inactive(self):
        share = VideoShare.create(video_id="v1", user_id="u1")
        assert share.is_active is True
        share.revoke()
        assert share.is_active is False

    def test_revoke_makes_inaccessible(self):
        share = VideoShare.create(video_id="v1", user_id="u1")
        share.revoke()
        assert share.is_accessible is False

    def test_revoke_idempotent(self):
        share = VideoShare.create(video_id="v1", user_id="u1")
        share.revoke()
        share.revoke()  # 第二次也不报错
        assert share.is_active is False
