"""Bills & Debts Tracking — Stories 5.1–5.4."""
from decimal import Decimal
from datetime import date, timedelta

import pytest

from app.extensions import db as _db
from app.models.bill import Bill
from app.models.debt import Debt
from app.models.account import Account
from app.models.category import Category
from app.models.transaction import Transaction


# ── fixtures ─────────────────────────────────────────────────────────────────

def _make_account(name="Checking"):
    acct = Account(name=name, type="checking", is_active=True)
    _db.session.add(acct)
    _db.session.commit()
    return acct


def _make_category(name="Utilities"):
    cat = Category(name=name, is_system=True, is_active=True)
    _db.session.add(cat)
    _db.session.commit()
    return cat


def _make_bill(name="Internet", amount="50.00", due_day=15, is_active=True):
    today = date.today()
    bill = Bill(
        name=name,
        amount=Decimal(amount),
        due_day=due_day,
        is_active=is_active,
        due_date=today.replace(day=min(due_day, 28)).isoformat(),
    )
    _db.session.add(bill)
    _db.session.commit()
    return bill


def _make_debt(name="Medical Bill", balance="1000.00", rate="5.00", min_pay="50.00"):
    today = date.today()
    debt = Debt(
        name=name,
        current_balance=Decimal(balance),
        interest_rate=Decimal(rate),
        min_payment=Decimal(min_pay),
        due_date=(today + timedelta(days=10)).isoformat(),
        is_active=True,
    )
    _db.session.add(debt)
    _db.session.commit()
    return debt


# ── Story 5.1: Bill CRUD ──────────────────────────────────────────────────────

class TestBillCreate:
    def test_get_create_form_returns_200(self, client, db):
        response = client.get("/bills/create")
        assert response.status_code == 200

    def test_valid_post_creates_bill(self, client, db):
        response = client.post("/bills/create", data={
            "name": "Netflix",
            "amount": "15.99",
            "due_day": "1",
            "payee": "Netflix",
            "category_id": "0",
            "is_active": "y",
        }, follow_redirects=False)
        assert response.status_code == 302
        assert Bill.query.filter_by(name="Netflix").count() == 1

    def test_valid_post_flashes_success(self, client, db):
        response = client.post("/bills/create", data={
            "name": "Gym",
            "amount": "40.00",
            "due_day": "5",
            "category_id": "0",
            "is_active": "y",
        }, follow_redirects=True)
        assert b"added" in response.data.lower()

    def test_bill_is_active_by_default(self, client, db):
        client.post("/bills/create", data={
            "name": "Phone",
            "amount": "80.00",
            "due_day": "10",
            "category_id": "0",
            "is_active": "y",
        })
        bill = Bill.query.filter_by(name="Phone").first()
        assert bill is not None
        assert bill.is_active is True

    def test_due_date_computed_on_create(self, client, db):
        client.post("/bills/create", data={
            "name": "Rent",
            "amount": "1200.00",
            "due_day": "1",
            "category_id": "0",
            "is_active": "y",
        })
        bill = Bill.query.filter_by(name="Rent").first()
        assert bill.due_date is not None

    def test_missing_name_rerenders(self, client, db):
        response = client.post("/bills/create", data={
            "name": "",
            "amount": "50.00",
            "due_day": "5",
            "category_id": "0",
        })
        assert response.status_code == 200

    def test_invalid_amount_rejected(self, client, db):
        response = client.post("/bills/create", data={
            "name": "Test",
            "amount": "abc",
            "due_day": "5",
            "category_id": "0",
            "is_active": "y",
        }, follow_redirects=True)
        assert Bill.query.count() == 0

    def test_invalid_due_day_rejected(self, client, db):
        response = client.post("/bills/create", data={
            "name": "Test",
            "amount": "50.00",
            "due_day": "99",
            "category_id": "0",
            "is_active": "y",
        }, follow_redirects=True)
        assert Bill.query.count() == 0


