<?php

/**
 * 钱包划转场景：鉴权、幂等、转入/转出成功·失败·处理中（与 PRD / MainSiteWalletGateway 行为对齐）。
 */

use Firebase\JWT\JWT;
use App\Models\Player;
use App\Models\WalletTxn;
use App\Lottery\ErrorCode;
use App\Models\PlayerWallet;
use App\Models\TransferOrder;
use Database\Seeders\CurrencySeeder;
use Illuminate\Support\Facades\Http;
use Database\Seeders\LotterySettingsSeeder;
use Illuminate\Foundation\Testing\RefreshDatabase;

uses(RefreshDatabase::class);

beforeEach(function (): void {
    config(['lottery.main_site.wallet_api_url' => null]);
    $this->seed(CurrencySeeder::class);
    $this->seed(LotterySettingsSeeder::class);
});

// ——— 鉴权 ———

test('wallet transfer-in without bearer returns 8001', function (): void {
    $this->postJson('/api/v1/wallet/transfer-in', [
        'amount' => 100,
        'idempotent_key' => 'no-auth',
    ])
        ->assertStatus(401)
        ->assertJsonPath('code', ErrorCode::PlayerAuthorizationInvalid->value);
});

test('wallet transfer-out with empty bearer token returns 8001', function (): void {
    $this->withHeader('Authorization', 'Bearer ')
        ->postJson('/api/v1/wallet/transfer-out', [
            'amount' => 100,
            'idempotent_key' => 'empty-bearer',
        ])
        ->assertStatus(401)
        ->assertJsonPath('code', ErrorCode::PlayerAuthorizationInvalid->value);
});

test('wallet transfer-in rejects expired jwt when dev bypass is off', function (): void {
    config(['lottery.player_auth.dev_bypass' => false]);
    config(['lottery.main_site.sso_jwt_secret' => 'unit-test-jwt-secret-for-expiry']);

    $token = JWT::encode([
        'site_code' => 'main',
        'site_player_id' => 'expired-jwt-user',
        'iat' => now()->subHours(2)->timestamp,
        'exp' => now()->subMinute()->timestamp,
    ], 'unit-test-jwt-secret-for-expiry', 'HS256');

    $this->withHeader('Authorization', 'Bearer '.$token)
        ->postJson('/api/v1/wallet/transfer-in', [
            'amount' => 100,
            'idempotent_key' => 'jwt-exp',
        ])
        ->assertStatus(401)
        ->assertJsonPath('code', ErrorCode::PlayerTokenInvalid->value);
});

test('wallet transfer-in dev token for missing player returns 8003', function (): void {
    $missingId = 9_999_999;

    expect(Player::query()->find($missingId))->toBeNull();

    $this->withHeader('Authorization', 'Bearer dev:'.$missingId)
        ->postJson('/api/v1/wallet/transfer-in', [
            'amount' => 100,
            'idempotent_key' => 'no-player',
        ])
        ->assertStatus(401)
        ->assertJsonPath('code', ErrorCode::PlayerNotRegistered->value);
});

// ——— 转入：成功 / 失败 / 处理中 ———

test('transfer in succeeds with stub main site', function (): void {
    $player = Player::query()->create([
        'site_code' => 'main',
        'site_player_id' => 'in-ok',
        'username' => null,
        'nickname' => null,
        'default_currency' => 'NPR',
        'status' => 0,
    ]);

    $key = 'in-ok-'.uniqid('', true);

    $this->withHeader('Authorization', 'Bearer dev:'.$player->id)
        ->postJson('/api/v1/wallet/transfer-in', [
            'amount' => 500,
            'currency' => 'NPR',
            'idempotent_key' => $key,
        ])
        ->assertOk()
        ->assertJsonPath('code', ErrorCode::Success->value)
        ->assertJsonPath('data.status', 'success');

    $order = TransferOrder::query()->where('idempotent_key', $key)->first();
    expect($order?->status)->toBe('success');
    expect(WalletTxn::query()->where('player_id', $player->id)->where('biz_type', 'transfer_in')->count())->toBe(1);
});

test('transfer in main site explicit failure returns 1009 and marks order failed', function (): void {
    Http::fake([
        'reject-debit.test/*' => Http::response(['success' => false, 'message' => 'main_insufficient'], 200),
    ]);
    config(['lottery.main_site.wallet_api_url' => 'https://reject-debit.test']);
    config(['lottery.main_site.wallet_debit_path' => 'debit']);

    $player = Player::query()->create([
        'site_code' => 'main',
        'site_player_id' => 'in-fail',
        'username' => null,
        'nickname' => null,
        'default_currency' => 'NPR',
        'status' => 0,
    ]);

    $key = 'in-fail-'.uniqid('', true);

    $this->withHeader('Authorization', 'Bearer dev:'.$player->id)
        ->postJson('/api/v1/wallet/transfer-in', [
            'amount' => 500,
            'currency' => 'NPR',
            'idempotent_key' => $key,
        ])
        ->assertStatus(400)
        ->assertJsonPath('code', ErrorCode::WalletExternalRejected->value);

    $order = TransferOrder::query()->where('idempotent_key', $key)->first();
    expect($order?->status)->toBe('failed')
        ->and(WalletTxn::query()->where('player_id', $player->id)->count())->toBe(0);
});

