<?php

namespace Tests\Feature;

use App\Models\Server;
use App\Services\CommandRunner;
use App\Services\FleetService;
use Illuminate\Database\Eloquent\Collection;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Mockery;
use Tests\TestCase;

class CommandRunnerTest extends TestCase
{
    use RefreshDatabase;

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

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

    public function test_runs_the_command_on_every_server_and_collects_results(): void
    {
        $a = $this->server('a', '10.0.0.1');
        $b = $this->server('b', '10.0.0.2');
        $fleet = Mockery::mock(FleetService::class);
        $fleet->shouldReceive('runPlain')->twice()->andReturn(['ok' => true, 'output' => 'done']);

        $results = (new CommandRunner($fleet))->run('uptime', new Collection([$a, $b]));

        $this->assertCount(2, $results);
        $this->assertSame('a', $results[0]['server']);
        $this->assertTrue($results[0]['ok']);
        $this->assertSame('done', $results[0]['output']);
    }

    public function test_one_server_failing_does_not_abort_the_batch(): void
    {
        $a = $this->server('a', '10.0.0.1');
        $b = $this->server('b', '10.0.0.2');
        $fleet = Mockery::mock(FleetService::class);
        $fleet->shouldReceive('runPlain')->once()->with($a, Mockery::any(), Mockery::any())->andThrow(new \RuntimeException('ssh timeout'));
        $fleet->shouldReceive('runPlain')->once()->with($b, Mockery::any(), Mockery::any())->andReturn(['ok' => true, 'output' => 'ok']);

        $results = (new CommandRunner($fleet))->run('uptime', new Collection([$a, $b]));

        $this->assertFalse($results[0]['ok']);              // a failed
        $this->assertStringContainsString('ssh timeout', $results[0]['output']);
        $this->assertTrue($results[1]['ok']);               // b still ran
    }

    public function test_a_nonzero_exit_is_reported_as_not_ok(): void
    {
        $a = $this->server('a', '10.0.0.1');
        $fleet = Mockery::mock(FleetService::class);
        $fleet->shouldReceive('runPlain')->once()->andReturn(['ok' => false, 'output' => 'command not found']);

        $results = (new CommandRunner($fleet))->run('nope', new Collection([$a]));

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

    public function test_output_is_byte_capped_and_utf8_scrubbed(): void
    {
        $a = $this->server('a', '10.0.0.1');
        $fleet = Mockery::mock(FleetService::class);
        // 300 KiB of output + an invalid UTF-8 byte → must be capped and scrubbed (no broken snapshot).
        $fleet->shouldReceive('runPlain')->once()->andReturn(['ok' => true, 'output' => str_repeat('x', 300000)."\xB1"]);

        $results = (new CommandRunner($fleet))->run('cat big', new Collection([$a]));

        $this->assertLessThanOrEqual(262144, strlen($results[0]['output']));
        $this->assertSame($results[0]['output'], mb_scrub($results[0]['output'], 'UTF-8')); // already valid UTF-8
    }
}
