<?php

namespace Tests\Feature;

use App\Livewire\Files\Index as Files;
use App\Livewire\Modals\FileEditor;
use App\Livewire\Services\Index as Services;
use App\Models\Server;
use App\Models\User;
use App\Services\FleetService;
use App\Support\Confirm\ConfirmToken;
use App\Support\Ssh\Sftp;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Livewire\Livewire;
use Mockery;
use Tests\TestCase;

/**
 * RBAC `operate` gate on the day-to-day MUTATIONS of a managed server. Viewing the
 * lists stays open to everyone (read-only), but changing a systemd unit's state or
 * writing the remote filesystem requires operate (admin + operator; viewer denied).
 *
 * The RBAC foundation (Role enum, User::roleAtLeast(), Gate `operate`) plus the
 * network/fleet gates are committed separately; this proves the abort_unless(...)
 * guards are wired at every service/file mutation entry point:
 *  - Services\Index::applyService (systemctl start/stop/restart apply handler);
 *  - Files\Index::deleteEntry (SFTP unlink apply handler);
 *  - Modals\FileEditor::save (SFTP write).
 * Every SSH/FleetService/Sftp call is mocked — no real host is ever touched. Viewer
 * → 403; operator ALLOWED; admin (factory default) ALLOWED. The pure readers (service
 * list render, file browse) stay open for a viewer — proving read-only, not locked-out.
 */
