<?php

declare(strict_types=1);

/**
 * Verification tests — RegionResolverCascadeTest (Task 5, Phase 1 imitation).
 *
 * PROVING tests against existing production code in LeadRegionResolver.
 * If the resolver behaves differently than the plan describes → that is a FINDING,
 * captured in test comments and the final report. Tests assert ACTUAL correct
 * behaviour, not the plan's stale expectations.
 *
 * Plan: docs/superpowers/plans/2026-06-03-portal-client-imitation-phase1.md §Task 5
 * Spec: §7 п.9-17
 *
 * Key verified facts (from reading prod code, migration DDL, README):
 *  - Москва = subject_code 82 (порядковый, НЕ ГИБДД), via RussianRegions::nameToCode()
 *  - phone_ranges schema: def_code (SMALLINT), from_num (BIGINT), to_num (BIGINT),
 *    operator (TEXT), region (TEXT), region_normalized (TEXT), subject_code (SMALLINT),
 *    imported_at (TIMESTAMPTZ), import_id (BIGINT NOT NULL FK phone_ranges_imports).
 *  - ImitationTestCase::seedPhoneRange() uses WRONG columns (range_from/range_to) and
 *    omits import_id — this is a FINDING (F1). We use insertPhoneRange() below instead.
 *  - RossvyazPrefixLookup parses: def_code=digits[1:4], subscriber=digits[4:] (BIGINT)
 *    e.g. phone=79995550011 → def_code=999, subscriber=5550011
 *  - LeadRegionResolver::resolve() does NOT persist to supplier_leads.
 *    Persistence happens in RouteSupplierLeadJob::handle() after calling resolve().
 *    The persistence test (branch 7) calls the job directly.
 *  - qc=2 (мусор) with empty tag → source='unknown' (tagCode=null), NOT 'tag'.
 *    The plan §7 says "source='tag' immediately" but the resolver goes tagFallback()
 *    which returns 'unknown' when tag is empty. This is a FINDING (F2).
 *  - flag off (services.dadata.enabled=false) with empty tag → source='unknown', not 'tag'.
 *    Same reason. FINDING (F3).
 */

use App\Jobs\RouteSupplierLeadJob;
use App\Models\SupplierLead;
use App\Models\SupplierProject;
use App\Services\DaData\DaDataPhoneClient;
use App\Services\LeadRegionResolver;
use App\Support\RussianRegions;
use Database\Seeders\PricingTierSeeder;
use Illuminate\Foundation\Testing\DatabaseTransactions;
use Illuminate\Support\Facades\DB;
use Tests\Concerns\SharesSupplierPdo;
use Tests\Support\Imitation\FakeDaDataPhoneClient;

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

// ---------------------------------------------------------------------------
// Helpers local to this test file
// ---------------------------------------------------------------------------

/**
 * Insert a phone_ranges row correctly, creating a phone_ranges_imports record first.
 * ImitationTestCase::seedPhoneRange() uses wrong column names (FINDING F1).
 *
 * @param  int  $defCode  e.g. 999
 * @param  int  $from  e.g. 5550000
 * @param  int  $to  e.g. 5559999
 * @param  int  $subjectCode  ordinal 1..89
 */
function insertPhoneRange(int $defCode, int $from, int $to, int $subjectCode): void
{
    // Ensure a phone_ranges_imports anchor row exists.
    $importId = DB::table('phone_ranges_imports')->insertGetId([
        'imported_at' => now(),
        'source_url' => 'test://rossvyaz',
        'rows_inserted' => 1,
        'rows_updated' => 0,
        'checksum_sha256' => hash('sha256', "test-{$defCode}-{$from}-{$to}-{$subjectCode}"),
        'status' => 'completed',
        'completed_at' => now(),
    ]);

    DB::table('phone_ranges')->insert([
        'def_code' => $defCode,
        'from_num' => $from,
        'to_num' => $to,
        'operator' => 'test-operator',
        'region' => RussianRegions::CODE_TO_NAME[$subjectCode] ?? 'test-region',
        'region_normalized' => null,
        'subject_code' => $subjectCode,
        'imported_at' => now(),
        'import_id' => $importId,
    ]);
}

