<?php

namespace Tests\Feature;

use App\Domain\Identity\Models\Branch;
use App\Domain\Identity\Services\TerminologyService;
use App\Domain\Shared\Context\BranchContext;
use App\Domain\Shared\Models\Academy;
use App\Livewire\Settings\TerminologySettings;
use App\Models\User;
use Illuminate\Support\Facades\DB;
use Livewire\Livewire;
use Tests\TestCase;

/**
 * A branch's own word for the people it serves, end to end.
 *
 *   DB_CONNECTION=pgsql DB_HOST=127.0.0.1 DB_DATABASE=oc_sport_test \
 *   DB_USERNAME=mahmoudaglan DB_PASSWORD= ./vendor/bin/phpunit --filter BranchTerminologyTest
 *
 * The point of the feature is that one academy can run a club and a compound
 * at once, so the assertions that matter are the ones about two branches
 * disagreeing — a single-branch test would pass just as happily against a
 * global setting, which is the thing this deliberately is not.
 *
 * Everything written here is rolled back; the restored tenant is left as found.
 */
class BranchTerminologyTest extends TestCase
{
    private User $owner;

    private Branch $branchA;

    private Branch $branchB;

    protected function setUp(): void
    {
        parent::setUp();

        if (config('database.default') !== 'pgsql') {
            $this->markTestSkipped('Needs a restored Postgres tenant; see the class comment.');
        }

        if (! $academy = Academy::first()) {
            $this->markTestSkipped('No academy in the restored tenant.');
        }

        app()->instance('current_academy', $academy);

        $owner = User::withoutGlobalScopes()
            ->whereHas('primaryRole', fn ($q) => $q->where('slug', 'academy_owner'))
            ->first();

        if (! $owner) {
            $this->markTestSkipped('No academy_owner in the restored tenant.');
        }

        $branches = Branch::withoutGlobalScopes()
            ->where('academy_id', $academy->id)
            ->where('is_active', true)
            ->orderBy('id')
            ->limit(2)
            ->get();

        if ($branches->count() < 2) {
            $this->markTestSkipped('Needs two active branches to prove they disagree.');
        }

        $this->owner = $owner;
        $this->branchA = $branches[0];
        $this->branchB = $branches[1];

        DB::beginTransaction();
    }

    protected function tearDown(): void
    {
        if (config('database.default') === 'pgsql') {
            DB::rollBack();
        }

        parent::tearDown();
    }

    private function service(): TerminologyService
    {
        return app(TerminologyService::class);
    }

    public function test_an_unconfigured_branch_speaks_the_default_preset(): void
    {
        $vocabulary = $this->service()->for($this->branchA->id);

        $this->assertSame('club', $vocabulary->preset);
        $this->assertSame('عضو', $vocabulary->member);
        $this->assertSame('نوع العضوية', $vocabulary->membership_type);
    }

    public function test_a_saved_preset_relabels_that_branch_and_leaves_the_other_alone(): void
    {
        $service = $this->service();

        $service->save($this->branchA->id, 'compound', []);

        $this->assertSame('مقيم', $service->for($this->branchA->id)->member);
        $this->assertSame('رقم الإقامة', $service->for($this->branchA->id)->membership_id);

        // The whole reason this is a branch setting and not a system one.
        $this->assertSame('عضو', $service->for($this->branchB->id)->member);
    }

    public function test_one_edited_word_overrides_the_preset_and_the_rest_still_follows_it(): void
    {
        $service = $this->service();

        $service->save($this->branchA->id, 'compound', [
            'non_member' => 'ضيف',
            'non_member_definite' => 'الضيف',
        ]);

        $vocabulary = $service->for($this->branchA->id);

        $this->assertSame('ضيف', $vocabulary->non_member);
        $this->assertSame('سعر الضيف', $vocabulary->non_member_price);
        $this->assertSame('مقيم', $vocabulary->member, 'the untouched word should still follow the preset');
    }

    public function test_clearing_a_word_falls_back_to_the_preset_rather_than_rendering_nothing(): void
    {
        $service = $this->service();

        $service->save($this->branchA->id, 'compound', ['member' => 'قاطن']);
        $this->assertSame('قاطن', $service->for($this->branchA->id)->member);

        $service->save($this->branchA->id, 'compound', ['member' => '   ']);
        $this->assertSame('مقيم', $service->for($this->branchA->id)->member);

        $this->assertDatabaseMissing('branch_settings', [
            'branch_id' => $this->branchA->id,
            'key' => 'terminology.member',
        ]);
    }

    public function test_the_term_helper_follows_the_branch_the_user_is_in(): void
    {
        $this->service()->save($this->branchA->id, 'beach', []);
        $this->service()->save($this->branchB->id, 'hotel', []);

        app(BranchContext::class)->set((int) $this->branchA->id);
        $this->assertSame('مشترك', term('member'));

        app(BranchContext::class)->set((int) $this->branchB->id);
        $this->assertSame('نزيل', term('member'));

        // An explicit branch id wins over the session, which is what labels a
        // cross-branch report row correctly.
        $this->assertSame('مشترك', term('member', (int) $this->branchA->id));
    }

