import pytest
from werkzeug.security import generate_password_hash, check_password_hash
from flask import Flask
from api.auth_api import auth_api, _hash_token
from db.db import users_db, refresh_tokens_db
from tinydb import Query
from models.user import User
from datetime import datetime, timedelta, timezone
from tests.conftest import TEST_SECRET_KEY, TEST_REFRESH_TOKEN_EXPIRY_DAYS

@pytest.fixture
def client():
    """Setup Flask test client with auth blueprint."""
    app = Flask(__name__)
    app.register_blueprint(auth_api, url_prefix='/auth')
    app.config['TESTING'] = True
    app.config['SECRET_KEY'] = TEST_SECRET_KEY
    app.config['REFRESH_TOKEN_EXPIRY_DAYS'] = TEST_REFRESH_TOKEN_EXPIRY_DAYS
    app.config['FRONTEND_URL'] = 'http://localhost:5173'
    with app.test_client() as client:
        yield client

def test_signup_hashes_password(client):
    """Test that signup hashes the password."""
    # Clean up any existing user
    users_db.remove(Query().email == 'test@example.com')
    
    data = {
        'first_name': 'Test',
        'last_name': 'User',
        'email': 'test@example.com',
        'password': 'password123'
    }
    response = client.post('/auth/signup', json=data)
    assert response.status_code == 201

    # Check that password is hashed in DB
    user_dict = users_db.get(Query().email == 'test@example.com')
    assert user_dict is not None
    assert user_dict['password'].startswith('scrypt:')

def test_login_with_correct_password(client):
    """Test login succeeds with correct password."""
    # Clean up and create a user with hashed password
    users_db.remove(Query().email == 'test@example.com')
    hashed_pw = generate_password_hash('password123')
    user = User(
        first_name='Test',
        last_name='User',
        email='test@example.com',
        password=hashed_pw,
        verified=True
    )
    users_db.insert(user.to_dict())

    data = {'email': 'test@example.com', 'password': 'password123'}
    response = client.post('/auth/login', json=data)
    assert response.status_code == 200
    cookies = response.headers.getlist('Set-Cookie')
    cookie_str = ' '.join(cookies)
    assert 'access_token=' in cookie_str
    assert 'refresh_token=' in cookie_str

def test_login_with_incorrect_password(client):
    """Test login fails with incorrect password."""
    # Clean up and create a user with hashed password
    users_db.remove(Query().email == 'test@example.com')
    hashed_pw = generate_password_hash('password123')
    user = User(
        first_name='Test',
        last_name='User',
        email='test@example.com',
        password=hashed_pw,
        verified=True
    )
    users_db.insert(user.to_dict())

    data = {'email': 'test@example.com', 'password': 'wrongpassword'}
    response = client.post('/auth/login', json=data)
    assert response.status_code == 401
    assert response.json['code'] == 'INVALID_CREDENTIALS'

def test_reset_password_hashes_new_password(client):
    """Test that reset-password hashes the new password."""
    # Clean up and create a user with reset token
    users_db.remove(Query().email == 'test@example.com')
    user = User(
        first_name='Test',
        last_name='User',
        email='test@example.com',
        password=generate_password_hash('oldpassword'),
        verified=True,
        reset_token='validtoken',
        reset_token_created=datetime.utcnow().isoformat()
    )
    users_db.insert(user.to_dict())

    data = {'token': 'validtoken', 'password': 'newpassword123'}
    response = client.post('/auth/reset-password', json=data)
    assert response.status_code == 200

    # Check that password is hashed in DB
    user_dict = users_db.get(Query().email == 'test@example.com')
    assert user_dict is not None
    assert user_dict['password'].startswith('scrypt:')
    assert check_password_hash(user_dict['password'], 'newpassword123')