/**
 * Create a SupplierLead with a fixed phone for the cascade tests.
 */
function makeLeadWithPhone(string $phone, string $tag = ''): SupplierLead
{
    $sp = SupplierProject::factory()->create();

    return SupplierLead::factory()->create([
        'supplier_project_id' => $sp->id,
        'phone' => $phone,
        'raw_payload' => ['tag' => $tag],
    ]);
}

// ---------------------------------------------------------------------------
// Seed pricing_tiers reference data (required by some full-flow tests).
// ---------------------------------------------------------------------------
beforeEach(function (): void {
    // Tenant context bypass for cross-tenant reads during seeding.
    DB::statement("SELECT set_config('app.current_tenant_id', '0', true)");
    // Reset DaData config to a known state before each test.
    config(['services.dadata.enabled' => false]);
});

// ---------------------------------------------------------------------------
// Branch 1 — feature flag off → tag-fallback
// ---------------------------------------------------------------------------

it('flag services.dadata.enabled=false falls through to tag-fallback (empty tag → unknown)', function (): void {
    // FINDING F3: plan §7 says source='tag'; actual is source='unknown' when tag is empty.
    // tagFallback() → tagCode=null (empty tag, RegionTagResolver returns null) → source='unknown'.
    config(['services.dadata.enabled' => false]);

    $lead = makeLeadWithPhone('79990000001', tag: '');
    $res = app(LeadRegionResolver::class)->resolve($lead);

    expect($res->source)->toBe('unknown')
        ->and($res->subjectCode)->toBeNull()
        ->and($res->cacheHit)->toBeFalse();
})->group('imitation');

it('flag services.dadata.enabled=false with a valid tag resolves to source=tag', function (): void {
    // When tag contains a valid region name, source is 'tag' (not 'unknown').
    config(['services.dadata.enabled' => false]);

    $lead = makeLeadWithPhone('79990000002', tag: 'Москва');
    $res = app(LeadRegionResolver::class)->resolve($lead);

    expect($res->source)->toBe('tag')
        ->and($res->subjectCode)->toBe(RussianRegions::nameToCode()['Москва']);
})->group('imitation');

// ---------------------------------------------------------------------------
// Branch 2 — qc=0 + unambiguous mapped region → source='dadata'
// ---------------------------------------------------------------------------

it('qc=0 + region Москва (unambiguous, maps to subject_code 82) → source=dadata', function (): void {
    // DERIVES code via RussianRegions — does NOT hardcode 82.
    $moscowCode = RussianRegions::nameToCode()['Москва'];

    config(['services.dadata.enabled' => true]);

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

    $lead = makeLeadWithPhone('79990000010');
    $res = app(LeadRegionResolver::class)->resolve($lead);

    expect($res->source)->toBe('dadata')
        ->and($res->subjectCode)->toBe($moscowCode)
        ->and($res->phoneOperator)->toBe('МТС')
        ->and($res->qc)->toBe(0)
        ->and($res->cacheHit)->toBeFalse();
})->group('imitation');

it('qc=0 + ambiguous region (Санкт-Петербург и область) falls through to rossvyaz', function (): void {
    // DaDataRegionMap::isAmbiguous() → true → resolver skips dadata code, goes to Россвязь.
    config(['services.dadata.enabled' => true]);

    // Seed a phone range so Россвязь lookup succeeds.
    // phone 79996660020: def_code=999, subscriber=6660020
    $spbCode = RussianRegions::nameToCode()['Санкт-Петербург'];
    insertPhoneRange(defCode: 999, from: 6660000, to: 6669999, subjectCode: $spbCode);

    $fake = (new FakeDaDataPhoneClient)->stub('79996660020', qc: 0, region: 'Санкт-Петербург и область', provider: 'МегаФон');
    app()->instance(DaDataPhoneClient::class, $fake);

    $lead = makeLeadWithPhone('79996660020');
    $res = app(LeadRegionResolver::class)->resolve($lead);

    expect($res->source)->toBe('rossvyaz')
        ->and($res->rossvyazMatched)->toBeTrue()
        ->and($res->subjectCode)->toBe($spbCode);
})->group('imitation');

