<?php

use Carbon\Carbon;
use App\Models\Draw;
use App\Models\Player;
use App\Models\AdminRole;
use App\Models\AdminUser;
use App\Models\RiskPool;
use App\Models\TicketItem;
use App\Models\TicketOrder;
use App\Models\PlayerWallet;
use App\Models\WalletTxn;
use App\Models\TicketCombination;
use App\Lottery\DrawStatus;
use App\Models\DrawResultItem;
use App\Models\DrawResultBatch;
use App\Models\SettlementBatch;
use Illuminate\Support\Facades\Hash;
use Illuminate\Support\Facades\Event;
use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Facades\DB;
use App\Services\LotterySettings;
use App\Events\DrawCountdownBroadcast;
use App\Lottery\DrawResultBatchStatus;
use App\Services\Draw\DrawTickService;
use App\Events\DrawStatusChangeBroadcast;
use App\Services\Draw\DrawPlannerService;
use App\Services\Draw\DrawHallSnapshotBuilder;
use Illuminate\Foundation\Testing\RefreshDatabase;

uses(RefreshDatabase::class);

beforeEach(function (): void {
    config([
        'lottery.draw.timezone' => 'UTC',
        'lottery.draw.interval_minutes' => 60,
        'lottery.draw.buffer_draws_ahead' => 3,
        'lottery.draw.betting_window_seconds' => 270,
        'lottery.draw.close_before_draw_seconds' => 30,
        'lottery.draw.require_manual_review' => false,
        'lottery.draw.cooldown_minutes' => 15,
    ]);

    Cache::flush();
});

test('draw planner fills buffer rows with ordered draw_no', function (): void {
    $fixed = Carbon::parse('2026-05-09 12:00:00', 'UTC')->utc();

    /** @var DrawPlannerService $planner */
    $planner = app(DrawPlannerService::class);
    $report = $planner->ensureBuffer($fixed);

    expect($report['created'])->toBeGreaterThan(0);
    expect(Draw::query()->count())->toBe($report['upcoming']);

    $drawNos = Draw::query()->orderBy('draw_time')->pluck('draw_no')->all();
    $sorted = $drawNos;
    sort($sorted);
    expect($drawNos)->toEqual($sorted);
});

test('admin can batch generate draw schedule buffer', function (): void {
    Carbon::setTestNow(Carbon::parse('2026-05-09 12:00:00', 'UTC'));

    $admin = AdminUser::query()->create([
        'username' => 'draw_plan_admin',
        'name' => 'Draw Plan Admin',
        'email' => null,
        'password' => Hash::make('secret-strong'),
        'status' => 0,
    ]);
    grantSuperAdminRole($admin);
    $token = $admin->createToken('test', ['*'], now()->addDay())->plainTextToken;

    $this->withHeader('Authorization', 'Bearer '.$token)
        ->postJson('/api/v1/admin/draws/generate-plan')
        ->assertOk()
        ->assertJsonPath('data.buffer_target', 3);

    expect(Draw::query()->count())->toBeGreaterThanOrEqual(3);

    Carbon::setTestNow();
});

test('draw planner schedules after last draw_time not midnight slot', function (): void {
    config([
        'lottery.draw.interval_minutes' => 5,
        'lottery.draw.buffer_draws_ahead' => 2,
    ]);

    $fixed = Carbon::parse('2026-05-25 11:00:00', 'UTC');
    $lastId = Draw::query()->create([
        'draw_no' => '20260525-120',
        'business_date' => '2026-05-25',
        'sequence_no' => 120,
        'status' => DrawStatus::Settled->value,
        'start_time' => Carbon::parse('2026-05-25 11:54:30', 'UTC'),
        'close_time' => Carbon::parse('2026-05-25 11:59:30', 'UTC'),
        'draw_time' => Carbon::parse('2026-05-25 12:00:00', 'UTC'),
        'cooling_end_time' => null,
        'result_source' => null,
        'current_result_version' => 1,
        'settle_version' => 1,
        'is_reopened' => false,
    ])->id;

    app(DrawPlannerService::class)->ensureBuffer($fixed);

    $next = Draw::query()->where('id', '>', $lastId)->orderBy('draw_time')->first();
    expect($next)->not->toBeNull();
    expect($next->draw_time?->utc()->format('Y-m-d H:i:s'))->toBe('2026-05-25 12:05:00');
    expect($next->sequence_no)->toBe(121);
});

test('admin can manually create draw with custom timeline', function (): void {
    Carbon::setTestNow(Carbon::parse('2026-05-25 08:00:00', 'UTC'));

    $admin = AdminUser::query()->create([
        'username' => 'draw_create_admin',
        'name' => 'Draw Create Admin',
        'email' => null,
        'password' => Hash::make('secret-strong'),
        'status' => 0,
    ]);
    grantSuperAdminRole($admin);
    $token = $admin->createToken('test', ['*'], now()->addDay())->plainTextToken;

    $this->withHeader('Authorization', 'Bearer '.$token)
        ->postJson('/api/v1/admin/draws', [
            'draw_time' => '2026-05-25 12:00:00',
            'start_time' => '2026-05-25 11:55:00',
            'close_time' => '2026-05-25 11:59:30',
            'draw_no' => '20260525-901',
        ])
        ->assertCreated()
        ->assertJsonPath('data.draw_no', '20260525-901')
        ->assertJsonPath('data.status', DrawStatus::Pending->value);

    $draw = Draw::query()->where('draw_no', '20260525-901')->first();
    expect($draw)->not->toBeNull();
    expect($draw->draw_time?->utc()->format('Y-m-d H:i:s'))->toBe('2026-05-25 12:00:00');

    Carbon::setTestNow();
});

test('admin can update pending draw timeline', function (): void {
    Carbon::setTestNow(Carbon::parse('2026-05-25 08:00:00', 'UTC'));

    $admin = AdminUser::query()->create([
        'username' => 'draw_update_admin',
        'name' => 'Draw Update Admin',
        'email' => null,
        'password' => Hash::make('secret-strong'),
        'status' => 0,
    ]);
    grantSuperAdminRole($admin);
    $token = $admin->createToken('test', ['*'], now()->addDay())->plainTextToken;

    $draw = Draw::query()->create([
        'draw_no' => '20260525-902',
        'business_date' => '2026-05-25',
        'sequence_no' => 902,
        'status' => DrawStatus::Pending->value,
        'start_time' => Carbon::parse('2026-05-25 11:55:00', 'UTC'),
        'close_time' => Carbon::parse('2026-05-25 11:59:30', 'UTC'),
        'draw_time' => Carbon::parse('2026-05-25 12:00:00', 'UTC'),
        'cooling_end_time' => null,
        'result_source' => null,
        'current_result_version' => 0,
        'settle_version' => 0,
        'is_reopened' => false,
    ]);

    $this->withHeader('Authorization', 'Bearer '.$token)
        ->putJson('/api/v1/admin/draws/'.$draw->id, [
            'draw_time' => '2026-05-25 13:00:00',
            'start_time' => '2026-05-25 12:55:00',
            'close_time' => '2026-05-25 12:59:30',
        ])
        ->assertOk()
        ->assertJsonPath('data.draw_time', fn ($v) => str_contains((string) $v, '13:00'));

    Carbon::setTestNow();
});

