<?php

declare(strict_types=1);

use App\Models\Deal;
use App\Models\Project;
use App\Models\SupplierProject;
use App\Models\Tenant;
use App\Services\DaData\DaDataPhoneClient;
use App\Services\LeadRouter;
use Database\Seeders\PricingTierSeeder;
use Illuminate\Foundation\Testing\DatabaseTransactions;
use Illuminate\Support\Facades\DB;
use Random\Engine\Mt19937;
use Random\Randomizer;
use Tests\Concerns\SharesSupplierPdo;
use Tests\Support\Imitation\FakeDaDataPhoneClient;
use Tests\Support\Imitation\LeadInjector;
use Tests\Support\Imitation\SnapshotForge;

uses(DatabaseTransactions::class, SharesSupplierPdo::class);

/**
 * Scenario A — взвешенный жребий по объёму (§6.2 A + §6.5 X2).
 *
 * VERIFICATION test against existing prod routing code in LeadRouter.
 * This proves (or disproves) the weighted-lottery distribution behaviour.
 * NOT TDD — no prod code is modified. Findings are reported, not silently fixed.
 *
 * Setup:
 *   - 5 tenants/projects on ONE shared SupplierProject, region = Москва (code 82).
 *   - Daily limits: {300, 30, 30, 3, 3}.  delivered_today = 0 (full capacity).
 *   - Deterministic seeded LeadRouter (Mt19937 seed 42) for reproducibility.
 *   - FakeDaDataPhoneClient: all phones → Москва (qc=0, subject_code=82).
 *   - N = 300 leads injected synchronously.
 *
 * Cap=3 means each lead is delivered to AT MOST 3 of the 5 projects (weighted pick
 * from eligible candidates per phase). Projects drop out of eligibility once
 * delivered_today >= daily_limit. So the two limit-3 projects each receive at most 3
 * leads total, then become ineligible for the rest of the run.
 *
 * Assertions (per plan §6.5):
 *   (a) The biggest-limit project (300) receives the most deals.
 *   (b) Both smallest projects (limit=3) receive > 0 deals (weight≥1 guarantee).
 *   (c) The big-limit project's share is substantially larger than any small project's.
 *
 * Task 6 — Phase 1 Portal Client Imitation, Scenario A.
 * Spec: docs/superpowers/specs/2026-06-03-portal-client-imitation-phase1-design.md §6.2/§6.5
 * Plan: docs/superpowers/plans/2026-06-03-portal-client-imitation-phase1.md §Task 6
 */

/**
 * Москва subject code — порядковый (НЕ ГИБДД).
 * Подтверждено: App\Support\RussianRegions::CODE_TO_NAME[82] = 'Москва'.
 */
const MOSCOW_SUBJECT_CODE = 82;

/**
 * Deterministic seed for Mt19937 — same seed = same sequence of rolls.
 */
const LOTTERY_SEED = 42;

/**
 * Number of leads to inject in the distribution run.
 */
const LEAD_COUNT = 300;

/**
 * Daily limits for the 5 projects.  Index → limit.
 */
const PROJECT_LIMITS = [300, 30, 30, 3, 3];

/**
 * Shared supplier domain (B1 site signal).
 */
const SUPPLIER_DOMAIN = 'scenario-a-test.ru';

/**
 * Shared supplier platform.
 */
const SUPPLIER_PLATFORM = 'B1';

beforeEach(function (): void {
    // Seed pricing tiers required by LedgerService::chargeForDelivery.
    $this->seed(PricingTierSeeder::class);

    // Global bypass RLS for seeding.
    DB::statement("SELECT set_config('app.current_tenant_id', '0', true)");

    // Bind deterministic DaData fake: every phone maps to Москва (qc=0, code=82).
    // We'll register stubs per phone inside the test.
    config([
        'services.dadata.enabled' => true,
        'services.dadata.api_key' => 'fake-key',
        'services.dadata.secret' => 'fake-secret',
        'services.dadata.daily_cap_rub' => 1_000_000,
    ]);

    // Bind deterministic LeadRouter with seeded Mt19937.
    $seededRouter = new LeadRouter(new Randomizer(new Mt19937(LOTTERY_SEED)));
    app()->instance(LeadRouter::class, $seededRouter);
});

