<?php

use App\Models\AdminUser;
use App\Models\AgentProfile;
use App\Models\Draw;
use App\Models\Player;
use App\Lottery\DrawStatus;
use App\Services\Agent\AgentNodeService;
use App\Services\AgentSettlement\AgentSettlementPeriodCloseService;
use App\Services\AgentSettlement\SettlementPaymentService;
use App\Support\Settlement\DesignDocExample12;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Hash;

uses(RefreshDatabase::class);

beforeEach(function (): void {
    $this->artisan('lottery:admin-auth-sync')->assertExitCode(0);
});

function createSiteWithRoot(string $code): array
{
    $siteId = (int) DB::table('admin_sites')->insertGetId([
        'code' => $code,
        'name' => $code,
        'is_default' => false,
        'created_at' => now(),
        'updated_at' => now(),
    ]);

    $rootId = (int) DB::table('agent_nodes')->insertGetId([
        'admin_site_id' => $siteId,
        'parent_id' => null,
        'depth' => 0,
        'path' => '/'.$code,
        'code' => $code,
        'name' => 'Root '.$code,
        'status' => 1,
        'created_at' => now(),
        'updated_at' => now(),
    ]);

    AgentProfile::query()->create([
        'agent_node_id' => $rootId,
        'total_share_rate' => 100,
        'credit_limit' => 500_000,
        'allocated_credit' => 0,
        'used_credit' => 0,
        'rebate_limit' => 0.01,
        'default_player_rebate' => 0.005,
    ]);

    return ['site_id' => $siteId, 'site_code' => $code, 'root_id' => $rootId];
}

function createTicketItemForPlayer(Player $player, string $ticketNo): int
{
    $draw = Draw::query()->create([
        'draw_no' => 'DRAW-'.$ticketNo,
        'business_date' => now()->toDateString(),
        'sequence_no' => random_int(1, 9999),
        'status' => DrawStatus::Open->value,
        'current_result_version' => 0,
        'settle_version' => 0,
        'is_reopened' => false,
    ]);

    $orderId = (int) DB::table('ticket_orders')->insertGetId([
        'order_no' => 'ORD-'.$ticketNo,
        'player_id' => $player->id,
        'draw_id' => $draw->id,
        'currency_code' => 'NPR',
        'total_bet_amount' => 10_000,
        'total_rebate_amount' => 0,
        'total_actual_deduct' => 10_000,
        'total_estimated_payout' => 0,
        'status' => 'confirmed',
        'submit_source' => 'h5',
        'client_trace_id' => null,
        'created_at' => now(),
        'updated_at' => now(),
    ]);

    return (int) DB::table('ticket_items')->insertGetId([
        'ticket_no' => $ticketNo,
        'order_id' => $orderId,
        'player_id' => $player->id,
        'draw_id' => $draw->id,
        'original_number' => null,
        'normalized_number' => '1234',
        'play_code' => 'big',
        'dimension' => 2,
        'digit_slot' => null,
        'bet_mode' => null,
        'unit_bet_amount' => 10_000,
        'total_bet_amount' => 10_000,
        'rebate_rate_snapshot' => 0,
        'commission_rate_snapshot' => 0,
        'actual_deduct_amount' => 10_000,
        'odds_snapshot_json' => null,
        'rule_snapshot_json' => null,
        'combination_count' => 1,
        'estimated_max_payout' => 0,
        'risk_locked_amount' => 0,
        'status' => 'settled_lose',
        'win_amount' => 0,
        'jackpot_win_amount' => 0,
        'settled_at' => null,
        'created_at' => now(),
        'updated_at' => now(),
    ]);
}