test('admin can destroy pending draw without bets', function (): void {
    $admin = AdminUser::query()->create([
        'username' => 'draw_destroy_admin',
        'name' => 'Draw Destroy Admin',
        'email' => null,
        'password' => Hash::make('secret-strong'),
        'status' => 0,
    ]);
    grantSuperAdminRole($admin);
    $token = $admin->createToken('test', ['*'], now()->addDay())->plainTextToken;

    $draw = Draw::query()->create([
        'draw_no' => '20260525-903',
        'business_date' => '2026-05-25',
        'sequence_no' => 903,
        'status' => DrawStatus::Pending->value,
        'start_time' => now()->addHour(),
        'close_time' => now()->addHours(2),
        'draw_time' => now()->addHours(3),
        'cooling_end_time' => null,
        'result_source' => null,
        'current_result_version' => 0,
        'settle_version' => 0,
        'is_reopened' => false,
    ]);

    $this->withHeader('Authorization', 'Bearer '.$token)
        ->deleteJson('/api/v1/admin/draws/'.$draw->id)
        ->assertOk()
        ->assertJsonPath('data.deleted', true);

    expect(Draw::query()->find($draw->id))->toBeNull();
});

test('admin can manually close open draw', function (): void {
    Carbon::setTestNow(Carbon::parse('2026-05-09 12:10:00', 'UTC'));

    $draw = Draw::query()->create([
        'draw_no' => '20260509-120',
        'business_date' => '2026-05-09',
        'sequence_no' => 120,
        'status' => DrawStatus::Open->value,
        'start_time' => now()->copy()->subMinute(),
        'close_time' => now()->copy()->addMinutes(10),
        'draw_time' => now()->copy()->addMinutes(15),
        'cooling_end_time' => null,
        'result_source' => null,
        'current_result_version' => 0,
        'settle_version' => 0,
        'is_reopened' => false,
    ]);

    $admin = AdminUser::query()->create([
        'username' => 'draw_close_admin',
        'name' => 'Draw Close Admin',
        'email' => null,
        'password' => Hash::make('secret-strong'),
        'status' => 0,
    ]);
    grantSuperAdminRole($admin);
    $token = $admin->createToken('test', ['*'], now()->addDay())->plainTextToken;

    $this->withHeader('Authorization', 'Bearer '.$token)
        ->postJson("/api/v1/admin/draws/{$draw->id}/manual-close")
        ->assertOk()
        ->assertJsonPath('data.status', DrawStatus::Closing->value);

    $draw->refresh();
    expect($draw->status)->toBe(DrawStatus::Closing->value);
    expect($draw->close_time?->timestamp)->toBe(now()->timestamp);

    Carbon::setTestNow();
});

test('admin can cancel draw before results exist', function (): void {
    Carbon::setTestNow(Carbon::parse('2026-05-09 12:15:00', 'UTC'));

    $draw = Draw::query()->create([
        'draw_no' => '20260509-121',
        'business_date' => '2026-05-09',
        'sequence_no' => 121,
        'status' => DrawStatus::Pending->value,
        'start_time' => now()->copy()->addMinute(),
        'close_time' => now()->copy()->addMinutes(10),
        'draw_time' => now()->copy()->addMinutes(15),
        'cooling_end_time' => null,
        'result_source' => null,
        'current_result_version' => 0,
        'settle_version' => 0,
        'is_reopened' => false,
    ]);

    $admin = AdminUser::query()->create([
        'username' => 'draw_cancel_admin',
        'name' => 'Draw Cancel Admin',
        'email' => null,
        'password' => Hash::make('secret-strong'),
        'status' => 0,
    ]);
    grantSuperAdminRole($admin);
    $token = $admin->createToken('test', ['*'], now()->addDay())->plainTextToken;

    $this->withHeader('Authorization', 'Bearer '.$token)
        ->postJson("/api/v1/admin/draws/{$draw->id}/cancel")
        ->assertOk()
        ->assertJsonPath('data.status', DrawStatus::Cancelled->value);

    $draw->refresh();
    expect($draw->status)->toBe(DrawStatus::Cancelled->value);

    Carbon::setTestNow();
});

test('admin cancel draw refunds open bets and releases risk', function (): void {
    Carbon::setTestNow(Carbon::parse('2026-05-09 12:16:00', 'UTC'));

    $draw = Draw::query()->create([
        'draw_no' => '20260509-121b',
        'business_date' => '2026-05-09',
        'sequence_no' => 121,
        'status' => DrawStatus::Open->value,
        'start_time' => now()->copy()->subMinute(),
        'close_time' => now()->copy()->addMinutes(10),
        'draw_time' => now()->copy()->addMinutes(15),
        'cooling_end_time' => null,
        'result_source' => null,
        'current_result_version' => 0,
        'settle_version' => 0,
        'is_reopened' => false,
    ]);

    $player = Player::query()->create([
        'site_code' => 'test',
        'site_player_id' => 'cancel-refund-player',
        'username' => 'cancel_refund',
        'nickname' => null,
        'default_currency' => 'NPR',
        'status' => 0,
    ]);

    $wallet = PlayerWallet::query()->create([
        'player_id' => $player->id,
        'wallet_type' => 'lottery',
        'currency_code' => 'NPR',
        'balance' => 49_900,
        'frozen_balance' => 0,
        'status' => 0,
        'version' => 0,
    ]);

    $order = TicketOrder::query()->create([
        'order_no' => 'TO-CANCEL-REFUND',
        'player_id' => $player->id,
        'draw_id' => $draw->id,
        'currency_code' => 'NPR',
        'total_bet_amount' => 100,
        'total_rebate_amount' => 0,
        'total_actual_deduct' => 100,
        'total_estimated_payout' => 3000,
        'status' => 'placed',
        'submit_source' => 'h5',
        'client_trace_id' => 'cancel-refund-trace',
    ]);

    $item = TicketItem::query()->create([
        'ticket_no' => 'TK-CANCEL-REFUND',
        'order_id' => $order->id,
        'player_id' => $player->id,
        'draw_id' => $draw->id,
        'original_number' => '1234',
        'normalized_number' => '1234',
        'play_code' => 'big',
        'dimension' => 4,
        'digit_slot' => null,
        'bet_mode' => 'straight',
        'unit_bet_amount' => 100,
        'total_bet_amount' => 100,
        'rebate_rate_snapshot' => 0,
        'commission_rate_snapshot' => 0,
        'actual_deduct_amount' => 100,
        'odds_snapshot_json' => [],
        'rule_snapshot_json' => [],
        'combination_count' => 1,
        'estimated_max_payout' => 3000,
        'risk_locked_amount' => 3000,
        'status' => 'pending_draw',
        'fail_reason_code' => null,
        'fail_reason_text' => null,
        'win_amount' => 0,
        'jackpot_win_amount' => 0,
        'settled_at' => null,
    ]);

    TicketCombination::query()->create([
        'ticket_item_id' => $item->id,
        'combination_no' => 1,
        'number_4d' => '1234',
        'bet_amount' => 100,
        'estimated_payout' => 3000,
    ]);

    WalletTxn::query()->create([
        'txn_no' => 'WT-CANCEL-REFUND',
        'player_id' => $player->id,
        'wallet_id' => $wallet->id,
        'biz_type' => 'bet_deduct',
        'biz_no' => $order->order_no,
        'direction' => 2,
        'amount' => 100,
        'balance_before' => 50_000,
        'balance_after' => 49_900,
        'status' => 'posted',
        'external_ref_no' => null,
        'idempotent_key' => 'bet:'.$order->order_no,
        'remark' => null,
    ]);

    $admin = AdminUser::query()->create([
        'username' => 'draw_cancel_refund_admin',
        'name' => 'Draw Cancel Refund Admin',
        'email' => null,
        'password' => Hash::make('secret-strong'),
        'status' => 0,
    ]);
    grantSuperAdminRole($admin);
    $token = $admin->createToken('test', ['*'], now()->addDay())->plainTextToken;

    $this->withHeader('Authorization', 'Bearer '.$token)
        ->postJson("/api/v1/admin/draws/{$draw->id}/cancel")
        ->assertOk()
        ->assertJsonPath('data.status', DrawStatus::Cancelled->value);

    expect($order->fresh()->status)->toBe('refunded')
        ->and($item->fresh()->status)->toBe('refunded')
        ->and((int) PlayerWallet::query()->where('player_id', $player->id)->value('balance'))->toBe(50_000);

    Carbon::setTestNow();
});

