<?php

declare(strict_types=1);

use Builder\Core\Foundation\Contracts\Modules\BlockFieldHandler;
use Builder\Core\Foundation\Contracts\Modules\FieldHandler;
use Builder\Core\Foundation\Definition\Field;
use Builder\Core\Foundation\Definition\FieldBuilder;
use Builder\Core\Foundation\Resources\FieldHandlerRegistry;
use Builder\Core\Foundation\Resources\Resource;
use Builder\Core\Foundation\Resources\ResourceDefinition;
use Builder\Core\Foundation\Revisions\RevisionDiff;
use Builder\Core\Foundation\Revisions\RevisionService;
use Builder\Core\Models\Revision;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;

final class RevisionServiceTestModel extends Model
{
    protected $table = 'revision_service_items';

    protected $guarded = [];
}

final class RevisionServiceTestResource extends Resource
{
    public static function getSlug(): string
    {
        return 'revision-service-items';
    }

    public static function getLabel(): string
    {
        return 'Revision Service Items';
    }

    /** @return class-string<Model> */
    public static function getModel(): string
    {
        return RevisionServiceTestModel::class;
    }

    /** @return array<string, mixed> */
    public function rules(string $action): array
    {
        return [];
    }

    public function isRevisable(): bool
    {
        return true;
    }
}

