"""Tests for B-2.7 — Stripe Checkout (3 plans CAD + TPS/TVQ + Apple/Google Pay).

Covers:
  - plans.py: Plan dataclass, env-resolved Price IDs, helpers, is_configured.
  - stripe_client.py: lazy api_key init, get_or_create_customer, create_checkout_session.
  - routes.py: GET /checkout/<plan>, /checkout/success, /checkout/cancel.
  - Integration: app.py _PUBLIC_INDEXABLE_ENDPOINTS includes 'billing.success'.

Mocks the stripe library functions (stripe.Customer.create, stripe.checkout.Session.create)
via unittest.mock.patch — no real Stripe API calls.

Note: pytest cannot collect this file on Windows native because src/init_db.py
imports `fcntl` (POSIX-only). Use tests/_run_stripe_checkout_windows.py.
"""
import os
import sys
from unittest.mock import patch, MagicMock

sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
os.environ.setdefault('SQLALCHEMY_DATABASE_URI', 'sqlite:///:memory:')
os.environ.setdefault('SECRET_KEY', 'test-secret-key-stripe')
os.environ.setdefault('ENABLE_EMAIL_VERIFICATION', 'false')
os.environ.setdefault('REQUIRE_EMAIL_VERIFICATION', 'false')
os.environ.setdefault('RATELIMIT_ENABLED', 'false')

from src.app import app, db, bcrypt  # noqa: E402
from src.models.user import User  # noqa: E402


_PRICE_ENV_VARS = (
    'STRIPE_SECRET_KEY',
    'STRIPE_DICTIA_8_SETUP', 'STRIPE_DICTIA_8_MONTHLY', 'STRIPE_DICTIA_8_YEARLY',
    'STRIPE_DICTIA_16_SETUP', 'STRIPE_DICTIA_16_MONTHLY', 'STRIPE_DICTIA_16_YEARLY',
    'STRIPE_DICTIA_CLOUD_MONTHLY', 'STRIPE_DICTIA_CLOUD_YEARLY',
)


def _clear_stripe_env():
    for k in _PRICE_ENV_VARS:
        os.environ.pop(k, None)
    # Reset stripe module-level api_key state
    import stripe
    stripe.api_key = None


def _disable_csrf():
    app.config['WTF_CSRF_ENABLED'] = False


def _make_user(email='checkout@example.qc.ca', password='Password!123',
               username=None, name='Checkout User',
               stripe_customer_id=None):
    hashed = bcrypt.generate_password_hash(password).decode('utf-8')
    u = User(
        username=username or email.split('@', 1)[0][:20],
        email=email,
        password=hashed,
        email_verified=True,
        name=name,
        stripe_customer_id=stripe_customer_id,
    )
    db.session.add(u)
    db.session.commit()
    return u


def _login_session(client, user):
    with client.session_transaction() as sess:
        sess['_user_id'] = str(user.id)
        sess['_fresh'] = True


# ----------------------------------------------------------------------
# 1-2. is_stripe_configured
# ----------------------------------------------------------------------

def test_is_stripe_configured_when_env_set():
    _clear_stripe_env()
    try:
        os.environ['STRIPE_SECRET_KEY'] = 'sk_test_fake'
        from src.billing.stripe_client import is_stripe_configured
        assert is_stripe_configured() is True
    finally:
        _clear_stripe_env()


def test_is_stripe_configured_when_env_unset():
    _clear_stripe_env()
    try:
        from src.billing.stripe_client import is_stripe_configured
        assert is_stripe_configured() is False
    finally:
        _clear_stripe_env()


# ----------------------------------------------------------------------
# 3-4. get_plan
# ----------------------------------------------------------------------

def test_get_plan_returns_known_plan():
    from src.billing.plans import get_plan, Plan
    plan = get_plan('dictia-cloud')
    assert plan is not None
    assert isinstance(plan, Plan)
    assert plan.slug == 'dictia-cloud'
    assert plan.has_setup_fee is False


def test_get_plan_returns_none_for_unknown():
    from src.billing.plans import get_plan
    assert get_plan('foo') is None
    assert get_plan('') is None


# ----------------------------------------------------------------------
# 5-7. Plan.is_configured
# ----------------------------------------------------------------------

def test_plan_is_configured_when_env_set():
    _clear_stripe_env()
    try:
        os.environ['STRIPE_DICTIA_CLOUD_MONTHLY'] = 'price_cloud_m'
        os.environ['STRIPE_DICTIA_CLOUD_YEARLY'] = 'price_cloud_y'
        from src.billing.plans import get_plan
        assert get_plan('dictia-cloud').is_configured() is True
    finally:
        _clear_stripe_env()