test('transfer in main site timeout returns 1002 and pending_reconcile', function (): void {
    Http::fake([
        'timeout-debit.test/*' => Http::response([], 504),
    ]);
    config(['lottery.main_site.wallet_api_url' => 'https://timeout-debit.test']);
    config(['lottery.main_site.wallet_debit_path' => 'debit']);

    $player = Player::query()->create([
        'site_code' => 'main',
        'site_player_id' => 'in-pend',
        'username' => null,
        'nickname' => null,
        'default_currency' => 'NPR',
        'status' => 0,
    ]);

    $key = 'in-pend-'.uniqid('', true);

    $this->withHeader('Authorization', 'Bearer dev:'.$player->id)
        ->postJson('/api/v1/wallet/transfer-in', [
            'amount' => 500,
            'currency' => 'NPR',
            'idempotent_key' => $key,
        ])
        ->assertStatus(409)
        ->assertJsonPath('code', ErrorCode::WalletTransferPending->value);

    expect(TransferOrder::query()->where('idempotent_key', $key)->first()?->status)->toBe('pending_reconcile');
});

// ——— 转出：成功 / 失败 / 处理中 ———

test('transfer out succeeds with stub main site credit', function (): void {
    $player = Player::query()->create([
        'site_code' => 'main',
        'site_player_id' => 'out-ok',
        'username' => null,
        'nickname' => null,
        'default_currency' => 'NPR',
        'status' => 0,
    ]);

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

    $key = 'out-ok-'.uniqid('', true);

    $this->withHeader('Authorization', 'Bearer dev:'.$player->id)
        ->postJson('/api/v1/wallet/transfer-out', [
            'amount' => 300,
            'idempotent_key' => $key,
        ])
        ->assertOk()
        ->assertJsonPath('code', ErrorCode::Success->value)
        ->assertJsonPath('data.status', 'success')
        ->assertJsonPath('data.lottery_balance_after', 500);

    expect(TransferOrder::query()->where('idempotent_key', $key)->first()?->status)->toBe('success');
});

test('transfer out main site failure refunds lottery and returns 1009', function (): void {
    Http::fake([
        'reject-credit.test/*' => Http::response(['success' => false, 'message' => 'credit_denied'], 200),
    ]);
    config(['lottery.main_site.wallet_api_url' => 'https://reject-credit.test']);
    config(['lottery.main_site.wallet_credit_path' => 'credit']);

    $player = Player::query()->create([
        'site_code' => 'main',
        'site_player_id' => 'out-fail',
        'username' => null,
        'nickname' => null,
        'default_currency' => 'NPR',
        'status' => 0,
    ]);

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

    $key = 'out-fail-'.uniqid('', true);

    $this->withHeader('Authorization', 'Bearer dev:'.$player->id)
        ->postJson('/api/v1/wallet/transfer-out', [
            'amount' => 200,
            'idempotent_key' => $key,
        ])
        ->assertStatus(400)
        ->assertJsonPath('code', ErrorCode::WalletExternalRejected->value);

    expect((int) PlayerWallet::query()->where('player_id', $player->id)->first()?->balance)->toBe(1000);
    expect(TransferOrder::query()->where('idempotent_key', $key)->first()?->status)->toBe('failed');
    expect(WalletTxn::query()->where('player_id', $player->id)->where('biz_type', 'transfer_out_refund')->count())->toBe(1);
});

test('transfer out main site timeout returns 1002 and pending_reconcile on order and txn', function (): void {
    Http::fake([
        'timeout-credit.test/*' => Http::response([], 504),
    ]);
    config(['lottery.main_site.wallet_api_url' => 'https://timeout-credit.test']);
    config(['lottery.main_site.wallet_credit_path' => 'credit']);

    $player = Player::query()->create([
        'site_code' => 'main',
        'site_player_id' => 'out-pend',
        'username' => null,
        'nickname' => null,
        'default_currency' => 'NPR',
        'status' => 0,
    ]);

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

    $key = 'out-pend-'.uniqid('', true);

    $this->withHeader('Authorization', 'Bearer dev:'.$player->id)
        ->postJson('/api/v1/wallet/transfer-out', [
            'amount' => 200,
            'idempotent_key' => $key,
        ])
        ->assertStatus(409)
        ->assertJsonPath('code', ErrorCode::WalletTransferPending->value);

    $order = TransferOrder::query()->where('idempotent_key', $key)->first();
    expect($order?->status)->toBe('pending_reconcile');

    $outTxn = WalletTxn::query()
        ->where('player_id', $player->id)
        ->where('biz_type', 'transfer_out')
        ->latest('id')
        ->first();
    expect($outTxn?->status)->toBe('pending_reconcile');
    expect((int) PlayerWallet::query()->where('player_id', $player->id)->first()?->balance)->toBe(400);
});

