<?php

declare(strict_types=1);

namespace Tests\Feature;

use App\Models\Client;
use App\Models\Company;
use App\Models\Credit;
use App\Models\Installment;
use App\Models\Payment;
use App\Models\Plan;
use App\Models\Subscription;
use App\Models\User;
use Illuminate\Foundation\Testing\DatabaseTransactions;
use Illuminate\Support\Str;
use Illuminate\Testing\TestResponse;
use PHPUnit\Framework\Attributes\Test;
use Spatie\Permission\Models\Role;
use Tests\TestCase;

/**
 * Tests para POST /api/pwa/sync/payments
 *
 * Cubre:
 *   1. Pago válido de collector → success + fila en DB
 *   2. Idempotencia — misma key dos veces → segunda es 'duplicate', sin duplicado en DB
 *   3. Collector no puede pagar crédito de otro collector → held (retenido para revisión)
 *   4. Monto mayor que saldo pendiente → held (retenido para revisión)
 *   5. Crédito de un cobrador ajeno → held (retenido para revisión), no se aplica
 *   6. Batch parcial — 1 válido + 1 inválido → held (retenido para revisión), counts correctos en summary
 *   7. Validación: array vacío retorna 422
 *   8. Admin puede pagar cualquier crédito de la empresa (sin restricción de collector)
 */
class SyncPaymentsTest extends TestCase
{
    use DatabaseTransactions;

    private Company $company;

    private User $collector;

    private User $admin;

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

        foreach (['admin', 'supervisor', 'collector'] as $role) {
            Role::firstOrCreate(['name' => $role, 'guard_name' => 'web']);
        }

        $plan = Plan::factory()->create();
        $this->company = Company::factory()->create();

        Subscription::factory()->create([
            'company_id' => $this->company->id,
            'plan_id' => $plan->id,
            'status' => Subscription::STATUS_ACTIVE,
            'is_active' => true,
            'starts_at' => now()->subMonth(),
            'ends_at' => now()->addYear(),
        ]);

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

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

    // -------------------------------------------------------------------------
    // Helpers
    // -------------------------------------------------------------------------

    /**
     * Crea un crédito activo con una cuota pendiente asignado al collector.
     * remaining_balance = total_amount - amount_paid = $totalAmount.
     */
    private function makeCreditWithInstallment(
        User $collector,
        float $totalAmount = 60_000.0,
    ): Credit {
        $client = Client::factory()->create(['company_id' => $this->company->id]);

        $credit = Credit::factory()->create([
            'company_id' => $this->company->id,
            'client_id' => $client->id,
            'collector_user_id' => $collector->id,
            'created_by_user_id' => $collector->id,
            'status' => Credit::STATUS_ACTIVE,
            'amount' => $totalAmount,
            'installments_count' => 1,
            'periodicity' => 'monthly',
            'start_date' => now()->subDays(5),
            'due_date' => now()->addDays(25),
        ]);

        Installment::factory()->create([
            'company_id' => $this->company->id,
            'credit_id' => $credit->id,
            'installment_number' => 1,
            'status' => 'pending',
            'amount_paid' => 0,
            'total_amount' => $totalAmount,
            'principal_amount' => $totalAmount * 0.8,
            'interest_amount' => $totalAmount * 0.2,
            'due_date' => now()->addDays(25),
        ]);

        return $credit;
    }

    private function makePaymentPayload(Credit $credit, array $overrides = []): array
    {
        return array_merge([
            'idempotency_key' => (string) Str::uuid(),
            'credit_id' => $credit->id,
            'amount' => 30_000.0,
            'payment_method' => 'cash',
            'payment_date' => now()->toDateString(),
        ], $overrides);
    }

    private function postSync(User $user, array $payments): TestResponse
    {
        return $this->actingAs($user, 'sanctum')
            ->postJson('/api/pwa/sync/payments', ['payments' => $payments]);
    }

    // -------------------------------------------------------------------------
    // Tests
    // -------------------------------------------------------------------------

    #[Test]
    public function valid_payment_by_collector_on_own_credit_returns_success(): void
    {
        $credit = $this->makeCreditWithInstallment($this->collector, 60_000.0);
        $payload = $this->makePaymentPayload($credit, ['amount' => 30_000.0]);

        $response = $this->postSync($this->collector, [$payload]);

        $response->assertOk();

        $summary = $response->json('summary');
        $this->assertSame(1, $summary['success']);
        $this->assertSame(0, $summary['errors']);
        $this->assertSame(0, $summary['duplicates']);

        $result = $response->json('results.0');
        $this->assertSame('success', $result['status']);
        $this->assertArrayHasKey('payment_id', $result);

        // El payment debe existir en DB
        $this->assertDatabaseHas('payments', [
            'id' => $result['payment_id'],
            'company_id' => $this->company->id,
            'credit_id' => $credit->id,
            'idempotency_key' => $payload['idempotency_key'],
            'registered_by_user_id' => $this->collector->id,
        ]);
    }

    #[Test]
    public function idempotency_key_prevents_duplicate_payment(): void
    {
        $credit = $this->makeCreditWithInstallment($this->collector, 60_000.0);
        $payload = $this->makePaymentPayload($credit, ['amount' => 30_000.0]);

        // Primera sincronización: crea el pago
        $this->postSync($this->collector, [$payload])
            ->assertOk()
            ->assertJsonPath('summary.success', 1);

        $countAfterFirst = Payment::withoutGlobalScopes()
            ->where('idempotency_key', $payload['idempotency_key'])
            ->count();

        // Segunda sincronización: misma key → duplicate, sin nuevo registro
        $response = $this->postSync($this->collector, [$payload]);

        $response->assertOk();
        $this->assertSame('duplicate', $response->json('results.0.status'));
        $this->assertSame(0, $response->json('summary.success'));
        $this->assertSame(1, $response->json('summary.duplicates'));

        $countAfterSecond = Payment::withoutGlobalScopes()
            ->where('idempotency_key', $payload['idempotency_key'])
            ->count();

        $this->assertSame($countAfterFirst, $countAfterSecond, 'No debe crearse un segundo pago con la misma idempotency_key');
        $this->assertSame(1, $countAfterSecond);
    }