class TestBillEdit:
    def test_get_edit_returns_200(self, client, db):
        bill = _make_bill()
        assert client.get(f"/bills/{bill.id}/edit").status_code == 200

    def test_edit_nonexistent_returns_404(self, client, db):
        assert client.get("/bills/9999/edit").status_code == 404

    def test_valid_edit_updates_bill(self, client, db):
        bill = _make_bill()
        client.post(f"/bills/{bill.id}/edit", data={
            "name": "Updated Bill",
            "amount": "75.00",
            "due_day": "20",
            "category_id": "0",
            "is_active": "y",
        })
        _db.session.refresh(bill)
        assert bill.name == "Updated Bill"
        assert bill.amount == Decimal("75.00")

    def test_edit_flashes_success(self, client, db):
        bill = _make_bill()
        response = client.post(f"/bills/{bill.id}/edit", data={
            "name": "Updated",
            "amount": "60.00",
            "due_day": "5",
            "category_id": "0",
            "is_active": "y",
        }, follow_redirects=True)
        assert b"updated" in response.data.lower()


class TestBillDelete:
    def test_delete_removes_bill(self, client, db):
        bill = _make_bill()
        bid = bill.id
        client.post(f"/bills/{bid}/delete")
        assert Bill.query.get(bid) is None

    def test_delete_flashes_success(self, client, db):
        bill = _make_bill()
        response = client.post(f"/bills/{bill.id}/delete", follow_redirects=True)
        assert b"deleted" in response.data.lower()

    def test_delete_nonexistent_returns_404(self, client, db):
        assert client.post("/bills/9999/delete").status_code == 404


# ── Story 5.2: Bills List View ────────────────────────────────────────────────

class TestBillsListView:
    def test_list_returns_200(self, client, db):
        assert client.get("/bills/").status_code == 200

    def test_empty_state_shown_when_no_bills(self, client, db):
        response = client.get("/bills/")
        assert b"No bills yet" in response.data

    def test_active_bills_shown(self, client, db):
        _make_bill("Netflix")
        response = client.get("/bills/")
        assert b"Netflix" in response.data

    def test_inactive_bill_hidden_by_default(self, client, db):
        _make_bill("OldBill", is_active=False)
        response = client.get("/bills/")
        assert b"OldBill" not in response.data

    def test_overdue_bill_shows_overdue_status(self, client, db):
        yesterday = (date.today() - timedelta(days=1)).isoformat()
        bill = Bill(name="OldBill", amount=Decimal("50"), due_day=1,
                    due_date=yesterday, is_active=True)
        _db.session.add(bill)
        _db.session.commit()
        response = client.get("/bills/")
        assert b"Overdue" in response.data

    def test_due_today_bill_shows_due_today_status(self, client, db):
        today_str = date.today().isoformat()
        bill = Bill(name="TodayBill", amount=Decimal("50"), due_day=date.today().day,
                    due_date=today_str, is_active=True)
        _db.session.add(bill)
        _db.session.commit()
        response = client.get("/bills/")
        assert b"Due Today" in response.data

    def test_upcoming_bill_shows_upcoming_status(self, client, db):
        future = (date.today() + timedelta(days=5)).isoformat()
        bill = Bill(name="FutureBill", amount=Decimal("50"), due_day=1,
                    due_date=future, is_active=True)
        _db.session.add(bill)
        _db.session.commit()
        response = client.get("/bills/")
        assert b"Upcoming" in response.data

    def test_view_all_param_shows_beyond_30_days(self, client, db):
        far_future = (date.today() + timedelta(days=60)).isoformat()
        bill = Bill(name="FarBill", amount=Decimal("50"), due_day=1,
                    due_date=far_future, is_active=True)
        _db.session.add(bill)
        _db.session.commit()
        # Without 'all', bill beyond 30 days should not appear
        assert b"FarBill" not in client.get("/bills/").data
        # With 'all=1', it should appear
        assert b"FarBill" in client.get("/bills/?all=1").data

    def test_active_page_is_bills(self, client, db):
        response = client.get("/bills/")
        assert b"bills" in response.data


# ── Story 5.3: Mark Bill as Paid ─────────────────────────────────────────────

