<?php

declare(strict_types=1);

use App\Mail\ZeroBalancePausedMail;
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 Illuminate\Support\Facades\Mail;
use Random\Engine\Mt19937;
use Random\Randomizer;
use Tests\Concerns\SharesSupplierPdo;
use Tests\Support\Imitation\ConditionLevers;
use Tests\Support\Imitation\FakeDaDataPhoneClient;
use Tests\Support\Imitation\LeadInjector;
use Tests\Support\Imitation\SnapshotForge;

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

/**
 * Scenarios E1 / E2 / F — freeze + daily-limit verification tests.
 *
 * VERIFICATION tests against existing prod billing+routing code.
 * NOT TDD — no prod code is modified. Differences vs plan → reported as FINDINGs.
 *
 * E1: auto-pause on insufficient balance.
 *     Client1 balance < price of one lead → InsufficientBalance on charge attempt →
 *     project.is_active=false, ZeroBalancePausedMail queued, lead delivered to Client2.
 *
 * E2: frozen tenant excluded at eligibility filter stage (before any charge).
 *     frozen_by_balance_at IS NOT NULL → LeadRouter WHERE tenants.frozen_by_balance_at IS NULL
 *     excludes the frozen tenant entirely; healthy Client2 gets the lead.
 *
 * F: daily limit reached → project excluded by delivered_today >= snap.daily_limit.
 *    Client1 delivered_today == its snapshot daily_limit → ineligible; Client2 gets the lead.
 *
 * Subject codes: порядковые 1..89. Москва = 82. Confirmed via RussianRegions::CODE_TO_NAME.
 * Tier 1 price = 50 000 kopecks = 500 RUB (PricingTierSeeder).
 *
 * Task 9 — Phase 1 Portal Client Imitation.
 * Plan: docs/superpowers/plans/2026-06-03-portal-client-imitation-phase1.md §Task 9
 * Spec: docs/superpowers/specs/2026-06-03-portal-client-imitation-phase1-design.md §6.2 E1/E2/F
 */

// ── Shared constants ──────────────────────────────────────────────────────────

/** Москва subject code (порядковый, НЕ ГИБДД). */
const EF_MOSCOW_CODE = 82;

/** Domain for B1 site signal shared across all three scenarios. */
const EF_DOMAIN = 'scenario-ef-test.ru';

/** Platform prefix. */
const EF_PLATFORM = 'B1';

/** Deterministic seed — makes weighted lottery pick reproducible. */
const EF_SEED = 99;

/** Tier-1 price in RUB (first 100 leads of month): 500 RUB. */
const EF_TIER1_PRICE_RUB = '500.00';

/** Daily limit used for healthy clients — large enough to never block. */
const EF_HEALTHY_LIMIT = 50;

// ── Shared beforeEach setup ───────────────────────────────────────────────────

beforeEach(function (): void {
    // Seed pricing tiers (tier 1: first 100 leads → 50 000 kopecks = 500 RUB/lead).
    $this->seed(PricingTierSeeder::class);

    // Allow cross-tenant reads during seeding (shared helper pattern).
    DB::statement("SELECT set_config('app.current_tenant_id', '0', true)");

    // DaData config — required by LeadRegionResolver even when FakeDaDataPhoneClient is used.
    config([
        'services.dadata.enabled' => true,
        'services.dadata.api_key' => 'fake-key',
        'services.dadata.secret' => 'fake-secret',
        'services.dadata.daily_cap_rub' => 1_000_000,
    ]);

    // Deterministic LeadRouter: Mt19937 seed so weighted pick is reproducible.
    app()->instance(LeadRouter::class, new LeadRouter(new Randomizer(new Mt19937(EF_SEED))));
});

// ─────────────────────────────────────────────────────────────────────────────
// E1 — Auto-pause on insufficient balance
// ─────────────────────────────────────────────────────────────────────────────

