<?php

declare(strict_types=1);

use Builder\Core\Foundation\Concerns\HasBuilderAccess;
use Builder\Core\Foundation\Concerns\HasCommandPaletteSearch;
use Builder\Core\Foundation\Concerns\HasTranslations;
use Builder\Core\Foundation\Contracts\BuilderAccessible;
use Builder\Core\Foundation\Contracts\CommandPaletteSearchable;
use Builder\Core\Foundation\Contracts\Translatable;
use Builder\Core\Foundation\Registry;
use Builder\Core\Foundation\Resources\Resource;
use Builder\Core\Foundation\Tables\Column;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Foundation\Auth\User as Authenticatable;
use Illuminate\Support\Facades\Schema;

// ---------------------------------------------------------------------------
// Test models
// ---------------------------------------------------------------------------

final class SearchPersonModel extends Model
{
    protected $table = 'search_persons';

    protected $guarded = [];
}

final class SearchRoleModel extends Model
{
    protected $table = 'search_roles';

    protected $guarded = [];
}

// ---------------------------------------------------------------------------
// Resources
// ---------------------------------------------------------------------------

/** Opts in to global search via the contract + trait */
final class SearchPersonResource extends Resource implements CommandPaletteSearchable
{
    use HasCommandPaletteSearch;

    public static function getSlug(): string
    {
        return 'search-persons';
    }

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

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

    public function getPermission(): ?string
    {
        return 'read:search-persons';
    }

    /** @return list<string> */
    public function searchable(): array
    {
        return ['name', 'email'];
    }

    /** @return list<Column> */
    public function columns(): array
    {
        return [
            Column::make('name')->searchable(),
            Column::make('email')->searchable(),
        ];
    }
}

/** Does NOT opt in — must never appear in search results */
final class SearchRoleResource extends Resource
{
    public static function getSlug(): string
    {
        return 'search-roles';
    }

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

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

    /** @return list<string> */
    public function searchable(): array
    {
        return ['name'];
    }
}

// ---------------------------------------------------------------------------
// Users with varying permissions
// ---------------------------------------------------------------------------

final class SearchWildcardUser extends Authenticatable implements BuilderAccessible
{
    use HasBuilderAccess;

    protected $table = 'users';

    protected $guarded = [];

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

    /** @return list<string> */
    public function builderPermissions(): array
    {
        return ['*'];
    }
}

final class SearchPermittedUser extends Authenticatable implements BuilderAccessible
{
    use HasBuilderAccess;

    protected $table = 'users';

    protected $guarded = [];

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

    /** @return list<string> */
    public function builderPermissions(): array
    {
        return ['read:search-persons'];
    }
}

final class SearchDeniedUser extends Authenticatable implements BuilderAccessible
{
    use HasBuilderAccess;

    protected $table = 'users';

    protected $guarded = [];

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

    /** @return list<string> */
    public function builderPermissions(): array
    {
        return ['read:something-else'];
    }
}

// ---------------------------------------------------------------------------
// Translatable models & resources
// ---------------------------------------------------------------------------

final class SearchPageModel extends Model implements Translatable
{
    use HasTranslations;

    protected $table = 'search_pages';

    protected $guarded = [];

    /** @var list<string> */
    protected array $translatable = ['title'];
}

final class SearchPageModelTranslation extends Model
{
    protected $table = 'search_page_translations';

    protected $guarded = [];
}

final class SearchPageResource extends Resource implements CommandPaletteSearchable
{
    use HasCommandPaletteSearch;

    public static function getSlug(): string
    {
        return 'search-pages';
    }

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

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

    /** @return list<string> */
    public function searchable(): array
    {
        return ['title'];
    }

    /** @return list<Column> */
    public function columns(): array
    {
        return [
            Column::make('title')->searchable(),
        ];
    }
}

// ---------------------------------------------------------------------------
// Mixed translatable model (code = non-translatable, title = translatable)
// ---------------------------------------------------------------------------

final class SearchMixedModel extends Model implements Translatable
{
    use HasTranslations;

    protected $table = 'search_mixed';

    protected $guarded = [];

    /** @var list<string> */
    protected array $translatable = ['title'];
}

final class SearchMixedModelTranslation extends Model
{
    protected $table = 'search_mixed_translations';

    protected $guarded = [];
}

