"""Tests for Telegram bot storage (user linking and verification codes)."""

import json
import os
import time
from pathlib import Path
from unittest.mock import patch

import pytest


@pytest.fixture()
def storage_paths(tmp_path, monkeypatch):
    """Redirect storage file paths to tmp_path."""
    users_file = str(tmp_path / "telegram_users.json")
    codes_file = str(tmp_path / "pending_codes.json")

    import services.telegram_bot.config as cfg
    monkeypatch.setattr(cfg, "TELEGRAM_USERS_FILE", users_file)
    monkeypatch.setattr(cfg, "PENDING_CODES_FILE", codes_file)
    # Also patch in the storage module namespace
    import services.telegram_bot.storage as storage_mod
    monkeypatch.setattr(storage_mod, "config", cfg)

    return {"users": users_file, "codes": codes_file}


class TestUserLinking:
    def test_link_user_and_get_chat_id(self, storage_paths):
        from services.telegram_bot.storage import get_chat_id, link_user
        link_user("alice", 100)
        assert get_chat_id("alice") == 100

    def test_get_chat_id_unknown_user_returns_none(self, storage_paths):
        from services.telegram_bot.storage import get_chat_id
        assert get_chat_id("nobody") is None

    def test_unlink_user_returns_true_when_linked(self, storage_paths):
        from services.telegram_bot.storage import link_user, unlink_user
        link_user("bob", 200)
        result = unlink_user("bob")
        assert result is True

    def test_unlink_user_removes_entry(self, storage_paths):
        from services.telegram_bot.storage import get_chat_id, link_user, unlink_user
        link_user("carol", 300)
        unlink_user("carol")
        assert get_chat_id("carol") is None

    def test_unlink_user_returns_false_when_not_linked(self, storage_paths):
        from services.telegram_bot.storage import unlink_user
        result = unlink_user("ghost")
        assert result is False

    def test_link_multiple_users(self, storage_paths):
        from services.telegram_bot.storage import get_chat_id, link_user
        link_user("user1", 111)
        link_user("user2", 222)
        assert get_chat_id("user1") == 111
        assert get_chat_id("user2") == 222


class TestVerificationCodes:
    def test_create_verification_code_returns_string(self, storage_paths):
        from services.telegram_bot.storage import create_verification_code
        code = create_verification_code(chat_id=42)
        assert isinstance(code, str)
        assert len(code) > 0

    def test_verify_code_returns_chat_id(self, storage_paths):
        from services.telegram_bot.storage import create_verification_code, verify_code
        code = create_verification_code(chat_id=55)
        result = verify_code(code)
        assert result == 55

    def test_code_consumed_after_first_verify(self, storage_paths):
        from services.telegram_bot.storage import create_verification_code, verify_code
        code = create_verification_code(chat_id=77)
        verify_code(code)
        # Second call must return None (code consumed)
        result = verify_code(code)
        assert result is None

    def test_verify_invalid_code_returns_none(self, storage_paths):
        from services.telegram_bot.storage import verify_code
        result = verify_code("000000")
        assert result is None

    def test_create_code_replaces_existing_for_same_chat_id(self, storage_paths):
        from services.telegram_bot.storage import create_verification_code, verify_code
        old_code = create_verification_code(chat_id=88)
        new_code = create_verification_code(chat_id=88)
        # Old code should be gone
        assert verify_code(old_code) is None
        # New code should work
        assert verify_code(new_code) == 88

    def test_expired_code_not_valid(self, storage_paths):
        """Manually write an expired code and verify it returns None."""
        import services.telegram_bot.config as cfg
        from services.telegram_bot.storage import verify_code

        # Write a code that expired long ago
        expired_data = {
            "123456": {
                "chat_id": 99,
                "created_at": time.time() - cfg.CODE_TTL_SECONDS - 1,
            }
        }
        Path(cfg.PENDING_CODES_FILE).parent.mkdir(parents=True, exist_ok=True)
        with open(cfg.PENDING_CODES_FILE, "w") as f:
            json.dump(expired_data, f)

        result = verify_code("123456")
        assert result is None

    def test_code_uses_csprng_not_random_module(self, storage_paths):
        """Issue #84: pairing-code RNG must not be derivable from
        `random.seed`. Pre-fix the generator used `random.choices`, which
        means an attacker who scrapes one code can recover the PRNG state
        and predict subsequent codes issued in the same process. The fix
        switched to `secrets.choice` (CSPRNG-backed); seeding the `random`
        module must therefore have no effect on the produced codes.
        """
        import random as _random
        from services.telegram_bot.storage import _generate_code

        _random.seed(42)
        first = _generate_code()
        _random.seed(42)
        second = _generate_code()
        # If the generator still used `random`, seed(42) would force two
        # identical sequences. With `secrets`, they're independent draws
        # from the OS CSPRNG and equal only by astronomical coincidence
        # — well below any reasonable test flake threshold for a
        # length-CODE_LENGTH digit string (1 in 10**CODE_LENGTH).
        assert first != second, (
            f"Generator appears to use seedable PRNG (got identical "
            f"codes {first!r} after re-seeding); fix #84 may have "
            f"regressed."
        )