beforeEach(function (): void {
    Schema::create('revision_service_items', function (Blueprint $table): void {
        $table->id();
        $table->string('name');
        $table->timestamps();
    });

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

it('capture creates a revision record', function (): void {
    $service = app(RevisionService::class);
    $definition = ResourceDefinition::make(new RevisionServiceTestResource);

    $record = RevisionServiceTestModel::create(['name' => 'Hello']);

    $revision = $service->capture($record, $definition, null);

    expect(Revision::count())->toBe(1)
        ->and($revision->revision_data['name'])->toBe('Hello')
        ->and($revision->revisable_type)->toBe(RevisionServiceTestModel::class)
        ->and($revision->revisable_id)->toBe($record->id);
});

it('capture stores _restored_from when isRestore is true', function (): void {
    $service = app(RevisionService::class);
    $definition = ResourceDefinition::make(new RevisionServiceTestResource);

    $record = RevisionServiceTestModel::create(['name' => 'Hello']);

    $revision = $service->capture($record, $definition, null, isRestore: true, restoredFrom: 42);

    expect($revision->revision_data['_restored_from'])->toBe(42);
});

it('capture prunes old revisions beyond keep limit', function (): void {
    config(['builder.revisions.keep' => 5]);

    $service = app(RevisionService::class);
    $definition = ResourceDefinition::make(new RevisionServiceTestResource);

    $record = RevisionServiceTestModel::create(['name' => 'Hello']);

    for ($i = 0; $i < 7; $i++) {
        $service->capture($record, $definition, null);
    }

    expect(Revision::where('revisable_type', RevisionServiceTestModel::class)
        ->where('revisable_id', $record->id)
        ->count()
    )->toBe(5);
});

it('restore fills record from revision data', function (): void {
    $service = app(RevisionService::class);

    $record = RevisionServiceTestModel::create(['name' => 'Updated']);

    $revision = Revision::create([
        'revisable_type' => RevisionServiceTestModel::class,
        'revisable_id' => $record->id,
        'user_id' => null,
        'revision_data' => ['name' => 'Original'],
    ]);

    $service->restore($revision, $record);

    expect($record->fresh()?->name)->toBe('Original');
});

it('restore strips system keys from revision data', function (): void {
    $service = app(RevisionService::class);

    $record = RevisionServiceTestModel::create(['name' => 'Updated']);

    $revision = Revision::create([
        'revisable_type' => RevisionServiceTestModel::class,
        'revisable_id' => $record->id,
        'user_id' => null,
        'revision_data' => [
            'id' => 999,
            'created_at' => '2020-01-01',
            'updated_at' => '2020-01-01',
            '_restored_from' => 5,
            'name' => 'Restored Name',
        ],
    ]);

    $service->restore($revision, $record);

    expect($record->fresh()?->name)->toBe('Restored Name')
        ->and($record->fresh()?->id)->toBe($record->id);
});

it('computeSnapshot captures current attributes', function (): void {
    $service = app(RevisionService::class);

    $record = RevisionServiceTestModel::create(['name' => 'Test']);

    $snapshot = $service->computeSnapshot($record);

    expect($snapshot['name'])->toBe('Test')
        ->and($snapshot['id'])->toBe($record->id);
});

it('pruneRecord removes revisions beyond keep limit', function (): void {
    $service = app(RevisionService::class);

    $record = RevisionServiceTestModel::create(['name' => 'Test']);

    for ($i = 0; $i < 5; $i++) {
        Revision::create([
            'revisable_type' => RevisionServiceTestModel::class,
            'revisable_id' => $record->id,
            'user_id' => null,
            'revision_data' => ['name' => "Version {$i}"],
        ]);
    }

    $deleted = $service->pruneRecord($record, 3);

    expect($deleted)->toBe(2)
        ->and(Revision::where('revisable_type', RevisionServiceTestModel::class)
            ->where('revisable_id', $record->id)
            ->count()
        )->toBe(3);
});

it('pruneRecord does nothing when keep is 0', function (): void {
    $service = app(RevisionService::class);
    $record = RevisionServiceTestModel::create(['name' => 'Test']);

    Revision::create([
        'revisable_type' => RevisionServiceTestModel::class,
        'revisable_id' => $record->id,
        'user_id' => null,
        'revision_data' => ['name' => 'Test'],
    ]);

    $deleted = $service->pruneRecord($record, 0);

    expect($deleted)->toBe(0)
        ->and(Revision::count())->toBe(1);
});

it('pruneModelClass does nothing when keep is 0', function (): void {
    $service = app(RevisionService::class);
    $record = RevisionServiceTestModel::create(['name' => 'Test']);

    Revision::create([
        'revisable_type' => RevisionServiceTestModel::class,
        'revisable_id' => $record->id,
        'user_id' => null,
        'revision_data' => ['name' => 'Test'],
    ]);

    $deleted = $service->pruneModelClass(RevisionServiceTestModel::class, 0);

    expect($deleted)->toBe(0)
        ->and(Revision::count())->toBe(1);
});

it('pruneModelClass removes revisions across all records', function (): void {
    $service = app(RevisionService::class);

    $record1 = RevisionServiceTestModel::create(['name' => 'Record 1']);
    $record2 = RevisionServiceTestModel::create(['name' => 'Record 2']);

    for ($i = 0; $i < 5; $i++) {
        Revision::create([
            'revisable_type' => RevisionServiceTestModel::class,
            'revisable_id' => $record1->id,
            'user_id' => null,
            'revision_data' => ['name' => "R1 V{$i}"],
        ]);

        Revision::create([
            'revisable_type' => RevisionServiceTestModel::class,
            'revisable_id' => $record2->id,
            'user_id' => null,
            'revision_data' => ['name' => "R2 V{$i}"],
        ]);
    }

    $deleted = $service->pruneModelClass(RevisionServiceTestModel::class, 3);

    expect($deleted)->toBe(4)
        ->and(Revision::count())->toBe(6);
});

// ---------------------------------------------------------------------------
// FieldHandler-related paths: computeSnapshot with definition, syncBlocks,
// syncMedia, captureHandlerFieldsIntoSnapshot, extractMediaIds
// ---------------------------------------------------------------------------

/**
 * Minimal BlockFieldHandler for testing revision service handler paths.
 */
final class RevisionTestBlockHandler implements BlockFieldHandler
{
    /** @var array<string, mixed> */
    public array $syncedFields = [];

    /** @param array<string, array<string, mixed>> $fields */
    public function loadFields(Model $record, array $fields, ?string $locale = null): void
    {
        foreach (array_keys($fields) as $fieldName) {
            $record->setAttribute($fieldName, []);
        }
    }

    /**
     * @param  array<string, array<string, mixed>>  $fields
     * @param  array<string, mixed>  $requestData
     */
    public function syncFields(Model $record, array $fields, array $requestData, ?string $locale = null): void
    {
        foreach (array_keys($fields) as $fieldName) {
            $this->syncedFields[$fieldName] = $requestData[$fieldName] ?? [];
        }
    }

    /**
     * @param  array<string, array<string, mixed>>  $blockFields
     * @param  array<string, mixed>  $requestData
     * @return array<string, list<string>>
     */
    public function rulesFor(array $blockFields, array $requestData): array
    {
        return [];
    }

    /**
     * @param  array<string, array<string, mixed>>  $blockFields
     * @param  array<string, mixed>  $requestData
     * @return array<string, string>
     */
    public function attributesFor(array $blockFields, array $requestData): array
    {
        return [];
    }
}

interface RevisionTestBlockHandlerInterface extends BlockFieldHandler {}

/**
 * Minimal FieldHandler (non-block) for testing media snapshot paths.
 */
final class RevisionTestMediaHandler implements FieldHandler
{
    /** @param array<string, array<string, mixed>> $fields */
    public function loadFields(Model $record, array $fields, ?string $locale = null): void
    {
        foreach (array_keys($fields) as $fieldName) {
            $record->setAttribute($fieldName, [1, 2, 3]);
        }
    }

    /**
     * @param  array<string, array<string, mixed>>  $fields
     * @param  array<string, mixed>  $requestData
     */
    public function syncFields(Model $record, array $fields, array $requestData, ?string $locale = null): void {}
}

interface RevisionTestMediaHandlerInterface extends FieldHandler {}

final class RevisionServiceHandlerResource extends Resource
{
    public static function getSlug(): string
    {
        return 'revision-handler-items';
    }

    public static function getLabel(): string
    {
        return 'Revision Handler Items';
    }

    /** @return class-string<Model> */
    public static function getModel(): string
    {
        return RevisionServiceTestModel::class;
    }

    /** @return array<string, mixed> */
    public function rules(string $action): array
    {
        return [];
    }

    public function isRevisable(): bool
    {
        return true;
    }

    public function fields(): FieldBuilder
    {
        return FieldBuilder::make()->add(
            Field::text('name'),
            Field::text('blocks_field')->component('RevisionBlockComponent'),
        );
    }
}

final class RevisionServiceMediaHandlerResource extends Resource
{
    public static function getSlug(): string
    {
        return 'revision-media-items';
    }

    public static function getLabel(): string
    {
        return 'Revision Media Items';
    }

    /** @return class-string<Model> */
    public static function getModel(): string
    {
        return RevisionServiceTestModel::class;
    }

    /** @return array<string, mixed> */
    public function rules(string $action): array
    {
        return [];
    }

    public function fields(): FieldBuilder
    {
        return FieldBuilder::make()->add(
            Field::text('name'),
            Field::text('media_field')->component('RevisionMediaComponent'),
        );
    }
}

it('computeSnapshot with definition calls captureHandlerFieldsIntoSnapshot for registered block handler', function (): void {
    $blockHandler = new RevisionTestBlockHandler;
    app()->instance(RevisionTestBlockHandlerInterface::class, $blockHandler);
    app(FieldHandlerRegistry::class)->register('RevisionBlockComponent', RevisionTestBlockHandlerInterface::class);

    $service = app(RevisionService::class);
    $definition = ResourceDefinition::make(new RevisionServiceHandlerResource);

    $record = RevisionServiceTestModel::create(['name' => 'Block Handler Test']);

    $snapshot = $service->computeSnapshot($record, $definition);

    // captureHandlerFieldsIntoSnapshot was called — snapshot should be an array with the base attributes
    expect($snapshot)->toBeArray()
        ->and($snapshot['name'])->toBe('Block Handler Test');
});

it('restore with _blocks calls syncBlocks via registered block handler', function (): void {
    $blockHandler = new RevisionTestBlockHandler;
    app()->instance(RevisionTestBlockHandlerInterface::class, $blockHandler);
    app(FieldHandlerRegistry::class)->register('RevisionBlockComponent', RevisionTestBlockHandlerInterface::class);

    $service = app(RevisionService::class);
    $record = RevisionServiceTestModel::create(['name' => 'Blocks Restore Test']);

    $revision = Revision::create([
        'revisable_type' => RevisionServiceTestModel::class,
        'revisable_id' => $record->id,
        'user_id' => null,
        'revision_data' => [
            'name' => 'Restored Name',
            '_blocks' => [
                'blocks_field' => [
                    ['key' => 'paragraph', 'data' => ['text' => 'Hello'], 'media' => [], 'order' => 0],
                ],
            ],
        ],
    ]);

    $service->restore($revision, $record);

    expect($record->fresh()?->name)->toBe('Restored Name');

    // syncFields was called on the block handler with the block data
    expect($blockHandler->syncedFields)->toHaveKey('blocks_field');
});

it('restore with _media calls syncMedia when model has the method', function (): void {
    $service = app(RevisionService::class);

    // Create a model with a syncMedia method
    $model = new class extends Model
    {
        protected $table = 'revision_service_items';

        protected $guarded = [];

        /** @var array<string, list<int>> */
        public array $syncedMedia = [];

        /** @param list<int> $ids */
        public function syncMedia(array $ids, string $collection): void
        {
            $this->syncedMedia[$collection] = $ids;
        }
    };

    $record = $model::create(['name' => 'Media Restore Test']);

    $revision = Revision::create([
        'revisable_type' => get_class($record),
        'revisable_id' => $record->id,
        'user_id' => null,
        'revision_data' => [
            'name' => 'Restored With Media',
            '_media' => [
                'images' => [1, 2, 3],
            ],
        ],
    ]);

    $service->restore($revision, $record);

    expect($record->fresh()?->name)->toBe('Restored With Media')
        ->and($record->syncedMedia)->toHaveKey('images')
        ->and($record->syncedMedia['images'])->toBe([1, 2, 3]);
});

it('computeSnapshot with media handler definition calls extractMediaIds', function (): void {
    $mediaHandler = new RevisionTestMediaHandler;
    app()->instance(RevisionTestMediaHandlerInterface::class, $mediaHandler);
    app(FieldHandlerRegistry::class)->register('RevisionMediaComponent', RevisionTestMediaHandlerInterface::class);

    $service = app(RevisionService::class);
    $definition = ResourceDefinition::make(new RevisionServiceMediaHandlerResource);

    $record = RevisionServiceTestModel::create(['name' => 'Media Handler Test']);

    $snapshot = $service->computeSnapshot($record, $definition);

    // captureHandlerFieldsIntoSnapshot was called and extracted media IDs
    expect($snapshot)->toBeArray()
        ->and($snapshot['name'])->toBe('Media Handler Test');
});

it('captureHandlerFieldsIntoSnapshot skips handler when resolve returns null', function (): void {
    // Register a component name for an interface that is NOT bound in the container.
    // This causes resolve() to return null, hitting the `continue` on line 170.
    app(FieldHandlerRegistry::class)->register('UnboundComponent', RevisionTestBlockHandlerInterface::class);

    $service = app(RevisionService::class);
    $definition = ResourceDefinition::make(new RevisionServiceHandlerResource);

    $record = RevisionServiceTestModel::create(['name' => 'Null Handler Test']);

    // Should not throw — the null handler is skipped gracefully
    $snapshot = $service->computeSnapshot($record, $definition);

    expect($snapshot)->toBeArray()
        ->and($snapshot['name'])->toBe('Null Handler Test');
});

it('captureHandlerFieldsIntoSnapshot skips handler when no matching fields in definition', function (): void {
    // Register a handler for a component that no field in the resource uses.
    // This causes getFieldDefinitionsByComponent to return [] and hits the `continue` on line 176.
    $blockHandler = new RevisionTestBlockHandler;

    interface UnmatchedHandlerInterface extends BlockFieldHandler {}
    app()->instance(UnmatchedHandlerInterface::class, $blockHandler);
    app(FieldHandlerRegistry::class)->register('UnmatchedComponent', UnmatchedHandlerInterface::class);

    $service = app(RevisionService::class);
    // RevisionServiceTestResource has no fields with component 'UnmatchedComponent'
    $definition = ResourceDefinition::make(new RevisionServiceTestResource);

    $record = RevisionServiceTestModel::create(['name' => 'Unmatched Component Test']);

    $snapshot = $service->computeSnapshot($record, $definition);

    expect($snapshot)->toBeArray()
        ->and($snapshot['name'])->toBe('Unmatched Component Test');
});

it('captureHandlerFieldsIntoSnapshot captures non-empty block data for non-translatable model', function (): void {
    // Use a block handler that sets NON-EMPTY block data so the array_map path is hit.
    $blockHandlerWithData = new class implements BlockFieldHandler
    {
        /** @param array<string, array<string, mixed>> $fields */
        public function loadFields(Model $record, array $fields, ?string $locale = null): void
        {
            foreach (array_keys($fields) as $fieldName) {
                $record->setAttribute($fieldName, [
                    ['key' => 'paragraph', 'data' => ['text' => 'Hello'], 'media' => []],
                ]);
            }
        }

        /**
         * @param  array<string, array<string, mixed>>  $fields
         * @param  array<string, mixed>  $requestData
         */
        public function syncFields(Model $record, array $fields, array $requestData, ?string $locale = null): void {}

        /**
         * @param  array<string, array<string, mixed>>  $blockFields
         * @param  array<string, mixed>  $requestData
         * @return array<string, list<string>>
         */
        public function rulesFor(array $blockFields, array $requestData): array
        {
            return [];
        }

        /**
         * @param  array<string, array<string, mixed>>  $blockFields
         * @param  array<string, mixed>  $requestData
         * @return array<string, string>
         */
        public function attributesFor(array $blockFields, array $requestData): array
        {
            return [];
        }
    };

    interface NonEmptyBlockHandlerInterface extends BlockFieldHandler {}
    app()->instance(NonEmptyBlockHandlerInterface::class, $blockHandlerWithData);
    app(FieldHandlerRegistry::class)->register('NonEmptyBlockComponent', NonEmptyBlockHandlerInterface::class);

    // Define a resource with a field using this component
    $resource = new class extends Resource
    {
        public static function getSlug(): string
        {
            return 'nonempty-block-items';
        }

        public static function getLabel(): string
        {
            return 'Non-Empty Block Items';
        }

        /** @return class-string<Model> */
        public static function getModel(): string
        {
            return RevisionServiceTestModel::class;
        }

        /** @return array<string, mixed> */
        public function rules(string $action): array
        {
            return [];
        }

        public function fields(): FieldBuilder
        {
            return FieldBuilder::make()->add(
                Field::text('name'),
                Field::text('blocks_field')->component('NonEmptyBlockComponent'),
            );
        }
    };

    $service = app(RevisionService::class);
    $definition = ResourceDefinition::make($resource);

    $record = RevisionServiceTestModel::create(['name' => 'Non-Empty Blocks Test']);

    $snapshot = $service->computeSnapshot($record, $definition);

    // The block data was set via loadFields and should be captured
    expect($snapshot)->toBeArray()
        ->and($snapshot)->toHaveKey(RevisionDiff::KEY_BLOCKS);
});

it('syncBlocks continues when registered handler is not a BlockFieldHandler', function (): void {
    // Register a plain FieldHandler (not BlockFieldHandler) — syncBlocks should skip it (line 273)
    $plainHandler = new RevisionTestMediaHandler;
    app()->instance(RevisionTestMediaHandlerInterface::class, $plainHandler);
    app(FieldHandlerRegistry::class)->register('SyncPlainComponent', RevisionTestMediaHandlerInterface::class);

    $service = app(RevisionService::class);
    $record = RevisionServiceTestModel::create(['name' => 'SyncBlocks Skip Test']);

    $revision = Revision::create([
        'revisable_type' => RevisionServiceTestModel::class,
        'revisable_id' => $record->id,
        'user_id' => null,
        'revision_data' => [
            'name' => 'Restored Name',
            '_blocks' => [
                'some_field' => [
                    ['key' => 'para', 'data' => [], 'media' => [], 'order' => 0],
                ],
            ],
        ],
    ]);

    // Should not throw — plain handler is skipped in syncBlocks
    $service->restore($revision, $record);

    expect($record->fresh()?->name)->toBe('Restored Name');
});