/** Resource that relies entirely on column-based search field resolution (searchable() returns []). */
final class SearchColumnsOnlyResource extends Resource implements CommandPaletteSearchable
{
    use HasCommandPaletteSearch;

    public static function getSlug(): string
    {
        return 'search-columns-only';
    }

    public static function getLabel(): string
    {
        return 'Columns Only';
    }

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

    /** @return list<Column> */
    public function columns(): array
    {
        return [
            Column::make('code')->searchable(),
            Column::make('title')->searchable(),
        ];
    }
}

/** Resource with only non-translatable search keys on a translatable model. */
final class SearchMixedNonTransResource extends Resource implements CommandPaletteSearchable
{
    use HasCommandPaletteSearch;

    public static function getSlug(): string
    {
        return 'search-mixed-non-trans';
    }

    public static function getLabel(): string
    {
        return 'Mixed Non Trans';
    }

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

    /** @return list<string> */
    public function searchable(): array
    {
        return ['code'];
    }

    /** @return list<Column> */
    public function columns(): array
    {
        return [Column::make('code')->searchable()];
    }
}

/** CommandPaletteSearchable resource with no searchable keys at all. */
final class SearchNoKeysResource extends Resource implements CommandPaletteSearchable
{
    use HasCommandPaletteSearch;

    public static function getSlug(): string
    {
        return 'search-no-keys';
    }

    public static function getLabel(): string
    {
        return 'No Keys';
    }

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

// ---------------------------------------------------------------------------
// Setup
// ---------------------------------------------------------------------------

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

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

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

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

    Schema::create('search_page_translations', function ($table): void {
        $table->id();
        $table->foreignId('search_page_id')->constrained('search_pages')->cascadeOnDelete();
        $table->string('locale', 10);
        $table->string('title')->nullable();
        $table->unique(['search_page_id', 'locale']);
        $table->timestamps();
    });

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

    Schema::create('search_mixed_translations', function ($table): void {
        $table->id();
        $table->foreignId('search_mixed_id')->constrained('search_mixed')->cascadeOnDelete();
        $table->string('locale', 10);
        $table->string('title')->nullable();
        $table->unique(['search_mixed_id', 'locale']);
        $table->timestamps();
    });

    config(['auth.providers.users.model' => SearchWildcardUser::class]);

    app(Registry::class)->resource(SearchPersonResource::class, SearchRoleResource::class, SearchPageResource::class);
});

// ---------------------------------------------------------------------------
// Tests
// ---------------------------------------------------------------------------

it('redirects unauthenticated requests', function (): void {
    $this->getJson(route('builder.search', [], false).'?q=alice')
        ->assertRedirect();
});

it('returns empty groups when query is too short', function (): void {
    $user = SearchWildcardUser::create(['name' => 'Admin', 'email' => 'a@a.com', 'password' => 'x']);

    $this->actingAs($user)
        ->getJson(route('builder.search', [], false).'?q=a')
        ->assertOk()
        ->assertExactJson(['groups' => []]);
});

it('returns empty groups when query is empty', function (): void {
    $user = SearchWildcardUser::create(['name' => 'Admin', 'email' => 'a@a.com', 'password' => 'x']);

    $this->actingAs($user)
        ->getJson(route('builder.search', [], false))
        ->assertOk()
        ->assertExactJson(['groups' => []]);
});

it('returns matching records for a wildcard user', function (): void {
    SearchPersonModel::create(['name' => 'Alice Doe', 'email' => 'alice@example.com']);
    SearchPersonModel::create(['name' => 'Bob Smith', 'email' => 'bob@example.com']);

    $user = SearchWildcardUser::create(['name' => 'Admin', 'email' => 'a@a.com', 'password' => 'x']);

    $response = $this->actingAs($user)
        ->getJson(route('builder.search', [], false).'?q=alice')
        ->assertOk();

    $groups = $response->json('groups');
    expect($groups)->toHaveCount(1);

    $group = $groups[0];
    expect($group['slug'])->toBe('search-persons');
    expect($group['items'])->toHaveCount(1);
    expect($group['items'][0]['title'])->toBe('Alice Doe');
    expect($group['items'][0]['subtitle'])->toBe('alice@example.com');
    expect($group['items'][0]['route'])->toContain('search-persons');
});

