import { test, expect, Page } from '@playwright/test';
import {
    BASE_URL,
    USER,
    AGENT,
    ADMIN,
    loginAs,
    ensureLiveRacingEnabled,
    createArena,
    createEventShowUrl,
    addRace,
    addHorses,
    offerSingleBetTypes,
    offerMultiBetTypeCombos,
    publishEvent,
    closeBetting,
    scratchHorseBeforeClose,
    saveResult,
    placeWizardBet,
    readWalletBalance,
    registerUserWithReferralCode,
    adjustWalletAsAdmin,
    setLiveRacingCommissionRate,
    readUserWalletAsAdmin,
} from '../helpers';

// End-to-end coverage of the full Live Racing flow: admin builds a 7-race
// event with every bet type offered, a player bets all 14 types, then the
// settlement engine is exercised across won / lost / void / refund paths,
// before-close and after-close scratches in first / middle / last legs, the
// last-leg substitute-dividend split, dead heats (single and multi-race), and
// agent commission generation on lost bets only.
//
// The database is shared across the whole run, so every test creates its own
// uniquely-named arena/event. Tests run serially; the first two share the
// "main" event built in beforeAll, later tests are self-contained.

test.describe.configure({ mode: 'serial' });

// These flows chain dozens of admin page loads (7-9 races per event, each
// closed and settled through the WFBO UI) plus full wizard bets, so both the
// shared setup hook and individual tests need far more than the global 45s.
const SETUP_TIMEOUT  = 240_000;
const TEST_TIMEOUT   = 300_000;

