<?php

declare(strict_types=1);

use Builder\Core\Models\Revision;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Relations\MorphTo;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;

beforeEach(function (): void {
    Schema::create('revisions', function (Blueprint $table): void {
        $table->id();
        $table->string('revisable_type');
        $table->unsignedBigInteger('revisable_id');
        $table->unsignedBigInteger('user_id')->nullable();
        $table->json('revision_data');
        $table->timestamps();
    });

    config(['builder.auth.user_model' => Model::class]);
});

it('revisable() returns a MorphTo relation', function (): void {
    $revision = new Revision;

    expect($revision->revisable())->toBeInstanceOf(MorphTo::class);
});

it('user() returns a BelongsTo relation', function (): void {
    config(['builder.auth.user_model' => Revision::class]);

    $revision = new Revision;

    expect($revision->user())->toBeInstanceOf(BelongsTo::class);
});

it('diffFrom returns empty array when nothing changed', function (): void {
    $revision = new Revision;
    $revision->revision_data = ['title' => 'Hello', 'status' => 'draft'];

    expect($revision->diffFrom(['title' => 'Hello', 'status' => 'draft']))->toBe([]);
});

it('diffFrom returns changed fields', function (): void {
    $revision = new Revision;
    $revision->revision_data = ['title' => 'Hello World', 'status' => 'published'];

    $diff = $revision->diffFrom(['title' => 'Hello', 'status' => 'draft']);

    expect($diff)->toBe([
        'title' => ['old' => 'Hello', 'new' => 'Hello World'],
        'status' => ['old' => 'draft', 'new' => 'published'],
    ]);
});

it('diffFrom skips system keys', function (): void {
    $revision = new Revision;
    $revision->revision_data = [
        'id' => 2,
        'created_at' => '2026-01-02',
        'updated_at' => '2026-01-02',
        '_restored_from' => 5,
        'title' => 'Same',
    ];

    $diff = $revision->diffFrom([
        'id' => 1,
        'created_at' => '2026-01-01',
        'updated_at' => '2026-01-01',
        '_restored_from' => 3,
        'title' => 'Same',
    ]);

    expect($diff)->toBe([]);
});

it('diffFrom reports removed fields as null new', function (): void {
    $revision = new Revision;
    $revision->revision_data = ['status' => 'draft'];

    $diff = $revision->diffFrom(['status' => 'draft', 'slug' => 'old-slug']);

    expect($diff)->toBe([
        'slug' => ['old' => 'old-slug', 'new' => null],
    ]);
});

it('diffFrom ignores translations key', function (): void {
    $revision = new Revision;
    $revision->revision_data = [
        'title' => 'Same',
        '_translations' => ['nl' => ['title' => 'Hallo']],
    ];

    $diff = $revision->diffFrom([
        'title' => 'Same',
        '_translations' => ['nl' => ['title' => 'Old']],
    ]);

    expect($diff)->toBe([]);
});

it('translationDiffFrom returns empty when no translations changed', function (): void {
    $revision = new Revision;
    $revision->revision_data = [
        '_translations' => ['nl' => ['title' => 'Hallo']],
    ];

    expect($revision->translationDiffFrom([
        '_translations' => ['nl' => ['title' => 'Hallo']],
    ]))->toBe([]);
});

it('translationDiffFrom returns per-locale diff', function (): void {
    $revision = new Revision;
    $revision->revision_data = [
        '_translations' => [
            'nl' => ['title' => 'Hallo Wereld', 'body' => 'Tekst'],
            'en' => ['title' => 'Hello World'],
        ],
    ];

    $diff = $revision->translationDiffFrom([
        '_translations' => [
            'nl' => ['title' => 'Hallo', 'body' => 'Tekst'],
            'en' => ['title' => 'Hello World'],
        ],
    ]);

    expect($diff)->toBe([
        'nl' => ['title' => ['old' => 'Hallo', 'new' => 'Hallo Wereld']],
    ]);
});