it('splits leads weighted by remaining limit, small clients are not cut off', function (): void {
    // ── ARRANGE ─────────────────────────────────────────────────────────────────

    // One shared supplier project (B1 site signal).
    $supplier = SupplierProject::factory()->create([
        'platform' => SUPPLIER_PLATFORM,
        'signal_type' => 'site',
        'unique_key' => SUPPLIER_DOMAIN,
    ]);

    // Create 5 tenants + projects, one per limit tier.
    $limits = PROJECT_LIMITS;
    $projects = [];
    $tenants = [];

    foreach ($limits as $idx => $limit) {
        $tenant = Tenant::factory()->create([
            'balance_rub' => '99999.00', // ample balance — billing must not block
            'frozen_by_balance_at' => null,
        ]);
        $tenants[] = $tenant;

        $project = Project::factory()->create([
            'tenant_id' => $tenant->id,
            'is_active' => true,
            'signal_type' => 'site',
            'signal_identifier' => SUPPLIER_DOMAIN,
            'daily_limit_target' => $limit,
            'effective_daily_limit_today' => null,
            'delivered_today' => 0,
            'delivered_in_month' => 0,
            'delivery_days_mask' => 127, // all days
            'preflight_blocked_at' => null,
            // PostgresIntArray cast: pass PHP array, cast serialises to '{82}'.
            'regions' => [MOSCOW_SUBJECT_CODE],
        ]);

        // Link project to supplier.
        linkProjectToSupplier($project, $supplier);
        $projects[] = $project;
    }

    // Active date for snapshot — mirrors LeadRouter::activeSnapshotDate().
    $activeDate = SnapshotForge::activeDate();

    // Build routing snapshots for each project with Москва region (code 82)
    // and the correct daily_limit. Using createRoutingSnapshotFromProject helper
    // (defined in tests/Pest.php) with explicit dailyLimit and regions='{82}'.
    foreach ($projects as $idx => $project) {
        createRoutingSnapshotFromProject(
            project: $project,
            date: $activeDate,
            signalType: 'site',
            signalIdentifier: SUPPLIER_DOMAIN,
            dailyLimit: $limits[$idx],
            regions: '{'.MOSCOW_SUBJECT_CODE.'}',
        );
    }

    // Build a FakeDaDataPhoneClient that maps all test phones to Москва (qc=0).
    // We generate deterministic phone numbers: 7(916)000XXXX where XXXX = index 0001..0300.
    $fakeDaData = new FakeDaDataPhoneClient;
    for ($i = 1; $i <= LEAD_COUNT; $i++) {
        $phone = '7916'.str_pad((string) $i, 7, '0', STR_PAD_LEFT);
        $fakeDaData->stub($phone, qc: 0, region: 'Москва', provider: 'МТС');
    }
    app()->instance(DaDataPhoneClient::class, $fakeDaData);

    // ── ACT ─────────────────────────────────────────────────────────────────────

    $injector = new LeadInjector;
    $vidBase = 9_000_000_000; // high range, outside real supplier VIDs

    for ($i = 1; $i <= LEAD_COUNT; $i++) {
        $phone = '7916'.str_pad((string) $i, 7, '0', STR_PAD_LEFT);
        $injector->site(
            domain: SUPPLIER_DOMAIN,
            phone: $phone,
            tag: 'Москва',
            platform: SUPPLIER_PLATFORM,
            vid: $vidBase + $i,
        );
    }

    // ── GATHER DISTRIBUTION ──────────────────────────────────────────────────────

    // Count deals per project (each deal has a tenant_id; project-id is on the deal row).
    // Using pgsql_supplier (BYPASSRLS) to see deals across all tenants.
    $dealCounts = [];
    foreach ($projects as $idx => $project) {
        $count = DB::connection('pgsql_supplier')
            ->table('deals')
            ->where('tenant_id', $tenants[$idx]->id)
            ->count();
        $dealCounts[$idx] = $count;
    }

    $totalDeals = array_sum($dealCounts);

    // ── REPORT ──────────────────────────────────────────────────────────────────
    // Print observed distribution for the required report.
    fwrite(STDOUT, PHP_EOL.'=== SCENARIO A DISTRIBUTION REPORT ==='.PHP_EOL);
    fwrite(STDOUT, sprintf('Total leads injected: %d%s', LEAD_COUNT, PHP_EOL));
    fwrite(STDOUT, sprintf('Total deals created:  %d (≤ %d × cap=3 = %d)%s',
        $totalDeals, LEAD_COUNT, LEAD_COUNT * 3, PHP_EOL));
    fwrite(STDOUT, PHP_EOL.sprintf('%-10s %-12s %-10s %-10s%s', 'Project', 'Limit', 'Deals', 'Share%', PHP_EOL));
    fwrite(STDOUT, str_repeat('-', 45).PHP_EOL);
    foreach ($projects as $idx => $project) {
        $deals = $dealCounts[$idx];
        $share = $totalDeals > 0 ? round($deals / $totalDeals * 100, 1) : 0.0;
        fwrite(STDOUT, sprintf(
            '%-10s %-12d %-10d %-10s%s',
            "P{$idx} (lim={$limits[$idx]})",
            $limits[$idx],
            $deals,
            "{$share}%",
            PHP_EOL
        ));
    }
    fwrite(STDOUT, '=== END DISTRIBUTION ==='.PHP_EOL.PHP_EOL);

    // ── ASSERTIONS ───────────────────────────────────────────────────────────────

    // (a) The biggest-limit project (P0, limit=300) got the most deals.
    //     After the two limit-3 projects hit their cap, P0 competes with two limit-30
    //     projects; but even before that, its weight (300) vastly outweighs theirs.
    $bigProjectDeals = $dealCounts[0]; // limit=300
    $maxSmallDeals = max($dealCounts[1], $dealCounts[2]); // limit=30 × 2

    expect($bigProjectDeals)->toBeGreaterThan($maxSmallDeals,
        'FINDING: big-limit project (limit=300) should receive more deals than any limit-30 project. '.
        "Got: P0={$dealCounts[0]}, P1={$dealCounts[1]}, P2={$dealCounts[2]}. ".
        'This would indicate the weighted lottery is not proportional.'
    );

    // (b) Both smallest projects (limit=3) received > 0 deals.
    //     Weight ≥1 guarantee means even tiny projects see some traffic.
    //     They CAN only receive at most 3 deals each (their limit), so they'll
    //     hit their limit early and then drop out of eligibility.
    expect($dealCounts[3])->toBeGreaterThan(0,
        'FINDING: small-limit project P3 (limit=3) received 0 deals. '.
        'The spec guarantees weight≥1 so small clients are NOT cut off. '.
        "Actual: P3={$dealCounts[3]}. This is a bug."
    );
    expect($dealCounts[4])->toBeGreaterThan(0,
        'FINDING: small-limit project P4 (limit=3) received 0 deals. '.
        'The spec guarantees weight≥1 so small clients are NOT cut off. '.
        "Actual: P4={$dealCounts[4]}. This is a bug."
    );

    // (c) Proportionality: big-limit project's share vs small-limit projects.
    //     With limits {300,30,30,3,3} and cap=3 per lead:
    //     - The two limit-3 projects hit their cap and drop out early (after 3 leads each).
    //     - For the remaining 294 leads, it's P0(300), P1(30), P2(30) competing for 3 slots.
    //     - P0 weight starts at 300, P1/P2 at 30 each → P0 wins ~83% of selections per lead.
    //     - Total picks per lead: 3 (P0 + P1 + P2 all eligible for all 3 slots after picks).
    //     - Expected P0 deals ≈ 294 × 300/360 × ... roughly ~240+ (but with depletion it's less).
    //     - Tolerant check: P0 has at least 50% of all deals.
    $p0Share = $totalDeals > 0 ? $bigProjectDeals / $totalDeals : 0.0;

    expect($p0Share)->toBeGreaterThan(0.45,
        'FINDING: big-limit project P0 (limit=300) has a share of '.
        round($p0Share * 100, 1).'% which is below 45%. '.
        'Expected substantially more than limit-30 projects given weights 300 vs 30. '.
        'Limits: '.json_encode($limits).', Deals: '.json_encode($dealCounts)
    );

    // (d) Small projects each received exactly their limit (3) or fewer.
    //     They drop out once delivered_today == daily_limit, so max is 3.
    expect($dealCounts[3])->toBeLessThanOrEqual(3,
        "FINDING: P3 (limit=3) received {$dealCounts[3]} deals which exceeds its daily limit. ".
        'The eligibility guard (delivered_today < daily_limit) should prevent this.'
    );
    expect($dealCounts[4])->toBeLessThanOrEqual(3,
        "FINDING: P4 (limit=3) received {$dealCounts[4]} deals which exceeds its daily limit. ".
        'The eligibility guard (delivered_today < daily_limit) should prevent this.'
    );

})->group('imitation');