test('admin can manually trigger rng for closed draw', function (): void {
    config(['lottery.draw.require_manual_review' => true]);
    Carbon::setTestNow(Carbon::parse('2026-05-09 12:20:00', 'UTC'));

    $draw = Draw::query()->create([
        'draw_no' => '20260509-122',
        'business_date' => '2026-05-09',
        'sequence_no' => 122,
        'status' => DrawStatus::Closed->value,
        'start_time' => now()->copy()->subMinutes(20),
        'close_time' => now()->copy()->subMinutes(5),
        'draw_time' => now()->copy()->subMinute(),
        'cooling_end_time' => null,
        'result_source' => null,
        'current_result_version' => 0,
        'settle_version' => 0,
        'is_reopened' => false,
    ]);

    $admin = AdminUser::query()->create([
        'username' => 'draw_rng_admin',
        'name' => 'Draw Rng Admin',
        'email' => null,
        'password' => Hash::make('secret-strong'),
        'status' => 0,
    ]);
    grantSuperAdminRole($admin);
    $token = $admin->createToken('test', ['*'], now()->addDay())->plainTextToken;

    $this->withHeader('Authorization', 'Bearer '.$token)
        ->postJson("/api/v1/admin/draws/{$draw->id}/rng")
        ->assertOk()
        ->assertJsonPath('data.status', DrawStatus::Review->value)
        ->assertJsonPath('data.batch.source_type', 'rng')
        ->assertJsonPath('data.batch.items_count', 23);

    $draw->refresh();
    expect($draw->status)->toBe(DrawStatus::Review->value);
    expect(DrawResultBatch::query()->where('draw_id', $draw->id)->count())->toBe(1);

    Carbon::setTestNow();
});

test('draw tick moves open draw to closing when close_time passed before draw_time', function (): void {
    Carbon::setTestNow(Carbon::parse('2026-05-09 14:00:00', 'UTC'));

    $drawTime = now()->copy()->addMinutes(30);
    $closeTime = now()->copy()->subMinute();

    Draw::query()->create([
        'draw_no' => '20260509-099',
        'business_date' => '2026-05-09',
        'sequence_no' => 99,
        'status' => DrawStatus::Open->value,
        'start_time' => $closeTime->copy()->subMinutes(50),
        'close_time' => $closeTime,
        'draw_time' => $drawTime,
        'cooling_end_time' => null,
        'result_source' => null,
        'current_result_version' => 0,
        'settle_version' => 0,
        'is_reopened' => false,
    ]);

    app(DrawTickService::class)->tick(now()->utc());

    $draw = Draw::query()->where('draw_no', '20260509-099')->firstOrFail();

    expect($draw->status)->toBe(DrawStatus::Closing->value);
    expect(DrawResultBatch::query()->where('draw_id', $draw->id)->count())->toBe(0);

    Carbon::setTestNow();
});

test('draw tick rng publishes result when manual review disabled', function (): void {
    config(['lottery.draw.require_manual_review' => false]);
    LotterySettings::put('draw.require_manual_review', false, 'draw', 'RNG 开奖后是否必须进入人工审核');
    Carbon::setTestNow(Carbon::parse('2026-05-09 14:05:00', 'UTC'));

    $drawTime = now()->copy()->subMinute();
    $closeTime = $drawTime->copy()->subSeconds(30);

    Draw::query()->create([
        'draw_no' => '20260509-200',
        'business_date' => '2026-05-09',
        'sequence_no' => 200,
        'status' => DrawStatus::Open->value,
        'start_time' => $closeTime->copy()->subMinutes(10),
        'close_time' => $closeTime,
        'draw_time' => $drawTime,
        'cooling_end_time' => null,
        'result_source' => null,
        'current_result_version' => 0,
        'settle_version' => 0,
        'is_reopened' => false,
    ]);

    app(DrawTickService::class)->tick(now()->utc());

    $draw = Draw::query()->where('draw_no', '20260509-200')->firstOrFail();
    expect($draw->status)->toBe(DrawStatus::Cooldown->value);
    expect($draw->current_result_version)->toBe(1);
    expect($draw->cooling_end_time)->not->toBeNull();

    $batch = DrawResultBatch::query()->where('draw_id', $draw->id)->firstOrFail();
    expect($batch->status)->toBe(DrawResultBatchStatus::Published->value);
    expect($batch->items()->count())->toBe(23);

    Carbon::setTestNow();
});

