<?php

declare(strict_types=1);

use App\Models\Project;
use App\Models\SupplierProject;
use App\Models\Tenant;
use App\Services\Supplier\Import\SupplierProjectImporter;
use App\Services\Supplier\SupplierPortalClient;
use Illuminate\Foundation\Testing\DatabaseTransactions;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Http;
use Illuminate\Support\Str;
use Tests\Concerns\SharesSupplierPdo;

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

/**
 * @param  list<array<string, mixed>>  $rows
 */
function importerWithRows(array $rows): SupplierProjectImporter
{
    $client = Mockery::mock(SupplierPortalClient::class);
    $client->shouldReceive('listProjects')->andReturn($rows);

    return new SupplierProjectImporter($client);
}

test('buildPlan groups B1/B2/B3 call rows into one planned project, limit = sum', function (): void {
    $tenant = Tenant::factory()->create();

    $plan = importerWithRows([
        ['id' => '4001', 'src' => 'rt', 'type' => 'calls', 'content' => '79991112233', 'tag' => 'Каранга', 'lim' => '6', 'status' => true, 'regions' => '', 'workdays' => ['1', '2', '3', '4', '5']],
        ['id' => '4002', 'src' => 'bl', 'type' => 'calls', 'content' => '79991112233', 'tag' => 'Каранга', 'lim' => '6', 'status' => true, 'regions' => '', 'workdays' => ['1', '2', '3', '4', '5']],
        ['id' => '4003', 'src' => 'mt', 'type' => 'calls', 'content' => '79991112233', 'tag' => 'Каранга', 'lim' => '6', 'status' => true, 'regions' => '', 'workdays' => ['1', '2', '3', '4', '5']],
    ])->buildPlan($tenant->id);

    expect($plan['planned'])->toHaveCount(1);
    $p = $plan['planned'][0];
    expect($p['signal_type'])->toBe('call');
    expect($p['signal_identifier'])->toBe('79991112233');
    expect($p['daily_limit_target'])->toBe(18);
    expect($p['delivery_days_mask'])->toBe(31);
    expect($p['tag'])->toBe('Каранга');
    expect($p['regions'])->toBe([]);
    expect(collect($p['platforms'])->pluck('platform')->sort()->values()->all())->toBe(['B1', 'B2', 'B3']);
    expect(collect($p['platforms'])->firstWhere('platform', 'B1')['external_id'])->toBe(4001);
});

test('buildPlan skips inactive rows (status=false)', function (): void {
    $tenant = Tenant::factory()->create();

    $plan = importerWithRows([
        ['id' => '5001', 'src' => 'rt', 'type' => 'calls', 'content' => '79995550000', 'tag' => 'X', 'lim' => '5', 'status' => false, 'regions' => '', 'workdays' => []],
    ])->buildPlan($tenant->id);

    expect($plan['planned'])->toHaveCount(0);
});

test('buildPlan skips dop2 (unsupported source) and reports it', function (): void {
    $tenant = Tenant::factory()->create();

    $plan = importerWithRows([
        ['id' => '6001', 'src' => 'dop2', 'type' => 'calls', 'content' => '79996660000', 'tag' => 'X', 'lim' => '5', 'status' => true, 'regions' => '', 'workdays' => []],
    ])->buildPlan($tenant->id);

    expect($plan['planned'])->toHaveCount(0);
    expect(collect($plan['skipped'])->pluck('reason'))->toContain('unsupported_source');
});

test('buildPlan reverse-maps regions and unions across platforms', function (): void {
    $tenant = Tenant::factory()->create();

    $plan = importerWithRows([
        ['id' => '7001', 'src' => 'rt', 'type' => 'hosts', 'content' => 'okna.ru', 'tag' => 'Окна', 'lim' => '3', 'status' => true, 'regions' => '24', 'workdays' => [], 'regions_reverse' => false],
        ['id' => '7002', 'src' => 'bl', 'type' => 'hosts', 'content' => 'okna.ru', 'tag' => 'Окна', 'lim' => '3', 'status' => true, 'regions' => '77', 'workdays' => [], 'regions_reverse' => false],
        ['id' => '7003', 'src' => 'mt', 'type' => 'hosts', 'content' => 'okna.ru', 'tag' => 'Окна', 'lim' => '3', 'status' => true, 'regions' => '24', 'workdays' => [], 'regions_reverse' => false],
    ])->buildPlan($tenant->id);

    expect($plan['planned'][0]['regions'])->toBe([29, 82]);
});

