<?php

namespace Tests\Feature;

use App\Models\Server;
use App\Models\SshCredential;
use App\Services\FirewallService;
use App\Services\FleetService;
use App\Services\HardeningService;
use App\Support\Os\OsDetector;
use App\Support\Os\OsProfile;
use Mockery;
use Tests\TestCase;

class HardeningServiceTest extends TestCase
{
    protected function tearDown(): void
    {
        Mockery::close();
        parent::tearDown();
    }

    private function debianProfile(): OsProfile
    {
        return new OsProfile(
            family: 'debian',
            id: 'debian',
            prettyName: 'Debian GNU/Linux 13',
            packageManager: 'apt',
            firewallTool: 'ufw',
            serviceManager: 'systemd',
            available: ['apt-get', 'dpkg', 'systemctl', 'fail2ban-client', 'ufw'],
        );
    }

    /**
     * The checklist must agree with the fail2ban/firewall panels: a component counts as
     * installed when the package manager lists it (^ii/rpm) OR its binary is on PATH OR
     * the service is running — not via dpkg `^ii` alone (the false "nicht installiert").
     */
    public function test_installed_probe_falls_back_to_binary_and_service(): void
    {
        $os = $this->debianProfile();

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

        $captured = '';
        $fleet = Mockery::mock(FleetService::class);
        $fleet->shouldReceive('runPrivileged')
            ->andReturnUsing(function ($server, $cmd) use (&$captured) {
                $captured = $cmd;

                // fail2ban running from a source/`rc` install: present + active.
                return ['ok' => true, 'output' => "fail2ban 1 active\nfirewall 1 active\nautoupdate 0 inactive"];
            });

        $service = new HardeningService($fleet, Mockery::mock(FirewallService::class), $detector);

        $rows = $service->state(new Server);

        // fail2ban installed-probe gains the binary fallback (was dpkg ^ii only — the bug).
        $this->assertStringContainsString('command -v fail2ban-client', $captured);
        // firewall installed-probe gains the package-manager fallback (was command -v only).
        $this->assertStringContainsString('dpkg -l ufw', $captured);

        $f2b = collect($rows)->firstWhere('key', 'fail2ban');
        $this->assertTrue($f2b['featureOn'], 'fail2ban active must read as on');
    }

    public function test_autoupdate_row_is_neutral_and_never_insecure(): void
    {
        $os = $this->debianProfile();

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

        $fleet = Mockery::mock(FleetService::class);
        $fleet->shouldReceive('runPrivileged')->andReturn([
            'ok' => true,
            // auto-update reported INACTIVE by the host:
            'output' => "fail2ban 1 active\nfirewall 1 active\nautoupdate 1 inactive",
        ]);

        $service = new HardeningService($fleet, Mockery::mock(FirewallService::class), $detector);
        $rows = $service->state(new Server);

        $au = collect($rows)->firstWhere('key', 'unattended');
        $this->assertTrue($au['secure'], 'auto-updates must never read as insecure');
        $this->assertTrue($au['neutral'] ?? false, 'auto-updates row must be flagged neutral');
        $this->assertFalse($au['featureOn'], 'inactive auto-updates → toggle shows enable');
    }

    /**
     * Lock-out guard: when Clusev connects AS root, disabling SSH root login (PermitRootLogin no)
     * would sever its own access — it must be refused before any command runs.
     */
    public function test_disabling_root_login_is_refused_when_clusev_connects_as_root(): void
    {
        $detector = Mockery::mock(OsDetector::class);
        $detector->shouldReceive('detect')->andReturn($this->debianProfile());

        $fleet = Mockery::mock(FleetService::class);
        $fleet->shouldNotReceive('runPrivileged'); // guard must short-circuit before any command

        $server = new Server;
        $server->setRelation('credential', new SshCredential(['username' => 'root']));

        $service = new HardeningService($fleet, Mockery::mock(FirewallService::class), $detector);
        $res = $service->apply($server, 'ssh_root', false);

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

    public function test_fail2ban_enable_pins_the_sshd_jail_to_the_systemd_backend(): void
    {
        $detector = Mockery::mock(OsDetector::class);
        $detector->shouldReceive('detect')->andReturn($this->debianProfile());

        $captured = '';
        $fleet = Mockery::mock(FleetService::class);
        $fleet->shouldReceive('runPrivileged')->once()
            ->andReturnUsing(function ($s, $cmd) use (&$captured) {
                $captured = $cmd;

                return ['ok' => true, 'output' => ''];
            });

        $service = new HardeningService($fleet, Mockery::mock(FirewallService::class), $detector);
        $service->apply(new Server, 'fail2ban', true);

        // The default /var/log/auth.log backend fails on minimal images — pin systemd (journal).
        $this->assertStringContainsString('backend = systemd', $captured);
        $this->assertStringContainsString('/etc/fail2ban/jail.d/00-clusev-sshd.local', $captured);
    }

    public function test_disabling_root_login_proceeds_for_a_non_root_credential(): void
    {
        $detector = Mockery::mock(OsDetector::class);
        $detector->shouldReceive('detect')->andReturn($this->debianProfile());

        $fleet = Mockery::mock(FleetService::class);
        $fleet->shouldReceive('runPrivileged')->once()->andReturn(['ok' => true, 'output' => '']);

        $server = new Server;
        $server->setRelation('credential', new SshCredential(['username' => 'deploy']));

        $service = new HardeningService($fleet, Mockery::mock(FirewallService::class), $detector);
        $res = $service->apply($server, 'ssh_root', false);

        $this->assertTrue($res['ok']);
    }
}
