<?php

namespace Tests\Feature;

use App\Domain\Financial\Models\Invoice;
use App\Domain\Participant\Models\Participant;
use App\Livewire\Reports\ReportViewer;
use App\Models\User;
use Tests\TestCase;

/**
 * The two dues reports, rendered against real books.
 *
 * A report is a config array, a dynamically-called service method and a Blade
 * table; nothing type-checks the join between them, so a renamed column or a
 * filter the method does not accept is a 500 at request time and nothing
 * earlier catches it. The birth-year filter reaches invoices → participants →
 * people, three tables deep, which is exactly the kind of query that is fine in
 * isolation and wrong under the branch scope.
 *
 * Runs against a restored Postgres tenant; skips on the default SQLite suite:
 *
 *   DB_CONNECTION=pgsql DB_DATABASE=oc_sport_test ./vendor/bin/phpunit --filter DuesReportRenderTest
 */
class DuesReportRenderTest extends TestCase
{
    protected function setUp(): void
    {
        parent::setUp();

        if (config('database.default') !== 'pgsql') {
            $this->markTestSkipped('Needs a restored Postgres tenant; see the class comment.');
        }
    }

    /**
     * The academy owner, who holds reports.view. Picked by role slug rather
     * than by taking the first user in the table — most users in a real tenant
     * are receptionists and coaches, and the pages under test are gated.
     */
    private function anAdmin(): User
    {
        $user = User::withoutGlobalScopes()
            ->whereHas('primaryRole', fn ($q) => $q->where('slug', 'academy_owner'))
            ->first();

        if (! $user) {
            $this->markTestSkipped('No academy_owner in the restored tenant.');
        }

        return $user;
    }

    /** A window wide enough to contain the tenant's whole history. */
    private function wholeHistory(): array
    {
        return ['from' => '2000-01-01', 'to' => now()->addYear()->toDateString()];
    }

    public function test_both_dues_reports_are_registered_and_declare_their_filters(): void
    {
        $configs = (new ReportViewer())->getReportConfig();

        foreach (['participant_dues', 'participant_invoice_history'] as $key) {
            $this->assertArrayHasKey($key, $configs);
            $this->assertSame(
                ['birth_year', 'search'],
                $configs[$key]['extra_filters'],
                'The export builds its arguments from this list, so it is what keeps the CSV and the page in step.'
            );
            $this->assertSameSize(
                $configs[$key]['headers'],
                $configs[$key]['columns'],
                'A header with no column prints an empty cell and shifts the whole row.'
            );
        }
    }

    public function test_the_dues_report_renders(): void
    {
        $response = $this->actingAs($this->anAdmin())
            ->get(route('reports.viewer', ['report' => 'participant_dues'] + $this->wholeHistory()));

        $response->assertOk();
        $response->assertSee('المتبقي', escape: false);
    }

    public function test_the_invoice_history_report_renders(): void
    {
        $response = $this->actingAs($this->anAdmin())
            ->get(route('reports.viewer', ['report' => 'participant_invoice_history'] + $this->wholeHistory()));

        $response->assertOk();
        $response->assertSee('رقم الفاتورة', escape: false);
    }

    public function test_the_birth_year_filter_narrows_to_that_cohort(): void
    {
        $participant = Participant::query()
            ->whereHas('person', fn ($q) => $q->whereNotNull('date_of_birth'))
            ->whereHas('invoices')
            ->with('person')
            ->first();

        if (! $participant) {
            $this->markTestSkipped('No invoiced participant with a date of birth in the restored tenant.');
        }

        $year = (int) $participant->person->date_of_birth->format('Y');

        $rows = app(\App\Domain\Shared\Services\ReportService::class)
            ->participantDues('2000-01-01', now()->addYear()->toDateString(), null, (string) $year);

        $this->assertNotEmpty($rows, "No rows for the cohort of {$year}, whose own row should be there.");

        foreach ($rows as $row) {
            $this->assertSame((string) $year, $row['birth_year'], 'A player from another cohort came back.');
        }
    }

    public function test_an_empty_birth_year_is_not_a_filter_on_year_zero(): void
    {
        $service = app(\App\Domain\Shared\Services\ReportService::class);
        [$from, $to] = ['2000-01-01', now()->addYear()->toDateString()];

        // The property behind this box is a string and is usually blank; an int
        // property would read '' as 0 and quietly return nothing.
        $this->assertEquals(
            $service->participantDues($from, $to, null)->count(),
            $service->participantDues($from, $to, null, '')->count(),
        );
    }

    public function test_what_a_player_owes_never_goes_negative_on_an_overpayment(): void
    {
        $rows = app(\App\Domain\Shared\Services\ReportService::class)
            ->participantDues('2000-01-01', now()->addYear()->toDateString(), null);

        foreach ($rows as $row) {
            $this->assertGreaterThanOrEqual(
                0,
                $row['remaining'],
                'An overpaid invoice carries a negative due_amount; summed raw it would cancel a real debt.'
            );
        }
    }

    public function test_the_totals_are_the_invoices_own_stored_figures(): void
    {
        $service = app(\App\Domain\Shared\Services\ReportService::class);
        [$from, $to] = ['2000-01-01', now()->addYear()->toDateString()];

        $rows = $service->participantDues($from, $to, null);

        $this->assertNotEmpty($rows, 'The restored tenant has invoices; the report returned none.');

        $reportBilled = $rows->sum('billed');

        $booksBilled = (int) Invoice::query()
            ->where('billable_type', Participant::class)
            ->whereNotIn('status', ['cancelled', 'draft'])
            ->whereDate('issue_date', '>=', $from)
            ->whereDate('issue_date', '<=', $to)
            ->sum('total_amount');

        $this->assertSame($booksBilled, (int) $reportBilled, 'The report is billing a different total from the books.');
    }
}