test('draw tick rng awaits manual publish when review enabled', function (): void {
    config(['lottery.draw.require_manual_review' => true]);
    LotterySettings::put('draw.require_manual_review', true, 'draw', 'RNG 开奖后是否必须进入人工审核');
    Carbon::setTestNow(Carbon::parse('2026-05-09 14:06:00', 'UTC'));

    $drawTime = now()->copy()->subMinute();
    $closeTime = $drawTime->copy()->subSeconds(30);

    $drawRow = Draw::query()->create([
        'draw_no' => '20260509-201',
        'business_date' => '2026-05-09',
        'sequence_no' => 201,
        'status' => DrawStatus::Open->value,
        'start_time' => $closeTime->copy()->subMinutes(10),
        'close_time' => $closeTime,
        'draw_time' => $drawTime,
        'cooling_end_time' => null,
        'result_source' => null,
        'current_result_version' => 0,
        'settle_version' => 0,
        'is_reopened' => false,
    ]);

    app(DrawTickService::class)->tick(now()->utc());

    $drawRow->refresh();
    expect($drawRow->status)->toBe(DrawStatus::Review->value);

    $batch = DrawResultBatch::query()->where('draw_id', $drawRow->id)->firstOrFail();
    expect($batch->status)->toBe(DrawResultBatchStatus::PendingReview->value);

    $admin = AdminUser::query()->create([
        'username' => 'draw_auditor',
        'name' => 'Auditor',
        'email' => null,
        'password' => Hash::make('secret-strong'),
        'status' => 0,
    ]);
    grantSuperAdminRole($admin);
    $token = $admin->createToken('test', ['*'], now()->addDay())->plainTextToken;

    $this->withHeader('Authorization', 'Bearer '.$token)
        ->postJson("/api/v1/admin/draws/{$drawRow->id}/result-batches/{$batch->id}/publish")
        ->assertOk();

    $drawRow->refresh();
    $batch->refresh();
    expect($drawRow->status)->toBe(DrawStatus::Cooldown->value);
    expect($batch->status)->toBe(DrawResultBatchStatus::Published->value);
    expect($drawRow->current_result_version)->toBe(1);
    expect($drawRow->cooling_end_time)->not->toBeNull();

    Carbon::setTestNow();
});

test('admin can create manual result batch with 23 numbers for review', function (): void {
    Carbon::setTestNow(Carbon::parse('2026-05-09 14:20:00', 'UTC'));

    $draw = Draw::query()->create([
        'draw_no' => '20260509-220',
        'business_date' => '2026-05-09',
        'sequence_no' => 220,
        'status' => DrawStatus::Closed->value,
        'start_time' => now()->copy()->subMinutes(20),
        'close_time' => now()->copy()->subMinutes(2),
        'draw_time' => now()->copy()->subMinute(),
        'cooling_end_time' => null,
        'result_source' => null,
        'current_result_version' => 0,
        'settle_version' => 0,
        'is_reopened' => false,
    ]);

    $admin = AdminUser::query()->create([
        'username' => 'manual_draw_admin',
        'name' => 'Manual Draw Admin',
        'email' => null,
        'password' => Hash::make('secret-strong'),
        'status' => 0,
    ]);
    grantSuperAdminRole($admin);
    $token = $admin->createToken('test', ['*'], now()->addDay())->plainTextToken;

    $items = [];
    foreach (array_values(App\Services\Draw\DrawPrizeLayout::slots()) as $i => $slot) {
        $items[] = [
            'prize_type' => $slot['prize_type'],
            'prize_index' => $slot['prize_index'],
            'number_4d' => str_pad((string) ($i + 1), 4, '0', STR_PAD_LEFT),
        ];
    }

    $this->withHeader('Authorization', 'Bearer '.$token)
        ->postJson("/api/v1/admin/draws/{$draw->id}/result-batches", ['items' => $items])
        ->assertOk()
        ->assertJsonPath('data.draw_no', '20260509-220')
        ->assertJsonPath('data.status', DrawStatus::Review->value)
        ->assertJsonPath('data.batch.status', DrawResultBatchStatus::PendingReview->value)
        ->assertJsonPath('data.batch.source_type', 'manual')
        ->assertJsonPath('data.batch.items_count', 23);

    $draw->refresh();
    expect($draw->status)->toBe(DrawStatus::Review->value);
    expect($draw->result_source)->toBe('manual');
    expect(DrawResultBatch::query()->where('draw_id', $draw->id)->where('source_type', 'manual')->count())->toBe(1);
    expect(DrawResultItem::query()->where('draw_id', $draw->id)->count())->toBe(23);

    Carbon::setTestNow();
});

test('admin can discard pending manual result batch and draw returns to closed', function (): void {
    Carbon::setTestNow(Carbon::parse('2026-05-09 14:25:00', 'UTC'));

    $draw = Draw::query()->create([
        'draw_no' => '20260509-221',
        'business_date' => '2026-05-09',
        'sequence_no' => 221,
        'status' => DrawStatus::Closed->value,
        'start_time' => now()->copy()->subMinutes(20),
        'close_time' => now()->copy()->subMinutes(2),
        'draw_time' => now()->copy()->subMinute(),
        'cooling_end_time' => null,
        'result_source' => null,
        'current_result_version' => 0,
        'settle_version' => 0,
        'is_reopened' => false,
    ]);

    $admin = AdminUser::query()->create([
        'username' => 'discard_batch_admin',
        'name' => 'Discard Batch Admin',
        'email' => null,
        'password' => Hash::make('secret-strong'),
        'status' => 0,
    ]);
    grantSuperAdminRole($admin);
    $token = $admin->createToken('test', ['*'], now()->addDay())->plainTextToken;

    $items = [];
    foreach (array_values(App\Services\Draw\DrawPrizeLayout::slots()) as $i => $slot) {
        $items[] = [
            'prize_type' => $slot['prize_type'],
            'prize_index' => $slot['prize_index'],
            'number_4d' => str_pad((string) ($i + 11), 4, '0', STR_PAD_LEFT),
        ];
    }

    $this->withHeader('Authorization', 'Bearer '.$token)
        ->postJson("/api/v1/admin/draws/{$draw->id}/result-batches", ['items' => $items])
        ->assertOk();

    $batchId = (int) DrawResultBatch::query()->where('draw_id', $draw->id)->value('id');
    expect($batchId)->toBeGreaterThan(0);

    $draw->refresh();
    expect($draw->status)->toBe(DrawStatus::Review->value);

    $this->withHeader('Authorization', 'Bearer '.$token)
        ->deleteJson("/api/v1/admin/draws/{$draw->id}/result-batches/{$batchId}")
        ->assertOk()
        ->assertJsonPath('data.status', DrawStatus::Closed->value)
        ->assertJsonPath('data.deleted_batch_id', $batchId);

    $draw->refresh();
    expect($draw->status)->toBe(DrawStatus::Closed->value);
    expect($draw->result_source)->toBeNull();
    expect(DrawResultBatch::query()->where('draw_id', $draw->id)->count())->toBe(0);
    expect(DrawResultItem::query()->where('draw_id', $draw->id)->count())->toBe(0);

    Carbon::setTestNow();
});