it('E1: project is paused and mail sent when balance below tier price; lead goes to healthy client', function (): void {
    Mail::fake();

    // ── ARRANGE ──────────────────────────────────────────────────────────────

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

    // Client1: balance BELOW tier-1 price (500 RUB). Even 1 kopeck short triggers pause.
    // We set balance to 0 which is definitely below 500 RUB threshold.
    $tenant1 = Tenant::factory()->create([
        'balance_rub' => '0.00',
        'frozen_by_balance_at' => null,
    ]);
    $project1 = Project::factory()->create([
        'tenant_id' => $tenant1->id,
        'is_active' => true,
        'signal_type' => 'site',
        'signal_identifier' => EF_DOMAIN,
        'daily_limit_target' => EF_HEALTHY_LIMIT,
        'effective_daily_limit_today' => null,
        'delivered_today' => 0,
        'delivered_in_month' => 0,
        'delivery_days_mask' => 127,
        'preflight_blocked_at' => null,
        'regions' => [EF_MOSCOW_CODE],
    ]);
    linkProjectToSupplier($project1, $supplier);

    // Client2: healthy balance — should receive the lead.
    $tenant2 = Tenant::factory()->create([
        'balance_rub' => '9999.00',
        'frozen_by_balance_at' => null,
    ]);
    $project2 = Project::factory()->create([
        'tenant_id' => $tenant2->id,
        'is_active' => true,
        'signal_type' => 'site',
        'signal_identifier' => EF_DOMAIN,
        'daily_limit_target' => EF_HEALTHY_LIMIT,
        'effective_daily_limit_today' => null,
        'delivered_today' => 0,
        'delivered_in_month' => 0,
        'delivery_days_mask' => 127,
        'preflight_blocked_at' => null,
        'regions' => [EF_MOSCOW_CODE],
    ]);
    linkProjectToSupplier($project2, $supplier);

    // Snapshot: both clients eligible (positive balance + unfrozen are live-state checks,
    // but snapshot itself is built before the charge; LeadRouter's SQL also checks balance > 0
    // and frozen_by_balance_at IS NULL). Client1 has balance=0 so it will NOT appear in the
    // router's eligibility query (balance_rub > 0 filter), making this effectively test the
    // case where balance becomes 0 AFTER snapshot is built but before the charge.
    //
    // IMPORTANT FINDING NOTE: LeadRouter SQL WHERE tenants.balance_rub > 0 means that if
    // balance is already 0 before the route call, Client1 is excluded at the query stage
    // (not at the charge stage). To truly test E1 (insufficient balance at charge time),
    // we must set balance to a non-zero amount that is nonetheless below the tier price.
    // Tier 1 = 500 RUB. Set to 499.99 — positive but insufficient.
    ConditionLevers::setBalance($tenant1, '499.99');

    $activeDate = SnapshotForge::activeDate();

    // Build snapshots for both clients so both pass the snapshot filter.
    createRoutingSnapshotFromProject(
        project: $project1,
        date: $activeDate,
        signalType: 'site',
        signalIdentifier: EF_DOMAIN,
        dailyLimit: EF_HEALTHY_LIMIT,
        regions: '{'.EF_MOSCOW_CODE.'}',
    );
    createRoutingSnapshotFromProject(
        project: $project2,
        date: $activeDate,
        signalType: 'site',
        signalIdentifier: EF_DOMAIN,
        dailyLimit: EF_HEALTHY_LIMIT,
        regions: '{'.EF_MOSCOW_CODE.'}',
    );

    // FakeDaData: phone → Москва (qc=0, subject_code=82).
    $phone = '79161234001';
    $fakeDaData = new FakeDaDataPhoneClient;
    $fakeDaData->stub($phone, qc: 0, region: 'Москва', provider: 'МТС');
    app()->instance(DaDataPhoneClient::class, $fakeDaData);

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

    $injector = new LeadInjector;
    $lead = $injector->site(
        domain: EF_DOMAIN,
        phone: $phone,
        tag: 'Москва',
        platform: EF_PLATFORM,
        vid: 1_100_000_001,
    );

    // ── ASSERT ───────────────────────────────────────────────────────────────

    // Client1's project must be paused (is_active=false) after the balance failure.
    $project1->refresh();
    expect($project1->is_active)->toBeFalse(
        'FINDING E1: project1 was NOT paused after InsufficientBalance. '.
        'Expected RouteSupplierLeadJob::handleInsufficientBalance to set is_active=false. '.
        'Actual is_active='.($project1->is_active ? 'true' : 'false')
    );

    // ZeroBalancePausedMail must have been sent for tenant1 (rate-limit: first call always fires).
    Mail::assertSent(ZeroBalancePausedMail::class, function (ZeroBalancePausedMail $mail) use ($tenant1): bool {
        return $mail->hasTo($tenant1->contact_email);
    });

    // Client2 (healthy) must have received the lead.
    $tenant2Deals = DB::connection('pgsql_supplier')
        ->table('deals')
        ->where('tenant_id', $tenant2->id)
        ->count();

    expect($tenant2Deals)->toBe(1,
        'FINDING E1: Client2 (healthy) did NOT receive the lead. '.
        "Expected 1 deal for tenant2 (id={$tenant2->id}), got {$tenant2Deals}. ".
        'The auto-pause flow should skip Client1 and continue routing to Client2.'
    );

    // Client1 must NOT have a deal (charge rolled back).
    $tenant1Deals = DB::connection('pgsql_supplier')
        ->table('deals')
        ->where('tenant_id', $tenant1->id)
        ->count();

    expect($tenant1Deals)->toBe(0,
        'FINDING E1: Client1 (insufficient balance) has a deal when it should have 0. '.
        "Tenant1 id={$tenant1->id} has {$tenant1Deals} deals. ".
        'InsufficientBalance should roll back the transaction, preventing deal creation.'
    );

    // lead must be marked processed.
    expect($lead->processed_at)->not->toBeNull(
        'FINDING E1: SupplierLead.processed_at is null — lead was not marked processed.'
    );
})->group('imitation');