test('period close only tags share ledger rows for the closing site', function (): void {
    $siteA = createSiteWithRoot('close-site-a');
    $siteB = createSiteWithRoot('close-site-b');

    $playerA = Player::query()->create([
        'site_code' => $siteA['site_code'],
        'agent_node_id' => $siteA['root_id'],
        'site_player_id' => 'p-close-a',
        'username' => 'close_a',
        'nickname' => null,
        'default_currency' => 'NPR',
        'status' => 0,
    ]);

    $playerB = Player::query()->create([
        'site_code' => $siteB['site_code'],
        'agent_node_id' => $siteB['root_id'],
        'site_player_id' => 'p-close-b',
        'username' => 'close_b',
        'nickname' => null,
        'default_currency' => 'NPR',
        'status' => 0,
    ]);

    $settledAt = now();
    $snapshot = json_encode([
        'total_shares' => ['close-site-a' => 100],
        'chain_codes' => ['close-site-a'],
        'agent_path' => [$siteA['root_id']],
    ]);
    $snapshotB = json_encode([
        'total_shares' => ['close-site-b' => 100],
        'chain_codes' => ['close-site-b'],
        'agent_path' => [$siteB['root_id']],
    ]);

    $ticketAId = createTicketItemForPlayer($playerA, 'T-P0-A');
    $ticketBId = createTicketItemForPlayer($playerB, 'T-P0-B');

    $ledgerAId = (int) DB::table('share_ledger')->insertGetId([
        'ticket_item_id' => $ticketAId,
        'player_id' => $playerA->id,
        'agent_node_id' => $siteA['root_id'],
        'agent_path' => json_encode([$siteA['root_id']]),
        'share_snapshot' => $snapshot,
        'game_win_loss' => 1000,
        'basic_rebate' => 0,
        'shared_net_win_loss' => 1000,
        'allocations_json' => json_encode([]),
        'settled_at' => $settledAt,
        'created_at' => $settledAt,
        'updated_at' => $settledAt,
    ]);

    $ledgerBId = (int) DB::table('share_ledger')->insertGetId([
        'ticket_item_id' => $ticketBId,
        'player_id' => $playerB->id,
        'agent_node_id' => $siteB['root_id'],
        'agent_path' => json_encode([$siteB['root_id']]),
        'share_snapshot' => $snapshotB,
        'game_win_loss' => 2000,
        'basic_rebate' => 0,
        'shared_net_win_loss' => 2000,
        'allocations_json' => json_encode([]),
        'settled_at' => $settledAt,
        'created_at' => $settledAt,
        'updated_at' => $settledAt,
    ]);

    $periodId = (int) DB::table('settlement_periods')->insertGetId([
        'admin_site_id' => $siteA['site_id'],
        'period_start' => $settledAt->copy()->subDay(),
        'period_end' => $settledAt->copy()->addDay(),
        'status' => 'open',
        'created_at' => now(),
        'updated_at' => now(),
    ]);

    app(AgentSettlementPeriodCloseService::class)->closePeriod($periodId);

    expect((int) DB::table('share_ledger')->where('id', $ledgerAId)->value('settlement_period_id'))
        ->toBe($periodId);
    expect(DB::table('share_ledger')->where('id', $ledgerBId)->value('settlement_period_id'))
        ->toBeNull();
});