// ---------------------------------------------------------------------------
// Branch 3 — qc=1 + phone inside seeded phone_ranges range → source='rossvyaz'
// ---------------------------------------------------------------------------

it('qc=1 + phone inside seeded phone_ranges range → source=rossvyaz, rossvyazMatched=true', function (): void {
    config(['services.dadata.enabled' => true]);

    // Phone 79995550011: def_code = digits[1..3] = 999, subscriber = digits[4..] = 5550011
    $tyumenCode = RussianRegions::nameToCode()['Тюменская область']; // code 77

    insertPhoneRange(defCode: 999, from: 5550000, to: 5559999, subjectCode: $tyumenCode);

    $fake = (new FakeDaDataPhoneClient)->stub('79995550011', qc: 1);
    app()->instance(DaDataPhoneClient::class, $fake);

    $lead = makeLeadWithPhone('79995550011');
    $res = app(LeadRegionResolver::class)->resolve($lead);

    expect($res->source)->toBe('rossvyaz')
        ->and($res->rossvyazMatched)->toBeTrue()
        ->and($res->subjectCode)->toBe($tyumenCode);
})->group('imitation');

// ---------------------------------------------------------------------------
// Branch 4 — qc=2 (мусор/иностранец) → tag-fallback immediately (Россвязь skipped)
// ---------------------------------------------------------------------------

it('qc=2 with empty tag → source=unknown immediately (no rossvyaz)', function (): void {
    // FINDING F2: plan §7 says "source='tag' immediately"; actual behaviour:
    // resolver calls tagFallback() → empty tag → tagCode=null → source='unknown'.
    // The key invariant IS correct: Россвязь is NOT called for qc=2 (mусор).
    config(['services.dadata.enabled' => true]);

    $fake = (new FakeDaDataPhoneClient)->stub('79990000020', qc: 2);
    app()->instance(DaDataPhoneClient::class, $fake);

    $lead = makeLeadWithPhone('79990000020', tag: '');
    $res = app(LeadRegionResolver::class)->resolve($lead);

    // qc=2 → tagFallback → empty tag → source='unknown' (NOT 'tag')
    expect($res->source)->toBe('unknown')
        ->and($res->subjectCode)->toBeNull()
        ->and($res->qc)->toBe(2);
})->group('imitation');

it('qc=2 with valid tag → source=tag (Россвязь skipped)', function (): void {
    // When qc=2 but tag resolves to a region, source='tag' (still no Россвязь).
    config(['services.dadata.enabled' => true]);

    $fake = (new FakeDaDataPhoneClient)->stub('79990000021', qc: 2);
    app()->instance(DaDataPhoneClient::class, $fake);

    $lead = makeLeadWithPhone('79990000021', tag: 'Москва');
    $res = app(LeadRegionResolver::class)->resolve($lead);

    expect($res->source)->toBe('tag')
        ->and($res->subjectCode)->toBe(RussianRegions::nameToCode()['Москва'])
        ->and($res->qc)->toBe(2);
})->group('imitation');

// ---------------------------------------------------------------------------
// Branch 5 — DaData throws DaDataException (degradation) → falls to rossvyaz
// ---------------------------------------------------------------------------

it('DaDataException (degradation) falls through to rossvyaz when range seeded', function (): void {
    config(['services.dadata.enabled' => true]);

    // Phone 79997770030: def_code=999, subscriber=7770030
    $voronezCode = RussianRegions::nameToCode()['Воронежская область']; // code 42
    insertPhoneRange(defCode: 999, from: 7770000, to: 7779999, subjectCode: $voronezCode);

    $fake = (new FakeDaDataPhoneClient)->stubThrows('79997770030');
    app()->instance(DaDataPhoneClient::class, $fake);

    $lead = makeLeadWithPhone('79997770030');
    $res = app(LeadRegionResolver::class)->resolve($lead);

    expect($res->source)->toBe('rossvyaz')
        ->and($res->rossvyazMatched)->toBeTrue()
        ->and($res->subjectCode)->toBe($voronezCode);
})->group('imitation');

