<?php

namespace Tests\Feature;

use App\Models\User;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Tests\TestCase;

/**
 * Who can reach what.
 *
 * The panel can activate subscriptions and suspend businesses, so these are
 * not cosmetic checks.
 */
class AccessControlTest extends TestCase
{
    use RefreshDatabase;

    public function test_the_panel_is_closed_to_guests(): void
    {
        $this->get('/dashboard')->assertRedirect('/login');
        $this->get('/shops')->assertRedirect('/login');
        $this->get('/payments/queue')->assertRedirect('/login');
    }

    public function test_there_is_no_public_signup(): void
    {
        $this->get('/register')->assertNotFound();
    }

    public function test_support_staff_cannot_open_the_verification_queue(): void
    {
        $this->actingAsStaff('support');

        $this->get('/payments/queue')->assertForbidden();
    }

    public function test_an_accountant_can_open_the_verification_queue(): void
    {
        $this->actingAsStaff('accountant');

        $this->get('/payments/queue')->assertOk();
    }

    public function test_an_accountant_cannot_edit_the_plan_catalogue(): void
    {
        $this->actingAsStaff('accountant');

        $this->get('/plans')->assertForbidden();
        $this->get('/receiving-accounts')->assertForbidden();
        $this->get('/settings')->assertForbidden();
    }

    public function test_an_admin_can_edit_the_catalogue(): void
    {
        $this->actingAsStaff('admin');

        $this->get('/plans')->assertOk();
        $this->get('/receiving-accounts')->assertOk();
    }

    /**
     * A session outlives a deactivation, and this panel can hand out money —
     * so the check has to happen on every request, not only at sign-in.
     */
    public function test_a_deactivated_staff_member_is_signed_out_mid_session(): void
    {
        $user = User::factory()->admin()->create();

        $this->actingAs($user);
        $this->get('/dashboard')->assertOk();

        $user->forceFill(['is_active' => false])->save();

        $this->get('/dashboard')->assertRedirect('/login');
        $this->assertGuest();
    }

    /**
     * Open every screen, with data behind it.
     *
     * An empty database hides exactly the errors this is looking for — a report
     * calling a method on the wrong type, a view reading a property that is not
     * there — because the loop that would have hit them never runs. So a
     * verified payment and a pending one are seeded first.
     *
     * Failures are collected rather than thrown on the first one, so a single
     * run names every broken screen.
     */
    public function test_every_authenticated_screen_renders(): void
    {
        $this->actingAsStaff('super_admin');

        $product = $this->product();
        $plan = $this->plan($product);
        $tenant = $this->tenant($product);

        $paid = $this->subscription($tenant, $plan);
        $this->payment($paid, $this->invoice($paid), [
            'status' => \App\Enums\ProofStatus::Verified,
            'reviewed_at' => now()->subDay(),
        ]);

        $pending = $this->subscription($tenant, $plan);
        $this->payment($pending, $this->invoice($pending));

        $screens = [
            '/dashboard', '/shops', '/subscriptions', '/invoices', '/payments',
            '/payments/queue', '/products', '/plans', '/receiving-accounts',
            '/announcements', '/reports/revenue', '/reports/growth', '/staff',
            '/activity', '/notifications', '/settings', '/profile',
            '/shops/'.$tenant->id,
        ];

        $failures = [];

        foreach ($screens as $url) {
            try {
                $status = $this->get($url)->getStatusCode();

                if ($status !== 200) {
                    // Ask again without the handler, so the catch below reports
                    // the cause rather than just the status code.
                    $this->withoutExceptionHandling();

                    try {
                        $this->get($url);
                        $failures[] = "{$url} returned {$status}";
                    } finally {
                        $this->withExceptionHandling();
                    }
                }
            } catch (\Throwable $exception) {
                $failures[] = sprintf('%s threw %s: %s', $url, class_basename($exception), $exception->getMessage());
            }
        }

        $this->assertSame([], $failures, "These screens did not render:\n  - ".implode("\n  - ", $failures)."\n");
    }
}
