<?php

declare(strict_types=1);

namespace Tests\Unit\Services\Metrics;

use App\Models\Client;
use App\Models\Company;
use App\Models\Credit;
use App\Models\Installment;
use App\Models\User;
use App\Services\Dashboard\DashboardCacheService;
use App\Services\Metrics\DelinquencyMetricsService;
use Carbon\Carbon;
use Illuminate\Foundation\Testing\DatabaseTransactions;
use Illuminate\Support\Facades\Cache;
use PHPUnit\Framework\Attributes\Test;
use Tests\TestCase;

class DelinquencyMetricsServiceTest extends TestCase
{
    use DatabaseTransactions;

    private DelinquencyMetricsService $service;

    private Company $company;

    private User $collector;

    private Client $client;

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

        Cache::flush();

        $this->service = new DelinquencyMetricsService(
            new DashboardCacheService
        );

        $this->company = Company::factory()->create();
        $this->collector = User::factory()->create(['company_id' => $this->company->id]);
        $this->client = Client::factory()->create(['company_id' => $this->company->id]);
    }

    #[Test]
    public function it_calculates_par_metrics_for_all_horizons()
    {
        // Crédito con cuotas vencidas hace 45 días
        $credit = $this->createCredit(100000, 'overdue');
        $this->createInstallments($credit, 4, 27500, now()->subDays(45));

        $par = $this->service->getPARMetrics($this->company->id);

        // Debe tener métricas para todos los horizontes estándar
        $this->assertArrayHasKey('par1', $par);
        $this->assertArrayHasKey('par7', $par);
        $this->assertArrayHasKey('par15', $par);
        $this->assertArrayHasKey('par30', $par);
        $this->assertArrayHasKey('par60', $par);
        $this->assertArrayHasKey('par90', $par);

        // Verificar estructura
        $this->assertArrayHasKey('days', $par['par30']);
        $this->assertArrayHasKey('at_risk_balance', $par['par30']);
        $this->assertArrayHasKey('total_portfolio', $par['par30']);
        $this->assertArrayHasKey('par_percentage', $par['par30']);
        $this->assertArrayHasKey('credits_count', $par['par30']);
    }

    #[Test]
    public function it_calculates_par30_correctly()
    {
        // Crédito 1: Al día (no debe contar en PAR30)
        $credit1 = $this->createCredit(100000, 'active');
        $this->createInstallments($credit1, 4, 27500, now()->addDays(7));

        // Crédito 2: Vencido hace 45 días (debe contar en PAR30)
        $credit2 = $this->createCredit(100000, 'overdue');
        $this->createInstallments($credit2, 4, 27500, now()->subDays(45));

        $par30 = $this->service->getPAR30($this->company->id);

        // PAR30 = balance crédito vencido / balance total × 100
        // = 110000 / 220000 × 100 = 50%
        $this->assertEquals(50, $par30);
    }

    #[Test]
    public function it_returns_zero_par_when_no_overdue_credits()
    {
        // Solo créditos al día
        $credit = $this->createCredit(100000, 'active');
        $this->createInstallments($credit, 4, 27500, now()->addDays(7));

        $par30 = $this->service->getPAR30($this->company->id);

        $this->assertEquals(0, $par30);
    }

    #[Test]
    public function it_generates_aging_report()
    {
        // Crédito al día
        $credit1 = $this->createCredit(100000, 'active');
        $this->createInstallments($credit1, 4, 25000, now()->addDays(7));

        // Crédito con mora 5 días
        $credit2 = $this->createCredit(100000, 'delayed');
        $this->createInstallments($credit2, 4, 25000, now()->subDays(5));

        // Crédito con mora 20 días
        $credit3 = $this->createCredit(100000, 'delayed');
        $this->createInstallments($credit3, 4, 25000, now()->subDays(20));

        // Crédito con mora 50 días
        $credit4 = $this->createCredit(100000, 'overdue');
        $this->createInstallments($credit4, 4, 25000, now()->subDays(50));

        $aging = $this->service->getAgingReport($this->company->id);

        $this->assertArrayHasKey('current', $aging);
        $this->assertArrayHasKey('1_7', $aging);
        $this->assertArrayHasKey('8_15', $aging);
        $this->assertArrayHasKey('16_30', $aging);
        $this->assertArrayHasKey('31_60', $aging);
        $this->assertArrayHasKey('61_90', $aging);
        $this->assertArrayHasKey('90_plus', $aging);

        // Verificar estructura
        $this->assertArrayHasKey('label', $aging['current']);
        $this->assertArrayHasKey('overdue_balance', $aging['current']);
        $this->assertArrayHasKey('credits_count', $aging['current']);
        $this->assertArrayHasKey('percentage', $aging['current']);
    }

    #[Test]
    public function it_gets_top_overdue_credits()
    {
        // Crédito con 30 días de mora
        $credit1 = $this->createCredit(150000, 'overdue');
        $this->createInstallments($credit1, 4, 41250, now()->subDays(30));

        // Crédito con 15 días de mora
        $credit2 = $this->createCredit(100000, 'delayed');
        $this->createInstallments($credit2, 4, 27500, now()->subDays(15));

        // Crédito al día (no debe aparecer)
        $credit3 = $this->createCredit(200000, 'active');
        $this->createInstallments($credit3, 4, 55000, now()->addDays(7));

        $topOverdue = $this->service->getTopOverdueCredits($this->company->id, 10);

        $this->assertCount(2, $topOverdue);

        // Ordenado por días de mora descendente
        $this->assertEquals($credit1->id, $topOverdue->first()['credit_id']);
        $this->assertGreaterThan(
            $topOverdue->last()['max_days_overdue'],
            $topOverdue->first()['max_days_overdue']
        );

        // Verificar estructura
        $first = $topOverdue->first();
        $this->assertArrayHasKey('credit_id', $first);
        $this->assertArrayHasKey('client_name', $first);
        $this->assertArrayHasKey('total_balance', $first);
        $this->assertArrayHasKey('overdue_balance', $first);
        $this->assertArrayHasKey('max_days_overdue', $first);
    }

    #[Test]
    public function it_scopes_metrics_to_assigned_collectors()
    {
        $collectorA = User::factory()->create(['company_id' => $this->company->id]);
        $collectorB = User::factory()->create(['company_id' => $this->company->id]);

        // A y B: cada uno un crédito vencido ~45 días (mismo saldo: 4 × 27500).
        $creditA = $this->createCredit(100000, 'overdue', null, $collectorA->id);
        $this->createInstallments($creditA, 4, 27500, now()->subDays(45));

        $creditB = $this->createCredit(100000, 'overdue', null, $collectorB->id);
        $this->createInstallments($creditB, 4, 27500, now()->subDays(45));

        // Company-wide: ambos créditos en mora.
        $this->assertCount(2, $this->service->getTopOverdueCredits($this->company->id, 10));

        // Scoped a A: solo su crédito.
        $scopedTop = $this->service->getTopOverdueCredits($this->company->id, 10, [$collectorA->id]);
        $this->assertCount(1, $scopedTop);
        $this->assertEquals($creditA->id, $scopedTop->first()['credit_id']);

        // total_portfolio scoped a A = mitad del company-wide (A y B aportan igual).
        $companySummary = $this->service->getDelinquencySummary($this->company->id);
        $scopedSummary = $this->service->getDelinquencySummary($this->company->id, [$collectorA->id]);
        $this->assertEqualsWithDelta(
            $companySummary['total_portfolio'] / 2,
            $scopedSummary['total_portfolio'],
            0.01
        );

        // Unassigned (scope vacío): todo en cero.
        $this->assertCount(0, $this->service->getTopOverdueCredits($this->company->id, 10, []));
        $this->assertEquals(0.0, $this->service->getDelinquencySummary($this->company->id, [])['total_portfolio']);

        // PAR trend scoped → punto en vivo (datos parciales, sin snapshots por-cobrador).
        $trend = $this->service->getPARTrend($this->company->id, 30, [$collectorA->id]);
        $this->assertTrue($trend['is_partial_data']);
        $this->assertCount(1, $trend['data']);
    }

    #[Test]
    public function it_calculates_delinquency_by_collector()
    {
        $collector1 = User::factory()->create(['company_id' => $this->company->id]);
        $collector2 = User::factory()->create(['company_id' => $this->company->id]);

        // Collector 1: 2 créditos al día
        $credit1 = $this->createCredit(100000, 'active', null, $collector1->id);
        $this->createInstallments($credit1, 4, 27500, now()->addDays(7));

        $credit2 = $this->createCredit(100000, 'active', null, $collector1->id);
        $this->createInstallments($credit2, 4, 27500, now()->addDays(7));

        // Collector 2: 1 crédito al día, 1 vencido
        $credit3 = $this->createCredit(100000, 'active', null, $collector2->id);
        $this->createInstallments($credit3, 4, 27500, now()->addDays(7));

        $credit4 = $this->createCredit(100000, 'overdue', null, $collector2->id);
        $this->createInstallments($credit4, 4, 27500, now()->subDays(45));

        $byCollector = $this->service->getDelinquencyByCollector($this->company->id);

        $this->assertCount(2, $byCollector);

        $collector1Data = $byCollector->firstWhere('collector_id', $collector1->id);
        $collector2Data = $byCollector->firstWhere('collector_id', $collector2->id);

        $this->assertEquals(2, $collector1Data['total_credits']);
        $this->assertEquals(0, $collector1Data['overdue_credits']);
        $this->assertEquals(0, $collector1Data['delinquency_rate']);

        $this->assertEquals(2, $collector2Data['total_credits']);
        $this->assertEquals(1, $collector2Data['overdue_credits']);
        $this->assertEquals(50, $collector2Data['delinquency_rate']);
    }

    #[Test]
    public function it_calculates_delinquency_summary()
    {
        // Crédito al día
        $credit1 = $this->createCredit(100000, 'active');
        $this->createInstallments($credit1, 4, 27500, now()->addDays(7));

        // Crédito vencido
        $credit2 = $this->createCredit(100000, 'overdue');
        $this->createInstallments($credit2, 4, 27500, now()->subDays(45));

        $summary = $this->service->getDelinquencySummary($this->company->id);

        $this->assertArrayHasKey('par1', $summary);
        $this->assertArrayHasKey('par30', $summary);
        $this->assertArrayHasKey('par60', $summary);
        $this->assertArrayHasKey('par90', $summary);
        $this->assertArrayHasKey('total_overdue_balance', $summary);
        $this->assertArrayHasKey('health_indicator', $summary);

        // Health indicator debe ser uno de los valores válidos
        $this->assertContains($summary['health_indicator'], [
            'excellent', 'good', 'fair', 'poor', 'critical',
        ]);
    }

    #[Test]
    public function it_excludes_parent_credits_from_par_calculation()
    {
        // Crédito padre (extendido)
        $parentCredit = $this->createCredit(100000, 'extended');
        $this->createInstallments($parentCredit, 4, 27500, now()->subDays(60));

        // Crédito hijo (activo, al día)
        $childCredit = Credit::create([
            'company_id' => $this->company->id,
            'client_id' => $this->client->id,
            'collector_user_id' => $this->collector->id,
            'created_by_user_id' => $this->collector->id,
            'parent_credit_id' => $parentCredit->id,
            'amount' => 120000,
            'interest_rate' => 10,
            'installments_count' => 4,
            'periodicity' => 'weekly',
            'start_date' => now(),
            'status' => 'active',
        ]);
        $this->createInstallments($childCredit, 4, 33000, now()->addDays(7));

        $par30 = $this->service->getPAR30($this->company->id);

        // El crédito padre no debe afectar el PAR
        // Solo el hijo está activo y al día, así que PAR = 0
        $this->assertEquals(0, $par30);
    }

    /*=========================================================
    | HELPERS
    =========================================================*/

    private function createCredit(
        float $amount,
        string $status,
        ?int $parentId = null,
        ?int $collectorId = null
    ): Credit {
        return Credit::create([
            'company_id' => $this->company->id,
            'client_id' => $this->client->id,
            'collector_user_id' => $collectorId ?? $this->collector->id,
            'created_by_user_id' => $this->collector->id,
            'parent_credit_id' => $parentId,
            'amount' => $amount,
            'interest_rate' => 10,
            'installments_count' => 4,
            'periodicity' => 'weekly',
            'start_date' => now(),
            'status' => $status,
        ]);
    }

    private function createInstallments(
        Credit $credit,
        int $count,
        float $amount,
        ?Carbon $firstDueDate = null
    ): void {
        $dueDate = $firstDueDate ?? now()->addWeek();

        for ($i = 1; $i <= $count; $i++) {
            Installment::create([
                'company_id' => $credit->company_id,
                'credit_id' => $credit->id,
                'installment_number' => $i,
                'due_date' => $dueDate->copy()->addWeeks($i - 1),
                'principal_amount' => $amount * 0.9,
                'interest_amount' => $amount * 0.1,
                'total_amount' => $amount,
                'principal_balance_after' => $amount,
                'amount_paid' => 0,
                'status' => Installment::STATUS_PENDING,
            ]);
        }
    }
}