it('excludes resources that do not implement CommandPaletteSearchable', function (): void {
    SearchPersonModel::create(['name' => 'Alice', 'email' => 'alice@example.com']);
    SearchRoleModel::create(['name' => 'Alice Role']);

    $user = SearchWildcardUser::create(['name' => 'Admin', 'email' => 'a@a.com', 'password' => 'x']);

    $response = $this->actingAs($user)
        ->getJson(route('builder.search', [], false).'?q=alice')
        ->assertOk();

    $groups = $response->json('groups');
    $slugs = array_column($groups, 'slug');

    expect($slugs)->toContain('search-persons');
    expect($slugs)->not->toContain('search-roles');
});

it('excludes resources the user cannot read', function (): void {
    SearchPersonModel::create(['name' => 'Alice', 'email' => 'alice@example.com']);

    $user = SearchDeniedUser::create(['name' => 'Denied', 'email' => 'd@a.com', 'password' => 'x']);

    config(['auth.providers.users.model' => SearchDeniedUser::class]);

    $response = $this->actingAs($user)
        ->getJson(route('builder.search', [], false).'?q=alice')
        ->assertOk();

    expect($response->json('groups'))->toBeEmpty();
});

it('includes resource when user has the correct read permission', function (): void {
    SearchPersonModel::create(['name' => 'Alice', 'email' => 'alice@example.com']);

    $user = SearchPermittedUser::create(['name' => 'Permitted', 'email' => 'p@a.com', 'password' => 'x']);

    config(['auth.providers.users.model' => SearchPermittedUser::class]);

    $response = $this->actingAs($user)
        ->getJson(route('builder.search', [], false).'?q=alice')
        ->assertOk();

    $groups = $response->json('groups');
    expect($groups)->toHaveCount(1);
    expect($groups[0]['slug'])->toBe('search-persons');
});

it('returns empty groups when no records match the query', function (): void {
    SearchPersonModel::create(['name' => 'Alice', 'email' => 'alice@example.com']);

    $user = SearchWildcardUser::create(['name' => 'Admin', 'email' => 'a@a.com', 'password' => 'x']);

    $this->actingAs($user)
        ->getJson(route('builder.search', [], false).'?q=zzznomatch')
        ->assertOk()
        ->assertExactJson(['groups' => []]);
});

it('searches across multiple searchable fields', function (): void {
    SearchPersonModel::create(['name' => 'Charlie Brown', 'email' => 'uniquetoken@example.com']);

    $user = SearchWildcardUser::create(['name' => 'Admin', 'email' => 'a@a.com', 'password' => 'x']);

    $response = $this->actingAs($user)
        ->getJson(route('builder.search', [], false).'?q=uniquetoken')
        ->assertOk();

    expect($response->json('groups.0.items.0.title'))->toBe('Charlie Brown');
});

it('finds translatable records across any stored locale', function (): void {
    $page = SearchPageModel::create([]);
    $page->setTranslation('nl', ['title' => 'Homepagina']);
    $page->setTranslation('en', ['title' => 'Homepage']);

    $user = SearchWildcardUser::create(['name' => 'Admin', 'email' => 'a@a.com', 'password' => 'x']);

    // Searching the Dutch word finds the record and uses the Dutch locale
    $response = $this->actingAs($user)
        ->getJson(route('builder.search', [], false).'?q=Homepagina')
        ->assertOk();

    $pageGroup = collect($response->json('groups'))->firstWhere('slug', 'search-pages');
    expect($pageGroup)->not->toBeNull();

    $item = $pageGroup['items'][0];
    expect($item['title'])->toBe('Homepagina');
    expect($item['route'])->toContain('locale=nl');
});

it('uses the matched locale for the route and title', function (): void {
    $page = SearchPageModel::create([]);
    $page->setTranslation('nl', ['title' => 'Startpagina']);
    $page->setTranslation('en', ['title' => 'Home']);

    $user = SearchWildcardUser::create(['name' => 'Admin', 'email' => 'a@a.com', 'password' => 'x']);

    // Searching 'Home' matches the English translation
    $enResponse = $this->actingAs($user)
        ->getJson(route('builder.search', [], false).'?q=Home')
        ->assertOk();

    $enItem = collect($enResponse->json('groups'))->firstWhere('slug', 'search-pages')['items'][0];
    expect($enItem['title'])->toBe('Home');
    expect($enItem['route'])->toContain('locale=en');

    // Searching 'Start' matches the Dutch translation
    $nlResponse = $this->actingAs($user)
        ->getJson(route('builder.search', [], false).'?q=Start')
        ->assertOk();

    $nlItem = collect($nlResponse->json('groups'))->firstWhere('slug', 'search-pages')['items'][0];
    expect($nlItem['title'])->toBe('Startpagina');
    expect($nlItem['route'])->toContain('locale=nl');
});