test('buildPlan treats any empty-regions platform as all-Russia', function (): void {
    $tenant = Tenant::factory()->create();

    $plan = importerWithRows([
        ['id' => '7101', 'src' => 'rt', 'type' => 'hosts', 'content' => 'all.ru', 'tag' => 'A', 'lim' => '3', 'status' => true, 'regions' => '24', 'workdays' => [], 'regions_reverse' => false],
        ['id' => '7102', 'src' => 'bl', 'type' => 'hosts', 'content' => 'all.ru', 'tag' => 'A', 'lim' => '3', 'status' => true, 'regions' => '', 'workdays' => [], 'regions_reverse' => false],
    ])->buildPlan($tenant->id);

    expect($plan['planned'][0]['regions'])->toBe([]);
});

test('buildPlan skips group when any active row has regions_reverse=true', function (): void {
    $tenant = Tenant::factory()->create();

    $plan = importerWithRows([
        ['id' => '7201', 'src' => 'rt', 'type' => 'hosts', 'content' => 'excl.ru', 'tag' => 'A', 'lim' => '3', 'status' => true, 'regions' => '24', 'workdays' => [], 'regions_reverse' => true],
        ['id' => '7202', 'src' => 'bl', 'type' => 'hosts', 'content' => 'excl.ru', 'tag' => 'A', 'lim' => '3', 'status' => true, 'regions' => '24', 'workdays' => [], 'regions_reverse' => false],
    ])->buildPlan($tenant->id);

    expect($plan['planned'])->toHaveCount(0);
    expect(collect($plan['skipped'])->pluck('reason'))->toContain('regions_exclude');
});

test('buildPlan groups sms by sender: B2 (sender+keyword) and B3 (sender)', function (): void {
    $tenant = Tenant::factory()->create();

    $plan = importerWithRows([
        ['id' => '8001', 'src' => 'bl', 'type' => 'sms', 'content' => '79001234567+KVARTIRA', 'tag' => 'СМС', 'lim' => '4', 'status' => true, 'regions' => '', 'workdays' => []],
        ['id' => '8002', 'src' => 'mt', 'type' => 'sms', 'content' => '79001234567', 'tag' => 'СМС', 'lim' => '4', 'status' => true, 'regions' => '', 'workdays' => []],
    ])->buildPlan($tenant->id);

    expect($plan['planned'])->toHaveCount(1);
    $p = $plan['planned'][0];
    expect($p['signal_type'])->toBe('sms');
    expect($p['signal_identifier'])->toBeNull();
    expect($p['sms_senders'])->toBe(['79001234567']);
    expect($p['sms_keyword'])->toBe('KVARTIRA');
    expect($p['daily_limit_target'])->toBe(8);
    expect(collect($p['platforms'])->pluck('platform')->sort()->values()->all())->toBe(['B2', 'B3']);
});

test('buildPlan handles sms B3-only (no keyword)', function (): void {
    $tenant = Tenant::factory()->create();

    $plan = importerWithRows([
        ['id' => '8101', 'src' => 'mt', 'type' => 'sms', 'content' => '79009998877', 'tag' => 'СМС', 'lim' => '5', 'status' => true, 'regions' => '', 'workdays' => []],
    ])->buildPlan($tenant->id);

    expect($plan['planned'])->toHaveCount(1);
    expect($plan['planned'][0]['sms_senders'])->toBe(['79009998877']);
    expect($plan['planned'][0]['sms_keyword'])->toBeNull();
    expect($plan['planned'][0]['platforms'][0]['platform'])->toBe('B3');
});

test('buildPlan skips a group whose Project already exists for the tenant', function (): void {
    $tenant = Tenant::factory()->create();

    Project::factory()->create([
        'tenant_id' => $tenant->id,
        'signal_type' => 'call',
        'signal_identifier' => '79993332211',
    ]);

    $plan = importerWithRows([
        ['id' => '9001', 'src' => 'rt', 'type' => 'calls', 'content' => '79993332211', 'tag' => 'X', 'lim' => '6', 'status' => true, 'regions' => '', 'workdays' => []],
    ])->buildPlan($tenant->id);

    expect($plan['planned'])->toHaveCount(0);
    expect(collect($plan['skipped'])->pluck('reason'))->toContain('already_exists');
});

