<?php

declare(strict_types=1);

use App\Http\Requests\NovaCards\UploadNovaCardBackgroundRequest;
use App\Events\NovaCards\NovaCardAutosaved;
use App\Events\NovaCards\NovaCardBackgroundUploaded;
use App\Events\NovaCards\NovaCardCreated;
use App\Events\NovaCards\NovaCardPublished;
use App\Events\NovaCards\NovaCardTemplateSelected;
use App\Models\NovaCard;
use App\Models\NovaCardCategory;
use App\Models\NovaCardBackground;
use App\Models\NovaCardTemplate;
use App\Models\User;
use App\Services\NovaCards\NovaCardBackgroundService;
use App\Services\NovaCards\NovaCardRenderService;
use Illuminate\Http\UploadedFile;
use Illuminate\Support\Arr;
use Illuminate\Support\Facades\Event;
use Illuminate\Support\Facades\Storage;
use Illuminate\Support\Str;

function draftCategory(array $attributes = []): NovaCardCategory
{
    return NovaCardCategory::query()->create(array_merge([
        'slug' => 'draft-category-' . Str::lower(Str::random(6)),
        'name' => 'Draft Category',
        'description' => 'Draft category',
        'active' => true,
        'order_num' => 0,
    ], $attributes));
}

function draftTemplate(array $attributes = []): NovaCardTemplate
{
    return NovaCardTemplate::query()->create(array_merge([
        'slug' => 'draft-template-' . Str::lower(Str::random(6)),
        'name' => 'Draft Template',
        'description' => 'Draft template',
        'config_json' => [
            'font_preset' => 'modern-sans',
            'gradient_preset' => 'midnight-nova',
            'layout' => 'quote_heavy',
            'text_align' => 'center',
            'text_color' => '#ffffff',
            'overlay_style' => 'dark-soft',
        ],
        'supported_formats' => ['square', 'portrait'],
        'active' => true,
        'official' => true,
        'order_num' => 0,
    ], $attributes));
}

function draftCard(User $user, array $attributes = []): NovaCard
{
    $category = $attributes['category'] ?? draftCategory();
    $template = $attributes['template'] ?? draftTemplate();

    return NovaCard::query()->create(array_merge([
        'user_id' => $user->id,
        'category_id' => $category->id,
        'template_id' => $template->id,
        'title' => 'Draft Card',
        'slug' => 'draft-card-' . Str::lower(Str::random(6)),
        'quote_text' => 'A draft quote for editing.',
        'quote_author' => 'Test Author',
        'quote_source' => 'Notebook',
        'description' => 'Draft description',
        'format' => NovaCard::FORMAT_SQUARE,
        'project_json' => [
            'content' => [
                'title' => 'Draft Card',
                'quote_text' => 'A draft quote for editing.',
                'quote_author' => 'Test Author',
                'quote_source' => 'Notebook',
            ],
            'layout' => [
                'layout' => 'quote_heavy',
                'position' => 'center',
                'alignment' => 'center',
                'padding' => 'comfortable',
                'max_width' => 'balanced',
            ],
            'typography' => [
                'font_preset' => 'modern-sans',
                'text_color' => '#ffffff',
                'accent_color' => '#e0f2fe',
                'quote_size' => 72,
                'author_size' => 28,
                'letter_spacing' => 0,
                'line_height' => 1.2,
                'shadow_preset' => 'soft',
            ],
            'background' => [
                'type' => 'gradient',
                'gradient_preset' => 'midnight-nova',
                'gradient_colors' => ['#0f172a', '#1d4ed8'],
                'overlay_style' => 'dark-soft',
                'focal_position' => 'center',
                'blur_level' => 0,
                'opacity' => 50,
            ],
            'decorations' => [],
        ],
        'render_version' => 1,
        'background_type' => 'gradient',
        'visibility' => NovaCard::VISIBILITY_PRIVATE,
        'status' => NovaCard::STATUS_DRAFT,
        'moderation_status' => NovaCard::MOD_PENDING,
        'featured' => false,
        'allow_download' => true,
    ], Arr::except($attributes, ['category', 'template'])));
}

afterEach(function (): void {
    \Mockery::close();
});

