import path from 'path';
import { test, expect, Page } from '@playwright/test';
import { BASE_URL, USER, ADMIN, loginAs, uniqueFutureDate, ensureLiveRacingEnabled, closeBetting, registerAndLogin, adjustWalletAsAuditor, placeWizardBet } from '../helpers';

const TEST_BANNER_IMAGE = path.join(__dirname, '..', 'fixtures', 'test-banner.png');

// Every helper here does its own admin setup (arena, event, race, horses, bet
// type, publish) via the WFBO UI, then leaves the browser logged in as ADMIN -
// callers switch to USER themselves once setup is done. This mirrors the setup
// pattern already established in tests/acceptance_test/wfbo/event_race_dividends.spec.ts,
// adapted for the player-facing flow.

async function createArena(page: Page, betTypeKey: string, amount: string): Promise<string> {
    const name = `LR Arena ${Date.now()}-${Math.random().toString(36).slice(2, 6)}`;
    const fieldTestId = 'input-' + betTypeKey.replace(/_/g, '-') + '-amount';

    await page.goto(`${BASE_URL}/wfbo/managements/new`);
    await page.fill('[data-testid="input-name"]', name);
    await page.fill(`[data-testid="${fieldTestId}"]`, amount);
    await page.click('[data-testid="btn-submit"]');
    await expect(page).toHaveURL(`${BASE_URL}/wfbo/managements`);

    return name;
}

async function createEventShowUrl(page: Page, arenaName: string): Promise<string> {
    const dateStr = uniqueFutureDate();
    await page.goto(`${BASE_URL}/wfbo/events/new`);
    await page.selectOption('[data-testid="select-event-management"]', { label: arenaName });
    await page.fill('[data-testid="input-event-date"]', dateStr);
    await page.click('[data-testid="btn-submit"]');
    await expect(page).toHaveURL(`${BASE_URL}/wfbo/events`);

    const row = page.locator('[data-testid^="event-row-"]').filter({ hasText: dateStr });
    const eventHref = await row.locator('[data-testid^="btn-view-"]').getAttribute('href');
    return eventHref ?? '';
}

async function addRace(page: Page, eventUrl: string, raceNumber: number): Promise<string> {
    await page.goto(`${BASE_URL}${eventUrl}`);
    await page.click('[data-testid="btn-new-race"]');
    await page.fill('[data-testid="input-race-number"]', String(raceNumber));
    await page.click('[data-testid="btn-submit"]');

    const row = page.locator('[data-testid^="race-row-"]').filter({ hasText: String(raceNumber) });
    const raceHref = await row.locator('[data-testid^="btn-view-race-"]').getAttribute('href');
    return raceHref ?? '';
}

async function addHorse(page: Page, raceUrl: string, horseNumber: number): Promise<string> {
    await page.goto(`${BASE_URL}${raceUrl}`);
    await page.click('[data-testid="btn-new-horse"]');
    await page.fill('[data-testid="input-horse-number"]', String(horseNumber));
    await page.fill('[data-testid="input-horse-name"]', `Horse ${horseNumber}`);
    await page.fill('[data-testid="input-horse-jockey"]', `Jockey ${horseNumber}`);
    await page.click('[data-testid="btn-submit"]');

    const row = page.locator('[data-testid^="horse-row-"]').last();
    const horseId = (await row.getAttribute('data-testid'))?.replace('horse-row-', '');
    return horseId ?? '';
}

async function offerSingleBetType(page: Page, eventUrl: string, key: string, raceId: string): Promise<void> {
    await page.goto(`${BASE_URL}${eventUrl}?tab=bet-types`);
    await page.check(`[data-testid="checkbox-leg-${key}-${raceId}"]`);
    await page.click('[data-testid="btn-save-bet-types"]');
    await expect(page.locator('[data-testid="flash-success"]')).toBeVisible();
}

async function publishEvent(page: Page, eventUrl: string): Promise<void> {
    await page.goto(`${BASE_URL}${eventUrl}`);
    page.once('dialog', dialog => dialog.accept());
    await page.click('[data-testid="btn-publish-event"]');
}

/**
 * Full setup for a single-race bet type: arena with the given ticket price,
 * a published event with one open race and `horseCount` active horses, the
 * bet type offered on that race, and Live Racing enabled. Leaves the browser
 * logged in as ADMIN.
 */
