"""
Service-layer tests for category_service — Story 3.2.
Tests run against the in-memory DB via the db fixture.
"""
import pytest
from app.models.category import Category
from app.models.transaction import Transaction
from app.models.account import Account
from app.extensions import db as _db
import app.services.category_service as svc


# ── Helpers ──────────────────────────────────────────────────────────────────

def seed_system_category(name='Groceries'):
    cat = Category(name=name, is_system=True, is_active=True)
    _db.session.add(cat)
    _db.session.commit()
    return cat


def seed_custom_category(name='Custom'):
    cat = Category(name=name, is_system=False, is_active=True)
    _db.session.add(cat)
    _db.session.commit()
    return cat


# ── get_all_active ────────────────────────────────────────────────────────────

class TestGetAllActive:
    def test_returns_only_active(self, app, db):
        with app.app_context():
            active = seed_custom_category('Active')
            inactive = Category(name='Inactive', is_system=False, is_active=False)
            _db.session.add(inactive)
            _db.session.commit()
            result = svc.get_all_active()
            names = [c.name for c in result]
            assert 'Active' in names
            assert 'Inactive' not in names

    def test_system_categories_come_first(self, app, db):
        with app.app_context():
            seed_system_category('Groceries')
            seed_custom_category('AAA Custom')  # alphabetically before Groceries
            result = svc.get_all_active()
            # first category must be system
            assert result[0].is_system is True


# ── create_custom ─────────────────────────────────────────────────────────────

class TestCreateCustom:
    def test_creates_category(self, app, db):
        with app.app_context():
            cat = svc.create_custom('Travel')
            _db.session.commit()
            assert Category.query.filter_by(name='Travel').first() is not None

    def test_new_category_is_not_system(self, app, db):
        with app.app_context():
            cat = svc.create_custom('Travel')
            assert cat.is_system is False

    def test_new_category_is_active(self, app, db):
        with app.app_context():
            cat = svc.create_custom('Travel')
            assert cat.is_active is True

    def test_empty_name_raises(self, app, db):
        with app.app_context():
            with pytest.raises(ValueError, match='required'):
                svc.create_custom('')

    def test_whitespace_only_raises(self, app, db):
        with app.app_context():
            with pytest.raises(ValueError, match='required'):
                svc.create_custom('   ')

    def test_name_too_long_raises(self, app, db):
        with app.app_context():
            with pytest.raises(ValueError, match='50'):
                svc.create_custom('x' * 51)

    def test_duplicate_name_raises(self, app, db):
        with app.app_context():
            seed_custom_category('Travel')
            with pytest.raises(ValueError, match='already exists'):
                svc.create_custom('Travel')


# ── rename ────────────────────────────────────────────────────────────────────

class TestRename:
    def test_renames_category(self, app, db):
        with app.app_context():
            cat = seed_custom_category('Old Name')
            svc.rename(cat, 'New Name')
            _db.session.commit()
            assert Category.query.filter_by(name='New Name').first() is not None

    def test_empty_name_raises(self, app, db):
        with app.app_context():
            cat = seed_custom_category('Test')
            with pytest.raises(ValueError, match='required'):
                svc.rename(cat, '')

    def test_name_too_long_raises(self, app, db):
        with app.app_context():
            cat = seed_custom_category('Test')
            with pytest.raises(ValueError, match='50'):
                svc.rename(cat, 'x' * 51)

    def test_duplicate_name_raises(self, app, db):
        with app.app_context():
            seed_custom_category('Existing')
            cat = seed_custom_category('Target')
            with pytest.raises(ValueError, match='already exists'):
                svc.rename(cat, 'Existing')

    def test_rename_to_same_name_does_not_raise(self, app, db):
        with app.app_context():
            cat = seed_custom_category('Same')
            svc.rename(cat, 'Same')  # no error — same record


# ── soft_delete ───────────────────────────────────────────────────────────────

class TestSoftDelete:
    def test_soft_deletes_unused_custom_category(self, app, db):
        with app.app_context():
            cat = seed_custom_category('Unused')
            svc.soft_delete(cat)
            _db.session.commit()
            refreshed = Category.query.get(cat.id)
            assert refreshed.is_active is False

    def test_system_category_raises(self, app, db):
        with app.app_context():
            cat = seed_system_category('Groceries')
            with pytest.raises(ValueError, match='System'):
                svc.soft_delete(cat)

    def test_category_with_transactions_raises(self, app, db):
        with app.app_context():
            acct = Account(name='Bank', type='checking', is_active=True)
            cat = seed_custom_category('Used')
            _db.session.add(acct)
            _db.session.commit()
            txn = Transaction(
                date='2026-05-01', merchant_normalized='Shop',
                amount='10.00', account_id=acct.id, category_id=cat.id,
            )
            _db.session.add(txn)
            _db.session.commit()
            with pytest.raises(ValueError, match='in use'):
                svc.soft_delete(cat)