def test_plan_is_not_configured_when_env_missing():
    _clear_stripe_env()
    try:
        from src.billing.plans import get_plan
        assert get_plan('dictia-cloud').is_configured() is False
    finally:
        _clear_stripe_env()


def test_hardware_plan_requires_setup_env():
    _clear_stripe_env()
    try:
        os.environ['STRIPE_DICTIA_8_MONTHLY'] = 'price_8_m'
        os.environ['STRIPE_DICTIA_8_YEARLY'] = 'price_8_y'
        # NO STRIPE_DICTIA_8_SETUP
        from src.billing.plans import get_plan
        assert get_plan('dictia-8').is_configured() is False
        os.environ['STRIPE_DICTIA_8_SETUP'] = 'price_8_setup'
        assert get_plan('dictia-8').is_configured() is True
    finally:
        _clear_stripe_env()


# ----------------------------------------------------------------------
# 8-9. get_or_create_customer
# ----------------------------------------------------------------------

def test_get_or_create_customer_creates_when_missing():
    with app.app_context():
        _clear_stripe_env()
        os.environ['STRIPE_SECRET_KEY'] = 'sk_test_fake'
        db.create_all()
        try:
            user = _make_user(email='newcust@example.qc.ca', name='Alice')
            assert user.stripe_customer_id is None
            with patch('src.billing.stripe_client.stripe.Customer.create') as mock_cust:
                mock_cust.return_value = MagicMock(id='cus_fakeNEW')
                from src.billing.stripe_client import get_or_create_customer
                cust_id = get_or_create_customer(user)
            assert cust_id == 'cus_fakeNEW'
            mock_cust.assert_called_once()
            kwargs = mock_cust.call_args.kwargs
            assert kwargs['email'] == 'newcust@example.qc.ca'
            assert kwargs['name'] == 'Alice'
            assert kwargs['metadata']['dictia_user_id'] == str(user.id)
            db.session.refresh(user)
            assert user.stripe_customer_id == 'cus_fakeNEW'
        finally:
            db.session.rollback()
            db.drop_all()
            _clear_stripe_env()


def test_get_or_create_customer_reuses_existing():
    with app.app_context():
        _clear_stripe_env()
        os.environ['STRIPE_SECRET_KEY'] = 'sk_test_fake'
        db.create_all()
        try:
            user = _make_user(email='oldcust@example.qc.ca',
                              stripe_customer_id='cus_existing')
            with patch('src.billing.stripe_client.stripe.Customer.create') as mock_cust:
                from src.billing.stripe_client import get_or_create_customer
                cust_id = get_or_create_customer(user)
            assert cust_id == 'cus_existing'
            mock_cust.assert_not_called()
        finally:
            db.session.rollback()
            db.drop_all()
            _clear_stripe_env()


# ----------------------------------------------------------------------
# 10-13. create_checkout_session
# ----------------------------------------------------------------------

def test_create_checkout_session_includes_setup_for_hardware_plan():
    with app.app_context():
        _clear_stripe_env()
        os.environ['STRIPE_SECRET_KEY'] = 'sk_test_fake'
        os.environ['STRIPE_DICTIA_8_SETUP'] = 'price_setup'
        os.environ['STRIPE_DICTIA_8_MONTHLY'] = 'price_8m'
        os.environ['STRIPE_DICTIA_8_YEARLY'] = 'price_8y'
        db.create_all()
        try:
            user = _make_user(email='hwsetup@example.qc.ca', name='Bob')
            with patch('src.billing.stripe_client.stripe.Customer.create') as mock_cust, \
                 patch('src.billing.stripe_client.stripe.checkout.Session.create') as mock_sess:
                mock_cust.return_value = MagicMock(id='cus_x')
                mock_sess.return_value = MagicMock(url='https://checkout.stripe.test/cs_x')
                from src.billing.stripe_client import create_checkout_session
                create_checkout_session(
                    plan_slug='dictia-8', period='monthly', user=user,
                    success_url='https://x.ca/success', cancel_url='https://x.ca/cancel',
                )
                kwargs = mock_sess.call_args.kwargs
                assert len(kwargs['line_items']) == 2
                assert kwargs['line_items'][0]['price'] == 'price_setup'
                assert kwargs['line_items'][1]['price'] == 'price_8m'
                assert kwargs['mode'] == 'subscription'
                assert kwargs['currency'] == 'cad'
                assert kwargs['automatic_tax']['enabled'] is True
                assert kwargs['allow_promotion_codes'] is True
                assert kwargs['billing_address_collection'] == 'required'
                # success_url must include CHECKOUT_SESSION_ID placeholder
                assert '{CHECKOUT_SESSION_ID}' in kwargs['success_url']
        finally:
            db.session.rollback()
            db.drop_all()
            _clear_stripe_env()


