<?php

namespace Tests\Feature;

use App\Livewire\Docker\Index;
use App\Livewire\Modals\ContainerLogs;
use App\Models\HostCredential;
use App\Models\Server;
use App\Models\User;
use App\Services\DockerService;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Livewire\Livewire;
use Mockery;
use Tests\TestCase;

class DockerComponentTest extends TestCase
{
    use RefreshDatabase;

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

    private function admin(): User
    {
        return User::factory()->create(['must_change_password' => false]);
    }

    private function operator(): User
    {
        return User::factory()->operator()->create(['must_change_password' => false]);
    }

    private function viewer(): User
    {
        return User::factory()->viewer()->create(['must_change_password' => false]);
    }

    private function activeServer(): Server
    {
        $server = Server::create(['name' => 'box', 'ip' => '10.0.0.2', 'ssh_port' => 22, 'status' => 'online']);
        $server->credential()->create(['username' => 'root', 'auth_type' => 'password', 'secret' => 'x']);
        session(['active_server_id' => $server->id]);

        return $server;
    }

    private function stubDocker(array $containers = []): DockerService
    {
        $docker = Mockery::mock(DockerService::class);
        $docker->shouldReceive('available')->andReturn(true);
        $docker->shouldReceive('containers')->andReturn($containers);
        app()->instance(DockerService::class, $docker);

        return $docker;
    }

    private function hostCred(): HostCredential
    {
        return HostCredential::create([
            'host' => 'host.docker.internal', 'port' => 22,
            'username' => 'root', 'auth_type' => 'password', 'secret' => 'x',
        ]);
    }

    public function test_admin_defaults_to_the_clusev_host_when_a_host_login_is_configured(): void
    {
        // The host is where the operator's own containers live, so it is the default target — but
        // only for admins (manage-fleet); the control-plane machine is not exposed to lower roles.
        $this->actingAs($this->admin());
        $this->hostCred();
        $this->activeServer(); // a fleet server also exists, but the host wins the default
        $this->stubDocker([['id' => 'h1', 'name' => 'clusev-app-1', 'image' => 'clusev-app:prod', 'state' => 'running', 'status' => 'Up', 'ports' => '']]);

        Livewire::test(Index::class)
            ->call('load')
            ->assertOk()
            ->assertSet('connected', true)
            ->assertSee('clusev-app-1')
            ->assertSee(__('docker.host_target'));
    }

    public function test_non_admin_never_targets_the_host_even_when_configured(): void
    {
        // A viewer/operator must land on a fleet server, never the Clusev host — no toggle, no default.
        $this->actingAs($this->operator());
        $this->hostCred();
        $this->activeServer();
        $this->stubDocker([['id' => 's1', 'name' => 'fleet-web', 'image' => 'nginx', 'state' => 'running', 'status' => 'Up', 'ports' => '']]);

        Livewire::test(Index::class)
            ->call('load')
            ->assertOk()
            ->assertSet('connected', true)
            ->assertSee('fleet-web')
            ->assertDontSee(__('docker.host_target')); // no host switch, no host subtitle
    }

    public function test_switching_to_host_without_a_login_shows_the_unconfigured_state(): void
    {
        $this->actingAs($this->admin());
        $this->activeServer();
        $this->stubDocker();

        Livewire::test(Index::class)
            ->call('setTarget', 'host')
            ->assertOk()
            ->assertSet('hostNotConfigured', true)
            ->assertSet('connected', false)
            ->assertSee(__('docker.host_unconfigured_title'));
    }

    public function test_can_switch_from_the_host_to_the_active_fleet_server(): void
    {
        $this->actingAs($this->admin());
        $this->hostCred();
        $this->activeServer();
        $this->stubDocker([['id' => 's1', 'name' => 'web', 'image' => 'nginx', 'state' => 'running', 'status' => 'Up', 'ports' => '80/tcp']]);

        Livewire::test(Index::class)
            ->call('setTarget', 'server')
            ->assertOk()
            ->assertSet('connected', true)
            ->assertSee('web');
    }

    public function test_operator_target_forced_to_host_still_hits_a_fleet_server_not_the_host(): void
    {
        // Even if a non-admin forces target=host, effectiveTarget() pins them to a fleet server, so
        // the action runs against the server credential path — never the control-plane host.
        $this->actingAs($this->operator());
        $this->hostCred();
        $this->activeServer();
        $docker = $this->stubDocker();
        $docker->shouldReceive('containerAction')->once()
            ->with(Mockery::on(fn ($s) => $s->exists === true), 'web', 'restart') // a real fleet server, not the transient host
            ->andReturn(['ok' => true, 'output' => '']);

        Livewire::test(Index::class)
            ->set('target', 'host')
            ->call('action', 'web', 'restart')
            ->assertOk();
    }

    public function test_admin_can_action_a_host_container_and_it_is_audited(): void
    {
        $this->actingAs($this->admin());
        $this->hostCred(); // default target = host
        $docker = $this->stubDocker();
        $docker->shouldReceive('containerAction')->once()
            ->with(Mockery::type(Server::class), 'clusev-redis-1', 'restart')
            ->andReturn(['ok' => true, 'output' => '']);

        Livewire::test(Index::class)
            ->call('action', 'clusev-redis-1', 'restart')
            ->assertOk();

        // Host actions carry a null server_id (transient host server); the target label names it.
        $this->assertDatabaseHas('audit_events', ['action' => 'docker.action', 'server_id' => null]);
    }

