<?php

namespace Tests\Feature;

use App\Livewire\Release\Index as ReleaseIndex;
use App\Livewire\Settings\Email;
use App\Livewire\Settings\LoginProtection;
use App\Livewire\Settings\Sessions;
use App\Models\BannedIp;
use App\Models\User;
use App\Services\SessionService;
use App\Support\Confirm\ConfirmToken;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Support\Facades\Cache;
use Livewire\Livewire;
use Mockery;
use Tests\TestCase;

/**
 * RBAC guards on the SETTINGS + RELEASE admin surfaces. The RBAC foundation (Role enum, Gates
 * manage-panel/manage-users) is committed separately; this proves the abort_unless(...) guards are
 * wired at every dangerous entry point:
 *  - manage-panel gates LoginProtection (brute-force config / whitelist / ban clearing), Email (SMTP
 *    config) and the whole Release page + its actions;
 *  - manage-users gates the destructive GLOBAL logout (Sessions::logoutAll) while per-session
 *    self-revoke stays open to everyone.
 * Admin (factory default) may act; operator + viewer are refused with 403.
 */
class RbacSettingsGateTest extends TestCase
{
    use RefreshDatabase;

    protected function setUp(): void
    {
        parent::setUp();
        Cache::flush();
    }

    protected function tearDown(): void
    {
        Mockery::close();
        parent::tearDown();
    }

    private function admin(): User
    {
        return User::factory()->create(['must_change_password' => false]);
    }

    private function operator(): User
    {
        return User::factory()->operator()->create(['must_change_password' => false]);
    }

    private function viewer(): User
    {
        return User::factory()->viewer()->create(['must_change_password' => false]);
    }

    // ── manage-panel: LoginProtection (mount + save + unban + unbanAll + whitelistMyIp) ──

    public function test_operator_cannot_mount_login_protection(): void
    {
        Livewire::actingAs($this->operator())
            ->test(LoginProtection::class)
            ->assertForbidden();
    }

    public function test_viewer_cannot_mount_login_protection(): void
    {
        Livewire::actingAs($this->viewer())
            ->test(LoginProtection::class)
            ->assertForbidden();
    }

    public function test_admin_can_mount_login_protection(): void
    {
        Livewire::actingAs($this->admin())
            ->test(LoginProtection::class)
            ->assertOk();
    }

    public function test_operator_cannot_save_login_protection(): void
    {
        // Boot as admin so the snapshot is valid, then swap to an operator and call the mutation —
        // the save() method-level guard must still fire 403 (models a demotion / crafted update).
        $component = Livewire::actingAs($this->admin())->test(LoginProtection::class)->assertOk();

        $this->actingAs($this->operator());

        $component->set('maxretry', 5)->call('save')->assertForbidden();
    }

    public function test_viewer_cannot_call_whitelist_my_ip(): void
    {
        $component = Livewire::actingAs($this->admin())->test(LoginProtection::class)->assertOk();

        $this->actingAs($this->viewer());

        $component->call('whitelistMyIp')->assertForbidden();
    }

    public function test_operator_cannot_unban(): void
    {
        BannedIp::create(['ip' => '203.0.113.5', 'banned_until' => now()->addHour(), 'attempts' => 10]);

        $component = Livewire::actingAs($this->admin())->test(LoginProtection::class)->assertOk();

        $this->actingAs($this->operator());

        $component->call('unban', 'any-token')->assertForbidden();
        $this->assertDatabaseCount('banned_ips', 1); // guard fired before the ban was touched
    }

    public function test_viewer_cannot_unban_all(): void
    {
        BannedIp::create(['ip' => '203.0.113.5', 'banned_until' => now()->addHour(), 'attempts' => 10]);

        $component = Livewire::actingAs($this->admin())->test(LoginProtection::class)->assertOk();

        $this->actingAs($this->viewer());

        $component->call('unbanAll', 'any-token')->assertForbidden();
        $this->assertDatabaseCount('banned_ips', 1);
    }