    #[Test]
    public function collector_cannot_pay_another_collectors_credit(): void
    {
        $otherCollector = User::factory()->create(['company_id' => $this->company->id]);
        $otherCollector->assignRole('collector');
        $otherCollector = $otherCollector->fresh();

        // Crédito asignado al otherCollector
        $credit = $this->makeCreditWithInstallment($otherCollector, 60_000.0);
        $payload = $this->makePaymentPayload($credit, ['amount' => 30_000.0]);

        $response = $this->postSync($this->collector, [$payload]);

        $response->assertOk();
        $this->assertSame('held', $response->json('results.0.status'));
        $this->assertSame('credit_unavailable', $response->json('results.0.reason'));
        $this->assertSame(1, $response->json('summary.held'));
        $this->assertSame(0, $response->json('summary.success'));

        // No se debe crear ningún pago
        $this->assertDatabaseMissing('payments', [
            'credit_id' => $credit->id,
            'idempotency_key' => $payload['idempotency_key'],
        ]);
    }

    #[Test]
    public function amount_exceeding_remaining_balance_is_held(): void
    {
        $credit = $this->makeCreditWithInstallment($this->collector, 60_000.0);
        // Monto > remaining_balance (60000)
        $payload = $this->makePaymentPayload($credit, ['amount' => 90_000.0]);

        $response = $this->postSync($this->collector, [$payload]);

        $response->assertOk();
        $this->assertSame('held', $response->json('results.0.status'));
        $this->assertSame('exceeds_balance', $response->json('results.0.reason'));

        $this->assertDatabaseMissing('payments', ['idempotency_key' => $payload['idempotency_key']]);
    }

    #[Test]
    public function credit_of_a_foreign_collector_is_held_and_not_applied(): void
    {
        // Cobrador de otra empresa. Ojo: makeCreditWithInstallment crea el crédito
        // SIEMPRE en $this->company; lo ajeno es el cobrador asignado.
        $otherCompany = Company::factory()->create();
        $otherCollector = User::factory()->create(['company_id' => $otherCompany->id]);
        $otherCredit = $this->makeCreditWithInstallment($otherCollector, 60_000.0);

        // El collector de $this->company intenta pagar un crédito que no tiene asignado:
        // no se aplica, queda retenido en la empresa de quien sube.
        $payload = $this->makePaymentPayload($otherCredit, ['amount' => 30_000.0]);

        $response = $this->postSync($this->collector, [$payload]);

        $response->assertOk();
        $this->assertSame('held', $response->json('results.0.status'));
        $this->assertSame('credit_unavailable', $response->json('results.0.reason'));
        $this->assertDatabaseHas('held_payments', [
            'idempotency_key' => $payload['idempotency_key'],
            'company_id' => $this->company->id,
        ]);

        $this->assertDatabaseMissing('payments', ['idempotency_key' => $payload['idempotency_key']]);
    }

    #[Test]
    public function partial_batch_applies_valid_items_and_holds_the_rest(): void
    {
        $credit = $this->makeCreditWithInstallment($this->collector, 60_000.0);

        $validPayload = $this->makePaymentPayload($credit, ['amount' => 30_000.0]);
        $invalidPayload = $this->makePaymentPayload($credit, [
            'credit_id' => 99_999_999, // crédito inexistente
            'amount' => 10_000.0,
        ]);

        $response = $this->postSync($this->collector, [$validPayload, $invalidPayload]);

        $response->assertOk();

        $summary = $response->json('summary');
        $this->assertSame(2, $summary['total']);
        $this->assertSame(1, $summary['success']);
        $this->assertSame(1, $summary['held']);
        $this->assertSame(0, $summary['errors']);
        $this->assertSame(0, $summary['duplicates']);

        // El pago válido sí se creó
        $this->assertDatabaseHas('payments', ['idempotency_key' => $validPayload['idempotency_key']]);
        // El inválido no
        $this->assertDatabaseMissing('payments', ['idempotency_key' => $invalidPayload['idempotency_key']]);
    }

    #[Test]
    public function empty_payments_array_returns_422_validation_error(): void
    {
        // La regla 'min:1' en SyncPaymentsRequest rechaza arrays vacíos
        $this->actingAs($this->collector, 'sanctum')
            ->postJson('/api/pwa/sync/payments', ['payments' => []])
            ->assertUnprocessable();
    }

    #[Test]
    public function admin_can_pay_any_company_credit_regardless_of_collector_assignment(): void
    {
        // Crédito asignado al collector, no al admin
        $credit = $this->makeCreditWithInstallment($this->collector, 60_000.0);
        $payload = $this->makePaymentPayload($credit, ['amount' => 30_000.0]);

        $response = $this->postSync($this->admin, [$payload]);

        $response->assertOk();
        $this->assertSame(1, $response->json('summary.success'));
        $this->assertSame(0, $response->json('summary.errors'));

        // El pago debe registrar al admin como quien lo procesó
        $this->assertDatabaseHas('payments', [
            'idempotency_key' => $payload['idempotency_key'],
            'credit_id' => $credit->id,
            'registered_by_user_id' => $this->admin->id,
        ]);
    }
}