it('translationDiffFrom handles missing previous translations', function (): void {
    $revision = new Revision;
    $revision->revision_data = [
        '_translations' => ['nl' => ['title' => 'Hallo']],
    ];

    $diff = $revision->translationDiffFrom([]);

    expect($diff)->toBe([
        'nl' => ['title' => ['old' => null, 'new' => 'Hallo']],
    ]);
});

it('translationDiffFrom returns empty when no translations present', function (): void {
    $revision = new Revision;
    $revision->revision_data = ['title' => 'Hello'];

    expect($revision->translationDiffFrom([]))->toBe([]);
});

it('translationDiffFrom skips _blocks key in translation fields', function (): void {
    $revision = new Revision;
    $revision->revision_data = [
        '_translations' => [
            'nl' => [
                'title' => 'Hallo',
                '_blocks' => ['content' => [['key' => 'hero', 'data' => [], 'order' => 0]]],
            ],
        ],
    ];

    $diff = $revision->translationDiffFrom([
        '_translations' => [
            'nl' => [
                'title' => 'Hallo',
                '_blocks' => ['content' => [['key' => 'text', 'data' => [], 'order' => 0]]],
            ],
        ],
    ]);

    expect($diff)->toBe([]);
});

it('diffFrom skips _blocks key', function (): void {
    $revision = new Revision;
    $revision->revision_data = [
        'title' => 'Same',
        '_blocks' => ['content' => [['key' => 'hero', 'data' => [], 'order' => 0]]],
    ];

    $diff = $revision->diffFrom([
        'title' => 'Same',
        '_blocks' => ['content' => [['key' => 'text', 'data' => [], 'order' => 0]]],
    ]);

    expect($diff)->toBe([]);
});

it('diffFrom skips _media key', function (): void {
    $revision = new Revision;
    $revision->revision_data = [
        'title' => 'Same',
        '_media' => ['featured_image' => [1, 2]],
    ];

    $diff = $revision->diffFrom([
        'title' => 'Same',
        '_media' => ['featured_image' => [3, 4]],
    ]);

    expect($diff)->toBe([]);
});

it('blockDiffFrom returns empty when no blocks present', function (): void {
    $revision = new Revision;
    $revision->revision_data = ['title' => 'Hello'];

    expect($revision->blockDiffFrom([]))->toBe([]);
});

it('blockDiffFrom returns diff for changed collections', function (): void {
    $revision = new Revision;
    $revision->revision_data = [
        '_blocks' => [
            'content' => [
                ['key' => 'hero', 'data' => ['title' => 'New'], 'order' => 0],
                ['key' => 'text', 'data' => [], 'order' => 1],
            ],
        ],
    ];

    $diff = $revision->blockDiffFrom([
        '_blocks' => [
            'content' => [
                ['key' => 'hero', 'data' => ['title' => 'Old'], 'order' => 0],
            ],
        ],
    ]);

    expect($diff)->toBe([
        'content' => [
            'old' => [['key' => 'hero', 'data' => ['title' => 'Old'], 'media' => []]],
            'new' => [['key' => 'hero', 'data' => ['title' => 'New'], 'media' => []], ['key' => 'text', 'data' => [], 'media' => []]],
        ],
    ]);
});

it('blockDiffFrom detects data changes with same block structure', function (): void {
    $revision = new Revision;
    $revision->revision_data = [
        '_blocks' => [
            'content' => [
                ['key' => 'hero', 'data' => ['title' => 'New Title'], 'order' => 0],
            ],
        ],
    ];

    $diff = $revision->blockDiffFrom([
        '_blocks' => [
            'content' => [
                ['key' => 'hero', 'data' => ['title' => 'Old Title'], 'order' => 0],
            ],
        ],
    ]);

    expect($diff)->toBe([
        'content' => [
            'old' => [['key' => 'hero', 'data' => ['title' => 'Old Title'], 'media' => []]],
            'new' => [['key' => 'hero', 'data' => ['title' => 'New Title'], 'media' => []]],
        ],
    ]);
});