    /**
     * The whole point of the request: not one screen, every screen.
     *
     * Each route is paired with a label that only exists once the compound
     * vocabulary is loaded, so a screen that quietly kept its hardcoded عضو
     * fails here rather than being noticed by a client.
     */
    public static function relabelledScreens(): array
    {
        return [
            'participant list'     => ['participants.list', 'الإقامة'],
            'participant form'     => ['participants.create', 'نوع الإقامة'],
            'participant import'   => ['participants.import', 'رقم الإقامة'],
            'registration wizard'  => ['receptionist.new-registration', 'مقيم'],
            'programme form'       => ['programs.create', 'سعر المقيم'],
            'product form'         => ['inventory.products.create', 'المقيمين'],
            'POS terminal'         => ['pos.terminal', 'المقيمين'],
            'bulk messaging'       => ['messaging.bulk', 'حسب الإقامة'],
            'reports hub'          => ['reports.hub', 'انتهاء الإقامة'],
            'service requests'     => ['service-requests.index', 'المقيمين'],
            'payment proofs'       => ['payment-proofs.index', 'المقيم'],
        ];
    }

    #[\PHPUnit\Framework\Attributes\DataProvider('relabelledScreens')]
    public function test_every_screen_prints_the_branch_word(string $route, string $expected): void
    {
        $this->service()->save($this->branchA->id, 'compound', []);
        app(BranchContext::class)->set((int) $this->branchA->id);

        $html = $this->actingAs($this->owner)->get(route($route))->assertOk()->getContent();

        $this->assertStringContainsString($expected, $html, "[{$route}] did not print [{$expected}]");
    }

    public function test_the_club_wording_is_gone_from_a_compound_branch(): void
    {
        $this->service()->save($this->branchA->id, 'compound', []);
        app(BranchContext::class)->set((int) $this->branchA->id);

        foreach (['participants.list', 'participants.create', 'programs.create'] as $route) {
            $html = $this->actingAs($this->owner)->get(route($route))->assertOk()->getContent();

            foreach (['كل أنواع العضوية', 'سعر العضو', 'رقم عضوية النادي'] as $clubWording) {
                $this->assertStringNotContainsString($clubWording, $html, "[{$route}] still says [{$clubWording}]");
            }
        }
    }

    public function test_the_import_screen_still_documents_the_untranslated_column_names(): void
    {
        $this->service()->save($this->branchA->id, 'compound', []);
        app(BranchContext::class)->set((int) $this->branchA->id);

        $html = $this->actingAs($this->owner)->get(route('participants.import'))->assertOk()->getContent();

        // Renaming the label must never rename the CSV contract — a file
        // exported last year has to keep importing.
        $this->assertStringContainsString('membership_type', $html);
        $this->assertStringContainsString('membership_id', $html);
    }

    public function test_the_settings_screen_renders_with_every_preset_offered(): void
    {
        app(BranchContext::class)->set((int) $this->branchA->id);

        $html = $this->actingAs($this->owner)->get(route('settings.terminology'))->assertOk()->getContent();

        foreach (config('terminology.presets') as $definition) {
            $this->assertStringContainsString($definition['label'], $html);
        }

        // The sidebar entry names itself after the branch it is in.
        $this->service()->save($this->branchA->id, 'compound', []);
        $html = $this->actingAs($this->owner)->get(route('settings.terminology'))->assertOk()->getContent();
        $this->assertStringContainsString('مسميات المقيمين', $html);
    }

    public function test_the_settings_screen_saves_and_copies_across_the_academy(): void
    {
        app(BranchContext::class)->set((int) $this->branchA->id);

        Livewire::actingAs($this->owner)
            ->test(TerminologySettings::class)
            ->set('selectedBranchId', $this->branchA->id)
            ->call('applyPreset', 'hotel')
            ->assertSet('terms.member', 'نزيل')
            ->call('save')
            ->assertHasNoErrors();

        $this->assertSame('نزيل', $this->service()->for($this->branchA->id)->member);
        $this->assertSame('عضو', $this->service()->for($this->branchB->id)->member);

        Livewire::actingAs($this->owner)
            ->test(TerminologySettings::class)
            ->set('selectedBranchId', $this->branchA->id)
            ->call('applyToAllBranches')
            ->assertHasNoErrors();

        $this->service()->flush();
        $this->assertSame('نزيل', $this->service()->for($this->branchB->id)->member);
    }

    public function test_a_blank_required_word_is_refused(): void
    {
        app(BranchContext::class)->set((int) $this->branchA->id);

        Livewire::actingAs($this->owner)
            ->test(TerminologySettings::class)
            ->set('selectedBranchId', $this->branchA->id)
            ->set('terms.member', '')
            ->call('save')
            ->assertHasErrors(['terms.member' => 'required']);
    }

    public function test_the_settings_screen_is_closed_to_an_account_without_the_permission(): void
    {
        $resolver = app(\App\Domain\Identity\Services\GuardianResolver::class);

        $member = User::withoutGlobalScopes()->whereNotNull('person_id')->limit(200)->get()
            ->first(fn ($u) => ! $u->can('settings.manage'));

        if (! $member) {
            $this->markTestSkipped('No account without settings.manage on this tenant.');
        }

        $this->actingAs($member)->get(route('settings.terminology'))->assertForbidden();
    }
}
