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,
    adjustWalletAsAuditor,
    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,
// scratches, 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 combination-entry block on a race's
     * result form. Matching uses the block's leading label ("Win", "Forecast",
     * "Daily Double (2-…)", ...) rather than the whole block text.
     */
    async function dividendRowId(page: Page, label: string | RegExp): Promise<string> {
        const blocks = page.locator('[data-testid^="combo-bettype-"]');
        const count  = await blocks.count();
        const hits: string[] = [];
        for (let i = 0; i < count; i++) {
            const block = blocks.nth(i);
            const text  = (await block.locator('label.fw-semibold').first().textContent())
                ?.replace(/\s+/g, ' ')
                .replace(/\s*\([\d-]+…\)\s*$/u, '')
                .trim() ?? '';
            const hit   = typeof label === 'string' ? text === label : label.test(text);
            if (hit) {
                const testId = await block.getAttribute('data-testid');
                hits.push(testId?.replace('combo-bettype-', '') ?? '');
            }
        }
        if (hits.length !== 1) {
            throw new Error(`Expected exactly one combination block 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], {
            // Every bet type offered on this race needs at least one winning
            // combination to save. h2's Win bet matches; the Forecast bet picked
            // the wrong order so it LOSES; the Trifecta bet is VOID by scratch
            // regardless of the combination entered.
            combinations: {
                [winId]:      [{ horseIds: [h2], dividend: '20.00' }],
                [forecastId]: [{ horseIds: [h2, h1], dividend: '15.00' }],
                [trifectaId]: [{ horseIds: [h1, h2, h3], dividend: '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], {
            refunds: [refundWinId],
        });
        expected += 40;
        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 adjustWalletAsAuditor(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], {
            combinations: { [winId]: [{ horseIds: [winner], dividend: '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 adjustWalletAsAuditor(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], {
            combinations: { [winId]: [{ horseIds: [winner], dividend: '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);
    });
});