    public function test_admin_can_save_login_protection(): void
    {
        Livewire::actingAs($this->admin())
            ->test(LoginProtection::class)
            ->set('maxretry', 7)
            ->call('save')
            ->assertHasNoErrors(); // allowed through the guard
    }

    // ── manage-panel: Email (mount + save) ──────────────────────────────────────

    public function test_operator_cannot_mount_email(): void
    {
        Livewire::actingAs($this->operator())
            ->test(Email::class)
            ->assertForbidden();
    }

    public function test_viewer_cannot_mount_email(): void
    {
        Livewire::actingAs($this->viewer())
            ->test(Email::class)
            ->assertForbidden();
    }

    public function test_operator_cannot_save_email(): void
    {
        $component = Livewire::actingAs($this->admin())->test(Email::class)->assertOk();

        $this->actingAs($this->operator());

        $component
            ->set('mail_host', 'smtp.example.com')
            ->set('mail_from_address', 'noreply@example.com')
            ->set('mail_from_name', 'Clusev')
            ->call('save')
            ->assertForbidden();
    }

    public function test_admin_can_save_email(): void
    {
        Livewire::actingAs($this->admin())
            ->test(Email::class)
            ->set('mail_host', 'smtp.example.com')
            ->set('mail_from_address', 'noreply@example.com')
            ->set('mail_from_name', 'Clusev')
            ->call('save')
            ->assertHasNoErrors();
    }

    // ── manage-users: Sessions::logoutAll (destructive GLOBAL logout) ─────────────

    public function test_operator_cannot_logout_all(): void
    {
        $component = Livewire::actingAs($this->admin())->test(Sessions::class)->assertOk();

        $this->actingAs($this->operator());

        $component->call('logoutAll', 'any-token')->assertForbidden();
    }

    public function test_viewer_cannot_logout_all(): void
    {
        $component = Livewire::actingAs($this->admin())->test(Sessions::class)->assertOk();

        $this->actingAs($this->viewer());

        $component->call('logoutAll', 'any-token')->assertForbidden();
    }

    public function test_admin_can_logout_all(): void
    {
        $admin = $this->admin();

        // The global logout truncates every session — mock it so the test session survives.
        $sessions = Mockery::mock(SessionService::class)->makePartial();
        $sessions->shouldReceive('logoutEveryone')->once();
        $sessions->shouldReceive('forUser')->andReturn([]);
        $this->app->instance(SessionService::class, $sessions);

        $this->actingAs($admin);
        $token = ConfirmToken::issue('sessionsLogoutAll', [], 'session.logout_all', $admin->email);
        ConfirmToken::confirm($token);

        Livewire::actingAs($admin)
            ->test(Sessions::class)
            ->call('logoutAll', $token)
            ->assertRedirect(route('login')); // allowed through the guard → redirect to login
    }

    public function test_non_admin_self_session_revoke_is_not_forbidden(): void
    {
        // A non-admin managing their OWN sessions (per-session self-revoke) is NOT gated. Opening the
        // "sign out other devices" confirm must not 403 for an operator.
        Livewire::actingAs($this->operator())
            ->test(Sessions::class)
            ->call('confirmLogoutOthers')
            ->assertOk();
    }

    // ── manage-panel: Release\Index (flag ON, but role still enforced) ───────────

    public function test_operator_cannot_mount_release_even_with_flag_on(): void
    {
        config()->set('clusev.release_controls', true);

        Livewire::actingAs($this->operator())
            ->test(ReleaseIndex::class)
            ->assertForbidden();
    }

    public function test_viewer_cannot_mount_release_even_with_flag_on(): void
    {
        config()->set('clusev.release_controls', true);

        Livewire::actingAs($this->viewer())
            ->test(ReleaseIndex::class)
            ->assertForbidden();
    }

    public function test_admin_can_mount_release_with_flag_on(): void
    {
        config()->set('clusev.release_controls', true);

        Livewire::actingAs($this->admin())
            ->test(ReleaseIndex::class)
            ->assertOk();
    }
}
