"""Tests for the email service, forced password change, welcome email, and
on-demand report/alert delivery.

smtplib is always patched so tests never touch the network.
"""

import os
from unittest.mock import patch, MagicMock

import pytest
from werkzeug.security import generate_password_hash


@pytest.fixture(autouse=True)
def _strip_smtp_env(monkeypatch):
    """Remove any SMTP_* env so env-fallback cannot enable email unexpectedly."""
    for key in list(os.environ):
        if key.startswith('SMTP_'):
            monkeypatch.delenv(key, raising=False)


def _enable_smtp(db):
    """Seed the minimum settings to make SMTP 'configured'."""
    from shopdb.core.models import Setting
    from shopdb.core.api.settings import invalidate_settings_cache

    rows = {
        'smtp_enabled': ('true', 'boolean'),
        'smtp_host': ('smtp.test.local', 'string'),
        'smtp_port': ('587', 'integer'),
        'smtp_use_tls': ('true', 'boolean'),
        'smtp_from_address': ('noreply@test.local', 'string'),
        'smtp_from_name': ('ShopDB', 'string'),
        'alert_recipients': ('ops@test.local', 'string'),
    }
    for key, (value, valuetype) in rows.items():
        db.session.add(Setting(key=key, value=value, valuetype=valuetype,
                               category='email'))
    db.session.commit()
    invalidate_settings_cache()


def _make_user(db, username, password='temppass1', mustchange=False,
               failedlogins=0):
    from shopdb.core.models import User
    user = User(
        username=username,
        email=f'{username}@test.local',
        passwordhash=generate_password_hash(password),
        mustchangepassword=mustchange,
        failedlogins=failedlogins,
    )
    db.session.add(user)
    db.session.commit()
    return user


def _login(client, username, password):
    response = client.post('/api/auth/login',
                           json={'username': username, 'password': password})
    return response


# ---------------------------------------------------------------------------
# Mailer no-op behavior
# ---------------------------------------------------------------------------

def test_mailer_noop_when_disabled(app, db):
    """send_email is a no-op returning False when SMTP is disabled; no connect."""
    from shopdb.utils import mailer
    with app.app_context():
        with patch('shopdb.utils.mailer.smtplib.SMTP') as smtp:
            result = mailer.send_email('a@test.local', 'Hi', '<p>Hi</p>')
    assert result is False
    smtp.assert_not_called()


def test_mailer_sends_when_configured(app, db):
    """send_email connects and sends when SMTP is configured (patched)."""
    from shopdb.utils import mailer
    with app.app_context():
        _enable_smtp(db)
        with patch('shopdb.utils.mailer.smtplib.SMTP') as smtp:
            instance = MagicMock()
            smtp.return_value = instance
            result = mailer.send_email('a@test.local', 'Hi', '<p>Hi</p>')
    assert result is True
    instance.sendmail.assert_called_once()


def test_mailer_never_logs_password(app, db):
    """A send failure carrying the password is scrubbed in the returned error."""
    from shopdb.utils import mailer
    with app.app_context():
        from shopdb.core.models import Setting
        from shopdb.core.api.settings import invalidate_settings_cache
        _enable_smtp(db)
        db.session.add(Setting(key='smtp_password', value='s3cret',
                               valuetype='string', category='email'))
        db.session.commit()
        invalidate_settings_cache()
        with patch('shopdb.utils.mailer.smtplib.SMTP') as smtp:
            instance = MagicMock()
            instance.sendmail.side_effect = RuntimeError('auth failed s3cret')
            smtp.return_value = instance
            ok, error = mailer.try_send('a@test.local', 'Hi', '<p>Hi</p>')
    assert ok is False
    assert 's3cret' not in error


# ---------------------------------------------------------------------------
# Test-email endpoint
# ---------------------------------------------------------------------------

def test_test_email_requires_admin(client, db, member_headers):
    response = client.post('/api/settings/test-email',
                           json={'to': 'x@test.local'}, headers=member_headers)
    assert response.status_code == 403


def test_test_email_graceful_when_unconfigured(client, db, auth_headers):
    response = client.post('/api/settings/test-email',
                           json={'to': 'x@test.local'}, headers=auth_headers)
    assert response.status_code == 200
    assert response.get_json()['data']['sent'] is False


def test_test_email_sends_when_configured(client, db, auth_headers):
    _enable_smtp(db)
    with patch('shopdb.utils.mailer.smtplib.SMTP') as smtp:
        smtp.return_value = MagicMock()
        response = client.post('/api/settings/test-email',
                               json={'to': 'x@test.local'}, headers=auth_headers)
    assert response.status_code == 200
    assert response.get_json()['data']['sent'] is True


# ---------------------------------------------------------------------------
# Forced password change
# ---------------------------------------------------------------------------

def test_login_surfaces_mustchangepassword(client, db):
    _make_user(db, 'forceduser', password='temppass1', mustchange=True)
    response = _login(client, 'forceduser', 'temppass1')
    assert response.status_code == 200
    assert response.get_json()['data']['user']['mustchangepassword'] is True