test('transfer out insufficient balance returns 1001 failed order', function (): void {
    $player = Player::query()->create([
        'site_code' => 'main',
        'site_player_id' => 'out-poor',
        'username' => null,
        'nickname' => null,
        'default_currency' => 'NPR',
        'status' => 0,
    ]);

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

    $key = 'out-broke-key';

    $this->withHeader('Authorization', 'Bearer dev:'.$player->id)
        ->postJson('/api/v1/wallet/transfer-out', [
            'amount' => 300,
            'idempotent_key' => $key,
        ])
        ->assertStatus(400)
        ->assertJsonPath('code', ErrorCode::WalletInsufficientBalance->value);

    expect(TransferOrder::query()->where('idempotent_key', $key)->first()?->status)->toBe('failed')
        ->and(TransferOrder::query()->where('idempotent_key', $key)->first()?->fail_reason)->toBe('insufficient_balance');
});

// ——— 幂等 ———

test('transfer in idempotent replay returns same transfer_no and single wallet credit', function (): void {
    $player = Player::query()->create([
        'site_code' => 'main',
        'site_player_id' => 'idem-in',
        'username' => null,
        'nickname' => null,
        'default_currency' => 'NPR',
        'status' => 0,
    ]);

    $key = 'idem-in-replay-key';

    $first = $this->withHeader('Authorization', 'Bearer dev:'.$player->id)
        ->postJson('/api/v1/wallet/transfer-in', [
            'amount' => 150,
            'idempotent_key' => $key,
        ]);
    $second = $this->withHeader('Authorization', 'Bearer dev:'.$player->id)
        ->postJson('/api/v1/wallet/transfer-in', [
            'amount' => 150,
            'idempotent_key' => $key,
        ]);

    $first->assertOk();
    $second->assertOk();

    expect((string) $first->json('data.transfer_no'))->toBe((string) $second->json('data.transfer_no'));
    expect(TransferOrder::query()->where('idempotent_key', $key)->count())->toBe(1);
    expect((int) PlayerWallet::query()->where('player_id', $player->id)->first()?->balance)->toBe(150);
});

test('transfer out idempotent replay returns same transfer_no', function (): void {
    $player = Player::query()->create([
        'site_code' => 'main',
        'site_player_id' => 'idem-out',
        'username' => null,
        'nickname' => null,
        'default_currency' => 'NPR',
        'status' => 0,
    ]);

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

    $key = 'idem-out-replay';

    $first = $this->withHeader('Authorization', 'Bearer dev:'.$player->id)
        ->postJson('/api/v1/wallet/transfer-out', [
            'amount' => 100,
            'idempotent_key' => $key,
        ]);
    $second = $this->withHeader('Authorization', 'Bearer dev:'.$player->id)
        ->postJson('/api/v1/wallet/transfer-out', [
            'amount' => 100,
            'idempotent_key' => $key,
        ]);

    $first->assertOk();
    $second->assertOk();

    expect((string) $first->json('data.transfer_no'))->toBe((string) $second->json('data.transfer_no'));
    expect((int) PlayerWallet::query()->where('player_id', $player->id)->first()?->balance)->toBe(800);
});

test('idempotent key reused with different amount returns 1010', function (): void {
    $player = Player::query()->create([
        'site_code' => 'main',
        'site_player_id' => 'idem-conflict',
        'username' => null,
        'nickname' => null,
        'default_currency' => 'NPR',
        'status' => 0,
    ]);

    $key = 'same-key-diff-amount';

    $this->withHeader('Authorization', 'Bearer dev:'.$player->id)
        ->postJson('/api/v1/wallet/transfer-in', [
            'amount' => 100,
            'idempotent_key' => $key,
        ])
        ->assertOk();

    $this->withHeader('Authorization', 'Bearer dev:'.$player->id)
        ->postJson('/api/v1/wallet/transfer-in', [
            'amount' => 200,
            'idempotent_key' => $key,
        ])
        ->assertStatus(400)
        ->assertJsonPath('code', ErrorCode::WalletIdempotentConflict->value);

    expect((int) PlayerWallet::query()->where('player_id', $player->id)->first()?->balance)->toBe(100);
});