async function setupSingleRaceBetType(page: Page, betTypeKey: string, amount: string, horseCount = 2): Promise<{ arenaName: string; eventUrl: string; eventId: string; raceId: string; horseIds: string[] }> {
    await loginAs(page, ADMIN.mobile, ADMIN.password);

    const arenaName = await createArena(page, betTypeKey, amount);
    const eventUrl  = await createEventShowUrl(page, arenaName);
    const eventId   = eventUrl.split('/').pop() ?? '';
    const raceUrl   = await addRace(page, eventUrl, Date.now() % 1000);
    const raceId    = raceUrl.split('/').pop() ?? '';

    const horseIds: string[] = [];
    for (let i = 1; i <= horseCount; i++) {
        horseIds.push(await addHorse(page, raceUrl, i));
    }

    await offerSingleBetType(page, eventUrl, betTypeKey, raceId);
    await publishEvent(page, eventUrl);
    await ensureLiveRacingEnabled(page);

    return { arenaName, eventUrl, eventId, raceId, horseIds };
}

// Bet-type-KEY tabs (one per betTypeKey, grouping together any race-pair
// combo instances that share it) are all present in the DOM unconditionally -
// there's no race pre-pick filtering them. Every test in this file uses a
// single-kind type, which always has exactly one combo per key, so this
// resolves to that combo's id directly.
async function betTypeCardId(page: Page, label: string): Promise<string> {
    const tab = page.locator('.wizard-key-tab').filter({ hasText: label });
    const key = await tab.getAttribute('data-key');
    const radio = page.locator(`.wizard-key-panel[data-key="${key}"] .wizard-bet-radio`).first();
    return (await radio.getAttribute('value')) ?? '';
}

// Step 1 shares one #wizard-form with Step 2, so there's a single CSRF token
// for the whole flow rather than one per bet type.
async function wizardToken(page: Page): Promise<string> {
    return page.locator('#wizard-form input[name="_token"]').inputValue();
}

async function wizardPickBet(page: Page, cardId: string): Promise<void> {
    const key = await page.locator(`[data-testid="bet-panel-${cardId}"]`).getAttribute('data-key');
    await page.click(`[data-testid="tab-bet-key-${key}"]`);
}

// Only relevant when the bet type is offered on more than one race - a
// single-leg bet type never renders a race tab bar, so there's nothing to
// click (its one leg is already active by default).
async function wizardPickRaceTab(page: Page, raceId: string, cardId: string): Promise<void> {
    await page.click(`[data-testid="tab-race-${raceId}-bet-${cardId}"]`);
}

function walletAmount(text: string | null): number {
    return parseFloat((text ?? '').replace(/[^0-9.]/g, ''));
}

