<?php

use App\Models\Draw;
use App\Models\Player;
use App\Models\AdminUser;
use App\Models\TicketItem;
use App\Lottery\DrawStatus;
use App\Models\TicketOrder;
use App\Models\PlayerWallet;
use App\Models\DrawResultItem;
use App\Models\DrawResultBatch;
use App\Models\SettlementBatch;
use App\Models\TicketCombination;
use App\Services\LotterySettings;
use Illuminate\Support\Facades\Hash;
use App\Lottery\DrawResultBatchStatus;
use App\Lottery\SettlementBatchStatus;
use App\Services\Draw\DrawPrizeLayout;
use Illuminate\Foundation\Testing\RefreshDatabase;
use App\Services\Settlement\SettlementOrchestrator;
use App\Services\Settlement\SettlementTickFinalizer;
use App\Services\Settlement\SettlementBatchWorkflowService;

uses(RefreshDatabase::class);

function createChunkTestDraw(string $suffix): Draw
{
    return Draw::query()->create([
        'draw_no' => 'CHUNK-'.$suffix,
        'business_date' => '2026-07-22',
        'sequence_no' => random_int(1000, 9999),
        'status' => DrawStatus::Settling->value,
        'start_time' => now()->subMinutes(10),
        'close_time' => now()->subMinutes(2),
        'draw_time' => now()->subMinute(),
        'current_result_version' => 1,
        'settle_version' => 0,
        'is_reopened' => false,
    ]);
}

function createChunkTestPlayer(string $suffix): Player
{
    $player = Player::query()->create([
        'site_code' => 'test',
        'site_player_id' => 'chunk-'.$suffix,
        'username' => 'chunk_'.$suffix,
        'default_currency' => 'NPR',
        'status' => 0,
    ]);
    PlayerWallet::query()->create([
        'player_id' => $player->id,
        'wallet_type' => 'lottery',
        'currency_code' => 'NPR',
        'balance' => 0,
        'frozen_balance' => 0,
        'status' => 0,
        'version' => 0,
    ]);

    return $player;
}

function createChunkTestResult(Draw $draw, string $providerCode): DrawResultBatch
{
    $batch = DrawResultBatch::query()->create([
        'draw_id' => $draw->id,
        'provider_code' => $providerCode,
        'provider_name' => $providerCode,
        'result_version' => 1,
        'source_type' => 'manual',
        'status' => DrawResultBatchStatus::Published->value,
        'confirmed_at' => now(),
    ]);

    foreach (DrawPrizeLayout::slots() as $slot) {
        $number = $slot['prize_type'] === 'first' ? '1234' : '5678';
        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) $number[0],
            'tail_digit' => (int) $number[3],
        ]);
    }

    return $batch;
}

function createChunkTestTicket(
    Draw $draw,
    Player $player,
    TicketOrder $order,
    string $providerCode,
    string $suffix,
): TicketItem {
    $item = TicketItem::query()->create([
        'ticket_no' => 'CHUNK-TICKET-'.$suffix,
        'order_id' => $order->id,
        'player_id' => $player->id,
        'draw_id' => $draw->id,
        'provider_code' => $providerCode,
        'provider_name' => $providerCode,
        'original_number' => '1234',
        'normalized_number' => '1234',
        'play_code' => 'pos_4a',
        'bet_mode' => 'unit',
        'unit_bet_amount' => 10_000,
        'total_bet_amount' => 10_000,
        'actual_deduct_amount' => 10_000,
        'odds_snapshot_json' => [['prize_scope' => 'first', 'odds_value' => 100_000]],
        'rule_snapshot_json' => [],
        'combination_count' => 1,
        'estimated_max_payout' => 100_000,
        'risk_locked_amount' => 0,
        'status' => 'pending_draw',
        'win_amount' => 0,
        'jackpot_win_amount' => 0,
    ]);
    TicketCombination::query()->create([
        'ticket_item_id' => $item->id,
        'combination_no' => 1,
        'number_4d' => '1234',
        'bet_amount' => 10_000,
        'estimated_payout' => 100_000,
    ]);

    return $item;
}

