<?php

namespace Tests\Feature;

use App\Livewire\Modals\CreateServer;
use App\Livewire\Servers\Show;
use App\Livewire\System\Index as SystemIndex;
use App\Livewire\Terminal\Index as TerminalIndex;
use App\Models\HostCredential;
use App\Models\Server;
use App\Models\TerminalSession;
use App\Models\User;
use App\Services\DeploymentService;
use App\Services\FleetService;
use App\Support\Confirm\ConfirmToken;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Livewire\Livewire;
use Mockery;
use Tests\TestCase;

/**
 * RBAC guards on the DANGEROUS fleet + host-terminal + domain/TLS actions. The RBAC foundation
 * (Role enum, Gates manage-fleet/manage-panel + operate) is committed separately; this proves the
 * abort_unless(...) guards fire at every dangerous entry point:
 *  - manage-fleet gates server registration + credential surface (CreateServer, credential
 *    delete/toggle, SSH-key install, host-terminal login) and the HOST shell target;
 *  - operate gates a SERVER terminal session (day-to-day), not the host shell;
 *  - manage-panel gates the domain/TLS control-plane mutations (System\Index).
 * Admin (factory default) may act; operator/viewer are refused with 403 where they lack the
 * ability. Every host/SSH service is mocked so no real network is touched (hermetic RefreshDatabase).
 */