def test_forced_change_clears_flag_and_lockout(client, db):
    user = _make_user(db, 'forceduser', password='temppass1', mustchange=True,
                      failedlogins=3)
    token = _login(client, 'forceduser', 'temppass1').get_json()['data']['access_token']
    headers = {'Authorization': f'Bearer {token}'}
    # Forced case: new_password only, no current required.
    response = client.post('/api/auth/change-password',
                           json={'new_password': 'brandnew123'}, headers=headers)
    assert response.status_code == 200

    from shopdb.core.models import User
    refreshed = db.session.get(User, user.userid)
    assert refreshed.mustchangepassword is False
    assert refreshed.failedlogins == 0
    assert refreshed.lockeduntil is None
    # New password works.
    assert _login(client, 'forceduser', 'brandnew123').status_code == 200


def test_selfservice_change_rejects_wrong_current(client, db):
    _make_user(db, 'normaluser', password='rightpass1', mustchange=False)
    token = _login(client, 'normaluser', 'rightpass1').get_json()['data']['access_token']
    headers = {'Authorization': f'Bearer {token}'}
    response = client.post('/api/auth/change-password',
                           json={'current_password': 'wrongpass',
                                 'new_password': 'brandnew123'}, headers=headers)
    assert response.status_code == 401


def test_selfservice_change_succeeds_with_correct_current(client, db):
    _make_user(db, 'normaluser', password='rightpass1', mustchange=False)
    token = _login(client, 'normaluser', 'rightpass1').get_json()['data']['access_token']
    headers = {'Authorization': f'Bearer {token}'}
    response = client.post('/api/auth/change-password',
                           json={'current_password': 'rightpass1',
                                 'new_password': 'brandnew123'}, headers=headers)
    assert response.status_code == 200
    assert _login(client, 'normaluser', 'brandnew123').status_code == 200


def test_change_password_rejects_short_password(client, db):
    _make_user(db, 'normaluser', password='rightpass1', mustchange=False)
    token = _login(client, 'normaluser', 'rightpass1').get_json()['data']['access_token']
    headers = {'Authorization': f'Bearer {token}'}
    response = client.post('/api/auth/change-password',
                           json={'current_password': 'rightpass1',
                                 'new_password': 'short'}, headers=headers)
    assert response.status_code == 400


# ---------------------------------------------------------------------------
# New-user welcome
# ---------------------------------------------------------------------------

def test_create_user_sets_mustchangepassword_and_sends(client, db, auth_headers):
    _enable_smtp(db)
    with patch('shopdb.utils.mailer.smtplib.SMTP') as smtp:
        instance = MagicMock()
        smtp.return_value = instance
        response = client.post('/api/users',
                               json={'username': 'newhire',
                                     'email': 'newhire@test.local',
                                     'password': 'temppass1'},
                               headers=auth_headers)
    assert response.status_code == 201
    assert response.get_json()['data']['mustchangepassword'] is True
    instance.sendmail.assert_called_once()


def test_create_user_survives_mail_failure(client, db, auth_headers):
    """User is still created (with a warning) when the welcome email fails."""
    _enable_smtp(db)
    with patch('shopdb.utils.mailer.smtplib.SMTP') as smtp:
        instance = MagicMock()
        instance.sendmail.side_effect = RuntimeError('relay down')
        smtp.return_value = instance
        response = client.post('/api/users',
                               json={'username': 'newhire2',
                                     'email': 'newhire2@test.local',
                                     'password': 'temppass1'},
                               headers=auth_headers)
    assert response.status_code == 201
    assert 'warning' in response.get_json()['data']


# ---------------------------------------------------------------------------
# On-demand report/alert delivery
# ---------------------------------------------------------------------------

def test_email_report_requires_permission(client, db, member_headers):
    response = client.post('/api/reports/email',
                           json={'subject': 'X', 'columns': [], 'rows': []},
                           headers=member_headers)
    assert response.status_code == 403


def test_email_report_renders_and_sends(client, db, auth_headers):
    _enable_smtp(db)
    with patch('shopdb.utils.mailer.smtplib.SMTP') as smtp:
        instance = MagicMock()
        smtp.return_value = instance
        response = client.post('/api/reports/email',
                               json={'subject': 'Warranty Report',
                                     'columns': [{'key': 'vendor', 'label': 'Vendor'}],
                                     'rows': [{'vendor': 'Dell'}],
                                     'to': 'boss@test.local'},
                               headers=auth_headers)
    assert response.status_code == 200
    assert response.get_json()['data']['sent'] is True
    instance.sendmail.assert_called_once()


def test_email_report_defaults_to_alert_recipients(client, db, auth_headers):
    _enable_smtp(db)
    with patch('shopdb.utils.mailer.smtplib.SMTP') as smtp:
        instance = MagicMock()
        smtp.return_value = instance
        response = client.post('/api/reports/email',
                               json={'subject': 'Toner', 'columns': [], 'rows': []},
                               headers=auth_headers)
    assert response.status_code == 200
    assert response.get_json()['data']['sent'] is True