it('DaDataException with no rossvyaz range falls through to tag-fallback', function (): void {
    // No phone_ranges seeded → rossvyaz returns null → tagFallback.
    config(['services.dadata.enabled' => true]);

    $fake = (new FakeDaDataPhoneClient)->stubThrows('79998880040');
    app()->instance(DaDataPhoneClient::class, $fake);

    $lead = makeLeadWithPhone('79998880040', tag: '');
    $res = app(LeadRegionResolver::class)->resolve($lead);

    expect($res->source)->toBe('unknown')
        ->and($res->subjectCode)->toBeNull()
        ->and($res->rossvyazMatched)->toBeFalse();
})->group('imitation');

// ---------------------------------------------------------------------------
// Branch 6 — cache: same phone resolved twice → second has cacheHit=true
// ---------------------------------------------------------------------------

it('cache hit: resolving the same phone twice returns cacheHit=true on second call', function (): void {
    config(['services.dadata.enabled' => true]);

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

    $sp = SupplierProject::factory()->create();
    $lead = SupplierLead::factory()->create([
        'supplier_project_id' => $sp->id,
        'phone' => $phone,
        'raw_payload' => ['tag' => ''],
    ]);

    // First resolution — populates cache; cacheHit must be false.
    $res1 = app(LeadRegionResolver::class)->resolve($lead);
    expect($res1->cacheHit)->toBeFalse()
        ->and($res1->source)->toBe('dadata');

    // Second lead with the SAME phone (different row, same cache key).
    $lead2 = SupplierLead::factory()->create([
        'supplier_project_id' => $sp->id,
        'phone' => $phone,
        'raw_payload' => ['tag' => ''],
    ]);

    // Second resolution — must come from cache; DaData NOT called again.
    // The fake has the stub registered, but cacheHit=true proves cache was used.
    $res2 = app(LeadRegionResolver::class)->resolve($lead2);

    expect($res2->cacheHit)->toBeTrue()
        ->and($res2->source)->toBe('dadata')   // source preserved from cached value
        ->and($res2->subjectCode)->toBe($res1->subjectCode);
})->group('imitation');

// ---------------------------------------------------------------------------
// Branch 7 — lead persistence: RouteSupplierLeadJob writes resolver fields to supplier_leads
// ---------------------------------------------------------------------------

it('RouteSupplierLeadJob persists resolved_subject_code/region_source/dadata_qc/phone_operator to supplier_lead', function (): void {
    // NOTE: LeadRegionResolver::resolve() does NOT itself persist to supplier_leads.
    // Persistence is done by RouteSupplierLeadJob::handle() (see line ~159-164).
    // This test exercises that full path via the job.
    //
    // We bind a fake DaData client and dispatch the job synchronously (queue=sync).
    // The job will also call LeadRouter and LedgerService — we seed minimal required
    // data (pricing_tiers + supplier_project) but expect 0 deals (no routing snapshot)
    // and verify only the supplier_leads column updates.

    config(['services.dadata.enabled' => true]);

    // Seed pricing tiers so LedgerService doesn't crash on boot.
    try {
        $seeder = new PricingTierSeeder;
        $seeder->run();
    } catch (Throwable) {
        // Already seeded or not required for this path.
    }

    $phone = '79990000060';
    $moscowCode = RussianRegions::nameToCode()['Москва'];

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

    $sp = SupplierProject::factory()->create();
    $lead = SupplierLead::factory()->create([
        'supplier_project_id' => $sp->id,
        'phone' => $phone,
        'raw_payload' => [
            'tag' => '',
            'project' => 'B1_test.example.com',
            'time' => now()->getTimestamp(),
            'vid' => 123456789,
        ],
        'vid' => 123456789,
        'processed_at' => null,
    ]);

    // Dispatch the job synchronously. It will run the full handle() path.
    // LeadRouter::matchEligibleProjects() will return empty (no snapshot seeded) → 0 deals created.
    // The resolver + persistence UPDATE still executes before the routing loop.
    RouteSupplierLeadJob::dispatchSync($lead->id);

    $lead->refresh();

    expect($lead->resolved_subject_code)->toBe($moscowCode)
        ->and($lead->region_source)->toBe('dadata')
        ->and($lead->dadata_qc)->toBe(0)
        ->and($lead->phone_operator)->toBe('МТС');
})->group('imitation');