it('returns the title from loaded translation when base attribute is null', function (): void {
    $page = SearchPageModel::create([]);
    $page->setTranslation('nl', ['title' => 'Welkom pagina']);

    $user = SearchWildcardUser::create(['name' => 'Admin', 'email' => 'a@a.com', 'password' => 'x']);

    $response = $this->actingAs($user)
        ->getJson(route('builder.search', [], false).'?q=Welkom')
        ->assertOk();

    $items = $response->json('groups.0.items');
    expect($items[0]['title'])->toBe('Welkom pagina');
    expect($items[0]['route'])->toContain('locale=nl');
});

it('skips CommandPaletteSearchable resource that has no searchable keys', function (): void {
    SearchPersonModel::create(['name' => 'Alice', 'email' => 'alice@example.com']);

    app(Registry::class)->resource(SearchNoKeysResource::class);

    $user = SearchWildcardUser::create(['name' => 'Admin', 'email' => 'a@a.com', 'password' => 'x']);

    $response = $this->actingAs($user)
        ->getJson(route('builder.search', [], false).'?q=alice')
        ->assertOk();

    $slugs = array_column($response->json('groups'), 'slug');
    expect($slugs)->not->toContain('search-no-keys');
});

it('returns record key as title when resource has no searchable fields', function (): void {
    $record = SearchPersonModel::create(['name' => 'NoFields', 'email' => 'nf@example.com']);
    $resource = new SearchNoKeysResource;

    expect($resource->searchResultTitle($record))->toBe((string) $record->getKey());
});

it('uses searchable columns as fallback when searchable() returns empty', function (): void {
    $record = SearchMixedModel::create(['code' => 'testcolcode']);
    $record->setTranslation('en', ['title' => 'Column Page']);

    app(Registry::class)->resource(SearchColumnsOnlyResource::class);

    $user = SearchWildcardUser::create(['name' => 'Admin', 'email' => 'a@a.com', 'password' => 'x']);

    $response = $this->actingAs($user)
        ->getJson(route('builder.search', [], false).'?q=testcolcode')
        ->assertOk();

    $group = collect($response->json('groups'))->firstWhere('slug', 'search-columns-only');
    expect($group)->not->toBeNull();
    expect($group['items'][0]['title'])->toBe('testcolcode');
    expect($group['items'][0]['subtitle'])->toBe('Column Page');
});

it('returns null subtitle when translatable field has no translation', function (): void {
    SearchMixedModel::create(['code' => 'codeonly']);

    app(Registry::class)->resource(SearchColumnsOnlyResource::class);

    $user = SearchWildcardUser::create(['name' => 'Admin', 'email' => 'a@a.com', 'password' => 'x']);

    $response = $this->actingAs($user)
        ->getJson(route('builder.search', [], false).'?q=codeonly')
        ->assertOk();

    $group = collect($response->json('groups'))->firstWhere('slug', 'search-columns-only');
    expect($group)->not->toBeNull();
    expect($group['items'][0]['subtitle'])->toBeNull();
});

it('skips locale tagging when all search keys are non-translatable', function (): void {
    $record = SearchMixedModel::create(['code' => 'nontracode']);
    $record->setTranslation('en', ['title' => 'Some Title']);

    app(Registry::class)->resource(SearchMixedNonTransResource::class);

    $user = SearchWildcardUser::create(['name' => 'Admin', 'email' => 'a@a.com', 'password' => 'x']);

    $response = $this->actingAs($user)
        ->getJson(route('builder.search', [], false).'?q=nontracode')
        ->assertOk();

    $group = collect($response->json('groups'))->firstWhere('slug', 'search-mixed-non-trans');
    expect($group)->not->toBeNull();
    expect($group['items'][0]['route'])->not->toContain('locale=');
});
