<?php

declare(strict_types=1);

use App\Exceptions\Billing\InsufficientBalanceException;
use App\Models\Deal;
use App\Models\LeadCharge;
use App\Models\Supplier;
use App\Models\SupplierLead;
use App\Models\SupplierProject;
use App\Models\Tenant;
use App\Services\Billing\LedgerService;
use Database\Seeders\PricingTierSeeder;
use Illuminate\Foundation\Testing\DatabaseTransactions;
use Illuminate\Support\Facades\DB;

uses(DatabaseTransactions::class);

beforeEach(function () {
    // Под `pest --parallel` Laravel создаёт per-worker testing DBs (liderra_testing_test_1..N)
    // и migrate'ит их без seed'ов. Поэтому seed'им ступени в beforeEach (DatabaseTransactions
    // rollback'ает их в end-of-test, поэтому seed безопасен и не накапливается).
    // PricingTierSeeder использует updateOrCreate — idempotent при повторном вызове.
    $this->seed(PricingTierSeeder::class);
    $this->ledger = app(LedgerService::class);
});

function makeTenantWith(int $balanceLeads, string $balanceRub, int $deliveredInMonth = 0): Tenant
{
    return Tenant::factory()->create([
        'balance_leads' => $balanceLeads,
        'balance_rub' => $balanceRub,
        'delivered_in_month' => $deliveredInMonth,
    ]);
}

function makeDealForTenant(Tenant $tenant): Deal
{
    return Deal::factory()->create([
        'tenant_id' => $tenant->id,
        'received_at' => now(),
    ]);
}

it('charges rub when balance_rub >= price', function () {
    $tenant = makeTenantWith(balanceLeads: 0, balanceRub: '1000.00', deliveredInMonth: 0);
    $deal = makeDealForTenant($tenant);

    $result = DB::transaction(function () use ($tenant, $deal) {
        DB::statement("SET LOCAL app.current_tenant_id = '{$tenant->id}'");
        $locked = Tenant::whereKey($tenant->id)->lockForUpdate()->firstOrFail();

        return $this->ledger->chargeForDelivery($locked, $deal);
    });

    expect($result->priceKopecks)->toBe(50000);

    $tenant->refresh();
    expect((string) $tenant->balance_rub)->toBe('500.00');
    expect((int) $tenant->balance_leads)->toBe(0);
    expect($tenant->delivered_in_month)->toBe(1);

    $charge = LeadCharge::first();
    expect($charge->charge_source)->toBe('rub');
    expect($charge->price_per_lead_kopecks)->toBe(50000);
});

it('throws InsufficientBalanceException when balance_rub * 100 < priceKopecks', function () {
    $tenant = makeTenantWith(balanceLeads: 0, balanceRub: '400.00', deliveredInMonth: 0);
    $deal = makeDealForTenant($tenant);

    expect(function () use ($tenant, $deal) {
        DB::transaction(function () use ($tenant, $deal) {
            DB::statement("SET LOCAL app.current_tenant_id = '{$tenant->id}'");
            $locked = Tenant::whereKey($tenant->id)->lockForUpdate()->firstOrFail();
            $this->ledger->chargeForDelivery($locked, $deal);
        });
    })->toThrow(InsufficientBalanceException::class);

    expect(LeadCharge::count())->toBe(0);
    $tenant->refresh();
    expect((int) $tenant->balance_leads)->toBe(0);
    expect((string) $tenant->balance_rub)->toBe('400.00');
    expect($tenant->delivered_in_month)->toBe(0);
});

it('charges rub at exact balance == price boundary', function () {
    $tenant = makeTenantWith(balanceLeads: 0, balanceRub: '500.00', deliveredInMonth: 0);
    $deal = makeDealForTenant($tenant);

    $result = DB::transaction(function () use ($tenant, $deal) {
        DB::statement("SET LOCAL app.current_tenant_id = '{$tenant->id}'");
        $locked = Tenant::whereKey($tenant->id)->lockForUpdate()->firstOrFail();

        return $this->ledger->chargeForDelivery($locked, $deal);
    });

    $tenant->refresh();
    expect((string) $tenant->balance_rub)->toBe('0.00');
});