test('agent bill unpaid amount uses abs net so player-wins chain can be paid', function (): void {
    $siteId = (int) DB::table('admin_sites')->where('is_default', true)->value('id');
    $siteCode = (string) DB::table('admin_sites')->where('id', $siteId)->value('code');
    $rootId = (int) DB::table('agent_nodes')->where('admin_site_id', $siteId)->where('depth', 0)->value('id');

    $super = AdminUser::query()->create([
        'username' => 'p0_agent_pay',
        'name' => 'P0',
        'email' => null,
        'password' => Hash::make('secret-strong'),
        'status' => 0,
    ]);
    grantSuperAdminRole($super);

    $service = app(AgentNodeService::class);
    $a = $service->createChild($super, agentChildPayload([
        'parent_id' => $rootId,
        'code' => 'P0A',
        'name' => 'P0A',
        'username' => 'p0_a',
        'total_share_rate' => 60,
        'credit_limit' => 500_000,
    ]));
    $b = $service->createChild($super, agentChildPayload([
        'parent_id' => $a->id,
        'code' => 'P0B',
        'name' => 'P0B',
        'username' => 'p0_b',
        'total_share_rate' => 40,
        'credit_limit' => 200_000,
    ]));
    $c = $service->createChild($super, agentChildPayload([
        'parent_id' => $b->id,
        'code' => 'P0C',
        'name' => 'P0C',
        'username' => 'p0_c',
        'total_share_rate' => 25,
        'credit_limit' => 100_000,
    ]));

    $player = Player::query()->create([
        'site_code' => $siteCode,
        'agent_node_id' => $c->id,
        'site_player_id' => 'p0-win-p1',
        'username' => 'p0win',
        'nickname' => null,
        'default_currency' => 'NPR',
        'status' => 0,
    ]);

    $settledAt = now();
    $periodId = (int) DB::table('settlement_periods')->insertGetId([
        'admin_site_id' => $siteId,
        'period_start' => $settledAt->copy()->subDay(),
        'period_end' => $settledAt->copy()->addDay(),
        'status' => 'open',
        'created_at' => now(),
        'updated_at' => now(),
    ]);

    $ticketItemId = createTicketItemForPlayer($player, 'T-P0-WIN');

    // Player wins: negative game_win_loss produces negative agent edge settlements.
    DB::table('share_ledger')->insert([
        'ticket_item_id' => $ticketItemId,
        'player_id' => $player->id,
        'agent_node_id' => $c->id,
        'agent_path' => json_encode([$a->id, $b->id, $c->id]),
        'share_snapshot' => json_encode([
            'total_shares' => ['P0C' => 25, 'P0B' => 40, 'P0A' => 60],
            'actual_shares' => ['P0C' => 25, 'P0B' => 15, 'P0A' => 20, 'platform' => 40],
            'chain_codes' => ['P0C', 'P0B', 'P0A'],
            'agent_path' => [$a->id, $b->id, $c->id],
        ]),
        'game_win_loss' => -DesignDocExample12::GAME_WIN_LOSS,
        'basic_rebate' => DesignDocExample12::BASIC_REBATE,
        'shared_net_win_loss' => -DesignDocExample12::SHARED_NET_WIN_LOSS,
        'allocations_json' => json_encode([]),
        'settled_at' => $settledAt,
        'created_at' => $settledAt,
        'updated_at' => $settledAt,
    ]);

    app(AgentSettlementPeriodCloseService::class)->closePeriod($periodId);

    $agentBill = DB::table('settlement_bills')
        ->where('settlement_period_id', $periodId)
        ->where('bill_type', 'agent')
        ->where('meta_json', 'like', '%P0C_to_P0B%')
        ->first();

    expect($agentBill)->not->toBeNull();
    expect((int) $agentBill->net_amount)->toBeLessThan(0);
    expect((int) $agentBill->unpaid_amount)->toBe(abs((int) $agentBill->net_amount));

    DB::table('settlement_bills')->where('id', $agentBill->id)->update([
        'status' => 'confirmed',
        'updated_at' => now(),
    ]);

    app(SettlementPaymentService::class)->recordPayment(
        (int) $agentBill->id,
        (int) $agentBill->unpaid_amount,
        (int) $super->id,
        ['method' => 'cash'],
    );

    $record = DB::table('payment_records')->where('settlement_bill_id', $agentBill->id)->first();
    expect($record)->not->toBeNull();
    expect((string) $record->payer_type)->toBe('agent');
    expect((int) $record->payer_id)->toBe((int) $agentBill->counterparty_id);
    expect((string) $record->payee_type)->toBe('agent');
    expect((int) $record->payee_id)->toBe((int) $agentBill->owner_id);

    $refreshed = DB::table('settlement_bills')->where('id', $agentBill->id)->first();
    expect((int) $refreshed->unpaid_amount)->toBe(0);
    expect((string) $refreshed->status)->toBe('settled');
});

test('period close fails when share ledger row is missing snapshot', function (): void {
    $siteId = (int) DB::table('admin_sites')->where('is_default', true)->value('id');
    $siteCode = (string) DB::table('admin_sites')->where('id', $siteId)->value('code');
    $rootId = (int) DB::table('agent_nodes')->where('admin_site_id', $siteId)->where('depth', 0)->value('id');

    $player = Player::query()->create([
        'site_code' => $siteCode,
        'agent_node_id' => $rootId,
        'site_player_id' => 'p0-missing-snap',
        'username' => 'p0snap',
        'nickname' => null,
        'default_currency' => 'NPR',
        'status' => 0,
    ]);

    $settledAt = now();
    $ticketItemId = createTicketItemForPlayer($player, 'T-P0-NOSNAP');

    DB::table('share_ledger')->insert([
        'ticket_item_id' => $ticketItemId,
        'player_id' => $player->id,
        'agent_node_id' => $rootId,
        'agent_path' => json_encode([$rootId]),
        'share_snapshot' => null,
        'game_win_loss' => 500,
        'basic_rebate' => 0,
        'shared_net_win_loss' => 500,
        'allocations_json' => json_encode([]),
        'settled_at' => $settledAt,
        'created_at' => $settledAt,
        'updated_at' => $settledAt,
    ]);

    $periodId = (int) DB::table('settlement_periods')->insertGetId([
        'admin_site_id' => $siteId,
        'period_start' => $settledAt->copy()->subDay(),
        'period_end' => $settledAt->copy()->addDay(),
        'status' => 'open',
        'created_at' => now(),
        'updated_at' => now(),
    ]);

    $caught = null;
    try {
        app(AgentSettlementPeriodCloseService::class)->closePeriod($periodId);
    } catch (\Illuminate\Validation\ValidationException $e) {
        $caught = $e;
    }

    expect($caught)->toBeInstanceOf(\Illuminate\Validation\ValidationException::class);
    expect($caught?->errors()['period'][0] ?? null)->toBe('share_snapshot_missing');

    expect((string) DB::table('settlement_periods')->where('id', $periodId)->value('status'))
        ->toBe('open');
    expect(DB::table('settlement_bills')->where('settlement_period_id', $periodId)->count())
        ->toBe(0);
});