test('commit creates Project + supplier_projects (external_id from portal) + pivot, no portal write', function (): void {
    Http::fake(); // ловушка: НИ один HTTP не должен уйти на портал
    $tenant = Tenant::factory()->create();

    $importer = importerWithRows([
        ['id' => '4001', 'src' => 'rt', 'type' => 'calls', 'content' => '79991112233', 'tag' => 'Каранга', 'lim' => '6', 'status' => true, 'regions' => '24', 'workdays' => ['1', '2', '3', '4', '5']],
        ['id' => '4002', 'src' => 'bl', 'type' => 'calls', 'content' => '79991112233', 'tag' => 'Каранга', 'lim' => '6', 'status' => true, 'regions' => '24', 'workdays' => ['1', '2', '3', '4', '5']],
        ['id' => '4003', 'src' => 'mt', 'type' => 'calls', 'content' => '79991112233', 'tag' => 'Каранга', 'lim' => '6', 'status' => true, 'regions' => '24', 'workdays' => ['1', '2', '3', '4', '5']],
    ]);

    $plan = $importer->buildPlan($tenant->id);
    $result = $importer->commit($plan, $tenant->id);

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

    $project = Project::on('pgsql_supplier')
        ->where('tenant_id', $tenant->id)
        ->where('signal_identifier', '79991112233')
        ->first();
    expect($project)->not->toBeNull();
    expect($project->daily_limit_target)->toBe(18);
    expect($project->is_active)->toBeTrue();
    expect($project->regions)->toBe([29]);
    expect($project->delivery_days_mask)->toBe(31);

    $sps = SupplierProject::on('pgsql_supplier')->where('unique_key', '79991112233')->get();
    expect($sps)->toHaveCount(3);
    expect($sps->pluck('supplier_external_id')->sort()->values()->all())->toBe(['4001', '4002', '4003']);
    expect($sps->pluck('sync_status')->unique()->all())->toBe(['ok']);
    expect($sps->firstWhere('platform', 'B1')->current_limit)->toBe(6);

    $pivot = DB::connection('pgsql_supplier')->table('project_supplier_links')
        ->where('project_id', $project->id)->count();
    expect($pivot)->toBe(3);

    Http::assertNothingSent();
});

test('commit reuses an existing supplier_project row instead of duplicating', function (): void {
    Http::fake();
    $tenant = Tenant::factory()->create();

    // supplier_project уже есть (например, создан webhook resolveOrStub ранее)
    SupplierProject::on('pgsql_supplier')->forceCreate([
        'platform' => 'B1',
        'signal_type' => 'call',
        'unique_key' => '79994445566',
        'subject_code' => null,
        'supplier_external_id' => 'EXIST1',
        'current_limit' => 6,
        'current_workdays' => [1, 2, 3, 4, 5],
        'current_regions' => [],
        'sync_status' => 'ok',
        'last_synced_at' => now(),
    ]);

    $importer = importerWithRows([
        ['id' => '4500', 'src' => 'rt', 'type' => 'calls', 'content' => '79994445566', 'tag' => 'Y', 'lim' => '6', 'status' => true, 'regions' => '', 'workdays' => ['1', '2', '3', '4', '5']],
    ]);
    $plan = $importer->buildPlan($tenant->id);
    $importer->commit($plan, $tenant->id);

    // по-прежнему ровно 1 supplier_project с этим ключом+платформой (реюз, не дубль)
    expect(SupplierProject::on('pgsql_supplier')
        ->where('unique_key', '79994445566')->where('platform', 'B1')->count())->toBe(1);

    // pivot привязал существующую строку к новому проекту
    $project = Project::on('pgsql_supplier')->where('signal_identifier', '79994445566')->first();
    $sp = SupplierProject::on('pgsql_supplier')->where('unique_key', '79994445566')->first();
    expect(DB::connection('pgsql_supplier')->table('project_supplier_links')
        ->where('project_id', $project->id)->where('supplier_project_id', $sp->id)->count())->toBe(1);
});

test('buildPlan unions workdays across platforms with different schedules', function (): void {
    $tenant = Tenant::factory()->create();

    // B1 = Пн-Ср [1,2,3] → mask 0b0000111 = 7; B2 = Чт-Пт [4,5] → mask 0b0011000 = 24;
    // union = 31 (Пн-Пт). Тест проверяет реальный OR-merge, не одинаковые расписания.
    $plan = importerWithRows([
        ['id' => '5001', 'src' => 'rt', 'type' => 'calls', 'content' => '79992223344', 'tag' => 'W', 'lim' => '4', 'status' => true, 'regions' => '', 'workdays' => ['1', '2', '3']],
        ['id' => '5002', 'src' => 'bl', 'type' => 'calls', 'content' => '79992223344', 'tag' => 'W', 'lim' => '4', 'status' => true, 'regions' => '', 'workdays' => ['4', '5']],
    ])->buildPlan($tenant->id);

    expect($plan['planned'])->toHaveCount(1);
    expect($plan['planned'][0]['delivery_days_mask'])->toBe(31);
});

