<?php

namespace Tests\Feature;

use App\Livewire\Threats\Index;
use App\Models\AuditEvent;
use App\Models\BannedIp;
use App\Models\User;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Support\Facades\Cache;
use Livewire\Livewire;
use Tests\TestCase;

/**
 * The THREATS honeypot dashboard: admin-only access (route middleware + mount guard),
 * the KPI counts + probe feed, and the unban mutation (reuses auth.ip_unbanned audit code).
 */
class ThreatsPageTest extends TestCase
{
    use RefreshDatabase;

    private const ATTACKER_IP = '203.0.113.99';

    protected function setUp(): void
    {
        parent::setUp();
        Cache::flush();
    }

    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]);
    }

    // ── Access control ──────────────────────────────────────────────────────────

    public function test_admin_can_load_the_threats_route(): void
    {
        $this->actingAs($this->admin());

        $this->get('/threats')->assertOk();
    }

    public function test_operator_is_forbidden_from_the_threats_route(): void
    {
        $this->actingAs($this->operator());

        $this->get('/threats')->assertForbidden();
    }

    public function test_viewer_is_forbidden_from_the_threats_route(): void
    {
        $this->actingAs($this->viewer());

        $this->get('/threats')->assertForbidden();
    }

    public function test_admin_can_mount_the_component(): void
    {
        Livewire::actingAs($this->admin())
            ->test(Index::class)
            ->assertOk();
    }

    public function test_operator_mount_aborts_403(): void
    {
        Livewire::actingAs($this->operator())
            ->test(Index::class)
            ->assertForbidden();
    }

    public function test_viewer_mount_aborts_403(): void
    {
        Livewire::actingAs($this->viewer())
            ->test(Index::class)
            ->assertForbidden();
    }

    // ── Feed + KPIs ─────────────────────────────────────────────────────────────

    public function test_page_shows_honeypot_events_and_correct_kpis(): void
    {
        // Two probes within 24h + one honeytoken trip + one stale probe (outside the 24h window).
        AuditEvent::create(['actor' => 'system', 'action' => 'security.honeypot_hit', 'target' => '/wp-login.php', 'ip' => self::ATTACKER_IP]);
        AuditEvent::create(['actor' => 'system', 'action' => 'security.honeypot_hit', 'target' => '/.env', 'ip' => self::ATTACKER_IP]);
        AuditEvent::create(['actor' => 'system', 'action' => 'security.honeytoken_used', 'target' => 'db_password', 'ip' => self::ATTACKER_IP]);
        AuditEvent::create(['actor' => 'system', 'action' => 'security.honeypot_hit', 'target' => '/old', 'ip' => '198.51.100.7', 'created_at' => now()->subDays(3)]);

        BannedIp::create(['ip' => self::ATTACKER_IP, 'banned_until' => now()->addHour(), 'reason' => 'honeypot', 'attempts' => 1]);

        Livewire::actingAs($this->admin())
            ->test(Index::class)
            ->assertSee(self::ATTACKER_IP)
            ->assertSee('/wp-login.php')
            ->assertSee('Angriffsversuch')       // disguised action label (DE default) — no "Honeypot" leak
            ->assertSee('Kompromittierte Zugangsdaten')
            ->assertViewHas('probes_24h', 2)     // 2 recent hits, the 3-day-old one excluded
            ->assertViewHas('banned_total', 1)
            ->assertViewHas('honeytoken_trips', 1)
            ->assertViewHas('top_ip', self::ATTACKER_IP);
    }

    public function test_page_shows_honeypot_login_attempts_with_tried_credentials(): void
    {
        AuditEvent::create([
            'actor' => 'system',
            'action' => 'security.honeypot_login',
            'target' => '/wp-login.php',
            'ip' => self::ATTACKER_IP,
            'meta' => ['tried_user' => 'admin', 'tried_pass' => 'hunter2', 'method' => 'POST'],
        ]);

        Livewire::actingAs($this->admin())
            ->test(Index::class)
            ->assertSee(self::ATTACKER_IP)
            ->assertSee('Login-Angriffsversuch')   // disguised action label (DE default) — no "Honeypot" leak
            ->assertSee('admin')                   // captured tried_user
            ->assertSee('hunter2')                 // captured tried_pass
            ->assertViewHas('login_attempts_24h', 1);
    }

    public function test_the_threats_page_never_reveals_the_deception_layer(): void
    {
        // OPSEC: even the admin-only intel page must not NAME the mechanism — a shoulder-surf, a
        // shared screen, or a breached admin session must not learn the decoy paths are a trap.
        AuditEvent::create(['actor' => 'system', 'action' => 'security.honeypot_hit', 'target' => '/wp-admin', 'ip' => self::ATTACKER_IP]);
        AuditEvent::create(['actor' => 'system', 'action' => 'security.honeytoken_used', 'target' => 'db_password', 'ip' => self::ATTACKER_IP]);

        Livewire::actingAs($this->admin())
            ->test(Index::class)
            ->assertSee('Angriffsversuch')          // disguised event label
            ->assertSee('Sicherheitsereignisse')    // disguised feed title
            ->assertSee('Geleakte Zugangsdaten')    // disguised honeytoken KPI
            ->assertDontSee('Honeypot')
            ->assertDontSee('Honeytoken')
            ->assertDontSee('Täuschung')            // "Täuschungsebene"
            ->assertDontSee('Köder');               // "Köder-Pfade"
    }

    public function test_tried_credentials_are_html_escaped(): void
    {
        // Attacker-controlled meta must render escaped, never as live markup.
        AuditEvent::create([
            'actor' => 'system',
            'action' => 'security.honeypot_login',
            'target' => '/wp-login.php',
            'ip' => self::ATTACKER_IP,
            'meta' => ['tried_user' => '<script>alert(1)</script>', 'tried_pass' => 'x'],
        ]);

        Livewire::actingAs($this->admin())
            ->test(Index::class)
            ->assertDontSee('<script>alert(1)</script>', false)  // raw markup must NOT be present
            ->assertSee('&lt;script&gt;', false);                // it is escaped instead
    }

    public function test_top_ip_is_null_when_there_is_no_traffic(): void
    {
        Livewire::actingAs($this->admin())
            ->test(Index::class)
            ->assertViewHas('top_ip', null)
            ->assertViewHas('probes_24h', 0)
            ->assertSee('—');
    }

    public function test_search_filters_the_feed_by_ip(): void
    {
        AuditEvent::create(['actor' => 'system', 'action' => 'security.honeypot_hit', 'target' => '/wp-login.php', 'ip' => self::ATTACKER_IP]);
        AuditEvent::create(['actor' => 'system', 'action' => 'security.honeypot_hit', 'target' => '/xmlrpc.php', 'ip' => '198.51.100.7']);

        Livewire::actingAs($this->admin())
            ->test(Index::class)
            ->set('q', self::ATTACKER_IP)
            ->assertViewHas('events', fn ($e) => $e->total() === 1)
            ->assertSee('/wp-login.php')
            ->assertDontSee('/xmlrpc.php');
    }

    // ── Unban mutation ──────────────────────────────────────────────────────────

    public function test_admin_can_unban_and_it_is_audited(): void
    {
        BannedIp::create(['ip' => self::ATTACKER_IP, 'banned_until' => now()->addHour(), 'reason' => 'honeypot', 'attempts' => 1]);

        Livewire::actingAs($this->admin())
            ->test(Index::class)
            ->call('unban', self::ATTACKER_IP)
            ->assertDispatched('notify');

        $this->assertDatabaseMissing('banned_ips', ['ip' => self::ATTACKER_IP]);
        $this->assertTrue(
            AuditEvent::where('action', 'auth.ip_unbanned')
                ->where('target', self::ATTACKER_IP)
                ->exists()
        );
    }

    public function test_operator_cannot_unban(): void
    {
        BannedIp::create(['ip' => self::ATTACKER_IP, 'banned_until' => now()->addHour(), 'reason' => 'honeypot', 'attempts' => 1]);

        // An admin boots the component so the snapshot is valid, then we swap to an operator and
        // call the mutation — the unban() abort_unless must still fire 403 (mid-session demotion or
        // a hand-crafted /livewire/update). Mirrors RbacNetworkGateTest::test_operator_cannot_add…
        $component = Livewire::actingAs($this->admin())->test(Index::class)->assertOk();

        $this->actingAs($this->operator());

        $component->call('unban', self::ATTACKER_IP)->assertForbidden();

        $this->assertDatabaseHas('banned_ips', ['ip' => self::ATTACKER_IP]);
    }

    public function test_viewer_cannot_unban(): void
    {
        BannedIp::create(['ip' => self::ATTACKER_IP, 'banned_until' => now()->addHour(), 'reason' => 'honeypot', 'attempts' => 1]);

        $component = Livewire::actingAs($this->admin())->test(Index::class)->assertOk();

        $this->actingAs($this->viewer());

        $component->call('unban', self::ATTACKER_IP)->assertForbidden();

        $this->assertDatabaseHas('banned_ips', ['ip' => self::ATTACKER_IP]);
    }
}