it('blockDiffFrom returns empty for removed collection when no previous blocks', function (): void {
    $revision = new Revision;
    $revision->revision_data = [
        '_blocks' => [
            'content' => [],
        ],
    ];

    $diff = $revision->blockDiffFrom([
        '_blocks' => ['content' => []],
    ]);

    expect($diff)->toBe([]);
});

it('blockDiffFrom shows removed collection as empty', function (): void {
    $revision = new Revision;
    $revision->revision_data = ['title' => 'Hello'];

    $diff = $revision->blockDiffFrom([
        '_blocks' => [
            'content' => [['key' => 'hero', 'data' => [], 'order' => 0]],
        ],
    ]);

    expect($diff)->toBe([
        'content' => [
            'old' => [['key' => 'hero', 'data' => [], 'media' => []]],
            'new' => [],
        ],
    ]);
});

it('translationBlockDiffFrom returns empty when no block changes', function (): void {
    $revision = new Revision;
    $revision->revision_data = [
        '_translations' => [
            'nl' => ['_blocks' => ['content' => [['key' => 'hero', 'data' => [], 'order' => 0]]]],
        ],
    ];

    expect($revision->translationBlockDiffFrom([
        '_translations' => [
            'nl' => ['_blocks' => ['content' => [['key' => 'hero', 'data' => [], 'order' => 0]]]],
        ],
    ]))->toBe([]);
});

it('translationBlockDiffFrom returns per-locale block diff', function (): void {
    $revision = new Revision;
    $revision->revision_data = [
        '_translations' => [
            'nl' => [
                '_blocks' => [
                    'content' => [
                        ['key' => 'hero', 'data' => [], 'order' => 0],
                        ['key' => 'text', 'data' => [], 'order' => 1],
                    ],
                ],
            ],
        ],
    ];

    $diff = $revision->translationBlockDiffFrom([
        '_translations' => [
            'nl' => [
                '_blocks' => [
                    'content' => [
                        ['key' => 'hero', 'data' => [], 'order' => 0],
                    ],
                ],
            ],
        ],
    ]);

    expect($diff)->toBe([
        'nl' => [
            'content' => [
                'old' => [['key' => 'hero', 'data' => [], 'media' => []]],
                'new' => [['key' => 'hero', 'data' => [], 'media' => []], ['key' => 'text', 'data' => [], 'media' => []]],
            ],
        ],
    ]);
});

it('translationBlockDiffFrom detects data changes with same block structure', function (): void {
    $revision = new Revision;
    $revision->revision_data = [
        '_translations' => [
            'en' => [
                '_blocks' => [
                    'content' => [
                        ['key' => 'hero', 'data' => ['title' => 'New Title'], 'order' => 0],
                    ],
                ],
            ],
        ],
    ];

    $diff = $revision->translationBlockDiffFrom([
        '_translations' => [
            'en' => [
                '_blocks' => [
                    'content' => [
                        ['key' => 'hero', 'data' => ['title' => 'Old Title'], 'order' => 0],
                    ],
                ],
            ],
        ],
    ]);

    expect($diff)->toBe([
        'en' => [
            'content' => [
                'old' => [['key' => 'hero', 'data' => ['title' => 'Old Title'], 'media' => []]],
                'new' => [['key' => 'hero', 'data' => ['title' => 'New Title'], 'media' => []]],
            ],
        ],
    ]);
});

it('translationBlockDiffFrom returns empty when no translations present', function (): void {
    $revision = new Revision;
    $revision->revision_data = ['title' => 'Hello'];

    expect($revision->translationBlockDiffFrom([]))->toBe([]);
});

it('mediaDiffFrom returns empty when no media present', function (): void {
    $revision = new Revision;
    $revision->revision_data = ['title' => 'Hello'];

    expect($revision->mediaDiffFrom([]))->toBe([]);
});

it('mediaDiffFrom returns diff for changed collections', function (): void {
    $revision = new Revision;
    $revision->revision_data = [
        '_media' => ['featured_image' => [2, 3]],
    ];

    $diff = $revision->mediaDiffFrom([
        '_media' => ['featured_image' => [1]],
    ]);

    expect($diff)->toBe([
        'featured_image' => ['old' => [1], 'new' => [2, 3]],
    ]);
});

