<?php

namespace Tests\Feature;

use App\Models\Server;
use App\Services\DockerService;
use App\Services\FleetService;
use Illuminate\Foundation\Testing\RefreshDatabase;
use InvalidArgumentException;
use Mockery;
use Tests\TestCase;

class DockerServiceTest 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.1', 'ssh_port' => 22, 'status' => 'online']);
    }

    /** DockerService over a mocked FleetService::runPrivileged; returns [$docker, $fleet]. */
    private function make(): array
    {
        $fleet = Mockery::mock(FleetService::class);

        return [new DockerService($fleet), $fleet];
    }

    public function test_containers_parses_the_tab_separated_ps_output_and_derives_state(): void
    {
        [$docker, $fleet] = $this->make();
        // id \t names \t image \t status \t ports — state is derived from the Status text.
        $out = "abc123\tweb\tnginx\tUp 2 hours\t80/tcp\n"
            ."def456\tdb\tmariadb\tExited (0) 3 minutes ago\t\n"
            ."ghi789\tcache\tredis\tUp 1 hour (Paused)\t6379/tcp";
        $fleet->shouldReceive('runPrivileged')->once()->andReturn(['ok' => true, 'output' => $out]);

        $rows = $docker->containers($this->server());

        $this->assertCount(3, $rows);
        $this->assertSame('web', $rows[0]['name']);
        $this->assertSame('running', $rows[0]['state']);   // "Up ..."
        $this->assertSame('exited', $rows[1]['state']);    // "Exited ..."
        $this->assertSame('paused', $rows[2]['state']);    // "Up ... (Paused)"
    }

    public function test_containers_throws_the_real_error_when_docker_fails(): void
    {
        [$docker, $fleet] = $this->make();
        // A daemon/permission error must NOT read as "no containers" — it surfaces to the UI.
        $fleet->shouldReceive('runPrivileged')->andReturn(['ok' => false, 'output' => "Cannot connect to the Docker daemon at unix:///var/run/docker.sock.\nIs the docker daemon running?"]);

        $this->expectException(\RuntimeException::class);
        $this->expectExceptionMessage('Cannot connect to the Docker daemon');

        $docker->containers($this->server());
    }

    public function test_container_action_runs_the_right_command_for_a_valid_ref(): void
    {
        [$docker, $fleet] = $this->make();
        $fleet->shouldReceive('runPrivileged')->once()
            ->with(Mockery::type(Server::class), Mockery::on(fn ($cmd) => str_starts_with($cmd, 'export PATH=') && str_contains($cmd, 'docker restart web1')))
            ->andReturn(['ok' => true, 'output' => 'web1']);

        $res = $docker->containerAction($this->server(), 'web1', 'restart');

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

    public function test_container_action_rejects_an_unknown_op(): void
    {
        [$docker, $fleet] = $this->make();
        $fleet->shouldReceive('runPrivileged')->never();
        $this->expectException(InvalidArgumentException::class);

        $docker->containerAction($this->server(), 'web', 'rm -rf');
    }

    public function test_container_action_rejects_a_ref_with_shell_metacharacters(): void
    {
        [$docker, $fleet] = $this->make();
        $fleet->shouldReceive('runPrivileged')->never();
        $this->expectException(InvalidArgumentException::class);

        $docker->containerAction($this->server(), 'web; rm -rf /', 'stop');
    }

    public function test_container_action_rejects_a_leading_dash_ref(): void
    {
        [$docker, $fleet] = $this->make();
        $fleet->shouldReceive('runPrivileged')->never();
        $this->expectException(InvalidArgumentException::class);

        $docker->containerAction($this->server(), '--help', 'stop'); // arg injection
    }

    public function test_logs_clamps_the_line_count_and_validates_the_ref(): void
    {
        [$docker, $fleet] = $this->make();
        $fleet->shouldReceive('runPrivileged')->once()
            ->with(Mockery::type(Server::class), Mockery::on(fn ($cmd) => str_starts_with($cmd, 'export PATH=') && str_contains($cmd, 'docker logs --tail 2000 web 2>&1 | head -c 262144')))
            ->andReturn(['ok' => true, 'output' => 'log line']);

        $this->assertSame('log line', $docker->logs($this->server(), 'web', 99999)); // clamped to 2000, byte-capped
    }

    public function test_container_action_rejects_a_ref_with_a_trailing_newline(): void
    {
        [$docker, $fleet] = $this->make();
        $fleet->shouldReceive('runPrivileged')->never();
        $this->expectException(InvalidArgumentException::class);

        $docker->containerAction($this->server(), "web\n", 'stop'); // \z anchor blocks the trailing newline
    }

    public function test_available_reflects_the_probe(): void
    {
        [$docker, $fleet] = $this->make();
        $fleet->shouldReceive('runPrivileged')->andReturn(['ok' => true, 'output' => 'yes']);

        $this->assertTrue($docker->available($this->server()));
    }
}