class RbacOperateGateTest extends TestCase
{
    use RefreshDatabase;

    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]);
    }

    /** An online server with a credential, selected as the active fleet server. */
    private function activeServer(): Server
    {
        $server = Server::create(['name' => 'box', 'ip' => '10.0.0.2', 'ssh_port' => 22, 'status' => 'online']);
        $server->credential()->create(['username' => 'root', 'auth_type' => 'password', 'secret' => 'x']);
        session(['active_server_id' => $server->id]);

        return $server;
    }

    /** A confirmed, single-use token for the given apply handler + server. */
    private function confirmed(string $event, array $params, int $serverId): string
    {
        $token = ConfirmToken::issue($event, $params, '', null, $serverId);
        ConfirmToken::confirm($token); // the modal confirmation step

        return $token;
    }

    // ── operate: Services\Index::applyService (systemctl restart) ────────────────

    public function test_viewer_cannot_apply_a_service_action(): void
    {
        $this->actingAs($this->viewer());
        $server = $this->activeServer();

        // The privileged SSH call must never be reached — the guard fires first.
        $fleet = Mockery::mock(FleetService::class);
        $fleet->shouldReceive('serviceAction')->never();
        app()->instance(FleetService::class, $fleet);

        $token = $this->confirmed('serviceConfirmed', ['op' => 'restart', 'name' => 'nginx.service'], $server->id);

        Livewire::test(Services::class)
            ->call('applyService', $token)
            ->assertForbidden();
    }

    public function test_operator_can_apply_a_service_action(): void
    {
        $this->actingAs($this->operator());
        $server = $this->activeServer();

        $fleet = Mockery::mock(FleetService::class);
        $fleet->shouldReceive('serviceAction')->once()
            ->with(Mockery::type(Server::class), 'restart', 'nginx.service')
            ->andReturn(['ok' => true, 'output' => '']);
        $fleet->shouldReceive('systemd')->andReturn(['services' => []]); // reload after success
        app()->instance(FleetService::class, $fleet);

        $token = $this->confirmed('serviceConfirmed', ['op' => 'restart', 'name' => 'nginx.service'], $server->id);

        Livewire::test(Services::class)
            ->call('applyService', $token)
            ->assertOk(); // allowed through the guard, SSH mocked away
    }

    public function test_admin_can_apply_a_service_action(): void
    {
        $this->actingAs($this->admin());
        $server = $this->activeServer();

        $fleet = Mockery::mock(FleetService::class);
        $fleet->shouldReceive('serviceAction')->once()
            ->with(Mockery::type(Server::class), 'restart', 'nginx.service')
            ->andReturn(['ok' => true, 'output' => '']);
        $fleet->shouldReceive('systemd')->andReturn(['services' => []]);
        app()->instance(FleetService::class, $fleet);

        $token = $this->confirmed('serviceConfirmed', ['op' => 'restart', 'name' => 'nginx.service'], $server->id);

        Livewire::test(Services::class)
            ->call('applyService', $token)
            ->assertOk();
    }

    // ── operate: Files\Index::deleteEntry (SFTP unlink) ─────────────────────────

    public function test_viewer_cannot_delete_a_file(): void
    {
        $this->actingAs($this->viewer());
        $server = $this->activeServer();

        $fleet = Mockery::mock(FleetService::class);
        $fleet->shouldReceive('files')->andReturn([]); // any dir listing during mount/load
        $fleet->shouldReceive('deleteFile')->never();  // guard fires before the SFTP unlink
        app()->instance(FleetService::class, $fleet);

        $token = $this->confirmed('fileConfirmed', ['path' => '/etc/passwd'], $server->id);

        Livewire::test(Files::class)
            ->call('deleteEntry', $token)
            ->assertForbidden();
    }

    public function test_operator_can_delete_a_file(): void
    {
        $this->actingAs($this->operator());
        $server = $this->activeServer();

        $fleet = Mockery::mock(FleetService::class);
        $fleet->shouldReceive('files')->andReturn([]);
        $fleet->shouldReceive('deleteFile')->once()
            ->with(Mockery::type(Server::class), '/etc/passwd');
        app()->instance(FleetService::class, $fleet);

        $token = $this->confirmed('fileConfirmed', ['path' => '/etc/passwd'], $server->id);

        Livewire::test(Files::class)
            ->call('deleteEntry', $token)
            ->assertOk(); // allowed through the guard, SFTP mocked away
    }

    public function test_admin_can_delete_a_file(): void
    {
        $this->actingAs($this->admin());
        $server = $this->activeServer();

        $fleet = Mockery::mock(FleetService::class);
        $fleet->shouldReceive('files')->andReturn([]);
        $fleet->shouldReceive('deleteFile')->once()
            ->with(Mockery::type(Server::class), '/etc/passwd');
        app()->instance(FleetService::class, $fleet);

        $token = $this->confirmed('fileConfirmed', ['path' => '/etc/passwd'], $server->id);

        Livewire::test(Files::class)
            ->call('deleteEntry', $token)
            ->assertOk();
    }

    // ── operate: Modals\FileEditor::save (SFTP write) ───────────────────────────

    public function test_viewer_cannot_save_a_file_edit(): void
    {
        $this->actingAs($this->viewer());
        $server = Server::create(['name' => 's', 'ip' => '10.0.0.9', 'ssh_port' => 22, 'status' => 'online']);

        $fleet = Mockery::mock(FleetService::class);
        $fleet->shouldReceive('writeFile')->never(); // guard fires before the remote write
        $this->app->instance(FleetService::class, $fleet);
        $this->app->instance(Sftp::class, Mockery::mock(Sftp::class)); // resolvable, unused for text

        Livewire::test(FileEditor::class, ['serverId' => $server->id, 'path' => '/etc/x.conf', 'name' => 'x.conf'])
            ->set('content', 'edited = wert')
            ->call('save')
            ->assertForbidden();
    }

    public function test_operator_can_save_a_file_edit(): void
    {
        $this->actingAs($this->operator());
        $server = Server::create(['name' => 's', 'ip' => '10.0.0.9', 'ssh_port' => 22, 'status' => 'online']);

        $fleet = Mockery::mock(FleetService::class);
        $fleet->shouldReceive('writeFile')->once()
            ->with(Mockery::type(Server::class), '/etc/x.conf', 'edited = wert');
        $this->app->instance(FleetService::class, $fleet);
        $this->app->instance(Sftp::class, Mockery::mock(Sftp::class));

        Livewire::test(FileEditor::class, ['serverId' => $server->id, 'path' => '/etc/x.conf', 'name' => 'x.conf'])
            ->set('content', 'edited = wert')
            ->call('save')
            ->assertOk(); // allowed through the guard, SFTP mocked away
    }

    public function test_admin_can_save_a_file_edit(): void
    {
        $this->actingAs($this->admin());
        $server = Server::create(['name' => 's', 'ip' => '10.0.0.9', 'ssh_port' => 22, 'status' => 'online']);

        $fleet = Mockery::mock(FleetService::class);
        $fleet->shouldReceive('writeFile')->once()
            ->with(Mockery::type(Server::class), '/etc/x.conf', 'edited = wert');
        $this->app->instance(FleetService::class, $fleet);
        $this->app->instance(Sftp::class, Mockery::mock(Sftp::class));

        Livewire::test(FileEditor::class, ['serverId' => $server->id, 'path' => '/etc/x.conf', 'name' => 'x.conf'])
            ->set('content', 'edited = wert')
            ->call('save')
            ->assertOk();
    }

    // ── operate: file CONTENT reads (download + editor view) ────────────────────
    // Browsing the LISTING (names/perms/size) stays open to a viewer — see the browse
    // test below. But pulling a file's BYTES (SFTP get) discloses secrets the server's
    // credential can read (/etc/shadow, keys, .env) when the credential is root, which is
    // beyond "read-only". So the content reads — Files::download, Files::edit (opens the
    // editor) and FileEditor::load (the actual read) — require operate, like the writes.

    public function test_viewer_cannot_download_a_file(): void
    {
        $this->actingAs($this->viewer());
        $this->activeServer();

        $fleet = Mockery::mock(FleetService::class);
        $fleet->shouldReceive('files')->andReturn([]);
        $fleet->shouldReceive('getFile')->never(); // guard fires before the SFTP get
        app()->instance(FleetService::class, $fleet);

        Livewire::test(Files::class)
            ->set('entries', [['name' => 'shadow', 'type' => 'file', 'size' => 1, 'perms' => '-rw-------', 'owner' => 'root', 'modified' => 'now']])
            ->call('download', 0)
            ->assertForbidden();
    }

    public function test_operator_can_download_a_file(): void
    {
        $this->actingAs($this->operator());
        $this->activeServer();

        $fleet = Mockery::mock(FleetService::class);
        $fleet->shouldReceive('files')->andReturn([]);
        $fleet->shouldReceive('getFile')->once()
            ->with(Mockery::type(Server::class), '/shadow')
            ->andReturn('root:x:...');
        app()->instance(FleetService::class, $fleet);

        Livewire::test(Files::class)
            ->set('entries', [['name' => 'shadow', 'type' => 'file', 'size' => 1, 'perms' => '-rw-------', 'owner' => 'root', 'modified' => 'now']])
            ->call('download', 0)
            ->assertOk(); // allowed through the guard, SFTP mocked away
    }

    public function test_viewer_cannot_open_the_file_editor(): void
    {
        $this->actingAs($this->viewer());
        $this->activeServer();

        $fleet = Mockery::mock(FleetService::class);
        $fleet->shouldReceive('files')->andReturn([]);
        app()->instance(FleetService::class, $fleet);

        Livewire::test(Files::class)
            ->set('entries', [['name' => 'x.conf', 'type' => 'file', 'size' => 1, 'perms' => '-rw-r--r--', 'owner' => 'root', 'modified' => 'now']])
            ->call('edit', 0)
            ->assertForbidden();
    }

    public function test_viewer_cannot_read_file_content_in_the_editor(): void
    {
        $this->actingAs($this->viewer());
        $server = Server::create(['name' => 's', 'ip' => '10.0.0.9', 'ssh_port' => 22, 'status' => 'online']);

        $fleet = Mockery::mock(FleetService::class);
        $fleet->shouldReceive('readFile')->never(); // guard fires before the remote read
        $this->app->instance(FleetService::class, $fleet);
        $this->app->instance(Sftp::class, Mockery::mock(Sftp::class));

        Livewire::test(FileEditor::class, ['serverId' => $server->id, 'path' => '/etc/x.conf', 'name' => 'x.conf'])
            ->call('load')
            ->assertForbidden();
    }

    public function test_operator_can_read_file_content_in_the_editor(): void
    {
        $this->actingAs($this->operator());
        $server = Server::create(['name' => 's', 'ip' => '10.0.0.9', 'ssh_port' => 22, 'status' => 'online']);

        $fleet = Mockery::mock(FleetService::class);
        $fleet->shouldReceive('readFile')->once()
            ->with(Mockery::type(Server::class), '/etc/x.conf')
            ->andReturn(['content' => 'edited = wert', 'binary' => false, 'tooBig' => false]);
        $this->app->instance(FleetService::class, $fleet);
        $this->app->instance(Sftp::class, Mockery::mock(Sftp::class));

        Livewire::test(FileEditor::class, ['serverId' => $server->id, 'path' => '/etc/x.conf', 'name' => 'x.conf'])
            ->call('load')
            ->assertOk() // allowed through the guard, SFTP mocked away
            ->assertSet('content', 'edited = wert');
    }

    // ── read-only: a viewer keeps READ access (proves read-only, not locked-out) ─

    public function test_viewer_can_load_the_services_list(): void
    {
        $this->actingAs($this->viewer());
        $server = $this->activeServer();

        $fleet = Mockery::mock(FleetService::class);
        $fleet->shouldReceive('systemd')->andReturn([
            'services' => [['name' => 'nginx.service', 'status' => 'online', 'sub' => 'running', 'enabled' => true, 'desc' => 'web']],
            'journal' => [],
            'cursor' => null,
        ]);
        app()->instance(FleetService::class, $fleet);

        Livewire::test(Services::class)
            ->assertOk()                 // page mounts for a viewer
            ->call('load')               // the lazy read runs
            ->assertOk()
            ->assertSet('connected', true)
            ->assertSet('services.0.name', 'nginx.service'); // the list is visible
    }

    public function test_viewer_can_browse_the_file_manager(): void
    {
        $this->actingAs($this->viewer());
        $this->activeServer();

        $fleet = Mockery::mock(FleetService::class);
        $fleet->shouldReceive('files')->andReturn([
            ['name' => 'etc', 'type' => 'dir', 'size' => null, 'perms' => 'drwxr-xr-x', 'owner' => 'root', 'modified' => 'now'],
        ]);
        app()->instance(FleetService::class, $fleet);

        Livewire::test(Files::class)
            ->assertOk()                 // page mounts for a viewer
            ->call('load')               // the lazy directory read runs
            ->assertOk()
            ->assertSet('connected', true)
            ->assertSet('entries.0.name', 'etc'); // the listing is visible
    }
}
