"""
Transaction list — filtering, search, sorting & pagination tests — Story 3.5.
"""
from decimal import Decimal

import pytest

from app.extensions import db as _db
from app.models.account import Account
from app.models.category import Category
from app.models.transaction import Transaction


# ── Fixtures ──────────────────────────────────────────────────────────────────

@pytest.fixture()
def acct(db):
    a = Account(name='Chase', type='checking', is_active=True)
    _db.session.add(a)
    _db.session.commit()
    return a


@pytest.fixture()
def acct2(db):
    a = Account(name='Savings', type='savings', is_active=True)
    _db.session.add(a)
    _db.session.commit()
    return a


@pytest.fixture()
def cat(db):
    c = Category(name='Groceries', is_system=True, is_active=True)
    _db.session.add(c)
    _db.session.commit()
    return c


@pytest.fixture()
def cat2(db):
    c = Category(name='Dining', is_system=False, is_active=True)
    _db.session.add(c)
    _db.session.commit()
    return c


def _make_txn(acct, date='2026-05-10', merchant='ALDI', amount='25.00',
              cat=None, notes=None):
    t = Transaction(
        date=date,
        merchant_normalized=merchant,
        merchant_raw=merchant,
        amount=Decimal(amount),
        account_id=acct.id,
        category_id=cat.id if cat else None,
        notes=notes,
        is_manual=True,
    )
    _db.session.add(t)
    return t


@pytest.fixture()
def three_txns(db, acct, cat):
    t1 = _make_txn(acct, date='2026-05-01', merchant='Amazon', amount='10.00', cat=cat)
    t2 = _make_txn(acct, date='2026-05-10', merchant='Whole Foods', amount='55.00', cat=cat)
    t3 = _make_txn(acct, date='2026-05-20', merchant='Target', amount='30.00')
    _db.session.commit()
    return t1, t2, t3


# ── Basic render ──────────────────────────────────────────────────────────────

class TestListBasic:
    def test_returns_200(self, client, db, acct):
        response = client.get('/transactions/')
        assert response.status_code == 200

    def test_shows_transaction_merchant(self, client, db, three_txns):
        response = client.get('/transactions/')
        assert b'Whole Foods' in response.data

    def test_shows_all_three_transactions(self, client, db, three_txns):
        response = client.get('/transactions/')
        assert b'Amazon' in response.data
        assert b'Whole Foods' in response.data
        assert b'Target' in response.data

    def test_each_row_links_to_edit(self, client, db, three_txns):
        t1, t2, t3 = three_txns
        response = client.get('/transactions/')
        assert f'/transactions/{t1.id}/edit'.encode() in response.data

    def test_total_count_displayed(self, client, db, three_txns):
        response = client.get('/transactions/')
        assert b'3' in response.data


# ── Empty states ──────────────────────────────────────────────────────────────

class TestEmptyStates:
    def test_no_transactions_shows_no_transactions_yet(self, client, db):
        response = client.get('/transactions/')
        assert b'No transactions yet' in response.data

    def test_no_transactions_shows_add_transaction_cta(self, client, db):
        response = client.get('/transactions/')
        assert b'Add Transaction' in response.data

    def test_filters_no_match_shows_no_match_message(self, client, db, three_txns):
        response = client.get('/transactions/?q=zzznomatch')
        assert b'No transactions match' in response.data

    def test_filters_no_match_shows_clear_filters_link(self, client, db, three_txns):
        response = client.get('/transactions/?q=zzznomatch')
        assert b'Clear' in response.data


# ── Search ────────────────────────────────────────────────────────────────────

