<?php

namespace Tests\Feature;

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

/**
 * The settlement screens are Blade, so a renamed method or a wrong variable is
 * a 500 at request time and nothing earlier catches it.
 *
 * Runs against a restored Postgres tenant; skips on the default SQLite suite:
 *
 *   DB_CONNECTION=pgsql DB_DATABASE=oc_sport_test ./vendor/bin/phpunit --filter SettlementScreensRenderTest
 */
class SettlementScreensRenderTest extends TestCase
{
    protected function setUp(): void
    {
        parent::setUp();

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

    private function anAdmin(): User
    {
        $user = User::query()->get()->first(fn (User $u) => $u->can('settlements.manage'));

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

        return $user;
    }

    public function test_the_worklist_renders_and_finds_accounts(): void
    {
        $response = $this->actingAs($this->anAdmin())->get(route('admin.settlement-worklist'));

        $response->assertOk();
        $response->assertSee('حالات تحتاج تسوية', escape: false);
        $response->assertSee('تسوية', escape: false);
    }

    public function test_the_worklist_can_be_filtered_to_one_case(): void
    {
        $response = $this->actingAs($this->anAdmin())
            ->get(route('admin.settlement-worklist', ['case' => 'never_paid']));

        $response->assertOk();
        $response->assertSee('لم يُسجَّل له أي دفع', escape: false);
    }

    public function test_the_wizard_opens_on_a_participant_and_shows_the_account(): void
    {
        // A live participant: an invoice whose player was later deleted must
        // not open the wizard, and the screen says so rather than pretending.
        $invoice = Invoice::withoutGlobalScopes()
            ->where('billable_type', Participant::class)
            ->whereColumn('total_amount', '>', 'paid_amount')
            ->whereNull('deleted_at')
            ->whereIn('billable_id', Participant::withoutGlobalScopes()->whereNull('deleted_at')->select('id'))
            ->firstOrFail();

        $response = $this->actingAs($this->anAdmin())
            ->get(route('admin.account-settlement', ['participant' => $invoice->billable_id]));

        $response->assertOk();
        $response->assertSee('الفواتير', escape: false);
        $response->assertSee('دفعة سابقة', escape: false);
        $response->assertSee($invoice->number);
    }

    public function test_the_wizard_opens_empty_without_a_participant(): void
    {
        $response = $this->actingAs($this->anAdmin())->get(route('admin.account-settlement'));

        $response->assertOk();
        $response->assertSee('ابحث بالاسم أو رقم الهاتف', escape: false);
    }

    public function test_a_user_without_the_permission_is_refused(): void
    {
        $user = User::query()->get()->first(fn (User $u) => ! $u->can('settlements.manage'));

        if (! $user) {
            $this->markTestSkipped('Every user in this tenant may settle accounts.');
        }

        $this->actingAs($user)->get(route('admin.settlement-worklist'))->assertForbidden();
        $this->actingAs($user)->get(route('admin.account-settlement'))->assertForbidden();
    }

    public function test_the_screens_use_logical_properties_only(): void
    {
        // Arabic is the default locale; a physical margin flips the layout.
        foreach ([
            'livewire/admin/account-settlement-wizard.blade.php',
            'livewire/admin/settlement-worklist.blade.php',
            'livewire/admin/partials/settlement-cart.blade.php',
        ] as $view) {
            $content = file_get_contents(resource_path('views/' . $view));

            $offenders = [];
            foreach (explode("\n", $content) as $i => $line) {
                if (preg_match('/class="[^"]*\b(m[lr]|p[lr])-[0-9]/', $line)) {
                    $offenders[] = $view . ':' . ($i + 1) . ' ' . trim($line);
                }
            }

            $this->assertSame([], $offenders, "Physical margins/paddings found:\n" . implode("\n", $offenders));
        }
    }

    public function test_no_dead_links_in_the_settlement_screens(): void
    {
        // href="#" is forbidden: a link that goes nowhere is a bug report from
        // a parent standing at the desk.
        foreach ([
            'livewire/admin/account-settlement-wizard.blade.php',
            'livewire/admin/settlement-worklist.blade.php',
            'livewire/admin/partials/settlement-cart.blade.php',
        ] as $view) {
            $content = file_get_contents(resource_path('views/' . $view));
            $this->assertStringNotContainsString('href="#"', $content, $view);
        }
    }

    // ---- the double-billing case, end to end on the screen -----------------

    /** A participant the scanner flags as billed above a requirement's price. */
    private function anOverBilledParticipant(): Participant
    {
        $rows = app(\App\Domain\Financial\Services\AccountAnomalyScanner::class)
            ->scan(null, 'over_billed_bundle', 5);

        if ($rows === []) {
            $this->markTestSkipped('No account in the restored tenant is billed above a card price.');
        }

        return $rows[0]['participant'];
    }

    public function test_the_worklist_offers_the_double_billing_filter(): void
    {
        $response = $this->actingAs($this->anAdmin())
            ->get(route('admin.settlement-worklist', ['case' => 'over_billed_bundle']));

        $response->assertOk();
        $response->assertSee('مستلزم محاسَب بأكثر من سعره', escape: false);
    }

    public function test_the_worklist_row_states_the_excess_without_opening_the_wizard(): void
    {
        $this->anOverBilledParticipant();

        $response = $this->actingAs($this->anAdmin())
            ->get(route('admin.settlement-worklist', ['case' => 'over_billed_bundle']));

        $response->assertOk();
        $response->assertSee('زيادة', escape: false);
    }

    public function test_the_wizard_proposes_the_invoice_to_reduce_and_the_amount(): void
    {
        $participant = $this->anOverBilledParticipant();

        $component = \Livewire\Livewire::actingAs($this->anAdmin())
            ->test(\App\Livewire\Admin\AccountSettlementWizard::class, ['participant' => $participant->id]);

        $fixes = $component->instance()->overBilledFixes;

        $this->assertNotEmpty($fixes, 'A flagged account must come with something to do about it.');

        $fix = $fixes[0];
        $this->assertGreaterThan(0, $fix['over_billed']);
        $this->assertNotEmpty($fix['fixes'], 'The excess sits on an uncollected invoice, so a fix is offerable.');

        $candidate = $fix['fixes'][0];
        $this->assertSame(
            $candidate['current'] - $candidate['removed'],
            $candidate['new_amount'],
            'The proposed amount is the invoice minus its share of the excess.'
        );

        $invoice = Invoice::withoutGlobalScopes()->findOrFail($candidate['invoice_id']);
        $this->assertSame(0, (int) $invoice->paid_amount,
            'Only invoices with nothing collected are ever proposed for reduction.');
        $this->assertSame((int) $participant->id, (int) $invoice->billable_id);
    }

    public function test_the_fix_can_be_staged_into_the_settlement_cart(): void
    {
        $participant = $this->anOverBilledParticipant();

        $component = \Livewire\Livewire::actingAs($this->anAdmin())
            ->test(\App\Livewire\Admin\AccountSettlementWizard::class, ['participant' => $participant->id]);

        $candidate = $component->instance()->overBilledFixes[0]['fixes'][0];

        $component->call('startDraft', 'void_duplicate', [
            'invoice_id' => $candidate['invoice_id'],
            'invoice_number' => $candidate['invoice_number'],
            'current' => $candidate['current'],
            'new_amount' => $candidate['new_amount'],
        ])->call('addDraft')->assertHasNoErrors();

        $cart = $component->instance()->cart;

        $this->assertCount(1, $cart);
        $this->assertSame('void_duplicate', $cart[0]['type']);
        $this->assertSame($candidate['new_amount'], $cart[0]['new_amount']);
        $this->assertSame($candidate['removed'], $cart[0]['removed']);

        // Negative billing, and nothing written off — the club is not out of pocket.
        $totals = $component->instance()->totals;
        $this->assertSame(-$candidate['removed'], $totals['billed']);
        $this->assertSame(0, $totals['waived']);
        $this->assertSame(0, $totals['collected']);
    }

    public function test_the_wizard_refuses_to_stage_a_reduction_on_a_collected_invoice(): void
    {
        $participant = $this->anOverBilledParticipant();

        $collected = Invoice::withoutGlobalScopes()
            ->where('billable_id', $participant->id)
            ->where('billable_type', Participant::class)
            ->where('paid_amount', '>', 0)
            ->whereNull('deleted_at')
            ->first();

        if (! $collected) {
            $this->markTestSkipped('This account has no collected invoice to try it on.');
        }

        \Livewire\Livewire::actingAs($this->anAdmin())
            ->test(\App\Livewire\Admin\AccountSettlementWizard::class, ['participant' => $participant->id])
            ->call('startDraft', 'void_duplicate', [
                'invoice_id' => $collected->id,
                'invoice_number' => $collected->number,
                'current' => (int) $collected->total_amount,
                'new_amount' => 0,
            ])
            ->call('addDraft')
            ->assertHasErrors('draft.invoice_id');
    }
}
