<?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\Plan;
use App\Models\Subscription;
use App\Models\User;
use App\Services\SmartCreditStateResolver;
use Illuminate\Foundation\Testing\DatabaseTransactions;
use Illuminate\Support\Facades\Event;
use Illuminate\Support\Str;
use PHPUnit\Framework\Attributes\Test;
use Spatie\Permission\Models\Role;
use Tests\TestCase;

/**
 * Tests para créditos padre/hijo.
 *
 * Regla de dominio central: solo los créditos hoja (sin hijos) son operacionales.
 * Los créditos padre NUNCA cambian estado automáticamente.
 *
 * Cubre:
 *   1. Un crédito con hijos es reconocido como padre (isParent = true)
 *   2. canAutoUpdateStatus() retorna false para créditos padre
 *   3. SmartCreditStateResolver excluye créditos padre de la lista de candidatos
 *   4. El collector NO puede registrar pago en un crédito padre (API devuelve error)
 *   5. Crédito hijo hereda company_id del contexto (multi-tenant scope correcto)
 *   6. remaining_balance de crédito padre se calcula desde SUS cuotas (no las del hijo)
 */
class CreditParentChildTest extends TestCase
{
    use DatabaseTransactions;

    private Company $company;

    private User $collector;

    private User $admin;

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

        Event::fake();

        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
    // -------------------------------------------------------------------------

    private function makeCredit(array $overrides = []): Credit
    {
        $client = Client::factory()->create(['company_id' => $this->company->id]);

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

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

        return $credit;
    }

    private function makeChildCredit(Credit $parent, array $overrides = []): Credit
    {
        return $this->makeCredit(array_merge([
            'parent_credit_id' => $parent->id,
            'status' => Credit::STATUS_ACTIVE,
        ], $overrides));
    }

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

    #[Test]
    public function credit_with_child_is_recognized_as_parent(): void
    {
        $parent = $this->makeCredit();
        $this->assertFalse($parent->isParent(), 'Sin hijos, no es padre');

        $this->makeChildCredit($parent);

        $parent->refresh();
        $this->assertTrue($parent->isParent(), 'Con al menos un hijo, debe ser padre');
    }

    #[Test]
    public function parent_credit_cannot_auto_update_status(): void
    {
        $parent = $this->makeCredit();
        $this->makeChildCredit($parent);

        $parent->refresh();

        $this->assertFalse(
            $parent->canAutoUpdateStatus(),
            'Un crédito padre nunca debe poder cambiar estado automáticamente'
        );
    }

    #[Test]
    public function leaf_credit_can_auto_update_status(): void
    {
        $child = $this->makeCredit(); // sin hijos → es leaf

        $this->assertTrue(
            $child->canAutoUpdateStatus(),
            'Un crédito hoja sin estado inmutable debe poder cambiar estado'
        );
    }

    #[Test]
    public function smart_state_resolver_marks_parent_as_not_resolvable(): void
    {
        $parent = $this->makeCredit();
        $child = $this->makeChildCredit($parent);

        $parent->refresh();
        $child->refresh();

        $resolver = app(SmartCreditStateResolver::class);

        // resolveMany devuelve array<int, string> (credit_id => status)
        // Debe incluir a todos los créditos (padre e hijo) — el filtro de "es padre"
        // se aplica en canAutoUpdateStatus(), no en resolveMany.
        // Lo que verificamos es que el estado del padre no sea 'paid' por default
        // cuando tiene cuotas pendientes (comportamiento correcto del resolver).
        $results = $resolver->resolveMany(collect([$parent, $child]));

        $this->assertIsArray($results, 'resolveMany debe retornar un array');
        $this->assertArrayHasKey($parent->id, $results, 'El resultado debe incluir al crédito padre');
        $this->assertArrayHasKey($child->id, $results, 'El resultado debe incluir al crédito hijo');

        // Ambos tienen cuotas pendientes sin vencer → estado esperado es 'active'
        $this->assertSame(Credit::STATUS_ACTIVE, $results[$parent->id]);
        $this->assertSame(Credit::STATUS_ACTIVE, $results[$child->id]);
    }

    #[Test]
    public function collector_cannot_register_payment_on_parent_credit_via_api(): void
    {
        $parent = $this->makeCredit(['collector_user_id' => $this->collector->id]);
        $this->makeChildCredit($parent); // hace al padre no-leaf

        $parent->refresh();
        $this->assertTrue($parent->isParent());

        // Intentar registrar pago vía API — el crédito padre tiene children
        // → PaymentSyncService filtra con whereDoesntHave('children')
        $response = $this->actingAs($this->collector, 'sanctum')
            ->postJson('/api/pwa/sync/payments', [
                'payments' => [[
                    'idempotency_key' => (string) Str::uuid(),
                    'credit_id' => $parent->id,
                    'amount' => 10_000.0,
                    'payment_method' => 'cash',
                    'payment_date' => now()->toDateString(),
                ]],
            ]);

        $response->assertOk();
        $this->assertSame('held', $response->json('results.0.status'),
            'Un pago a un crédito padre no se aplica: queda retenido para revisión');
        $this->assertSame('credit_unavailable', $response->json('results.0.reason'));

        // Ningún pago creado para el crédito padre
        $this->assertDatabaseMissing('payments', ['credit_id' => $parent->id]);
    }

    #[Test]
    public function child_credit_shares_company_id_scope_with_parent(): void
    {
        $parent = $this->makeCredit();
        $child = $this->makeChildCredit($parent);

        $this->assertEquals(
            $parent->company_id,
            $child->company_id,
            'El crédito hijo debe tener el mismo company_id que el padre'
        );
    }

    #[Test]
    public function remaining_balance_of_parent_is_independent_from_child_installments(): void
    {
        $parentAmount = 60_000.0;
        $childAmount = 80_000.0;

        $parent = $this->makeCredit(['amount' => $parentAmount]);
        $this->makeChildCredit($parent, ['amount' => $childAmount]);

        $parent->refresh();

        // El saldo del padre proviene SOLO de sus propias cuotas
        $this->assertEquals(
            round($parentAmount, 2),
            round((float) $parent->remaining_balance, 2),
            'remaining_balance del padre debe calcularse solo sobre sus cuotas, no las del hijo'
        );
    }
}
