"""
EXAMPLE TRANSACTIONS — written for the USER, not for the developer.

A port of `features/budget/data/mock.js`. The set is a plausible couple of
months in a real person's life, so a first-run user learns what a category
limit is for by seeing one being bumped into rather than by reading a tooltip.

Dropped from the set it replaced: a £12,500 transfer with a ninety-character
name, a row with no name at all, and a row for £0.00. All three were there to
break the columns on purpose, and all three read as somebody else's messy
ledger.

What it still shows, one clean example each — the list is here so a row cannot
be deleted without noticing what went with it:

    a category going OVER its limit      food
    a category exactly ON its limit      transport
    a category with room left            learning
    a category with no limit at all      home
    an expense with no category          the cash withdrawal
    two transactions on the same day     the coffee and the lunch
    an amount under a pound              the e-book
    two sources of income in one month   the salary and the tutoring

**IT SPANS TWO MONTHS, AND THAT IS NOT PADDING.** Every "vs last month" figure
on the statistics view reads from the previous period, so a one-month fixture
would render that whole column as em-dashes and the feature would look
unfinished rather than un-comparable.

**NOTHING IS DATED IN THE FUTURE.** Days in the current month are clamped to
today, so on the 3rd the set is three days of this month plus a full previous
one. A ledger containing money you have not spent yet is a ledger that
disagrees with your bank, and it would draw the cash-flow chart running past
today.

THE CATEGORY IDS ARE THE PAGE'S SEED VOCABULARY — `food`, `going-out`,
`transport`, `subscriptions`, `books-media`, `health`, `home`, `learning`, from
`features/budget/settings.js`. They are the one part of this set that depends
on something the server does not own, and the limits those ids carry are what
make "over", "exactly on" and "room left" above true. A page whose categories
have been renamed gets an example set of orphans, which `categoryMeta` renders
as grey uncategorised rows rather than failing — the honest degradation.
"""

from datetime import date

# The scale is a constant of the model, not a function of the currency —
# `MONEY_SCALE` in `features/budget/model.js`. See `models/budget.py`.
MONEY_SCALE = 100


def _minor(major):
    """A major-unit number (12.34) as minor units (1234)."""
    return round(major * MONEY_SCALE)


def _sample(index, kind, name, day, major, category=None, notes='', created_at=None):
    return {
        'id': f'sample-{index}',
        'kind': kind,
        'name': name,
        'date': day,
        'amount_minor': _minor(major),
        'category': category,
        'notes': notes,
        'properties': {},
        'is_sample': True,
        'created_at': created_at,
    }


def _month_start(today, months_back=0):
    """The first of the month `months_back` before `today`'s."""
    year, month = today.year, today.month - months_back
    while month < 1:
        month += 12
        year -= 1
    return date(year, month, 1)