it('always uses charge_source=rub regardless of historic balance_leads value', function () {
    // Historic prepaid leftover — must be ignored by new always-rub flow.
    $tenant = makeTenantWith(balanceLeads: 5, balanceRub: '1000.00', deliveredInMonth: 0);
    $deal = makeDealForTenant($tenant);

    DB::transaction(function () use ($tenant, $deal) {
        DB::statement("SET LOCAL app.current_tenant_id = '{$tenant->id}'");
        $locked = Tenant::whereKey($tenant->id)->lockForUpdate()->firstOrFail();
        $this->ledger->chargeForDelivery($locked, $deal);
    });

    $tenant->refresh();

    // balance_leads must remain UNCHANGED (we never touch this column anymore).
    expect((int) $tenant->balance_leads)->toBe(5);
    // balance_rub must be debited by tier 1 price (500₽ for delivered_in_month=0 → tier 1).
    expect((string) $tenant->balance_rub)->toBe('500.00');
    expect($tenant->delivered_in_month)->toBe(1);

    $charge = LeadCharge::first();
    expect($charge->charge_source)->toBe('rub');
    expect($charge->price_per_lead_kopecks)->toBe(50000);
});

it('crosses tier boundary: delivered_in_month=99 → tier 1; delivered_in_month=100 → tier 2', function () {
    $tenantA = makeTenantWith(balanceLeads: 0, balanceRub: '10000.00', deliveredInMonth: 99);
    $tenantB = makeTenantWith(balanceLeads: 0, balanceRub: '10000.00', deliveredInMonth: 100);

    $dealA = makeDealForTenant($tenantA);
    $dealB = makeDealForTenant($tenantB);

    $resultA = DB::transaction(function () use ($tenantA, $dealA) {
        DB::statement("SET LOCAL app.current_tenant_id = '{$tenantA->id}'");
        $locked = Tenant::whereKey($tenantA->id)->lockForUpdate()->firstOrFail();

        return $this->ledger->chargeForDelivery($locked, $dealA);
    });
    $resultB = DB::transaction(function () use ($tenantB, $dealB) {
        DB::statement("SET LOCAL app.current_tenant_id = '{$tenantB->id}'");
        $locked = Tenant::whereKey($tenantB->id)->lockForUpdate()->firstOrFail();

        return $this->ledger->chargeForDelivery($locked, $dealB);
    });

    expect($resultA->tier->tier_no)->toBe(1);
    expect($resultB->tier->tier_no)->toBe(2);
});

it('writes supplier_lead_costs (gap-fix: Plan 2/3 не писали в sharing-flow)', function () {
    $tenant = makeTenantWith(balanceLeads: 0, balanceRub: '10000.00');
    $supplier = Supplier::where('code', 'b1')->first();
    $supplierProject = SupplierProject::factory()->create([
        'platform' => 'B1',
        'signal_type' => 'site',
    ]);
    $deal = Deal::factory()->create(['tenant_id' => $tenant->id, 'received_at' => now()]);
    $lead = SupplierLead::factory()->create(['supplier_project_id' => $supplierProject->id]);

    DB::transaction(function () use ($tenant, $deal, $lead) {
        DB::statement("SET LOCAL app.current_tenant_id = '{$tenant->id}'");
        $locked = Tenant::whereKey($tenant->id)->lockForUpdate()->firstOrFail();
        $this->ledger->chargeForDelivery($locked, $deal, $lead);
    });

    $cost = DB::table('supplier_lead_costs')->where('deal_id', $deal->id)->first();
    expect($cost)->not->toBeNull();
    expect((int) $cost->supplier_id)->toBe($supplier->id);
    expect((string) $cost->cost_rub)->toBe($supplier->cost_rub);
});

// Stage 3 / Task 3.1 — R-03 (spec §4.3.1): a frozen tenant must be rejected at
// charge time even when balance_rub > 0. Guard is BEFORE bcmath arithmetic so
// no balance / charges state is touched on rejection. The same auto-pause flow
// kicks in (InsufficientBalanceException → RouteSupplierLeadJob handler flips
// projects.is_active=false and queues ZeroBalancePausedMail rate-limited).
it('throws InsufficientBalanceException when tenant frozen_by_balance_at is set', function () {
    $tenant = Tenant::factory()->create([
        'balance_rub' => '500.00',
        'frozen_by_balance_at' => now(),
    ]);
    $deal = makeDealForTenant($tenant);

    expect(function () use ($tenant, $deal) {
        DB::transaction(function () use ($tenant, $deal) {
            DB::statement("SET LOCAL app.current_tenant_id = '{$tenant->id}'");
            $locked = Tenant::whereKey($tenant->id)->lockForUpdate()->firstOrFail();
            $this->ledger->chargeForDelivery($locked, $deal);
        });
    })->toThrow(InsufficientBalanceException::class);

    // No side effects on frozen reject — balance and charges untouched.
    $tenant->refresh();
    expect((string) $tenant->balance_rub)->toBe('500.00');
    expect(LeadCharge::where('tenant_id', $tenant->id)->count())->toBe(0);
});