def test_create_checkout_session_no_setup_for_cloud_plan():
    with app.app_context():
        _clear_stripe_env()
        os.environ['STRIPE_SECRET_KEY'] = 'sk_test_fake'
        os.environ['STRIPE_DICTIA_CLOUD_MONTHLY'] = 'price_cm'
        os.environ['STRIPE_DICTIA_CLOUD_YEARLY'] = 'price_cy'
        db.create_all()
        try:
            user = _make_user(email='cloudplan@example.qc.ca', name='Carol')
            with patch('src.billing.stripe_client.stripe.Customer.create') as mock_cust, \
                 patch('src.billing.stripe_client.stripe.checkout.Session.create') as mock_sess:
                mock_cust.return_value = MagicMock(id='cus_y')
                mock_sess.return_value = MagicMock(url='https://x/cs_y')
                from src.billing.stripe_client import create_checkout_session
                create_checkout_session(
                    plan_slug='dictia-cloud', period='monthly', user=user,
                    success_url='https://x.ca/success', cancel_url='https://x.ca/cancel',
                )
                kwargs = mock_sess.call_args.kwargs
                assert len(kwargs['line_items']) == 1
                assert kwargs['line_items'][0]['price'] == 'price_cm'
        finally:
            db.session.rollback()
            db.drop_all()
            _clear_stripe_env()


def test_create_checkout_session_uses_yearly_price_when_period_yearly():
    with app.app_context():
        _clear_stripe_env()
        os.environ['STRIPE_SECRET_KEY'] = 'sk_test_fake'
        os.environ['STRIPE_DICTIA_CLOUD_MONTHLY'] = 'price_cm'
        os.environ['STRIPE_DICTIA_CLOUD_YEARLY'] = 'price_cy'
        db.create_all()
        try:
            user = _make_user(email='yearly@example.qc.ca', name='Dan')
            with patch('src.billing.stripe_client.stripe.Customer.create') as mock_cust, \
                 patch('src.billing.stripe_client.stripe.checkout.Session.create') as mock_sess:
                mock_cust.return_value = MagicMock(id='cus_z')
                mock_sess.return_value = MagicMock(url='https://x/cs_z')
                from src.billing.stripe_client import create_checkout_session
                create_checkout_session(
                    plan_slug='dictia-cloud', period='yearly', user=user,
                    success_url='https://x.ca/success', cancel_url='https://x.ca/cancel',
                )
                kwargs = mock_sess.call_args.kwargs
                assert kwargs['line_items'][0]['price'] == 'price_cy'
        finally:
            db.session.rollback()
            db.drop_all()
            _clear_stripe_env()


def test_create_checkout_session_includes_metadata():
    with app.app_context():
        _clear_stripe_env()
        os.environ['STRIPE_SECRET_KEY'] = 'sk_test_fake'
        os.environ['STRIPE_DICTIA_CLOUD_MONTHLY'] = 'price_cm'
        os.environ['STRIPE_DICTIA_CLOUD_YEARLY'] = 'price_cy'
        db.create_all()
        try:
            user = _make_user(email='meta@example.qc.ca', name='Eve')
            with patch('src.billing.stripe_client.stripe.Customer.create') as mock_cust, \
                 patch('src.billing.stripe_client.stripe.checkout.Session.create') as mock_sess:
                mock_cust.return_value = MagicMock(id='cus_q')
                mock_sess.return_value = MagicMock(url='https://x/cs_q')
                from src.billing.stripe_client import create_checkout_session
                create_checkout_session(
                    plan_slug='dictia-cloud', period='monthly', user=user,
                    success_url='https://x.ca/success', cancel_url='https://x.ca/cancel',
                )
                kwargs = mock_sess.call_args.kwargs
                meta = kwargs['metadata']
                assert meta['dictia_user_id'] == str(user.id)
                assert meta['dictia_plan_slug'] == 'dictia-cloud'
                assert meta['dictia_period'] == 'monthly'
                # Subscription-level metadata too (used by webhook B-2.8)
                sub_meta = kwargs['subscription_data']['metadata']
                assert sub_meta['dictia_user_id'] == str(user.id)
                assert sub_meta['dictia_plan_slug'] == 'dictia-cloud'
                assert sub_meta['dictia_period'] == 'monthly'
        finally:
            db.session.rollback()
            db.drop_all()
            _clear_stripe_env()