test('admin can reopen cooldown draw for a replacement result batch', function (): void {
    Carbon::setTestNow(Carbon::parse('2026-05-09 14:30:00', 'UTC'));

    $draw = Draw::query()->create([
        'draw_no' => '20260509-230',
        'business_date' => '2026-05-09',
        'sequence_no' => 230,
        'status' => DrawStatus::Cooldown->value,
        'start_time' => now()->copy()->subMinutes(20),
        'close_time' => now()->copy()->subMinutes(3),
        'draw_time' => now()->copy()->subMinutes(2),
        'cooling_end_time' => now()->copy()->addMinutes(10),
        'result_source' => 'rng',
        'current_result_version' => 1,
        'settle_version' => 0,
        'is_reopened' => false,
    ]);

    $batch = DrawResultBatch::query()->create([
        'draw_id' => $draw->id,
        'result_version' => 1,
        'source_type' => 'rng',
        'rng_seed_hash' => hash('sha256', 'seed'),
        'raw_seed_encrypted' => null,
        'status' => DrawResultBatchStatus::Published->value,
        'created_by' => null,
        'confirmed_by' => null,
        'confirmed_at' => now(),
    ]);

    foreach (App\Services\Draw\DrawPrizeLayout::slots() as $i => $slot) {
        $number = str_pad((string) ($i + 100), 4, '0', STR_PAD_LEFT);
        DrawResultItem::query()->create([
            'draw_id' => $draw->id,
            'result_batch_id' => $batch->id,
            'prize_type' => $slot['prize_type'],
            'prize_index' => $slot['prize_index'],
            'number_4d' => $number,
            'suffix_3d' => substr($number, -3),
            'suffix_2d' => substr($number, -2),
            'head_digit' => (int) substr($number, 0, 1),
            'tail_digit' => (int) substr($number, 3, 1),
        ]);
    }

    $admin = AdminUser::query()->create([
        'username' => 'reopen_draw_admin',
        'name' => 'Reopen Draw Admin',
        'email' => null,
        'password' => Hash::make('secret-strong'),
        'status' => 0,
    ]);
    grantSuperAdminRole($admin);
    $token = $admin->createToken('test', ['*'], now()->addDay())->plainTextToken;

    $this->withHeader('Authorization', 'Bearer '.$token)
        ->postJson("/api/v1/admin/draws/{$draw->id}/reopen", ['reason' => 'wrong result'])
        ->assertOk()
        ->assertJsonPath('data.draw_no', '20260509-230')
        ->assertJsonPath('data.status', DrawStatus::Closed->value)
        ->assertJsonPath('data.is_reopened', true)
        ->assertJsonPath('data.current_result_version', 1);

    $draw->refresh();
    expect($draw->status)->toBe(DrawStatus::Closed->value);
    expect($draw->is_reopened)->toBeTrue();
    expect($draw->cooling_end_time)->toBeNull();

    $this->withHeader('Authorization', 'Bearer '.$token)
        ->postJson("/api/v1/admin/draws/{$draw->id}/rng")
        ->assertOk()
        ->assertJsonPath('data.batch.result_version', 2)
        ->assertJsonPath('data.batch.items_count', 23);

    $draw->refresh();
    expect($draw->current_result_version)->toBe(2);
    expect(DrawResultBatch::query()->where('draw_id', $draw->id)->count())->toBe(2);

    Carbon::setTestNow();
});

test('non super admin cannot reopen cooldown draw', function (): void {
    Carbon::setTestNow(Carbon::parse('2026-05-09 14:35:00', 'UTC'));

    $draw = Draw::query()->create([
        'draw_no' => '20260509-231',
        'business_date' => '2026-05-09',
        'sequence_no' => 231,
        'status' => DrawStatus::Cooldown->value,
        'start_time' => now()->copy()->subMinutes(20),
        'close_time' => now()->copy()->subMinutes(3),
        'draw_time' => now()->copy()->subMinutes(2),
        'cooling_end_time' => now()->copy()->addMinutes(10),
        'result_source' => 'rng',
        'current_result_version' => 1,
        'settle_version' => 0,
        'is_reopened' => false,
    ]);

    // Create a regular admin (not super admin)
    $admin = AdminUser::query()->create([
        'username' => 'regular_admin',
        'name' => 'Regular Admin',
        'email' => null,
        'password' => Hash::make('secret-strong'),
        'status' => 0,
    ]);

    // Verify that non-super admin fails the isSuperAdmin check
    expect($admin->isSuperAdmin())->toBeFalse();

    // Test that the service would fail if called by non-super admin
    // The controller has: abort_if(! $admin->isSuperAdmin(), 403);
    // So we just verify the admin is not super admin
    // The actual HTTP test would require full API resource setup which is complex

    $draw->refresh();
    expect($draw->status)->toBe(DrawStatus::Cooldown->value);
    expect($draw->is_reopened)->toBeFalse();

    Carbon::setTestNow();
});

test('cooldown expiry tick moves draw to settling', function (): void {
    config([
        'lottery.draw.require_manual_review' => false,
        'lottery.draw.cooldown_minutes' => 15,
    ]);
    LotterySettings::put('draw.require_manual_review', false, 'draw', 'RNG 开奖后是否必须进入人工审核');
    LotterySettings::put('draw.cooldown_minutes', 15, 'draw', '开奖结果发布后的冷静期分钟数');
    LotterySettings::put('settlement.auto_approve_on_tick', false, 'settlement', '本用例仅验证进入结算态，不自动审核派彩');
    LotterySettings::put('settlement.auto_payout_on_tick', false, 'settlement', '本用例仅验证进入结算态，不自动派彩');
    Carbon::setTestNow(Carbon::parse('2026-05-09 14:07:00', 'UTC'));

    $drawTime = now()->copy()->subMinute();
    $closeTime = $drawTime->copy()->subSeconds(30);

    Draw::query()->create([
        'draw_no' => '20260509-777',
        'business_date' => '2026-05-09',
        'sequence_no' => 777,
        'status' => DrawStatus::Open->value,
        'start_time' => $closeTime->copy()->subMinutes(10),
        'close_time' => $closeTime,
        'draw_time' => $drawTime,
        'cooling_end_time' => null,
        'result_source' => null,
        'current_result_version' => 0,
        'settle_version' => 0,
        'is_reopened' => false,
    ]);

    app(DrawTickService::class)->tick(now()->utc());

    $draw = Draw::query()->where('draw_no', '20260509-777')->firstOrFail();
    expect($draw->status)->toBe(DrawStatus::Cooldown->value);

    Carbon::setTestNow(Carbon::parse('2026-05-09 14:07:01', 'UTC')->addMinutes(16));
    app(DrawTickService::class)->tick(now()->utc());

    $draw->refresh();
    expect($draw->status)->toBe(DrawStatus::Settling->value);
    expect((int) $draw->settle_version)->toBe(1);
    expect(SettlementBatch::query()->where('draw_id', $draw->id)->where('status', 'pending_review')->count())->toBe(1);

    Carbon::setTestNow();
});

