<?php

namespace Tests\Feature;

use App\Mail\AlertNotification;
use App\Models\AlertIncident;
use App\Models\AlertRule;
use App\Models\Server;
use App\Models\Setting;
use App\Models\User;
use App\Services\AlertNotifier;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Support\Facades\Http;
use Illuminate\Support\Facades\Mail;
use Tests\TestCase;

class AlertNotifierTest extends TestCase
{
    use RefreshDatabase;

    private function incident(): AlertIncident
    {
        $rule = AlertRule::create(['name' => 'CPU', 'metric' => 'cpu', 'comparator' => 'gt', 'threshold' => 80, 'scope_type' => 'all', 'enabled' => true]);
        $server = Server::create(['name' => 'box', 'ip' => '10.0.0.1', 'ssh_port' => 22, 'status' => 'online']);

        return AlertIncident::create(['alert_rule_id' => $rule->id, 'server_id' => $server->id, 'state' => 'firing', 'value' => 95, 'started_at' => now()]);
    }

    public function test_email_is_queued_to_the_configured_recipients(): void
    {
        Mail::fake();
        Setting::put('alert_email_to', "ops@example.com\nsecond@example.com");

        app(AlertNotifier::class)->notify($this->incident(), resolved: false);

        Mail::assertQueued(AlertNotification::class, fn (AlertNotification $m) => $m->hasTo('ops@example.com'));
    }

    public function test_falls_back_to_admin_emails_when_none_configured(): void
    {
        Mail::fake();
        $admin = User::factory()->create(['email' => 'admin@example.com', 'must_change_password' => false]); // admin by default

        app(AlertNotifier::class)->notify($this->incident(), resolved: false);

        Mail::assertQueued(AlertNotification::class, fn (AlertNotification $m) => $m->hasTo('admin@example.com'));
    }

    public function test_a_webhook_is_posted_with_the_payload(): void
    {
        Mail::fake();
        Http::fake();
        Setting::put('alert_webhooks', 'https://8.8.8.8/incoming'); // public IP literal (no DNS, passes the SSRF check)

        app(AlertNotifier::class)->notify($this->incident(), resolved: false);

        Http::assertSent(fn ($request) => $request->url() === 'https://8.8.8.8/incoming'
            && $request['state'] === 'firing'
            && $request['server'] === 'box'
            && (int) $request['value'] === 95); // value is a float column (95.0)
    }

    public function test_a_non_http_webhook_value_is_ignored(): void
    {
        Mail::fake();
        Http::fake();
        Setting::put('alert_webhooks', "javascript:alert(1)\nnot a url");

        app(AlertNotifier::class)->notify($this->incident(), resolved: false);

        Http::assertNothingSent();
    }

    public function test_unsafe_webhook_urls_are_rejected(): void
    {
        $unsafe = [
            'http://127.0.0.1/hook',                     // loopback
            'https://10.0.0.5/hook',                     // private
            'http://192.168.1.10/x',                     // private
            'http://169.254.169.254/latest/meta-data',   // link-local (cloud-metadata SSRF classic)
            'http://localhost/x',                        // resolves to loopback
            'ftp://example.com/x',                       // bad scheme
            'httpx://8.8.8.8/x',                         // bad scheme
            'https:///x',                                // no host
        ];

        foreach ($unsafe as $url) {
            $this->assertFalse(AlertNotifier::isSafeWebhookUrl($url), "should reject {$url}");
        }
    }

    public function test_public_https_webhook_is_accepted(): void
    {
        $this->assertTrue(AlertNotifier::isSafeWebhookUrl('https://8.8.8.8/hook')); // public IP, no DNS needed
    }

    public function test_a_failing_channel_is_swallowed(): void
    {
        Mail::fake();
        Http::fake(fn () => throw new \RuntimeException('connection refused'));
        Setting::put('alert_webhooks', 'https://8.8.8.8/incoming');

        // Must NOT throw — a broken channel can never break the poll loop that called notify().
        app(AlertNotifier::class)->notify($this->incident(), resolved: false);

        $this->assertTrue(true);
    }
}
