<?php

declare(strict_types=1);

use Builder\Core\Foundation\Concerns\HasBuilderAccess;
use Builder\Core\Foundation\Contracts\BuilderAccessible;
use Builder\Core\Foundation\Definition\Field;
use Builder\Core\Foundation\Definition\FieldBuilder;
use Builder\Core\Foundation\Registry;
use Builder\Core\Foundation\Resources\Resource;
use Builder\Core\Foundation\Revisions\RevisionDiff;
use Builder\Core\Models\Revision;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Foundation\Auth\User as Authenticatable;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Schema;
use Inertia\Testing\AssertableInertia;

// Stub for the optional Builder\Media\Models\Media class (not a real dependency of core).
// Defined here so class_exists('Builder\Media\Models\Media') returns true in tests that
// need to exercise the media-lookup branch of ResourceRevisionIndexController.
if (! class_exists('Builder\Media\Models\Media')) {
    class MediaStubModel extends Model
    {
        protected $table = 'stub_media';

        protected $guarded = [];
    }

    class_alias(MediaStubModel::class, 'Builder\Media\Models\Media');
}

// Stub for the optional Builder\BlockEditor\Blocks\BlockRegistry.
if (! class_exists('Builder\BlockEditor\Blocks\BlockRegistry')) {
    class BlockRegistryStub
    {
        /** @return null */
        public function resolve(string $key): mixed
        {
            return null;
        }
    }

    class_alias(BlockRegistryStub::class, 'Builder\BlockEditor\Blocks\BlockRegistry');
}

// A stub block that can be returned by the registry, simulating a resolved block component.
if (! class_exists('StubBlockComponent')) {
    final class StubBlockComponent
    {
        public static function getLabel(): string
        {
            return 'Stub Block';
        }

        public function fields(): FieldBuilder
        {
            return FieldBuilder::make()->add(
                Field::text('text')->label('Text'),
            );
        }
    }
}

// A registry stub that returns a StubBlockComponent for the 'paragraph' key.
if (! class_exists('BlockRegistryWithBlockStub')) {
    final class BlockRegistryWithBlockStub extends BlockRegistryStub
    {
        public function resolve(string $key): mixed
        {
            if ($key === 'paragraph') {
                return new StubBlockComponent;
            }

            return null;
        }
    }
}

final class RevisableTestModel extends Model
{
    protected $table = 'revisable_items';

    protected $guarded = [];
}

final class RevisableTestResource extends Resource
{
    public static function getSlug(): string
    {
        return 'revisable-items';
    }

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

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

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

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

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

final class PermissionedRevisableTestResource extends Resource
{
    public static function getSlug(): string
    {
        return 'permissioned-revisable-items';
    }

    public static function getLabel(): string
    {
        return 'Permissioned Revisable Items';
    }

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

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

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

    public function getPermission(): ?string
    {
        return 'edit:revisable-items';
    }
}

final class RevisionAdminUser extends Authenticatable implements BuilderAccessible
{
    use HasBuilderAccess;

    protected $table = 'users';

