"""
Manual transaction entry tests — Story 3.3.
Tests cover: form render, create, dedup warning, force-save, and validation.
"""
import hashlib
from datetime import date, timedelta
from decimal import Decimal

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


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

@pytest.fixture()
def acct(db):
    a = Account(name='Chase Checking', type='checking', 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 post_txn(client, acct_id, cat_id=0, date_str='2026-05-15',
             merchant='ALDI', amount='29.99', notes='', force_save='0'):
    return client.post('/transactions/create', data={
        'date': date_str,
        'merchant': merchant,
        'amount': amount,
        'category_id': cat_id,
        'account_id': acct_id,
        'notes': notes,
        'force_save': force_save,
    }, follow_redirects=False)


def dedup_hash(merchant, amount, date_str):
    amt = str(Decimal(str(amount)).quantize(Decimal('0.01')))
    return hashlib.sha256((merchant + amt + date_str).encode()).hexdigest()


# ── List stub ─────────────────────────────────────────────────────────────────

class TestTransactionsList:
    def test_list_returns_200(self, client, db):
        response = client.get('/transactions/')
        assert response.status_code == 200


# ── Create form render ────────────────────────────────────────────────────────

class TestCreateFormRender:
    def test_create_get_returns_200(self, client, db, acct, cat):
        response = client.get('/transactions/create')
        assert response.status_code == 200

    def test_form_has_required_fields(self, client, db, acct, cat):
        response = client.get('/transactions/create')
        assert b'merchant' in response.data.lower() or b'Merchant' in response.data
        assert b'amount' in response.data.lower() or b'Amount' in response.data
        assert b'date' in response.data.lower() or b'Date' in response.data


# ── Valid create ──────────────────────────────────────────────────────────────

class TestCreateTransaction:
    def test_valid_post_creates_transaction(self, client, db, acct, cat):
        response = post_txn(client, acct.id, cat.id)
        assert response.status_code == 302
        txn = Transaction.query.filter_by(merchant_normalized='ALDI').first()
        assert txn is not None

    def test_saves_correct_fields(self, client, db, acct, cat):
        post_txn(client, acct.id, cat.id, date_str='2026-05-15',
                 merchant='Whole Foods', amount='55.00')
        txn = Transaction.query.filter_by(merchant_normalized='Whole Foods').first()
        assert txn is not None
        assert txn.date == '2026-05-15'
        assert txn.amount == Decimal('55.00')
        assert txn.account_id == acct.id
        assert txn.category_id == cat.id
        assert txn.is_manual is True

    def test_merchant_raw_equals_merchant_normalized(self, client, db, acct):
        post_txn(client, acct.id, merchant='Target')
        txn = Transaction.query.filter_by(merchant_normalized='Target').first()
        assert txn.merchant_raw == txn.merchant_normalized

    def test_dedup_hash_stored(self, client, db, acct):
        post_txn(client, acct.id, date_str='2026-05-15',
                 merchant='ALDI', amount='29.99')
        txn = Transaction.query.filter_by(merchant_normalized='ALDI').first()
        expected = dedup_hash('ALDI', '29.99', '2026-05-15')
        assert txn.dedup_hash == expected

    def test_redirects_to_list(self, client, db, acct):
        response = post_txn(client, acct.id)
        assert '/transactions/' in response.headers['Location']

    def test_flashes_success(self, client, db, acct):
        response = post_txn(client, acct.id, follow_redirects=True)
        assert b'Transaction added' in response.data

    def test_category_optional_saved_as_null(self, client, db, acct):
        post_txn(client, acct.id, cat_id=0)
        txn = Transaction.query.filter_by(merchant_normalized='ALDI').first()
        assert txn.category_id is None

    def test_notes_saved(self, client, db, acct):
        post_txn(client, acct.id, notes='Weekly shop')
        txn = Transaction.query.filter_by(merchant_normalized='ALDI').first()
        assert txn.notes == 'Weekly shop'


# This helper needs to actually follow redirects
def post_txn(client, acct_id, cat_id=0, date_str='2026-05-15',
             merchant='ALDI', amount='29.99', notes='', force_save='0',
             follow_redirects=False):
    return client.post('/transactions/create', data={
        'date': date_str,
        'merchant': merchant,
        'amount': amount,
        'category_id': cat_id,
        'account_id': acct_id,
        'notes': notes,
        'force_save': force_save,
    }, follow_redirects=follow_redirects)


# ── Validation ────────────────────────────────────────────────────────────────

class TestValidation:
    def test_missing_merchant_rerenders_form(self, client, db, acct):
        response = client.post('/transactions/create', data={
            'date': '2026-05-15', 'merchant': '', 'amount': '29.99',
            'account_id': acct.id, 'category_id': 0,
        })
        assert response.status_code == 200
        assert Transaction.query.count() == 0

    def test_missing_amount_rerenders_form(self, client, db, acct):
        response = client.post('/transactions/create', data={
            'date': '2026-05-15', 'merchant': 'ALDI', 'amount': '',
            'account_id': acct.id, 'category_id': 0,
        })
        assert response.status_code == 200
        assert Transaction.query.count() == 0

    def test_invalid_amount_rerenders_form(self, client, db, acct):
        response = client.post('/transactions/create', data={
            'date': '2026-05-15', 'merchant': 'ALDI', 'amount': 'abc',
            'account_id': acct.id, 'category_id': 0,
        })
        assert response.status_code == 200
        assert Transaction.query.count() == 0

    def test_negative_amount_rerenders_form(self, client, db, acct):
        response = client.post('/transactions/create', data={
            'date': '2026-05-15', 'merchant': 'ALDI', 'amount': '-5.00',
            'account_id': acct.id, 'category_id': 0,
        })
        assert response.status_code == 200
        assert Transaction.query.count() == 0


# ── Duplicate detection ───────────────────────────────────────────────────────

class TestDuplicateDetection:
    def _seed_existing(self, acct_id, merchant='ALDI', amount='29.99',
                       date_str='2026-05-15'):
        h = dedup_hash(merchant, amount, date_str)
        txn = Transaction(
            date=date_str, merchant_normalized=merchant,
            amount=Decimal(amount), account_id=acct_id,
            is_manual=True, dedup_hash=h,
        )
        _db.session.add(txn)
        _db.session.commit()
        return txn

    def test_exact_duplicate_shows_warning(self, client, db, acct):
        self._seed_existing(acct.id)
        response = post_txn(client, acct.id, date_str='2026-05-15',
                            merchant='ALDI', amount='29.99')
        assert response.status_code == 200
        assert b'duplicate' in response.data.lower()

    def test_exact_duplicate_does_not_save(self, client, db, acct):
        self._seed_existing(acct.id)
        post_txn(client, acct.id, date_str='2026-05-15',
                 merchant='ALDI', amount='29.99')
        assert Transaction.query.count() == 1  # only the seeded one

    def test_force_save_bypasses_duplicate_check(self, client, db, acct):
        self._seed_existing(acct.id)
        response = post_txn(client, acct.id, date_str='2026-05-15',
                            merchant='ALDI', amount='29.99', force_save='1')
        assert response.status_code == 302
        assert Transaction.query.count() == 2

    def test_different_merchant_no_warning(self, client, db, acct):
        self._seed_existing(acct.id, merchant='ALDI')
        response = post_txn(client, acct.id, date_str='2026-05-15',
                            merchant='Target', amount='29.99')
        assert response.status_code == 302

    def test_outside_3_day_window_no_warning(self, client, db, acct):
        self._seed_existing(acct.id, date_str='2026-05-01')
        response = post_txn(client, acct.id, date_str='2026-05-15',
                            merchant='ALDI', amount='29.99')
        assert response.status_code == 302