function createChunkTestOrder(Draw $draw, Player $player, string $suffix, int $ticketCount): TicketOrder
{
    return TicketOrder::query()->create([
        'order_no' => 'CHUNK-ORDER-'.$suffix,
        'player_id' => $player->id,
        'draw_id' => $draw->id,
        'currency_code' => 'NPR',
        'total_bet_amount' => 10_000 * $ticketCount,
        'total_rebate_amount' => 0,
        'total_actual_deduct' => 10_000 * $ticketCount,
        'total_estimated_payout' => 100_000 * $ticketCount,
        'status' => 'placed',
        'submit_source' => 'test',
        'client_trace_id' => 'chunk-trace-'.$suffix,
    ]);
}

test('settlement processes every ticket chunk into one provider batch', function (): void {
    config()->set('lottery.settlement.ticket_chunk_size', 2);
    $suffix = bin2hex(random_bytes(4));
    $draw = createChunkTestDraw($suffix);
    $player = createChunkTestPlayer($suffix);
    $order = createChunkTestOrder($draw, $player, $suffix, 3);
    createChunkTestResult($draw, 'SG');

    for ($i = 1; $i <= 3; $i++) {
        createChunkTestTicket($draw, $player, $order, 'SG', $suffix.'-'.$i);
    }

    expect(app(SettlementOrchestrator::class)->trySettleDraw($draw))->toBeTrue();

    $batch = SettlementBatch::query()->where('draw_id', $draw->id)->firstOrFail();
    expect(SettlementBatch::query()->where('draw_id', $draw->id)->count())->toBe(1)
        ->and($batch->status)->toBe(SettlementBatchStatus::PendingReview->value)
        ->and((int) $batch->total_ticket_count)->toBe(3)
        ->and((int) $batch->total_win_count)->toBe(3)
        ->and((int) $batch->total_payout_amount)->toBe(300_000)
        ->and($batch->details()->count())->toBe(3)
        ->and(TicketItem::query()->where('draw_id', $draw->id)->where('status', 'pending_draw')->exists())->toBeFalse();
});

test('settlement resumes a legacy partial provider batch instead of skipping remaining tickets', function (): void {
    config()->set('lottery.settlement.ticket_chunk_size', 2);
    $suffix = bin2hex(random_bytes(4));
    $draw = createChunkTestDraw($suffix);
    $player = createChunkTestPlayer($suffix);
    $order = createChunkTestOrder($draw, $player, $suffix, 3);
    createChunkTestResult($draw, 'SG');
    createChunkTestTicket($draw, $player, $order, 'SG', $suffix.'-1');
    createChunkTestTicket($draw, $player, $order, 'SG', $suffix.'-2');

    app(SettlementOrchestrator::class)->trySettleDraw($draw);
    $existingBatch = SettlementBatch::query()->where('draw_id', $draw->id)->firstOrFail();

    createChunkTestTicket($draw, $player, $order, 'SG', $suffix.'-3');
    expect(app(SettlementOrchestrator::class)->trySettleDraw($draw->fresh()))->toBeTrue();

    $resumedBatch = SettlementBatch::query()->where('draw_id', $draw->id)->firstOrFail();
    expect((int) $resumedBatch->id)->toBe((int) $existingBatch->id)
        ->and(SettlementBatch::query()->where('draw_id', $draw->id)->count())->toBe(1)
        ->and($resumedBatch->status)->toBe(SettlementBatchStatus::PendingReview->value)
        ->and((int) $resumedBatch->total_ticket_count)->toBe(3)
        ->and($resumedBatch->details()->count())->toBe(3)
        ->and(TicketItem::query()->where('draw_id', $draw->id)->where('status', 'pending_draw')->exists())->toBeFalse();
});

