<?php

namespace Tests\Feature;

use App\Models\Server;
use App\Services\FleetService;
use App\Services\HardeningService;
use App\Services\SshKeyProvisioner;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Mockery;
use Tests\TestCase;

class SshKeyProvisionerTest extends TestCase
{
    use RefreshDatabase;

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

    private function passwordServer(): Server
    {
        $server = Server::create(['name' => 'box', 'ip' => '10.0.0.5', 'ssh_port' => 22, 'status' => 'online']);
        $server->credential()->create(['username' => 'admin', 'auth_type' => 'password', 'secret' => 'pw-secret']);

        return $server->fresh('credential');
    }

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

        $fleet = Mockery::mock(FleetService::class);
        $fleet->shouldReceive('addAuthorizedKey')->once();
        $fleet->shouldReceive('testConnection')->once()->andReturn(['ok' => true, 'error' => null]);

        $hardening = Mockery::mock(HardeningService::class);
        $hardening->shouldReceive('apply')->once()->with(Mockery::type(Server::class), 'ssh_password', false)
            ->andReturn(['ok' => true, 'output' => '']);

        $result = (new SshKeyProvisioner($fleet, $hardening))->enableKeyOnlyAccess($server);

        $this->assertTrue($result['ok']);
        $this->assertNotEmpty($result['privateKey']);
        $this->assertStringContainsString('ssh-ed25519', $result['publicKey']);

        $cred = $server->fresh('credential')->credential;
        $this->assertSame('key', $cred->auth_type);
        $this->assertStringContainsString('PRIVATE KEY', $cred->secret); // now holds the private PEM
    }

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

        $fleet = Mockery::mock(FleetService::class);
        $fleet->shouldReceive('addAuthorizedKey')->once();
        $fleet->shouldReceive('testConnection')->once()->andReturn(['ok' => false, 'error' => 'auth failed']);

        $hardening = Mockery::mock(HardeningService::class);
        $hardening->shouldNotReceive('apply'); // never disable password if the key didn't verify

        $result = (new SshKeyProvisioner($fleet, $hardening))->enableKeyOnlyAccess($server);

        $this->assertFalse($result['ok']);
        $this->assertNotEmpty($result['error']);

        $cred = $server->fresh('credential')->credential;
        $this->assertSame('password', $cred->auth_type, 'credential restored to password');
        $this->assertSame('pw-secret', $cred->secret, 'original secret restored');
    }

    public function test_already_key_credential_skips_keygen_and_just_disables_password(): void
    {
        $server = Server::create(['name' => 'box2', 'ip' => '10.0.0.6', 'ssh_port' => 22, 'status' => 'online']);
        $server->credential()->create(['username' => 'admin', 'auth_type' => 'key', 'secret' => 'EXISTING-PRIVATE-KEY']);
        $server = $server->fresh('credential');

        $fleet = Mockery::mock(FleetService::class);
        $fleet->shouldNotReceive('addAuthorizedKey'); // already key — no new key generated
        $fleet->shouldNotReceive('testConnection');

        $hardening = Mockery::mock(HardeningService::class);
        $hardening->shouldReceive('apply')->once()->with(Mockery::type(Server::class), 'ssh_password', false)
            ->andReturn(['ok' => true, 'output' => '']);

        $result = (new SshKeyProvisioner($fleet, $hardening))->enableKeyOnlyAccess($server);

        $this->assertTrue($result['ok']);
        $this->assertArrayNotHasKey('privateKey', $result); // nothing new to reveal
        $this->assertSame('EXISTING-PRIVATE-KEY', $server->fresh('credential')->credential->secret);
    }

    public function test_no_credential_returns_error(): void
    {
        $server = Server::create(['name' => 'box3', 'ip' => '10.0.0.7', 'ssh_port' => 22, 'status' => 'online']);

        $result = (new SshKeyProvisioner(Mockery::mock(FleetService::class), Mockery::mock(HardeningService::class)))
            ->enableKeyOnlyAccess($server);

        $this->assertFalse($result['ok']);
    }

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

        $fleet = Mockery::mock(FleetService::class);
        $fleet->shouldReceive('addAuthorizedKey')->once();
        $fleet->shouldReceive('testConnection')->once()->andReturn(['ok' => true, 'error' => null]);

        $hardening = Mockery::mock(HardeningService::class);
        $hardening->shouldReceive('apply')->once()->andReturn(['ok' => false, 'output' => 'sshd reload failed']);

        $result = (new SshKeyProvisioner($fleet, $hardening))->enableKeyOnlyAccess($server);

        $this->assertFalse($result['ok']);
        $this->assertNotEmpty($result['privateKey']); // UI can still reveal it
        // Credential stays the verified key — operator keeps access (NOT rolled back).
        $this->assertSame('key', $server->fresh('credential')->credential->auth_type);
    }
}
