import os
import time
import pytest
from tinydb import Query

from db.db import digest_action_tokens_db
from utils.digest_token import (
    create_action_token,
    validate_and_consume_token,
    create_unsubscribe_token,
    validate_unsubscribe_token,
)


def cleanup():
    digest_action_tokens_db.truncate()


@pytest.fixture(autouse=True)
def clear_tokens():
    cleanup()
    yield
    cleanup()


class TestCreateActionToken:
    def test_returns_token_with_correct_fields(self):
        token = create_action_token('u1', 'c1', 'e1', 'chore', 'approve')
        assert token.user_id == 'u1'
        assert token.child_id == 'c1'
        assert token.entity_id == 'e1'
        assert token.entity_type == 'chore'
        assert token.action == 'approve'
        assert token.used is False
        assert token.signature

    def test_persists_to_db(self):
        token = create_action_token('u1', 'c1', 'e1', 'chore', 'approve')
        stored = digest_action_tokens_db.get(Query().id == token.id)
        assert stored is not None
        assert stored['entity_type'] == 'chore'

    def test_different_tokens_have_unique_ids(self):
        t1 = create_action_token('u1', 'c1', 'e1', 'chore', 'approve')
        t2 = create_action_token('u1', 'c1', 'e1', 'reward', 'deny')
        assert t1.id != t2.id


class TestValidateAndConsumeToken:
    def test_valid_token_is_returned_and_marked_used(self):
        token = create_action_token('u1', 'c1', 'e1', 'chore', 'approve')
        result = validate_and_consume_token(token.id)
        assert result is not None
        assert result.id == token.id

        stored = digest_action_tokens_db.get(Query().id == token.id)
        assert stored['used'] is True

    def test_nonexistent_token_returns_none(self):
        assert validate_and_consume_token('nonexistent-id') is None

    def test_already_used_token_returns_none(self):
        token = create_action_token('u1', 'c1', 'e1', 'chore', 'approve')
        validate_and_consume_token(token.id)  # first use
        result = validate_and_consume_token(token.id)  # second use
        assert result is None

    def test_expired_token_returns_none(self):
        # Create a token that is already expired
        from datetime import datetime, timedelta, timezone
        from db.digest_action_tokens import insert_token
        from models.digest_action_token import DigestActionToken
        import uuid, json, hmac, hashlib

        token_id = str(uuid.uuid4())
        expires_at = (datetime.now(timezone.utc) - timedelta(hours=1)).isoformat()
        payload = {
            'id': token_id,
            'user_id': 'u1',
            'child_id': 'c1',
            'entity_id': 'e1',
            'entity_type': 'chore',
            'action': 'approve',
            'expires_at': expires_at,
        }
        secret = os.environ.get('DIGEST_TOKEN_SECRET')
        canonical = json.dumps(payload, sort_keys=True, separators=(',', ':'))
        sig = hmac.new(secret.encode(), canonical.encode(), hashlib.sha256).hexdigest()
        token = DigestActionToken(
            id=token_id, user_id='u1', child_id='c1', entity_id='e1',
            entity_type='chore', action='approve', expires_at=expires_at,
            used=False, signature=sig,
        )
        insert_token(token)
        assert validate_and_consume_token(token_id) is None

    def test_tampered_signature_returns_none(self):
        token = create_action_token('u1', 'c1', 'e1', 'chore', 'approve')
        # Tamper the signature in DB
        digest_action_tokens_db.update(
            {'signature': 'deadbeef' * 8},
            Query().id == token.id
        )
        assert validate_and_consume_token(token.id) is None


class TestUnsubscribeToken:
    def test_create_and_validate(self):
        token = create_unsubscribe_token('user123')
        assert token
        result = validate_unsubscribe_token(token)
        assert result == 'user123'

    def test_invalid_token_returns_none(self):
        assert validate_unsubscribe_token('garbage-token') is None

    def test_tampered_token_returns_none(self):
        token = create_unsubscribe_token('user123')
        # Change one character
        tampered = token[:-1] + ('A' if token[-1] != 'A' else 'B')
        assert validate_unsubscribe_token(tampered) is None

    def test_expired_token_returns_none(self):
        """Build a token with a past expiry timestamp by mocking time."""
        import base64, hmac, hashlib
        user_id = 'user_expired'
        expiry_ts = int(time.time()) - 1  # already expired
        payload_str = f"{user_id}:{expiry_ts}"
        secret = os.environ.get('DIGEST_TOKEN_SECRET')
        sig = hmac.new(secret.encode(), payload_str.encode(), hashlib.sha256).hexdigest()
        raw = f"{payload_str}:{sig}"
        token = base64.urlsafe_b64encode(raw.encode()).decode()
        assert validate_unsubscribe_token(token) is None