    protected $guarded = [];
}

beforeEach(function (): void {
    Schema::create('users', function ($table): void {
        $table->id();
        $table->string('name')->nullable();
        $table->string('email')->nullable()->unique();
        $table->string('password')->nullable();
        $table->rememberToken();
        $table->timestamps();
    });

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

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

    // Stub media table for the optional Builder\Media\Models\Media integration test
    Schema::create('stub_media', function ($table): void {
        $table->id();
        $table->string('original_filename');
        $table->timestamps();
    });

    config([
        'auth.providers.users.model' => RevisionAdminUser::class,
        'builder.auth.user_model' => RevisionAdminUser::class,
    ]);

    app(Registry::class)->resource(RevisableTestResource::class);
});

it('returns 404 for non-revisable resources', function (): void {
    $admin = RevisionAdminUser::create(['name' => 'Admin', 'email' => 'a@test.com', 'password' => 'x']);

    $item = RevisableTestModel::create(['name' => 'Item']);

    // Register non-revisable resource under a different slug
    $this->actingAs($admin)
        ->get(route('builder.resource.revisions', ['resource' => 'non-existent', 'id' => $item->id]))
        ->assertNotFound();
});

it('renders revisions page for a revisable resource', function (): void {
    $admin = RevisionAdminUser::create(['name' => 'Admin', 'email' => 'a@test.com', 'password' => 'x']);
    $item = RevisableTestModel::create(['name' => 'Item']);

    Revision::create([
        'revisable_type' => RevisableTestModel::class,
        'revisable_id' => $item->id,
        'user_id' => null,
        'revision_data' => ['name' => 'Item'],
    ]);

    $this->actingAs($admin)
        ->get(route('builder.resource.revisions', ['resource' => 'revisable-items', 'id' => $item->id]))
        ->assertOk()
        ->assertInertia(fn (AssertableInertia $page): AssertableInertia => $page
            ->component('Resource/Revisions')
            ->has('revisions', 1)
        );
});

it('renders revisions page with empty list when no revisions', function (): void {
    $admin = RevisionAdminUser::create(['name' => 'Admin', 'email' => 'a@test.com', 'password' => 'x']);
    $item = RevisableTestModel::create(['name' => 'Item']);

    $this->actingAs($admin)
        ->get(route('builder.resource.revisions', ['resource' => 'revisable-items', 'id' => $item->id]))
        ->assertOk()
        ->assertInertia(fn (AssertableInertia $page): AssertableInertia => $page
            ->component('Resource/Revisions')
            ->has('revisions', 0)
        );
});

it('restores a revision', function (): void {
    $admin = RevisionAdminUser::create(['name' => 'Admin', 'email' => 'a@test.com', 'password' => 'x']);
    $item = RevisableTestModel::create(['name' => 'Updated Name']);

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

    $this->actingAs($admin)
        ->post(route('builder.resource.revisions.restore', [
            'resource' => 'revisable-items',
            'id' => $item->id,
            'revision' => $revision->id,
        ]))
        ->assertRedirect(route('builder.resource.edit', ['resource' => 'revisable-items', 'id' => $item->id]));

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

it('restore captures a new revision after restoring', function (): void {
    $admin = RevisionAdminUser::create(['name' => 'Admin', 'email' => 'a@test.com', 'password' => 'x']);
    $item = RevisableTestModel::create(['name' => 'Updated Name']);

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

    $this->actingAs($admin)
        ->post(route('builder.resource.revisions.restore', [
            'resource' => 'revisable-items',
            'id' => $item->id,
            'revision' => $revision->id,
        ]));

    $latestRevision = Revision::query()
        ->where('revisable_type', RevisableTestModel::class)
        ->where('revisable_id', $item->id)
        ->orderByDesc('id')
        ->first();

    expect($latestRevision?->revision_data[RevisionDiff::KEY_RESTORED_FROM])->toBe($revision->id);
});

it('restore returns 404 for non-revisable resources', function (): void {
    $admin = RevisionAdminUser::create(['name' => 'Admin', 'email' => 'a@test.com', 'password' => 'x']);
    $item = RevisableTestModel::create(['name' => 'Item']);

    $revision = Revision::create([
        'revisable_type' => RevisableTestModel::class,
        'revisable_id' => $item->id,
        'user_id' => null,
        'revision_data' => ['name' => 'Item'],
    ]);

    $this->actingAs($admin)
        ->post(route('builder.resource.revisions.restore', [
            'resource' => 'non-existent',
            'id' => $item->id,
            'revision' => $revision->id,
        ]))
        ->assertNotFound();
});

it('restore returns 404 for cross-record revision', function (): void {
    $admin = RevisionAdminUser::create(['name' => 'Admin', 'email' => 'a@test.com', 'password' => 'x']);
    $item1 = RevisableTestModel::create(['name' => 'Item 1']);
    $item2 = RevisableTestModel::create(['name' => 'Item 2']);

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

    $this->actingAs($admin)
        ->post(route('builder.resource.revisions.restore', [
            'resource' => 'revisable-items',
            'id' => $item2->id,
            'revision' => $revision->id,
        ]))
        ->assertNotFound();
});

it('returns 403 when user lacks required permission', function (): void {
    app(Registry::class)->resource(PermissionedRevisableTestResource::class);

    $admin = RevisionAdminUser::create(['name' => 'Admin', 'email' => 'b@test.com', 'password' => 'x']);
    $item = RevisableTestModel::create(['name' => 'Item']);

    $this->actingAs($admin)
        ->get(route('builder.resource.revisions', ['resource' => 'permissioned-revisable-items', 'id' => $item->id]))
        ->assertForbidden();
});

it('restore returns 403 when user lacks required permission', function (): void {
    app(Registry::class)->resource(PermissionedRevisableTestResource::class);

    $admin = RevisionAdminUser::create(['name' => 'Admin', 'email' => 'b@test.com', 'password' => 'x']);
    $item = RevisableTestModel::create(['name' => 'Item']);

    $revision = Revision::create([
        'revisable_type' => RevisableTestModel::class,
        'revisable_id' => $item->id,
        'user_id' => null,
        'revision_data' => ['name' => 'Item'],
    ]);

    $this->actingAs($admin)
        ->post(route('builder.resource.revisions.restore', [
            'resource' => 'permissioned-revisable-items',
            'id' => $item->id,
            'revision' => $revision->id,
        ]))
        ->assertForbidden();
});

it('builds diff from previous revision when multiple revisions exist', function (): void {
    $admin = RevisionAdminUser::create(['name' => 'Admin', 'email' => 'c@test.com', 'password' => 'x']);
    $item = RevisableTestModel::create(['name' => 'Current']);

    Revision::create([
        'revisable_type' => RevisableTestModel::class,
        'revisable_id' => $item->id,
        'user_id' => null,
        'revision_data' => ['name' => 'Current'],
        'created_at' => now(),
        'updated_at' => now(),
    ]);

    Revision::create([
        'revisable_type' => RevisableTestModel::class,
        'revisable_id' => $item->id,
        'user_id' => null,
        'revision_data' => ['name' => 'Original'],
        'created_at' => now()->subMinute(),
        'updated_at' => now()->subMinute(),
    ]);

    $this->actingAs($admin)
        ->get(route('builder.resource.revisions', ['resource' => 'revisable-items', 'id' => $item->id]))
        ->assertOk()
        ->assertInertia(fn (AssertableInertia $page): AssertableInertia => $page
            ->component('Resource/Revisions')
            ->has('revisions', 2)
            ->where('revisions.0.diff.name.old', 'Original')
            ->where('revisions.0.diff.name.new', 'Current')
        );
});

it('includes user name in revision entry when user is set', function (): void {
    $admin = RevisionAdminUser::create(['name' => 'Jeffrey', 'email' => 'a@test.com', 'password' => 'x']);
    $item = RevisableTestModel::create(['name' => 'Item']);

    Revision::create([
        'revisable_type' => RevisableTestModel::class,
        'revisable_id' => $item->id,
        'user_id' => $admin->id,
        'revision_data' => ['name' => 'Item'],
    ]);

    $this->actingAs($admin)
        ->get(route('builder.resource.revisions', ['resource' => 'revisable-items', 'id' => $item->id]))
        ->assertOk()
        ->assertInertia(fn (AssertableInertia $page): AssertableInertia => $page
            ->component('Resource/Revisions')
            ->where('revisions.0.user_name', 'Jeffrey')
        );
});

it('scans _media, _blocks and _translations._blocks for media IDs without error', function (): void {
    $admin = RevisionAdminUser::create(['name' => 'Admin', 'email' => 'c@test.com', 'password' => 'x']);
    $item = RevisableTestModel::create(['name' => 'Item']);

    Revision::create([
        'revisable_type' => RevisableTestModel::class,
        'revisable_id' => $item->id,
        'user_id' => null,
        'revision_data' => [
            'name' => 'Item',
            '_media' => ['featured' => [1, 2]],
            '_blocks' => [
                'content' => [
                    ['key' => 'image', 'data' => [], 'media' => ['image' => [3, 4]], 'order' => 0],
                ],
            ],
            '_translations' => [
                'en' => [
                    '_blocks' => [
                        'body' => [
                            ['key' => 'hero', 'data' => [], 'media' => ['bg' => [5]], 'order' => 0],
                        ],
                    ],
                ],
            ],
        ],
    ]);

    $this->actingAs($admin)
        ->get(route('builder.resource.revisions', ['resource' => 'revisable-items', 'id' => $item->id]))
        ->assertOk()
        ->assertInertia(fn (AssertableInertia $page): AssertableInertia => $page
            ->component('Resource/Revisions')
            ->has('revisions', 1)
        );
});

it('includes field_labels, media_names, and granular block diff keys in revision entries', function (): void {
    $admin = RevisionAdminUser::create(['name' => 'Admin', 'email' => 'b@test.com', 'password' => 'x']);
    $item = RevisableTestModel::create(['name' => 'Item']);

    Revision::create([
        'revisable_type' => RevisableTestModel::class,
        'revisable_id' => $item->id,
        'user_id' => null,
        'revision_data' => ['name' => 'Item'],
    ]);

    $this->actingAs($admin)
        ->get(route('builder.resource.revisions', ['resource' => 'revisable-items', 'id' => $item->id]))
        ->assertOk()
        ->assertInertia(fn (AssertableInertia $page): AssertableInertia => $page
            ->component('Resource/Revisions')
            ->has('revisions', 1)
            ->has('revisions.0.field_labels')
            ->has('revisions.0.media_names')
            ->has('revisions.0.block_granular_diff')
            ->has('revisions.0.translation_block_granular_diff')
            ->missing('revisions.0.block_diff')
            ->missing('revisions.0.translation_block_diff')
        );
});

it('resolves media names when Builder Media package is present and revision has media IDs', function (): void {
    $admin = RevisionAdminUser::create(['name' => 'Admin', 'email' => 'media-pkg@test.com', 'password' => 'x']);
    $item = RevisableTestModel::create(['name' => 'Item']);

    // Seed a record in the stub media table so the pluck returns a result
    DB::table('stub_media')->insert([
        'id' => 42,
        'original_filename' => 'photo.jpg',
        'created_at' => now(),
        'updated_at' => now(),
    ]);

    Revision::create([
        'revisable_type' => RevisableTestModel::class,
        'revisable_id' => $item->id,
        'user_id' => null,
        'revision_data' => [
            'name' => 'Item',
            '_media' => ['featured' => [42]],
        ],
    ]);

    $this->actingAs($admin)
        ->get(route('builder.resource.revisions', ['resource' => 'revisable-items', 'id' => $item->id]))
        ->assertOk()
        ->assertInertia(fn (AssertableInertia $page): AssertableInertia => $page
            ->component('Resource/Revisions')
            ->where('revisions.0.media_names.42', 'photo.jpg')
        );
});

it('resolves block labels when Builder BlockEditor package is present and revision has block keys', function (): void {
    $admin = RevisionAdminUser::create(['name' => 'Admin', 'email' => 'block-pkg@test.com', 'password' => 'x']);
    $item = RevisableTestModel::create(['name' => 'Item']);

    // Bind the stub BlockRegistry that returns null for all keys (tests the loop runs)
    app()->bind('Builder\BlockEditor\Blocks\BlockRegistry', fn () => new BlockRegistryStub);

    Revision::create([
        'revisable_type' => RevisableTestModel::class,
        'revisable_id' => $item->id,
        'user_id' => null,
        'revision_data' => [
            'name' => 'Item',
            '_blocks' => [
                'content' => [
                    ['key' => 'paragraph', 'data' => ['text' => 'Hello'], 'media' => [], 'order' => 0],
                ],
            ],
        ],
    ]);

    $this->actingAs($admin)
        ->get(route('builder.resource.revisions', ['resource' => 'revisable-items', 'id' => $item->id]))
        ->assertOk()
        ->assertInertia(fn (AssertableInertia $page): AssertableInertia => $page
            ->component('Resource/Revisions')
            ->has('revisions', 1)
        );
});

it('resolves non-null block and extracts block labels when block is found in registry', function (): void {
    $admin = RevisionAdminUser::create(['name' => 'Admin', 'email' => 'block-found@test.com', 'password' => 'x']);
    $item = RevisableTestModel::create(['name' => 'Item']);

    // Bind a registry that returns a real stub block for 'paragraph' (covers lines 134-138)
    app()->bind('Builder\BlockEditor\Blocks\BlockRegistry', fn () => new BlockRegistryWithBlockStub);

    Revision::create([
        'revisable_type' => RevisableTestModel::class,
        'revisable_id' => $item->id,
        'user_id' => null,
        'revision_data' => [
            'name' => 'Item',
            '_blocks' => [
                'content' => [
                    ['key' => 'paragraph', 'data' => ['text' => 'Hello'], 'media' => [], 'order' => 0],
                ],
            ],
        ],
    ]);

    $this->actingAs($admin)
        ->get(route('builder.resource.revisions', ['resource' => 'revisable-items', 'id' => $item->id]))
        ->assertOk()
        ->assertInertia(fn (AssertableInertia $page): AssertableInertia => $page
            ->component('Resource/Revisions')
            ->where('revisions.0.block_labels.paragraph', 'Stub Block')
        );
});