it('creates and fetches a draft through the draft api', function (): void {
    Event::fake([NovaCardCreated::class, NovaCardTemplateSelected::class]);

    $user = User::factory()->create();
    $category = draftCategory();
    $template = draftTemplate();

    $create = $this->actingAs($user)
        ->postJson(route('api.cards.drafts.store'), [
            'title' => 'First Draft',
            'quote_text' => 'The first saved quote.',
            'format' => 'square',
            'category_id' => $category->id,
            'template_id' => $template->id,
            'tags' => ['focus', 'clarity'],
        ]);

    $create->assertCreated()
        ->assertJsonPath('data.title', 'First Draft')
        ->assertJsonPath('data.format', 'square')
        ->assertJsonCount(2, 'data.tags');

    $cardId = (int) $create->json('data.id');

    $this->actingAs($user)
        ->getJson(route('api.cards.drafts.show', ['id' => $cardId]))
        ->assertOk()
        ->assertJsonPath('data.id', $cardId)
        ->assertJsonPath('data.project_json.content.quote_text', 'The first saved quote.');

    Event::assertDispatched(NovaCardCreated::class);
    Event::assertDispatched(NovaCardTemplateSelected::class);
});

it('autosaves project json changes for the draft owner', function (): void {
    Event::fake([NovaCardAutosaved::class]);

    $user = User::factory()->create();
    $card = draftCard($user);

    $response = $this->actingAs($user)
        ->postJson(route('api.cards.drafts.autosave', ['id' => $card->id]), [
            'title' => 'Updated Draft',
            'quote_text' => 'Updated quote text for autosave.',
            'editor_mode_last_used' => 'quick',
            'project_json' => [
                'text_blocks' => [
                    ['key' => 'quote', 'type' => 'quote', 'text' => 'Updated quote text for autosave.', 'enabled' => true],
                    ['key' => 'title', 'type' => 'title', 'text' => 'Updated Draft', 'enabled' => true],
                    ['key' => 'body-1', 'type' => 'body', 'text' => 'Supporting body copy.', 'enabled' => true],
                ],
                'layout' => [
                    'alignment' => 'left',
                    'position' => 'top',
                ],
                'typography' => [
                    'quote_size' => 88,
                    'text_color' => '#ffffff',
                ],
            ],
        ]);

    $response->assertOk()
        ->assertJsonPath('data.title', 'Updated Draft')
        ->assertJsonPath('data.project_json.layout.alignment', 'left')
        ->assertJsonPath('data.project_json.text_blocks.0.type', 'quote')
        ->assertJsonPath('data.editor_mode_last_used', 'quick')
        ->assertJsonPath('meta.saved_at', fn ($value): bool => is_string($value) && $value !== '');

    expect($card->fresh()->project_json['layout']['alignment'])->toBe('left')
        ->and($card->fresh()->project_json['text_blocks'][0]['type'])->toBe('quote')
        ->and($card->fresh()->editor_mode_last_used)->toBe('quick');
    Event::assertDispatched(NovaCardAutosaved::class);
});

it('returns snapshot payloads in the draft versions api for compare views', function (): void {
    $user = User::factory()->create();
    $card = draftCard($user);

    $this->actingAs($user)
        ->postJson(route('api.cards.drafts.autosave', ['id' => $card->id]), [
            'project_json' => [
                'text_blocks' => [
                    ['key' => 'title', 'type' => 'title', 'text' => 'Versioned title', 'enabled' => true],
                    ['key' => 'quote', 'type' => 'quote', 'text' => 'Versioned quote', 'enabled' => true],
                ],
                'layout' => [
                    'layout' => 'minimal',
                ],
            ],
        ])
        ->assertOk();

    $this->actingAs($user)
        ->getJson(route('api.cards.drafts.versions', ['id' => $card->id]))
        ->assertOk()
        ->assertJsonPath('data.0.snapshot_json.text_blocks.0.type', 'title')
        ->assertJsonPath('data.0.snapshot_json.layout.layout', 'minimal');
});

it('prevents another user from editing a draft they do not own', function (): void {
    $owner = User::factory()->create();
    $other = User::factory()->create();
    $card = draftCard($owner);

    $this->actingAs($other)
        ->patchJson(route('api.cards.drafts.update', ['id' => $card->id]), [
            'title' => 'Illegal update',
        ])
        ->assertNotFound();
});

it('publishes a draft when title and quote are present', function (): void {
    Event::fake([NovaCardPublished::class]);

    $user = User::factory()->create();
    $card = draftCard($user, ['title' => 'Publishable Card', 'quote_text' => 'Ready for publishing.']);

    $renderService = \Mockery::mock(NovaCardRenderService::class);
    $renderService->shouldReceive('render')
        ->once()
        ->andReturn([
            'preview_path' => 'nova-cards/previews/test.webp',
            'preview_width' => 1080,
            'preview_height' => 1080,
        ]);
    app()->instance(NovaCardRenderService::class, $renderService);

    $response = $this->actingAs($user)
        ->postJson(route('api.cards.drafts.publish', ['id' => $card->id]), []);

    $response->assertOk()
        ->assertJsonPath('data.status', NovaCard::STATUS_PUBLISHED)
        ->assertJsonPath('data.moderation_status', NovaCard::MOD_APPROVED);

    expect($card->fresh()->status)->toBe(NovaCard::STATUS_PUBLISHED);
    Event::assertDispatched(NovaCardPublished::class);
});

