<?php

declare(strict_types=1);

namespace Tests\Feature\Warranty;

use App\Enums\AuditEvent;
use App\Mail\WarrantyExpiryDigest;
use App\Models\Asset;
use App\Models\AssetWarranty;
use App\Models\AuditLog;
use App\Models\User;
use Database\Seeders\SettingSeeder;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Http\UploadedFile;
use Illuminate\Support\Facades\Mail;
use Maatwebsite\Excel\Facades\Excel;
use PHPUnit\Framework\Attributes\Test;
use Tests\TestCase;

class WarrantyManagementTest extends TestCase
{
    use RefreshDatabase;

    // -------------------------------------------------------------- access

    #[Test]
    public function a_guest_cannot_reach_warranty_screens(): void
    {
        $asset = Asset::factory()->create();

        $this->get(route('warranty.index'))->assertRedirect(route('login'));
        $this->get(route('assets.warranties.create', $asset))->assertRedirect(route('login'));
    }

    #[Test]
    public function any_signed_in_user_can_add_a_warranty(): void
    {
        $user = User::factory()->create();
        $asset = Asset::factory()->create();

        $this->actingAs($user)->get(route('assets.warranties.create', $asset))->assertOk();

        $response = $this->actingAs($user)->post(route('assets.warranties.store', $asset), [
            'type' => 'manufacturer',
            'provider' => 'Dell',
            'start_date' => now()->toDateString(),
            'end_date' => now()->addYear()->toDateString(),
        ]);

        $response->assertRedirect(route('assets.show', $asset));
        $this->assertDatabaseHas('asset_warranties', ['asset_id' => $asset->id, 'provider' => 'Dell']);
    }

    // ------------------------------------------------------------- primary

    #[Test]
    public function the_first_warranty_added_becomes_primary_automatically(): void
    {
        $user = User::factory()->create();
        $asset = Asset::factory()->create();

        $this->actingAs($user)->post(route('assets.warranties.store', $asset), [
            'type' => 'manufacturer',
            'start_date' => now()->toDateString(),
            'end_date' => now()->addYear()->toDateString(),
        ]);

        $warranty = AssetWarranty::query()->where('asset_id', $asset->id)->firstOrFail();
        $this->assertTrue($warranty->is_primary);
        $this->assertTrue($asset->refresh()->isUnderWarranty());
    }

    #[Test]
    public function a_second_warranty_does_not_become_primary_unless_asked(): void
    {
        $user = User::factory()->create();
        $asset = Asset::factory()->create();
        $first = AssetWarranty::factory()->forAsset($asset)->primary()->create();

        $this->actingAs($user)->post(route('assets.warranties.store', $asset), [
            'type' => 'insurance',
            'start_date' => now()->toDateString(),
            'end_date' => now()->addYear()->toDateString(),
        ]);

        $this->assertTrue($first->refresh()->is_primary);
        $this->assertSame(1, AssetWarranty::query()->where('asset_id', $asset->id)->where('is_primary', true)->count());
    }

    #[Test]
    public function marking_a_new_warranty_primary_demotes_the_previous_one(): void
    {
        $user = User::factory()->create();
        $asset = Asset::factory()->create();
        $first = AssetWarranty::factory()->forAsset($asset)->primary()->create();

        $this->actingAs($user)->post(route('assets.warranties.store', $asset), [
            'type' => 'amc',
            'start_date' => now()->toDateString(),
            'end_date' => now()->addYear()->toDateString(),
            'make_primary' => '1',
        ]);

        $this->assertFalse($first->refresh()->is_primary);
        $this->assertSame(1, AssetWarranty::query()->where('asset_id', $asset->id)->where('is_primary', true)->count());
    }

    #[Test]
    public function updating_a_warranty_never_changes_which_one_is_primary(): void
    {
        $user = User::factory()->create();
        $asset = Asset::factory()->create();
        $primary = AssetWarranty::factory()->forAsset($asset)->primary()->create();
        $other = AssetWarranty::factory()->forAsset($asset)->create();

        $this->actingAs($user)->put(route('assets.warranties.update', [$asset, $other]), [
            'type' => $other->type->value,
            'provider' => 'Updated Provider',
            'start_date' => $other->start_date->toDateString(),
            'end_date' => $other->end_date->toDateString(),
        ]);

        $this->assertTrue($primary->refresh()->is_primary);
        $this->assertFalse($other->refresh()->is_primary);
        $this->assertSame('Updated Provider', $other->provider);
    }

    // --------------------------------------------------------------- renew