test('replay while order still processing returns 1002', function (): void {
    $player = Player::query()->create([
        'site_code' => 'main',
        'site_player_id' => 'proc-replay',
        'username' => null,
        'nickname' => null,
        'default_currency' => 'NPR',
        'status' => 0,
    ]);

    TransferOrder::query()->create([
        'transfer_no' => 'TI_manual_proc',
        'player_id' => $player->id,
        'direction' => 'in',
        'currency_code' => 'NPR',
        'amount' => 100,
        'idempotent_key' => 'stuck-processing',
        'status' => 'processing',
    ]);

    $this->withHeader('Authorization', 'Bearer dev:'.$player->id)
        ->postJson('/api/v1/wallet/transfer-in', [
            'amount' => 100,
            'idempotent_key' => 'stuck-processing',
        ])
        ->assertStatus(409)
        ->assertJsonPath('code', ErrorCode::WalletTransferPending->value);
});

test('transfer in replay while pending_reconcile stays pending without wallet credit', function (): void {
    Http::fake([
        'timeout-debit-replay.test/*' => Http::response([], 504),
    ]);
    config(['lottery.main_site.wallet_api_url' => 'https://timeout-debit-replay.test']);
    config(['lottery.main_site.wallet_debit_path' => 'debit']);

    $player = Player::query()->create([
        'site_code' => 'main',
        'site_player_id' => 'in-pending-replay',
        'username' => null,
        'nickname' => null,
        'default_currency' => 'NPR',
        'status' => 0,
    ]);

    $key = 'in-pending-replay-key';
    $payload = [
        'amount' => 500,
        'currency' => 'NPR',
        'idempotent_key' => $key,
    ];

    $this->withHeader('Authorization', 'Bearer dev:'.$player->id)
        ->postJson('/api/v1/wallet/transfer-in', $payload)
        ->assertStatus(409)
        ->assertJsonPath('code', ErrorCode::WalletTransferPending->value);

    $this->withHeader('Authorization', 'Bearer dev:'.$player->id)
        ->postJson('/api/v1/wallet/transfer-in', $payload)
        ->assertStatus(409)
        ->assertJsonPath('code', ErrorCode::WalletTransferPending->value);

    expect(TransferOrder::query()->where('idempotent_key', $key)->count())->toBe(1)
        ->and(TransferOrder::query()->where('idempotent_key', $key)->value('status'))->toBe('pending_reconcile')
        ->and(WalletTxn::query()->where('biz_type', 'transfer_in')->count())->toBe(0);
});

test('transfer out replay while pending_reconcile keeps single pending txn', function (): void {
    Http::fake([
        'timeout-credit-replay.test/*' => Http::response([], 504),
    ]);
    config(['lottery.main_site.wallet_api_url' => 'https://timeout-credit-replay.test']);
    config(['lottery.main_site.wallet_credit_path' => 'credit']);

    $player = Player::query()->create([
        'site_code' => 'main',
        'site_player_id' => 'out-pending-replay',
        'username' => null,
        'nickname' => null,
        'default_currency' => 'NPR',
        'status' => 0,
    ]);
    PlayerWallet::query()->create([
        'player_id' => $player->id,
        'wallet_type' => 'lottery',
        'currency_code' => 'NPR',
        'balance' => 1_000,
        'frozen_balance' => 0,
        'status' => 0,
        'version' => 0,
    ]);

    $key = 'out-pending-replay-key';
    $payload = [
        'amount' => 200,
        'idempotent_key' => $key,
    ];

    $this->withHeader('Authorization', 'Bearer dev:'.$player->id)
        ->postJson('/api/v1/wallet/transfer-out', $payload)
        ->assertStatus(409)
        ->assertJsonPath('code', ErrorCode::WalletTransferPending->value);

    $this->withHeader('Authorization', 'Bearer dev:'.$player->id)
        ->postJson('/api/v1/wallet/transfer-out', $payload)
        ->assertStatus(409)
        ->assertJsonPath('code', ErrorCode::WalletTransferPending->value);

    expect(TransferOrder::query()->where('idempotent_key', $key)->count())->toBe(1)
        ->and(TransferOrder::query()->where('idempotent_key', $key)->value('status'))->toBe('pending_reconcile')
        ->and(WalletTxn::query()->where('biz_type', 'transfer_out')->count())->toBe(1)
        ->and(WalletTxn::query()->where('biz_type', 'transfer_out')->value('status'))->toBe('pending_reconcile')
        ->and((int) PlayerWallet::query()->where('player_id', $player->id)->value('balance'))->toBe(800);
});