    public function test_operator_cannot_open_host_logs(): void
    {
        // The host-logs sentinel (serverId 0) is admin-only; an operator is refused at the read.
        $this->actingAs($this->operator());
        $this->hostCred();
        $docker = Mockery::mock(DockerService::class);
        $docker->shouldReceive('logs')->never();
        app()->instance(DockerService::class, $docker);

        Livewire::test(ContainerLogs::class, ['serverId' => 0, 'ref' => 'clusev-app-1'])
            ->call('load')
            ->assertForbidden();
    }

    public function test_logs_modal_loads_a_host_container_tail(): void
    {
        $this->actingAs($this->admin());
        $this->hostCred();
        $docker = Mockery::mock(DockerService::class);
        $docker->shouldReceive('logs')->once()->with(Mockery::type(Server::class), 'clusev-app-1', 200)->andReturn('host log line');
        app()->instance(DockerService::class, $docker);

        Livewire::test(ContainerLogs::class, ['serverId' => 0, 'ref' => 'clusev-app-1'])
            ->call('load')
            ->assertOk()
            ->assertSet('logs', 'host log line');
    }

    public function test_viewer_can_browse_the_container_list(): void
    {
        $this->actingAs($this->viewer());
        $this->activeServer();
        $this->stubDocker([['id' => 'abc', 'name' => 'web', 'image' => 'nginx', 'state' => 'running', 'status' => 'Up', 'ports' => '80/tcp']]);

        Livewire::test(Index::class)
            ->call('load')
            ->assertOk()
            ->assertSet('connected', true)
            ->assertSee('web');
    }

    public function test_shows_a_clean_not_installed_state_when_docker_is_absent(): void
    {
        // A host without a container runtime (e.g. a native postfix/dovecot mail server) must NOT
        // render the raw "sh: 1: docker: not found" shell error — that reads as a broken feature.
        // available() is probed first; false => a clean, honest "not installed" panel.
        $this->actingAs($this->viewer());
        $this->activeServer();
        $docker = Mockery::mock(DockerService::class);
        $docker->shouldReceive('available')->once()->andReturn(false);
        $docker->shouldReceive('containers')->never(); // never even attempt ps when docker is absent
        app()->instance(DockerService::class, $docker);

        Livewire::test(Index::class)
            ->call('load')
            ->assertOk()
            ->assertSet('connected', false)
            ->assertSet('notInstalled', true)
            ->assertSet('error', null)
            ->assertSee(__('docker.not_installed_title'))
            ->assertDontSee('not found');
    }

    public function test_viewer_cannot_run_a_container_action(): void
    {
        $this->actingAs($this->viewer());
        $this->activeServer();
        $docker = $this->stubDocker();
        $docker->shouldReceive('containerAction')->never(); // guard fires first

        Livewire::test(Index::class)
            ->call('action', 'web', 'stop')
            ->assertForbidden();
    }

    public function test_operator_can_run_a_container_action_and_it_is_audited(): void
    {
        $this->actingAs($this->operator());
        $server = $this->activeServer();
        $docker = $this->stubDocker();
        $docker->shouldReceive('containerAction')->once()
            ->with(Mockery::type(Server::class), 'web', 'restart')
            ->andReturn(['ok' => true, 'output' => 'web']);

        Livewire::test(Index::class)
            ->call('action', 'web', 'restart')
            ->assertOk();

        $this->assertDatabaseHas('audit_events', ['action' => 'docker.action', 'server_id' => $server->id]);
    }

    public function test_admin_can_run_a_container_action(): void
    {
        $this->actingAs($this->admin());
        $this->activeServer();
        $docker = $this->stubDocker();
        $docker->shouldReceive('containerAction')->once()->andReturn(['ok' => true, 'output' => '']);

        Livewire::test(Index::class)->call('action', 'db', 'start')->assertOk();
    }

    public function test_viewer_cannot_open_logs(): void
    {
        $this->actingAs($this->viewer());
        $this->activeServer();
        $this->stubDocker();

        Livewire::test(Index::class)->call('viewLogs', 'web')->assertForbidden();
    }

    public function test_viewer_cannot_read_logs_in_the_modal(): void
    {
        $this->actingAs($this->viewer());
        $server = $this->activeServer();
        $docker = Mockery::mock(DockerService::class);
        $docker->shouldReceive('logs')->never(); // guard fires before the read
        app()->instance(DockerService::class, $docker);

        Livewire::test(ContainerLogs::class, ['serverId' => $server->id, 'ref' => 'web'])
            ->call('load')
            ->assertForbidden();
    }

    public function test_logs_modal_loads_a_container_tail(): void
    {
        $this->actingAs($this->admin());
        $server = $this->activeServer();
        $docker = Mockery::mock(DockerService::class);
        $docker->shouldReceive('logs')->once()->with(Mockery::type(Server::class), 'web', 200)->andReturn('log line 1');
        app()->instance(DockerService::class, $docker);

        Livewire::test(ContainerLogs::class, ['serverId' => $server->id, 'ref' => 'web'])
            ->call('load')
            ->assertOk()
            ->assertSet('logs', 'log line 1');
    }
}