    #[Test]
    public function renewing_closes_the_old_warranty_and_opens_a_new_primary_one(): void
    {
        $user = User::factory()->create();
        $asset = Asset::factory()->create();
        $original = AssetWarranty::factory()->forAsset($asset)->primary()->create([
            'type' => 'amc',
            'end_date' => now()->addDays(10),
        ]);

        $response = $this->actingAs($user)->post(route('assets.warranties.renew.store', [$asset, $original]), [
            'provider' => 'New Provider',
            'start_date' => now()->addDays(11)->toDateString(),
            'end_date' => now()->addYear()->toDateString(),
        ]);

        $response->assertRedirect(route('assets.show', $asset));

        $this->assertFalse($original->refresh()->is_primary);

        $renewed = AssetWarranty::query()->where('asset_id', $asset->id)->where('is_primary', true)->firstOrFail();
        $this->assertSame('amc', $renewed->type->value);
        $this->assertSame('New Provider', $renewed->provider);
        $this->assertNotSame($original->id, $renewed->id);

        $this->assertSame(2, AssetWarranty::query()->where('asset_id', $asset->id)->count());
    }

    #[Test]
    public function renewing_is_audited(): void
    {
        $user = User::factory()->create();
        $asset = Asset::factory()->create();
        $original = AssetWarranty::factory()->forAsset($asset)->primary()->create(['type' => 'manufacturer']);

        $this->actingAs($user)->post(route('assets.warranties.renew.store', [$asset, $original]), [
            'start_date' => now()->toDateString(),
            'end_date' => now()->addYear()->toDateString(),
        ]);

        $log = AuditLog::query()->where('event', AuditEvent::Renewed->value)->firstOrFail();
        $this->assertStringContainsString($asset->asset_tag, $log->description);

        // Attached to the asset, not the new warranty row, so it lands in
        // the asset's own Recent Activity feed alongside every other
        // domain event (Assigned, Returned, Imported).
        $this->assertSame($asset->getMorphClass(), $log->auditable_type);
        $this->assertSame($asset->id, $log->auditable_id);
    }

    // -------------------------------------------------------------- delete

    #[Test]
    public function a_regular_user_cannot_delete_a_warranty(): void
    {
        $user = User::factory()->create();
        $asset = Asset::factory()->create();
        $warranty = AssetWarranty::factory()->forAsset($asset)->create();

        $this->actingAs($user)->delete(route('assets.warranties.destroy', [$asset, $warranty]))->assertForbidden();
    }

    #[Test]
    public function a_master_can_delete_and_restore_a_warranty(): void
    {
        $master = User::factory()->master()->create();
        $asset = Asset::factory()->create();
        $warranty = AssetWarranty::factory()->forAsset($asset)->create();

        $this->actingAs($master)->delete(route('assets.warranties.destroy', [$asset, $warranty]))->assertRedirect();
        $this->assertSoftDeleted($warranty);

        $this->actingAs($master)->patch(route('assets.warranties.restore', [$asset, $warranty]))->assertRedirect();
        $this->assertNotSoftDeleted($warranty->fresh());
    }

    #[Test]
    public function a_warranty_belonging_to_a_different_asset_404s(): void
    {
        $user = User::factory()->create();
        $assetA = Asset::factory()->create();
        $assetB = Asset::factory()->create();
        $warranty = AssetWarranty::factory()->forAsset($assetB)->create();

        $this->actingAs($user)->get(route('assets.warranties.edit', [$assetA, $warranty]))->assertNotFound();
    }

    // ------------------------------------------------------------ document

    #[Test]
    public function a_document_can_be_uploaded_and_downloaded(): void
    {
        $user = User::factory()->create();
        $asset = Asset::factory()->create();

        $this->actingAs($user)->post(route('assets.warranties.store', $asset), [
            'type' => 'manufacturer',
            'start_date' => now()->toDateString(),
            'end_date' => now()->addYear()->toDateString(),
            'document' => UploadedFile::fake()->create('warranty.pdf', 200, 'application/pdf'),
        ]);

        $warranty = AssetWarranty::query()->where('asset_id', $asset->id)->firstOrFail();
        $this->assertTrue($warranty->hasDocument());

        $this->actingAs($user)->get(route('assets.warranties.document', [$asset, $warranty]))->assertOk();
    }

    #[Test]
    public function a_document_can_be_removed_on_update(): void
    {
        $user = User::factory()->create();
        $asset = Asset::factory()->create();
        $warranty = AssetWarranty::factory()->forAsset($asset)->create([
            'document_path' => 'warranties/1/existing.pdf',
            'document_original_name' => 'existing.pdf',
            'document_mime' => 'application/pdf',
        ]);

        $this->actingAs($user)->put(route('assets.warranties.update', [$asset, $warranty]), [
            'type' => $warranty->type->value,
            'start_date' => $warranty->start_date->toDateString(),
            'end_date' => $warranty->end_date->toDateString(),
            'remove_document' => '1',
        ]);

        $this->assertFalse($warranty->fresh()->hasDocument());
    }

