<?php

declare(strict_types=1);

use App\Models\Project;
use Database\Seeders\PricingTierSeeder;
use Illuminate\Foundation\Testing\DatabaseTransactions;
use Illuminate\Support\Facades\Artisan;
use Illuminate\Support\Facades\DB;
use Tests\Concerns\SharesSupplierPdo;
use Tests\Support\Imitation\ImitationClientsSeeder;

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

/**
 * Schema bootstrap for the liderra_testing DB (idempotent, runs per-test).
 *
 * ─── WHY THIS EXISTS ────────────────────────────────────────────────────────
 * `php artisan migrate:fresh` wraps each migration in a Laravel DB transaction.
 * The initial migration calls `Artisan::call('partitions:create-months')` which
 * opens a NEW pgsql_supplier connection. A new connection cannot see the
 * uncommitted changes of the initial migration's pgsql connection in PostgreSQL
 * READ COMMITTED isolation. Result: "table does not exist" when creating partitions.
 *
 * In addition, MonthlyPartitionManager::PARTITIONED_TABLES includes tables
 * (project_routing_snapshots, lead_region_resolution_log) that are NOT in
 * schema.sql but in later delta migrations — so partitions:create-months also
 * fails because those parent tables don't exist yet.
 *
 * ─── FIX ─────────────────────────────────────────────────────────────────────
 * In beforeEach() (after SharesSupplierPdo has made pgsql_supplier share the
 * same PDO as pgsql), we:
 *   1. Check if the schema is already loaded (fast no-op if so).
 *   2. If not: load schema.sql, then manually create the two delta-migration
 *      tables that partitions:create-months needs.
 *   3. Then call Artisan::call('partitions:create-months') — with shared PDO,
 *      pgsql_supplier sees our in-transaction DDL.
 *   4. Mark remaining delta migrations as "ran" so migrate won't re-run them.
 *
 * Since DatabaseTransactions rolls back at test end, this is ephemeral.
 * If liderra_testing was externally migrated the schema already exists →
 * we skip and proceed directly.
 */