it('flags risky duplicate publishes for moderation review', function (): void {
    Event::fake([NovaCardPublished::class]);

    $user = User::factory()->create();
    NovaCard::query()->create([
        'user_id' => $user->id,
        'category_id' => draftCategory()->id,
        'template_id' => draftTemplate()->id,
        'title' => 'Repeated Card',
        'slug' => 'repeated-card-existing',
        'quote_text' => 'This quote already exists publicly.',
        'quote_author' => 'Existing Author',
        'quote_source' => 'Existing Source',
        'description' => 'Existing public card',
        'format' => NovaCard::FORMAT_SQUARE,
        'project_json' => ['content' => ['title' => 'Repeated Card', 'quote_text' => 'This quote already exists publicly.']],
        'render_version' => 1,
        'background_type' => 'gradient',
        'visibility' => NovaCard::VISIBILITY_PUBLIC,
        'status' => NovaCard::STATUS_PUBLISHED,
        'moderation_status' => NovaCard::MOD_APPROVED,
        'allow_download' => true,
        'published_at' => now()->subDay(),
    ]);

    $card = draftCard($user, [
        'title' => 'Repeated Card',
        'quote_text' => 'This quote already exists publicly.',
    ]);

    $renderService = \Mockery::mock(NovaCardRenderService::class);
    $renderService->shouldReceive('render')
        ->once()
        ->andReturn([
            'preview_path' => 'nova-cards/previews/risky.webp',
            'preview_width' => 1080,
            'preview_height' => 1080,
        ]);
    app()->instance(NovaCardRenderService::class, $renderService);

    $response = $this->actingAs($user)
        ->postJson(route('api.cards.drafts.publish', ['id' => $card->id]), []);

    $response->assertOk()
        ->assertJsonPath('data.status', NovaCard::STATUS_PUBLISHED)
        ->assertJsonPath('data.moderation_status', NovaCard::MOD_FLAGGED)
        ->assertJsonPath('data.moderation_reasons.0', 'duplicate_content')
        ->assertJsonPath('data.moderation_reason_labels.0', 'Duplicate content');

    expect($card->fresh()->moderation_status)->toBe(NovaCard::MOD_FLAGGED)
        ->and($card->fresh()->project_json['moderation']['reasons'] ?? [])->toContain('duplicate_content');
    Event::assertDispatched(NovaCardPublished::class);
});

it('rejects publish when required fields are missing', function (): void {
    $user = User::factory()->create();
    $card = draftCard($user, ['title' => 'Ok title', 'quote_text' => 'Short quote']);

    $response = $this->actingAs($user)
        ->postJson(route('api.cards.drafts.publish', ['id' => $card->id]), [
            'title' => '',
        ]);

    $response->assertStatus(422);
});

it('uploads a background image and attaches it to the draft', function (): void {
    Event::fake([NovaCardBackgroundUploaded::class]);

    Storage::fake('local');
    Storage::fake('public');

    config()->set('nova_cards.storage.private_disk', 'local');
    config()->set('nova_cards.storage.public_disk', 'public');

    $user = User::factory()->create();
    $card = draftCard($user);

    $response = $this->actingAs($user)
        ->postJson(route('api.cards.drafts.background', ['id' => $card->id]), [
            'background' => UploadedFile::fake()->image('background.png', 1400, 1000),
        ]);

    $response->assertCreated()
        ->assertJsonPath('data.background_type', 'upload')
        ->assertJsonPath('data.project_json.background.type', 'upload')
        ->assertJsonPath('background.width', 1400)
        ->assertJsonPath('background.height', 1000);

    $backgroundId = (int) $response->json('background.id');
    $background = NovaCardBackground::query()->findOrFail($backgroundId);

    Storage::disk('local')->assertExists($background->original_path);
    Storage::disk('public')->assertExists($background->processed_path);

    expect($card->fresh()->background_image_id)->toBe($backgroundId);
    Event::assertDispatched(NovaCardBackgroundUploaded::class);
});

