import { test, expect, type Page, type Locator } from '@playwright/test';
import { BASE_URL, USER, ADMIN, loginAs } from '../helpers';

async function ensureMinesEnabled(page: Page) {
    await loginAs(page, ADMIN.mobile, ADMIN.password);
    await page.goto(`${BASE_URL}/wfbo/mines/settings`, { waitUntil: 'domcontentloaded' });
    const toggle = page.locator('[data-testid="toggle-mg-enabled"]');
    if (!(await toggle.isChecked())) {
        await toggle.check();
        await page.click('[data-testid="btn-save-mg-settings"]');
        await expect(page.locator('[data-testid="flash-success"]')).toBeVisible();
    }
}

// Clicks a tile and waits for the actual /play/mines/reveal response, returning
// its parsed body. Driving assertions off the real API response (rather than
// polling DOM disabled-state, which flips the instant a click is dispatched
// regardless of whether the reveal turns out safe or a bust) is what makes the
// outcome deterministic — DOM polling alone can't distinguish "click landed,
// bust animation about to cascade" from "click landed, definitely safe" during
// the ~500ms window between dispatch and the bust's result overlay appearing.
async function revealTile(page: Page, tile: Locator) {
    const [response] = await Promise.all([
        page.waitForResponse(r => r.url().includes('/play/mines/reveal') && r.request().method() === 'POST'),
        tile.click(),
    ]);
    return response.json();
}

// Reveals the first still-enabled tile from `startIndex` on and resolves the
// round from its outcome — cash out immediately on a safe reveal, or wait for
// the result overlay on a bust / flawless auto cash-out — then returns to the
// bet screen so no active game is left dangling (Mines only allows one active
// game per user at a time).
async function resolveActiveRound(page: Page, startIndex = 0) {
    const cashOutBtn = page.locator('[data-testid="btn-mg-cashout"]');
    const resultTitle = page.locator('[data-testid="mg-result-title"]');

    if (!(await resultTitle.isVisible().catch(() => false))) {
        let tile: Locator | null = null;
        for (let i = startIndex; i < 16; i++) {
            const candidate = page.locator(`[data-testid="mg-tile-${i}"]`);
            if (!(await candidate.isDisabled().catch(() => true))) { tile = candidate; break; }
        }

        if (tile) {
            const body = await revealTile(page, tile);
            if (!body.isMine && body.game?.status !== 'cashed_out') {
                await expect(cashOutBtn).toBeEnabled({ timeout: 10000 });
                await cashOutBtn.click();
            }
        }
    }

    await expect(resultTitle).toBeVisible({ timeout: 10000 });
    await page.click('#playAgainBtn');
    await expect(page.locator('[data-testid="btn-mg-start"]')).toBeVisible();
}

test.describe('Mines — Game Page', () => {

    test.beforeEach(async ({ page }) => {
        await ensureMinesEnabled(page);
        await loginAs(page, USER.mobile, USER.password);
    });

    test('game page loads at /play/mines', async ({ page }) => {
        await page.goto(`${BASE_URL}/play/mines`, { waitUntil: 'domcontentloaded' });
        await expect(page.locator('[data-testid="mg-page"]')).toBeVisible();
    });

    test('wallet bar is visible on the game page', async ({ page }) => {
        await page.goto(`${BASE_URL}/play/mines`, { waitUntil: 'domcontentloaded' });
        await expect(page.locator('[data-testid="wallet-bar"]')).toBeVisible();
    });

    test('4x4 grid renders 16 crate tiles', async ({ page }) => {
        await page.goto(`${BASE_URL}/play/mines`, { waitUntil: 'domcontentloaded' });
        // Grid only exists on the (hidden) game screen until a round starts, but the
        // 16 overlay buttons are always present in the DOM.
        await expect(page.locator('[data-testid="mg-grid"] [data-testid^="mg-tile-"]')).toHaveCount(16);
    });

    test('tier picker and start button are present', async ({ page }) => {
        await page.goto(`${BASE_URL}/play/mines`, { waitUntil: 'domcontentloaded' });
        await expect(page.locator('[data-testid="mg-tier-picker"]')).toBeVisible();
        await expect(page.locator('[data-testid="btn-mg-start"]')).toBeVisible();
        await expect(page.locator('[data-testid="mg-tier-picker"] > [data-testid^="mg-tier-"]')).toHaveCount(4);
    });

    test('unauthenticated access redirects to /login', async ({ page }) => {
        await page.context().clearCookies();
        await page.goto(`${BASE_URL}/play/mines`);
        expect(page.url()).toContain('/login');
    });

});