test('approved provider batches payout independently and settle draw after the last provider', function (): void {
    config()->set('lottery.settlement.ticket_chunk_size', 1);
    $suffix = bin2hex(random_bytes(4));
    $draw = createChunkTestDraw($suffix);
    $player = createChunkTestPlayer($suffix);
    $order = createChunkTestOrder($draw, $player, $suffix, 2);

    foreach (['SG', 'MY'] as $providerCode) {
        createChunkTestResult($draw, $providerCode);
        createChunkTestTicket($draw, $player, $order, $providerCode, $suffix.'-'.$providerCode);
    }

    expect(app(SettlementOrchestrator::class)->trySettleDraw($draw))->toBeTrue();

    $batches = SettlementBatch::query()->where('draw_id', $draw->id)->orderBy('id')->get();
    expect($batches)->toHaveCount(2)
        ->and($batches->every(fn (SettlementBatch $batch): bool => $batch->status === SettlementBatchStatus::PendingReview->value))->toBeTrue();

    $admin = AdminUser::query()->create([
        'username' => 'chunk_reviewer_'.$suffix,
        'name' => 'Chunk Reviewer',
        'password' => Hash::make('secret-strong'),
        'status' => 0,
    ]);
    $workflow = app(SettlementBatchWorkflowService::class);
    foreach ($batches as $batch) {
        $workflow->approve($batch, $admin);
    }

    expect(app(SettlementOrchestrator::class)->trySettleDraw($draw->fresh()))->toBeTrue()
        ->and(SettlementBatch::query()->where('draw_id', $draw->id)->where('status', SettlementBatchStatus::Approved->value)->count())->toBe(2);

    $workflow->payout($batches[0]->fresh());
    expect($draw->fresh()->status)->toBe(DrawStatus::Settling->value)
        ->and(SettlementBatch::query()->whereKey($batches[0]->id)->value('status'))->toBe(SettlementBatchStatus::Paid->value)
        ->and(TicketItem::query()->where('draw_id', $draw->id)->where('status', 'pending_payout')->count())->toBe(1);

    $workflow->payout($batches[1]->fresh());
    expect($draw->fresh()->status)->toBe(DrawStatus::Settled->value)
        ->and(SettlementBatch::query()->where('draw_id', $draw->id)->where('status', SettlementBatchStatus::Paid->value)->count())->toBe(2)
        ->and(TicketItem::query()->where('draw_id', $draw->id)->where('status', 'settled_win')->count())->toBe(2)
        ->and($order->fresh()->status)->toBe('settled');
});

test('tick finalizer approves all provider batches before paying them', function (): void {
    $suffix = bin2hex(random_bytes(4));
    $draw = createChunkTestDraw($suffix);
    $player = createChunkTestPlayer($suffix);
    $order = createChunkTestOrder($draw, $player, $suffix, 2);

    foreach (['SG', 'MY'] as $providerCode) {
        createChunkTestResult($draw, $providerCode);
        createChunkTestTicket($draw, $player, $order, $providerCode, $suffix.'-'.$providerCode);
    }

    app(SettlementOrchestrator::class)->trySettleDraw($draw);
    $result = app(SettlementTickFinalizer::class)->finalizePendingBatches();

    expect($result)->toMatchArray(['approved' => 2, 'paid' => 2, 'payout_failed' => 0])
        ->and($draw->fresh()->status)->toBe(DrawStatus::Settled->value)
        ->and(SettlementBatch::query()->where('draw_id', $draw->id)->where('status', SettlementBatchStatus::Paid->value)->count())->toBe(2)
        ->and(TicketItem::query()->where('draw_id', $draw->id)->where('status', 'settled_win')->count())->toBe(2);
});

test('tick finalizer resumes approved batches left before payout', function (): void {
    $suffix = bin2hex(random_bytes(4));
    $draw = createChunkTestDraw($suffix);
    $player = createChunkTestPlayer($suffix);
    $order = createChunkTestOrder($draw, $player, $suffix, 2);

    foreach (['SG', 'MY'] as $providerCode) {
        createChunkTestResult($draw, $providerCode);
        createChunkTestTicket($draw, $player, $order, $providerCode, $suffix.'-'.$providerCode);
    }

    app(SettlementOrchestrator::class)->trySettleDraw($draw);
    $workflow = app(SettlementBatchWorkflowService::class);
    foreach (SettlementBatch::query()->where('draw_id', $draw->id)->get() as $batch) {
        $workflow->approveBySystem($batch, 'simulate restart after approval');
    }

    expect(SettlementBatch::query()->where('draw_id', $draw->id)->where('status', SettlementBatchStatus::PendingReview->value)->exists())->toBeFalse()
        ->and(SettlementBatch::query()->where('draw_id', $draw->id)->where('status', SettlementBatchStatus::Approved->value)->count())->toBe(2);

    $result = app(SettlementTickFinalizer::class)->finalizePendingBatches();

    expect($result)->toMatchArray(['approved' => 0, 'paid' => 2, 'payout_failed' => 0])
        ->and($draw->fresh()->status)->toBe(DrawStatus::Settled->value)
        ->and(SettlementBatch::query()->where('draw_id', $draw->id)->where('status', SettlementBatchStatus::Paid->value)->count())->toBe(2)
        ->and(TicketItem::query()->where('draw_id', $draw->id)->where('status', 'settled_win')->count())->toBe(2);
});