test('period close succeeds with no share ledger rows in window', function (): void {
    ['site_id' => $siteId] = createSiteWithRoot('empty-close');

    $periodId = (int) DB::table('settlement_periods')->insertGetId([
        'admin_site_id' => $siteId,
        'period_start' => '2026-06-01 00:00:00',
        'period_end' => '2026-06-07 23:59:59',
        'status' => 'open',
        'created_at' => now(),
        'updated_at' => now(),
    ]);

    $result = app(AgentSettlementPeriodCloseService::class)->closePeriod($periodId);

    expect($result['bill_ids'])->toBe([]);
    expect($result['player_count'])->toBe(0);
    expect((string) DB::table('settlement_periods')->where('id', $periodId)->value('status'))
        ->toBe('closed');
    expect(DB::table('settlement_bills')->where('settlement_period_id', $periodId)->count())
        ->toBe(0);
});

test('period close does not dispatch rebates from other sites', function (): void {
    $siteA = createSiteWithRoot('rebate-site-a');
    $siteB = createSiteWithRoot('rebate-site-b');

    $playerA = Player::query()->create([
        'site_code' => $siteA['site_code'],
        'agent_node_id' => $siteA['root_id'],
        'site_player_id' => 'p-rebate-a',
        'username' => 'rebate_a',
        'nickname' => null,
        'default_currency' => 'NPR',
        'status' => 0,
    ]);

    $playerB = Player::query()->create([
        'site_code' => $siteB['site_code'],
        'agent_node_id' => $siteB['root_id'],
        'site_player_id' => 'p-rebate-b',
        'username' => 'rebate_b',
        'nickname' => null,
        'default_currency' => 'NPR',
        'status' => 0,
    ]);

    $settledAt = now();

    $ticketAId = createTicketItemForPlayer($playerA, 'T-REB-A');
    $ticketBId = createTicketItemForPlayer($playerB, 'T-REB-B');

    $snapshotA = json_encode([
        'total_shares' => ['rebate-site-a' => 100],
        'chain_codes' => ['rebate-site-a'],
        'agent_path' => [$siteA['root_id']],
    ]);
    $snapshotB = json_encode([
        'total_shares' => ['rebate-site-b' => 100],
        'chain_codes' => ['rebate-site-b'],
        'agent_path' => [$siteB['root_id']],
    ]);

    DB::table('share_ledger')->insert([
        [
            'ticket_item_id' => $ticketAId,
            'player_id' => $playerA->id,
            'agent_node_id' => $siteA['root_id'],
            'agent_path' => json_encode([$siteA['root_id']]),
            'share_snapshot' => $snapshotA,
            'game_win_loss' => 1000,
            'basic_rebate' => 0,
            'shared_net_win_loss' => 1000,
            'allocations_json' => json_encode([]),
            'settled_at' => $settledAt,
            'created_at' => $settledAt,
            'updated_at' => $settledAt,
        ],
        [
            'ticket_item_id' => $ticketBId,
            'player_id' => $playerB->id,
            'agent_node_id' => $siteB['root_id'],
            'agent_path' => json_encode([$siteB['root_id']]),
            'share_snapshot' => $snapshotB,
            'game_win_loss' => 2000,
            'basic_rebate' => 0,
            'shared_net_win_loss' => 2000,
            'allocations_json' => json_encode([]),
            'settled_at' => $settledAt,
            'created_at' => $settledAt,
            'updated_at' => $settledAt,
        ],
    ]);

    $rebateBId = (int) DB::table('rebate_records')->insertGetId([
        'player_id' => $playerB->id,
        'ticket_item_id' => $ticketBId,
        'game_type' => '*',
        'valid_bet_amount' => 10000,
        'rebate_rate' => 0.005,
        'rebate_amount' => 50,
        'rebate_type' => 'basic',
        'owner_agent_id' => $siteB['root_id'],
        'status' => 'accrued',
        'created_at' => $settledAt,
        'updated_at' => $settledAt,
    ]);

    $periodAId = (int) DB::table('settlement_periods')->insertGetId([
        'admin_site_id' => $siteA['site_id'],
        'period_start' => $settledAt->copy()->subDay(),
        'period_end' => $settledAt->copy()->addDay(),
        'status' => 'open',
        'created_at' => now(),
        'updated_at' => now(),
    ]);

    $result = app(AgentSettlementPeriodCloseService::class)->closePeriod($periodAId);

    expect($result['rebate_dispatched'])->toBe(0);

    expect((string) DB::table('rebate_records')->where('id', $rebateBId)->value('status'))
        ->toBe('accrued');
    expect(DB::table('rebate_records')->where('id', $rebateBId)->value('settlement_period_id'))
        ->toBeNull();
});