def sample_transactions(today=None):
    """
    The example ledger, dated relative to today at request time.

    `created_at` is DERIVED FROM THE DATE rather than left to the insert, and
    that is not decoration: the running balance breaks same-day ties with it,
    so the coffee and the lunch below have to accumulate in the same order on
    every read. Random timestamps would make the pair reorder on every reload,
    which is exactly the kind of fixture that hides a real bug.
    """
    today = today or date.today()
    this_month = _month_start(today)
    last_month = _month_start(today, 1)

    # A day in this month, NEVER LATER THAN TODAY.
    def d(day):
        return this_month.replace(day=min(day, today.day)).isoformat()

    # A day in the previous month — always complete, so it can be used whole.
    def p(day):
        return last_month.replace(day=day).isoformat()

    rows = []

    def tx(kind, name, day, major, category=None, notes=''):
        index = len(rows) + 1
        # Eight in the morning onwards, spread by position, so same-day rows
        # carry distinct and increasing stamps.
        stamp = f'{day}T{8 + (index % 12):02d}:{(index * 7) % 60:02d}:00Z'
        rows.append(_sample(index, kind, name, day, major, category, notes, stamp))

    # ------------------------------------------------------------ last month
    tx('income', 'Salary — last month', p(1), 2400)
    tx('expense', 'Rent', p(1), 950, 'home')
    tx('expense', 'Weekly shop', p(4), 68.40, 'food')
    tx('expense', 'Weekly shop', p(11), 74.15, 'food')
    tx('expense', 'Weekly shop', p(18), 81.20, 'food')
    tx('expense', 'Weekly shop', p(25), 62.05, 'food')
    tx('expense', 'Dinner with Sam', p(9), 44, 'going-out')
    tx('expense', 'Cinema', p(16), 18.50, 'going-out')
    tx('expense', 'Monthly travel pass', p(2), 78, 'transport')
    tx('expense', 'Streaming bundle', p(6), 21.99, 'subscriptions')
    tx('expense', 'Music', p(6), 10.99, 'subscriptions')
    tx('expense', 'Two paperbacks', p(20), 23.50, 'books-media')
    tx('income', 'Sold an old lens', p(22), 140)
    tx('expense', 'Spanish class', p(12), 45, 'learning')

    # ------------------------------------------------------------ this month
    tx('income', 'Salary', d(1), 2400, None, 'Same as last month.')

    # NO LIMIT, and a big lumpy number — the case a limit would be useless for.
    tx('expense', 'Rent', d(1), 950, 'home')

    # FOOD GOES OVER ITS 350 LIMIT and is the page's worked example: four shops
    # and one big top-up, so the meter fills through the month rather than in
    # one jump, and the last row is the one that tips it.
    tx('expense', 'Weekly shop', d(3), 71.30, 'food')
    tx('expense', 'Weekly shop', d(10), 88.90, 'food')
    tx('expense', 'Weekly shop', d(17), 79.45, 'food')
    tx('expense', 'Weekly shop', d(24), 84.60, 'food')
    tx('expense', 'Top-up shop before guests', d(26), 41.75, 'food')

    # TWO ON ONE DAY. The running balance has to break the tie by creation
    # order, and these two are how you see whether it does.
    tx('expense', 'Coffee', d(12), 3.40, 'going-out')
    tx('expense', 'Lunch', d(12), 11.80, 'going-out')

    tx('expense', 'Birthday dinner', d(19), 52, 'going-out', 'Split four ways, my share.')

    # EXACTLY ON THE LIMIT — 90 of 90. The meter must read full without reading
    # as over, which is the one boundary a ratio-driven bar gets wrong.
    tx('expense', 'Monthly travel pass', d(2), 78, 'transport')
    tx('expense', 'Airport train', d(21), 12, 'transport')

    # COMFORTABLY UNDER — 32.98 of 45.
    tx('expense', 'Streaming bundle', d(6), 21.99, 'subscriptions')
    tx('expense', 'Music', d(6), 10.99, 'subscriptions')

    # AN AMOUNT UNDER A POUND, to check the column is not formatted for round
    # numbers only, and a category BELOW last month's — so the "vs last month"
    # column has a fall in it as well as a rise.
    tx('expense', 'E-book', d(14), 0.99, 'books-media')

    # A CATEGORY WITH ROOM LEFT — 45 of 60, so the strip has a meter that is
    # neither full nor nearly empty to read the middle of the scale against.
    tx('expense', 'Spanish class', d(11), 45, 'learning')

    # UNCATEGORISED. It has to appear in the expense total, in the breakdown
    # and in the filter list, or the category totals quietly fail to add up.
    tx(
        'expense',
        'Cash withdrawal',
        d(8),
        40,
        None,
        'Never worked out what this went on.',
    )

    # A SECOND SOURCE OF INCOME. Money coming in is not only the salary, and
    # the ledger has to read properly when two of them land in one month.
    tx('income', 'Tutoring — two sessions', d(15), 60)

    tx('expense', 'Prescription', d(22), 15.60, 'health')
    tx('expense', 'Dentist', d(27), 45, 'health')

    tx('expense', 'New headphones', d(20), 89, None, 'The old ones finally died.')

    return rows