# ----------------------------------------------------------------------
# 14-17. create_checkout_session error paths
# ----------------------------------------------------------------------

def test_create_checkout_session_raises_on_unknown_plan():
    with app.app_context():
        _clear_stripe_env()
        os.environ['STRIPE_SECRET_KEY'] = 'sk_test_fake'
        db.create_all()
        try:
            user = _make_user(email='unkplan@example.qc.ca')
            from src.billing.stripe_client import create_checkout_session
            try:
                create_checkout_session(
                    plan_slug='foo', period='monthly', user=user,
                    success_url='https://x/s', cancel_url='https://x/c',
                )
                raise AssertionError('Expected ValueError')
            except ValueError:
                pass
        finally:
            db.session.rollback()
            db.drop_all()
            _clear_stripe_env()


def test_create_checkout_session_raises_on_invalid_period():
    with app.app_context():
        _clear_stripe_env()
        os.environ['STRIPE_SECRET_KEY'] = 'sk_test_fake'
        os.environ['STRIPE_DICTIA_CLOUD_MONTHLY'] = 'price_cm'
        os.environ['STRIPE_DICTIA_CLOUD_YEARLY'] = 'price_cy'
        db.create_all()
        try:
            user = _make_user(email='badperiod@example.qc.ca')
            from src.billing.stripe_client import create_checkout_session
            try:
                create_checkout_session(
                    plan_slug='dictia-cloud', period='quarterly', user=user,
                    success_url='https://x/s', cancel_url='https://x/c',
                )
                raise AssertionError('Expected ValueError')
            except ValueError:
                pass
        finally:
            db.session.rollback()
            db.drop_all()
            _clear_stripe_env()


def test_create_checkout_session_raises_when_stripe_not_configured():
    with app.app_context():
        _clear_stripe_env()
        # NO STRIPE_SECRET_KEY
        db.create_all()
        try:
            user = _make_user(email='nokey@example.qc.ca')
            from src.billing.stripe_client import (
                create_checkout_session, StripeNotConfiguredError,
            )
            try:
                create_checkout_session(
                    plan_slug='dictia-cloud', period='monthly', user=user,
                    success_url='https://x/s', cancel_url='https://x/c',
                )
                raise AssertionError('Expected StripeNotConfiguredError')
            except StripeNotConfiguredError:
                pass
        finally:
            db.session.rollback()
            db.drop_all()
            _clear_stripe_env()


def test_create_checkout_session_raises_when_plan_env_missing():
    with app.app_context():
        _clear_stripe_env()
        os.environ['STRIPE_SECRET_KEY'] = 'sk_test_fake'
        # NO price IDs for dictia-cloud
        db.create_all()
        try:
            user = _make_user(email='noprice@example.qc.ca')
            from src.billing.stripe_client import (
                create_checkout_session, StripeNotConfiguredError,
            )
            try:
                create_checkout_session(
                    plan_slug='dictia-cloud', period='monthly', user=user,
                    success_url='https://x/s', cancel_url='https://x/c',
                )
                raise AssertionError('Expected StripeNotConfiguredError')
            except StripeNotConfiguredError:
                pass
        finally:
            db.session.rollback()
            db.drop_all()
            _clear_stripe_env()


# ----------------------------------------------------------------------
# 18-22. /checkout/<plan> route
# ----------------------------------------------------------------------

def test_checkout_route_redirects_to_stripe_url():
    with app.app_context():
        _disable_csrf()
        _clear_stripe_env()
        os.environ['STRIPE_SECRET_KEY'] = 'sk_test_fake'
        os.environ['STRIPE_DICTIA_CLOUD_MONTHLY'] = 'price_cm'
        os.environ['STRIPE_DICTIA_CLOUD_YEARLY'] = 'price_cy'
        db.create_all()
        try:
            user = _make_user(email='rt-redir@example.qc.ca', name='Frank')
            with app.test_client() as client:
                _login_session(client, user)
                with patch('src.billing.routes.create_checkout_session') as mock_create:
                    mock_create.return_value = MagicMock(
                        url='https://checkout.stripe.test/cs_redir'
                    )
                    resp = client.get('/checkout/dictia-cloud?period=monthly',
                                      follow_redirects=False)
                assert resp.status_code == 303
                assert resp.headers['Location'] == 'https://checkout.stripe.test/cs_redir'
                # Ensure routes called the helper with the right args
                mock_create.assert_called_once()
                call_kwargs = mock_create.call_args.kwargs
                assert call_kwargs['plan_slug'] == 'dictia-cloud'
                assert call_kwargs['period'] == 'monthly'
        finally:
            db.session.rollback()
            db.drop_all()
            _clear_stripe_env()