test('blocked approved draw does not starve a later eligible draw at the scan limit', function (): void {
    config()->set('lottery.draw_tick_finalize_limit', 1);

    $blockedSuffix = bin2hex(random_bytes(4));
    $blockedDraw = createChunkTestDraw($blockedSuffix);
    $blockedResult = createChunkTestResult($blockedDraw, 'BLOCKED');
    foreach ([SettlementBatchStatus::Approved, SettlementBatchStatus::Running] as $status) {
        SettlementBatch::query()->create([
            'draw_id' => $blockedDraw->id,
            'result_batch_id' => $blockedResult->id,
            'settle_version' => 1,
            'status' => $status->value,
            'total_ticket_count' => 0,
            'total_win_count' => 0,
            'total_payout_amount' => 0,
            'total_jackpot_payout_amount' => 0,
            'review_status' => $status === SettlementBatchStatus::Approved ? 'approved' : 'pending',
            'reviewed_at' => $status === SettlementBatchStatus::Approved ? now() : null,
            'started_at' => now(),
            'finished_at' => $status === SettlementBatchStatus::Approved ? now() : null,
        ]);
    }

    $eligibleSuffix = bin2hex(random_bytes(4));
    $eligibleDraw = createChunkTestDraw($eligibleSuffix);
    $player = createChunkTestPlayer($eligibleSuffix);
    $order = createChunkTestOrder($eligibleDraw, $player, $eligibleSuffix, 1);
    createChunkTestResult($eligibleDraw, 'SG');
    createChunkTestTicket($eligibleDraw, $player, $order, 'SG', $eligibleSuffix.'-SG');
    app(SettlementOrchestrator::class)->trySettleDraw($eligibleDraw);
    $eligibleBatch = SettlementBatch::query()->where('draw_id', $eligibleDraw->id)->firstOrFail();
    app(SettlementBatchWorkflowService::class)->approveBySystem($eligibleBatch, 'simulate approved backlog');

    $result = app(SettlementTickFinalizer::class)->finalizePendingBatches();

    expect($result)->toMatchArray(['approved' => 0, 'paid' => 1, 'payout_failed' => 0])
        ->and($eligibleBatch->fresh()->status)->toBe(SettlementBatchStatus::Paid->value)
        ->and($eligibleDraw->fresh()->status)->toBe(DrawStatus::Settled->value)
        ->and(SettlementBatch::query()->where('draw_id', $blockedDraw->id)->where('status', SettlementBatchStatus::Approved->value)->count())->toBe(1);
});