// ─────────────────────────────────────────────────────────────────────────────
// E2 — Frozen tenant excluded at eligibility filter (before charge)
// ─────────────────────────────────────────────────────────────────────────────

it('E2: frozen tenant is excluded from eligibility; healthy client gets the lead', function (): void {
    // ── ARRANGE ──────────────────────────────────────────────────────────────

    $supplier = SupplierProject::factory()->create([
        'platform' => EF_PLATFORM,
        'signal_type' => 'site',
        'unique_key' => EF_DOMAIN,
    ]);

    // Client1: frozen via ConditionLevers::freeze().
    // After freeze(), frozen_by_balance_at IS NOT NULL → excluded by LeadRouter SQL
    // WHERE tenants.frozen_by_balance_at IS NULL.
    $tenant1 = Tenant::factory()->create([
        'balance_rub' => '9999.00', // balance is fine — frozen is the blocker
        'frozen_by_balance_at' => null,
    ]);
    $project1 = Project::factory()->create([
        'tenant_id' => $tenant1->id,
        'is_active' => true,
        'signal_type' => 'site',
        'signal_identifier' => EF_DOMAIN,
        'daily_limit_target' => EF_HEALTHY_LIMIT,
        'effective_daily_limit_today' => null,
        'delivered_today' => 0,
        'delivered_in_month' => 0,
        'delivery_days_mask' => 127,
        'preflight_blocked_at' => null,
        'regions' => [EF_MOSCOW_CODE],
    ]);
    linkProjectToSupplier($project1, $supplier);

    // Freeze Client1 BEFORE snapshot rebuild.
    // LeadRouter SQL: AND tenants.frozen_by_balance_at IS NULL
    // Frozen clients are excluded at the query stage — no charge attempt is made.
    ConditionLevers::freeze($tenant1);

    // Client2: healthy.
    $tenant2 = Tenant::factory()->create([
        'balance_rub' => '9999.00',
        'frozen_by_balance_at' => null,
    ]);
    $project2 = Project::factory()->create([
        'tenant_id' => $tenant2->id,
        'is_active' => true,
        'signal_type' => 'site',
        'signal_identifier' => EF_DOMAIN,
        'daily_limit_target' => EF_HEALTHY_LIMIT,
        'effective_daily_limit_today' => null,
        'delivered_today' => 0,
        'delivered_in_month' => 0,
        'delivery_days_mask' => 127,
        'preflight_blocked_at' => null,
        'regions' => [EF_MOSCOW_CODE],
    ]);
    linkProjectToSupplier($project2, $supplier);

    $activeDate = SnapshotForge::activeDate();

    // Snapshots for both clients. The snapshot itself may include Client1 (snapshot was
    // built from static project data), but LeadRouter's SQL live-checks frozen_by_balance_at.
    createRoutingSnapshotFromProject(
        project: $project1,
        date: $activeDate,
        signalType: 'site',
        signalIdentifier: EF_DOMAIN,
        dailyLimit: EF_HEALTHY_LIMIT,
        regions: '{'.EF_MOSCOW_CODE.'}',
    );
    createRoutingSnapshotFromProject(
        project: $project2,
        date: $activeDate,
        signalType: 'site',
        signalIdentifier: EF_DOMAIN,
        dailyLimit: EF_HEALTHY_LIMIT,
        regions: '{'.EF_MOSCOW_CODE.'}',
    );

    $phone = '79161234002';
    $fakeDaData = new FakeDaDataPhoneClient;
    $fakeDaData->stub($phone, qc: 0, region: 'Москва', provider: 'МТС');
    app()->instance(DaDataPhoneClient::class, $fakeDaData);

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

    $injector = new LeadInjector;
    $lead = $injector->site(
        domain: EF_DOMAIN,
        phone: $phone,
        tag: 'Москва',
        platform: EF_PLATFORM,
        vid: 1_100_000_002,
    );

    // ── ASSERT ───────────────────────────────────────────────────────────────

    // Client1 (frozen) must have ZERO deals — excluded at filter stage.
    $tenant1Deals = DB::connection('pgsql_supplier')
        ->table('deals')
        ->where('tenant_id', $tenant1->id)
        ->count();

    expect($tenant1Deals)->toBe(0,
        'FINDING E2: Frozen client (tenant1) received a deal. '.
        "Expected 0 deals for tenant1 (id={$tenant1->id}), got {$tenant1Deals}. ".
        'LeadRouter SQL WHERE tenants.frozen_by_balance_at IS NULL should exclude this tenant. '.
        'This is a serious billing/freeze bug.'
    );

    // Client2 (healthy) must have received the lead.
    $tenant2Deals = DB::connection('pgsql_supplier')
        ->table('deals')
        ->where('tenant_id', $tenant2->id)
        ->count();

    expect($tenant2Deals)->toBe(1,
        'FINDING E2: Healthy Client2 did NOT receive the lead. '.
        "Expected 1 deal for tenant2 (id={$tenant2->id}), got {$tenant2Deals}. ".
        'With frozen Client1 excluded, Client2 should be the sole eligible recipient.'
    );

    // Verify tenant1 IS still frozen (no accidental unfreeze by any path).
    $tenant1->refresh();
    expect($tenant1->frozen_by_balance_at)->not->toBeNull(
        'FINDING E2: tenant1.frozen_by_balance_at was cleared during routing. '.
        'Freeze state must be preserved across the routing cycle.'
    );

    // lead processed.
    expect($lead->processed_at)->not->toBeNull(
        'FINDING E2: SupplierLead.processed_at is null — lead was not marked processed.'
    );
})->group('imitation');