beforeEach(function (): void {
    // Fast path: if tenants table exists the DB is already set up — skip setup.
    $hasTenants = DB::selectOne(
        "SELECT 1 FROM information_schema.tables
         WHERE table_schema = 'public' AND table_name = 'tenants'"
    );

    if ($hasTenants === null) {
        $repoRoot = dirname(base_path());

        // Step 1: Load the base schema.sql (creates most tables, triggers, RLS, etc.)
        $schemaPath = $repoRoot.DIRECTORY_SEPARATOR.'db'.DIRECTORY_SEPARATOR.'schema.sql';
        if (! is_readable($schemaPath)) {
            throw new RuntimeException("schema.sql not found: {$schemaPath}");
        }
        DB::unprepared((string) file_get_contents($schemaPath));

        // Step 2: Fix the webhook_dedup_keys FK that PDO sometimes swallows.
        try {
            DB::statement(<<<'SQL'
                ALTER TABLE webhook_dedup_keys
                ADD FOREIGN KEY (deal_id, deal_received_at)
                REFERENCES deals (id, received_at)
                ON DELETE CASCADE
                DEFERRABLE INITIALLY DEFERRED
            SQL);
        } catch (Throwable) { /* already exists — idempotent */
        }

        // Step 3: Create tables that are in PARTITIONED_TABLES but NOT in schema.sql
        // (they were added via delta migrations). partitions:create-months needs them.

        // 3a. project_routing_snapshots (delta migration 2026_05_27_120000)
        DB::unprepared(<<<'SQL'
            CREATE TABLE IF NOT EXISTS project_routing_snapshots (
                snapshot_date      DATE        NOT NULL,
                project_id         BIGINT      NOT NULL,
                tenant_id          BIGINT      NOT NULL,
                daily_limit        INT         NOT NULL CHECK (daily_limit >= 0),
                delivery_days_mask INT         NOT NULL CHECK (delivery_days_mask BETWEEN 0 AND 127),
                regions            INT[]       NOT NULL DEFAULT '{}',
                signal_type        TEXT        NOT NULL CHECK (signal_type IN ('call','site','sms')),
                signal_identifier  TEXT,
                sms_senders        JSONB,
                sms_keyword        TEXT,
                expected_volume    INT         NOT NULL CHECK (expected_volume >= 0),
                delivered_count    INT         NOT NULL DEFAULT 0 CHECK (delivered_count >= 0),
                created_at         TIMESTAMP   NOT NULL DEFAULT NOW(),
                PRIMARY KEY (snapshot_date, project_id),
                FOREIGN KEY (tenant_id) REFERENCES tenants(id) ON DELETE CASCADE
            ) PARTITION BY RANGE (snapshot_date)
        SQL);

        try {
            DB::statement(
                'CREATE INDEX project_routing_snapshots_tenant_date_idx
                 ON project_routing_snapshots (tenant_id, snapshot_date)'
            );
        } catch (Throwable) { /* idempotent */
        }

        try {
            DB::statement('ALTER TABLE project_routing_snapshots ENABLE ROW LEVEL SECURITY');
            DB::statement(
                "CREATE POLICY project_routing_snapshots_tenant_isolation
                 ON project_routing_snapshots
                 USING (tenant_id = current_setting('app.current_tenant_id', true)::bigint)"
            );
        } catch (Throwable) { /* idempotent */
        }

        // 3b. phone_ranges_imports + phone_ranges + lead_region_resolution_log
        //     (delta migration 2026_05_31_100000)
        DB::unprepared(<<<'SQL'
            CREATE TABLE IF NOT EXISTS phone_ranges_imports (
                id              BIGSERIAL PRIMARY KEY,
                imported_at     TIMESTAMPTZ NOT NULL DEFAULT NOW(),
                source_url      TEXT NOT NULL,
                rows_inserted   INTEGER NOT NULL DEFAULT 0,
                rows_updated    INTEGER NOT NULL DEFAULT 0,
                checksum_sha256 TEXT NOT NULL,
                status          TEXT NOT NULL DEFAULT 'in_progress'
                    CHECK (status IN ('in_progress','completed','failed','rolled_back')),
                error           TEXT,
                completed_at    TIMESTAMPTZ
            )
        SQL);

        DB::unprepared(<<<'SQL'
            CREATE TABLE IF NOT EXISTS phone_ranges (
                id                BIGSERIAL PRIMARY KEY,
                def_code          SMALLINT NOT NULL,
                from_num          BIGINT NOT NULL,
                to_num            BIGINT NOT NULL,
                operator          TEXT NOT NULL,
                region            TEXT NOT NULL,
                region_normalized TEXT,
                subject_code      SMALLINT,
                imported_at       TIMESTAMPTZ NOT NULL,
                import_id         BIGINT NOT NULL REFERENCES phone_ranges_imports(id),
                CONSTRAINT chk_phone_ranges_def_code CHECK (def_code BETWEEN 300 AND 999),
                CONSTRAINT chk_phone_ranges_subject_code
                    CHECK (subject_code IS NULL OR subject_code BETWEEN 1 AND 89),
                CONSTRAINT chk_phone_ranges_range_valid CHECK (from_num <= to_num)
            )
        SQL);

        DB::unprepared(<<<'SQL'
            CREATE TABLE IF NOT EXISTS lead_region_resolution_log (
                id                       BIGSERIAL,
                supplier_lead_id         BIGINT NOT NULL,
                received_at              TIMESTAMPTZ NOT NULL,
                phone_masked             TEXT NOT NULL,
                subject_code_resolved    SMALLINT,
                subject_code_from_tag    SMALLINT,
                region_source            TEXT NOT NULL
                    CHECK (region_source IN ('dadata','rossvyaz','tag','unknown')),
                dadata_qc                SMALLINT,
                dadata_provider          TEXT,
                dadata_type              TEXT,
                dadata_response_masked   JSONB,
                rossvyaz_matched         BOOLEAN NOT NULL DEFAULT FALSE,
                actual_subject_code      SMALLINT
                    CHECK (actual_subject_code IS NULL OR actual_subject_code BETWEEN 1 AND 89),
                substituted_subject_code SMALLINT
                    CHECK (substituted_subject_code IS NULL OR substituted_subject_code BETWEEN 1 AND 89),
                routing_step             SMALLINT,
                cache_hit                BOOLEAN NOT NULL DEFAULT FALSE,
                duration_ms              INTEGER,
                error                    TEXT,
                PRIMARY KEY (id, received_at)
            ) PARTITION BY RANGE (received_at)
        SQL);

        try {
            DB::statement('ALTER TABLE lead_region_resolution_log ENABLE ROW LEVEL SECURITY');
        } catch (Throwable) { /* idempotent */
        }

        // Step 4: Create month partitions — with shared PDO, pgsql_supplier sees
        // all the tables we just created within the same transaction.
        Artisan::call('partitions:create-months', ['--ahead' => 2]);

        // Step 5: Mark the delta migrations as "ran" so they don't re-run if
        // someone calls Artisan::call('migrate') later in the test suite.
        $deltaRan = [
            '2026_05_27_120000_create_project_routing_snapshots_table',
            '2026_05_31_100000_create_phone_ranges_and_resolution_log',
        ];
        foreach ($deltaRan as $migration) {
            try {
                DB::table('migrations')->updateOrInsert(
                    ['migration' => $migration],
                    ['batch' => 1],
                );
            } catch (Throwable) { /* ok if migrations table doesn't exist */
            }
        }
    }

    // Seed pricing tiers (required by LedgerService::chargeForDelivery).
    (new PricingTierSeeder)->run();

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

/**
 * Task 4 — ImitationClientsSeeder: single-project matrix (36 rows).
 *
 * Matrix axes:
 *   signal  ∈ {site, call}             — 2
 *   regions ∈ {[], [82], [82,83]}      — 3  (empty=all-RF, [82]=Moscow, [82,83]=Moscow+SPb)
 *   days    ∈ {127 (7 days), 31 (Mon-Fri)} — 2
 *   limit   ∈ {3, 30, 300}             — 3
 *   Total: 2 × 3 × 2 × 3 = 36
 *
 * All project names are prefixed `IMIT-single-`.
 */
it('seeds the single-project matrix', function (): void {
    (new ImitationClientsSeeder)->run();

    expect(Project::where('name', 'like', 'IMIT-single-%')->count())->toBe(36);
})->group('imitation');
