<?php

declare(strict_types=1);

use App\Services\MonthlyPartitionManager;
use Illuminate\Support\Facades\Artisan;
use Illuminate\Support\Facades\DB;

/**
 * Тесты Artisan-команды `partitions:create-months` (замена pg_partman).
 *
 * NB: команда создаёт партиции через DDL (CREATE TABLE) — это глобальное
 * действие, не Rolled Back trait'ом DatabaseTransactions. Поэтому каждый
 * тест чистит за собой созданные партиции вручную (DROP TABLE IF EXISTS).
 */
beforeEach(function () {
    // Снимок партиций ДО теста для cleanup.
    $this->partitionsBefore = collect(DB::select("
        SELECT relname FROM pg_class
        WHERE relkind = 'r'
          AND relname ~ '^(deals|supplier_lead_costs|auth_log|activity_log|tenant_operations_log|balance_transactions|pd_processing_log|saas_admin_audit_log)_[0-9]{4}_[0-9]{2}$'
    "))->pluck('relname')->all();
});

afterEach(function () {
    $partitionsAfter = collect(DB::select("
        SELECT relname FROM pg_class
        WHERE relkind = 'r'
          AND relname ~ '^(deals|supplier_lead_costs|auth_log|activity_log|tenant_operations_log|balance_transactions|pd_processing_log|saas_admin_audit_log)_[0-9]{4}_[0-9]{2}$'
    "))->pluck('relname')->all();

    // DETACH перед DROP: иначе `DROP TABLE ... CASCADE` сносит FK от
    // webhook_dedup_keys → deals (parent partitioned table), и
    // последующий ON DELETE CASCADE тест валится.
    // Восстанавливаем имя parent-таблицы из имени партиции `<parent>_yYYYY_mMM`
    foreach (array_diff($partitionsAfter, $this->partitionsBefore) as $partition) {
        $parent = preg_replace('/_y?\d{4}_m?\d{2}$/', '', $partition);
        DB::statement("ALTER TABLE {$parent} DETACH PARTITION {$partition}");
        DB::statement("DROP TABLE IF EXISTS {$partition}");
    }
});

test('создаёт партиции на N месяцев вперёд для deals и supplier_lead_costs', function () {
    $exitCode = Artisan::call('partitions:create-months', ['--ahead' => 8]);

    expect($exitCode)->toBe(0);

    // Должны быть партиции до текущий+8 месяцев включительно.
    $futureMonth = now()->startOfMonth()->addMonths(8);
    $expectedDealName = 'deals_'.'y'.$futureMonth->format('Y').'_m'.$futureMonth->format('m');
    $expectedCostName = 'supplier_lead_costs_'.'y'.$futureMonth->format('Y').'_m'.$futureMonth->format('m');

    $row = DB::selectOne("SELECT 1 AS x FROM pg_class WHERE relname = ? AND relkind = 'r'", [$expectedDealName]);
    expect($row)->not->toBeNull();

    $row = DB::selectOne("SELECT 1 AS x FROM pg_class WHERE relname = ? AND relkind = 'r'", [$expectedCostName]);
    expect($row)->not->toBeNull();
});

test('идемпотентность: повторный запуск не падает и не создаёт дубли', function () {
    Artisan::call('partitions:create-months', ['--ahead' => 5]);
    $afterFirst = collect(DB::select("
        SELECT relname FROM pg_class
        WHERE relkind = 'r'
          AND relname ~ '^(deals|supplier_lead_costs|auth_log|activity_log|tenant_operations_log|balance_transactions|pd_processing_log|saas_admin_audit_log)_[0-9]{4}_[0-9]{2}$'
    "))->count();

    // Повторный запуск — должен только skip'ать.
    $exitCode = Artisan::call('partitions:create-months', ['--ahead' => 5]);
    expect($exitCode)->toBe(0);

    $afterSecond = collect(DB::select("
        SELECT relname FROM pg_class
        WHERE relkind = 'r'
          AND relname ~ '^(deals|supplier_lead_costs|auth_log|activity_log|tenant_operations_log|balance_transactions|pd_processing_log|saas_admin_audit_log)_[0-9]{4}_[0-9]{2}$'
    "))->count();

    expect($afterSecond)->toBe($afterFirst);

    // Output второго запуска должен сказать «0 created» по всем партиционированным таблицам × 6 месяцев
    // (текущий + ahead=5). Число таблиц берём из PARTITIONED_TABLES — тест не ломается при добавлении новых.
    $expectedSkipped = count(MonthlyPartitionManager::PARTITIONED_TABLES) * 6;
    $output = Artisan::output();
    expect($output)->toContain("0 created, {$expectedSkipped} skipped");
});

test('--ahead=0 создаёт только текущий месяц', function () {
    Artisan::call('partitions:create-months', ['--ahead' => 0]);

    $currentMonth = now()->startOfMonth();
    $name = 'deals_y'.$currentMonth->format('Y').'_m'.$currentMonth->format('m');

    $row = DB::selectOne("SELECT 1 AS x FROM pg_class WHERE relname = ? AND relkind = 'r'", [$name]);
    expect($row)->not->toBeNull();
});

test('партиция корректно принимает INSERT в окно своего месяца', function () {
    Artisan::call('partitions:create-months', ['--ahead' => 8]);

    // Проверяем, что INSERT в deals с received_at в новой партиции работает.
    $futureMonth = now()->startOfMonth()->addMonths(8);
    $tenantId = DB::table('tenants')->insertGetId([
        'subdomain' => 'partition-test-'.uniqid(),
        'organization_name' => 'PartitionTest',
        'contact_email' => 'pt@test.local',
        'api_key_limit' => 5,
    ]);
    $projectId = DB::table('projects')->insertGetId([
        'tenant_id' => $tenantId,
        'name' => 'PartitionProject',
        'type' => 'webhook',
        'is_active' => true,
        'daily_limit_target' => 10,
        'region_mask' => 255,
        'region_mode' => 'include',
        'delivery_days_mask' => 127,
        'assignment_strategy' => 'manual',
        'ttfr_target_minutes' => 15,
    ]);

    $dealId = DB::table('deals')->insertGetId([
        'tenant_id' => $tenantId,
        'project_id' => $projectId,
        'phone' => '79001234567',
        'status' => 'new',
        'received_at' => $futureMonth->copy()->addDays(5),
    ]);

    expect($dealId)->toBeInt();

    // Cleanup (за пределами trait DatabaseTransactions, т.к. INSERT через DB::table в новой партиции).
    DB::table('deals')->where('id', $dealId)->delete();
    DB::table('projects')->where('id', $projectId)->delete();
    DB::table('tenants')->where('id', $tenantId)->delete();
});