def test_checkout_route_unknown_plan_redirects_to_tarifs():
    with app.app_context():
        _disable_csrf()
        _clear_stripe_env()
        os.environ['STRIPE_SECRET_KEY'] = 'sk_test_fake'
        db.create_all()
        try:
            user = _make_user(email='rt-unk@example.qc.ca')
            with app.test_client() as client:
                _login_session(client, user)
                resp = client.get('/checkout/foo', follow_redirects=False)
                assert resp.status_code == 302
                assert '/tarifs' in resp.headers['Location']
        finally:
            db.session.rollback()
            db.drop_all()
            _clear_stripe_env()


def test_checkout_route_normalizes_invalid_period_to_monthly():
    with app.app_context():
        _disable_csrf()
        _clear_stripe_env()
        os.environ['STRIPE_SECRET_KEY'] = 'sk_test_fake'
        os.environ['STRIPE_DICTIA_CLOUD_MONTHLY'] = 'price_cm'
        os.environ['STRIPE_DICTIA_CLOUD_YEARLY'] = 'price_cy'
        db.create_all()
        try:
            user = _make_user(email='rt-period@example.qc.ca')
            with app.test_client() as client:
                _login_session(client, user)
                with patch('src.billing.routes.create_checkout_session') as mock_create:
                    mock_create.return_value = MagicMock(
                        url='https://checkout.stripe.test/cs_norm'
                    )
                    resp = client.get('/checkout/dictia-cloud?period=quarterly',
                                      follow_redirects=False)
                assert resp.status_code == 303
                assert mock_create.call_args.kwargs['period'] == 'monthly'
        finally:
            db.session.rollback()
            db.drop_all()
            _clear_stripe_env()


def test_checkout_route_requires_login():
    with app.app_context():
        _disable_csrf()
        _clear_stripe_env()
        db.create_all()
        try:
            with app.test_client() as client:
                resp = client.get('/checkout/dictia-cloud',
                                  follow_redirects=False)
                assert resp.status_code == 302
                assert '/login' in resp.headers['Location']
        finally:
            db.session.rollback()
            db.drop_all()
            _clear_stripe_env()


def test_checkout_route_friendly_message_when_stripe_not_configured():
    with app.app_context():
        _disable_csrf()
        _clear_stripe_env()
        # NO STRIPE_SECRET_KEY
        db.create_all()
        try:
            user = _make_user(email='rt-noconfig@example.qc.ca')
            with app.test_client() as client:
                _login_session(client, user)
                resp = client.get('/checkout/dictia-cloud',
                                  follow_redirects=False)
                assert resp.status_code == 302
                assert '/tarifs' in resp.headers['Location']
                # Follow the redirect to see the flashed message
                resp2 = client.get('/tarifs')
                body = resp2.get_data(as_text=True)
                assert 'info@dictia.ca' in body
        finally:
            db.session.rollback()
            db.drop_all()
            _clear_stripe_env()


# ----------------------------------------------------------------------
# 23-24. /checkout/success and /checkout/cancel
# ----------------------------------------------------------------------

def test_success_route_renders_template():
    with app.app_context():
        _disable_csrf()
        db.create_all()
        try:
            with app.test_client() as client:
                resp = client.get('/checkout/success?session_id=cs_test_abc')
                assert resp.status_code == 200
                body = resp.get_data(as_text=True)
                # Body should mention the async-activation note (per spec)
                assert 'minutes' in body.lower() or 'activé' in body.lower() \
                    or 'activée' in body.lower() or 'confirmé' in body.lower()
        finally:
            db.session.rollback()
            db.drop_all()


def test_cancel_route_renders_template():
    with app.app_context():
        _disable_csrf()
        db.create_all()
        try:
            with app.test_client() as client:
                resp = client.get('/checkout/cancel')
                assert resp.status_code == 200
                body = resp.get_data(as_text=True)
                # "no charge made" reassurance in French
                assert 'aucun' in body.lower() or 'annulé' in body.lower()
        finally:
            db.session.rollback()
            db.drop_all()


# ----------------------------------------------------------------------
# 25. Integration with no-crawl headers
# ----------------------------------------------------------------------

def test_success_route_in_public_indexable_endpoints():
    """Defensive: 'billing.success' was added to _PUBLIC_INDEXABLE_ENDPOINTS in B-1.3."""
    from src.app import _PUBLIC_INDEXABLE_ENDPOINTS
    assert 'billing.success' in _PUBLIC_INDEXABLE_ENDPOINTS