test('manual approval still allows automatic payout when auto approval is disabled', function (): void {
    $approvedSuffix = bin2hex(random_bytes(4));
    $approvedDraw = createChunkTestDraw($approvedSuffix);
    $approvedPlayer = createChunkTestPlayer($approvedSuffix);
    $approvedOrder = createChunkTestOrder($approvedDraw, $approvedPlayer, $approvedSuffix, 1);
    createChunkTestResult($approvedDraw, 'SG');
    createChunkTestTicket($approvedDraw, $approvedPlayer, $approvedOrder, 'SG', $approvedSuffix.'-SG');
    app(SettlementOrchestrator::class)->trySettleDraw($approvedDraw);
    $approvedBatch = SettlementBatch::query()->where('draw_id', $approvedDraw->id)->firstOrFail();
    app(SettlementBatchWorkflowService::class)->approveBySystem($approvedBatch, 'manual approval simulation');

    $pendingSuffix = bin2hex(random_bytes(4));
    $pendingDraw = createChunkTestDraw($pendingSuffix);
    $pendingPlayer = createChunkTestPlayer($pendingSuffix);
    $pendingOrder = createChunkTestOrder($pendingDraw, $pendingPlayer, $pendingSuffix, 1);
    createChunkTestResult($pendingDraw, 'MY');
    createChunkTestTicket($pendingDraw, $pendingPlayer, $pendingOrder, 'MY', $pendingSuffix.'-MY');
    app(SettlementOrchestrator::class)->trySettleDraw($pendingDraw);
    $pendingBatch = SettlementBatch::query()->where('draw_id', $pendingDraw->id)->firstOrFail();

    LotterySettings::put('settlement.auto_approve_on_tick', false, 'settlement', 'test manual approval mode');
    LotterySettings::put('settlement.auto_payout_on_tick', true, 'settlement', 'test automatic payout mode');

    $result = app(SettlementTickFinalizer::class)->finalizePendingBatches();

    expect($result)->toMatchArray(['approved' => 0, 'paid' => 1, 'payout_failed' => 0])
        ->and($approvedBatch->fresh()->status)->toBe(SettlementBatchStatus::Paid->value)
        ->and($approvedDraw->fresh()->status)->toBe(DrawStatus::Settled->value)
        ->and($pendingBatch->fresh()->status)->toBe(SettlementBatchStatus::PendingReview->value)
        ->and($pendingDraw->fresh()->status)->toBe(DrawStatus::Settling->value);
});

test('a repeatedly failing approved draw does not starve a later healthy draw', function (): void {
    config()->set('lottery.draw_tick_finalize_limit', 1);

    $poisonSuffix = bin2hex(random_bytes(4));
    $poisonDraw = createChunkTestDraw($poisonSuffix);
    $poisonPlayer = createChunkTestPlayer($poisonSuffix);
    $poisonOrder = createChunkTestOrder($poisonDraw, $poisonPlayer, $poisonSuffix, 1);
    createChunkTestResult($poisonDraw, 'SG');
    createChunkTestTicket($poisonDraw, $poisonPlayer, $poisonOrder, 'SG', $poisonSuffix.'-SG');
    app(SettlementOrchestrator::class)->trySettleDraw($poisonDraw);
    $poisonBatch = SettlementBatch::query()->where('draw_id', $poisonDraw->id)->firstOrFail();
    app(SettlementBatchWorkflowService::class)->approveBySystem($poisonBatch, 'simulate poison batch');
    createChunkTestTicket($poisonDraw, $poisonPlayer, $poisonOrder, 'SG', $poisonSuffix.'-ORPHAN');

    $healthySuffix = bin2hex(random_bytes(4));
    $healthyDraw = createChunkTestDraw($healthySuffix);
    $healthyPlayer = createChunkTestPlayer($healthySuffix);
    $healthyOrder = createChunkTestOrder($healthyDraw, $healthyPlayer, $healthySuffix, 1);
    createChunkTestResult($healthyDraw, 'MY');
    createChunkTestTicket($healthyDraw, $healthyPlayer, $healthyOrder, 'MY', $healthySuffix.'-MY');
    app(SettlementOrchestrator::class)->trySettleDraw($healthyDraw);
    $healthyBatch = SettlementBatch::query()->where('draw_id', $healthyDraw->id)->firstOrFail();
    app(SettlementBatchWorkflowService::class)->approveBySystem($healthyBatch, 'simulate healthy backlog');

    $first = app(SettlementTickFinalizer::class)->finalizePendingBatches();
    expect($first)->toMatchArray(['approved' => 0, 'paid' => 0, 'payout_failed' => 1])
        ->and($poisonBatch->fresh()->status)->toBe(SettlementBatchStatus::Approved->value)
        ->and((int) $poisonBatch->fresh()->auto_payout_attempts)->toBe(1)
        ->and($healthyBatch->fresh()->status)->toBe(SettlementBatchStatus::Approved->value);

    $second = app(SettlementTickFinalizer::class)->finalizePendingBatches();
    expect($second)->toMatchArray(['approved' => 0, 'paid' => 1, 'payout_failed' => 0])
        ->and($healthyBatch->fresh()->status)->toBe(SettlementBatchStatus::Paid->value)
        ->and($healthyDraw->fresh()->status)->toBe(DrawStatus::Settled->value)
        ->and($poisonBatch->fresh()->status)->toBe(SettlementBatchStatus::Approved->value);
});