// ─────────────────────────────────────────────────────────────────────────────
// F — Daily limit reached: project excluded from eligibility
// ─────────────────────────────────────────────────────────────────────────────

it('F: client at daily limit is excluded; lead goes to client with remaining capacity', function (): void {
    // ── ARRANGE ──────────────────────────────────────────────────────────────

    $supplier = SupplierProject::factory()->create([
        'platform' => EF_PLATFORM,
        'signal_type' => 'site',
        'unique_key' => EF_DOMAIN,
    ]);

    $dailyLimit = 5; // small limit so fillToLimit is clear

    // Client1: delivered_today EQUAL to daily_limit → excluded (delivered_today < daily_limit is false).
    $tenant1 = Tenant::factory()->create([
        'balance_rub' => '9999.00',
        'frozen_by_balance_at' => null,
    ]);
    $project1 = Project::factory()->create([
        'tenant_id' => $tenant1->id,
        'is_active' => true,
        'signal_type' => 'site',
        'signal_identifier' => EF_DOMAIN,
        'daily_limit_target' => $dailyLimit,
        'effective_daily_limit_today' => null,
        'delivered_today' => 0,
        'delivered_in_month' => 0,
        'delivery_days_mask' => 127,
        'preflight_blocked_at' => null,
        'regions' => [EF_MOSCOW_CODE],
    ]);
    linkProjectToSupplier($project1, $supplier);

    // Client2: healthy with headroom.
    $tenant2 = Tenant::factory()->create([
        'balance_rub' => '9999.00',
        'frozen_by_balance_at' => null,
    ]);
    $project2 = Project::factory()->create([
        'tenant_id' => $tenant2->id,
        'is_active' => true,
        'signal_type' => 'site',
        'signal_identifier' => EF_DOMAIN,
        'daily_limit_target' => EF_HEALTHY_LIMIT,
        'effective_daily_limit_today' => null,
        'delivered_today' => 0,
        'delivered_in_month' => 0,
        'delivery_days_mask' => 127,
        'preflight_blocked_at' => null,
        'regions' => [EF_MOSCOW_CODE],
    ]);
    linkProjectToSupplier($project2, $supplier);

    $activeDate = SnapshotForge::activeDate();

    // Build snapshots BEFORE setting delivered_today to limit,
    // so both clients appear in the snapshot.
    createRoutingSnapshotFromProject(
        project: $project1,
        date: $activeDate,
        signalType: 'site',
        signalIdentifier: EF_DOMAIN,
        dailyLimit: $dailyLimit,
        regions: '{'.EF_MOSCOW_CODE.'}',
    );
    createRoutingSnapshotFromProject(
        project: $project2,
        date: $activeDate,
        signalType: 'site',
        signalIdentifier: EF_DOMAIN,
        dailyLimit: EF_HEALTHY_LIMIT,
        regions: '{'.EF_MOSCOW_CODE.'}',
    );

    // NOW set Client1 delivered_today = limit (5) so it fails the eligibility check:
    // LeadRouter SQL: AND projects.delivered_today < snap.daily_limit
    // Also the inner createDealCopyForProject recheck: $lockedProject->delivered_today >= $effectiveLimit
    ConditionLevers::fillToLimit($project1);

    $phone = '79161234003';
    $fakeDaData = new FakeDaDataPhoneClient;
    $fakeDaData->stub($phone, qc: 0, region: 'Москва', provider: 'МТС');
    app()->instance(DaDataPhoneClient::class, $fakeDaData);

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

    $injector = new LeadInjector;
    $lead = $injector->site(
        domain: EF_DOMAIN,
        phone: $phone,
        tag: 'Москва',
        platform: EF_PLATFORM,
        vid: 1_100_000_003,
    );

    // ── ASSERT ───────────────────────────────────────────────────────────────

    // Client1 (at limit) must have ZERO deals.
    $tenant1Deals = DB::connection('pgsql_supplier')
        ->table('deals')
        ->where('tenant_id', $tenant1->id)
        ->count();

    expect($tenant1Deals)->toBe(0,
        'FINDING F: Client1 (at daily limit) received a deal. '.
        "Expected 0 deals for tenant1 (id={$tenant1->id}), got {$tenant1Deals}. ".
        'LeadRouter SQL: projects.delivered_today < snap.daily_limit excludes at-limit projects. '.
        "project1.daily_limit_target={$dailyLimit}, delivered_today should be {$dailyLimit} after fillToLimit."
    );

    // Client2 (has headroom) must receive exactly 1 deal.
    $tenant2Deals = DB::connection('pgsql_supplier')
        ->table('deals')
        ->where('tenant_id', $tenant2->id)
        ->count();

    expect($tenant2Deals)->toBe(1,
        'FINDING F: Client2 (with headroom) did NOT receive the lead. '.
        "Expected 1 deal for tenant2 (id={$tenant2->id}), got {$tenant2Deals}. ".
        'With Client1 at limit and excluded, Client2 should be the sole eligible recipient.'
    );

    // Verify project1 delivered_today is still at limit (nothing was delivered to it).
    $project1->refresh();
    expect((int) $project1->delivered_today)->toBe($dailyLimit,
        'FINDING F: project1.delivered_today changed during routing. '.
        "Expected {$dailyLimit} (untouched), got {$project1->delivered_today}. ".
        'A lead was delivered to a project that exceeded its limit.'
    );

    // project1 is_active should still be true — limit exhaustion alone does NOT trigger
    // auto-pause (only InsufficientBalance does). This is a deliberate design check.
    expect($project1->is_active)->toBeTrue(
        'FINDING F: project1.is_active was set to false due to limit exhaustion. '.
        'DESIGN NOTE: daily-limit exhaustion alone must NOT trigger auto-pause. '.
        'Auto-pause (is_active=false) is only triggered by InsufficientBalance (billing failure). '.
        'If this fails: auto-pause is being triggered by the wrong condition — report as bug.'
    );

    // lead processed.
    expect($lead->processed_at)->not->toBeNull(
        'FINDING F: SupplierLead.processed_at is null — lead was not marked processed.'
    );
})->group('imitation');