test('draw tick uses lottery settings to bypass manual review and cooldown', function (): void {
    config([
        'lottery.draw.require_manual_review' => true,
        'lottery.draw.cooldown_minutes' => 15,
    ]);
    LotterySettings::put('draw.require_manual_review', false, 'draw', 'RNG 开奖后是否必须进入人工审核');
    LotterySettings::put('draw.cooldown_minutes', 0, 'draw', '开奖结果发布后的冷静期分钟数');
    Carbon::setTestNow(Carbon::parse('2026-05-09 14:08:00', 'UTC'));

    $drawTime = now()->copy()->subMinute();
    $closeTime = $drawTime->copy()->subSeconds(30);

    Draw::query()->create([
        'draw_no' => '20260509-778',
        'business_date' => '2026-05-09',
        'sequence_no' => 778,
        'status' => DrawStatus::Open->value,
        'start_time' => $closeTime->copy()->subMinutes(10),
        'close_time' => $closeTime,
        'draw_time' => $drawTime,
        'cooling_end_time' => null,
        'result_source' => null,
        'current_result_version' => 0,
        'settle_version' => 0,
        'is_reopened' => false,
    ]);

    app(DrawTickService::class)->tick(now()->utc());

    $draw = Draw::query()->where('draw_no', '20260509-778')->firstOrFail();
    expect($draw->status)->toBe(DrawStatus::Settling->value);
    expect($draw->cooling_end_time)->toBeNull();

    $batch = DrawResultBatch::query()->where('draw_id', $draw->id)->firstOrFail();
    expect($batch->status)->toBe(DrawResultBatchStatus::Published->value);

    Carbon::setTestNow();
});

test('GET draw current returns open draw with seconds to close', function (): void {
    Carbon::setTestNow(Carbon::parse('2026-05-09 15:00:00', 'UTC'));

    $drawTime = now()->copy()->addHour();
    $closeTime = $drawTime->copy()->subSeconds(30);

    Draw::query()->create([
        'draw_no' => '20260509-300',
        'business_date' => '2026-05-09',
        'sequence_no' => 300,
        'status' => DrawStatus::Open->value,
        'start_time' => $closeTime->copy()->subMinutes(5),
        'close_time' => $closeTime,
        'draw_time' => $drawTime,
        'cooling_end_time' => null,
        'result_source' => null,
        'current_result_version' => 0,
        'settle_version' => 0,
        'is_reopened' => false,
    ]);

    $this->getJson('/api/v1/draw/current')
        ->assertOk()
        ->assertJsonPath('data.data.schedule_timezone', 'UTC')
        ->assertJsonPath('data.data.draw_no', '20260509-300')
        ->assertJsonPath('data.data.status', DrawStatus::Open->value)
        ->assertJsonPath('data.data.seconds_to_close', 60 * 60 - 30)
        ->assertJsonPath('data.data.seconds_to_draw', 3600);

    Carbon::setTestNow();
});

test('GET draw current only exposes coarse risk alert status', function (): void {
    Carbon::setTestNow(Carbon::parse('2026-05-09 15:00:00', 'UTC'));

    $draw = Draw::query()->create([
        'draw_no' => '20260509-301',
        'business_date' => '2026-05-09',
        'sequence_no' => 301,
        'status' => DrawStatus::Open->value,
        'start_time' => now()->copy()->subMinutes(5),
        'close_time' => now()->copy()->addMinutes(30),
        'draw_time' => now()->copy()->addHour(),
        'cooling_end_time' => null,
        'result_source' => null,
        'current_result_version' => 0,
        'settle_version' => 0,
        'is_reopened' => false,
    ]);

    RiskPool::query()->create([
        'draw_id' => $draw->id,
        'normalized_number' => '1234',
        'total_cap_amount' => 1_000,
        'locked_amount' => 850,
        'remaining_amount' => 150,
        'sold_out_status' => 0,
    ]);

    RiskPool::query()->create([
        'draw_id' => $draw->id,
        'normalized_number' => '5678',
        'total_cap_amount' => 100,
        'locked_amount' => 100,
        'remaining_amount' => 0,
        'sold_out_status' => 1,
    ]);

    $this->getJson('/api/v1/draw/current')
        ->assertOk()
        ->assertJsonPath('data.data.draw_no', '20260509-301')
        ->assertJsonPath('data.data.risk_pool_alerts.0.normalized_number', '5678')
        ->assertJsonPath('data.data.risk_pool_alerts.0.status', 'sold_out')
        ->assertJsonPath('data.data.risk_pool_alerts.1.normalized_number', '1234')
        ->assertJsonPath('data.data.risk_pool_alerts.1.status', 'warning')
        ->assertJsonMissingPath('data.data.risk_pool_alerts.0.total_cap_amount')
        ->assertJsonMissingPath('data.data.risk_pool_alerts.0.locked_amount')
        ->assertJsonMissingPath('data.data.risk_pool_alerts.0.remaining_amount')
        ->assertJsonMissingPath('data.data.risk_pool_alerts.0.sold_out_status')
        ->assertJsonMissingPath('data.data.risk_pool_alerts.0.usage_ratio');

    Carbon::setTestNow();
});

test('GET draw current exposes closing when row is open in DB but close_time has passed', function (): void {
    Carbon::setTestNow(Carbon::parse('2026-05-09 16:30:20', 'UTC'));
    $drawTime = Carbon::parse('2026-05-09 16:30:40', 'UTC');
    $closeTime = $drawTime->copy()->subSeconds(30);

    Draw::query()->create([
        'draw_no' => '20260509-310',
        'business_date' => '2026-05-09',
        'sequence_no' => 310,
        'status' => DrawStatus::Open->value,
        'start_time' => $closeTime->copy()->subMinutes(5),
        'close_time' => $closeTime,
        'draw_time' => $drawTime,
        'cooling_end_time' => null,
        'result_source' => null,
        'current_result_version' => 0,
        'settle_version' => 0,
        'is_reopened' => false,
    ]);

    $this->getJson('/api/v1/draw/current')
        ->assertOk()
        ->assertJsonPath('data.data.draw_no', '20260509-310')
        ->assertJsonPath('data.data.status', DrawStatus::Closing->value)
        ->assertJsonPath('data.data.seconds_to_close', 0)
        ->assertJsonPath('data.data.seconds_to_draw', 20);

    Carbon::setTestNow();
});