    // -------------------------------------------------------------- scopes

    #[Test]
    public function expiring_within_includes_only_active_coverage_ending_by_then(): void
    {
        AssetWarranty::factory()->expiringInDays(10)->create();
        AssetWarranty::factory()->expiringInDays(80)->create();
        AssetWarranty::factory()->expired()->create();

        $this->assertSame(1, AssetWarranty::query()->expiringWithin(30)->count());
        $this->assertSame(2, AssetWarranty::query()->expiringWithin(90)->count());
        $this->assertSame(0, AssetWarranty::query()->expiringWithin(90)->expired()->count());
    }

    // --------------------------------------------------------------- pages

    #[Test]
    public function the_dashboard_cards_reflect_current_counts(): void
    {
        $user = User::factory()->create();
        AssetWarranty::factory()->expiringInDays(10)->create();
        AssetWarranty::factory()->expired()->create();

        $response = $this->actingAs($user)->get(route('warranty.index'));

        $response->assertOk();
        $response->assertViewHas('cards', fn (array $cards) => $cards['expiring_30'] === 1 && $cards['expired'] === 1);
    }

    #[Test]
    public function the_expiring_page_groups_by_urgency(): void
    {
        $user = User::factory()->create();
        $critical = AssetWarranty::factory()->expiringInDays(5)->primary()->create();
        $upcoming = AssetWarranty::factory()->expiringInDays(60)->primary()->create();
        $expired = AssetWarranty::factory()->expired()->create();

        $response = $this->actingAs($user)->get(route('warranty.expiring'));

        $response->assertOk();
        $response->assertSee($critical->asset->asset_tag);
        $response->assertSee($upcoming->asset->asset_tag);
        $response->assertSee($expired->asset->asset_tag);
    }

    #[Test]
    public function the_warranty_list_can_be_exported(): void
    {
        $user = User::factory()->create();
        AssetWarranty::factory()->create();

        Excel::fake();
        $this->travelTo($frozen = now());

        $this->actingAs($user)->get(route('warranty.export'))->assertOk();

        Excel::assertDownloaded(
            'aumni-ams-warranties-'.$frozen->format('Y-m-d-His').'.xlsx',
        );
    }

    // -------------------------------------------------------------- digest

    #[Test]
    public function the_digest_command_sends_nothing_when_emails_are_disabled(): void
    {
        $this->seed(SettingSeeder::class);
        settings()->set('enable_warranty_emails', false);
        settings()->set('warranty_alert_recipients', ['ops@aumnitechworks.com']);

        Mail::fake();

        $this->artisan('ams:send-warranty-digest')->assertSuccessful();

        Mail::assertNothingSent();
    }

    #[Test]
    public function the_digest_command_sends_nothing_when_no_recipients_are_configured(): void
    {
        $this->seed(SettingSeeder::class);
        settings()->set('enable_warranty_emails', true);
        settings()->set('warranty_alert_recipients', []);

        Mail::fake();

        $this->artisan('ams:send-warranty-digest')->assertSuccessful();

        Mail::assertNothingSent();
    }

    #[Test]
    public function the_digest_command_emails_configured_recipients_when_something_is_due(): void
    {
        $this->seed(SettingSeeder::class);
        settings()->set('enable_warranty_emails', true);
        settings()->set('warranty_alert_recipients', ['ops@aumnitechworks.com']);
        settings()->set('warranty_alert_days', [30]);

        AssetWarranty::factory()->primary()->create([
            'start_date' => now()->subYear(),
            'end_date' => now()->addDays(30),
        ]);

        Mail::fake();

        $this->artisan('ams:send-warranty-digest')->assertSuccessful();

        Mail::assertSent(WarrantyExpiryDigest::class, function (WarrantyExpiryDigest $mail): bool {
            return $mail->hasTo('ops@aumnitechworks.com') && $mail->windows[30]->isNotEmpty();
        });
    }

    #[Test]
    public function the_digest_command_sends_nothing_when_nothing_is_due(): void
    {
        $this->seed(SettingSeeder::class);
        settings()->set('enable_warranty_emails', true);
        settings()->set('warranty_alert_recipients', ['ops@aumnitechworks.com']);
        settings()->set('warranty_alert_days', [30]);

        // Expiring in 10 days, not exactly on the 30-day window.
        AssetWarranty::factory()->expiringInDays(10)->primary()->create();

        Mail::fake();

        $this->artisan('ams:send-warranty-digest')->assertSuccessful();

        Mail::assertNothingSent();
    }
}