test('period close nets out original and reversal share ledger rows in same period', function (): void {
    ['site_id' => $siteId, 'site_code' => $siteCode, 'root_id' => $rootId] = createSiteWithRoot('reversal-net');

    $player = Player::query()->create([
        'site_code' => $siteCode,
        'agent_node_id' => $rootId,
        'site_player_id' => 'reversal-net-p1',
        'auth_source' => 'lottery_native',
        'funding_mode' => 'credit',
        'username' => 'reversal_net_p',
        'nickname' => null,
        'default_currency' => 'NPR',
        'status' => 0,
    ]);

    $settledAt = now();
    $ticketId = createTicketItemForPlayer($player, 'T-REV-NET');

    $snapshot = json_encode([
        'total_shares' => ['reversal-net' => 100],
        'chain_codes' => ['reversal-net'],
        'agent_path' => [$rootId],
    ]);

    $originalLedgerId = (int) DB::table('share_ledger')->insertGetId([
        'ticket_item_id' => $ticketId,
        'player_id' => $player->id,
        'agent_node_id' => $rootId,
        'agent_path' => json_encode([$rootId]),
        'share_snapshot' => $snapshot,
        'game_win_loss' => 5000,
        'basic_rebate' => 0,
        'shared_net_win_loss' => 5000,
        'allocations_json' => json_encode([]),
        'settled_at' => $settledAt->copy()->subSecond(),
        'created_at' => $settledAt->copy()->subSecond(),
        'updated_at' => $settledAt->copy()->subSecond(),
    ]);

    $reversalLedgerId = (int) DB::table('share_ledger')->insertGetId([
        'ticket_item_id' => $ticketId,
        'player_id' => $player->id,
        'agent_node_id' => $rootId,
        'agent_path' => json_encode([$rootId]),
        'share_snapshot' => $snapshot,
        'game_win_loss' => -5000,
        'basic_rebate' => 0,
        'shared_net_win_loss' => -5000,
        'allocations_json' => json_encode([]),
        'reversal_of_id' => $originalLedgerId,
        'settled_at' => $settledAt,
        'created_at' => $settledAt,
        'updated_at' => $settledAt,
    ]);

    $periodId = (int) DB::table('settlement_periods')->insertGetId([
        'admin_site_id' => $siteId,
        'period_start' => $settledAt->copy()->subDay(),
        'period_end' => $settledAt->copy()->addDay(),
        'status' => 'open',
        'created_at' => now(),
        'updated_at' => now(),
    ]);

    $result = app(AgentSettlementPeriodCloseService::class)->closePeriod($periodId);

    expect($result['player_count'])->toBe(1);

    $playerBill = DB::table('settlement_bills')
        ->where('settlement_period_id', $periodId)
        ->where('bill_type', 'player')
        ->where('owner_id', $player->id)
        ->first();
    expect($playerBill)->not->toBeNull();
    expect((int) $playerBill->net_amount)->toBe(0);

    expect(DB::table('share_ledger')->where('id', $originalLedgerId)->value('settlement_period_id'))
        ->toBe($periodId);
    expect(DB::table('share_ledger')->where('id', $reversalLedgerId)->value('settlement_period_id'))
        ->toBe($periodId);
});