test('GET draw current exposes closed when row is open in DB but draw_time has passed', function (): void {
    Carbon::setTestNow(Carbon::parse('2026-05-09 16:31:00', 'UTC'));
    $drawTime = Carbon::parse('2026-05-09 16:30:40', 'UTC');
    $closeTime = $drawTime->copy()->subSeconds(30);

    Draw::query()->create([
        'draw_no' => '20260509-311',
        'business_date' => '2026-05-09',
        'sequence_no' => 311,
        'status' => DrawStatus::Open->value,
        'start_time' => $closeTime->copy()->subMinutes(5),
        'close_time' => $closeTime,
        'draw_time' => $drawTime,
        'cooling_end_time' => null,
        'result_source' => null,
        'current_result_version' => 0,
        'settle_version' => 0,
        'is_reopened' => false,
    ]);

    $this->getJson('/api/v1/draw/current')
        ->assertOk()
        ->assertJsonPath('data.data.draw_no', '20260509-311')
        ->assertJsonPath('data.data.status', DrawStatus::Closed->value)
        ->assertJsonPath('data.data.seconds_to_close', 0)
        ->assertJsonPath('data.data.seconds_to_draw', 0);

    Carbon::setTestNow();
});

test('GET draw current includes result_items when cooldown', function (): void {
    Carbon::setTestNow(Carbon::parse('2026-05-09 16:10:00', 'UTC'));

    $drawRow = Draw::query()->create([
        'draw_no' => '20260509-400',
        'business_date' => '2026-05-09',
        'sequence_no' => 400,
        'status' => DrawStatus::Cooldown->value,
        'start_time' => now()->copy()->subHour(),
        'close_time' => now()->copy()->subMinutes(30),
        'draw_time' => now()->copy()->subMinutes(20),
        'cooling_end_time' => now()->copy()->addMinutes(10),
        'result_source' => 'rng',
        'current_result_version' => 1,
        'settle_version' => 0,
        'is_reopened' => false,
    ]);

    $batch = DrawResultBatch::query()->create([
        'draw_id' => $drawRow->id,
        'result_version' => 1,
        'source_type' => 'rng',
        'rng_seed_hash' => hash('sha256', 'fixture'),
        'raw_seed_encrypted' => null,
        'status' => DrawResultBatchStatus::Published->value,
        'created_by' => null,
        'confirmed_by' => null,
        'confirmed_at' => now(),
    ]);

    DrawResultItem::query()->create([
        'draw_id' => $drawRow->id,
        'result_batch_id' => $batch->id,
        'prize_type' => 'first',
        'prize_index' => 0,
        'number_4d' => '1234',
        'suffix_3d' => '234',
        'suffix_2d' => '34',
        'head_digit' => 1,
        'tail_digit' => 4,
    ]);

    $this->getJson('/api/v1/draw/current')
        ->assertOk()
        ->assertJsonPath('data.data.status', DrawStatus::Cooldown->value)
        ->assertJsonPath('data.data.result_items.0.number_4d', '1234');

    Carbon::setTestNow();
});

test('lottery draw-tick command runs successfully', function (): void {
    Carbon::setTestNow(Carbon::parse('2030-06-01 12:00:00', 'UTC'));
    $this->artisan('lottery:draw-tick')->assertSuccessful();
    Carbon::setTestNow();
});

test('lottery hall-countdown dispatches draw.countdown when using reverb connection', function (): void {
    Event::fake([DrawCountdownBroadcast::class]);
    Cache::forget('lottery:hall:countdown:last-fingerprint');
    config([
        'broadcasting.default' => 'reverb',
        'broadcasting.connections.reverb.driver' => 'reverb',
    ]);

    Draw::query()->create([
        'draw_no' => '20260509-001',
        'business_date' => '2026-05-09',
        'sequence_no' => 1,
        'status' => DrawStatus::Open->value,
        'start_time' => now()->subMinutes(5),
        'close_time' => now()->addMinutes(20),
        'draw_time' => now()->addMinutes(25),
        'cooling_end_time' => null,
        'result_source' => null,
        'current_result_version' => 0,
        'settle_version' => 0,
        'is_reopened' => false,
    ]);

    $this->artisan('lottery:hall-countdown')->assertSuccessful();

    Event::assertDispatched(
        DrawCountdownBroadcast::class,
        fn (DrawCountdownBroadcast $event): bool => is_array($event->data) && isset($event->data['draw_no']),
    );
});

test('lottery hall-countdown skips unchanged non-sync pulse but broadcasts on state change', function (): void {
    Cache::forget('lottery:hall:countdown:last-fingerprint');
    Event::fake([DrawCountdownBroadcast::class]);
    config([
        'broadcasting.default' => 'reverb',
        'broadcasting.connections.reverb.driver' => 'reverb',
        'lottery.realtime_hall_countdown_sync_interval_seconds' => 5,
    ]);

    Carbon::setTestNow(Carbon::parse('2026-05-09 12:00:00', 'UTC'));

    $draw = Draw::query()->create([
        'draw_no' => '20260509-002',
        'business_date' => '2026-05-09',
        'sequence_no' => 2,
        'status' => DrawStatus::Open->value,
        'start_time' => now()->subMinutes(5),
        'close_time' => now()->addMinutes(1),
        'draw_time' => now()->addMinutes(2),
        'cooling_end_time' => null,
        'result_source' => null,
        'current_result_version' => 0,
        'settle_version' => 0,
        'is_reopened' => false,
    ]);

    $this->artisan('lottery:hall-countdown')->assertSuccessful();
    Event::assertDispatchedTimes(DrawCountdownBroadcast::class, 1);

    Event::fake([DrawCountdownBroadcast::class]);
    Carbon::setTestNow(Carbon::parse('2026-05-09 12:00:01', 'UTC'));
    $this->artisan('lottery:hall-countdown')->assertSuccessful();
    Event::assertNotDispatched(DrawCountdownBroadcast::class);

    Event::fake([DrawCountdownBroadcast::class]);
    $draw->forceFill([
        'status' => DrawStatus::Closing->value,
        'close_time' => Carbon::parse('2026-05-09 12:00:00', 'UTC'),
    ])->save();

    Carbon::setTestNow(Carbon::parse('2026-05-09 12:00:02', 'UTC'));
    $this->artisan('lottery:hall-countdown')->assertSuccessful();
    Event::assertDispatched(
        DrawCountdownBroadcast::class,
        fn (DrawCountdownBroadcast $event): bool => ($event->data['status'] ?? null) === DrawStatus::Closing->value,
    );

    Carbon::setTestNow();
});