test('buildPlan skips sms group when any active row has regions_reverse=true', function (): void {
    $tenant = Tenant::factory()->create();

    $plan = importerWithRows([
        ['id' => '6001', 'src' => 'bl', 'type' => 'sms', 'content' => '79007776655+CODE', 'tag' => 'СМС', 'lim' => '3', 'status' => true, 'regions' => '24', 'workdays' => [], 'regions_reverse' => true],
        ['id' => '6002', 'src' => 'mt', 'type' => 'sms', 'content' => '79007776655', 'tag' => 'СМС', 'lim' => '3', 'status' => true, 'regions' => '24', 'workdays' => [], 'regions_reverse' => false],
    ])->buildPlan($tenant->id);

    expect($plan['planned'])->toHaveCount(0);
    expect(collect($plan['skipped'])->pluck('reason'))->toContain('regions_exclude');
});

test('deriveName uses sms sender as fallback when tag is empty', function (): void {
    $tenant = Tenant::factory()->create();

    // tag='РФ' → попадает в fallback; sms → должен взять sender, а не 'проект'.
    $plan = importerWithRows([
        ['id' => '7001', 'src' => 'mt', 'type' => 'sms', 'content' => '79001112222', 'tag' => 'РФ', 'lim' => '2', 'status' => true, 'regions' => '', 'workdays' => []],
    ])->buildPlan($tenant->id);

    expect($plan['planned'][0]['name'])->toBe('79001112222');
});

// ---------------------------------------------------------------------------
// Stage 4 / Task 4.1 — R-17 (spec §4.4.1): unified buildUniqueKey.
// Before fix buildUniqueKey($p, 'B2') = sender+keyword while buildUniqueKey($p, 'B3')
// = sender alone → orphan supplier_projects rows on rebalance (B2 row keyed under
// sender+keyword, B3 row keyed under sender → can't be reconciled as same group).
// After fix all platforms use buildUniqueKeyAgnostic = sender+keyword for SMS with
// keyword (sender alone only when keyword is null/empty).
// ---------------------------------------------------------------------------

test('R-17 commit creates SMS supplier_projects with UNIFORM unique_key=sender+keyword (no B3 divergence)', function (): void {
    Http::fake();
    $tenant = Tenant::factory()->create();

    $sender = '7903'.fake()->numerify('#######');
    $keyword = 'TASKR17_'.Str::random(5);

    // SMS group with keyword: only B2 + B3 (no B1 — CHECK constraint chk_supplier_projects_b1_not_for_sms).
    // Content format: 'sender+keyword' for B2 (src='bl'), 'sender' for B3 (src='mt') — supplier portal convention.
    $importer = importerWithRows([
        ['id' => '9101', 'src' => 'bl', 'type' => 'sms', 'content' => $sender.'+'.$keyword, 'tag' => 'СМС', 'lim' => '5', 'status' => true, 'regions' => '', 'workdays' => ['1', '2', '3', '4', '5']],
        ['id' => '9102', 'src' => 'mt', 'type' => 'sms', 'content' => $sender, 'tag' => 'СМС', 'lim' => '5', 'status' => true, 'regions' => '', 'workdays' => ['1', '2', '3', '4', '5']],
    ]);
    $plan = $importer->buildPlan($tenant->id);
    $importer->commit($plan, $tenant->id);

    $expected = $sender.'+'.$keyword;

    // Both B2 and B3 supplier_projects must share the SAME unique_key (= sender+keyword).
    $sps = SupplierProject::on('pgsql_supplier')
        ->where('signal_type', 'sms')
        ->whereIn('platform', ['B2', 'B3'])
        ->where(function ($q) use ($expected, $sender) {
            $q->where('unique_key', $expected)->orWhere('unique_key', $sender);
        })
        ->get();
    expect($sps)->toHaveCount(2);
    expect($sps->pluck('unique_key')->unique()->values()->all())->toBe([$expected]);
});