def test_reset_password_invalidates_existing_jwt(client):
    users_db.remove(Query().email == 'test@example.com')
    user = User(
        first_name='Test',
        last_name='User',
        email='test@example.com',
        password=generate_password_hash('oldpassword123'),
        verified=True,
        reset_token='validtoken2',
        reset_token_created=datetime.utcnow().isoformat(),
    )
    users_db.insert(user.to_dict())

    login_response = client.post('/auth/login', json={'email': 'test@example.com', 'password': 'oldpassword123'})
    assert login_response.status_code == 200
    login_cookies = login_response.headers.getlist('Set-Cookie')
    login_cookie_str = ' '.join(login_cookies)
    assert 'access_token=' in login_cookie_str
    # Extract the old access token
    old_token = None
    for c in login_cookies:
        if c.startswith('access_token='):
            old_token = c.split('access_token=', 1)[1].split(';', 1)[0]
            break
    assert old_token

    reset_response = client.post('/auth/reset-password', json={'token': 'validtoken2', 'password': 'newpassword123'})
    assert reset_response.status_code == 200
    reset_cookies = reset_response.headers.getlist('Set-Cookie')
    reset_cookie_str = ' '.join(reset_cookies)
    assert 'access_token=' in reset_cookie_str

    # Verify all refresh tokens for this user are deleted
    user_dict = users_db.get(Query().email == 'test@example.com')
    user_tokens = refresh_tokens_db.search(Query().user_id == user_dict['id'])
    assert len(user_tokens) == 0

    # Set the old token as a cookie and test that it's now invalid
    client.set_cookie('access_token', old_token)
    me_response = client.get('/auth/me')
    assert me_response.status_code == 401
    assert me_response.json['code'] == 'INVALID_TOKEN'

def test_migration_script_hashes_plain_text_passwords():
    """Test the migration script hashes plain text passwords."""
    # Clean up
    users_db.remove(Query().email == 'test1@example.com')
    users_db.remove(Query().email == 'test2@example.com')
    
    # Create users with plain text passwords
    user1 = User(
        first_name='Test1',
        last_name='User',
        email='test1@example.com',
        password='plaintext1',
        verified=True
    )
    already_hashed = generate_password_hash('alreadyhashed')
    user2 = User(
        first_name='Test2',
        last_name='User',
        email='test2@example.com',
        password=already_hashed,  # Already hashed
        verified=True
    )
    users_db.insert(user1.to_dict())
    users_db.insert(user2.to_dict())

    # Run migration script
    import sys
    import os
    sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..'))
    from scripts.hash_passwords import main
    main()

    # Check user1 password is now hashed
    user1_dict = users_db.get(Query().email == 'test1@example.com')
    assert user1_dict['password'].startswith('scrypt:')
    assert check_password_hash(user1_dict['password'], 'plaintext1')

    # Check user2 password unchanged
    user2_dict = users_db.get(Query().email == 'test2@example.com')
    assert user2_dict['password'] == already_hashed


def _extract_cookie_value(response, cookie_name):
    """Extract a raw cookie value from a response's Set-Cookie headers."""
    for cookie in response.headers.getlist('Set-Cookie'):
        if cookie.startswith(f'{cookie_name}='):
            return cookie.split(f'{cookie_name}=', 1)[1].split(';', 1)[0]
    return None


def _create_verified_user(email, password):
    """Insert a verified user with the given credentials."""
    users_db.remove(Query().email == email)
    user = User(
        first_name='Test',
        last_name='User',
        email=email,
        password=generate_password_hash(password),
        verified=True,
    )
    users_db.insert(user.to_dict())
    return user


def _set_refresh_cookie(client, raw_token):
    """
    Set the refresh token cookie on a test client so it is sent to /auth/refresh.
    Production uses path='/api/auth' because the frontend calls /api/auth/refresh,
    but the test fixture exposes the blueprint at /auth/refresh directly.
    """
    client.set_cookie('refresh_token', raw_token, path='/')