test('hall snapshot skips stale pending draw and picks next upcoming row', function (): void {
    Carbon::setTestNow(Carbon::parse('2026-05-25 18:00:00', 'UTC'));

    Draw::query()->create([
        'draw_no' => '20260525-999',
        'business_date' => '2026-05-25',
        'sequence_no' => 999,
        'status' => DrawStatus::Pending->value,
        'start_time' => Carbon::parse('2026-05-25 17:32:00', 'UTC'),
        'close_time' => Carbon::parse('2026-05-25 17:36:30', 'UTC'),
        'draw_time' => Carbon::parse('2026-05-25 17:37:00', 'UTC'),
        'cooling_end_time' => null,
        'result_source' => null,
        'current_result_version' => 0,
        'settle_version' => 0,
        'is_reopened' => false,
    ]);

    Draw::query()->create([
        'draw_no' => '20260525-1006',
        'business_date' => '2026-05-25',
        'sequence_no' => 1006,
        'status' => DrawStatus::Pending->value,
        'start_time' => Carbon::parse('2026-05-25 18:07:00', 'UTC'),
        'close_time' => Carbon::parse('2026-05-25 18:11:30', 'UTC'),
        'draw_time' => Carbon::parse('2026-05-25 18:12:00', 'UTC'),
        'cooling_end_time' => null,
        'result_source' => null,
        'current_result_version' => 0,
        'settle_version' => 0,
        'is_reopened' => false,
    ]);

    $target = app(DrawHallSnapshotBuilder::class)->resolveHallTarget(now()->utc());
    expect($target)->not->toBeNull()
        ->and($target->draw_no)->toBe('20260525-1006');

    $payload = app(DrawHallSnapshotBuilder::class)->build(now()->utc());
    expect($payload['draw_no'])->toBe('20260525-1006')
        ->and($payload['seconds_to_start'])->toBeGreaterThan(0);

    Carbon::setTestNow();
});

test('hall snapshot switches to next bettable draw when cooldown ended', function (): void {
    Carbon::setTestNow(Carbon::parse('2026-05-10 12:00:00', 'UTC'));

    Draw::query()->create([
        'draw_no' => '20260510-001',
        'business_date' => '2026-05-10',
        'sequence_no' => 1,
        'status' => DrawStatus::Cooldown->value,
        'start_time' => now()->subHours(3),
        'close_time' => now()->subHours(2),
        'draw_time' => now()->subHours(2),
        'cooling_end_time' => now()->subMinutes(5),
        'result_source' => 'rng',
        'current_result_version' => 1,
        'settle_version' => 0,
        'is_reopened' => false,
    ]);

    $next = Draw::query()->create([
        'draw_no' => '20260510-002',
        'business_date' => '2026-05-10',
        'sequence_no' => 2,
        'status' => DrawStatus::Pending->value,
        'start_time' => now()->subMinutes(1),
        'close_time' => now()->addMinutes(20),
        'draw_time' => now()->addMinutes(25),
        'cooling_end_time' => null,
        'result_source' => null,
        'current_result_version' => 0,
        'settle_version' => 0,
        'is_reopened' => false,
    ]);

    $target = app(DrawHallSnapshotBuilder::class)->resolveHallTarget(now()->utc());
    expect($target)->not->toBeNull()
        ->and($target->draw_no)->toBe('20260510-002');

    $payload = app(DrawHallSnapshotBuilder::class)->build(now()->utc());
    expect($payload['status'] ?? null)->toBe('open');

    Carbon::setTestNow();
});

test('hall snapshot reuses cached heavy fragments between second-level countdown builds', function (): void {
    Carbon::setTestNow(Carbon::parse('2026-05-09 12:00:00', 'UTC'));

    $draw = Draw::query()->create([
        'draw_no' => '20260509-001',
        'business_date' => '2026-05-09',
        'sequence_no' => 1,
        'status' => DrawStatus::Cooldown->value,
        'start_time' => now()->subMinutes(5),
        'close_time' => now()->subMinute(),
        'draw_time' => now()->subSeconds(30),
        'cooling_end_time' => now()->addMinutes(5),
        'result_source' => 'rng',
        'current_result_version' => 1,
        'settle_version' => 0,
        'is_reopened' => false,
    ]);

    RiskPool::query()->create([
        'draw_id' => $draw->id,
        'normalized_number' => '1234',
        'total_cap_amount' => 100_000,
        'locked_amount' => 90_000,
        'sold_out_status' => 0,
    ]);

    $batch = DrawResultBatch::query()->create([
        'draw_id' => $draw->id,
        'result_version' => 1,
        'source_type' => 'rng',
        'rng_seed_hash' => hash('sha256', 'fixture'),
        'raw_seed_encrypted' => null,
        'status' => DrawResultBatchStatus::Published->value,
        'created_by' => null,
        'confirmed_by' => null,
        'confirmed_at' => now(),
    ]);

    DrawResultItem::query()->create([
        'draw_id' => $draw->id,
        'result_batch_id' => $batch->id,
        'prize_type' => 'first',
        'prize_index' => 0,
        'number_4d' => '1234',
        'suffix_3d' => '234',
        'suffix_2d' => '34',
        'head_digit' => 1,
        'tail_digit' => 4,
    ]);

    DB::enableQueryLog();
    app(DrawHallSnapshotBuilder::class)->build(now()->utc());
    $firstQueries = DB::getQueryLog();

    DB::flushQueryLog();
    Carbon::setTestNow(Carbon::parse('2026-05-09 12:00:01', 'UTC'));
    $payload = app(DrawHallSnapshotBuilder::class)->build(now()->utc());
    $secondQueries = DB::getQueryLog();
    DB::disableQueryLog();

    $secondSql = collect($secondQueries)->pluck('query')->implode("\n");

    expect($payload['draw_no'])->toBe('20260509-001')
        ->and($payload['result_items'][0]['number_4d'] ?? null)->toBe('1234')
        ->and($payload['risk_pool_alerts'][0]['normalized_number'] ?? null)->toBe('1234')
        ->and(count($secondQueries))->toBeLessThan(count($firstQueries))
        ->and($secondSql)->not->toContain('jackpot_pools')
        ->and($secondSql)->not->toContain('risk_pools')
        ->and($secondSql)->not->toContain('draw_result_batches')
        ->and($secondSql)->not->toContain('draw_result_items');

    Carbon::setTestNow();
});

test('draw tick dispatches draw.status_change when hall draw_no or status changes', function (): void {
    Event::fake([DrawStatusChangeBroadcast::class]);
    config([
        'broadcasting.default' => 'reverb',
        'broadcasting.connections.reverb.driver' => 'reverb',
    ]);

    Carbon::setTestNow(Carbon::parse('2026-05-09 14:00:00', 'UTC'));

    $drawTime = now()->copy()->addMinutes(30);
    $closeTime = now()->copy()->subMinute();

    Draw::query()->create([
        'draw_no' => '20260509-099',
        'business_date' => '2026-05-09',
        'sequence_no' => 99,
        'status' => DrawStatus::Open->value,
        'start_time' => $closeTime->copy()->subMinutes(50),
        'close_time' => $closeTime,
        'draw_time' => $drawTime,
        'cooling_end_time' => null,
        'result_source' => null,
        'current_result_version' => 0,
        'settle_version' => 0,
        'is_reopened' => false,
    ]);

    app(DrawTickService::class)->tick(now()->utc());

    Event::assertDispatched(DrawStatusChangeBroadcast::class);

    Carbon::setTestNow();
});