class RbacFleetGateTest 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]);
    }

    private function server(string $name = 's', string $ip = '10.0.0.9'): Server
    {
        return Server::create(['name' => $name, 'ip' => $ip, 'ssh_port' => 22, 'status' => 'online']);
    }

    /** A modal-confirmed, server-scoped token (mirrors the real confirm step). */
    private function confirmed(string $event, array $params, ?int $serverId = null): string
    {
        $token = ConfirmToken::issue($event, $params, '', null, $serverId);
        ConfirmToken::confirm($token);

        return $token;
    }

    // ── manage-fleet: CreateServer modal (mount + save) ─────────────────────────

    public function test_admin_can_open_and_save_create_server(): void
    {
        // SSH probe is mocked so the admin path performs no real login.
        $this->mock(FleetService::class, fn ($m) => $m->shouldReceive('testConnection')->andReturn(['ok' => true, 'error' => null]));

        Livewire::actingAs($this->admin())
            ->test(CreateServer::class)
            ->set('name', 'web-1')->set('ip', '10.0.0.5')->set('sshPort', 22)
            ->set('username', 'root')->set('authType', 'password')->set('secret', 'pw')
            ->call('save')
            ->assertHasNoErrors(); // allowed through, SSH mocked away

        $this->assertSame(1, Server::count());
    }

    public function test_operator_cannot_open_create_server(): void
    {
        // No FleetService probe should ever be reached — the mount guard fires first.
        $this->mock(FleetService::class, fn ($m) => $m->shouldReceive('testConnection')->never());

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

        $this->assertSame(0, Server::count());
    }

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

    // ── manage-fleet: Servers\Show::deleteCredential ────────────────────────────

    public function test_admin_can_delete_a_credential(): void
    {
        // ConfirmToken binds to the issuing uid — authenticate BEFORE minting it.
        $this->actingAs($this->admin());

        $server = $this->server();
        $server->credential()->create(['username' => 'root', 'auth_type' => 'password', 'secret' => 'x']);

        $token = $this->confirmed('credentialDeleted', [], $server->id);

        Livewire::test(Show::class, ['server' => $server])
            ->call('deleteCredential', $token);

        $this->assertFalse($server->credential()->exists()); // allowed → credential gone
    }

    public function test_operator_cannot_delete_a_credential(): void
    {
        $server = $this->server();
        $server->credential()->create(['username' => 'root', 'auth_type' => 'password', 'secret' => 'x']);

        // The method guard fires before the token is even consumed — no token needed.
        Livewire::actingAs($this->operator())
            ->test(Show::class, ['server' => $server])
            ->call('deleteCredential', 'irrelevant')
            ->assertForbidden();

        $this->assertTrue($server->credential()->exists(), 'operator must not be able to delete the credential');
    }

    public function test_viewer_cannot_delete_a_credential(): void
    {
        $server = $this->server();
        $server->credential()->create(['username' => 'root', 'auth_type' => 'password', 'secret' => 'x']);

        Livewire::actingAs($this->viewer())
            ->test(Show::class, ['server' => $server])
            ->call('deleteCredential', 'irrelevant')
            ->assertForbidden();

        $this->assertTrue($server->credential()->exists());
    }

    // ── manage-fleet vs operate: Terminal\Index::open (split by target) ──────────

    public function test_admin_can_open_host_and_server_shells(): void
    {
        HostCredential::create([
            'host' => 'host.docker.internal', 'port' => 22, 'username' => 'root',
            'auth_type' => 'password', 'secret' => 'r00t-pw',
        ]);
        $server = $this->server();

        Livewire::actingAs($this->admin())
            ->test(TerminalIndex::class)
            ->call('open', 'host')->assertSet('activeKey', 'host')
            ->call('open', 'server', $server->uuid)->assertSet('activeKey', $server->uuid);

        $this->assertSame(2, TerminalSession::count()); // both targets minted a session
    }

    public function test_operator_cannot_open_the_host_shell(): void
    {
        HostCredential::create([
            'host' => 'host.docker.internal', 'port' => 22, 'username' => 'root',
            'auth_type' => 'password', 'secret' => 'r00t-pw',
        ]);

        Livewire::actingAs($this->operator())
            ->test(TerminalIndex::class)
            ->call('open', 'host')
            ->assertForbidden(); // host shell = manage-fleet

        $this->assertSame(0, TerminalSession::count());
    }

    public function test_operator_can_open_a_server_shell(): void
    {
        $server = $this->server();

        Livewire::actingAs($this->operator())
            ->test(TerminalIndex::class)
            ->call('open', 'server', $server->uuid)
            ->assertSet('activeKey', $server->uuid); // server shell = operate → allowed

        $this->assertSame(1, TerminalSession::count());
    }

    public function test_viewer_cannot_open_host_or_server_shell(): void
    {
        $server = $this->server();

        Livewire::actingAs($this->viewer())
            ->test(TerminalIndex::class)
            ->call('open', 'host')
            ->assertForbidden();

        Livewire::actingAs($this->viewer())
            ->test(TerminalIndex::class)
            ->call('open', 'server', $server->uuid)
            ->assertForbidden();

        $this->assertSame(0, TerminalSession::count());
    }

    public function test_operator_cannot_configure_the_host_login(): void
    {
        Livewire::actingAs($this->operator())
            ->test(TerminalIndex::class)
            ->call('configureHost')
            ->assertForbidden();
    }

    // ── manage-panel: System\Index domain/TLS mutations ─────────────────────────

    public function test_admin_can_apply_a_domain_change(): void
    {
        // ConfirmToken binds to the issuing uid — authenticate BEFORE minting it.
        $this->actingAs($this->admin());

        $token = $this->confirmed('domainChanged', ['domain' => 'panel.example.test']);

        Livewire::test(SystemIndex::class)
            ->call('applyDomain', $token)
            ->assertSet('domainInput', 'panel.example.test'); // allowed → domain persisted

        $this->assertSame('panel.example.test', app(DeploymentService::class)->configuredDomain());
    }

    public function test_operator_cannot_apply_a_domain_change(): void
    {
        Livewire::actingAs($this->operator())
            ->test(SystemIndex::class)
            ->call('applyDomain', 'irrelevant')
            ->assertForbidden();

        $this->assertNull(app(DeploymentService::class)->configuredDomain());
    }

    public function test_operator_cannot_confirm_a_domain_change(): void
    {
        Livewire::actingAs($this->operator())
            ->test(SystemIndex::class)
            ->set('domainInput', 'panel.example.test')
            ->call('confirmDomain')
            ->assertForbidden();
    }

    public function test_viewer_cannot_apply_a_domain_change(): void
    {
        Livewire::actingAs($this->viewer())
            ->test(SystemIndex::class)
            ->call('applyDomain', 'irrelevant')
            ->assertForbidden();
    }
}