test.describe('Live Racing', () => {

    test('unauthenticated access redirects to /login', async ({ page }) => {
        await page.goto(`${BASE_URL}/play/live-racing`);
        await expect(page).toHaveURL(new RegExp('/login'));
    });

    test('an enabled ad banner appears below the video and links out', async ({ page }) => {
        const { eventId } = await setupSingleRaceBetType(page, 'win', '50.00');
        // setupSingleRaceBetType() leaves the browser logged in as ADMIN.

        const title = `Ad Banner Test ${Date.now()}`;
        await page.goto(`${BASE_URL}/wfbo/live-racing/ad-banners/new`);
        await page.fill('[data-testid="input-title"]', title);
        await page.setInputFiles('[data-testid="input-image"]', TEST_BANNER_IMAGE);
        await page.fill('[data-testid="input-link-url"]', 'https://example.com/promo');
        await page.click('[data-testid="btn-submit"]');
        await expect(page.locator('[data-testid="flash-success"]')).toBeVisible();

        await loginAs(page, USER.mobile, USER.password);
        await page.goto(`${BASE_URL}/play/live-racing/${eventId}`);

        const banner = page.locator('[data-testid="live-racing-ads-banner"]');
        await expect(banner).toBeVisible();
        await expect(banner.locator('img')).toHaveAttribute('alt', title);

        // The link routes through the click-tracker (not straight to the
        // external URL), so the click can be counted server-side.
        const bannerLink = page.locator('[data-testid="live-racing-ads-banner-link"]');
        const href = await bannerLink.getAttribute('href');
        expect(href).toMatch(/^\/play\/live-racing\/ad-banners\/\d+\/click$/);

        const clickResponse = await page.request.get(`${BASE_URL}${href}`, { maxRedirects: 0 });
        expect(clickResponse.status()).toBe(302);
        expect(clickResponse.headers()['location']).toBe('https://example.com/promo');
    });

    test('the wizard shows bet type tabs, and selecting one reveals its horse picker', async ({ page }) => {
        const { eventId } = await setupSingleRaceBetType(page, 'win', '50.00');

        await loginAs(page, USER.mobile, USER.password);
        await page.goto(`${BASE_URL}/play/live-racing/${eventId}`);

        await expect(page.locator('[data-testid="wallet-bar"]')).toBeVisible();
        await expect(page.locator('[data-testid="wizard-step-1"]')).toBeVisible();

        // Only one bet type is offered, so its tab is the sole (and default-active) one.
        await expect(page.locator('.wizard-key-tab')).toHaveCount(1);
        await expect(page.locator('.wizard-key-tab')).toContainText('Win');

        const cardId = await betTypeCardId(page, 'Win');
        await expect(page.locator(`[data-testid="bet-panel-${cardId}"]`)).toBeVisible();
        await expect(page.locator(`[data-testid="bet-panel-${cardId}"] .horse-row`)).toHaveCount(2);
    });

    test('a single-kind bet type offered on multiple races shows one race tab per race, and switching tabs re-targets the picker', async ({ page }) => {
        await loginAs(page, ADMIN.mobile, ADMIN.password);

        const arenaName = await createArena(page, 'win', '15.00');
        const eventUrl  = await createEventShowUrl(page, arenaName);
        const eventId   = eventUrl.split('/').pop() ?? '';

        const raceNumber1 = Date.now() % 1000;
        const race1Url = await addRace(page, eventUrl, raceNumber1);
        const race1Id  = race1Url.split('/').pop() ?? '';
        const race1Horse1 = await addHorse(page, race1Url, 1);
        await addHorse(page, race1Url, 2);

        const race2Url = await addRace(page, eventUrl, raceNumber1 + 1);
        const race2Id  = race2Url.split('/').pop() ?? '';
        const race2Horse1 = await addHorse(page, race2Url, 1);
        await addHorse(page, race2Url, 2);

        // Offer Win on BOTH races in one save, so its legs = [race1, race2].
        await page.goto(`${BASE_URL}${eventUrl}?tab=bet-types`);
        await page.check(`[data-testid="checkbox-leg-win-${race1Id}"]`);
        await page.check(`[data-testid="checkbox-leg-win-${race2Id}"]`);
        await page.click('[data-testid="btn-save-bet-types"]');
        await expect(page.locator('[data-testid="flash-success"]')).toBeVisible();

        await publishEvent(page, eventUrl);
        await ensureLiveRacingEnabled(page);

        await loginAs(page, USER.mobile, USER.password);
        await page.goto(`${BASE_URL}/play/live-racing/${eventId}`);

        const cardId = await betTypeCardId(page, 'Win');
        await expect(page.locator(`[data-testid="wizard-race-tabs-${cardId}"] .wizard-race-tab`)).toHaveCount(2);

        // Race 1's tab is active by default - its horse checkboxes are enabled,
        // race 2's are not yet.
        await expect(page.locator(`[data-testid="checkbox-pos-1-horse-${race1Horse1}-bet-${cardId}"]`)).toBeEnabled();
        await expect(page.locator(`[data-testid="checkbox-pos-1-horse-${race2Horse1}-bet-${cardId}"]`)).toBeDisabled();

        await wizardPickRaceTab(page, race2Id, cardId);

        await expect(page.locator(`[data-testid="checkbox-pos-1-horse-${race2Horse1}-bet-${cardId}"]`)).toBeEnabled();
        await expect(page.locator(`[data-testid="checkbox-pos-1-horse-${race1Horse1}-bet-${cardId}"]`)).toBeDisabled();
    });

    test('a closed race stays on the wizard as a disabled tab, and the whole place-bets section is hidden once every race is closed', async ({ page }) => {
        await loginAs(page, ADMIN.mobile, ADMIN.password);

        const arenaName = await createArena(page, 'win', '15.00');
        const eventUrl  = await createEventShowUrl(page, arenaName);
        const eventId   = eventUrl.split('/').pop() ?? '';

        const raceNumber1 = Date.now() % 1000;
        const race1Url = await addRace(page, eventUrl, raceNumber1);
        const race1Id  = race1Url.split('/').pop() ?? '';
        await addHorse(page, race1Url, 1);
        await addHorse(page, race1Url, 2);

        const race2Url = await addRace(page, eventUrl, raceNumber1 + 1);
        const race2Id  = race2Url.split('/').pop() ?? '';
        await addHorse(page, race2Url, 1);
        await addHorse(page, race2Url, 2);

        // Offer Win on BOTH races so its legs = [race1, race2].
        await page.goto(`${BASE_URL}${eventUrl}?tab=bet-types`);
        await page.check(`[data-testid="checkbox-leg-win-${race1Id}"]`);
        await page.check(`[data-testid="checkbox-leg-win-${race2Id}"]`);
        await page.click('[data-testid="btn-save-bet-types"]');
        await expect(page.locator('[data-testid="flash-success"]')).toBeVisible();

        await publishEvent(page, eventUrl);
        await ensureLiveRacingEnabled(page);

        // Close race 2 only.
        await closeBetting(page, race2Url);

        await loginAs(page, USER.mobile, USER.password);
        await page.goto(`${BASE_URL}/play/live-racing/${eventId}`);

        const cardId = await betTypeCardId(page, 'Win');
        // Both race tabs are still rendered - the closed one is NOT removed...
        await expect(page.locator(`[data-testid="wizard-race-tabs-${cardId}"] .wizard-race-tab`)).toHaveCount(2);
        // ...but the closed race's tab is disabled, and the open one is usable.
        await expect(page.locator(`[data-testid="tab-race-${race2Id}-bet-${cardId}"]`)).toBeDisabled();
        await expect(page.locator(`[data-testid="tab-race-${race1Id}-bet-${cardId}"]`)).toBeEnabled();
        await expect(page.locator('[data-testid="betting-wizard-card"]')).toBeVisible();
        await expect(page.locator('[data-testid="betting-closed-notice"]')).toBeHidden();

        // Close race 1 too → every race is closed → the place-bets section goes away.
        await loginAs(page, ADMIN.mobile, ADMIN.password);
        await closeBetting(page, race1Url);

        await loginAs(page, USER.mobile, USER.password);
        await page.goto(`${BASE_URL}/play/live-racing/${eventId}`);

        await expect(page.locator('[data-testid="betting-closed-notice"]')).toBeVisible();
        await expect(page.locator('[data-testid="betting-wizard-card"]')).toBeHidden();
    });

    test('a wide boxed bet shows its full stake total on My Bets, not a truncated subtotal', async ({ page }) => {
        test.setTimeout(180_000);

        // Trifecta box on 10 horses across all 3 positions = 10×9×8 = 720
        // combos - more than the 500-row window findRecentTicketsByUser used
        // to slice tickets at, which made My Bets under-report the stake.
        const { arenaName, eventId, raceId, horseIds } = await setupSingleRaceBetType(page, 'trifecta', '2.00', 10);

        const { mobile, password } = await registerAndLogin(page);
        await adjustWalletAsAuditor(page, mobile, '2000.00', 'Fund wide-boxed-bet test');

        await loginAs(page, mobile, password);
        await placeWizardBet(page, {
            eventId,
            raceId,
            cardLabel: 'Trifecta',
            positions: { 1: horseIds, 2: horseIds, 3: horseIds },
            amount: '2',
        });

        // 720 combos × ₱2 per combo = ₱1,440 total wager.
        const resultMessage = page.locator('[data-testid="wizard-result-message"]');
        await expect(resultMessage).toContainText('720 combo(s)');
        await expect(resultMessage).toContainText('₱1440.00');

        // My Bets must show the FULL ₱1,440 total. Before the fix this row
        // summed only the 500 most-recent combo rows → ₱1,000.
        await page.goto(`${BASE_URL}/play/live-racing/history`);
        const row = page.locator('[data-testid^="live-racing-history-row-"]').filter({ hasText: arenaName });
        await expect(row).toHaveCount(1);
        await expect(row).toContainText('Total : ₱1,440.00');
    });

    test('a straight Win bet decreases the wallet balance by the ticket price and appears in history', async ({ page }) => {
        const { eventId, horseIds } = await setupSingleRaceBetType(page, 'win', '50.00');

        await loginAs(page, USER.mobile, USER.password);
        await page.goto(`${BASE_URL}/play/live-racing/${eventId}`);

        const balanceBefore = walletAmount(await page.locator('[data-testid="wallet-bar-amount"]').textContent());

        const cardId = await betTypeCardId(page, 'Win');
        await wizardPickBet(page, cardId);

        await page.check(`[data-testid="checkbox-pos-1-horse-${horseIds[0]}-bet-${cardId}"]`);
        await page.click('[data-testid="btn-wizard-step1-next"]');

        // Step 2 shows the bet summary with the user's selections
        await expect(page.locator('[data-testid="wizard-summary"]')).toBeVisible();
        await expect(page.locator('[data-testid="wizard-summary-race"]')).not.toBeEmpty();
        await expect(page.locator('[data-testid="wizard-summary-bet-type"]')).toHaveText('Win');
        await expect(page.locator('[data-testid="wizard-summary-selections"]')).not.toBeEmpty();
        await expect(page.locator('[data-testid="wizard-summary-combos"]')).toHaveText('1 combo(s)');

        // Step 2 shows a single bet-amount field - typing the total divides
        // it evenly across the combo(s) to get the per-combo stake.
        await expect(page.locator('[data-testid="input-wizard-amount"]')).toBeVisible();
        await page.fill('[data-testid="input-wizard-amount"]', '50.00');
        await expect(page.locator('[data-testid="wizard-summary-amount-per-combo"]')).toHaveText('₱50.00');

        await page.click('[data-testid="btn-wizard-submit"]');

        await expect(page.locator('[data-testid="wizard-step-3"]')).toBeVisible();
        await expect(page.locator('[data-testid="wizard-result-message"]')).toContainText('₱50.00');

        // The wizard doesn't reload on success (step 3 renders in place from
        // the fetch response) - "Place Another Bet" is what triggers the
        // reload that refreshes the wallet bar, so use that to verify the
        // debit landed, then poll rather than assume the reload is instant.
        await page.click('[data-testid="btn-wizard-restart"]');
        await expect.poll(async () => {
            const text = await page.locator('[data-testid="wallet-bar-amount"]').textContent().catch(() => null);
            return walletAmount(text);
        }, { timeout: 15000 }).toBeCloseTo(balanceBefore - 50, 1);

        await page.goto(`${BASE_URL}/play/live-racing/history`);
        await expect(page.locator('[data-testid="live-racing-history-table"]')).toContainText('₱50.00');
    });

    test('a boxed bet decreases the wallet balance by the correct multi-combo total and collapses into one ticket row with the right combo count', async ({ page }) => {
        const { arenaName, eventId, horseIds } = await setupSingleRaceBetType(page, 'win', '20.00');

        await loginAs(page, USER.mobile, USER.password);
        await page.goto(`${BASE_URL}/play/live-racing/${eventId}`);

        const balanceBefore = walletAmount(await page.locator('[data-testid="wallet-bar-amount"]').textContent());

        const cardId = await betTypeCardId(page, 'Win');
        await wizardPickBet(page, cardId);

        // Win has a single position - checking both horses for it boxes two
        // straight combos in one submission.
        await page.check(`[data-testid="checkbox-pos-1-horse-${horseIds[0]}-bet-${cardId}"]`);
        await page.check(`[data-testid="checkbox-pos-1-horse-${horseIds[1]}-bet-${cardId}"]`);
        await page.click('[data-testid="btn-wizard-step1-next"]');

        // Step 2 shows the bet summary reflecting the boxed selections
        await expect(page.locator('[data-testid="wizard-summary"]')).toBeVisible();
        await expect(page.locator('[data-testid="wizard-summary-race"]')).not.toBeEmpty();
        await expect(page.locator('[data-testid="wizard-summary-bet-type"]')).toHaveText('Win');
        await expect(page.locator('[data-testid="wizard-summary-selections"]')).not.toBeEmpty();
        await expect(page.locator('[data-testid="wizard-summary-combos"]')).toHaveText('2 combo(s)');

        // Typing ₱40 total across 2 combos divides evenly into ₱20 per combo.
        await page.fill('[data-testid="input-wizard-amount"]', '40.00');
        await expect(page.locator('[data-testid="wizard-summary-amount-per-combo"]')).toHaveText('₱20.00');

        await page.click('[data-testid="btn-wizard-submit"]');

        await expect(page.locator('[data-testid="wizard-step-3"]')).toBeVisible();
        await expect(page.locator('[data-testid="wizard-result-message"]')).toContainText('2 combo(s)');
        await expect(page.locator('[data-testid="wizard-result-message"]')).toContainText('₱40.00'); // 2 combos x ₱20.00

        await page.click('[data-testid="btn-wizard-restart"]');
        await expect.poll(async () => {
            const text = await page.locator('[data-testid="wallet-bar-amount"]').textContent().catch(() => null);
            return walletAmount(text);
        }, { timeout: 15000 }).toBeCloseTo(balanceBefore - 40, 1);

        // Filter by the arena's unique name (not the ₱20.00 amount, which
        // other test runs against the same shared, un-isolated USER account
        // may also have produced) to count only this test's own row. History
        // groups a boxed bet's combos into one ticket row rather than one
        // row per combo, so the 2 combos placed above collapse into 1 row
        // carrying a ×2 badge and the merged ₱40.00 total.
        await page.goto(`${BASE_URL}/play/live-racing/history`);
        const rows = page.locator(`[data-testid^="live-racing-history-row-"][data-arena="${arenaName}"]`);
        await expect(rows).toHaveCount(1);
        const row = rows.first();
        await expect(row.locator('[data-testid^="combo-count-"]')).toHaveText('×2');
        await expect(row).toContainText('₱40.00');
    });

    test('betting more than the wallet balance is rejected server-side and creates no rows', async ({ page }) => {
        const { eventId, horseIds } = await setupSingleRaceBetType(page, 'win', '999999.00');

        await loginAs(page, USER.mobile, USER.password);
        await page.goto(`${BASE_URL}/play/live-racing/${eventId}`);

        const cardId = await betTypeCardId(page, 'Win');
        const token = await wizardToken(page);
        const response = await page.request.post(`${BASE_URL}/play/live-racing/${eventId}/bet`, {
            form: {
                event_bet_type_id: cardId,
                amount: '999999.00',
                _token: token,
                [`horses[1][]`]: horseIds[0],
            },
        });
        expect(response.status()).toBe(400);
        const body = await response.json();
        expect(body.error).toContain('Insufficient wallet balance.');

        // No bet at this arena's absurd ticket price was created (other
        // tests in this file may have already placed unrelated bets for the
        // same USER, so this checks for the rejected wager specifically
        // rather than asserting the whole history is empty).
        await page.goto(`${BASE_URL}/play/live-racing/history`);
        await expect(page.locator('[data-testid="live-racing-history-table"]')).not.toContainText('₱999999.00');
    });

    test('a zero or negative bet amount is rejected server-side', async ({ page }) => {
        const { eventId, horseIds } = await setupSingleRaceBetType(page, 'win', '10.00');

        await loginAs(page, USER.mobile, USER.password);
        await page.goto(`${BASE_URL}/play/live-racing/${eventId}`);

        const cardId = await betTypeCardId(page, 'Win');
        const token = await wizardToken(page);
        const response = await page.request.post(`${BASE_URL}/play/live-racing/${eventId}/bet`, {
            form: {
                event_bet_type_id: cardId,
                amount: '0',
                _token: token,
                [`horses[1][]`]: horseIds[0],
            },
        });
        expect(response.status()).toBe(400);
        const body = await response.json();
        expect(body.error).toContain('greater than zero');

        // A rejected zero-amount bet never persists, so no row can ever show a
        // ₱0.00 stake - real bets always require amount > 0.
        await page.goto(`${BASE_URL}/play/live-racing/history`);
        await expect(page.locator('[data-testid="live-racing-history-table"]')).not.toContainText('₱0.00');
    });

    test('an amount that does not divide evenly across combos is rounded down to the cent per combo, and only the rounded total is charged', async ({ page }) => {
        const { arenaName, eventId, horseIds } = await setupSingleRaceBetType(page, 'win', '20.00');

        await loginAs(page, USER.mobile, USER.password);
        await page.goto(`${BASE_URL}/play/live-racing/${eventId}`);

        const balanceBefore = walletAmount(await page.locator('[data-testid="wallet-bar-amount"]').textContent());

        const cardId = await betTypeCardId(page, 'Win');
        await wizardPickBet(page, cardId);

        // Two combos again, but ₱41.05 doesn't split evenly - it truncates to
        // ₱20.52 per combo, so only ₱41.04 actually gets charged.
        await page.check(`[data-testid="checkbox-pos-1-horse-${horseIds[0]}-bet-${cardId}"]`);
        await page.check(`[data-testid="checkbox-pos-1-horse-${horseIds[1]}-bet-${cardId}"]`);
        await page.click('[data-testid="btn-wizard-step1-next"]');

        await page.fill('[data-testid="input-wizard-amount"]', '41.05');
        await expect(page.locator('[data-testid="wizard-summary-amount-per-combo"]')).toHaveText('₱20.52');

        await page.click('[data-testid="btn-wizard-submit"]');

        await expect(page.locator('[data-testid="wizard-step-3"]')).toBeVisible();
        await expect(page.locator('[data-testid="wizard-result-message"]')).toContainText('₱41.04');

        await page.click('[data-testid="btn-wizard-restart"]');
        await expect.poll(async () => {
            const text = await page.locator('[data-testid="wallet-bar-amount"]').textContent().catch(() => null);
            return walletAmount(text);
        }, { timeout: 15000 }).toBeCloseTo(balanceBefore - 41.04, 1);

        await page.goto(`${BASE_URL}/play/live-racing/history`);
        const rows = page.locator(`[data-testid^="live-racing-history-row-"][data-arena="${arenaName}"]`);
        await expect(rows.first()).toContainText('₱41.04');
    });

    test('a scratched horse is shown disabled with a "Scratched" label and rejected server-side if forced', async ({ page }) => {
        const { eventId, raceId, horseIds } = await setupSingleRaceBetType(page, 'win', '10.00', 3);

        // Scratch one horse via the admin bulk-scratch action (available once
        // the event is published, which setupSingleRaceBetType already did).
        await page.goto(`${BASE_URL}/wfbo/event-races/${raceId}`);
        await page.check(`[data-testid="checkbox-scratch-${horseIds[0]}"]`);
        await page.click('[data-testid="btn-save-scratches"]');
        await expect(page.locator('[data-testid="flash-success"]')).toBeVisible();

        await loginAs(page, USER.mobile, USER.password);
        await page.goto(`${BASE_URL}/play/live-racing/${eventId}`);

        const cardId = await betTypeCardId(page, 'Win');
        await wizardPickBet(page, cardId);

        // The scratched horse still appears in the picker, tagged "Scratched"...
        const scratchedRow = page.locator(`[data-testid="wizard-horse-scratched-${horseIds[0]}"]`);
        await expect(scratchedRow).toBeVisible();
        await expect(page.locator(`[data-testid="horse-scratched-label-${horseIds[0]}"]`)).toHaveText('Scratched');

        // ...but its position checkbox is disabled and can't be picked, while
        // the two active horses are selectable.
        const scratchedCheckbox = page.locator(`[data-testid="checkbox-pos-1-horse-${horseIds[0]}-bet-${cardId}"]`);
        await expect(scratchedCheckbox).toBeDisabled();
        await expect(page.locator(`[data-testid="checkbox-pos-1-horse-${horseIds[1]}-bet-${cardId}"]`)).toBeEnabled();

        // Force-submit a bet on the scratched horse directly, bypassing the
        // UI, to confirm the server rejects it independently of the client.
        const token = await wizardToken(page);
        const response = await page.request.post(`${BASE_URL}/play/live-racing/${eventId}/bet`, {
            form: {
                event_bet_type_id: cardId,
                amount: '10',
                _token: token,
                [`horses[1][]`]: horseIds[0],
            },
        });
        const body = await response.json();
        expect(body.error).toContain('not active');
    });

});
