<?php

namespace Tests\Feature;

use App\Livewire\Modals\Fail2banBans;
use App\Models\AuditEvent;
use App\Models\Server;
use App\Models\User;
use App\Services\Fail2banService;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Livewire\Livewire;
use Mockery;
use Tests\TestCase;

class Fail2banBansModalTest extends TestCase
{
    use RefreshDatabase;

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

    /** The modal opens instantly: mount stores only the id, with no SSH read yet. */
    public function test_mount_does_not_read_status_so_the_modal_opens_instantly(): void
    {
        $this->actingAs(User::factory()->create());
        $server = Server::create(['name' => 'box', 'ip' => '10.0.0.9', 'ssh_port' => 22, 'status' => 'online']);

        // status() must NOT be called during mount — it is deferred to load().
        $svc = Mockery::mock(Fail2banService::class);
        $svc->shouldReceive('status')->never();
        app()->instance(Fail2banService::class, $svc);

        Livewire::test(Fail2banBans::class, ['serverId' => $server->id])
            ->assertSet('loaded', false)
            ->assertSet('jails', []);
    }

    /** load() (fired by wire:init) reads the live status and renders the jail/IP list. */
    public function test_load_reads_status_and_renders_the_jail_list(): void
    {
        $this->actingAs(User::factory()->create());
        $server = Server::create(['name' => 'box', 'ip' => '10.0.0.9', 'ssh_port' => 22, 'status' => 'online']);

        $svc = Mockery::mock(Fail2banService::class);
        $svc->shouldReceive('status')->once()->with(Mockery::on(fn ($s) => $s->id === $server->id))
            ->andReturn([
                'jails' => [
                    ['name' => 'sshd', 'currentlyBanned' => 1, 'currentlyFailed' => 3, 'bannedIps' => ['203.0.113.7']],
                ],
            ]);
        app()->instance(Fail2banService::class, $svc);

        Livewire::test(Fail2banBans::class, ['serverId' => $server->id])
            ->call('load')
            ->assertSet('loaded', true)
            ->assertSee('sshd')
            ->assertSee('203.0.113.7');
    }

    /** A read error from status() surfaces the translated error message. */
    public function test_load_surfaces_a_read_error(): void
    {
        $this->actingAs(User::factory()->create());
        $server = Server::create(['name' => 'box', 'ip' => '10.0.0.9', 'ssh_port' => 22, 'status' => 'online']);

        $svc = Mockery::mock(Fail2banService::class);
        $svc->shouldReceive('status')->once()->andReturn(['jails' => [], 'readError' => true]);
        app()->instance(Fail2banService::class, $svc);

        Livewire::test(Fail2banBans::class, ['serverId' => $server->id])
            ->call('load')
            ->assertSet('loaded', true)
            ->assertSet('error', __('servers.fail2ban_read_error'));
    }

    /** unban() calls the service, audits the success, refreshes the list, and signals the page. */
    public function test_unban_calls_service_audits_refreshes_and_dispatches_change(): void
    {
        $this->actingAs(User::factory()->create());
        $server = Server::create(['name' => 'box', 'ip' => '10.0.0.9', 'ssh_port' => 22, 'status' => 'online']);

        $svc = Mockery::mock(Fail2banService::class);
        // Initial load: one banned IP.
        $svc->shouldReceive('status')->once()->andReturn([
            'jails' => [
                ['name' => 'sshd', 'currentlyBanned' => 1, 'currentlyFailed' => 3, 'bannedIps' => ['203.0.113.7']],
            ],
        ]);
        $svc->shouldReceive('unban')->once()
            ->with(Mockery::on(fn ($s) => $s->id === $server->id), 'sshd', '203.0.113.7')
            ->andReturn(['ok' => true, 'output' => 'unbanned']);
        // Refresh after unban: the IP is gone.
        $svc->shouldReceive('status')->once()->andReturn([
            'jails' => [
                ['name' => 'sshd', 'currentlyBanned' => 0, 'currentlyFailed' => 3, 'bannedIps' => []],
            ],
        ]);
        app()->instance(Fail2banService::class, $svc);

        Livewire::test(Fail2banBans::class, ['serverId' => $server->id])
            ->call('load')
            ->assertSee('203.0.113.7')
            ->call('unbanIp', 'sshd', '203.0.113.7')
            ->assertDispatched('fail2banChanged')
            ->assertDispatched('notify')
            ->assertDontSee('203.0.113.7');

        $this->assertDatabaseHas('audit_events', [
            'server_id' => $server->id,
            'action' => 'fail2ban.unban',
        ]);
    }

    /** A failed unban surfaces a notice and does NOT audit or signal the page. */
    public function test_unban_failure_notifies_without_auditing(): void
    {
        $this->actingAs(User::factory()->create());
        $server = Server::create(['name' => 'box', 'ip' => '10.0.0.9', 'ssh_port' => 22, 'status' => 'online']);

        $svc = Mockery::mock(Fail2banService::class);
        $svc->shouldReceive('status')->once()->andReturn([
            'jails' => [
                ['name' => 'sshd', 'currentlyBanned' => 1, 'currentlyFailed' => 3, 'bannedIps' => ['203.0.113.7']],
            ],
        ]);
        $svc->shouldReceive('unban')->once()->andReturn(['ok' => false, 'output' => 'no permission']);
        app()->instance(Fail2banService::class, $svc);

        Livewire::test(Fail2banBans::class, ['serverId' => $server->id])
            ->call('load')
            ->call('unbanIp', 'sshd', '203.0.113.7')
            ->assertDispatched('notify')
            ->assertNotDispatched('fail2banChanged');

        $this->assertSame(0, AuditEvent::where('action', 'fail2ban.unban')->count());
    }
}