it('mediaDiffFrom shows removed media as empty', function (): void {
    $revision = new Revision;
    $revision->revision_data = ['title' => 'Hello'];

    $diff = $revision->mediaDiffFrom([
        '_media' => ['featured_image' => [1, 2]],
    ]);

    expect($diff)->toBe([
        'featured_image' => ['old' => [1, 2], 'new' => []],
    ]);
});

it('mediaDiffFrom returns empty when media is unchanged', function (): void {
    $revision = new Revision;
    $revision->revision_data = [
        '_media' => ['featured_image' => [1, 2]],
    ];

    expect($revision->mediaDiffFrom([
        '_media' => ['featured_image' => [1, 2]],
    ]))->toBe([]);
});

it('blockGranularDiffFrom returns empty when no blocks present', function (): void {
    $revision = new Revision;
    $revision->revision_data = ['title' => 'Hello'];

    expect($revision->blockGranularDiffFrom([]))->toBe([]);
});

it('blockGranularDiffFrom returns empty when blocks are unchanged', function (): void {
    $revision = new Revision;
    $revision->revision_data = [
        '_blocks' => [
            'content' => [
                ['key' => 'heading', 'data' => ['text' => 'Hello'], 'order' => 0],
            ],
        ],
    ];

    expect($revision->blockGranularDiffFrom([
        '_blocks' => [
            'content' => [
                ['key' => 'heading', 'data' => ['text' => 'Hello'], 'order' => 0],
            ],
        ],
    ]))->toBe([]);
});

it('blockGranularDiffFrom returns modified entry with data_diff', function (): void {
    $revision = new Revision;
    $revision->revision_data = [
        '_blocks' => [
            'content' => [
                ['key' => 'heading', 'data' => ['text' => 'New Title'], 'order' => 0],
            ],
        ],
    ];

    $diff = $revision->blockGranularDiffFrom([
        '_blocks' => [
            'content' => [
                ['key' => 'heading', 'data' => ['text' => 'Old Title'], 'order' => 0],
            ],
        ],
    ]);

    expect($diff)->toBe([
        'content' => [
            [
                'action' => 'modified',
                'position' => 0,
                'key' => 'heading',
                'data_diff' => ['text' => ['old' => 'Old Title', 'new' => 'New Title']],
                'media_diff' => [],
            ],
        ],
    ]);
});

it('blockGranularDiffFrom returns added entry for new block at end', function (): void {
    $revision = new Revision;
    $revision->revision_data = [
        '_blocks' => [
            'content' => [
                ['key' => 'heading', 'data' => ['text' => 'Hello'], 'order' => 0],
                ['key' => 'paragraph', 'data' => ['body' => 'New paragraph'], 'order' => 1],
            ],
        ],
    ];

    $diff = $revision->blockGranularDiffFrom([
        '_blocks' => [
            'content' => [
                ['key' => 'heading', 'data' => ['text' => 'Hello'], 'order' => 0],
            ],
        ],
    ]);

    expect($diff)->toBe([
        'content' => [
            [
                'action' => 'added',
                'position' => 1,
                'key' => 'paragraph',
                'data' => ['body' => 'New paragraph'],
                'media' => [],
            ],
        ],
    ]);
});

it('blockGranularDiffFrom returns removed entry when block deleted from end', function (): void {
    $revision = new Revision;
    $revision->revision_data = [
        '_blocks' => [
            'content' => [
                ['key' => 'heading', 'data' => ['text' => 'Hello'], 'order' => 0],
            ],
        ],
    ];

    $diff = $revision->blockGranularDiffFrom([
        '_blocks' => [
            'content' => [
                ['key' => 'heading', 'data' => ['text' => 'Hello'], 'order' => 0],
                ['key' => 'paragraph', 'data' => ['body' => 'Old paragraph'], 'order' => 1],
            ],
        ],
    ]);

    expect($diff)->toBe([
        'content' => [
            [
                'action' => 'removed',
                'position' => 1,
                'key' => 'paragraph',
                'data' => ['body' => 'Old paragraph'],
                'media' => [],
            ],
        ],
    ]);
});