class TestSearch:
    def test_search_filters_by_merchant(self, client, db, three_txns):
        response = client.get('/transactions/?q=whole')
        assert b'Whole Foods' in response.data
        assert b'Amazon' not in response.data
        assert b'Target' not in response.data

    def test_search_is_case_insensitive(self, client, db, three_txns):
        response = client.get('/transactions/?q=WHOLE')
        assert b'Whole Foods' in response.data

    def test_search_filters_by_notes(self, client, db, acct):
        t = _make_txn(acct, merchant='Gas Station', notes='road trip fuel')
        _db.session.commit()
        response = client.get('/transactions/?q=road+trip')
        assert b'Gas Station' in response.data

    def test_search_no_match_returns_empty(self, client, db, three_txns):
        response = client.get('/transactions/?q=zzznomatch')
        assert b'Amazon' not in response.data
        assert b'Whole Foods' not in response.data


# ── Category filter ───────────────────────────────────────────────────────────

class TestCategoryFilter:
    def test_filter_by_category_shows_only_matching(self, client, db, three_txns, cat):
        t1, t2, t3 = three_txns
        response = client.get(f'/transactions/?category_id={cat.id}')
        assert b'Amazon' in response.data
        assert b'Whole Foods' in response.data
        assert b'Target' not in response.data

    def test_filter_by_nonexistent_category_returns_empty(self, client, db, three_txns):
        response = client.get('/transactions/?category_id=9999')
        assert b'Amazon' not in response.data


# ── Account filter ────────────────────────────────────────────────────────────

class TestAccountFilter:
    def test_filter_by_account_shows_only_matching(self, client, db, acct, acct2, cat):
        _make_txn(acct, merchant='In Chase', amount='10.00')
        _make_txn(acct2, merchant='In Savings', amount='20.00')
        _db.session.commit()
        response = client.get(f'/transactions/?account_id={acct.id}')
        assert b'In Chase' in response.data
        assert b'In Savings' not in response.data


# ── Date range filter ─────────────────────────────────────────────────────────

class TestDateRangeFilter:
    def test_date_from_excludes_earlier(self, client, db, three_txns):
        response = client.get('/transactions/?date_from=2026-05-10')
        assert b'Amazon' not in response.data   # 2026-05-01
        assert b'Whole Foods' in response.data  # 2026-05-10
        assert b'Target' in response.data       # 2026-05-20

    def test_date_to_excludes_later(self, client, db, three_txns):
        response = client.get('/transactions/?date_to=2026-05-10')
        assert b'Amazon' in response.data       # 2026-05-01
        assert b'Whole Foods' in response.data  # 2026-05-10
        assert b'Target' not in response.data   # 2026-05-20

    def test_date_range_combined(self, client, db, three_txns):
        response = client.get('/transactions/?date_from=2026-05-10&date_to=2026-05-10')
        assert b'Whole Foods' in response.data
        assert b'Amazon' not in response.data
        assert b'Target' not in response.data


# ── Amount range filter ───────────────────────────────────────────────────────

class TestAmountRangeFilter:
    def test_amount_min_excludes_smaller(self, client, db, three_txns):
        response = client.get('/transactions/?amount_min=30')
        assert b'Amazon' not in response.data   # 10.00
        assert b'Whole Foods' in response.data  # 55.00
        assert b'Target' in response.data       # 30.00

    def test_amount_max_excludes_larger(self, client, db, three_txns):
        response = client.get('/transactions/?amount_max=30')
        assert b'Amazon' in response.data       # 10.00
        assert b'Target' in response.data       # 30.00
        assert b'Whole Foods' not in response.data  # 55.00

    def test_amount_range_combined(self, client, db, three_txns):
        response = client.get('/transactions/?amount_min=25&amount_max=40')
        assert b'Target' in response.data       # 30.00
        assert b'Amazon' not in response.data   # 10.00
        assert b'Whole Foods' not in response.data  # 55.00


# ── Sorting ───────────────────────────────────────────────────────────────────