def test_refresh_rotates_token(client):
    """A successful refresh marks the old token used and issues a new one in the same family."""
    email = 'refresh-rotate@test.com'
    password = 'password123'
    _create_verified_user(email, password)

    login_response = client.post('/auth/login', json={'email': email, 'password': password})
    assert login_response.status_code == 200
    old_refresh = _extract_cookie_value(login_response, 'refresh_token')
    assert old_refresh

    _set_refresh_cookie(client, old_refresh)
    refresh_response = client.post('/auth/refresh')
    assert refresh_response.status_code == 200
    new_refresh = _extract_cookie_value(refresh_response, 'refresh_token')
    assert new_refresh
    assert new_refresh != old_refresh

    user_dict = users_db.get(Query().email == email)
    old_hash = _hash_token(old_refresh)
    new_hash = _hash_token(new_refresh)

    old_record = refresh_tokens_db.get(Query().token_hash == old_hash)
    assert old_record is not None
    assert old_record['is_used'] is True
    assert old_record['rotated_at'] is not None

    new_record = refresh_tokens_db.get(Query().token_hash == new_hash)
    assert new_record is not None
    assert new_record['is_used'] is False
    assert new_record['token_family'] == old_record['token_family']


def test_refresh_reuse_only_invalidates_family(client):
    """Replay of a used refresh token only kills its own family, not other devices."""
    email = 'refresh-family@test.com'
    password = 'password123'
    user = _create_verified_user(email, password)

    # Device A logs in
    client_a = client
    login_a = client_a.post('/auth/login', json={'email': email, 'password': password})
    assert login_a.status_code == 200
    refresh_a = _extract_cookie_value(login_a, 'refresh_token')

    # Device B logs in (separate client = separate cookie jar)
    app = client_a.application
    client_b = app.test_client()
    login_b = client_b.post('/auth/login', json={'email': email, 'password': password})
    assert login_b.status_code == 200
    refresh_b = _extract_cookie_value(login_b, 'refresh_token')

    assert refresh_a != refresh_b

    # Device A refreshes normally
    _set_refresh_cookie(client_a, refresh_a)
    refresh_a_response = client_a.post('/auth/refresh')
    assert refresh_a_response.status_code == 200

    # Capture families before any purge so we can assert afterwards.
    family_a = refresh_tokens_db.get(Query().token_hash == _hash_token(refresh_a))['token_family']
    family_b = refresh_tokens_db.get(Query().token_hash == _hash_token(refresh_b))['token_family']
    assert family_a != family_b

    # Backdate rotation so the replay is past the grace period and treated as theft.
    old_hash_a = _hash_token(refresh_a)
    backdated = (datetime.now(timezone.utc) - timedelta(seconds=60)).isoformat()
    refresh_tokens_db.update({'rotated_at': backdated}, Query().token_hash == old_hash_a)

    # Attacker replays device A's old token
    _set_refresh_cookie(client_a, refresh_a)
    reuse_response = client_a.post('/auth/refresh')
    assert reuse_response.status_code == 401
    assert reuse_response.json['code'] == 'REFRESH_TOKEN_REUSE'

    # Device B's refresh token should still be valid
    _set_refresh_cookie(client_b, refresh_b)
    refresh_b_response = client_b.post('/auth/refresh')
    assert refresh_b_response.status_code == 200

    # Only family A should be purged; family B should remain
    remaining = refresh_tokens_db.search(Query().user_id == user.id)
    remaining_families = {t['token_family'] for t in remaining}
    assert family_a not in remaining_families
    assert family_b in remaining_families


def test_refresh_reuse_within_grace_period_is_tolerated(client):
    """A replay within the grace period is treated as a race condition, not theft."""
    email = 'refresh-race@test.com'
    password = 'password123'
    _create_verified_user(email, password)

    login_response = client.post('/auth/login', json={'email': email, 'password': password})
    assert login_response.status_code == 200
    refresh_token = _extract_cookie_value(login_response, 'refresh_token')

    # First refresh marks the token as used
    _set_refresh_cookie(client, refresh_token)
    first_refresh = client.post('/auth/refresh')
    assert first_refresh.status_code == 200

    # Immediate replay (same legitimate client racing) should succeed
    _set_refresh_cookie(client, refresh_token)
    race_response = client.post('/auth/refresh')
    assert race_response.status_code == 200

    # The family should still be valid
    user_dict = users_db.get(Query().email == email)
    family = refresh_tokens_db.get(Query().token_hash == _hash_token(refresh_token))['token_family']
    family_tokens = refresh_tokens_db.search(
        (Query().user_id == user_dict['id']) & (Query().token_family == family)
    )
    assert len(family_tokens) >= 1
    assert any(t['is_used'] is False for t in family_tokens)