test.describe('Mines — State API', () => {

    test('GET /play/mines/state returns JSON with wallet balance and tiers', async ({ page }) => {
        await ensureMinesEnabled(page);
        await loginAs(page, USER.mobile, USER.password);

        const response = await page.request.get(`${BASE_URL}/play/mines/state`);
        expect(response.status()).toBe(200);
        const body = await response.json();
        expect(body).toHaveProperty('walletBalance');
        expect(body).toHaveProperty('tiers');
        expect(body.tiers).toHaveProperty('green');
        expect(body.tiers).toHaveProperty('standard');
        expect(body.tiers).toHaveProperty('high_risk');
        expect(body.tiers).toHaveProperty('blackout');
    });

});

test.describe('Mines — History Page', () => {

    test.beforeEach(async ({ page }) => {
        await ensureMinesEnabled(page);
        await loginAs(page, USER.mobile, USER.password);
    });

    test('history page loads at /play/mines/history', async ({ page }) => {
        await page.goto(`${BASE_URL}/play/mines/history`, { waitUntil: 'domcontentloaded' });
        await expect(page).toHaveURL(`${BASE_URL}/play/mines/history`);
    });

    test('history page has back link to /play/mines', async ({ page }) => {
        await page.goto(`${BASE_URL}/play/mines/history`, { waitUntil: 'domcontentloaded' });
        const backLink = page.locator('a[href*="/play/mines"]').first();
        await expect(backLink).toBeVisible();
    });

});

