<?php

namespace Tests\Feature;

use App\Models\Server;
use App\Services\Fail2banService;
use App\Services\FleetService;
use App\Support\Os\OsDetector;
use App\Support\Os\OsProfile;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Mockery;
use Tests\TestCase;

/**
 * `fail2ban-client set <jail> banip <ip>` is built by interpolating the jail name. The
 * base64 transport in runPrivileged() neutralises SHELL metacharacters, but a jail name
 * beginning with `-` (e.g. "-h", "--help") is still parsed by fail2ban-client as an
 * OPTION rather than a positional jail argument. The jail reaches this sink from a
 * client-controlled source — Fail2banBan::$jail (a wire:model property) and
 * Fail2banBans::unbanIp($jail) (a method argument) — so validJail() must reject a
 * leading dash (and dot) while still accepting real jail names that contain an internal
 * dash, like "nginx-http-auth".
 */
class Fail2banJailValidationTest extends TestCase
{
    use RefreshDatabase;

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

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

    /** FleetService whose privileged exec must NEVER run (guard fires first). */
    private function fleetNeverRuns(): FleetService
    {
        $fleet = Mockery::mock(FleetService::class);
        $fleet->shouldReceive('runPrivileged')->never();

        return $fleet;
    }

    private function service(FleetService $fleet, ?OsDetector $detector = null): Fail2banService
    {
        return new Fail2banService($fleet, $detector ?? Mockery::mock(OsDetector::class));
    }

    public function test_ban_rejects_a_jail_beginning_with_a_dash(): void
    {
        $res = $this->service($this->fleetNeverRuns())->ban($this->server(), '-h', '1.2.3.4');

        $this->assertFalse($res['ok']);
        $this->assertSame(__('backend.invalid_input'), $res['output']);
    }

    public function test_unban_rejects_a_double_dash_option_jail(): void
    {
        $res = $this->service($this->fleetNeverRuns())->unban($this->server(), '--all', '1.2.3.4');

        $this->assertFalse($res['ok']);
        $this->assertSame(__('backend.invalid_input'), $res['output']);
    }

    public function test_ban_rejects_a_jail_beginning_with_a_dot(): void
    {
        $res = $this->service($this->fleetNeverRuns())->ban($this->server(), '.evil', '1.2.3.4');

        $this->assertFalse($res['ok']);
        $this->assertSame(__('backend.invalid_input'), $res['output']);
    }

    /**
     * A real jail with an INTERNAL dash must still pass validation — proves the tightened
     * regex is not over-broad. It gets PAST validJail and reaches support(), which we stub
     * to report "unsupported"; the distinguishing signal is that the output is the support
     * reason, NOT invalid_input (which would mean validJail wrongly rejected it).
     */
    public function test_valid_jail_with_internal_dash_passes_validation(): void
    {
        // A real profile where fail2ban is unsupported (apk/openrc), so support() returns a reason.
        $profile = new OsProfile('alpine', 'alpine', 'Alpine Linux', 'apk', 'none', 'openrc', []);
        $reason = $profile->supports('fail2ban');
        $this->assertNotNull($reason); // sanity: this profile really reports unsupported

        $detector = Mockery::mock(OsDetector::class);
        $detector->shouldReceive('detect')->andReturn($profile);

        $res = $this->service($this->fleetNeverRuns(), $detector)
            ->unban($this->server(), 'nginx-http-auth', '1.2.3.4');

        $this->assertFalse($res['ok']);
        $this->assertSame($reason, $res['output']); // reached support(), not the validJail reject
    }
}