def test_refresh_reuse_after_grace_period_invalidates_family(client):
    """A replay after the grace period is treated as theft and kills only that family."""
    email = 'refresh-theft@test.com'
    password = 'password123'
    user = _create_verified_user(email, password)

    login_response = client.post('/auth/login', json={'email': email, 'password': password})
    assert login_response.status_code == 200
    refresh_token = _extract_cookie_value(login_response, 'refresh_token')

    # Refresh once, then backdate the rotation timestamp past the grace period
    _set_refresh_cookie(client, refresh_token)
    client.post('/auth/refresh')

    old_hash = _hash_token(refresh_token)
    old_record = refresh_tokens_db.get(Query().token_hash == old_hash)
    old_family = old_record['token_family']
    backdated = (datetime.now(timezone.utc) - timedelta(seconds=60)).isoformat()
    refresh_tokens_db.update({'rotated_at': backdated}, Query().token_hash == old_hash)

    # Replay now should be detected as theft
    _set_refresh_cookie(client, refresh_token)
    reuse_response = client.post('/auth/refresh')
    assert reuse_response.status_code == 401
    assert reuse_response.json['code'] == 'REFRESH_TOKEN_REUSE'

    remaining = refresh_tokens_db.search(Query().user_id == user.id)
    remaining_families = {t['token_family'] for t in remaining}
    assert old_family not in remaining_families


def test_refresh_reuse_without_rotated_at_invalidates_family(client):
    """Legacy used tokens without rotated_at are treated as theft, not race conditions."""
    email = 'refresh-legacy@test.com'
    password = 'password123'
    user = _create_verified_user(email, password)

    login_response = client.post('/auth/login', json={'email': email, 'password': password})
    assert login_response.status_code == 200
    refresh_token = _extract_cookie_value(login_response, 'refresh_token')

    # Rotate the token, then strip rotated_at to simulate pre-migration data
    _set_refresh_cookie(client, refresh_token)
    client.post('/auth/refresh')
    old_hash = _hash_token(refresh_token)
    refresh_tokens_db.update({'rotated_at': None}, Query().token_hash == old_hash)

    old_record = refresh_tokens_db.get(Query().token_hash == old_hash)
    old_family = old_record['token_family']

    # Replay should be treated as theft because rotated_at is missing
    _set_refresh_cookie(client, refresh_token)
    reuse_response = client.post('/auth/refresh')
    assert reuse_response.status_code == 401
    assert reuse_response.json['code'] == 'REFRESH_TOKEN_REUSE'

    remaining = refresh_tokens_db.search(Query().user_id == user.id)
    remaining_families = {t['token_family'] for t in remaining}
    assert old_family not in remaining_families


def test_refresh_reuse_with_zero_grace_period(client):
    """A grace period of 0 means any replay of a used token is treated as theft."""
    email = 'refresh-zero-grace@test.com'
    password = 'password123'
    user = _create_verified_user(email, password)

    # Configure the app with a 0-second grace period
    client.application.config['REFRESH_TOKEN_REUSE_GRACE_PERIOD_SECONDS'] = 0

    login_response = client.post('/auth/login', json={'email': email, 'password': password})
    assert login_response.status_code == 200
    refresh_token = _extract_cookie_value(login_response, 'refresh_token')

    # Rotate the token; rotated_at is within the normal default grace period
    _set_refresh_cookie(client, refresh_token)
    client.post('/auth/refresh')

    old_hash = _hash_token(refresh_token)
    old_record = refresh_tokens_db.get(Query().token_hash == old_hash)
    old_family = old_record['token_family']

    # Immediate replay should still be theft with a 0-second grace period
    _set_refresh_cookie(client, refresh_token)
    reuse_response = client.post('/auth/refresh')
    assert reuse_response.status_code == 401
    assert reuse_response.json['code'] == 'REFRESH_TOKEN_REUSE'

    remaining = refresh_tokens_db.search(Query().user_id == user.id)
    remaining_families = {t['token_family'] for t in remaining}
    assert old_family not in remaining_families