test.describe('Mines — Gameplay', () => {

    test.beforeEach(async ({ page }) => {
        await ensureMinesEnabled(page);
        await loginAs(page, USER.mobile, USER.password);
    });

    // Correctness is asserted via the authoritative /play/mines/state API (the actual
    // game record and its bet amount) rather than diffing the wallet-bar's displayed
    // balance across the test — when run alongside the rest of the front suite, other
    // specs can credit/debit this same shared fixture account mid-test, which would
    // make a wallet-balance snapshot diff flaky through no fault of this feature.
    test('starting a game deducts the wallet, then revealing tiles resolves to a cash-out or a bust', async ({ page }) => {
        await page.goto(`${BASE_URL}/play/mines`, { waitUntil: 'domcontentloaded' });

        const balanceEl = page.locator('[data-testid="wallet-balance"]');
        const balanceBefore = parseFloat((await balanceEl.textContent() ?? '0').replace(/[^\d.]/g, ''));

        if (balanceBefore < 5) {
            test.skip(true, 'Insufficient balance to place bet');
            return;
        }

        // Cheapest bet chip (₱5) on the Green Light tier (lowest risk).
        await page.locator('[data-testid="mg-bet-chip"][data-amount="5"]').click();
        await page.locator('[data-testid="mg-tier-green"]').click();
        await page.click('[data-testid="btn-mg-start"]');
        await expect(page.locator('[data-testid="btn-mg-cashout"]')).toBeVisible();

        const stateAfterStart = await (await page.request.get(`${BASE_URL}/play/mines/state`)).json();
        expect(stateAfterStart.activeGame).not.toBeNull();
        expect(stateAfterStart.activeGame.status).toBe('active');
        expect(parseFloat(stateAfterStart.activeGame.amount)).toBe(5);

        // Reveal tiles (via the invisible overlay hit-targets — the real interaction
        // surface, not the Three.js canvas) until either a safe reveal lets us cash
        // out, or the round busts and the result overlay appears on its own.
        await resolveActiveRound(page);
    });

    // Regression test: the multiplier HUD used to show 1.00x right after starting a
    // round (the "achieved" multiplier for 0 tiles revealed) instead of previewing
    // what the first successful pick would actually pay.
    test('multiplier HUD shows the tier base multiplier immediately after starting, before any reveal', async ({ page }) => {
        await page.goto(`${BASE_URL}/play/mines`, { waitUntil: 'domcontentloaded' });

        const balanceEl = page.locator('[data-testid="wallet-balance"]');
        const balanceBefore = parseFloat((await balanceEl.textContent() ?? '0').replace(/[^\d.]/g, ''));
        if (balanceBefore < 5) {
            test.skip(true, 'Insufficient balance to place bet');
            return;
        }

        const state = await (await page.request.get(`${BASE_URL}/play/mines/state`)).json();
        const expectedBase = parseFloat(state.tiers.green.baseMultiplier).toFixed(2);

        await page.locator('[data-testid="mg-bet-chip"][data-amount="5"]').click();
        await page.locator('[data-testid="mg-tier-green"]').click();
        await page.click('[data-testid="btn-mg-start"]');
        await expect(page.locator('[data-testid="btn-mg-cashout"]')).toBeVisible();

        await expect(page.locator('[data-testid="mg-multiplier"]')).toHaveText(`${expectedBase}x`);

        await resolveActiveRound(page);
    });

    // Regression test: reloading the page mid-round used to leave every crate
    // unclickable because the invisible tile-hit overlay buttons never got
    // positioned (positionTileButtons() silently bailed out on a stale 0-size read).
    test('board tiles remain clickable after reloading the page mid-round', async ({ page }) => {
        await page.goto(`${BASE_URL}/play/mines`, { waitUntil: 'domcontentloaded' });

        const balanceEl = page.locator('[data-testid="wallet-balance"]');
        const balanceBefore = parseFloat((await balanceEl.textContent() ?? '0').replace(/[^\d.]/g, ''));
        if (balanceBefore < 5) {
            test.skip(true, 'Insufficient balance to place bet');
            return;
        }

        await page.locator('[data-testid="mg-bet-chip"][data-amount="5"]').click();
        await page.locator('[data-testid="mg-tier-green"]').click();
        await page.click('[data-testid="btn-mg-start"]');
        await expect(page.locator('[data-testid="btn-mg-cashout"]')).toBeVisible();

        // Reveal one tile so the resumed round has both a revealed and an unrevealed
        // crate to check. If it busts on the first tap, just fall through — the
        // reload/click check below still exercises the resume path either way.
        const cashOutBtn = page.locator('[data-testid="btn-mg-cashout"]');
        const resultTitle = page.locator('[data-testid="mg-result-title"]');
        const firstReveal = await revealTile(page, page.locator('[data-testid="mg-tile-0"]'));

        if (firstReveal.isMine) {
            await expect(resultTitle).toBeVisible({ timeout: 10000 });
            await page.click('#playAgainBtn');
            await expect(page.locator('[data-testid="btn-mg-start"]')).toBeVisible();
            return;
        }
        await expect(cashOutBtn).toBeEnabled();

        // Reload mid-round to exercise resumeActiveGame()'s onResize()/positionTileButtons() path.
        await page.reload({ waitUntil: 'domcontentloaded' });
        await expect(cashOutBtn).toBeVisible();

        const nextTile = page.locator('[data-testid="mg-tile-1"]');
        await expect(nextTile).toBeEnabled();
        // The regression this guards against: before the fix, positionTileButtons()
        // never ran after resume, so this click would silently hit nothing and the
        // /play/mines/reveal request below would simply never fire.
        await revealTile(page, nextTile);

        await resolveActiveRound(page, 2);
    });

});