class TestSorting:
    def test_default_sort_is_date_desc(self, client, db, three_txns):
        response = client.get('/transactions/')
        html = response.data.decode()
        target_pos = html.find('Target')   # 2026-05-20 — newest
        amazon_pos = html.find('Amazon')   # 2026-05-01 — oldest
        assert target_pos < amazon_pos

    def test_sort_date_asc(self, client, db, three_txns):
        response = client.get('/transactions/?sort=date&dir=asc')
        html = response.data.decode()
        amazon_pos = html.find('Amazon')
        target_pos = html.find('Target')
        assert amazon_pos < target_pos

    def test_sort_amount_desc(self, client, db, three_txns):
        response = client.get('/transactions/?sort=amount&dir=desc')
        html = response.data.decode()
        whole_pos = html.find('Whole Foods')  # 55.00
        amazon_pos = html.find('Amazon')       # 10.00
        assert whole_pos < amazon_pos

    def test_sort_amount_asc(self, client, db, three_txns):
        response = client.get('/transactions/?sort=amount&dir=asc')
        html = response.data.decode()
        amazon_pos = html.find('Amazon')       # 10.00
        whole_pos = html.find('Whole Foods')   # 55.00
        assert amazon_pos < whole_pos

    def test_sort_merchant_asc(self, client, db, three_txns):
        response = client.get('/transactions/?sort=merchant&dir=asc')
        html = response.data.decode()
        amazon_pos = html.find('Amazon')
        whole_pos = html.find('Whole Foods')
        target_pos = html.find('Target')
        assert amazon_pos < target_pos < whole_pos

    def test_sort_indicator_shown(self, client, db, three_txns):
        response = client.get('/transactions/?sort=date&dir=desc')
        assert b'sort=date' in response.data

    def test_invalid_sort_column_ignored(self, client, db, three_txns):
        response = client.get('/transactions/?sort=invalid&dir=desc')
        assert response.status_code == 200


# ── Pagination ────────────────────────────────────────────────────────────────

class TestPagination:
    def test_pagination_shows_first_page(self, client, db, acct):
        for i in range(55):
            _make_txn(acct, date=f'2026-01-{i % 28 + 1:02d}',
                      merchant=f'Merchant {i:03d}', amount='1.00')
        _db.session.commit()
        response = client.get('/transactions/')
        assert b'Page 1' in response.data

    def test_pagination_page_two_accessible(self, client, db, acct):
        for i in range(55):
            _make_txn(acct, date=f'2026-01-{i % 28 + 1:02d}',
                      merchant=f'Merchant {i:03d}', amount='1.00')
        _db.session.commit()
        response = client.get('/transactions/?page=2')
        assert response.status_code == 200
        assert b'Page 2' in response.data

    def test_pagination_shows_50_per_page(self, client, db, acct):
        for i in range(55):
            _make_txn(acct, date=f'2026-01-{i % 28 + 1:02d}',
                      merchant=f'Merchant {i:03d}', amount='1.00')
        _db.session.commit()
        response = client.get('/transactions/')
        html = response.data.decode()
        count = sum(1 for i in range(55) if f'Merchant {i:03d}' in html)
        assert count == 50

    def test_pagination_preserves_filters(self, client, db, acct):
        for i in range(55):
            _make_txn(acct, date=f'2026-01-{i % 28 + 1:02d}',
                      merchant=f'Merchant {i:03d}', amount='1.00')
        _db.session.commit()
        response = client.get('/transactions/?q=Merchant&page=1')
        assert b'q=Merchant' in response.data or b'q=Merchant' in response.data


# ── URL param persistence ─────────────────────────────────────────────────────

class TestUrlParams:
    def test_active_filters_appear_in_filter_form(self, client, db, three_txns, cat):
        response = client.get(f'/transactions/?category_id={cat.id}')
        assert str(cat.id).encode() in response.data

    def test_search_term_preserved_in_form(self, client, db, three_txns):
        response = client.get('/transactions/?q=whole')
        assert b'whole' in response.data

    def test_combined_filters_all_applied(self, client, db, three_txns, cat):
        response = client.get(
            f'/transactions/?q=Amazon&category_id={cat.id}'
            f'&date_from=2026-05-01&date_to=2026-05-10'
        )
        assert b'Amazon' in response.data
        assert b'Whole Foods' not in response.data
        assert b'Target' not in response.data