it('blockGranularDiffFrom handles mixed add, remove, modify in one collection', function (): void {
    $revision = new Revision;
    $revision->revision_data = [
        '_blocks' => [
            'content' => [
                ['key' => 'heading', 'data' => ['text' => 'Updated'], 'order' => 0],
                ['key' => 'paragraph', 'data' => ['body' => 'New block'], 'order' => 1],
            ],
        ],
    ];

    $diff = $revision->blockGranularDiffFrom([
        '_blocks' => [
            'content' => [
                ['key' => 'heading', 'data' => ['text' => 'Original'], 'order' => 0],
                ['key' => 'quote', 'data' => ['text' => 'Old quote'], 'order' => 1],
                ['key' => 'divider', 'data' => [], 'order' => 2],
            ],
        ],
    ]);

    expect($diff['content'])->toHaveCount(3)
        ->and($diff['content'][0]['action'])->toBe('modified')
        ->and($diff['content'][0]['data_diff']['text'])->toBe(['old' => 'Original', 'new' => 'Updated'])
        ->and($diff['content'][1]['action'])->toBe('modified')
        ->and($diff['content'][2]['action'])->toBe('removed');
});

it('translationBlockGranularDiffFrom returns per-locale granular diff', function (): void {
    $revision = new Revision;
    $revision->revision_data = [
        '_translations' => [
            'en' => [
                '_blocks' => [
                    'content' => [
                        ['key' => 'heading', 'data' => ['text' => 'New Title'], 'order' => 0],
                    ],
                ],
            ],
        ],
    ];

    $diff = $revision->translationBlockGranularDiffFrom([
        '_translations' => [
            'en' => [
                '_blocks' => [
                    'content' => [
                        ['key' => 'heading', 'data' => ['text' => 'Old Title'], 'order' => 0],
                    ],
                ],
            ],
        ],
    ]);

    expect($diff)->toBe([
        'en' => [
            'content' => [
                [
                    'action' => 'modified',
                    'position' => 0,
                    'key' => 'heading',
                    'data_diff' => ['text' => ['old' => 'Old Title', 'new' => 'New Title']],
                    'media_diff' => [],
                ],
            ],
        ],
    ]);
});

it('translationBlockGranularDiffFrom returns empty when no translations present', function (): void {
    $revision = new Revision;
    $revision->revision_data = ['title' => 'Hello'];

    expect($revision->translationBlockGranularDiffFrom([]))->toBe([]);
});

it('blockGranularDiffFrom returns media_diff for modified block with changed media', function (): void {
    $revision = new Revision;
    $revision->revision_data = [
        '_blocks' => [
            'content' => [
                ['key' => 'image', 'data' => [], 'media' => ['photo' => [2]], 'order' => 0],
            ],
        ],
    ];

    $diff = $revision->blockGranularDiffFrom([
        '_blocks' => [
            'content' => [
                ['key' => 'image', 'data' => [], 'media' => ['photo' => [1]], 'order' => 0],
            ],
        ],
    ]);

    expect($diff)->toBe([
        'content' => [
            [
                'action' => 'modified',
                'position' => 0,
                'key' => 'image',
                'data_diff' => [],
                'media_diff' => ['photo' => ['old' => [1], 'new' => [2]]],
            ],
        ],
    ]);
});

it('translationDiffFrom skips system keys in translation fields', function (): void {
    $revision = new Revision;
    $revision->revision_data = [
        '_translations' => [
            'nl' => [
                'id' => 99,
                'created_at' => '2026-01-01',
                'updated_at' => '2026-01-01',
                'title' => 'Hallo',
            ],
        ],
    ];

    $diff = $revision->translationDiffFrom([
        '_translations' => [
            'nl' => [
                'id' => 1,
                'created_at' => '2025-01-01',
                'updated_at' => '2025-01-01',
                'title' => 'Hallo',
            ],
        ],
    ]);

    expect($diff)->toBe([]);
});