it('rejects invalid background uploads', function (): void {
    $user = User::factory()->create();
    $card = draftCard($user);

    $this->actingAs($user)
        ->postJson(route('api.cards.drafts.background', ['id' => $card->id]), [
            'background' => UploadedFile::fake()->create('background.txt', 8, 'text/plain'),
        ])
        ->assertStatus(422)
        ->assertJsonValidationErrors(['background']);

    $this->actingAs($user)
        ->postJson(route('api.cards.drafts.background', ['id' => $card->id]), [
            'background' => UploadedFile::fake()->image('small.png', 320, 320),
        ])
        ->assertStatus(422)
        ->assertJsonValidationErrors(['background']);
});

it('rejects malformed background uploads without throwing', function (): void {
    $request = new UploadNovaCardBackgroundRequest();

    $tempPath = tempnam(sys_get_temp_dir(), 'nova-card-background-');
    file_put_contents($tempPath, 'broken');

    $invalidUpload = new class($tempPath) extends UploadedFile {
        public function __construct(string $path)
        {
            parent::__construct($path, 'broken.png', 'image/png', \UPLOAD_ERR_NO_FILE, true);
        }

        public function isValid(): bool
        {
            return false;
        }

        public function getRealPath(): string|false
        {
            return '';
        }

        public function getPathname(): string
        {
            return '';
        }
    };

    $validator = validator([
        'background' => $invalidUpload,
    ], $request->rules());

    expect(fn() => $validator->fails())->not->toThrow(ValueError::class);
    expect($validator->fails())->toBeTrue();
    expect($validator->errors()->keys())->toContain('background');

    @unlink($tempPath);
});

it('stores uploaded backgrounds when real path falls back to pathname', function (): void {
    Storage::fake('local');
    Storage::fake('public');

    config()->set('nova_cards.storage.private_disk', 'local');
    config()->set('nova_cards.storage.public_disk', 'public');

    $user = User::factory()->create();

    $tempFile = UploadedFile::fake()->image('background.png', 1400, 1000);
    $pathname = $tempFile->getPathname();

    $upload = new class($pathname) extends UploadedFile {
        public function __construct(string $path)
        {
            parent::__construct($path, 'background.png', 'image/png', \UPLOAD_ERR_OK, true);
        }

        public function getRealPath(): string|false
        {
            return false;
        }
    };

    $background = app(NovaCardBackgroundService::class)->storeUploadedBackground($user, $upload);

    expect($background->width)->toBe(1400)
        ->and($background->height)->toBe(1000)
        ->and($background->sha256)->not->toBeNull();

    Storage::disk('local')->assertExists($background->original_path);
    Storage::disk('public')->assertExists($background->processed_path);
});

it('renders a draft preview through the render endpoint', function (): void {
    $user = User::factory()->create();
    $card = draftCard($user);

    $renderService = \Mockery::mock(NovaCardRenderService::class);
    $renderService->shouldReceive('render')
        ->once()
        ->andReturn([
            'preview_path' => 'cards/previews/' . $user->id . '/rendered.webp',
            'og_path' => 'cards/previews/' . $user->id . '/rendered-og.jpg',
            'width' => 1080,
            'height' => 1080,
        ]);
    app()->instance(NovaCardRenderService::class, $renderService);

    $this->actingAs($user)
        ->postJson(route('api.cards.drafts.render', ['id' => $card->id]))
        ->assertOk()
        ->assertJsonPath('data.id', $card->id)
        ->assertJsonPath('render.preview_path', 'cards/previews/' . $user->id . '/rendered.webp')
        ->assertJsonPath('render.width', 1080)
        ->assertJsonPath('render.height', 1080);
});

it('deletes an unpublished draft through the draft api', function (): void {
    $user = User::factory()->create();
    $card = draftCard($user);

    $this->actingAs($user)
        ->deleteJson(route('api.cards.drafts.destroy', ['id' => $card->id]))
        ->assertOk()
        ->assertJsonPath('ok', true);

    expect(NovaCard::query()->whereKey($card->id)->exists())->toBeFalse();
    expect(NovaCard::withTrashed()->whereKey($card->id)->exists())->toBeTrue();
});

it('does not allow deleting a published public card through the draft api', function (): void {
    $user = User::factory()->create();
    $card = draftCard($user, [
        'status' => NovaCard::STATUS_PUBLISHED,
        'visibility' => NovaCard::VISIBILITY_PUBLIC,
        'moderation_status' => NovaCard::MOD_APPROVED,
        'published_at' => now()->subMinute(),
    ]);

    $this->actingAs($user)
        ->deleteJson(route('api.cards.drafts.destroy', ['id' => $card->id]))
        ->assertStatus(422)
        ->assertJsonPath('message', 'Published cards cannot be deleted from the draft API.');

    expect(NovaCard::query()->whereKey($card->id)->exists())->toBeTrue();
});
