"""
Transaction CSV export tests — Story 3.6.
"""
import csv
import io
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


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


def _parse_csv(response_data):
    text = response_data.decode('utf-8')
    reader = csv.DictReader(io.StringIO(text))
    return list(reader)


# ── Response basics ───────────────────────────────────────────────────────────

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

    def test_content_type_is_csv(self, client, db):
        response = client.get('/transactions/export.csv')
        assert 'text/csv' in response.content_type

    def test_content_disposition_attachment(self, client, db):
        response = client.get('/transactions/export.csv')
        cd = response.headers.get('Content-Disposition', '')
        assert 'attachment' in cd
        assert 'transactions.csv' in cd

    def test_has_header_row(self, client, db):
        response = client.get('/transactions/export.csv')
        text = response.data.decode('utf-8')
        assert 'Date' in text
        assert 'Merchant' in text
        assert 'Amount' in text
        assert 'Category' in text
        assert 'Account' in text
        assert 'Notes' in text

    def test_empty_db_returns_headers_only(self, client, db):
        response = client.get('/transactions/export.csv')
        rows = _parse_csv(response.data)
        assert rows == []


# ── Data content ──────────────────────────────────────────────────────────────

class TestExportContent:
    def test_rows_contain_all_transactions(self, client, db, three_txns):
        response = client.get('/transactions/export.csv')
        rows = _parse_csv(response.data)
        merchants = [r['Merchant'] for r in rows]
        assert 'Amazon' in merchants
        assert 'Whole Foods' in merchants
        assert 'Target' in merchants

    def test_row_fields_correct(self, client, db, acct, cat):
        _make_txn(acct, date='2026-05-15', merchant='Costco', amount='99.50',
                  cat=cat, notes='bulk run')
        _db.session.commit()
        response = client.get('/transactions/export.csv')
        rows = _parse_csv(response.data)
        row = next(r for r in rows if r['Merchant'] == 'Costco')
        assert row['Date'] == '2026-05-15'
        assert row['Amount'] == '99.50'
        assert row['Category'] == 'Groceries'
        assert row['Account'] == 'Chase'
        assert row['Notes'] == 'bulk run'

    def test_missing_category_is_empty_string(self, client, db, acct):
        _make_txn(acct, merchant='Gas Station')
        _db.session.commit()
        response = client.get('/transactions/export.csv')
        rows = _parse_csv(response.data)
        row = next(r for r in rows if r['Merchant'] == 'Gas Station')
        assert row['Category'] == ''

    def test_missing_notes_is_empty_string(self, client, db, acct, cat):
        _make_txn(acct, merchant='Pharmacy', cat=cat)
        _db.session.commit()
        response = client.get('/transactions/export.csv')
        rows = _parse_csv(response.data)
        row = next(r for r in rows if r['Merchant'] == 'Pharmacy')
        assert row['Notes'] == ''

    def test_default_sort_date_desc(self, client, db, three_txns):
        response = client.get('/transactions/export.csv')
        rows = _parse_csv(response.data)
        dates = [r['Date'] for r in rows]
        assert dates == sorted(dates, reverse=True)


# ── Filter application ────────────────────────────────────────────────────────

class TestExportFilters:
    def test_search_filter_applied(self, client, db, three_txns):
        response = client.get('/transactions/export.csv?q=whole')
        rows = _parse_csv(response.data)
        merchants = [r['Merchant'] for r in rows]
        assert merchants == ['Whole Foods']

    def test_category_filter_applied(self, client, db, three_txns, cat):
        response = client.get(f'/transactions/export.csv?category_id={cat.id}')
        rows = _parse_csv(response.data)
        merchants = [r['Merchant'] for r in rows]
        assert 'Amazon' in merchants
        assert 'Whole Foods' in merchants
        assert 'Target' not in merchants

    def test_date_range_filter_applied(self, client, db, three_txns):
        response = client.get('/transactions/export.csv?date_from=2026-05-10&date_to=2026-05-10')
        rows = _parse_csv(response.data)
        assert len(rows) == 1
        assert rows[0]['Merchant'] == 'Whole Foods'

    def test_amount_min_filter_applied(self, client, db, three_txns):
        response = client.get('/transactions/export.csv?amount_min=30')
        rows = _parse_csv(response.data)
        merchants = [r['Merchant'] for r in rows]
        assert 'Amazon' not in merchants
        assert 'Whole Foods' in merchants
        assert 'Target' in merchants

    def test_no_match_returns_headers_only(self, client, db, three_txns):
        response = client.get('/transactions/export.csv?q=zzznomatch')
        rows = _parse_csv(response.data)
        assert rows == []

    def test_exports_all_rows_not_just_one_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/export.csv')
        rows = _parse_csv(response.data)
        assert len(rows) == 55


# ── Sort ──────────────────────────────────────────────────────────────────────

class TestExportSort:
    def test_sort_amount_asc(self, client, db, three_txns):
        response = client.get('/transactions/export.csv?sort=amount&dir=asc')
        rows = _parse_csv(response.data)
        amounts = [float(r['Amount']) for r in rows]
        assert amounts == sorted(amounts)

    def test_sort_merchant_asc(self, client, db, three_txns):
        response = client.get('/transactions/export.csv?sort=merchant&dir=asc')
        rows = _parse_csv(response.data)
        merchants = [r['Merchant'] for r in rows]
        assert merchants == sorted(merchants)


# ── Export link on list page ──────────────────────────────────────────────────

class TestExportLink:
    def test_list_page_has_export_link(self, client, db, three_txns):
        response = client.get('/transactions/')
        assert b'export.csv' in response.data

    def test_export_link_preserves_search_filter(self, client, db, three_txns):
        response = client.get('/transactions/?q=Amazon')
        assert b'export.csv' in response.data
        assert b'q=Amazon' in response.data