test.describe('Live Racing — full flow', () => {
    test.setTimeout(TEST_TIMEOUT);

    let eventId: string;
    let eventUrl: string;
    const raceUrls: string[] = [];
    const raceIds: string[] = [];
    // horses[raceIndex][horseNumber-1] = horseId
    let horses: string[][] = [];

    /**
     * Polls the player wallet bar (read from the history page, which always
     * shows it) until it matches `expected`. Tolerance covers formatting.
     */
    async function expectBalance(page: Page, expected: number): Promise<void> {
        await page.goto(`${BASE_URL}/play/live-racing/history`);
        await expect.poll(() => readWalletBalance(page), { timeout: 15000 }).toBeCloseTo(expected, 1);
    }

    /**
     * Resolves an EventBetType id from a dividend row on a race's result form.
     * Matching uses the row's leading label ("Win", "Pick 4 (whole combo)", ...)
     * rather than the whole row text, which also contains field labels.
     */
    async function dividendRowId(page: Page, label: string | RegExp): Promise<string> {
        const rows  = page.locator('[data-testid^="dividend-row-"]');
        const count = await rows.count();
        const hits: string[] = [];
        for (let i = 0; i < count; i++) {
            const row  = rows.nth(i);
            const text = (await row.locator('label.form-hint').first().textContent())
                ?.replace(/\s+/g, ' ')
                .replace(/\s*\(whole combo\)\s*$/i, '')
                .trim() ?? '';
            const hit  = typeof label === 'string' ? text === label : label.test(text);
            if (hit) {
                const testId = await row.getAttribute('data-testid');
                hits.push(testId?.replace('dividend-row-', '') ?? '');
            }
        }
        if (hits.length !== 1) {
            throw new Error(`Expected exactly one dividend row for ${label}, found ${hits.length}`);
        }
        return hits[0];
    }

    /** Builds arena + event + races with horses via the WFBO admin UI. */
    async function buildEvent(
        page: Page,
        prices: Record<string, string>,
        raceCount: number,
        horsesPerRace: number,
    ): Promise<{ eventUrl: string; eventId: string; raceUrls: string[]; raceIds: string[]; horses: string[][] }> {
        const arenaName = await createArena(page, 'LRFlow', prices);
        const created   = await createEventShowUrl(page, arenaName);
        const urls: string[] = [];
        const ids: string[] = [];
        const allHorses: string[][] = [];

        for (let n = 1; n <= raceCount; n++) {
            const race = await addRace(page, created.eventUrl, n);
            urls.push(race.raceUrl);
            ids.push(race.raceId);
            allHorses.push(await addHorses(page, race.raceUrl, horsesPerRace));
        }

        return { eventUrl: created.eventUrl, eventId: created.eventId, raceUrls: urls, raceIds: ids, horses: allHorses };
    }

    test.beforeAll(async ({ browser }) => {
        test.setTimeout(SETUP_TIMEOUT);
        const page = await browser.newPage();
        await loginAs(page, ADMIN.mobile, ADMIN.password);

        const prices: Record<string, string> = {
            win: '10', forecast: '10', trifecta: '10', quartet: '10',
            pentafecta: '10', super_six: '10', odd_even: '5',
            daily_double: '10', daily_double_plus_1: '12', extra_double: '15',
            pick_4: '12', pick_5: '20', pick_6: '25', wta: '50',
        };
        const main = await buildEvent(page, prices, 7, 8);
        eventUrl = main.eventUrl;
        eventId  = main.eventId;
        raceUrls.push(...main.raceUrls);
        raceIds.push(...main.raceIds);
        horses = main.horses;

        // Single-race types, one per race.
        await offerSingleBetTypes(page, eventUrl, [
            { key: 'win', raceId: raceIds[0] },
            { key: 'forecast', raceId: raceIds[1] },
            { key: 'trifecta', raceId: raceIds[2] },
            { key: 'quartet', raceId: raceIds[3] },
            { key: 'pentafecta', raceId: raceIds[4] },
            { key: 'super_six', raceId: raceIds[5] },
            { key: 'odd_even', raceId: raceIds[6] },
        ]);
        // Multi-race combos. Extra Double's pattern skips a race ([0,2]);
        // multi-race cards in the wizard are keyed by their FIRST leg.
        await offerMultiBetTypeCombos(page, eventUrl, 'daily_double', [[raceIds[0], raceIds[1]]]);
        await offerMultiBetTypeCombos(page, eventUrl, 'daily_double_plus_1', [[raceIds[0], raceIds[1], raceIds[2]]]);
        await offerMultiBetTypeCombos(page, eventUrl, 'extra_double', [[raceIds[2], raceIds[4]]]);
        await offerMultiBetTypeCombos(page, eventUrl, 'pick_4', [[raceIds[3], raceIds[4], raceIds[5], raceIds[6]]]);
        await offerMultiBetTypeCombos(page, eventUrl, 'pick_5', [[raceIds[2], raceIds[3], raceIds[4], raceIds[5], raceIds[6]]]);
        await offerMultiBetTypeCombos(page, eventUrl, 'pick_6', [[raceIds[1], raceIds[2], raceIds[3], raceIds[4], raceIds[5], raceIds[6]]]);
        await offerMultiBetTypeCombos(page, eventUrl, 'wta', [raceIds]);

        await publishEvent(page, eventUrl);
        await ensureLiveRacingEnabled(page);
        await page.close();
    });

    test('admin creates arena, 7-race event with 8 horses each, offers all 14 bet types, publishes', async ({ page }) => {
        await loginAs(page, ADMIN.mobile, ADMIN.password);
        await page.goto(`${BASE_URL}${eventUrl}`);
        await expect(page.locator('[data-testid^="race-row-"]')).toHaveCount(7);

        for (let r = 0; r < 7; r++) {
            await page.goto(`${BASE_URL}${raceUrls[r]}`);
            await expect(page.locator('[data-testid^="horse-row-"]')).toHaveCount(8);
        }
    });

    test('player places a bet of every one of the 14 types via the wizard', async ({ page }) => {
        await loginAs(page, USER.mobile, USER.password);
        await page.goto(`${BASE_URL}/play/live-racing/history`);
        const rowsBefore = await page.locator('[data-testid^="live-racing-history-row-"]').count();
        const balanceStart = await readWalletBalance(page);

        const H = horses;
        const R = raceIds;
        // Horse #8 is reserved for WTA so no two bets here share a selection
        // accidentally; each ticket is otherwise independent.
        const bets: Array<Parameters<typeof placeWizardBet>[1]> = [
            // Single-race graded types.
            { eventId, raceId: R[0], cardLabel: /^Win$/, positions: { 1: [H[0][0]] } },
            { eventId, raceId: R[1], cardLabel: /^Forecast$/, positions: { 1: [H[1][0]], 2: [H[1][1]] } },
            { eventId, raceId: R[2], cardLabel: /^Trifecta$/, positions: { 1: [H[2][0]], 2: [H[2][1]], 3: [H[2][2]] } },
            { eventId, raceId: R[3], cardLabel: /^Quartet$/, positions: { 1: [H[3][0]], 2: [H[3][1]], 3: [H[3][2]], 4: [H[3][3]] } },
            { eventId, raceId: R[4], cardLabel: /^Pentafecta$/, positions: { 1: [H[4][0]], 2: [H[4][1]], 3: [H[4][2]], 4: [H[4][3]], 5: [H[4][4]] } },
            { eventId, raceId: R[5], cardLabel: /^Super Six$/, positions: { 1: [H[5][0]], 2: [H[5][1]], 3: [H[5][2]], 4: [H[5][3]], 5: [H[5][4]], 6: [H[5][5]] } },
            { eventId, raceId: R[6], cardLabel: /^Odd Even$/, oddEven: 'odd' },
            // Multi-race combos - step 1 picks the FIRST leg's race.
            { eventId, raceId: R[0], cardLabel: /^Daily Double$/, legs: { [R[0]]: [H[0][0]], [R[1]]: [H[1][0]] } },
            { eventId, raceId: R[0], cardLabel: /^Daily Double Plus 1$/, legs: { [R[0]]: [H[0][1]], [R[1]]: [H[1][1]], [R[2]]: [H[2][1]] } },
            { eventId, raceId: R[2], cardLabel: /^Extra Double$/, legs: { [R[2]]: [H[2][2]], [R[4]]: [H[4][1]] } },
            { eventId, raceId: R[3], cardLabel: /^Pick 4$/, legs: { [R[3]]: [H[3][1]], [R[4]]: [H[4][0]], [R[5]]: [H[5][0]], [R[6]]: [H[6][1]] } },
            { eventId, raceId: R[2], cardLabel: /^Pick 5$/, legs: { [R[2]]: [H[2][3]], [R[3]]: [H[3][3]], [R[4]]: [H[4][2]], [R[5]]: [H[5][1]], [R[6]]: [H[6][2]] } },
            { eventId, raceId: R[1], cardLabel: /^Pick 6$/, legs: { [R[1]]: [H[1][2]], [R[2]]: [H[2][4]], [R[3]]: [H[3][4]], [R[4]]: [H[4][3]], [R[5]]: [H[5][2]], [R[6]]: [H[6][3]] } },
            { eventId, raceId: R[0], cardLabel: /^WTA$/, legs: Object.fromEntries(R.map((r, i) => [r, [H[i][7]]])) },
        ];

        for (const bet of bets) {
            await placeWizardBet(page, bet);
        }

        // 60 singles + 5 odd/even + 10 DD + 12 DD+1 + 15 XD + 12 P4 + 20 P5 + 25 P6 + 50 WTA.
        const expected = balanceStart - (10 * 6 + 5 + 10 + 12 + 15 + 12 + 20 + 25 + 50);
        await expectBalance(page, expected);

        // History groups each wager's combos into one ticket row, so 14
        // straight (non-boxed) bets here means exactly 14 new rows.
        await page.goto(`${BASE_URL}/play/live-racing/history`);
        const rowsAfter = await page.locator('[data-testid^="live-racing-history-row-"]').count();
        expect(rowsAfter).toBe(rowsBefore + 14);
    });

    test('settlement grades WON, LOST, VOID-by-scratch and manual-refund bets correctly', async ({ page }) => {
        await loginAs(page, ADMIN.mobile, ADMIN.password);
        const ev = await buildEvent(page, { win: '10', forecast: '10', trifecta: '10' }, 2, 8);
        const h1 = ev.horses[0][0];
        const h2 = ev.horses[0][1];
        const h3 = ev.horses[0][2];
        const h8 = ev.horses[0][7];
        await offerSingleBetTypes(page, ev.eventUrl, [
            { key: 'win', raceId: ev.raceIds[0] },
            { key: 'forecast', raceId: ev.raceIds[0] },
            { key: 'trifecta', raceId: ev.raceIds[0] },
            { key: 'win', raceId: ev.raceIds[1] },
        ]);
        await publishEvent(page, ev.eventUrl);

        await loginAs(page, USER.mobile, USER.password);
        let expected = await readWalletBalance(page);

        // Win -> will win. Forecast -> wrong order, LOST. Trifecta includes
        // h8 which gets scratched before close -> VOID refund. The second
        // race's Win -> manually refunded at settlement.
        await placeWizardBet(page, { eventId: ev.eventId, raceId: ev.raceIds[0], cardLabel: /^Win$/, positions: { 1: [h2] } });
        await placeWizardBet(page, { eventId: ev.eventId, raceId: ev.raceIds[0], cardLabel: /^Forecast$/, positions: { 1: [h3], 2: [ev.horses[0][3]] } });
        await placeWizardBet(page, { eventId: ev.eventId, raceId: ev.raceIds[0], cardLabel: /^Trifecta$/, positions: { 1: [h1], 2: [h2], 3: [h8] } });
        await placeWizardBet(page, { eventId: ev.eventId, raceId: ev.raceIds[1], cardLabel: /^Win$/, positions: { 1: [ev.horses[1][0]] } });
        expected -= 40;

        await loginAs(page, ADMIN.mobile, ADMIN.password);
        await scratchHorseBeforeClose(page, ev.raceUrls[0], h8);
        await closeBetting(page, ev.raceUrls[0]);
        await closeBetting(page, ev.raceUrls[1]);

        await page.goto(`${BASE_URL}${ev.raceUrls[0]}`);
        const winId       = await dividendRowId(page, /^Win$/);
        const forecastId  = await dividendRowId(page, /^Forecast$/);
        const trifectaId  = await dividendRowId(page, /^Trifecta$/);
        await page.goto(`${BASE_URL}${ev.raceUrls[1]}`);
        const refundWinId = await dividendRowId(page, /^Win$/);

        await saveResult(page, ev.raceUrls[0], {
            finishers: [{ position: 1, horseId: h2 }, { position: 2, horseId: h1 }],
            // Every bet type offered on this race needs a dividend entry to
            // save, even ones whose outcome here (LOST/VOID) doesn't depend
            // on the value entered.
            dividends: { [winId]: '20.00', [forecastId]: '15.00', [trifectaId]: '30.00' },
        });

        // -40 stakes +20 win payout +10 trifecta scratch refund +10 manual
        // refund on race 2's Win = net zero.
        await saveResult(page, ev.raceUrls[1], {
            finishers: [{ position: 1, horseId: ev.horses[1][1] }],
            refunds: [refundWinId],
        });
        expected += 40;
        await loginAs(page, USER.mobile, USER.password);
        await expectBalance(page, expected);
    });

    test('last-leg after-close scratch with substitute pays TWO distinct Pick 4 dividends', async ({ page }) => {
        await loginAs(page, ADMIN.mobile, ADMIN.password);
        const ev = await buildEvent(page, { pick_4: '10' }, 4, 4);
        await offerMultiBetTypeCombos(page, ev.eventUrl, 'pick_4', [ev.raceIds]);
        await publishEvent(page, ev.eventUrl);

        // Both tickets share winning picks on legs 1-3. On the last leg, a
        // ticket only grades WON if its (post-substitution) pick matches the
        // race's actual recorded winner - there's no separate "would have
        // won the natural way" allowance. So both tickets here target the
        // SAME eventual winner (sub): one picks it directly, the other picks
        // the horse that gets scratched after close and rewritten onto sub.
        const w1  = ev.horses[0][0];
        const w2  = ev.horses[1][0];
        const w3  = ev.horses[2][0];
        const sub = ev.horses[3][0]; // wins leg 4 outright; also scr's substitute
        const scr = ev.horses[3][1]; // scratched after close, rewritten onto sub

        await loginAs(page, USER.mobile, USER.password);
        let expected = await readWalletBalance(page);
        const sharedLegs = { [ev.raceIds[0]]: [w1], [ev.raceIds[1]]: [w2], [ev.raceIds[2]]: [w3] };
        await placeWizardBet(page, { eventId: ev.eventId, raceId: ev.raceIds[0], cardLabel: /^Pick 4$/, legs: { ...sharedLegs, [ev.raceIds[3]]: [sub] } });
        await placeWizardBet(page, { eventId: ev.eventId, raceId: ev.raceIds[0], cardLabel: /^Pick 4$/, legs: { ...sharedLegs, [ev.raceIds[3]]: [scr] } });
        expected -= 20;

        await loginAs(page, ADMIN.mobile, ADMIN.password);
        for (let r = 0; r < 3; r++) {
            await closeBetting(page, ev.raceUrls[r]);
            await saveResult(page, ev.raceUrls[r], {
                finishers: [{ position: 1, horseId: [w1, w2, w3][r] }],
            });
        }

        await closeBetting(page, ev.raceUrls[3]);

        // Saving without the now-required substitute dividend must flash a
        // validation error (the substitute field only appears because an
        // after-close scratch with a substitute is present).
        await page.goto(`${BASE_URL}${ev.raceUrls[3]}`);
        await page.check(`[data-testid="checkbox-result-scratch-${scr}"]`);
        await page.selectOption(`[data-testid="select-substitute-${scr}"]`, sub);
        await page.selectOption('[data-testid="select-finisher-1"]', sub);
        const p4Id = await dividendRowId(page, /^Pick 4$/);
        await page.fill(`[data-testid="input-dividend-${p4Id}"]`, '40.00');
        page.once('dialog', dialog => dialog.accept());
        await page.click('[data-testid="btn-save-result"]');
        await expect(page.locator('[data-testid="flash-error"]')).toBeVisible();

        // Now supply both dividends and settle.
        await saveResult(page, ev.raceUrls[3], {
            scratchesAfterClose: [{ horseId: scr, substituteHorseId: sub }],
            finishers: [{ position: 1, horseId: sub }],
            dividends: { [p4Id]: '40.00' },
            substituteDividends: { [p4Id]: '55.00' },
        });

        // Dividend is a flat per-ticket payout: natural combo pays the plain
        // dividend, 40; scratched combo is rewritten to the substitute and
        // pays the substitute dividend, 55. Any other grading combination
        // produces a different total, so this single assertion pins both
        // payouts.
        expected += 95;
        await loginAs(page, USER.mobile, USER.password);
        await expectBalance(page, expected);
    });

    test('after-close scratch matrix across leg positions and substitute availability', async ({ page }) => {
        await loginAs(page, ADMIN.mobile, ADMIN.password);
        const prices = { win: '10', daily_double: '10', daily_double_plus_1: '12', pick_4: '12' };
        const ev = await buildEvent(page, prices, 9, 4);
        await offerSingleBetTypes(page, ev.eventUrl, [{ key: 'win', raceId: ev.raceIds[0] }]);
        await offerMultiBetTypeCombos(page, ev.eventUrl, 'daily_double', [[ev.raceIds[1], ev.raceIds[2]]]);
        // Two Daily Double Plus 1 combos on disjoint race ranges keep their
        // result-form rows unambiguous.
        await offerMultiBetTypeCombos(page, ev.eventUrl, 'daily_double_plus_1', [
            [ev.raceIds[3], ev.raceIds[4], ev.raceIds[5]],
            [ev.raceIds[6], ev.raceIds[7], ev.raceIds[8]],
        ]);
        await offerMultiBetTypeCombos(page, ev.eventUrl, 'pick_4', [[ev.raceIds[0], ev.raceIds[1], ev.raceIds[2], ev.raceIds[3]]]);
        await publishEvent(page, ev.eventUrl);

        const x = (raceIdx: number, num: number) => ev.horses[raceIdx][num - 1];
        // Leg winners the surviving tickets rely on.
        const winX1 = x(0, 1), winX3 = x(2, 3), winX4 = x(3, 4);
        // Scratch subjects.
        const p4MidScr    = x(1, 3); // Pick 4 middle leg (x2) - WITH substitute
        const p4MidSub    = x(1, 4); // ...and the substitute that wins x2
        const ddLastScr   = x(2, 1); // Daily Double last leg (x3) - WITH substitute
        const ddLastSub   = x(2, 2);
        const dd1FirstScr = x(3, 1); // DD1 combo A first leg (x4) - WITH substitute
        const dd1FirstSub = x(3, 2);
        const dd1MidNoSub = x(7, 3); // DD1 combo B middle leg (x8) - NO substitute

        await loginAs(page, USER.mobile, USER.password);
        let expected = await readWalletBalance(page);
        await placeWizardBet(page, { eventId: ev.eventId, raceId: ev.raceIds[0], cardLabel: /^Win$/, positions: { 1: [winX1] } });
        await placeWizardBet(page, {
            eventId: ev.eventId, raceId: ev.raceIds[0], cardLabel: /^Pick 4$/,
            legs: { [ev.raceIds[0]]: [winX1], [ev.raceIds[1]]: [p4MidScr], [ev.raceIds[2]]: [winX3], [ev.raceIds[3]]: [winX4] },
        });
        await placeWizardBet(page, {
            eventId: ev.eventId, raceId: ev.raceIds[1], cardLabel: /^Daily Double$/,
            legs: { [ev.raceIds[1]]: [p4MidSub], [ev.raceIds[2]]: [ddLastScr] },
        });
        await placeWizardBet(page, {
            eventId: ev.eventId, raceId: ev.raceIds[3], cardLabel: /^Daily Double Plus 1$/,
            legs: { [ev.raceIds[3]]: [dd1FirstScr], [ev.raceIds[4]]: [x(4, 1)], [ev.raceIds[5]]: [x(5, 1)] },
        });
        await placeWizardBet(page, {
            eventId: ev.eventId, raceId: ev.raceIds[6], cardLabel: /^Daily Double Plus 1$/,
            legs: { [ev.raceIds[6]]: [x(6, 1)], [ev.raceIds[7]]: [dd1MidNoSub], [ev.raceIds[8]]: [x(8, 1)] },
        });
        expected -= 56;

        await loginAs(page, ADMIN.mobile, ADMIN.password);
        // x1: control WIN settles immediately (+20, a flat per-ticket dividend).
        await closeBetting(page, ev.raceUrls[0]);
        const winId = await dividendRowId(page, /^Win$/);
        await saveResult(page, ev.raceUrls[0], {
            finishers: [{ position: 1, horseId: winX1 }],
            dividends: { [winId]: '20.00' },
        });
        expected += 20;

        // x2: Pick 4 middle-leg scratch WITH substitute -> rewritten to the
        // substitute, which must then WIN x2 to keep the ticket alive.
        await closeBetting(page, ev.raceUrls[1]);
        await saveResult(page, ev.raceUrls[1], {
            scratchesAfterClose: [{ horseId: p4MidScr, substituteHorseId: p4MidSub }],
            finishers: [{ position: 1, horseId: p4MidSub }],
        });

        // x3: Daily Double last-leg scratch WITH substitute -> plain DD never
        // rewrites -> VOID refund (+10).
        await closeBetting(page, ev.raceUrls[2]);
        await page.goto(`${BASE_URL}${ev.raceUrls[2]}`);
        const ddId = await dividendRowId(page, /^Daily Double$/);
        await saveResult(page, ev.raceUrls[2], {
            scratchesAfterClose: [{ horseId: ddLastScr, substituteHorseId: ddLastSub }],
            finishers: [{ position: 1, horseId: winX3 }, { position: 2, horseId: ddLastSub }],
            // Daily Double is offered here (this is its last leg) so a
            // dividend is required to save, even though the only ticket on
            // it voids via the scratch regardless of the value entered.
            dividends: { [ddId]: '20.00' },
        });
        expected += 10;

        // x4: DD1 combo A first-leg scratch WITH substitute -> first leg is
        // not rewritable -> VOID refund (+12). Also Pick 4's final leg:
        // winner + dividend 36 -> WON (+36).
        await closeBetting(page, ev.raceUrls[3]);
        const p4Id = await dividendRowId(page, /^Pick 4$/);
        await saveResult(page, ev.raceUrls[3], {
            scratchesAfterClose: [{ horseId: dd1FirstScr, substituteHorseId: dd1FirstSub }],
            finishers: [{ position: 1, horseId: winX4 }],
            dividends: { [p4Id]: '36.00' },
            // Pick 4 is offered here (its last leg) and can rewrite onto a
            // substitute on ANY leg including this one, so a substitute
            // dividend is required whenever this race has a scratch-with-
            // substitute - even though no Pick 4 ticket here actually picked
            // dd1FirstScr on this leg.
            substituteDividends: { [p4Id]: '48.00' },
        });
        expected += 12 + 36;

        // x5-x7: routine intermediate settles keeping combo B formally alive.
        // Race 5 is DD1 combo A's last leg, so it still needs a dividend to
        // save even though combo A's own ticket already voided at race 3.
        for (const r of [4, 5, 6]) {
            await closeBetting(page, ev.raceUrls[r]);
            if (r === 5) {
                await page.goto(`${BASE_URL}${ev.raceUrls[r]}`);
                const dd1AId = await dividendRowId(page, /^Daily Double Plus 1$/);
                await saveResult(page, ev.raceUrls[r], {
                    finishers: [{ position: 1, horseId: x(r, 1) }],
                    dividends: { [dd1AId]: '24.00' },
                });
            } else {
                await saveResult(page, ev.raceUrls[r], { finishers: [{ position: 1, horseId: x(r, 1) }] });
            }
        }

        // x8: DD1 combo B middle-leg scratch WITHOUT substitute -> VOID (+12).
        await closeBetting(page, ev.raceUrls[7]);
        await saveResult(page, ev.raceUrls[7], {
            scratchesAfterClose: [{ horseId: dd1MidNoSub }],
            finishers: [{ position: 1, horseId: x(7, 1) }],
        });
        expected += 12;

        // x9: final settle of the already-voided combo B ticket. Race 8 is
        // DD1 combo B's last leg, so a dividend is still required to save.
        await closeBetting(page, ev.raceUrls[8]);
        await page.goto(`${BASE_URL}${ev.raceUrls[8]}`);
        const dd1BId = await dividendRowId(page, /^Daily Double Plus 1$/);
        await saveResult(page, ev.raceUrls[8], {
            finishers: [{ position: 1, horseId: x(8, 1) }],
            dividends: { [dd1BId]: '24.00' },
        });

        // -56 stakes +20 win +10 DD refund +36 pick4 +12 DD1A refund +12 DD1B refund = +34.
        await loginAs(page, USER.mobile, USER.password);
        await expectBalance(page, expected);
    });

    test('before-close scratches void Pick 4 tickets in first, middle and last legs; survivor wins', async ({ page }) => {
        await loginAs(page, ADMIN.mobile, ADMIN.password);
        const ev = await buildEvent(page, { pick_4: '12' }, 4, 4);
        await offerMultiBetTypeCombos(page, ev.eventUrl, 'pick_4', [ev.raceIds]);
        await publishEvent(page, ev.eventUrl);

        // Winners (#2 of each race) vs scratch subjects (#1 of y1/y2/y4).
        const w1 = ev.horses[0][1], w2 = ev.horses[1][1], w3 = ev.horses[2][1], w4 = ev.horses[3][1];
        const c1s = ev.horses[0][0], c2s = ev.horses[1][0], c4s = ev.horses[3][0];

        await loginAs(page, USER.mobile, USER.password);
        let expected = await readWalletBalance(page);
        const legs = (l1: string, l2: string, l3: string, l4: string) => ({
            [ev.raceIds[0]]: [l1], [ev.raceIds[1]]: [l2], [ev.raceIds[2]]: [l3], [ev.raceIds[3]]: [l4],
        });
        await placeWizardBet(page, { eventId: ev.eventId, raceId: ev.raceIds[0], cardLabel: /^Pick 4$/, legs: legs(c1s, w2, w3, w4) }); // first leg
        await placeWizardBet(page, { eventId: ev.eventId, raceId: ev.raceIds[0], cardLabel: /^Pick 4$/, legs: legs(w1, c2s, w3, w4) }); // middle leg
        await placeWizardBet(page, { eventId: ev.eventId, raceId: ev.raceIds[0], cardLabel: /^Pick 4$/, legs: legs(w1, w2, w3, c4s) }); // last leg
        await placeWizardBet(page, { eventId: ev.eventId, raceId: ev.raceIds[0], cardLabel: /^Pick 4$/, legs: legs(w1, w2, w3, w4) }); // control
        expected -= 48;

        await loginAs(page, ADMIN.mobile, ADMIN.password);
        // Scratch all three scenario horses while betting is still open.
        await scratchHorseBeforeClose(page, ev.raceUrls[0], c1s);
        await scratchHorseBeforeClose(page, ev.raceUrls[1], c2s);
        await scratchHorseBeforeClose(page, ev.raceUrls[3], c4s);

        for (let r = 0; r < 4; r++) {
            await closeBetting(page, ev.raceUrls[r]);
        }
        await page.goto(`${BASE_URL}${ev.raceUrls[3]}`);
        const p4Id = await dividendRowId(page, /^Pick 4$/);
        await saveResult(page, ev.raceUrls[0], { finishers: [{ position: 1, horseId: w1 }] });
        await saveResult(page, ev.raceUrls[1], { finishers: [{ position: 1, horseId: w2 }] });
        await saveResult(page, ev.raceUrls[2], { finishers: [{ position: 1, horseId: w3 }] });
        await saveResult(page, ev.raceUrls[3], {
            finishers: [{ position: 1, horseId: w4 }],
            dividends: { [p4Id]: '36.00' },   // control pays a flat per-ticket dividend of 36
        });

        // -48 stakes +36 control payout +36 void refunds = +24.
        expected += 72;
        await loginAs(page, USER.mobile, USER.password);
        await expectBalance(page, expected);
    });

    test('single-race dead heat splits the Win dividend; tie outside pick count loses plainly', async ({ page }) => {
        await loginAs(page, ADMIN.mobile, ADMIN.password);
        const ev = await buildEvent(page, { win: '10' }, 1, 6);
        await offerSingleBetTypes(page, ev.eventUrl, [{ key: 'win', raceId: ev.raceIds[0] }]);
        await publishEvent(page, ev.eventUrl);

        const k1 = ev.horses[0][0];
        const k2 = ev.horses[0][1];
        const k5 = ev.horses[0][4];

        await loginAs(page, USER.mobile, USER.password);
        let expected = await readWalletBalance(page);
        await placeWizardBet(page, { eventId: ev.eventId, raceId: ev.raceIds[0], cardLabel: /^Win$/, positions: { 1: [k1] } });
        await placeWizardBet(page, { eventId: ev.eventId, raceId: ev.raceIds[0], cardLabel: /^Win$/, positions: { 1: [k2] } });
        await placeWizardBet(page, { eventId: ev.eventId, raceId: ev.raceIds[0], cardLabel: /^Win$/, positions: { 1: [k5] } });
        expected -= 30;

        await loginAs(page, ADMIN.mobile, ADMIN.password);
        await closeBetting(page, ev.raceUrls[0]);
        const winId = await dividendRowId(page, /^Win$/);
        await saveResult(page, ev.raceUrls[0], {
            finishers: [
                { position: 1, horseId: k1, deadHeatWithNext: true },
                { position: 2, horseId: k2 },
                { position: 3, horseId: k5, deadHeatWithNext: true },   // tie OUTSIDE pick count
                { position: 4, horseId: ev.horses[0][5] },
            ],
            deadHeatDividends: { [winId]: { a: '15.00', b: '15.00' } },
        });

        // Both tied winners get their own flat 15 dead-heat dividend; the
        // horse tied outside the pick count neither wins nor qualifies for
        // any split - plain LOST.
        expected += 30;
        await loginAs(page, USER.mobile, USER.password);
        await expectBalance(page, expected);
    });

    test('multi-race dead heat in the last leg splits Daily Double dividends A/B', async ({ page }) => {
        await loginAs(page, ADMIN.mobile, ADMIN.password);
        const ev = await buildEvent(page, { daily_double: '10' }, 2, 4);
        await offerMultiBetTypeCombos(page, ev.eventUrl, 'daily_double', [ev.raceIds]);
        await publishEvent(page, ev.eventUrl);

        const m1w = ev.horses[0][0]; // wins u1 for both tickets
        const ma  = ev.horses[1][0]; // ties for 1st in u2
        const mb  = ev.horses[1][1];

        await loginAs(page, USER.mobile, USER.password);
        let expected = await readWalletBalance(page);
        await placeWizardBet(page, { eventId: ev.eventId, raceId: ev.raceIds[0], cardLabel: /^Daily Double$/, legs: { [ev.raceIds[0]]: [m1w], [ev.raceIds[1]]: [ma] } });
        await placeWizardBet(page, { eventId: ev.eventId, raceId: ev.raceIds[0], cardLabel: /^Daily Double$/, legs: { [ev.raceIds[0]]: [m1w], [ev.raceIds[1]]: [mb] } });
        expected -= 20;

        await loginAs(page, ADMIN.mobile, ADMIN.password);
        await closeBetting(page, ev.raceUrls[0]);
        await saveResult(page, ev.raceUrls[0], { finishers: [{ position: 1, horseId: m1w }] });
        await closeBetting(page, ev.raceUrls[1]);

        const ddId = await dividendRowId(page, /^Daily Double$/);
        await saveResult(page, ev.raceUrls[1], {
            finishers: [
                { position: 1, horseId: ma, deadHeatWithNext: true },
                { position: 2, horseId: mb },
            ],
            deadHeatDividends: { [ddId]: { a: '20.00', b: '30.00' } },
        });

        // Ticket A gets the flat 20 dividend; ticket B gets the flat 30 one.
        expected += 50;
        await loginAs(page, USER.mobile, USER.password);
        await expectBalance(page, expected);
    });

    test('agent commission is generated on lost bets only, at the site rate', async ({ page }) => {
        await setLiveRacingCommissionRate(page, '10');

        // Referred user with no master agent: referrer AGENT earns the full rate.
        const ru = await registerUserWithReferralCode(page, 'ABCDEF01');
        await adjustWalletAsAdmin(page, ru.mobile, '5000.00', 'Live racing commission funding');

        await loginAs(page, ADMIN.mobile, ADMIN.password);
        const ev = await buildEvent(page, { win: '100' }, 1, 4);
        await offerSingleBetTypes(page, ev.eventUrl, [{ key: 'win', raceId: ev.raceIds[0] }]);
        await publishEvent(page, ev.eventUrl);

        const winner   = ev.horses[0][0];
        const loser    = ev.horses[0][1];
        const voided   = ev.horses[0][2];

        const agentBefore = await readUserWalletAsAdmin(page, AGENT.mobile);

        await loginAs(page, ru.mobile, ru.password);
        let expected = await readWalletBalance(page);
        await placeWizardBet(page, { eventId: ev.eventId, raceId: ev.raceIds[0], cardLabel: /^Win$/, positions: { 1: [winner] } });
        await placeWizardBet(page, { eventId: ev.eventId, raceId: ev.raceIds[0], cardLabel: /^Win$/, positions: { 1: [loser] } });
        await placeWizardBet(page, { eventId: ev.eventId, raceId: ev.raceIds[0], cardLabel: /^Win$/, positions: { 1: [voided] } });
        expected -= 300;

        await loginAs(page, ADMIN.mobile, ADMIN.password);
        await scratchHorseBeforeClose(page, ev.raceUrls[0], voided);
        await closeBetting(page, ev.raceUrls[0]);
        const winId = await dividendRowId(page, /^Win$/);
        await saveResult(page, ev.raceUrls[0], {
            finishers: [{ position: 1, horseId: winner }],
            dividends: { [winId]: '200.00' },
        });

        // Winning bet (+200) and voided bet (refund +100) generate nothing;
        // only the lost bet does: 10% x 100 stake = 10 to AGENT (no master).
        expected += 300;
        const agentDelta = (await readUserWalletAsAdmin(page, AGENT.mobile)) - agentBefore;
        expect(agentDelta).toBeCloseTo(10, 2);

        await loginAs(page, ru.mobile, ru.password);
        await expectBalance(page, expected);
    });

    test('commission splits between sub-agent and master agent on a lost bet', async ({ page }) => {
        await setLiveRacingCommissionRate(page, '10');

        // Register P1 under AGENT's code, then promote them to a sub-agent at 30%.
        const p1 = await registerUserWithReferralCode(page, 'ABCDEF01');
        await loginAs(page, AGENT.mobile, AGENT.password);
        await page.goto(`${BASE_URL}/agent/users?search=${encodeURIComponent(p1.mobile)}`);
        const promoteBtn = page.locator('[data-testid^="btn-promote-agent-"]').first();
        await promoteBtn.click();
        await page.fill('[data-testid="input-promote-commission-rate"]', '30');
        await page.click('[data-testid="btn-confirm-promote"]');
        await expect(page.locator('[data-testid="flash-success"]')).toBeVisible();

        // Loading the agent dashboard generates P1's own referral code.
        await loginAs(page, p1.mobile, p1.password);
        await page.goto(`${BASE_URL}/agent/dashboard`);
        const p1Code = (await page.locator('[data-testid="referral-code-display"]').inputValue()).trim();
        expect(p1Code).not.toBe('');

        const p2 = await registerUserWithReferralCode(page, p1Code);
        await adjustWalletAsAdmin(page, p2.mobile, '5000.00', 'Split test funding');

        await loginAs(page, ADMIN.mobile, ADMIN.password);
        const ev = await buildEvent(page, { win: '100' }, 1, 4);
        await offerSingleBetTypes(page, ev.eventUrl, [{ key: 'win', raceId: ev.raceIds[0] }]);
        await publishEvent(page, ev.eventUrl);

        const winner = ev.horses[0][0];
        const loser  = ev.horses[0][1];

        const p1Before    = await readUserWalletAsAdmin(page, p1.mobile);
        const agentBefore = await readUserWalletAsAdmin(page, AGENT.mobile);

        await loginAs(page, p2.mobile, p2.password);
        await placeWizardBet(page, { eventId: ev.eventId, raceId: ev.raceIds[0], cardLabel: /^Win$/, positions: { 1: [loser] } });

        await loginAs(page, ADMIN.mobile, ADMIN.password);
        await closeBetting(page, ev.raceUrls[0]);
        const winId = await dividendRowId(page, /^Win$/);
        await saveResult(page, ev.raceUrls[0], {
            finishers: [{ position: 1, horseId: winner }],
            dividends: { [winId]: '2.00' },
        });

        // Lost bet of 100 generates commission of 10: 30% to sub-agent P1,
        // 70% to master AGENT.
        const p1Delta    = (await readUserWalletAsAdmin(page, p1.mobile)) - p1Before;
        const agentDelta = (await readUserWalletAsAdmin(page, AGENT.mobile)) - agentBefore;
        expect(p1Delta).toBeCloseTo(3, 2);
        expect(agentDelta).toBeCloseTo(7, 2);
    });
});