class TestMarkBillAsPaid:
    def test_mark_paid_creates_transaction(self, client, db):
        _make_account()
        bill = _make_bill("Electricity", amount="120.00")
        client.post(f"/bills/{bill.id}/pay")
        txn = Transaction.query.filter_by(merchant_normalized="Electricity").first()
        assert txn is not None
        assert txn.amount == Decimal("120.00")

    def test_mark_paid_transaction_date_is_today(self, client, db):
        _make_account()
        bill = _make_bill()
        client.post(f"/bills/{bill.id}/pay")
        txn = Transaction.query.first()
        assert txn.date == date.today().isoformat()

    def test_mark_paid_advances_due_date(self, client, db):
        _make_account()
        bill = _make_bill("Rent", due_day=1)
        old_due = bill.due_date
        client.post(f"/bills/{bill.id}/pay")
        _db.session.refresh(bill)
        assert bill.due_date != old_due

    def test_mark_paid_updates_last_paid_date(self, client, db):
        _make_account()
        bill = _make_bill()
        client.post(f"/bills/{bill.id}/pay")
        _db.session.refresh(bill)
        assert bill.last_paid_date == date.today().isoformat()

    def test_mark_paid_uses_bill_category(self, client, db):
        _make_account()
        cat = _make_category("Utilities")
        bill = _make_bill()
        bill.category_id = cat.id
        _db.session.commit()
        client.post(f"/bills/{bill.id}/pay")
        txn = Transaction.query.first()
        assert txn.category_id == cat.id

    def test_mark_paid_flashes_success(self, client, db):
        _make_account()
        bill = _make_bill()
        response = client.post(f"/bills/{bill.id}/pay", follow_redirects=True)
        assert b"paid" in response.data.lower()

    def test_mark_paid_redirects(self, client, db):
        _make_account()
        bill = _make_bill()
        response = client.post(f"/bills/{bill.id}/pay")
        assert response.status_code == 302

    def test_mark_paid_transaction_is_not_credit(self, client, db):
        _make_account()
        bill = _make_bill()
        client.post(f"/bills/{bill.id}/pay")
        txn = Transaction.query.first()
        assert txn.is_credit is False


# ── Story 5.4: Non-Credit Debt Tracking ──────────────────────────────────────

class TestDebtCreate:
    def test_get_debt_form_returns_200(self, client, db):
        assert client.get("/bills/debts/create").status_code == 200

    def test_valid_post_creates_debt(self, client, db):
        today = date.today()
        client.post("/bills/debts/create", data={
            "name": "Medical",
            "current_balance": "1500.00",
            "interest_rate": "0.00",
            "min_payment": "100.00",
            "due_date": (today + timedelta(days=15)).isoformat(),
        })
        assert Debt.query.filter_by(name="Medical").count() == 1

    def test_valid_post_flashes_success(self, client, db):
        response = client.post("/bills/debts/create", data={
            "name": "Loan",
            "current_balance": "5000.00",
            "interest_rate": "3.50",
            "min_payment": "200.00",
            "due_date": "",
        }, follow_redirects=True)
        assert b"added" in response.data.lower()

    def test_debt_appears_in_bills_list(self, client, db):
        _make_debt("Student Loan")
        response = client.get("/bills/")
        assert b"Student Loan" in response.data

    def test_debt_shows_debt_badge(self, client, db):
        _make_debt("Car Loan")
        response = client.get("/bills/")
        assert b"Debt" in response.data

    def test_invalid_balance_rejected(self, client, db):
        response = client.post("/bills/debts/create", data={
            "name": "Bad",
            "current_balance": "not-a-number",
            "interest_rate": "5.00",
            "min_payment": "50.00",
            "due_date": "",
        }, follow_redirects=True)
        assert Debt.query.count() == 0

    def test_invalid_date_format_rejected(self, client, db):
        response = client.post("/bills/debts/create", data={
            "name": "Bad Date",
            "current_balance": "1000.00",
            "interest_rate": "5.00",
            "min_payment": "50.00",
            "due_date": "not-a-date",
        }, follow_redirects=True)
        assert Debt.query.count() == 0


class TestDebtEdit:
    def test_get_edit_debt_returns_200(self, client, db):
        debt = _make_debt()
        assert client.get(f"/bills/debts/{debt.id}/edit").status_code == 200

    def test_edit_updates_debt(self, client, db):
        debt = _make_debt()
        client.post(f"/bills/debts/{debt.id}/edit", data={
            "name": "Updated Debt",
            "current_balance": "800.00",
            "interest_rate": "4.00",
            "min_payment": "75.00",
            "due_date": "",
        })
        _db.session.refresh(debt)
        assert debt.name == "Updated Debt"
        assert debt.current_balance == Decimal("800.00")


class TestDebtDelete:
    def test_delete_debt_removes_it(self, client, db):
        debt = _make_debt()
        did = debt.id
        client.post(f"/bills/debts/{did}/delete")
        assert Debt.query.get(did) is None

    def test_delete_debt_flashes_success(self, client, db):
        debt = _make_debt()
        response = client.post(f"/bills/debts/{debt.id}/delete", follow_redirects=True)
        assert b"deleted" in response.data.lower()
