<?php

declare(strict_types=1);

namespace Tests\Feature\Pwa;

use App\Models\Client;
use App\Models\CollectionVisit;
use App\Models\Company;
use App\Models\Credit;
use App\Models\Plan;
use App\Models\Subscription;
use App\Models\User;
use Illuminate\Database\Events\QueryExecuted;
use Illuminate\Foundation\Testing\DatabaseTransactions;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Str;
use Illuminate\Testing\TestResponse;
use PHPUnit\Framework\Attributes\Test;
use Spatie\Permission\Models\Role;
use Tests\TestCase;

/**
 * POST /api/pwa/visits con la misma clave que ya tiene una visita: se devuelve la
 * original como duplicado, también si dos envíos chocan (el índice único de
 * `collection_visits.idempotency_key` frena al segundo), igual que el lote.
 */
class CollectionVisitIdempotencyTest extends TestCase
{
    use DatabaseTransactions;

    private Company $company;

    private User $collector;

    private Credit $credit;

    protected function setUp(): void
    {
        parent::setUp();

        Role::firstOrCreate(['name' => 'collector', 'guard_name' => 'web']);
        $plan = Plan::factory()->create(['has_pwa_access' => true]);
        $this->company = Company::factory()->create();
        Subscription::factory()->active()->create([
            'company_id' => $this->company->id,
            'plan_id' => $plan->id,
            'ends_at' => now()->addYear(),
        ]);

        $this->collector = User::factory()->create(['company_id' => $this->company->id]);
        $this->collector->assignRole('collector');
        $this->collector = $this->collector->fresh();

        $this->credit = Credit::factory()->create([
            'company_id' => $this->company->id,
            'client_id' => Client::factory()->create(['company_id' => $this->company->id])->id,
            'collector_user_id' => $this->collector->id,
            'status' => Credit::STATUS_ACTIVE,
        ]);
    }

    private function visita(string $clave): TestResponse
    {
        return $this->actingAs($this->collector, 'sanctum')->postJson('/api/pwa/visits', [
            'credit_id' => $this->credit->id,
            'visit_type' => 'no_payment',
            'reason' => 'no_money',
            'visit_date' => now()->toDateString(),
            'idempotency_key' => $clave,
        ]);
    }

    private function visitasConClave(string $clave): int
    {
        return CollectionVisit::withoutGlobalScopes()->where('idempotency_key', $clave)->count();
    }

    /** Una visita insertada directo: la que dejó "el otro" envío con la misma clave. */
    private function insertarVisita(string $clave): int
    {
        return DB::table('collection_visits')->insertGetId([
            'company_id' => $this->company->id,
            'credit_id' => $this->credit->id,
            'collector_user_id' => $this->collector->id,
            'visit_type' => 'no_payment',
            'reason' => 'no_money',
            'visit_date' => now()->toDateString(),
            'idempotency_key' => $clave,
            'created_at' => now(),
            'updated_at' => now(),
        ]);
    }

    #[Test]
    public function la_misma_clave_dos_veces_es_una_sola_visita(): void
    {
        $clave = (string) Str::uuid();

        $primera = $this->visita($clave)->assertCreated();

        $this->visita($clave)
            ->assertOk()
            ->assertJsonPath('duplicate', true)
            ->assertJsonPath('visit.id', $primera->json('visit.id'));

        $this->assertSame(1, $this->visitasConClave($clave));
    }

    #[Test]
    public function en_una_carrera_el_indice_frena_al_segundo_y_se_devuelve_la_original(): void
    {
        $clave = (string) Str::uuid();
        $original = null;

        // El otro envío con la misma clave gana entre la búsqueda y el insert: se
        // planta su visita justo después de que la búsqueda por clave vuelve vacía.
        DB::listen(function (QueryExecuted $consulta) use ($clave, &$original): void {
            if ($original !== null
                || ! str_starts_with(strtolower(ltrim($consulta->sql)), 'select')
                || ! str_contains($consulta->sql, 'collection_visits')
                || ! in_array($clave, $consulta->bindings, true)) {
                return;
            }

            $original = $this->insertarVisita($clave);
        });

        // Antes: el 1062 caía en el catch genérico y respondía 500.
        $this->visita($clave)
            ->assertOk()
            ->assertJsonPath('duplicate', true)
            ->assertJsonPath('visit.id', $original);

        $this->assertSame(1, $this->visitasConClave($clave));
    }

    #[Test]
    public function una_clave_que_ya_usa_otra_empresa_responde_el_500_de_siempre_sin_filtrar_la_excepcion(): void
    {
        // El índice único de la clave es global: si la tiene otra empresa, la
        // búsqueda (por empresa) no la ve y el insert choca. No es un duplicado
        // propio: se responde el 500 de siempre, con su registro en el log.
        $clave = (string) Str::uuid();
        $otra = Company::factory()->create();
        $otroCobrador = User::factory()->create(['company_id' => $otra->id]);
        DB::table('collection_visits')->insert([
            'company_id' => $otra->id,
            'credit_id' => Credit::factory()->create([
                'company_id' => $otra->id,
                'client_id' => Client::factory()->create(['company_id' => $otra->id])->id,
                'collector_user_id' => $otroCobrador->id,
            ])->id,
            'collector_user_id' => $otroCobrador->id,
            'visit_type' => 'no_payment',
            'visit_date' => now()->toDateString(),
            'idempotency_key' => $clave,
            'created_at' => now(),
            'updated_at' => now(),
        ]);

        $this->visita($clave)
            ->assertStatus(500)
            ->assertExactJson(['message' => 'Error al registrar la visita. Intenta de nuevo.']);

        $this->assertSame(1, $this->visitasConClave($clave));
    }
}
